No cenário de desenvolvimento utilizando JavaScript/TypeScript, é impensável a construção de aplicações profissionais sem a utilização de um gerenciador de pacotes. Tanto é que, ao instalar o runtime (ambiente de tempo de execução) de javascript Node.js em sua máquina, já consta a opção (marcada por padrão) de instalar também o seu gerenciador de pacotes padrão: npm.
A partir do npm, podemos instalar pacotes que muitas vezes formam a base para o desenvolvimento de aplicações complexas, além de embutir no projeto um sistema de gerenciamento desses pacotes e suas dependências.
Neste artigo, vamos explicar as funcionalidades e histórico dos gerenciadores mais populares e suas diferenças. Para melhor compreensão, iremos abordar os termos em inglês, que é a forma mais utilizada no dia a dia do desenvolvimento.
Por que é preciso ter um gerenciador de pacotes?
Como pessoas desenvolvedoras, faz parte da nossa responsabilidade prezar pela otimização de tempo e recursos em nossos projetos. Em uma analogia popular, imaginemos que o nosso trabalho fosse montar carros: não seria eficiente realizarmos da extração do petróleo até a produção final dos pneus, sendo que podemos pegá-los prontos e reutilizar em todos os carros que produzirmos.
Todavia, é sim necessário mantermos o registro de quais componentes foram utilizados e suas versões, para garantir que todas pessoas trabalhando no carro utilizem os mesmos componentes. Com projetos de desenvolvimento, a situação é similar: seria ineficiente que todo projeto desenvolvesse do zero uma aplicação que expõe um servidor para a web, considerando que a mesma atividade já foi realizada milhares de vezes.
Aí entra a necessidade de um gerenciador de pacotes: além de nos apontar para um repositório de recursos, ele também garante que todas pessoas envolvidas no desenvolvimento do projeto sigam a mesma árvore de dependências dele. Ainda, ao utilizar um gerenciador de pacotes, facilita-se em muito a atualização das versões dessas dependências. Ao invés de ter que atualizar o pacote e toda sua lista de dependências (e dependências dessas dependências sucessivamente). Tudo isso é solucionado com um comando simples como npm update nomeDoPacote.
Apesar de utilizar um nome que representa sua própria função, o npm não é o único gerenciador de pacotes do node. Como veremos adiante, empresas também desenvolveram suas próprias soluções, como é o caso do yarn.
As três partes do npm
O npm consiste em partes distintas: o site (https://www.npmjs.com/) e o registro e a Command Line Interface (CLI – Interface de linha de comandos). O site é utilizado para descobrir pacotes, configurar perfis e gerenciar outros aspectos da experiência com npm. A CLI é executada a partir de um terminal e é como a maioria das pessoas desenvolvedoras interagem com o npm. Por fim, o registro é um grande banco de dados público de software JavaScript e as meta-informações que o cercam.
Site npm
O site npm é o local onde é possível realizar o registro de uma conta para publicação de pacotes. Através dele, também é possível consultar os pacotes no registro, além de outras funções de gerenciamento (fazer parte de uma organização e administrar pacotes privados).
Registro npm
O registro npm público é o banco de dados que contém as informações sobre os pacotes publicados, cada um composto por código e metadados. Desenvolvedores de código aberto e desenvolvedores de empresas utilizam o registro npm para contribuir com pacotes para toda a comunidade ou membros de suas organizações e fazer download de pacotes para usar em seus próprios projetos.
Por ser um registro público, outros gerenciadores de pacotes (como o yarn, que explicaremos mais adiante) utilizam o mesmo registro do npm.
CLI npm
Ao utilizar a CLI do npm, é possível instalar pacotes que estejam em seu registro utilizando seu nome e, opcionalmente, a versão desejada. Os pacotes podem ser baixados de forma global (para serem utilizados a qualquer momento, independente do escopo) ou local (para serem vinculados e utilizados especificamente em um projeto). Um guia de uso da CLI exigiria um artigo focado nisso, porém os comandos mais usuais são de instalação e remoção de pacotes, respectivamente: npm install nomeDoP.
npm install nomeDoPacote npm uninstall nomeDoPacote
Podemos utilizar a flag -g para definir a instalação global do pacote, normalmente utilizada para pacotes voltados para o desenvolvimento em si, com scripts executáveis pelo comando npx.
Ao executar o comando install no escopo de um projeto, os arquivos do pacote e suas dependências são baixados para uma pasta chamada node_modules e organizados através de dois arquivos na raiz do projeto: package.json e package-lock.json. Como sempre, a melhor fonte para saber mais sobre comandos da CLI é a documentação oficial: https://docs.npmjs.com/.
Arquivos package
A organização dos pacotes instalados pelo npm se dá através de dois arquivos no formato JSON: package e package-lock. A seguir, explicaremos a função dos dois.
package.json
Ao instalar pacotes com o comando npm install xxx ou ao iniciar um projeto com npm init, é criado ou modificado um arquivo chamado package.json. Esse arquivo possui as informações básicas para que todas pessoas consigam ver as informações básicas sobre aquele projeto, scripts disponíveis e a rede de dependências, tanto de produção quanto de desenvolvimento.
Caso você apenas instale um pacote em um projeto não inicializado pelo comando npm init, a única chave será dependencies com o valor da propriedade instalada. Exemplo realizando a instalação do Express:
{ "dependencies": { "express": "^4.18.2" } }
Um arquivo package.json naturalmente cresce em conjunto com a complexidade do projeto, de forma automática. Para ver a lista de propriedades disponíveis no arquivo, recomendo a leitura da documentação, especialmente sobre a propriedade scripts que pode definir diversos comandos como execução de testes ou abertura de servidor para desenvolvimento.
package-lock.json
O arquivo package-lock.json é criado e atualizado de forma automática por qualquer operação do npm que modifique os conteúdos da pasta node_modules ou o arquivo package.json. Ele descreve a árvore de arquivos exata que foi gerada, de modo que as instalações subsequentes possam gerar árvores idênticas, independentemente das atualizações de dependência intermediárias.
Ao contrário do package.json, não devemos atualizar esse arquivo manualmente, pois ele serve exatamente para que o npm possa gerar operações idênticas independente do contexto em que esteja sendo rodado. Mais adiante veremos uma alternativa ao npm chamada yarn. Essa alternativa produz um arquivo similar chamado yarn.lock, que possui a mesma função do package-lock.json, mas descreve as instalações realizadas pelo yarn.
Comando npx
Acima, comentamos sobre como podemos usar o comando npx para executar scripts de pacotes instalados com npm. A utilização é simples (entre colchetes são parâmetros opcionais):
npx nomeDoPacote [@] [args...]
Este comando permite que você execute um comando arbitrário de um pacote npm (um instalado localmente ou obtido remotamente – se algum pacote solicitado não estiver presente nas dependências do projeto local ou na máquina de forma global, ele será instalado em uma pasta no cache do npm). Um dos scripts de pacote mais utilizados, principalmente para pessoas desenvolvedoras FrontEnd, é o create-react-app:
npx create-react-app nome-do-meu-app
A execução desse script cria toda estrutura básica de um projeto utilizando React, acelerando consideravelmente o setup do projeto. Dada a natureza do comando, tanto é possível integrar um executável em um pacote com mais ferramentas ou criar pacotes cuja única função é sua execução. Como inspiração, recomendo digitar o comando npx azhariel no terminal para um exemplo de cartão de visitas simples e disponível globalmente para usuários do npm.
Yarn
Apesar de todos os méritos do npm, algumas pessoas desenvolvedoras viram a necessidade de melhorias no gerenciador de pacotes. Uma dessas pessoas foi Sebastian McKenzie, que resolveu iniciar a implementação do gerenciador yarn (em português, literalmente “novelo [de lã]”, mas é uma sigla para “Yet Another Resource Negotiator”; em português, “Mais um negociador de recursos”) enquanto trabalhava no Facebook. Apesar dessa conexão inicial com a gigante tech, o gerenciador é open source e não é mantido diretamente por nenhuma empresa.
Para utilizar o yarn, devemos instalá-lo globalmente utilizando o próprio npm:
npm install --global yarn
Atualmente, o yarn utiliza o mesmo registro mantido pelo npm, apontando apenas para um domínio de CNAME diferente. Sendo assim, as principais diferenças com o gerenciador padrão se dão na arquitetura, estratégia e performance dos gerenciadores.
Comandos
Existem algumas diferenças nos comandos dos dois gerenciadores. Como sempre, sugiro sempre a consulta na documentação oficial de ambos, mas em resumo podemos utilizar a seguinte tabela:
Instalação dos Pacotes
Enquanto o npm instala os pacotes designados diretamente na pasta node_modules, o yarn, por padrão, realiza a instalação a partir de uma pasta cache que é utilizada globalmente na máquina. Apesar de também manter uma pasta cache, o npm busca os arquivos no registro para o projeto. Essa estratégia de cache permite ao yarn, a instalação de dependências de forma offline, contanto que já estejam disponíveis no cache local.
Ainda sobre a instalação de pacotes, o yarn opta por uma estratégia de download e instalação em paralelo, otimizando o tempo total necessário para instalação de diversas dependências. Em contrapartida, o npm realiza o download
e instalação de forma linear, um pacote por vez.
Estratégia Plug N Play (PNP)
Desde 2018, o yarn possibilita a utilização de uma estratégia otimizada para auxiliar o Node a encontrar a localização dos pacotes instalados. Normalmente, o Node procura por módulos instalados de forma recursiva: quando há o requerimento de uma dependência, o runtime procura pela mesma na pasta node_modules de onde houve a requisição. Caso não seja encontrada, ele volta a procurar na pasta node_modules um nível acima, sucessivamente, até encontrar o local de instalação da dependência (ou não encontrá-la e gerar um erro de dependência).
Como alternativa a essa sequência de busca, o yarn gera um único arquivo .pnp.cjs em vez da pasta node_modules. O arquivo .pnp.cjs contém vários mapas: um vinculando nomes e versões de pacotes à sua localização no disco e outro vinculando nomes e versões de pacotes à sua lista de dependências. Com essas tabelas de pesquisa, o yarn pode informar instantaneamente ao Node onde encontrar qualquer pacote que ele precise acessar, desde que faça parte da árvore de dependências.
Ela é utilizada por padrão na versão mais recente do yarn (2, apelidado de berry). Mais informações sobre podem ser encontradas na documentação oficial.
Segurança
Ambos gerenciadores possuem funções que garantem a segurança na instalação e execução dos pacotes. Porém, utilizam estratégias diferentes. Ao requisitar a instalação de um pacote, o yarn garante a integridade dos dados gerando um checksum, que é integrado aos dados e comparado localmente, rejeitando os dados caso os cálculos apresentem resultados diferentes. Por outro lado, o npm utiliza um algoritmo SHA-512 para verificar a integridade dos pacotes, comparando com a hash disposta no arquivo package-lock.json.
Além disso, o npm também conta com o comando npm audit, que informa sobre possíveis vulnerabilidades nas versões das dependências instaladas e o comando npm audit fix, que visa resolver as vulnerabilidades reportadas com upgrade para versões mais recentes.
Velocidade
Como existem diversos fatores que podem influenciar na velocidade final de uma operação dos gerenciadores de pacote, é necessária a análise de diversos cenários. Pensando nisso, um outro gerenciador de pacotes chamado pnpm realiza benchmarks diários entre npm, yarn e o próprio pnpm. Os resultados podem ser conferidos no site do próprio pnpm. Como exemplo, esses são os resultados obtidos na data de publicação deste artigo:
Na maioria dos cenários, o yarn aparece com vantagem sobre as operações do npm, em especial quando utiliza a estratégia PnP em conjunto com cache e lockfile, gerando uma instalação quase instantânea dos pacotes.
Conclusão
Apesar de diferenças pontuais, ao fim do dia, mais importante do que a escolha de gerenciador de pacotes é a escolha dos pacotes em si.
Quando optamos por utilizar um pacote, embarcamos também em todas as dependências relacionadas àquele pacote, o que pode gerar um efeito cascata desnecessário para o andamento do projeto.
Dito isso, a construção de aplicações modernas e profissionais quase que necessariamente exige a instalação de pacotes e, com isso, a compreensão básica sobre o gerenciamento desses pacotes é uma habilidade necessária para a pessoa desenvolvedora.
Veja também:
Como criar Micro Interações em ReactJS?
Autor: Renan Zambon Braga.