Olá! Meu nome é Anthony. Sou um homem de pele morena, cabelo preto, olhos castanhos, uso uma gorra, óculos e uma camiseta preta. Estou aqui para dar as boas-vindas ao curso de Gestão de Dependências.
Audiodescrição: Anthony é um homem de pele morena, cabelo preto, olhos castanhos, usa uma gorra, óculos e uma camiseta preta.
Antes de falarmos sobre o curso, gostaria de me apresentar um pouco mais. Atualmente, trabalho como Tech Lead de Front-end no Magalu, dentro do núcleo de desenvolvimento chamado Luisa Labs, mais especificamente na linha de Magalu Entregas. Além disso, também concilio minha vida com a carreira acadêmica. Estou cursando um mestrado na Universidade Federal do Rio Grande do Norte, onde faço parte do programa de pós-graduação em Sistemas e Computação, estudando microfrontends.
Voltando à nossa formação, após essas boas-vindas, quero compartilhar um pouco sobre o que você encontrará aqui e qual é o objetivo desta formação. Como pode ver, toda a minha trajetória está ligada tanto à indústria quanto à academia, o que me fez enxergar cada vez mais o que fazia dentro da indústria com mais rigor, foco na pesquisa, criticidade e muita curiosidade. É aí que entra esta formação.
Dentro da gestão de dependências, vamos explorar algo que fica um pouco oculto, nos bastidores, em nosso dia a dia. É necessário muita atenção e um nível de senioridade maior para lidar com essas mudanças. E o que é essa gestão de dependências? Vamos detalhar mais nas aulas, mas aqui você aprenderá como gerenciar cada uma das bibliotecas que instala, usando NPM, YARN ou PNPM.
Durante as formações e ao longo de nossa vida, acabamos conhecendo as bibliotecas apenas por um comando para adicioná-las ou instalá-las.
Agora vamos entender como funcionam todos esses números que definem uma versão e para que serve cada um deles. Vamos definir, por exemplo, qual é a opção mais segura ou menos segura entre todas as opções que temos. O que acontece quando uma dessas bibliotecas muda? O que ocorre quando chega uma atualização para nós? Devemos nos preocupar? Não devemos nos preocupar? E dentro dessas mudanças, há falhas de segurança? Haverá problemas de código morto, de código estagnado, de código que não utilizamos, código realmente inutilizado? Tudo isso será abordado nesta formação, que é equilibrada.
Como assim? Teremos tanto uma parte prática quanto um amplo conteúdo teórico. E por que isso? Vamos entender que, após a teoria, a prática se torna muito mais natural, e por isso adquire uma importância muito maior. Mas claro, aqui fazemos ao estilo Alura, então mesmo a parte teórica inclui prática. Em todo momento, estaremos trabalhando com algum exemplo, com algum cenário, e todo esse cenário será completamente detalhado para que entendamos melhor o problema.
Alinhando expectativas, este é um conteúdo, uma formação muito importante para o nosso futuro. Como pessoa desenvolvedora sênior, isso é um requisito mais que básico. Como avaliador, exigirei isso dos candidatos que participarem do processo seletivo comigo: esse tipo de conhecimento, esse tipo de cuidado e esse tipo de esmero.
Dando um pequeno spoiler, trabalharemos um pouco com dependências dentro do nosso CI, dentro das entregas automáticas de integração contínua, para automatizar um pouco o processo e conhecer ainda mais esses conceitos. Se deixamos você curioso, contenha essa curiosidade, pois esta formação tratará sobre tudo isso e mais. Nos vemos na Aula 1.
Chegou o momento de discutirmos algo presente em nosso dia a dia como pessoas desenvolvedoras, mas com o qual não temos uma afinidade mais profunda: as dependências de nosso projeto. Vamos aprender a gerenciá-las, fazê-las evoluir e escalar para acompanhar nosso projeto. Antes de tudo, precisamos fazer uma pergunta básica: o que é a gestão de dependências?
A gestão de dependências é um conceito ligado às dependências que nosso projeto possui. E o que são essas dependências, para que todos estejamos na mesma página? As dependências de nosso projeto são os pacotes que instalamos, mas não apenas eles. Os pacotes que, por sua vez, têm outros pacotes instanciados também fazem parte, ou seja, o pacote do pacote também se torna uma dependência dentro de nosso projeto. Isso é o que chamamos de dependência transitiva. Assim, temos todo esse conjunto de ferramentas disponível em nosso projeto.
Hoje em dia, todo software moderno é construído sobre outros softwares. Para construir uma biblioteca, geralmente usamos outras bibliotecas, exceto em casos muito básicos, onde usamos apenas a própria linguagem de programação. Provavelmente, essa biblioteca será usada por outra, que será usada por outra, criando um nicho. Uma dependência é qualquer pacote, biblioteca ou módulo externo que nosso projeto utiliza. A gestão de dependências é um conjunto de práticas para escolher, instalar, atualizar, auditar e eliminar essas peças externas. Sem gestão, enfrentamos problemas como builds que falham, vulnerabilidades que passam despercebidas e equipes que se atrapalham.
Por que é importante aprendermos isso? Em um projeto React, por exemplo, em uma aplicação típica com Vite e SPA, escrevemos em média 20% do código final. Os outros 80% vêm das dependências: o próprio React, o bundler que usamos, as bibliotecas que instalamos, os polyfills disponíveis. Tudo isso se agrega para gerar nosso código fonte. Cada dependência também traz suas próprias dependências, as transitivas, criando um efeito cascata.
Por exemplo, se usarmos nosso projeto Stackboard e instalarmos utilizando pnpm, teremos em média cerca de 900 pacotes disponíveis a partir de 20, talvez os que adicionamos em dependências ou devDependencies, enfim, os que estão disponíveis em nosso package.json. O que pode dar errado? Quando não fazemos essa gestão, não nos preocupamos com as versões disponíveis e com o que realmente estamos usando, a vulnerabilidade em dependências é evidente. Quando a vulnerabilidade está no pacote principal, estamos mais atentos, recebemos notícias em blogs, comunicados e temos ferramentas para isso. Mas quando a vulnerabilidade está em uma dependência transitiva, ou seja, na dependência de uma dependência, a situação muda, pois se torna oculta. Isso cria um verdadeiro árvore de dependências que pode prejudicar nosso processo. Uma atualização automática inesperada pode quebrar nosso build. Veremos por que e como isso acontece.
Nosso bundle pode se tornar inchado, demorando mais para carregar, devido ao excesso de pacotes que não usamos. Muitas vezes, instalamos um pacote por algum motivo, e ele permanece no build do projeto até certo momento. Sabemos que existem ferramentas e estratégias de tree shaking, como o Vite, mas isso não garante 100% que todo o código seja eliminado. Podemos ter inconsistências de versões entre ambientes, entre o ambiente local e produção, por exemplo, e perder tempo investigando bugs de dependências que talvez nem usemos, o que é literalmente jogar dinheiro fora.
Para falar de dependências de bibliotecas, precisamos falar de NPM. O NPM é o maior registro de pacotes que temos, com aproximadamente 12,5 milhões de pacotes registrados. Um projeto médio tem entre 40 e 80 dependências diretas, não transitivas, o que gera entre 800 e 1500 dependências transitivas. Isso representa muito código, muitos pacotes e muitas possibilidades de código malicioso em um só lugar. Em média, o registro NPM tem pelo menos 30 bilhões de downloads semanais, ou seja, 30 bilhões de requisições a pacotes semanalmente, um volume gigantesco.
Quando uma nova vulnerabilidade é publicada, há um maior rastreamento dentro do próprio NPM, algo que ocorre com frequência. Dentro das estatísticas do NPM, uma nova vulnerabilidade é publicada a cada poucas horas. Temos uma média de dependências diretas de uma aplicação React em produção que gira em torno de 55 pacotes. Como o NPM é o maior registro de pacotes do mundo, temos cada vez mais possibilidades. Com grande poder vem uma grande responsabilidade de gestão. Quanto maior nosso leque de possibilidades, mais perigoso é tomar uma decisão. Por isso, toda decisão, desde o início da construção técnica até este momento, deve ser bem pensada e cuidada para que tudo funcione bem.
Precisamos entender um conceito mais a fundo: o semver. O que é semver? Se dividirmos e mudarmos as palavras, podemos dizer versionamento semântico. Semver é uma convenção para nomear as versões de um pacote, utilizada praticamente de forma global. No entanto, é uma convenção em que todos confiam, mas não devemos confiar cegamente. Não porque a forma como é tratada não faça sentido, mas porque devemos desconfiar se essa correção trará uma vulnerabilidade ao nosso sistema ou se quebrará nosso sistema. Como pessoas desenvolvedoras, podemos acabar mudando um patch por um minor. Isso pode acontecer, podemos mudar um código que não deveríamos e que passe despercebido, pois somos humanos, é natural que ocorra. Por isso, precisamos estar cada vez mais atentos.
Para quem não está familiarizado, ou só ouviu falar de commits semânticos, também existe o versionamento semântico. Como funciona? Toda versão se divide em três partes: a maior é a major, a do meio é a minor e a última é o patch. Por exemplo, 1.0.0: o 1 é a major, o 0 do meio é a minor e o último 0 é o patch. O que significam respectivamente? A major implica breaking change, ou seja, uma mudança que quebra o código. A minor são novas funcionalidades retrocompatíveis e o patch são apenas correções de bugs.
É importante esclarecer que semver é uma convenção, não uma garantia. Não porque um projeto a use, e hoje praticamente todos os projetos a usam, significa que está garantido que funcione. Pacotes bem-intencionados podem quebrar em minor e patch. Existem pacotes grandes, como o Material UI, que tiveram rupturas de valores dentro de uma pequena mudança que provoca uma diferença, mesmo que visual, em nosso sistema, o que pode causar desconforto ao usuário e afetar sua experiência.
Outro ponto curioso: algumas ferramentas ou bibliotecas não precisam de uma grande mudança de compatibilidade para aumentar sua major. Há tecnologias que aumentam sua major periodicamente. Um exemplo clássico fora do ecossistema React é o Angular, que faz esse processo, se não me engano, a cada seis meses. Agrupa tudo o que foi feito, mesmo que não haja breaking changes, e sobe a versão major para manter todos alinhados.
Qual é a anatomia completa de uma versão? Temos as três principais: major, minor e patch, além de outras duas partes que servem para pre-release e para metadata do build.
Essa é a estrutura que podemos encontrar e aplicar em nossos projetos. Vamos agora entender o que é o range (intervalo). O range é exatamente o símbolo que vai à frente da versão do nosso pacote no package.json. O acento circunflexo (^) permite mudanças aceitando minor (menor) e patch (correção).
Como assim aceita mudanças de minor e patch? Mesmo que não tenhamos uma versão fixa e estejamos usando a versão 2.3.0, o acento circunflexo (^) permite que, ao instalar o pacote, ele seja resolvido por outras versões disponíveis dessa biblioteca, alternando entre versões de patch e minor, que em teoria não trarão breaking changes (mudanças que quebram a compatibilidade). Por isso usamos semver como convenção, mas não é uma garantia. Quando usamos a til (~), por exemplo, aceitamos mudanças apenas em patch. São essas mudanças de correção de bugs e similares. Se colocarmos uma versão exata, não haverá nenhuma atualização. O valor em nosso package.json será o mesmo que aparecerá em nosso lockfile, e o asterisco é para qualquer versão. Lembremos de nunca colocar isso em produção, pois pode quebrar sistemas de uma hora para outra com muita facilidade.
package.jsonPara ilustrar, vejamos alguns exemplos de como podemos definir essas versões no package.json:
"dependencies": {
"example-package": "^2.3.0", // Aceita mudanças de minor e patch
"another-package": "~2.3.0", // Aceita apenas mudanças de patch
"fixed-package": "2.3.0", // Versão exata
"any-package": "*" // Qualquer versão, não recomendado para produção
}
Existem também variações e combinações desses intervalos. Podemos ter valores maior e menor, onde deixamos um intervalo explícito de quais versões podemos resolver. Também podemos usar o "x" como um valor que pode ser iterado dentro do nosso package.json, equivalente à til (~). Se deixarmos apenas o minor e retirarmos o patch, é equivalente ao acento circunflexo (^). Temos também ranges compostos com o mul. Ou seja, versões maiores ou iguais a 2.30 ou versões menores que 1.0. Dentro disso, há um caso especial importante ao criar ou consumir bibliotecas. Quando usamos versões abaixo de 1.0, a regra muda um pouco. Mesmo com o acento circunflexo na minor, apenas o patch mudará, pois seguirá essa cadência de recorrência. Se usarmos o mesmo acento circunflexo, mas registrarmos apenas a versão de patch, será a versão exata, pois não há mais o que modificar. Mesmo que haja metatags de lançamento ou outros elementos, eles não são considerados. Apenas considera major, minor e patch. A partir da versão 1.0, começamos a ter a regra já comentada.
Podemos enfrentar um grande problema chamado Semver Drift. É aquele deslizamento dentro do versionamento semântico. Por exemplo, o package.json declarará um intervalo para a versão 2.3.0. O lockfile tem uma estrutura, e o npm e os gerenciadores de pacotes usam uma estrutura para resolver uma versão exata, que pode ser a 2.3.1. Mas se não tivermos esse lockfile, cada instalação com pnpm, por exemplo, pode resolver uma versão diferente. É aí que entra o drift. O drift será essa divergência silenciosa entre ambientes. A pessoa desenvolvedora A estará na versão 2.3.1, a pessoa desenvolvedora B estará na 2.4.0, e em produção teremos a 2.3.3. São três versões distintas que podem ter mudanças de contrato entre elas, e isso é muito perigoso.
Para entender melhor, vejamos como isso se reflete no código:
// package.json declara range
"dependencies": {
"example-package": "^2.3.0"
}
// lockfile resolve para versão exata
"example-package": "2.3.1"
// Sem lockfile: cada pnpm install pode resolver versão diferente
// Dev A tem 2.3.1, Dev B tem 2.4.0, CI tem 2.3.3
Como o npm decide qual versão instalar? Ao instalar, verifica o lockfile. Se o lockfile tem a mesma versão que está no package.json, ele a mantém. Se não a tem, busca a versão mais próxima para resolver. A partir disso, define-se a versão final. Por exemplo, se temos a versão 2.3 e no nosso lockfile não há informação, ele buscará a próxima versão disponível dentro do nosso intervalo e trará, por exemplo, a 2.3.4. Essas versões já serão distintas e ficarão refletidas, por exemplo, quando criarmos nosso lockfile, seja usando npm, pnpm ou yarn. É muito possível que nem estejamos usando exatamente a versão declarada no package.json. Por isso, é muito importante, sempre que atualizarmos uma biblioteca ou usarmos a mesma biblioteca, revisar bem os changelogs. Acompanhe a evolução dessa biblioteca.
Aqui temos alguns exemplos reais dessas rupturas em minor/patch. Separamos alguns pacotes onde ocorreram essas rupturas para que possamos estar atentos e verificá-los. Também podemos revisá-los nos changelogs. Por exemplo, o Material UI (MUI) fez uma mudança em um minor/patch que quebrou funcionalidades. A variante do TextField mudou de standard para outlined como variante padrão. Isso mudou completamente a interface dos locais onde esse componente era usado. O ESLint, por exemplo, quando subiu o minor/patch, ativou uma nova regra por padrão que quebrou em produção. Foi um choque quando apareceu a versão 8, pois mudou muito. O styled-components também tem um patch dentro da versão 5 que altera o SSR. Além disso, temos o webpack, TypeScript. Tudo isso é para mostrar que bibliotecas grandes podem sofrer esse problema. Ninguém está imune. Por isso, devemos estar muito atentos.
Vamos a um caso real, que é precisamente o do Material UI, onde poderia quebrar. Por que quebraria? Porque mudou o intervalo, o TextField mudou a variante padrão, e os formulários do nosso sistema, por exemplo, começaram a mostrar um input com aparência inesperada. Resultado? O layout se quebra, a interface se quebra, embora a funcionalidade se mantenha, os snapshots falham, e isso faz com que os testes falhem, alguns passos, por exemplo, de validação de regressão visual se rompem. Ao final, obtemos um único conselho, um grande conselho. A confiança cega em semver não funciona. E essa foi a causa do problema.
Aqui vemos quando usar cada intervalo. O intervalo de minor/patch para utilitários de baixo risco, o intervalo de apenas patch para dependências principais, a versão exata apenas para hotfixes forçados, para a reprodutibilidade de algum caso, pois não terá um security patch automático. O asterisco, que permite tudo, nosso grande allow, tem um risco máximo, então permite tudo, e podemos usá-lo a qualquer momento, menos em produção. Teste, faça as provas que quiser, explore, mas nunca coloque esse código em produção. E um intervalo explícito, podemos usá-lo quando os outros casos não se encaixam, e tem um risco controlado, dependendo das versões que permitimos.
Temos uma ferramenta essencial para isso, que é o próprio semver. Existem comandos que podemos usar para fazer essa validação. Em outros ecossistemas também está disponível. Muitas pessoas o usam no NPM junto com Node, está em Rust, em Go modules, em Python, mas o NPM é o ecossistema que mais sofre esse drift pelo volume de dependências que maneja.
Vamos a uma demonstração, para ver qual versão estamos usando hoje em nosso projeto com Material, qual versão resolve, ver se realmente é a mesma versão, e verificar as versões disponíveis para nosso código. Executamos o comando pnpm, colocamos o nome do pacote, que é Material. O pacote que está resolvendo, a versão é a 6.5.0. Voltamos ao nosso projeto, abrimos o package.json, e se buscarmos MuiMaterial, a versão que tem é a 6.4.0, mas está resolvendo para outra versão internamente. Com isso, podemos listar, por exemplo, qual versão estamos usando realmente, e de fato resolve para a 6.5.0, está usando hoje a 6.5.0, e não a 6.4.0. Além disso, podemos usar pnpm view e ver as versões disponíveis, e logicamente trará a última versão, que é a 7.3.8, que já está disponível de Material. Estamos, na verdade, um pouco desatualizados em relação à biblioteca, mas resolveremos ao longo do curso.
Para verificar se uma versão satisfaz um determinado intervalo, podemos usar o seguinte comando:
# Verificar se versão satisfaz range
npx semver 7.0.0 --range "~6.4.0"
# → (vazio = não satisfaz)
npx semver 6.4.3 --range "~6.4.0"
# → 6.4.3 (satisfaz)
E para listar as versões compatíveis de um pacote, podemos usar:
# Listar versões compatíveis de um pacote
pnpm view @mui/material versions --json | \
pnpx semver --range "~6.4.0"
Resumindo visualmente, cada nova dependência, devemos verificar se se mantém ativamente. Se é uma DepCore, a partir disso decidimos qual intervalo usar, por exemplo. Usem tanto as tabelas que já mostramos quanto esse fluxo, e façam testes também, que é super importante.
Para levar para casa, um resumo de tudo. Semver é uma convenção, não um contrato, então, por favor, sempre leiam os changelogs, com especial cuidado com as versões abaixo de 1.0, até as pré-1.0, porque o comportamento ali é distinto. Em geral, usaremos a til (~) para DepCore, e o acento circunflexo (^) para utilitários, e podemos validar também esses intervalos visualmente no site do semver, que está disponível. Combinar intervalos com automação, por exemplo, de atualizações, será discutido em outro momento. Restringir demais impede também de receber patches de segurança. Não adianta restringir uma versão sem considerar que as vulnerabilidades podem ser injetadas ou descobertas no futuro. É importante estar atento a isso também.
Os exemplos reais mostram que qualquer pacote pode quebrar em minor. E esse é o maior conselho, a conclusão principal desta aula. Com tudo isso, na próxima aula, entenderemos mais sobre o mundo das dependências.
Na aula passada, discutimos sobre as dependências e agora vamos abordar um arquivo que acompanha essas dependências, o lockfile. O lockfile será nossa linha de defesa contra atualizações que possam causar problemas ou qualquer coisa fora do acordado dentro do nosso contrato.
O que é o lockfile e o que ele armazena? O lockfile é um arquivo de definições do nosso pacote. Ele guarda uma versão resolvida, um hash de integridade desse pacote e uma estrutura de hospedagem, ou seja, de quem depende de quem para esse pacote. Em seu conteúdo, encontramos a URL do registro, indicando de onde foi baixado, seja de um repositório, de um registro aberto como o npm, ou de um registro privado como o Nexus, utilizado de forma privada dentro de empresas. A URL resolvida é garantida naquele momento exato. Além disso, assegura que o npm install, ou equivalente, produza exatamente os mesmos node_modules. Assim, o lockfile já nos oferece essa possibilidade.
Para ilustrar, vejamos um exemplo simplificado de um lockfile:
// Exemplo simplificado
{
"name": "devboard",
"lockfileVersion": 3,
"packages": {
"node_modules/@mui/material": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.0.tgz",
"integrity": "sha512-aBc12...",
"peerDependencies": {
"@emotion/react": "^11.5.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
}
}
}
Dentro do package-lock.json, o package-lock é criado no momento em que usamos o npm. Se estivermos utilizando pnpm ou yarn, o arquivo muda. Pode haver alteração no formato, mas não na informação, e vamos demonstrar isso. Um exemplo simplificado: dentro do package-lock teríamos o nome do nosso projeto, a versão desse lockfile e a lista de pacotes. Aqui, temos o listado do material, a versão, a URL da versão resolvida, o hash de integridade e todas as peer dependencies, ou seja, as dependências necessárias para usar esse pacote. Essas peer dependencies são comuns em pacotes que serão distribuídos. Em aplicações, SPIs e afins, não costumamos ver ou precisar desse tipo de estratégia.
Como mencionado, cada package manager terá seu lockfile. O npm possui o package-lock, o pnpm tem o pnpm-lock, e o yarn possui o yarn.lock, tanto na versão 1 quanto no Yarn Berry, que já está na versão 8 em diante. Para nossos exemplos, utilizaremos bastante o pnpm. Por que usamos o pnpm? Explicaremos em breve.
Antes disso, há uma funcionalidade que o package.json nos oferece, que é o corepack. O que ele faz? O corepack garante que estamos usando o package manager correto, assegurando que desenvolvedores diferentes não utilizem package managers distintos. Configuramos isso dentro do nosso package.json, indicando o package manager:
// package.json
{
"packageManager": "npm@10.2.0"
}
E ativamos o corepack a partir do terminal, pela linha de comando:
# Ativar Corepack (vem com Node 16+)
corepack enable
Essa funcionalidade já está disponível a partir do Node 16.
Qual é a diferença entre usar ou não um lockfile? Sem o lockfile, a cada dia uma versão pode ser resolvida de forma diferente. É o caso que vimos na aula passada, onde especificamos a versão 6.4 e ela foi resolvida para 6.5. O mesmo commit, por exemplo, pode produzir builds diferentes, pois a cada npm install, pnpm install ou yarn install, uma nova resolução dessas dependências pode ser gerada. Isso é crítico em nosso desenvolvimento.
É aí que entra a vantagem do pnpm. Na gestão de pacotes, temos algo chamado hospedagem. Os node_modules foram estruturados assim desde o início. Gestores de pacotes mais antigos, como npm e yarn, fazem hospedagem. O que é isso? As dependências transitivas ficam acessíveis dentro do código, o que chamamos de phantom dependencies ou dependências fantasma. No entanto, o pnpm possui um isolamento estrito. As dependências transitivas não são acessíveis diretamente, ou seja, essas dependências fantasma estão ligadas a importar algo que não está no seu package.json. Devido à estrutura do pnpm e a todos os links simbólicos pelos quais ele é famoso, a pasta node_modules do pnpm é muito menor e não permite esse acesso direto.
Para resolver isso, temos uma grande aplicabilidade aqui. Existe um comando no npm chamado npm ci, que é equivalente ao install. Na tabela, isso é explicado com mais detalhes, mas o que precisamos saber é que esse CI é, na verdade, um alias, uma abreviatura de --frozen-lockfile. O que ele faz? O --frozen-lockfile realmente bloqueia a versão que estamos pedindo e a versão que será utilizada em nosso projeto. Assim, não temos divergência de versões quando usamos esse comando. Recomenda-se usá-lo precisamente no comando do CI. É mais lento, mas garante uma compatibilidade total dessas ferramentas e bibliotecas dentro do nosso ambiente produtivo.
Um caso real que podemos ter é um desenvolvedor executar pnpm install sem o --frozen-lockfile. Uma dependência transitiva, por exemplo, dentro do turnstick carry do react-query, pode subir de versão silenciosamente. Esse build passa em desenvolvimento porque estamos testando, mas em produção o cache se comporta de forma diferente. Em um novo patch, essa mudança pode alterar o tempo padrão de stale.
Agora temos um comportamento distinto, causando um bug em produção que pode levar tempo para identificar. Este é um cenário muito comum, que todos provavelmente já enfrentaram, que é o conflito dentro do lockfile. Basicamente, ao executar pnpm install, por exemplo, e depois de um tempo executá-lo novamente, o lockfile pode mudar devido a algumas novas resoluções. Podemos gerenciar isso manualmente, mas existem ferramentas mais seguras para essa tarefa. O npm possui um merge driver, mas o pnpm tem seu próprio merge driver, que é um pacote muito bom. Deixaremos aqui o link com a documentação para que possam consultá-la, implementá-la e utilizá-la de maneira mais ampla, entendendo como funciona. Com isso, podemos automatizar algumas tarefas.
Para lidar com conflitos de merge no lockfile, podemos seguir os seguintes passos:
# Passo 1: aceitar qualquer versão do lockfile
git checkout --theirs pnpm-lock.yaml
# Passo 2: regenerar a partir do package.json
pnpm install
# Passo 3: commitar o resultado
git add pnpm-lock.yaml
git commit -m "chore: regenerate lockfile after merge"
Além disso, podemos instalar um driver de merge para facilitar esse processo:
# Instalar driver de merge para lockfiles
pnpm add -g @pnpm/merge-driver
Neste ponto do curso, já vimos várias estratégias de automação e bibliotecas, e já adquirimos muita maturidade. Para proteger o lockfile, recomendamos fortemente criar um hook, como o post-merge, dentro de ferramentas como husky, lefthook ou qualquer outra para gerenciar esses hooks do git. Assim, podemos usar o comando pnpm install --frozen-lockfile, que, após um merge ou uma alteração, garantirá que sempre tenhamos as versões corretas dentro de node_modules.
Para configurar o husky e criar um hook pós-merge, siga os passos abaixo:
# Instalar husky
pnpm install -D husky
pnpm husky init
# Criar hook post-merge
echo 'pnpm install --frozen-lockfile' > .husky/post-merge
chmod +x .husky/post-merge
Agora, apresentamos cinco regras de ouro para nosso lockfile. Sempre inclua o lockfile no repositório; é um arquivo essencial. Use pnpm install com --frozen-lockfile em CI, Docker e Deploy, sem exceções. Nunca coloque esse arquivo no gitignore, pois isso gerará muitos conflitos, o que não é bom. Revise também as diferenças do lockfile em grandes PRs e preste atenção especial às novas dependências. Adicionar esse post-merge hook para um auto-install é muito interessante.
Vamos simular um drift dentro do nosso código. Vamos voltar ao código e tomar a anotação que temos aqui. A primeira coisa a fazer é executar ls para listar os índices do projeto, e podemos ver no resultado do terminal nosso pnpm lock.yml. Vamos limpar e remover o pnpm lock.yml. Após executar ls novamente, o arquivo não estará mais disponível. Em seguida, executamos pnpm install. Instalamos tudo, e o sistema indicou que tudo estava sendo utilizado corretamente. Vamos copiar este comando para verificar se há mudanças no nosso lockfile, e não há nenhuma.
Para simular um drift, podemos seguir os passos abaixo:
# Simular drift
rm pnpm-lock.yaml
pnpm install
git diff pnpm-lock.yaml | wc -l # centenas de linhas mudaram!
git checkout pnpm-lock.yaml # restaurar
Vamos forçar uma mudança no lockfile para ver a mágica acontecer. Vamos fixar a versão, eliminar novamente o pnpm lock, executar pnpm install outra vez, e ele baixará as novas dependências do MUI, que estava na versão 6.5.0. Agora, estamos usando a versão fixa no projeto, que é a 6.4.0. Vamos ver a diferença, e temos 77 novas linhas de mudanças no nosso pnpm lock. Vamos restaurar usando checkout no arquivo pnpm lock, atualizar, executar o diff novamente, e ele estará a zero. No package.json, vamos reverter a mudança, retornando ao estado original, e usar pnpm install --frozen-lockfile. Como havia apenas uma dependência com mudança, foi mais rápido, mas eliminou a 6.4.0 e reinstalou a versão MUI 6.5.0, pois essa era a versão resolvida no nosso pnpm lock, que será usada como versão final.
Para garantir a reprodutibilidade em um ambiente de CI, podemos usar o seguinte comando:
# Em CI: garantir reprodutibilidade
pnpm install --frozen-lockfile # falha se lockfile diverge
Voltando à apresentação, temos um checklist de CI para o lockfile, que podemos adicionar em nossas pipelines de CI. Esse código estará disponível no projeto, na Aula 2. Para levar para casa, o lockfile é um artefato de primeira classe, então trate-o como código. O pnpm CI, ou equivalentes em CI, devem ser utilizados nesses ambientes sem exceção. O drift silencioso é a causa número um do famoso "na minha máquina funciona". Isso aconteceu recentemente, ao atualizar uma aplicação. Em um dia, fiz o build da aplicação, e alguns dias depois, ao testar novamente, nada funcionava. Esses minor patches podem quebrar nosso fluxo de trabalho e aplicação. Essa aplicação em particular tem muitas dependências, o que tornou a resolução do problema um caos. O hosting dos gestores, como npm e yarn, pode causar dependências pendentes. O Corepack garante o mesmo package manager para todos. Os Git hooks podem evitar que o lockfile fique desatualizado. Conflitos de merge no lockfile nunca devem ser resolvidos manualmente. Sempre regenere, resete a versão e instale novamente, usando --frozen-lockfile e bibliotecas como merge driver para realizar essa resolução.
O curso React: gerenciando dependências e evoluindo projetos com segurança possui 268 minutos de vídeos, em um total de 49 atividades. Gostou? Conheça nossos outros cursos de React em Front-end, ou leia nossos artigos de Front-end.
Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:
O Plano Plus evoluiu: agora com Luri para impulsionar sua carreira com os melhores cursos e acesso à maior comunidade tech.
2 anos de Alura
Matricule-se no plano PLUS 24 e garanta:
Jornada de estudos progressiva que te guia desde os fundamentos até a atuação prática. Você acompanha sua evolução, entende os próximos passos e se aprofunda nos conteúdos com quem é referência no mercado.
Programação, Data Science, Front-end, DevOps, Mobile, Inovação & Gestão, UX & Design, Inteligência Artificial
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
Acesso à inteligência artificial da Alura.
No Discord, você participa de eventos exclusivos, pode tirar dúvidas em estudos colaborativos e ainda conta com mentorias em grupo com especialistas de diversas áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Luri Vision chegou no Plano Pro: a IA da Alura que enxerga suas dúvidas, acelera seu aprendizado e conta também com o Alura Língua que prepara você para competir no mercado internacional.
2 anos de Alura
Todos os benefícios do PLUS 24 e mais vantagens exclusivas:
Chat, busca, exercícios abertos, revisão de aula, geração de legenda para certificado.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.
Para quem quer atingir seus objetivos mais rápido: Luri Vision ilimitado, vagas de emprego exclusivas e mentorias para acelerar cada etapa da jornada.
2 anos de Alura
Todos os benefícios do PRO 24 e mais vantagens exclusivas:
Catálogo de tecnologia para quem é da área de Marketing
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais de forma ilimitada.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.
Conecte-se ao mercado com mentoria individual personalizada, vagas exclusivas e networking estratégico que impulsionam sua carreira tech para o próximo nível.