Résolution du module

Références

L'actualité

Librairie

L'information

Résolution du module


Remarque : Cette section suppose des connaissances de base sur les modules. Veuillez consulter la documentation sur les modules pour plus d'informations.

La résolution de module est le processus utilisé par le compilateur pour déterminer à quoi une importation fait référence. Considérons une déclaration d'importation telle que import { a } from "moduleA"; afin de vérifier toute utilisation de a, le compilateur doit savoir exactement ce qu'il représente et devra en vérifier la définition de moduleA.

À ce stade, le compilateur demandera "quelle est la forme de moduleA ?". Cela peut sembler simple, mais moduleA peut être défini dans l'un de vos propres fichiers .ts ou .tsx ou dans un fichier .d.ts dont dépend votre code.

Tout d'abord, le compilateur essaiera de localiser un fichier qui représente le module importé. Pour ce faire, le compilateur suit l'une des deux stratégies suivantes: classique ou noeud. Ces stratégies indiquent au compilateur où chercher moduleA.

Si cela ne fonctionne pas et si le nom du module n'est pas relatif (et dans le cas de "moduleA", c'est le cas), le compilateur tentera de localiser une déclaration de module ambiante. Nous couvrirons ensuite les importations non relatives.

Enfin, si le compilateur n'a pas pu résoudre le module, il enregistre une erreur. Dans ce cas, l'erreur serait quelque chose comme error TS2307 : Cannot find module 'moduleA'.

Importations de modules relatives ou non relatives

Les importations de modules sont résolues différemment selon que la référence de module est relative ou non relative.

Une importation relative est celle qui commence par /, ./ ou ../. Quelques exemples incluent :

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

Toute autre importation est considérée comme non relative. Quelques exemples incluent :

  • import * as $ from "jquery";
  • import { Component } from "@angular/core";

Une importation relative est résolue par rapport au fichier d'importation et ne peut pas être résolue en une déclaration de module ambiante. Vous devez utiliser des importations relatives pour vos propres modules afin de garantir leur emplacement relatif au moment de l'exécution.

Une importation non relative peut être résolue par rapport à baseUrl, ou par le biais d'un mappage de chemin, que nous couvrirons ci-dessous. Ils peuvent également résoudre des déclarations de module ambiant. Utilisez des chemins non relatifs lors de l'importation de vos dépendances externes.

Stratégies de résolution de module

Il existe deux stratégies de résolution de module possibles: Noeud et Classic. Vous pouvez utiliser le drapeau --moduleResolution pour spécifier la stratégie de résolution du module. Si non spécifié, la valeur par défaut est classique pour --module AMD | System | ES2015 ou noeud autrement.

Classique

C'était la stratégie de résolution par défaut de TypeScript. De nos jours, cette stratégie est principalement présente pour la compatibilité ascendante.

Une importation relative sera résolue par rapport au fichier d'importation. Ainsi, import { b } from "./moduleB" dans le fichier source, /root/src/folder/A.ts les recherches suivantes seront effectuées:

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts
Pour les importations de modules non relatives, cependant, le compilateur parcourt l'arborescence en commençant par le répertoire contenant le fichier d'importation, en essayant de localiser un fichier de définition correspondant.

Par exemple :

Une importation non relative vers moduleB tel que import { b } from "moduleB", par exemple, dans un fichier source /root/src/folder/A.ts, entraî nerait une tentative de localisation des emplacements suivants "moduleB" :

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts
  • /root/src/moduleB.ts
  • /root/src/moduleB.d.ts
  • /root/moduleB.ts
  • /root/moduleB.d.ts
  • /moduleB.ts
  • /moduleB.d.ts

Noeud

Cette stratégie de résolution tente d'imiter le mécanisme de résolution du module Node.js lors de l'exécution. Le plein algorithme de résolution Node.js est décrit dans la documentation du module Node.js.

Comment Node.js résout les modules

Pour comprendre les étapes que le compilateur TS, il est important de faire la lumière sur les modules Node.js. Traditionnellement, les importations dans Node.js sont effectuées en appelant une fonction nommée require. Le comportement que prend Node.js diffère selon que l'on require l'attribue un chemin relatif ou non.

