Les projets évoluent souvent vers des structures de répertoires complexes et imbriquées. Par conséquent, les chemins d'importation peuvent devenir plus longs et plus confus, ce qui peut affecter négativement l'apparence du code et rendre plus difficile la compréhension de l'origine du code importé.
L'utilisation d'alias de chemin peut résoudre le problème en permettant la définition d'importations relatives à des répertoires prédéfinis. Cette approche résout non seulement les problèmes de compréhension des chemins d'importation, mais elle simplifie également le processus de déplacement du code lors de la refactorisation.
// 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';
Il existe plusieurs bibliothèques disponibles pour configurer les alias de chemin dans Node.js, telles que alias-hq et tsconfig-paths . Cependant, en parcourant la documentation de Node.js, j'ai découvert un moyen de configurer des alias de chemin sans avoir à recourir à des bibliothèques tierces.
De plus, cette approche permet l'utilisation d'alias sans nécessiter l'étape de génération.
Dans cet article, nous discuterons des importations de sous-chemins Node.js et de la manière de configurer les alias de chemin en les utilisant. Nous explorerons également leur prise en charge dans l'écosystème frontal.
À partir de Node.js v12.19.0, les développeurs peuvent utiliser Subpath Imports pour déclarer des alias de chemin dans un package npm. Cela peut être fait via le champ imports
dans le fichier package.json
. Il n'est pas nécessaire de publier le package sur npm.
Créer un fichier package.json
dans n'importe quel répertoire suffit. Par conséquent, cette méthode convient également aux projets privés.
Voici un fait intéressant : Node.js a introduit la prise en charge du champ imports
en 2020 via la RFC appelée " Bare Module Specificer Resolution in node.js ". Alors que cette RFC est principalement reconnue pour le champ exports
, qui permet la déclaration des points d'entrée pour les packages npm, les champs exports
et imports
traitent des tâches complètement différentes, même s'ils ont des noms et une syntaxe similaires.
La prise en charge native des alias de chemin présente en théorie les avantages suivants :
J'ai essayé de configurer des alias de chemin dans mes projets et testé ces instructions dans la pratique.
Prenons l'exemple d'un projet avec la structure de répertoires suivante :
my-awesome-project ├── src/ │ ├── entities/ │ │ └── product/ │ │ └── components/ │ │ └── ProductView.js │ ├── features/ │ │ └── add-to-cart/ │ │ └── actions/ │ │ └── index.js │ └── shared/ │ └── api/ │ └── index.js └── package.json
Pour configurer les alias de chemin, vous pouvez ajouter quelques lignes à package.json
comme décrit dans la documentation. Par exemple, si vous souhaitez autoriser les importations relatives au répertoire src
, ajoutez le champ imports
suivant à package.json
:
{ "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Pour utiliser l'alias configuré, les importations peuvent être écrites comme ceci :
import { apiClient } from '#shared/api'; import { ProductView } from '#entities/product/components/ProductView'; import { addProductToCart } from '#features/add-to-cart/actions';
À partir de la phase de configuration, nous sommes confrontés à la première limitation : les entrées dans le champ imports
doivent commencer par le symbole #
. Cela garantit qu'ils sont distingués des spécificateurs de package tels que @
.
Je pense que cette limitation est utile car elle permet aux développeurs de déterminer rapidement quand un alias de chemin est utilisé dans une importation et où les configurations d'alias peuvent être trouvées.
Pour ajouter plus d'alias de chemin pour les modules couramment utilisés, le champ imports
peut être modifié comme suit :
{ "name": "my-awesome-project", "imports": { "#modules/*": "./path/to/modules/*", "#logger": "./src/shared/lib/logger.js", "#*": "./src/*" } }
Il serait idéal de conclure l'article avec la phrase "tout le reste fonctionnera comme prévu". Cependant, en réalité, si vous envisagez d'utiliser le champ imports
, vous pouvez rencontrer certaines difficultés.
Si vous envisagez d'utiliser des alias de chemin avec les modules CommonJS , j'ai une mauvaise nouvelle pour vous : le code suivant ne fonctionnera pas.
const { apiClient } = require('#shared/api'); const { ProductView } = require('#entities/product/components/ProductView'); const { addProductToCart } = require('#features/add-to-cart/actions');
Lorsque vous utilisez des alias de chemin dans Node.js, vous devez suivre les règles de résolution de module du monde ESM. Cela s'applique à la fois aux modules ES et aux modules CommonJS et entraîne deux nouvelles exigences qui doivent être satisfaites :
Il est nécessaire de spécifier le chemin d'accès complet à un fichier, y compris l'extension de fichier.
Il n'est pas autorisé de spécifier un chemin d'accès à un répertoire et de s'attendre à importer un fichier index.js
. Au lieu de cela, le chemin complet vers un fichier index.js
doit être spécifié.
Pour permettre à Node.js de résoudre correctement les modules, les importations doivent être corrigées comme suit :
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');
Ces limitations peuvent entraîner des problèmes lors de la configuration du champ imports
dans un projet comportant de nombreux modules CommonJS. Cependant, si vous utilisez déjà des modules ES, votre code répond à toutes les exigences.
De plus, si vous créez du code à l'aide d'un bundler, vous pouvez contourner ces limitations. Nous verrons comment procéder ci-dessous.
Pour résoudre correctement les modules importés pour la vérification de type, TypeScript doit prendre en charge le champ imports
. Cette fonctionnalité est prise en charge à partir de la version 4.8.1, mais uniquement si les limitations Node.js répertoriées ci-dessus sont remplies.
Pour utiliser le champ imports
pour la résolution de module, quelques options doivent être configurées dans le fichier 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" } }
Cette configuration permet au champ imports
de fonctionner de la même manière que dans Node.js. Cela signifie que si vous oubliez d'inclure une extension de fichier dans une importation de module, TypeScript générera une erreur vous en avertissant.
// 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';
Je ne voulais pas réécrire toutes les importations, car la plupart de mes projets utilisent un bundler pour générer du code, et je n'ajoute jamais d'extensions de fichiers lors de l'importation de modules. Pour contourner cette limitation, j'ai trouvé un moyen de configurer le projet comme suit :
{ "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" ] } }
Cette configuration permet la manière habituelle d'importer des modules sans avoir besoin de spécifier des extensions. Cela fonctionne même lorsqu'un chemin d'importation pointe vers un répertoire.
// 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';
Il nous reste un problème concernant l'importation à l'aide d'un chemin relatif. Ce problème n'est pas lié aux alias de chemin. TypeScript génère une erreur car nous avons configuré la résolution du module pour utiliser le mode nodenext
.
Heureusement, un nouveau mode de résolution de module a été ajouté dans la récente version de TypeScript 5.0 qui supprime la nécessité de spécifier le chemin complet dans les importations. Pour activer ce mode, quelques options doivent être configurées dans le fichier 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" } }
Une fois la configuration terminée, les importations de chemins relatifs fonctionneront comme d'habitude.
// 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';
Désormais, nous pouvons utiliser pleinement les alias de chemin via le champ imports
sans aucune limitation supplémentaire sur la manière d'écrire les chemins d'importation.
Lors de la construction du code source à l'aide du compilateur tsc
, une configuration supplémentaire peut être nécessaire. Une limitation de TypeScript est qu'un code ne peut pas être construit au format de module CommonJS lors de l'utilisation du champ imports
.
Par conséquent, le code doit être compilé au format ESM et le champ type
doit être ajouté à package.json
pour exécuter le code compilé dans Node.js.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": "./src/*" } }
Si votre code est compilé dans un répertoire séparé, tel que build/
, le module peut ne pas être trouvé par Node.js car l'alias de chemin pointerait vers l'emplacement d'origine, tel que src/
. Pour résoudre ce problème, des chemins d'importation conditionnels peuvent être utilisés dans le fichier package.json
.
Cela permet d'importer du code déjà construit depuis le répertoire build/
au lieu du répertoire src/
.
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
Pour utiliser une condition d'importation spécifique, Node.js doit être lancé avec l'indicateur --conditions
.
node --conditions=production build/index.js
Les bundlers de code utilisent généralement leur propre implémentation de résolution de module, plutôt que celle intégrée à Node.js. Par conséquent, il est important pour eux de mettre en œuvre un support pour le domaine imports
.
J'ai testé des alias de chemin avec Webpack, Rollup et Vite dans mes projets et je suis prêt à partager mes découvertes.
Voici la configuration d'alias de chemin que j'ai utilisée pour tester les bundlers. J'ai utilisé la même astuce que pour TypeScript pour éviter d'avoir à spécifier le chemin complet des fichiers à l'intérieur des importations.
{ "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 prend en charge le champ imports
à partir de la v5.0. Les alias de chemin fonctionnent sans aucune configuration supplémentaire. Voici la configuration Webpack que j'ai utilisée pour créer un projet de test avec 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;
La prise en charge du champ imports
a été ajoutée dans Vite version 4.2.0. Cependant, un bogue important a été corrigé dans la version 4.3.3, il est donc recommandé d'utiliser au moins cette version. Dans Vite, les alias de chemin fonctionnent sans nécessiter de configuration supplémentaire dans les modes dev
et build
.
Par conséquent, j'ai construit un projet de test avec une configuration complètement vide.
Bien que Rollup soit utilisé dans Vite, le champ imports
ne fonctionne pas immédiatement. Pour l'activer, vous devez installer le @rollup/plugin-node-resolve
version 11.1.0 ou supérieure. Voici un exemple de configuration :
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'], }), ], }, ];
Malheureusement, avec cette configuration, les alias de chemin ne fonctionnent que dans les limites de Node.js. Cela signifie que vous devez spécifier le chemin d'accès complet au fichier, y compris l'extension. La spécification d'un tableau dans le champ imports
ne contournera pas cette limitation, car Rollup n'utilise que le premier chemin du tableau.
Je pense qu'il est possible de résoudre ce problème en utilisant les plugins Rollup, mais je n'ai pas essayé de le faire car j'utilise principalement Rollup pour les petites bibliothèques. Dans mon cas, il était plus facile de réécrire les chemins d'importation tout au long du projet.
Les testeurs sont un autre groupe d'outils de développement qui dépendent fortement du mécanisme de résolution de module. Ils utilisent souvent leur propre implémentation de résolution de module, similaire aux bundlers de code. Par conséquent, il est possible que le champ imports
ne fonctionne pas comme prévu.
Heureusement, les outils que j'ai testés fonctionnent bien. J'ai testé les alias de chemin avec Jest v29.5.0 et Vite v0.30.1. Dans les deux cas, les alias de chemin ont fonctionné de manière transparente sans aucune configuration ou limitation supplémentaire. Jest prend en charge le champ imports
depuis la version v29.4.0.
Le niveau de support dans Vitest repose uniquement sur la version de Vite, qui doit être au moins v4.2.0.
Le champ imports
dans les bibliothèques populaires est actuellement bien pris en charge. Mais qu'en est-il des éditeurs de code ? J'ai testé la navigation dans le code, en particulier la fonction "Aller à la définition", dans un projet qui utilise des alias de chemin. Il s'avère que la prise en charge de cette fonctionnalité dans les éditeurs de code présente quelques problèmes.
En ce qui concerne VS Code, la version de TypeScript est cruciale. Le serveur de langage TypeScript est responsable de l'analyse et de la navigation dans le code JavaScript et TypeScript.
En fonction de vos paramètres, VS Code utilisera soit la version intégrée de TypeScript, soit celle installée dans votre projet.
J'ai testé la prise en charge des champs imports
dans VS Code v1.77.3 en combinaison avec TypeScript v5.0.4.
VS Code a les problèmes suivants avec les alias de chemin :
TypeScript n'utilise pas le champ imports
tant que le paramètre de résolution du module n'est pas défini sur nodenext
ou bundler
. Par conséquent, pour l'utiliser dans VS Code, vous devez spécifier la résolution du module dans votre projet.
IntelliSense ne prend actuellement pas en charge la suggestion de chemins d'importation à l'aide du champ imports
. Il y a un problème ouvert pour ce problème.
Pour contourner ces deux problèmes, vous pouvez répliquer une configuration d'alias de chemin dans le fichier tsconfig.json
. Si vous n'utilisez pas TypeScript, vous pouvez faire de même dans jsconfig.json
.
// tsconfig.json OR jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Depuis la version 2021.3 (j'ai testé en 2022.3.4), WebStorm prend en charge le champ imports
. Cette fonctionnalité fonctionne indépendamment de la version TypeScript, car WebStorm utilise son propre analyseur de code. Cependant, WebStorm présente un ensemble de problèmes distincts concernant la prise en charge des alias de chemin :
L'éditeur suit strictement les restrictions imposées par Node.js sur l'utilisation des alias de chemin. La navigation dans le code ne fonctionnera pas si l'extension de fichier n'est pas explicitement spécifiée. Il en va de même pour l'importation de répertoires avec un fichier index.js
.
WebStorm a un bogue qui empêche l'utilisation d'un tableau de chemins dans le champ imports
. Dans ce cas, la navigation dans le code cesse complètement de fonctionner.
{ "name": "my-awesome-project", // OK "imports": { "#*": "./src/*" }, // This breaks code navigation "imports": { "#*": ["./src/*", "./src/*.ts", "./src/*.tsx"] } }
Heureusement, nous pouvons utiliser la même astuce qui résout tous les problèmes de VS Code. Plus précisément, nous pouvons répliquer une configuration d'alias de chemin dans le fichier tsconfig.json
ou jsconfig.json
. Cela permet l'utilisation d'alias de chemin sans aucune limitation.
Sur la base de mes expériences et de mon expérience d'utilisation du champ imports
dans divers projets, j'ai identifié les meilleures configurations d'alias de chemin pour différents types de projets.
Cette configuration est destinée aux projets où le code source s'exécute dans Node.js sans nécessiter d'étapes de génération supplémentaires. Pour l'utiliser, suivez ces étapes :
Configurez le champ imports
dans un fichier package.json
. Une configuration très basique suffit dans ce cas.
Pour que la navigation dans le code fonctionne dans les éditeurs de code, il est nécessaire de configurer des alias de chemin dans un fichier jsconfig.json
.
// jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
Cette configuration doit être utilisée pour les projets où le code source est écrit en TypeScript et construit à l'aide du compilateur tsc
. Il est important de configurer les éléments suivants dans cette configuration :
Le champ imports
dans un fichier package.json
. Dans ce cas, il est nécessaire d'ajouter des alias de chemin conditionnels pour s'assurer que Node.js résout correctement le code compilé.
L'activation du format de package ESM dans un fichier package.json
est nécessaire car TypeScript ne peut compiler du code qu'au format ESM lors de l'utilisation du champ imports
.
Dans un fichier tsconfig.json
, définissez le format de module ESM et moduleResolution
. Cela permettra à TypeScript de suggérer des extensions de fichiers oubliées dans les importations. Si une extension de fichier n'est pas spécifiée, le code ne s'exécutera pas dans Node.js après la compilation.
Pour corriger la navigation dans le code dans les éditeurs de code, les alias de chemin doivent être répétés dans un fichier 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/*" } } }
Cette configuration est destinée aux projets où le code source est regroupé. TypeScript n'est pas requis dans ce cas. S'il n'est pas présent, tous les paramètres peuvent être définis dans un fichier jsconfig.json
.
La principale caractéristique de cette configuration est qu'elle vous permet de contourner les limitations de Node.js concernant la spécification des extensions de fichiers dans les importations.
Il est important de configurer les éléments suivants :
Configurez le champ imports
dans un fichier package.json
. Dans ce cas, vous devez ajouter un tableau de chemins à chaque alias. Cela permettra à un bundler de trouver le module importé sans avoir besoin de spécifier l'extension de fichier.
Pour corriger la navigation dans le code dans les éditeurs de code, vous devez répéter les alias de chemin dans un fichier 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" ] } }
La configuration des alias de chemin via le champ imports
présente à la fois des avantages et des inconvénients par rapport à la configuration via des bibliothèques tierces. Bien que cette approche soit prise en charge par des outils de développement communs (à partir d'avril 2023), elle présente également des limites.
Cette méthode offre les avantages suivants :
package.json
).
Il existe cependant des inconvénients temporaires qui seront éliminés au fur et à mesure de l'évolution des outils de développement :
imports
. Pour éviter ces problèmes, vous pouvez utiliser le fichier jsconfig.json
. Cependant, cela entraîne une duplication de la configuration des alias de chemin dans deux fichiers.
imports
prêt à l'emploi. Par exemple, Rollup nécessite l'installation de plugins supplémentaires.
imports
dans Node.js ajoute de nouvelles contraintes sur les chemins d'importation. Ces contraintes sont les mêmes que celles des modules ES, mais elles peuvent compliquer l'utilisation du champ imports
.
Alors, vaut-il la peine d'utiliser le champ imports
pour configurer les alias de chemin ? Je pense que pour les nouveaux projets, oui, cette méthode vaut la peine d'être utilisée à la place des bibliothèques tierces.
Le champ imports
a de bonnes chances de devenir un moyen standard de configurer les alias de chemin pour de nombreux développeurs dans les années à venir, car il offre des avantages significatifs par rapport aux méthodes de configuration traditionnelles.
Cependant, si vous avez déjà un projet avec des alias de chemin configurés, le passage au champ imports
n'apportera pas d'avantages significatifs.
J'espère que vous avez appris quelque chose de nouveau grâce à cet article. Merci pour la lecture!
Également publié ici