loading...

Conheça um pouco mais sobre o sistema de módulos em Node.JS e NPM

michelaraujo profile image Michel Araujo ・12 min read

Imagem intro do post Conheça um pouco mais sobre o sistema de módulos em Node.JS e NPM

O objetivo desse post é mostrar de uma maneira objetiva como algumas coisas funcionam no sistema de módulos do Node.JS e recursos que podem ser úteis no dia a dia.

O que vamos abordar neste artigo:

  • Começando do começo! (Comandos basicos do NPM)
  • Está rodando NPM com sudo ? Saiba como ajustar isso.
  • Como funciona a versão dos módulos
  • Para que serve o caracter “^” ?
  • Como funciona exports e require em NodeJS
  • NPM Scripts
  • Como configurar um registry Scope
  • Boas ferramentas!

Começando do começo!

Vamos apresentar aqui algumas configurações básicas do NPM.
O comando “npm config” pode ser usado para alterar as configurações do seu npm permanentemente, por exemplo para configurar um nome de autor podemos usar o seguinte comando:

npm config set init.author.name "<name here>"

Obs: Esse nome de autor será usado quando executamos o comando “npm init”

Você pode listar todas as configurações atuais que você fez usando o comando “npm config ls” ou visualizar todas as configurações inclusive as que já vem por padrão com o comando “npm config ls -l”.

Para visualizar a documentação completa das configurações do NPM acesse npm-config page, sabendo usar essas configurações ao nosso favor podemos aproveitar muito mais alguns recursos do NPM como cache, diferentes locais de instalação de dependências ou usar um registry customizado, ao longo deste post vamos ver mais a fundo algumas dessas config.

Comandos basicos do NPM

  • npm install : Baixa um módulo para o diretório node_modules sem salvar permanentemente, ou seja, se o diretório node_modules for deletado no próximo npm install essa módulo não será baixado.
  • npm install --save : Baixa um módulo no diretório node_modules e registra a versão baixada no arquivo package.json assim no próximo npm install o módulo será baixado novamente, atualiza o package-lock quando há alguma atualização de versão.
  • npm install --save-dev : Baixa um módulo no diretório node_modules e registra no package.json como uma dependência de desenvolvimento.
  • npm install --production: Baixa todas as dependências menos as que são instaladas com a flag --save-dev (dependências de desenvolvimento)
  • npm ci: Baixa todas as dependência seguindo exatamente as versões que estão no arquivo package-lock não fazendo nem uma alteração no mesmo.
  • npm ls: Vai retornar toda a árvore de dependência de produção e desenvolvimento, pode ser muito útil para achar sub-dependências ou dependências que não deveriam existir.
  • npm audit: Faz uma varredura nas suas dependências procurando por versões que podem possuir algum tipo de vulnerabilidade, no final retorna um relatório com cada descoberta e sua criticidade.
  • npm outdated: Mostra todas as dependências que tem atualização disponível.

Está rodando NPM com sudo ? Saiba como ajustar isso.

Você já se deparou com a necessidade de executar algum comando do npm com sudo ? Ou todos os comandos? Se sim isso acontece provavelmente porque o usuário que você está executando o comando não tem permissão para acessar o diretório que contém os módulos globais ou locais do npm.
Existem algumas maneiras de ajustar isso, vou mostrar aqui a que eu mais gosto =).

Primeiro vamos executar o comando “npm config get prefix”, com esse comando podemos ver onde estão sendo instalados nossos módulos, provavelmente estão dentro de /usr/local/.
O que vamos fazer é trocar o local onde os módulos do npm são instalados criando um diretório para isso.
Vamos criar um diretório na nossa pasta home:

mkdir ~/npm-global

Depois vamos fazer com que nosso diretório central do npm seja esse que criamos usando o comando:

npm config set prefix ~/npm-global

Vamos precisar adicionar ao nosso PATH do sistema esse nosso diretório para conseguirmos chamar nossas libs pelo terminal sem problemas.

export PATH=$PATH:~/npm-global/bin
source ~/.profile

