Dia 2 — npm e o gerenciador de pacotes
npm: instalar pacotes
npm: instalar pacotes
npm é o gerenciador de pacotes do Node: instala dependências, grava o que
instalou e trava o resultado exato para a próxima máquina. Ele já vem com o
Node — não há nada a instalar à parte.
O comando completo é npm install, abreviado para npm i. Instalar é o
gesto mais repetido do projeto inteiro e não é o mesmo que declarar: o `npm
install copia os arquivos para node_modules` e grava o pacote no
package.json ao mesmo tempo, e é essa simultaneidade que faz funcionar.
npm init -y # cria o package.json do projeto npm i mysql2 # instala e grava em "dependencies" npm i -D nodemon # instala e grava em "devDependencies" npm uninstall mysql2 # remove do manifesto e da pasta npm ci # instala exatamente o que o lockfile manda
Os três arquivos, e o que cada um é
| Arquivo | Guarda | Vai para o git |
|---|---|---|
package.json | o que o projeto declara precisar | sim |
package-lock.json | a árvore exata que foi instalada | sim |
node_modules/ | os arquivos instalados | não |
node_modules é reconstruível: npm ci recria a pasta idêntica a partir dos
outros dois. É por isso que ele nunca é versionado, e um .gitignore com
node_modules/ e .env resolve os dois problemas de uma vez.
dependencies e devDependencies
dependencies é o que o programa precisa para funcionar: mysql2, dotenv. É o
que o termo dependência de produção quer dizer — o pacote faz parte do produto
entregue, e sem ele o programa nem sobe. devDependencies é o que só serve
para desenvolver: formatador, servidor de recarga, ferramenta de teste. Em
produção se instala só o primeiro grupo, e o programa continua inteiro. O
package.json deste material tem só dependencies, porque mysql2 é a única
coisa de que os exemplos dependem.
A distinção não é teórica: é o que decide se uma imagem de produção carrega o
servidor de recarga por cima do código que vai atender requisição.
O careto e o lockfile
Em dependencies, a regra se chama semver, e o ^1.0.0 é a forma mais comum
dela: o 1.0.0 é a versão mínima e o 1.0.0 do ^ significa "a partir
dela e antes da 2.0.0". Aplicado a 3.24.5, ^3.24.5 significa "a partir de
3.24.5 e antes da 4.0.0": qualquer 3.x mais novo entra. ~3.24.5 aperta
para <3.25.0. Sem versão, o npm assume ^.
| Forma | Aceita |
|---|---|
^3.24.5 | >=3.24.5 <4.0.0 |
~3.24.5 | >=3.24.5 <3.25.0 |
3.24.5 | exatamente 3.24.5 |
O ^ sozinho explica "funciona na minha máquina e não na sua": duas pessoas com
o mesmo package.json podem instalar 3.24.5 e 3.30.1. Quem impede isso é o
package-lock.json, que grava a versão exata de cada pacote da árvore,
inclusive das dependências indiretas — as que vieram junto sem ninguém pedir. Em
projeto de time é npm ci, que instala a partir do lockfile e falha se ele não
bater com o manifesto.
createRequire e require.resolve
createRequire(caminho) monta um require ancorado num arquivo, e
require.resolve('pacote') diz o caminho que o require carregaria, sem
carregar. É assim que se descobre uma segunda cópia da mesma dependência na
árvore, que é a causa clássica de "a versão é a errada mesmo depois do install".
Exemplo
'use strict'; // Exemplo da aula 1 do dia 2: como o npm guarda o que esta instalado. // // O `package.json` DECLARA o que o projeto precisa; o `node_modules` GUARDA o que // foi instalado; o `package-lock.json` TRAVA a exata arvore de dependencias. // Sao tres coisas diferentes, e misturar as tres e a origem de "funciona na minha // maquina e nao na sua". const fs = require('node:fs'); const path = require('node:path'); const { createRequire } = require('node:module'); // O exemplo mora em `codigo/t2/dia02/`, e o projeto mora tres niveis acima. // `__dirname` e o diretorio DESTE arquivo, que nao muda com o `cd`: e por isso // que ele, e nao `process.cwd()`, que se usa para chegar na raiz do projeto. const RAIZ = path.join(__dirname, '..', '..', '..'); // `createRequire` monta um `require` ancorado num arquivo — aqui, o manifesto do // projeto. E o mesmo mecanismo que o `require` usa por baixo, e e ele que diz de // onde um pacote foi carregado. const exigir = createRequire(path.join(RAIZ, 'package.json')); const manifesto = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package.json'), 'utf8')); console.log('nome do projeto:', manifesto.name); console.log('tipo de modulo:', manifesto.type || 'commonjs (padrao quando a chave falta)'); console.log('dependencias declaradas:', Object.keys(manifesto.dependencies || {}).join(', ') || '(nenhuma)'); console.log('dependencias de desenvolvimento:', Object.keys(manifesto.devDependencies || {}).join(', ') || '(nenhuma)'); // A regra do `^`: `^3.24.5` aceita qualquer 3.x a partir de 3.24.5, e nao a 4.0.0. // E por isso que o lockfile importa — sem ele, duas maquinas instalam versoes // diferentes a partir do mesmo manifesto. console.log(''); console.log('^3.24.5 permite: >=3.24.5 <4.0.0 (o 3.30.1 entra, o 4.0.0 nao)'); console.log('~3.24.5 permite: >=3.24.5 <3.25.0 (so patch e minor)'); console.log('3.24.5 permite: exatamente 3.24.5'); // O pacote instalado e o que o Node vai carregar. O careto esta entre o que o // manifesto pede e o que chegou na pasta. console.log(''); console.log('manifesto pede:', manifesto.dependencies.mysql2); const instalado = JSON.parse( fs.readFileSync(exigir.resolve('mysql2/package.json'), 'utf8') ).version; console.log('instalado de verdade:', instalado); console.log('carregado de:', path.relative(RAIZ, exigir.resolve('mysql2'))); // O sub-caminho `/promise` e o mesmo pacote, outra porta de entrada. console.log('entrada /promise:', path.relative(RAIZ, exigir.resolve('mysql2/promise'))); // --- `dependencies` declarado x `dependencies` instalado --- // // O manifesto pede UM pacote. A pasta tem onze: os outros dez veio junto como // dependencia indireta, e e por isso que `npm uninstall` nunca apaga a pasta // inteira — o que ele apaga e o que ninguem mais pede. const pedidos = Object.keys(manifesto.dependencies || {}); const instalados = fs.readdirSync(path.join(RAIZ, 'node_modules')) .filter((nome) => !nome.startsWith('.')) .flatMap((nome) => (nome.startsWith('@') ? fs.readdirSync(path.join(RAIZ, 'node_modules', nome)).map((sub) => nome + '/' + sub) : [nome])); console.log(''); console.log('declarado em dependencies:', pedidos.length, '->', pedidos.join(', ')); console.log('instalado em node_modules:', instalados.length); console.log('veio junto, sem ninguem pedir:'); for (const nome of instalados.filter((n) => !pedidos.includes(n))) { const v = JSON.parse( fs.readFileSync(path.join(RAIZ, 'node_modules', nome, 'package.json'), 'utf8') ).version; console.log(' ' + nome + '@' + v); } // O lockfile e o arquivo que trava essa arvore inteira. Sem ele, o manifesto // acima nao impede que a proxima instalacao pegue outras versoes. console.log(''); if (fs.existsSync(path.join(RAIZ, 'package-lock.json'))) { const lock = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package-lock.json'), 'utf8')); console.log('package-lock.json trava', Object.keys(lock.packages || {}).length, 'caminhos de pacote'); } else { console.log('package-lock.json ausente: cada instalacao pode resolver versoes diferentes'); } console.log(''); console.log('para o git: package.json + package-lock.json'); console.log('fora do git: node_modules/ (reconstruivel com npm ci)'); console.log('fora do git: .env (o segredo, e o assunto da aula 2 de hoje)'); console.log(''); console.log('comandos: npm i <pacote> | npm i -D <pacote> | npm uninstall <pacote> | npm ci | npm outdated');
Saída real
nome do projeto: materiais-node-mysql tipo de modulo: commonjs dependencias declaradas: mysql2 dependencias de desenvolvimento: (nenhuma) ^3.24.5 permite: >=3.24.5 <4.0.0 (o 3.30.1 entra, o 4.0.0 nao) ~3.24.5 permite: >=3.24.5 <3.25.0 (so patch e minor) 3.24.5 permite: exatamente 3.24.5 manifesto pede: ^3.24.5 instalado de verdade: 3.24.5 carregado de: node_modules/mysql2/index.js entrada /promise: node_modules/mysql2/promise.js declarado em dependencies: 1 -> mysql2 instalado em node_modules: 12 veio junto, sem ninguem pedir: @types/[email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] package-lock.json ausente: cada instalacao pode resolver versoes diferentes para o git: package.json + package-lock.json fora do git: node_modules/ (reconstruivel com npm ci) fora do git: .env (o segredo, e o assunto da aula 2 de hoje) comandos: npm i <pacote> | npm i -D <pacote> | npm uninstall <pacote> | npm ci | npm outdated
scripts e organização do projeto
scripts e organização do projeto
O bloco scripts do package.json transforma um comando longo em um nome. O
campo type decide como o Node lê os arquivos .js do projeto — e essa escolha
é do arquivo, não existe um interruptor global.
{
"type": "commonjs",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
}
}
| Comando | O que faz |
|---|---|
npm start | roda o script chamado start |
npm run dev | roda o script chamado dev |
npm run | lista os scripts do projeto |
npm test | atalho para o script test |
type aceita commonjs (padrão) e module. Com module, .js passa a ser
ESM e o require some: é import. Com commonjs, import num .js não
funciona. A decisão é por projeto, e o package.json é quem a guarda — os dois
modos estão na aula de ESM do dia 3.
A estrutura de projeto
Organizar arquivos é a decisão que mais economiza dor depois, e a convenção do
ecossistema cabe em uma frase: o código fica numa pasta src, e nada além dela
entra no git.
| Caminho | O que é |
|---|---|
src/server.js | o ponto de entrada, quem sobe o processo |
src/db.js | a conexão com o banco |
src/rotas/ | um arquivo por arquivo de rota |
package.json | manifesto e scripts |
node_modules/ | dependências instaladas, fora do git |
Duas regras resolvem quase tudo. A primeira é a pasta src: sair dela para a
raiz transforma cada arquivo novo em conflito de merge, porque package.json
e .gitignore são um só e todo mundo mexe. A segunda é a entrada única — um
arquivo que sobe o servidor, e todo o resto é função que esse arquivo chama.
O caminho relativo entre eles é o ./ do require e do import: de dentro de
src/rotas/alunos.js, o banco está em ../db.js e o servidor em
../server.js. Esse ../ é o que a estrutura de projeto torna possível — sem
pasta, o caminho relativo não existe.
.env e variável de ambiente
Variável de ambiente é chave e valor que o sistema entrega ao processo na
partida, sem estar no código. process.env é o objeto que as guarda. É o
mecanismo padrão de configuração no Node, e o motivo é concreto: senha em
variável de ambiente não aparece na listagem de processos de quem olha de fora,
enquanto senha em arquivo versionado aparece para todo mundo que leu o git log.
| Onde | Vai para o git | Tem valor real |
|---|---|---|
env.exemplo | sim | não, é modelo |
.env | não (.gitignore) | sim |
process.env | não existe em disco | vive só no processo |
O .env é um texto com uma variável por linha, no formato CHAVE=valor. Quem o
lê é o pacote dotenv, com uma chamada só, no começo do programa, antes de
qualquer outra linha de código:
require('dotenv').config();
Depois disso process.env.DB_PASS existe no processo. O nome da variável é o
que vai para o material e para o git; o valor não vai para lugar nenhum — e
é por isso que a saída de um exemplo de .env diz apenas se a senha chegou, e
nunca qual é ela.
NODE_ENV e a configuração por ambiente
NODE_ENV é a convenção do ecossistema Node: production em deploy,
development (ou ausente) no resto. O mesmo código muda de comportamento lendo
essa variável — é assim que um programa desliga log detalhado, troca mensagem de
erro técnica por mensagem genérica e liga o modo restrito, sem recompilar nada.
NODE_ENV=test é o que o npm test define por baixo, e é a razão de um
process.env perder um valor que estava setado no shell antes do comando.
Ler e escrever com colchetes
process.env.DB_NAME e process.env['DB_NAME'] devolvem a mesma coisa. Para
escrever, o colchete não é preciosismo: é o que o dotenv faz, e a diferença
aparece quando o nome da variável colide com palavra do JavaScript.
process.env.NODE_ENV = 'production'; // ok process.env['NODE_ENV'] = 'production'; // ok tambem
Depois do ponto só pode vir um identificador válido; entre colchetes pode vir
qualquer nome. É por isso que um .env com valor undefined, null ou NaN
chega ao processo como string vazia — o dotenv não escreve esses valores.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 2: variavel de ambiente e o arquivo `.env`. // // O ponto que a aula precisa deixar claro: a senha NAO esta neste arquivo. // O que esta aqui e o NOME da variavel e o mecanismo que a preenche. O valor // vive no `.env`, que o `.gitignore` exclui — e a saida deste exemplo vira // pagina publica, entao ela nunca imprime valor de senha, so se chegou. const mysql = require('mysql2/promise'); async function main() { // `process.env` e um objeto comum do Node: cada variavel de ambiente e uma // chave. `NODE_ENV` e a convencao do ecosystema Node — 'production' em deploy, // 'development' (ou ausente) no resto. console.log('NODE_ENV:', process.env.NODE_ENV || '(nao definida)'); // O que o arquivo `.env` trouxe para dentro do processo. Estes nomes sao // publicos por convencao e os valores de configuracao nao tem nada de secreto. console.log(''); console.log('variaveis do ambiente, por nome:'); for (const nome of ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_NAME']) { console.log(' ' + nome + ' = ' + (process.env[nome] || '(vazia)')); } // A senha e o caso oposto: o exemplo so diz se ela chegou. Imprimir o valor // aqui seria o mesmo que comitar a senha. console.log(''); console.log('DB_PASS preenchida?', process.env.DB_PASS ? 'sim, e o valor NAO sai daqui' : 'nao'); // --- a prova de que o `.env` chegou: o banco aceita a conexao --- // // O `dotenv` nao e chamado neste arquivo de proposito. Num programa de verdade // quem le o `.env` e o `require('dotenv').config()` que roda na primeira // linha, antes de qualquer outra coisa; e por isso que `process.env.DB_PASS` // ja esta preenchida aqui. Repetir a chamada em cada arquivo seria ruido que // o aluno copiaria sem necessidade. const conexao = await mysql.createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); const [ambiente] = await conexao.query('SELECT DATABASE() AS banco, USER() AS usuario'); console.log(''); console.log('o .env montou uma conexao de verdade:'); console.log(' banco:', ambiente[0].banco, '| usuario:', ambiente[0].usuario); await conexao.end(); console.log(' (a senha viajou de process.env para o driver e nao para a tela)'); // A variavel de ambiente existe para mudar o comportamento do mesmo codigo sem // mudar o codigo. `DB_NAME` aponta para o banco de teste aqui e para outro // banco no deploy, e nenhum dos dois precisa ser reescrito. console.log(''); console.log('--- o mesmo codigo, dois ambientes ---'); const ambienteAtual = process.env.NODE_ENV || 'development'; if (ambienteAtual === 'production') { console.log(' NODE_ENV=production: log detalhado desligado, so o essencial'); } else { console.log(' NODE_ENV=' + ambienteAtual + ': log detalhado ligado'); } // `process.env` aceita escrita, e o valor novo vale so neste processo e nos // filhos que ele criar. Nao ha como mudar o ambiente de outro processo ja // rodando, e o valor nao sobrevive ao fim do processo. // // A chave vai entre colchetes, e nao depois de um ponto, por um motivo que // importa mais do que parece: o `dotenv` faz exatamente assim, e e por isso // que ele nao consegue ler um `.env` cujo valor seja uma palavra do JavaScript // (`undefined`, `NaN`, `null` viram string vazia). Depois do ponto so pode vir // um identificador valido; entre colchetes pode vir qualquer nome. const antes = process.env['MARCADOR_DEMO']; process.env['MARCADOR_DEMO'] = 'definido pelo proprio processo'; console.log(''); console.log('MARCADOR_DEMO antes:', antes || '(nao existia)'); console.log('MARCADOR_DEMO agora:', process.env['MARCADOR_DEMO']); // Os dois arquivos e o que cada um guarda. O modelo vai para o git; o valor de // verdade, nao. console.log(''); console.log('env.exemplo -> git (nome da variavel + valor de exemplo)'); console.log('.env -> sem git (valor de verdade)'); console.log('.gitignore -> sem git (e lista .env, node_modules/ e *.log)'); console.log('process.env -> so memoria do processo, nunca em disco'); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
NODE_ENV: (nao definida) variaveis do ambiente, por nome: DB_HOST = 127.0.0.1 DB_PORT = 3306 DB_USER = materiais DB_NAME = materiais_teste DB_PASS preenchida? sim, e o valor NAO sai daqui o .env montou uma conexao de verdade: banco: materiais_teste | usuario: materiais@localhost (a senha viajou de process.env para o driver e nao para a tela) --- o mesmo codigo, dois ambientes --- NODE_ENV=development: log detalhado ligado MARCADOR_DEMO antes: (nao existia) MARCADOR_DEMO agora: definido pelo proprio processo env.exemplo -> git (nome da variavel + valor de exemplo) .env -> sem git (valor de verdade) .gitignore -> sem git (e lista .env, node_modules/ e *.log) process.env -> so memoria do processo, nunca em disco