Como o npm install resolve as dependências de um projeto
Tags: npm, javascript, node, braziliandevs
Published: 2023-09-26 01:07:47

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

```bash
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:

```json
{
  "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](https://docs.npmjs.com/cli/v9/using-npm/registry?v=true) sendo o padrão o [registry público do npm](https://registry.npmjs.org/) 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`](https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json) ou [`npm-shrinkwrap.json`](https://docs.npmjs.com/cli/v9/configuring-npm/npm-shrinkwrap-json) e também o [npm-cache](https://docs.npmjs.com/cli/v9/commands/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:

```tree
├── 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:

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

- [NPM Folders](https://docs.npmjs.com/cli/v9/configuring-npm/folders)
- [NPM Install Algorithm](https://docs.npmjs.com/cli/v9/commands/npm-install?v=true#algorithm)