Pronto, para conferir se a configuração está ok você pode executar o comando “npm config get prefix” e visualizar se é o seu diretório que retorna.
Se você já tiver algum projeto que esteja usando sudo para executar os comandos recomendo que remova o diretório node_modules e execute npm install novamente, assim tudo deverá funcionar sem sudo agora.

Como funciona a versão dos módulos

Os módulos do npm segue um padrão de versionamento chamado SemVer (Semantic Versioning), esse padrão consiste basicamente de 3 números sendo eles MAJOR, MINOR e PATCH separados por ponto, ex: 2.3.14. (Como se pode notar o número de cada posição da versão corresponde a Major.Minor.Patch).

PATCH: Está relacionado a qualquer alteração feita que não quebra a funcionalidade já existente e não adiciona uma funcionalidade nova, o melhor exemplo para isso é correção de bug.
Vamos imaginar que temos um módulo na versão 1.0.0, fizemos uma correção de bug e adicionamos uma documentação, a próxima versão será 1.0.1.

MINOR: Seu valor é aumentado quando adicionamos uma nova funcionalidade que não quebra as já existentes, mantendo a compatibilidade.
Voltando a o nosso módulo de exemplo vamos imaginar que adicionamos uma nova função para retorna uma lista de produtos, não altera as já existentes apenas faz algo novo então a próxima versão do nosso módulo será 1.1.0.

MAJOR: Está relacionado a qualquer alteração que tenha uma quebra de compatibilidade, por exemplo a alteração da interface de uma função, alteração no padrão de retorno de uma função já existente e etc.
Voltando a o nosso módulo imaginário digamos que vamos alterar nossa função que retorna a lista de produtos trocando seu retorno de Array para Json, sendo assim a próxima versão do nosso módulo será 2.0.0.

Observe que cada incremento na categoria maior reseta a menor, se fizemos um incremento no MINOR o PATCH é resetado, se fizemos um incremento no MAJOR, o MINOR e o PATCH é resetado.

Se quiser entender um pouco mais a fundo sobre esse padrão de versionamento recomendo que leia Semantic Versioning Web Page.

Podemos fazer essas alterações de versão alterando diretamente nosso arquivo package.json ou usando os comando do npm:

npm version patch // Incrementa o patch
npm version minor // Incrementa o minor
npm version major // Incrementa o major

Ou setar já tudo de uma vez com:

npm version 2.0.0

Para ver a versão do seu módulo como também a do node, npm, v8 é só executar o comando:

npm version

Para que serve o caracter “^” ?

Quando executamos o comando “npm install --save ” por padrão a versão do módulo é adicionado a o nosso package.json com o caractere “^” na frente, ex: express": "^4.17.1".
Esse caractere indica que qualquer alteração de PATCH que houver vai ser adicionada automaticamente, por exemplo vamos supor que foi feito um novo incremento no nível PATCH do express “4.17.2”, quando a gente executar o comando “npm install” novamente esse incremento já vai ser adicionado automaticamente. Segue a mesma regra para toda árvore de dependência, ou seja toda dependência e sub-dependência do express vai seguir essa mesma regra.
Esse comportamento por padrão permite que nossa aplicação receba um bug fixes sem mesmo a gente saber, por outro lado isso requer um certo nível de confiança sobre o módulo que estamos consumindo, se você não quiser esse comportamento pode simplesmente editar manualmente o arquivo package.json removendo o caractere “^”.
Ou se sua aplicação/módulo está em um nível estável e você quer garantir que nem uma alteração não conhecida de dependência seja feita, podemos desabilitar a atualização do arquivo package-lock.json, sendo assim todo “npm install” vai pegar as dependências com a versão fixada do package-lock que serve como um snapshot das dependências, sem fazer qualquer atualização de versão, para fazer isso é só executar o comando:

npm config set package-lock false

Obs: com essa configuração não vai ser gerado arquivo package-lock para novos projetos/módulos, fique atento com isso!

