Os projetos geralmente evoluem para estruturas de diretórios complexas e aninhadas. Como resultado, os caminhos de importação podem se tornar mais longos e confusos, o que pode afetar negativamente a aparência do código e dificultar a compreensão da origem do código importado.
O uso de path aliases pode resolver o problema ao permitir a definição de importações relativas a diretórios predefinidos. Essa abordagem não apenas resolve problemas com a compreensão dos caminhos de importação, mas também simplifica o processo de movimentação do código durante a refatoração.
// Without Aliases import { apiClient } from '../../../../shared/api'; import { ProductView } from '../../../../entities/product/components/ProductView'; import { addProductToCart } from '../../../add-to-cart/actions'; // With Aliases import { apiClient } from '#shared/api'; import { ProductView } from '#entities/product/components/ProductView'; import { addProductToCart } from '#features/add-to-cart/actions';
Existem várias bibliotecas disponíveis para configurar aliases de caminho em Node.js, como alias-hq e tsconfig-paths . No entanto, ao examinar a documentação do Node.js, descobri uma maneira de configurar aliases de caminho sem depender de bibliotecas de terceiros.
Além disso, essa abordagem permite o uso de aliases sem exigir a etapa de construção.
Neste artigo, discutiremos as importações de subcaminho do Node.js e como configurar aliases de caminho usando-as. Também exploraremos seu suporte no ecossistema front-end.
A partir do Node.js v12.19.0, os desenvolvedores podem usar Subpath Imports para declarar aliases de caminho em um pacote npm. Isso pode ser feito por meio do campo imports
no arquivo package.json
. Não é necessário publicar o pacote no npm.
Basta criar um arquivo package.json
em qualquer diretório. Portanto, este método também é adequado para projetos privados.
Aqui está um fato interessante: o Node.js introduziu o suporte para o campo imports
em 2020 por meio do RFC chamado " Resolução do especificador de módulo básico no node.js ". Embora este RFC seja reconhecido principalmente pelo campo exports
, que permite a declaração de pontos de entrada para pacotes npm, os campos exports
e imports
tratam de tarefas completamente diferentes, embora tenham nomes e sintaxe semelhantes.
O suporte nativo para aliases de caminho tem as seguintes vantagens em teoria:
Tentei configurar path aliases em meus projetos e testei essas instruções na prática.
Como exemplo, vamos considerar um projeto com a seguinte estrutura de diretórios:
my-awesome-project ├── src/ │ ├── entities/ │ │ └── product/ │ │ └── components/ │ │ └── ProductView.js │ ├── features/ │ │ └── add-to-cart/ │ │ └── actions/ │ │ └── index.js │ └── shared/ │ └── api/ │ └── index.js └── package.json
Para configurar aliases de caminho, você pode adicionar algumas linhas a package.json
conforme descrito na documentação. Por exemplo, se você quiser permitir importações relativas ao diretório src
, adicione o seguinte campo imports
a package.json
:
{ "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Para usar o alias configurado, as importações podem ser escritas assim:
import { apiClient } from '#shared/api'; import { ProductView } from '#entities/product/components/ProductView'; import { addProductToCart } from '#features/add-to-cart/actions';
A partir da fase de configuração, nos deparamos com a primeira limitação: as entradas no campo imports
devem começar com o símbolo #
. Isso garante que eles sejam diferenciados dos especificadores de pacote como @
.
Acredito que essa limitação seja útil porque permite que os desenvolvedores determinem rapidamente quando um alias de caminho é usado em uma importação e onde as configurações de alias podem ser encontradas.
Para adicionar mais aliases de caminho para módulos comumente usados, o campo imports
pode ser modificado da seguinte forma:
{ "name": "my-awesome-project", "imports": { "#modules/*": "./path/to/modules/*", "#logger": "./src/shared/lib/logger.js", "#*": "./src/*" } }
Seria ideal concluir o artigo com a frase "todo o resto funcionará imediatamente". No entanto, na realidade, se você planeja usar o campo imports
, poderá enfrentar algumas dificuldades.
Se você planeja usar aliases de caminho com módulos CommonJS , tenho más notícias para você: o código a seguir não funcionará.
const { apiClient } = require('#shared/api'); const { ProductView } = require('#entities/product/components/ProductView'); const { addProductToCart } = require('#features/add-to-cart/actions');
Ao usar aliases de caminho no Node.js, você deve seguir as regras de resolução de módulo do mundo ESM. Isso se aplica tanto aos módulos ES quanto aos módulos CommonJS e resulta em dois novos requisitos que devem ser atendidos:
É necessário especificar o caminho completo para um arquivo, incluindo a extensão do arquivo.
Não é permitido especificar um caminho para um diretório e esperar importar um arquivo index.js
. Em vez disso, o caminho completo para um arquivo index.js
precisa ser especificado.
Para permitir que o Node.js resolva os módulos corretamente, as importações devem ser corrigidas da seguinte forma:
const { apiClient } = require('#shared/api/index.js'); const { ProductView } = require('#entities/product/components/ProductView.js'); const { addProductToCart } = require('#features/add-to-cart/actions/index.js');
Essas limitações podem levar a problemas ao configurar o campo imports
em um projeto que possui muitos módulos CommonJS. No entanto, se você já estiver usando módulos ES, seu código atenderá a todos os requisitos.
Além disso, se você estiver criando código usando um bundler, poderá ignorar essas limitações. Discutiremos como fazer isso abaixo.
Para resolver adequadamente os módulos importados para verificação de tipo, o TypeScript precisa oferecer suporte ao campo imports
. Este recurso é suportado a partir da versão 4.8.1, mas somente se as limitações do Node.js listadas acima forem atendidas.
Para usar o campo imports
para resolução do módulo, algumas opções devem ser configuradas no arquivo tsconfig.json
.
{ "compilerOptions": { /* Specify what module code is generated. */ "module": "esnext", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "nodenext" } }
Essa configuração permite que o campo imports
funcione da mesma forma que no Node.js. Isso significa que, se você esquecer de incluir uma extensão de arquivo na importação de um módulo, o TypeScript gerará um erro avisando sobre isso.
// OK import { apiClient } from '#shared/api/index.js'; // Error: Cannot find module '#src/shared/api/index' or its corresponding type declarations. import { apiClient } from '#shared/api/index'; // Error: Cannot find module '#src/shared/api' or its corresponding type declarations. import { apiClient } from '#shared/api'; // Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'? import { foo } from './relative';
Não queria reescrever todas as importações, pois a maioria dos meus projetos usa um bundler para criar o código e nunca adiciono extensões de arquivo ao importar módulos. Para contornar essa limitação, encontrei uma forma de configurar o projeto da seguinte forma:
{ "name": "my-awesome-project", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
Essa configuração permite a maneira usual de importar módulos sem a necessidade de especificar extensões. Isso funciona até mesmo quando um caminho de importação aponta para um diretório.
// OK import { apiClient } from '#shared/api/index.js'; // OK import { apiClient } from '#shared/api/index'; // OK import { apiClient } from '#shared/api'; // Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'? import { foo } from './relative';
Temos um problema restante que diz respeito à importação usando um caminho relativo. Esse problema não está relacionado a aliases de caminho. O TypeScript lança um erro porque configuramos a resolução do módulo para usar o modo nodenext
.
Felizmente, um novo modo de resolução de módulo foi adicionado na versão recente do TypeScript 5.0 que elimina a necessidade de especificar o caminho completo dentro das importações. Para ativar este modo, algumas opções devem ser configuradas no arquivo tsconfig.json
.
{ "compilerOptions": { /* Specify what module code is generated. */ "module": "esnext", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "bundler" } }
Depois de concluir a configuração, as importações de caminhos relativos funcionarão normalmente.
// OK import { apiClient } from '#shared/api/index.js'; // OK import { apiClient } from '#shared/api/index'; // OK import { apiClient } from '#shared/api'; // OK import { foo } from './relative';
Agora, podemos utilizar totalmente os aliases de caminho por meio do campo imports
sem quaisquer limitações adicionais sobre como escrever caminhos de importação.
Ao criar o código-fonte usando o compilador tsc
, configurações adicionais podem ser necessárias. Uma limitação do TypeScript é que um código não pode ser criado no formato do módulo CommonJS ao usar o campo imports
.
Portanto, o código deve ser compilado no formato ESM e o campo type
deve ser adicionado ao package.json
para executar o código compilado no Node.js.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": "./src/*" } }
Se seu código for compilado em um diretório separado, como build/
, o módulo pode não ser encontrado pelo Node.js porque o alias de caminho apontaria para o local original, como src/
. Para resolver esse problema, os caminhos de importação condicional podem ser usados no arquivo package.json
.
Isso permite que o código já construído seja importado do diretório build/
em vez do diretório src/
.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
Para usar uma condição de importação específica, o Node.js deve ser iniciado com o sinalizador --conditions
.
node --conditions=production build/index.js
Os empacotadores de código geralmente usam sua própria implementação de resolução de módulo, em vez da incorporada ao Node.js. Portanto, é importante que eles implementem suporte para o campo imports
.
Testei path aliases com Webpack, Rollup e Vite em meus projetos e estou pronto para compartilhar minhas descobertas.
Aqui está a configuração do alias de caminho que usei para testar os bundlers. Usei o mesmo truque do TypeScript para evitar a necessidade de especificar o caminho completo para os arquivos dentro das importações.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
O Webpack oferece suporte ao campo imports
a partir da v5.0. Os aliases de caminho funcionam sem nenhuma configuração adicional. Aqui está a configuração do Webpack que usei para criar um projeto de teste com TypeScript:
const config = { mode: 'development', devtool: false, entry: './src/index.ts', module: { rules: [ { test: /\.tsx?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-typescript'], }, }, }, ], }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, }; export default config;
O suporte para o campo imports
foi adicionado no Vite versão 4.2.0. No entanto, um bug importante foi corrigido na versão 4.3.3, por isso é recomendável usar pelo menos esta versão. No Vite, os aliases de caminho funcionam sem a necessidade de configuração adicional nos modos dev
e build
.
Portanto, construí um projeto de teste com uma configuração completamente vazia.
Embora o Rollup seja usado dentro do Vite, o campo imports
não funciona imediatamente. Para habilitá-lo, você precisa instalar o @rollup/plugin-node-resolve
versão 11.1.0 ou superior. Aqui está um exemplo de configuração:
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { babel } from '@rollup/plugin-babel'; export default [ { input: 'src/index.ts', output: { name: 'mylib', file: 'build.js', format: 'es', }, plugins: [ nodeResolve({ extensions: ['.ts', '.tsx', '.js', '.jsx'], }), babel({ presets: ['@babel/preset-typescript'], extensions: ['.ts', '.tsx', '.js', '.jsx'], }), ], }, ];
Infelizmente, com essa configuração, os aliases de caminho funcionam apenas dentro das limitações do Node.js. Isso significa que você deve especificar o caminho completo do arquivo, incluindo a extensão. Especificar uma matriz dentro do campo imports
não ignorará essa limitação, pois o Rollup usa apenas o primeiro caminho na matriz.
Acredito que seja possível resolver esse problema usando plugins Rollup, mas não tentei fazer isso porque uso principalmente o Rollup para pequenas bibliotecas. No meu caso, foi mais fácil reescrever os caminhos de importação ao longo do projeto.
Os executores de teste são outro grupo de ferramentas de desenvolvimento que dependem fortemente do mecanismo de resolução do módulo. Eles costumam usar sua própria implementação de resolução de módulo, semelhante aos empacotadores de código. Como resultado, há uma chance de que o campo imports
não funcione conforme o esperado.
Felizmente, as ferramentas que testei funcionam bem. Eu testei aliases de caminho com Jest v29.5.0 e Vite v0.30.1. Em ambos os casos, os aliases de caminho funcionaram perfeitamente sem nenhuma configuração ou limitação adicional. Jest tem suporte para o campo imports
desde a versão v29.4.0.
O nível de suporte no Vitest depende exclusivamente da versão do Vite, que deve ser pelo menos v4.2.0.
O campo imports
em bibliotecas populares é atualmente bem suportado. No entanto, e os editores de código? Testei a navegação de código, especificamente a função "Ir para definição", em um projeto que usa aliases de caminho. Acontece que o suporte para esse recurso em editores de código tem alguns problemas.
Quando se trata de VS Code, a versão do TypeScript é crucial. O TypeScript Language Server é responsável por analisar e navegar pelo código JavaScript e TypeScript.
Dependendo de suas configurações, o VS Code usará a versão interna do TypeScript ou a instalada em seu projeto.
Testei o suporte de campo imports
no VS Code v1.77.3 em combinação com TypeScript v5.0.4.
O VS Code tem os seguintes problemas com aliases de caminho:
O TypeScript não usa o campo imports
até que a configuração de resolução do módulo seja definida como nodenext
ou bundler
. Portanto, para utilizá-lo no VS Code, você precisa especificar a resolução do módulo em seu projeto.
Atualmente, o IntelliSense não oferece suporte à sugestão de caminhos de importação usando o campo imports
. Há uma questão em aberto para este problema.
Para ignorar ambos os problemas, você pode replicar uma configuração de alias de caminho no arquivo tsconfig.json
. Se você não estiver usando TypeScript, poderá fazer o mesmo em jsconfig.json
.
// tsconfig.json OR jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Desde a versão 2021.3 (testei na 2022.3.4), o WebStorm suporta o campo imports
. Esse recurso funciona independentemente da versão do TypeScript, pois o WebStorm usa seu próprio analisador de código. No entanto, o WebStorm tem um conjunto separado de problemas em relação ao suporte a aliases de caminho:
O editor segue rigorosamente as restrições impostas pelo Node.js sobre o uso de path aliases. A navegação de código não funcionará se a extensão do arquivo não for explicitamente especificada. O mesmo se aplica à importação de diretórios com um arquivo index.js
.
O WebStorm tem um bug que impede o uso de uma matriz de caminhos dentro do campo imports
. Nesse caso, a navegação de código para de funcionar completamente.
{ "name": "my-awesome-project", // OK "imports": { "#*": "./src/*" }, // This breaks code navigation "imports": { "#*": ["./src/*", "./src/*.ts", "./src/*.tsx"] } }
Felizmente, podemos usar o mesmo truque que resolve todos os problemas do VS Code. Especificamente, podemos replicar uma configuração de alias de caminho no arquivo tsconfig.json
ou jsconfig.json
. Isso permite o uso de aliases de caminho sem quaisquer limitações.
Com base em meus experimentos e experiência usando o campo imports
em vários projetos, identifiquei as melhores configurações de alias de caminho para diferentes tipos de projetos.
Essa configuração destina-se a projetos em que o código-fonte é executado em Node.js sem a necessidade de etapas de compilação adicionais. Para usá-lo, siga estas etapas:
Configure o campo imports
em um arquivo package.json
. Uma configuração muito básica é suficiente neste caso.
Para que a navegação de código funcione em editores de código, é necessário configurar aliases de caminho em um arquivo jsconfig.json
.
// jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Essa configuração deve ser usada para projetos em que o código-fonte é escrito em TypeScript e construído usando o compilador tsc
. É importante configurar o seguinte nesta configuração:
O campo imports
em um arquivo package.json
. Nesse caso, é necessário adicionar aliases de caminho condicional para garantir que o Node.js resolva corretamente o código compilado.
A habilitação do formato de pacote ESM em um arquivo package.json
é necessária porque o TypeScript só pode compilar código no formato ESM ao usar o campo imports
.
Em um arquivo tsconfig.json
, defina o formato do módulo ESM e moduleResolution
. Isso permitirá que o TypeScript sugira extensões de arquivo esquecidas nas importações. Se uma extensão de arquivo não for especificada, o código não será executado no Node.js após a compilação.
Para corrigir a navegação de código em editores de código, os aliases de caminho devem ser repetidos em um arquivo tsconfig.json
.
// tsconfig.json { "compilerOptions": { "module": "esnext", "moduleResolution": "nodenext", "baseUrl": "./", "paths": { "#*": ["./src/*"] }, "outDir": "./build" } } // package.json { "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
Essa configuração destina-se a projetos nos quais o código-fonte é agrupado. TypeScript não é necessário neste caso. Se não estiver presente, todas as configurações podem ser definidas em um arquivo jsconfig.json
.
A principal característica dessa configuração é que ela permite ignorar as limitações do Node.js em relação à especificação de extensões de arquivo nas importações.
É importante configurar o seguinte:
Configure o campo imports
em um arquivo package.json
. Nesse caso, você precisa adicionar uma matriz de caminhos para cada alias. Isso permitirá que um empacotador localize o módulo importado sem exigir que a extensão do arquivo seja especificada.
Para corrigir a navegação de código em editores de código, você precisa repetir aliases de caminho em um arquivo tsconfig.json
ou jsconfig.json
.
// tsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
A configuração de aliases de caminho por meio do campo imports
tem prós e contras em comparação com a configuração por meio de bibliotecas de terceiros. Embora essa abordagem seja suportada por ferramentas de desenvolvimento comuns (a partir de abril de 2023), ela também tem limitações.
Este método oferece os seguintes benefícios:
package.json
).
Existem, no entanto, desvantagens temporárias que serão eliminadas à medida que as ferramentas de desenvolvimento evoluírem:
imports
. Para evitar esses problemas, você pode usar o arquivo jsconfig.json
. No entanto, isso leva à duplicação da configuração do alias de caminho em dois arquivos.
imports
pronto para uso. Por exemplo, o Rollup requer a instalação de plugins adicionais.
imports
no Node.js adiciona novas restrições aos caminhos de importação. Essas restrições são as mesmas dos módulos ES, mas podem dificultar o início do uso do campo imports
.
Então, vale a pena usar o campo imports
para configurar aliases de caminho? Acredito que para novos projetos sim, vale a pena usar esse método ao invés de bibliotecas de terceiros.
O campo imports
tem uma boa chance de se tornar uma maneira padrão de configurar aliases de caminho para muitos desenvolvedores nos próximos anos, pois oferece vantagens significativas em comparação com os métodos de configuração tradicionais.
No entanto, se você já tiver um projeto com aliases de caminho configurados, mudar para o campo imports
não trará benefícios significativos.
Espero que você tenha aprendido algo novo com este artigo. Obrigado por ler!
Também publicado aqui