paint-brush
Como configurar aliases de caminho em projetos de front-end da maneira nativapor@nodge
8,441 leituras
8,441 leituras

Como configurar aliases de caminho em projetos de front-end da maneira nativa

por Maksim Zemskov20m2023/05/04
Read on Terminal Reader

Muito longo; Para ler

O campo de importações tem uma boa chance de se tornar uma maneira padrão de configurar aliases de caminho para muitos desenvolvedores nos próximos anos. Oferece vantagens significativas em comparação com os métodos de configuração tradicionais e já é suportado por ferramentas de desenvolvimento comuns (a partir de abril de 2023). No entanto, ele também possui algumas limitações que podem ser mitigadas seguindo as práticas de configuração recomendadas.
featured image - Como configurar aliases de caminho em projetos de front-end da maneira nativa
Maksim Zemskov HackerNoon profile picture
0-item
1-item

Sobre Aliases de Caminho

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.

O Campo de Importações

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:


  • Não há necessidade de instalar nenhuma biblioteca de terceiros.


  • Não há necessidade de pré-compilar ou processar importações em tempo real para executar o código.


  • Aliases são suportados por qualquer ferramenta baseada em Node.js que usa o mecanismo de resolução de importação padrão.


  • A navegação de código e o preenchimento automático devem funcionar em editores de código sem exigir nenhuma configuração extra.


Tentei configurar path aliases em meus projetos e testei essas instruções na prática.

Configurando Aliases de Caminho

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.

Limitações do Node.js

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:


  1. É necessário especificar o caminho completo para um arquivo, incluindo a extensão do arquivo.


  2. 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.

Suporte para importações de subcaminho no TypeScript

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.

Construindo código com TypeScript

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

Suporte para importações de subcaminho em empacotadores de código

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" ] } }


Webpack

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;


vite

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.

Rolar

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.

Suporte para importações de subcaminho em executores de teste

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.

Suporte para importações de subcaminho em editores de código

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.

Código VS

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:


  1. 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.


  2. 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/*" } }


WebStorm

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:


  1. 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 .


  2. 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.

Configuração recomendada

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.

Sem TypeScript ou um Bundler

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:


  1. Configure o campo imports em um arquivo package.json . Uma configuração muito básica é suficiente neste caso.


  2. 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/*" } }


Construindo Código Usando TypeScript

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:


  1. 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.


  2. 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 .


  3. 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.


  4. 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/*" } } }


Construindo Código Usando um Bundler

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:


  1. 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.


  2. 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" ] } }


Conclusão

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:

  • Capacidade de usar aliases de caminho sem a necessidade de compilar ou transpilar código "on the fly".


  • As ferramentas de desenvolvimento mais populares oferecem suporte a aliases de caminho sem qualquer configuração adicional. Isso foi confirmado no Webpack, Vite, Jest e Vitest.


  • Essa abordagem promove a configuração de aliases de caminho em um local previsível (arquivo package.json ).


  • A configuração de aliases de caminho não requer a instalação de bibliotecas de terceiros.


Existem, no entanto, desvantagens temporárias que serão eliminadas à medida que as ferramentas de desenvolvimento evoluírem:


  • Mesmo os editores de código populares têm problemas com o suporte ao campo 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.


  • Algumas ferramentas de desenvolvimento podem não funcionar com o campo imports pronto para uso. Por exemplo, o Rollup requer a instalação de plugins adicionais.


  • O uso do campo 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 .


  • As restrições do Node.js podem resultar em diferenças na implementação entre o Node.js e outras ferramentas de desenvolvimento. Por exemplo, empacotadores de código podem ignorar as restrições do Node.js. Às vezes, essas diferenças podem complicar a configuração, especialmente ao configurar o TypeScript.


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!

Links Úteis

  1. RFC para implementação de exportações e importações
  2. Um conjunto de testes para entender melhor os recursos do campo de importações
  3. Documentação sobre o campo de importações em Node.js
  4. Limitações do Node.js em caminhos de importação em módulos ES

Também publicado aqui