Esse recurso pode ser muito útil se seu software passa por auditoria ou um processo mais rigoroso referente a atualização de dependência, com o snapshot do package-lock fica mais fácil controlar as versões e comprovar isso.

Como podemos ver o package-lock.json funciona como um snapshot de dependências e sub-dependências, ele é nada mais que uma árvore contendo o registro de versões das dependências e das dependências das dependências que são usadas na sua aplicação/módulo, sugiro que você abra um arquivo package-lock.json e dê uma olhada com atenção isso pode clarear mais as coisas.
Para mais informações Documentação npm package-lock.json.

Como funciona exports e require em NodeJS

Inicialmente para deixar claro todo arquivo que contenha código JS é considerado um módulo no NodeJS, contendo variáveis, funções exportadas ou não.

Quando fazemos alguma chamada (require) a um módulo em NodeJS o mesmo é encapsulado (wrapped) por uma função, essa função consiste em:

( function (exports, require, module, __filename, __dirname) {
/* código do modulo aqui */
})

Como podemos ver essa função contém 5 parâmetros cada uma responsável por armazenar diferentes informações sobre o módulo que está sendo exportado.
Para facilitar o entendimento de cada um desses parâmetros vamos criar um módulo teste chamado myModule.js e ver como isso funciona na prática.

O código do nosso módulo inicialmente vai ser o seguinte:

console.log('Exports => ', exports);
console.log('Require => ', require);
console.log('Module => ', module);
console.log('Filename => ', __filename);
console.log('Dirname => ', __dirname);

Quando executamos esse script (módulo) vamos ter o valor referente a cada parâmetro da função wrapped citada acima.
Vamos executar o comando:

node myModule.js

E o resultado será o seguinte:

Exports =>  {}
Require =>  [Function: require] {
  resolve: [Function: resolve] { paths: [Function: paths] },
  main: Module {
    id: '.',
    path: '/home/michel/Workspace/lab/examples_posts/modules_npm',
    exports: {},
    parent: null,
    filename: '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js',
    loaded: false,
    children: [],
    paths: [
      '/home/michel/Workspace/lab/examples_posts/modules_npm/node_modules',
      '/home/michel/Workspace/lab/examples_posts/node_modules',
      '/home/michel/Workspace/lab/node_modules',
      '/home/michel/Workspace/node_modules',
      '/home/michel/node_modules',
      '/home/node_modules',
      '/node_modules'
    ]
  },
  extensions: [Object: null prototype] {
    '.js': [Function],
    '.json': [Function],
    '.node': [Function]
  },
  cache: [Object: null prototype] {
    '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js': Module {
      id: '.',
      path: '/home/michel/Workspace/lab/examples_posts/modules_npm',
      exports: {},
      parent: null,
      filename: '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js',
      loaded: false,
      children: [],
      paths: [Array]
    }
  }
}
Module =>  Module {
  id: '.',
  path: '/home/michel/Workspace/lab/examples_posts/modules_npm',
  exports: {},
  parent: null,
  filename: '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js',
  loaded: false,
  children: [],
  paths: [
    '/home/michel/Workspace/lab/examples_posts/modules_npm/node_modules',
    '/home/michel/Workspace/lab/examples_posts/node_modules',
    '/home/michel/Workspace/lab/node_modules',
    '/home/michel/Workspace/node_modules',
    '/home/michel/node_modules',
    '/home/node_modules',
    '/node_modules'
  ]
}
Filename =>  /home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js
Dirname =>  /home/michel/Workspace/lab/examples_posts/modules_npm

Isso acontece porque quando executamos o nosso script (módulo) ele é encapsulado pela função já citada acima e seus parâmetros ficam disponíveis no contexto do atual módulo.

O parâmetro exports é uma referência para o module.exports (atalho) e contém tudo que é exportado dentro do nosso módulo, no momento como não estamos exportando nada o valor atual é: {}

Vamos fazer um teste e exporta algo no nosso módulo, uma variável “name” por exemplo, o código vai ficar assim:

exports.name = 'João';