Les chemins relatifs sont assez simples. A titre d'exemple, considérons un fichier situé à l'emplacement /root/src/moduleA.js qui contient l'importation. var x = require("./moduleB");, Node.js résout cette importation dans l'ordre suivant :

Demandez le fichier nommé /root/src/moduleB.js, s'il existe.

Demandez au dossier /root/src/moduleB s'il contient un fichier nommé package.js en spécifiant un module "main". Dans notre exemple, si Node.js a trouvé le fichier /root/src/moduleB/package.js concontenant { "main": "lib/mainModule.js" }, alors Node.js se référera à /root/src/moduleB/lib/mainModule.js.

Demandez au dossier /root/src/moduleB s'il contient un fichier nommé index.js. Ce fichier est implicitement considéré comme le module "principal" de ce dossier.

Vous pouvez en savoir plus à ce sujet dans la documentation de Node.js sur les modules de fichiers et les modules de dossiers.

Cependant, la résolution d'un nom de module non relatif est effectuée différemment. Node cherchera vos modules dans des dossiers spéciaux nommés node_modules. Un dossier node_modules peut être au même niveau que le fichier actuel ou plus haut dans la chaî ne de répertoires. Node va parcourir la chaî ne de répertoires, en parcourant chaque répertoire node_modules jusqu'à trouver le module que vous avez essayé de charger.

