DEV Community

Daniel Bertolini
Daniel Bertolini

Posted on

Como o npm install resolve as dependências de um projeto

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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 um package.json
  • npm install <nome-pacote> ou npm 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 no package.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:

  1. Verifica se existe uma pasta node_modules onde o comando foi executado

    1.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

  2. 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

  3. 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 ou npm-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
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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ão 7.0.0 seria criado na raiz da node_modules o eslint na versão 7.0.0 e dentro das pastas dos pacotes gulp-eslint e prettier-eslint seriam criadas sub pastas node_modules com as respectivas versões de eslint 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 vs projeto > gulp-eslint > eslint)
    1.2. Caso seja instalado, invés do pacote prettier-eslint, o pacote eslint-config-celebrate que depende do prettier-eslint a prioridade seria da versão de eslint definida pelo gulp-eslint. Isso ocorre pois o pacote gulp-eslint está no nível 2 enquanto o prettier-eslint agora está no nível 3 (projeto > gulp-eslint > eslint vs projeto > eslint-config-celebrate > prettier-eslint > eslint)

  2. 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 o gulp-eslint a prioridade será da versão de eslint definida pelo prettier-eslint
    2.2. Caso seja instalado primeiro o gulp-eslint e depois o prettier-eslint a prioridade será da versão de eslint definida pelo gulp-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 ;)

Referências

Top comments (2)

Collapse
 
jessilyneh profile image
Jessilyneh

Estou muito orgulhosa de você, veio ai o primeiro artigo! E um tema excelente!

Collapse
 
dan_bertolini profile image
Daniel Bertolini

Muito obrigado pelo apoio 😁❤️