console.log('Exports => ', exports);
console.log('Require => ', require);
console.log('Module => ', module);
console.log('Filename => ', __filename);
console.log('Dirname => ', __dirname);

E o resultado será esse:

Exports =>  { name: 'João' }
Require =>  [Function: require] {
  resolve: [Function: resolve] { paths: [Function: paths] },
  main: Module {
    id: '.',
    path: '/home/michel/Workspace/lab/examples_posts/modules_npm',
    exports: { name: 'João' },
    parent: null,
    filename: '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js',
    loaded: false,
    children: [],
    paths: [
      '/home/michel/Workspace/lab/examples_posts/modules_npm/node_modules',
      '/home/michel/Workspace/lab/examples_posts/node_modules',
      '/home/michel/Workspace/lab/node_modules',
      '/home/michel/Workspace/node_modules',
      '/home/michel/node_modules',
      '/home/node_modules',
      '/node_modules'
    ]
  },
  extensions: [Object: null prototype] {
    '.js': [Function],
    '.json': [Function],
    '.node': [Function]
  },
  cache: [Object: null prototype] {
    '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js': Module {
      id: '.',
      path: '/home/michel/Workspace/lab/examples_posts/modules_npm',
      exports: [Object],
      parent: null,
      filename: '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js',
      loaded: false,
      children: [],
      paths: [Array]
    }
  }
}
Module =>  Module {
  id: '.',
  path: '/home/michel/Workspace/lab/examples_posts/modules_npm',
  exports: { name: 'João' },
  parent: null,
  filename: '/home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js',
  loaded: false,
  children: [],
  paths: [
    '/home/michel/Workspace/lab/examples_posts/modules_npm/node_modules',
    '/home/michel/Workspace/lab/examples_posts/node_modules',
    '/home/michel/Workspace/lab/node_modules',
    '/home/michel/Workspace/node_modules',
    '/home/michel/node_modules',
    '/home/node_modules',
    '/node_modules'
  ]
}
Filename =>  /home/michel/Workspace/lab/examples_posts/modules_npm/myModule.js
Dirname =>  /home/michel/Workspace/lab/examples_posts/modules_npm

Agora podemos ver que a variável exports no nosso resultado contém em seu objeto a propriedade name com o valor João, isso vai se manter para tudo que exportamos no nosso módulo (função, classe e etc).

O parâmetro require armazena o resultado de module.exports seguido de algumas propriedades adicionais como cache, observe o retorno do script mencionado acima.
O parâmetro module armazena as informações do module em geral, é um objeto criado pelo Module system.
O parâmetro __filename é o nome do arquivo (com o caminho completo) e o __dirname é o diretório onde o arquivo foi encontrado (caminho completo).

Esse foi um resumo de como o export em NodeJS funciona focado na função wrapped, espero que tenha ficado entendível. È claro que essa é uma parte do processo e existe mais alguns passos quando a gente faz require de um módulo até ele ser retornado mas é um pouco “baixo nível” e não sei se teria tanta relevância entrar nos mínimos detalhe aqui, claro se você quiser pesquisar mais a fundo sentasse a vontade, vou deixar aqui uma imagem que resume todo esse processo.

Imagem do fluxo do require em node.js
Font: Node Js Cookbook

Se quiser saber mais: Modules DOC

NPM Scripts

Usar os scripts do npm ao nosso favor pode ser uma poderosa ferramenta para nos auxiliar no dia a dia automatizando pequenas tarefas. Podemos configurar scripts a ser executado na sessão “scripts” no arquivo package.json, por exemplo vamos configurar para rodar nossos testes unitários com jest, ficaria assim:

"scripts": {
    "test": "jest"
},

Se executamos “npm test” no terminal nossos testes unitários será executado.

Esse script “test” é um dos vários scripts pré definidos do npm, você pode ver a lista completa aqui Doc npm scripts.
Além desses scripts pré definidos a gente pode criar nossos próprios scripts adicionando uma propriedade no objeto da sessão “scripts” no package.json, a única diferença é que para executar nossos scripts “custom” vamos ter que add a prop run na execução do script ficando assim: “npm run ”, vamos ver os exemplos:

"scripts": {
    "test": "jest",
    "start": "echo \"Start something\"",
    "say_my_name": "echo \"Michel\""
  },

Ao executar o comando “npm run say_my_name” no terminal o nome “Michel” será mostrado na tela.

Podemos fazer também encadeamento de scripts, exemplo:

"scripts": {
    "test": "jest",
    "start": "echo \"Start something\"",
    "say_my_name": "echo \"Michel\"",
    "say_my_name:full": "npm run say_my_name \"Araujo\""
  },

Acrescentamos uma linha “"say_my_name:full": "npm run say_my_name \"Araujo\""” que vai executar o script “say_my_name” e adicionar a palavra “Araujo”, se executamos esse script o resultado será o seguinte:

npm run say_my_name:full
Resultado: “Michel Araujo”

Assim podemos fazer o encadeamento de script o quanto precisamos, exemplo:

"scripts": {
    "say_my_name_test": "npm test && npm run say_my_name \"Araujo\""
  },

IMPORTANTE: O carácter “&&” fazer a divisão de chamada sincronamente, no exemplo primeiro o “npm test” será executado e depois o “npm run say_my_name”, para fazer chamadas assíncronas basta usar somente um caractere “&”, exemplo:

"scripts": {
    "say_my_name_test": "npm test & npm run say_my_name \"Araujo\""
  },

Vale ressaltar que os scripts no npm é um sh portanto pode ser usado com comandos shell, exemplo:

"scripts": {
    "list": "ls -la"
  },

npm run list

Sendo assim podemos abusar da nossa criatividade!

Nota: Vale a pena citar que quando executamos um script npm os diretórios atual “node_modules/.bin” são adicionados a variável de ambiente PATH, sendo assim mesmo se não tivemos uma diferença de algum executável no PATH do sistema podemos referenciar no scripts do npm que irá funcionar.

Vale uma menção honrosa aqui sobre os scripts hook dê uma olhada aqui doc npm scripts para saber mais.

Como configurar um registry Scope

Vamos dizer que você tem um registry local e você quer mandar seus módulos para lá como também baixar do mesmo sem afetar o registry padrão ou seja sem influenciar nas outras dependências, você pode criar um Scope para isso simplesmente usando o caractere “@” no nome do módulo, exemplo:

"name": "@local/mymoduletest",

Assim quando fomos usar esse módulo como dependência

dependencies": {
    "express": "^4.17.1",
    "@local/mymoduletest": "^1.0.0"
  }

E executar npm install o express será baixado do registry padrão e o @local/mymoduletest do nosso registry local.

Exemplo de como ficou nosso package-lock:

"@local/mymoduletest": {
      "version": "1.0.0",
      "resolved": "http://localhost:4873/@local%2fmymoduletest/-/mymoduletest-1.0.0.tgz",
      "integrity": "sha512-7+mejz"
    },

Observe a prop “resolved”.

Usei o Sinopia para criar um registry local e executar esse exemplo.

Boas ferramentas!

Sinopia: Permite que você tenha um registry local e privado com facilidade, tem uma boa integração com o http://npmjs.org/ pode ser usado como cache do npm. Pode ser usado com Docker =)
Saiba mais em: https://www.npmjs.com/package/sinopia

IPFS protocol: Pode ser usado como uma alternativa ao registry oficial do npm para fazer publicação de módulos.
Saiba mais em: https://ipfs.io/ e https://www.npmjs.com/package/stay-cli

Registry-static: Literalmente baixa todo o registry do NPM na máquina local (mais de 200 Gbs) parece absurdo mas pode ser uma boa opção de cache para o npm install ficar mais rápido.
Saiba mais em: https://www.npmjs.com/package/registry-static

Referencias:

Stay hungry Stay foolish!

Discussion

pic
Editor guide
Collapse
ruyadorno profile image
Ruy Adorno

👏👏👏 show de bola! ❤️ vlw Michel, ficou foda!

Collapse
michelaraujo profile image