Se você é uma pessoa desenvolvedora de Front End ou Back End que utiliza JavaScript como sua linguagem de programação já deve ter, em algum momento de sua jornada, executado o famigerado comando npm install
algumas (muitas) vezes para baixar bibliotecas a serem usadas em seus projetos.
Mas afinal, o que esse comando faz por baixo dos panos e como ele resolve as dependências de um projeto? Bora descobrir na prática e entender de uma vez por todas como o npm install
funciona
Pré Requisitos
Caso for acompanhar os exemplos junto é fundamental ter o ambiente preparado, então garanta que possua:
- Node instalado minimamente na versão
18
- NPM instalado minimamente na versão
9
Até é possível utilizar versões anteriores mas acabaríamos perdendo as incríveis capacidades que as versões mais novas nos proporcionam
E também para ter um ambiente de testes mais limpo vamos criar um projeto npm
através do comando abaixo
npm init --yes
Esse comando deve criar um arquivo package.json
no diretório a qual o comando foi executado mais ou menos com o conteúdo abaixo:
{
"name": "projects",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Tudo pronto, vamos começar os nossos testes
Entendendo o npm install
Como o próprio nome já indica, o comando tem o objetivo de instalar pacotes e torná-los disponíveis para uso em nossos projetos. Esses pacotes são baixados de um npm-registry sendo o padrão o registry público do npm onde milhares de pacotes e suas versões são publicados diariamente
Existem algumas formas de usar o comando, sendo as principais que abordaremos nesse artigo são as seguintes:
-
npm install
: Instalação de dependências definidas em umpackage.json
-
npm install <nome-pacote>
ounpm install <nome-pacote>@<versão>
: Instalação de um pacote especifico e opcionalmente em uma versão especifica
Caso não seja especificado uma versão de pacote o
npm
avaliará se já não foi definido o uso do pacote nopackage.json
. Caso positivo ele utilizará a versão definida no arquivo. Caso negativo ele tentará baixar a última versão do pacote no npm-registry
Após a execução do comando os seguintes processos são executados:
-
Verifica se existe uma pasta
node_modules
onde o comando foi executado1.1. Caso exista será criada uma cópia da árvore de dependências baseada na estrutura da
node_modules
1.2. Caso não exista será criada uma árvore de dependências vazia -
Realiza o download das dependências definidas no
package.json
e, caso tenham sidos definidos no comando, os pacotes específicos a serem baixados.2.1. Os pacotes que faltam na cópia da árvore de dependências são adicionados a ela
2.2. Os pacotes que tiverem versões modificadas serão atualizados na cópia da árvore de dependências
2.3. Os pacotes presentes na cópia da árvore de dependências mas não forem resolvidos no download serão removidos da cópia -
Compara a estrutura da árvore de dependências original e a cópia da árvore de dependências e realiza as modificações necessárias
3.1. Em caso de conflitos realiza o tratamento da maneira que o
npm
achar mais eficaz e correta
Algumas coisas também afetam o comportamento do comando como a presença de arquivos
package-lock
ounpm-shrinkwrap.json
e também o npm-cache, mas não os abordaremos nesse momento, quem sabe em outro artigo ou uma sequência deste aqui
Estrutura da node_modules
e a Resolução de Conflitos de Dependências
Quando instalamos um pacote em nosso projeto é comum vir junto uma cacetada de outros pacotes junto, pois foram declarados como dependências pelo pacote que queremos usar. Então não é impossível que diferentes pacotes tenham como dependência um mesmo pacote, mas em versões distintas
Vamos entender o que ocorre quando baixamos dois pacotes que tem a mesma dependência em comum porém com versões diferentes. Para esse teste iremos baixar dois pacotes: prettier-eslint@15.0.0
e gulp-eslint@6.0.0
. Ambos tem como dependência o eslint
porém o primeiro depende da versão ^8.7.0
e o segundo da versão ^6.0.0
A escolha de bibliotecas foram apenas para fins demonstrativos, não é uma recomendação de uso em seus projetos
Nesse cenário é certo que haverá conflitos pois estamos lidando com duas versões diferentes de eslint
e o npm
precisa agir. Para isso deverá ser criado uma instância de eslint
geral que ficará na raíz da pasta node_modules
e também uma segunda instância exclusiva para o outro pacote ficando com uma estrutura mais ou menos assim:
├── node_modules
│ ├── eslint@8.50.0
│ ├── gulp-eslint@6.0.0
│ │ ├── node_modules
│ │ │ ├── eslint@6.8.0
│ ├── prettier-eslint@15.0.0
Note que foi criada uma sub pasta node_modules
abaixo da pasta do pacote gulp-eslint
. Isso ocorre pois é a forma do npm
segregar os escopos e permitir que o gulp-eslint
utilize o eslint
na versão que precisa.
Na prática, dentro de contexto de uso de pacotes em códigos JS quando realizamos a importação de uma dependência seja através de import
com ESModules
ou o uso de require(...)
o npm
resolve baseado na localização do pacote dentro da node_modules/<nome-dependencia>
mais próxima.
Em nosso exemplo, para o pacote prettier-eslint
e até mesmo o nosso projeto, a referência mais próxima acaba sendo o eslint
definido na raiz da node_modules
pois não há outra pasta node_modules/eslint
no caminho. Porém para o gulp-eslint
ele acabaria resolvendo com o eslint
dentro da pasta node_modules
que se encontra abaixo da própria pasta do gulp-eslint
pois é a referência relativamente mais próxima
Mas afinal, baseado em que o npm
decidiu criar a sub pasta de node_modules
abaixo do pacote gulp-eslint
e não do prettier-eslint
? Bem, existem alguns fatores determinantes em ordem de importância:
-
Quão próximo está na declaração direta do pacote - Quanto mais distante da ramificação principal menor a prioridade de definir a versão da dependência como dominante
1.1. No exemplo, caso seja instalado no projeto diretamente o
eslint
na versão7.0.0
seria criado na raiz danode_modules
oeslint
na versão7.0.0
e dentro das pastas dos pacotesgulp-eslint
eprettier-eslint
seriam criadas sub pastasnode_modules
com as respectivas versões deeslint
a qual eles dependem. Isso ocorre pois o projeto está no nível 1 enquanto os pacotes estão no nível 2 (projeto > eslint
vsprojeto > gulp-eslint > eslint
)
1.2. Caso seja instalado, invés do pacoteprettier-eslint
, o pacoteeslint-config-celebrate
que depende doprettier-eslint
a prioridade seria da versão deeslint
definida pelogulp-eslint
. Isso ocorre pois o pacotegulp-eslint
está no nível 2 enquanto oprettier-eslint
agora está no nível 3 (projeto > gulp-eslint > eslint
vsprojeto > eslint-config-celebrate > prettier-eslint > eslint
) -
Se mesmo assim ordem de prioridade for a mesma, será utilizado como fator de desempate a ordem de instalação
2.1. No exemplo, caso seja instalado primeiro o
prettier-eslint
e depois ogulp-eslint
a prioridade será da versão deeslint
definida peloprettier-eslint
2.2. Caso seja instalado primeiro ogulp-eslint
e depois oprettier-eslint
a prioridade será da versão deeslint
definida pelogulp-eslint
Conclusões
No exemplo que trabalhamos foram criadas duas instâncias da dependência em comum mas em projetos reais é comum ter muito mais instâncias. Se ter instâncias duplicadas não for algo que prejudique o funcionamento do uso da dependência não há com que se preocupar. Mas caso seja fundamental apenas uma instância é possível aplicar algumas práticas para alcançar isso. Quem sabe em um outro momento falamos sobre isso ;)
Top comments (2)
Estou muito orgulhosa de você, veio ai o primeiro artigo! E um tema excelente!
Muito obrigado pelo apoio 😁❤️