Quando estamos trabalhando em diversos projetos com Node.js, seja qual for a natureza deles, é muito comum haver a necessidade de compartilhamento de módulos para evitar duplicação de código e facilitar a manutenção futura.
Não vou falar aqui de publicação no NPM e nem no GitHub, que seriam duas alternativas bem viáveis, muito profissionais (principalmente NPM por permitir versionamento de módulos) e também e as mais fáceis de fazer, mas que no entanto exigem que você tenha os seus módulos compartilhados como públicos (free) ou como privados, mas pagando para o NPM.
Entendo aqui que você está usando uma abordagem monorepo para o versionamento do seu projeto, ou seja, guarda todos eles em um mesmo repositório.
E embora você possa fazer esse referenciamento de dependências simplesmente usando o require e passando o caminho relativo “indo buscar” o módulo em questão (o que costuma gerar caminhos gigantescos), existem outras alternativas, sendo que o intuito do post de hoje é justamente mostrá-las.
Ah, também não vou entrar em alternativas mais complexas de fazer gestão de monorepo usando Lerna ou Rush. Vou tentar abordagens mais light e que podem resolver o seu problema, como resolveram o meu.
NPM LINK
o NPM oferece uma solução usando NPM LINK, um utilitário e linha de comando que cria links simbólicos, os famosos atalhos, de um módulo para outro.
Para fazer um teste de NPM LINK, vamos criar primeiro a estrutura em questão, para você ver na prática.
Primeiro, crie um projeto commons e dentro dele um arquivo math.js, como abaixo.
1 2 3 4 5 |
module.exports.math = (val1, val2) => { return val1 + val2; } |
Agora, rode um npm init nesse commons e altere o package.json criado para incluir uma instrução de privacidade em relação à publicação no NPM.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "name": "commons", "version": "1.0.0", "private": true, "description": "", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } |
Pronto. Acabamos de criar nosso módulo que vai ser compartilhado entre diferentes projetos.
O próximo passo, é criamos outro projeto (ou vários, se quiser), um api-a, colocar um index.js dentro dele e rodar um npm init dentro. Só para deixar tudo inicializado.
Agora, vi linha de comando, entre no projeto api-a e execute o seguinte comando.
1 2 3 |
npm link ../commons/ |
Isso irá criar um link simbólico do commons no node_modules do seu projeto api-a, ou seja, se você tiver um index.js como abaixo, ele vai funcionar como se o commons fosse um módulo instalado via NPM.
1 2 3 4 5 6 |
const math = require("commons/math"); console.log("API A"); const resultado = math(1,2); console.log(resultado); |
O problema do NPM LINK é produção. Como geralmente a gente publica os arquivos do projeto sem node_modules, você teria de incluir um script de recriação do link para que ele fosse recriado toda vez que publicar o projeto no servidor.
NODE_PATH
Outra forma de resolver este problema de compartilhamento de módulos entre diferentes projetos é setando a variável de ambiente NODE_PATH. Esta variável define caminho(s) que o Node.js irá procurar por dependências além da node_modules.
Ou seja, se você está usando um módulo que não está na node_modules e sim em um outro projeto compartilhado, basta setar via .env ou na execução da aplicação a variável NODE_PATH apontando para esta pasta de módulos compartilhados.
Abaixo, exemplo considerando que a commons está um nível acima do projeto api-a e que estamos na pasta do mesmo (sem arquivo .env).
1 2 3 |
NODE_PATH=../commons node index |
Isso vai fazer com que possamos chamar os módulos da commons pelo nome, sem usar caminho relativo e mesmo assim o Node.js vai achar eles.
Com arquivo .env fica ainda mais fácil né, porque é só configurar o NODE_PATH nele e carregá-lo na inicialização do projeto, sendo que recomendo fazer isso via linha de comando, como abaixo (considerando .env na raiz do projeto).
1 2 3 |
node -r dotenv/config ./index |
TypeScript
Agora, se você está usando TypeScript, nenhuma das alternativas anteriores irá funcionar. Pelo menos não facilmente. Isso porque o TypeScript verifica já em tempo de desenvolvimento os módulos e caminhos utilizados, o que gera erros de compilação que te impedem de executar a aplicação.
Para resolver isso no TypeScript exigirá uma série de passos adicionais.
O primeiro deles é instalar dois pacotes no seu projeto.
1 2 3 |
npm i ts-node tsconfig-paths |
Depois, vá no seu tsconfig.json e ajuste quatro configurações:
1 2 3 4 5 6 7 8 |
"outDir": "./dist/", "baseUrl": ".", "paths": { "commons/*": ["../commons/*"] }, "rootDirs": ["./", "../commons"], |
A primeira configuração é apenas a definição da pasta onde ficarão os .JS compilados a partir dos .TS, provavelmente você já tem esta configuração na sua aplicação TypeScript.
A segunda configuração diz a URL base para resolução de caminhos de arquivos e é pré-requisito para a segunda configuração, só por isso defini ela (usei . que quer dizer “pasta atual”).
A terceira configuração define aliases (apelidos) para os caminhos relativos que queremos mapear os nossos módulos compartilhados. Aqui estou dizendo que todos módulos que começarem com commons/ na verdade devem ser resolvidos para ../commons/ (ou seja, estão um nível acima em relação a URL base).
E por último, para que não dê erro em tempo de desenvolvimento, a configuração rootDirs diz pro TypeScript onde estão todos os arquivos .TS necessários para a correta interpretação (em tempo de dev) e compilação do projeto como um todo. Assim, além de olhar na pasta raiz do projeto, o TS vai olhar na pasta commons também.
Feito essas configurações, agora é hora de ajustar nossos scripts de inicialização no package.json, para que:
- o ts-node suba a aplicação ao invés do node normal;
- o tsconfig-paths resolva os caminhos dos módulos baseados nos aliases;
O seu package.json deve ficar semelhante a esse:
1 2 3 4 5 6 |
"scripts": { "start": "tsc && ts-node -r tsconfig-paths/register ./dist/index", "dev": "ts-node -r tsconfig-paths/register ./index" }, |
Assim, quando você rodar npm run dev, vai executar o seu projeto a partir do index.ts e quando rodar npm start, vai compilar tudo e rodar o seu projeto a partir do arquivo index.js (compilado).
Use o npm run dev em tempo de desenvolvimento e o npm start em produção.
Bônus 1: Next.js
Se você estiver usando a solução acima para um projeto Next.js carregar uma dependência de uma biblioteca TypeScript, terá problemas com mensagens do tipo “you may need an appropriate loader to handle this file type” apontando para alguma linha que importe ou exporte um módulo TypeScript da commons. Isso porque o Next.js não saberá que deve transpilar também os arquivos TS do seu projeto commons. Para que ele saiba, você deve ir no next.config.js e adicionar a seguinte linha:
1 2 3 |
transpilePackages: ['@commons/*'], |
Isso fará com que ele entenda que deve transpilar qualquer arquivo presente na commons também, passando a funcionar com a mesma solução acima.
Bônus 2: Arquivos Estáticos
Se na sua commons você tiver arquivos estáticos, como por exemplo arquivos JSON, e quiser que eles sejam copiados para a versão “dist”, ou seja, que vai ser executada, você terá de ter cuidados adicionais, a depender do tipo de arquivo.
No caso de arquivos JSON, além de colocar a configuração resolveJsonModule como true no seu tsconfig.json, basta que você realize os imports deles em seu projeto e o TS vai entender que eles devem ser copiados para a versão final/transpilada.
Agora se os seus arquivos estáticos não são JSON, ou até são, mas eles nunca são importados no código, você deverá providenciar algum script de cópia de arquivos no seu package.json, que deve ser acionado junto da compilação/build do projeto. Pode ser um simples cp -rf ou algum pacote npm de cópia de arquivos, existindo inclusive alguns para TS que você apenas instala e configura no tsconfig.json.
Espero que isso seja tão útil para você quanto foi pra mim, especialmente este último.
Um abraço e sucesso.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.