En reprenant notre exemple ci-dessus, demandez-vous si vous /root/src/moduleA.js avez plutôt utilisé un chemin non relatif et importé var x = require("moduleB");. Le noeud essaierait alors de résoudre moduleB avec chacun des emplacements jusqu'à ce que l'un d'entre eux travaille.

  • /root/src/node_modules/moduleB.js
  • /root/src/node_modules/moduleB/package.json (s'il spécifie une "main"propriété)
  • /root/src/node_modules/moduleB/index.js

  • /root/node_modules/moduleB.js
  • /root/node_modules/moduleB/package.json (s'il spécifie une "main"propriété)
  • /root/node_modules/moduleB/index.js

  • /node_modules/moduleB.js
  • /node_modules/moduleB/package.json (s'il spécifie une "main"propriété)
  • /node_modules/moduleB/index.js

Notez que Node.js a sauté un répertoire dans les étapes (4) et (7).

Vous pouvez en savoir plus sur le processus dans la documentation de Node.js sur le chargement de modules à partir de node_modules.

Comment TypeScript résout les modules

TypeScript imitera la stratégie de résolution d'exécution Node.js afin de localiser les fichiers de définition des modules au moment de la compilation. Pour ce faire, il recouvre les extensions de fichier source dactylographiée ( .ts, .tsxet .d.ts) sur la logique de la résolution du noeud. TypeScript utilisera également un champ dans package.json, named "types" pour refléter son objectif "main". Le compilateur l'utilisera pour trouver le fichier de définition "principal" à consulter.

Par exemple, une instruction d'importation telle que import { b } from "./moduleB" in /root/src/moduleA.ts entraînerait la tentative de localisation des emplacements suivants "./moduleB" :

  • /root/src/moduleB.ts
  • /root/src/moduleB.tsx
  • /root/src/moduleB.d.ts
  • /root/src/moduleB/package.json (s'il spécifie une "types"propriété)
  • /root/src/moduleB/index.ts
  • /root/src/moduleB/index.tsx
  • /root/src/moduleB/index.d.ts

Rappelez-vous que Node.js a recherché un fichier nommé moduleB.js, puis un applicable package.json, puis un index.js.

De la même manière, une importation non relative suivra la logique de résolution de Node.js, recherchant d'abord un fichier, puis un dossier applicable. Ainsi, import { b } from "moduleB" dans le fichier source, /root/src/moduleA.ts les recherches suivantes seront effectuées:

  • /root/src/node_modules/moduleB.ts
  • /root/src/node_modules/moduleB.tsx
  • /root/src/node_modules/moduleB.d.ts
  • /root/src/node_modules/moduleB/package.json (s'il spécifie une "types"propriété)
  • /root/src/node_modules/@types/moduleB.d.ts
  • /root/src/node_modules/moduleB/index.ts
  • /root/src/node_modules/moduleB/index.tsx
  • /root/src/node_modules/moduleB/index.d.ts

  • /root/node_modules/moduleB.ts
  • /root/node_modules/moduleB.tsx
  • /root/node_modules/moduleB.d.ts
  • /root/node_modules/moduleB/package.json (s'il spécifie une "types"propriété)
  • /root/node_modules/@types/moduleB.d.ts
  • /root/node_modules/moduleB/index.ts
  • /root/node_modules/moduleB/index.tsx
  • /root/node_modules/moduleB/index.d.ts

  • /node_modules/moduleB.ts
  • /node_modules/moduleB.tsx
  • /node_modules/moduleB.d.ts
  • /node_modules/moduleB/package.json (s'il spécifie une "types"propriété)
  • /node_modules/@types/moduleB.d.ts
  • /node_modules/moduleB/index.ts
  • /node_modules/moduleB/index.tsx
  • /node_modules/moduleB/index.d.ts

Ne vous laissez pas intimider par le nombre d'étapes indiquées; TypeScript ne fait que sauter des répertoires deux fois aux étapes (9) et (17). Ce n'est vraiment pas plus complexe ce que Node.js fait lui-même.

Drapeaux de résolution de module supplémentaires

Une mise en page source du projet ne correspond parfois pas à celle de la sortie. Généralement, un ensemble d'étapes de construction génère la sortie finale. Celles-ci incluent la compilation de v .ts et la copie .js de dépendances depuis différents emplacements source vers un seul emplacement de sortie. Le résultat n'est que les modules au moment de l'exécution peuvent avoir des noms différents de ceux des fichiers source contenant leurs définitions. Ou bien les chemins de module dans la sortie finale peuvent ne pas correspondre à leurs chemins de fichier source correspondants au moment de la compilation.

Le compilateur TypeScript dispose d'un ensemble d'indicateurs supplémentaires pour informer le compilateur des transformations attendues des sources afin de générer la sortie finale.

Il est important de noter que le compilateur ne réaliser ces transformations; il utilise simplement ces informations pour guider le processus de résolution d'une importation de module dans son fichier de définition.

URL de base

L'utilisation de a baseUrl est une pratique courante dans les applications utilisant des chargeurs de modules AMD dans lesquels les modules sont "déployés" dans un seul dossier au moment de l'exécution. Les sources de ces modules peuvent vivre dans des répertoires différents, mais un script de construction les mettra tous ensemble.

Ce paramètre baseUrl indique au compilateur où trouver les modules. Toutes les importations de modules avec des noms non relatifs sont supposées être relatives à baseUrl.

La valeur de baseUrl est déterminée comme suit :

  • valeur de l'argument de ligne de commande baseUrl (si le chemin d'accès est relatif, il est calculé en fonction du répertoire en cours)
  • valeur de la propriété baseUrl dans 'tsconfig.json' (si le chemin indiqué est relatif, il est calculé en fonction de l'emplacement de 'tsconfig.json')

Notez que les importations de modules relatifs ne sont pas affectées par la définition de baseUrl, car elles sont toujours résolues par rapport à leurs fichiers d'importation.

Vous trouverez plus de documentation sur baseUrl dans les documentations RequireJS et SystemJS.

Mappage de chemin

Parfois, les modules ne sont pas directement situés sous baseUrl. Par exemple, une importation dans un module "jquery" serait traduite lors de l'exécution en "node_modules/jquery/dist/jquery.slim.min.js". Les chargeurs utilisent une configuration de mappage pour mapper les noms de module sur des fichiers au moment de l'exécution, voir Documentation de RequireJs et le documentation de SystemJS.

Le compilateur TypeScript prend en charge la déclaration de tels mappages à l'aide de "paths" propriétés dans des fichiers tsconfig.json. Voici un exemple pour savoir comment spécifier la propriété jquery "paths".


Noter que "paths" sont résolus par rapport à "baseUrl". Lorsque vous définissez "baseUrl" une autre valeur que celle "." du répertoire tsconfig.json, les mappages doivent être modifiés en conséquence. Supposons que vous définissiez "baseUrl" : "./src" dans l'exemple ci-dessus, puis jquery devrait être mappé "../node_modules/jquery/dist/jquery".

L'utilisation "paths" permet également des mappages plus sophistiqués comprenant plusieurs emplacements de secours. Envisagez une configuration de projet dans laquelle seuls certains modules sont disponibles dans un emplacement et les autres dans un autre. Une étape de construction les mettrait tous ensemble au même endroit. La mise en page du projet peut ressembler à:


Le correspondant tsconfig.json ressemblerait à :

Ceci indique au compilateur pour toute importation de module qui correspond au modèle "*" (c'est-à-dire toutes les valeurs) de rechercher dans deux emplacements :

  • "*" : signifiant le même nom inchangé, alors map ‹moduleName› => ‹baseUrl› / ‹moduleName›
  • "generated/*" signifiant le nom du module avec le préfixe ajouté "généré", donc map ‹moduleName› => ‹baseUrl› /generated/ ‹moduleName›

En suivant cette logique, le compilateur tentera de résoudre les deux importations en tant que telles :

importer 'dossier1 / fichier2'

  • le motif '*' est apparié et le caractère générique capture le nom du module complet
  • essayez la première substitution dans la liste: '*' -> folder1/file2
  • Le résultat de la substitution est un nom non relatif combinez-le avec baseUrl -> projectRoot/folder1/file2.ts.
  • Le fichier existe.
  • Terminé.

importer 'dossier2 / fichier3'

  • le motif '*' est apparié et le caractère générique capture le nom du module complet
  • essayez la première substitution dans la liste: '*' -> folder2/file3
  • Le résultat de la substitution est un nom non relatif combinez-le avec baseUrl -> projectRoot/folder2/file3.ts.
  • Le fichier n'existe pas, passez à la deuxième substitution
  • deuxième substitution 'généré / *' -> generated/folder2/file3
  • Le résultat de la substitution est un nom non relatif combinez-le avec baseUrl -> projectRoot/generated/folder2/file3.ts.
  • Le fichier existe.
  • Terminé.

Répertoires virtuels avec rootDirs

Parfois, les sources de projet de plusieurs répertoires au moment de la compilation sont toutes combinées pour générer un seul répertoire de sortie. Cela peut être vu comme un ensemble de répertoires sources créant un répertoire "virtuel".

En utilisant 'rootDirs', vous pouvez informer le compilateur des racines constituant ce répertoire "virtuel"; ainsi, le compilateur peut résoudre les importations de modules relatifs dans ces répertoires "virtuels" comme si elles étaient fusionnées dans un répertoire.

Par exemple, considérons cette structure de projet :

Les fichiers src/views sont des codes d'utilisateur pour certains contrôles de l'interface utilisateur. Les fichiers generated/templates sont des codes de liaison de modèles d'interface utilisateur générés automatiquement par un générateur de modèles dans le cadre de la construction. Une étape de construction copiera les fichiers dans /src/views et /generated/templates/views dans le même répertoire dans la sortie. Au moment de l'exécution, une vue peut s'attendre à ce que son modèle existe à côté d'elle et doit donc l'importer sous un nom relatif en tant que "./template".

Pour spécifier cette relation avec le compilateur, utilisez "rootDirs". "rootDirs" spécifiez une liste de racines dont le contenu doit fusionner au moment de l'exécution. En suivant notre exemple, le fichier tsconfig.json devrait ressembler à ceci :


Chaque fois que le compilateur voit une importation relative de module dans un sous-dossier de l'un des rootDirs, il tentera de rechercher cette importation dans chacune des entrées de rootDirs.

La flexibilité de rootDirs n'est pas limitée à la spécification d'une liste de répertoires sources physiques qui sont fusionnés de manière logique. Le tableau fourni peut inclure un nombre quelconque de noms de répertoires ad hoc et arbitraires, qu'ils existent ou non. Cela permet au compilateur de capturer de manière sécurisée les fonctions complexes de groupement et d'exécution telles que l'inclusion conditionnelle et les plug-ins de chargeur spécifiques au projet.

Prenons un scénario d'internationalisation dans lequel un outil de génération génère automatiquement des ensembles spécifiques à l'environnement local en interpolant un jeton de chemin d'accès spécial, par exemple #{locale}, dans le cadre d'un chemin de module relatif, tel que ./#{locale}/messages. Dans cette configuration hypothétique, 'outil énumère les paramètres régionaux pris en charge, mappant le chemin abstrait en ./zh/messages, ./de/messages etc...

Supposons que chacun de ces modules exporte un tableau de chaînes.

Par exemple ./zh/messages pourrait contenir :

En tirant parti de cette situation, rootDirs nous pouvons informer le compilateur de ce mappage et lui permettre ainsi de le résoudre en toute sécurité /#{locale}/messages, même si le répertoire n'existera jamais.

Par exemple, avec ce qui suit tsconfig.json :

Le compilateur va maintenant résoudre le problème import messages from './#{locale}/messages', import messages from './zh/messages' à des fins d'outillage, permettant ainsi un développement indépendant des paramètres régionaux sans compromettre la prise en charge de la conception.


Résolution du module de traçage

Comme indiqué précédemment, le compilateur peut visiter des fichiers situés en dehors du dossier actuel lors de la résolution d'un module. Cela peut s'avérer difficile lors du diagnostic de la raison pour laquelle un module n'est pas résolu ou est résolu en une définition incorrecte. L'activation du suivi de la résolution du module du compilateur à l'aide de --traceResolution donne un aperçu de ce qui s'est passé pendant le processus de résolution du module.

Disons que nous avons un exemple d'application qui utilise le module typescript app.ts a une importation comme import * as ts from "typescript".

Invoquer le compilateur avec --traceResolution

Résultats dans une sortie telle que :



Choses à surveiller

Nom et lieu de l'importation
======== Résolution du module 'typescript' à partir de 'src/app.ts' . ========

La stratégie suivie par le compilateur
Le type de résolution de module n'est pas spécifié, en utilisant 'NodeJs'.

Chargement des types à partir des paquets npm
'package.json' a le champ 'types' './lib/typescript.d.ts' qui fait référence à 'node_modules/typescript/lib/typescript.d.ts'.

Résultat final
======== Le nom de module 'typescript' a été résolu avec succès en 'node_modules/typescript/lib/typescript.d.ts'. ========

En utilisant --noResolve

Normalement, le compilateur essaiera de résoudre toutes les importations de modules avant de démarrer le processus de compilation. Chaque fois qu'il résout avec succès importun fichier, ce fichier est ajouté à l'ensemble des fichiers que le compilateur traitera ultérieurement.

Les --noResolveoptions du compilateur indiquent au compilateur de ne pas "ajouter" à la compilation des fichiers qui n'ont pas été transmis sur la ligne de commande. Il tentera toujours de résoudre le module en fichiers, mais si le fichier n'est pas spécifié, il ne sera pas inclus.

Par exemple :

app.ts


Compiler en app.ts utilisant --noResolve devrait aboutir à :
  • Trouver correctement moduleA comme il a été passé sur la ligne de commande.
  • Erreur pour ne pas trouver moduleB car il n'a pas été passé.

Questions courantes

Pourquoi un module de la liste d'exclusion est-il toujours choisi par le compilateur ?

tsconfig.json transforme un dossier en "projet". Sans spécifier une entrées "exclude" ou les "files", tous les fichiers dans le dossier contenant le tsconfig.json et tous ses sous-répertoires sont inclus dans votre compilation. Si vous souhaitez exclure certains des fichiers utilisés "exclude", si vous préférez spécifier tous les fichiers plutôt que de laisser le compilateur les rechercher, utilisez "files".

C'était l'inclusion automatique tsconfig.json. Cela n'intègre pas la résolution du module comme indiqué ci-dessus. Si le compilateur a identifié un fichier en tant que cible d'une importation de module, il sera inclus dans la compilation, qu'il ait été exclu ou non aux étapes précédentes.

Ainsi, pour exclure un fichier de la compilation, vous devez l'exclure, ainsi que tous les fichiers contenant une directive importou /// ‹reference path="..." /›.