Welcome to wechat public account: Front Reading Room
This section assumes that you already know the basics of modules read the module documentation for more information.
Module resolution refers to the process followed by the compiler to find the contents of an imported module. Suppose there is an import statement import {a} from “moduleA”; In order to check any use of a, the compiler needs to know exactly what it represents and needs to check its definition moduleA.
At this point, the compiler will ask “What is the structure of moduleA?” This sounds simple, but moduleA may be in one of the.ts/.tsx files you wrote or in the.d.ts on which your code depends.
First, the compiler tries to locate the file that represents the imported module. The compiler follows one of two strategies: Classic or Node. These policies tell the compiler where to look for moduleA.
If the above resolution fails and the module name is nonrelative (and in the case of “moduleA”), the compiler tries to locate an external module declaration. We’re going to talk about non-relative imports.
Finally, if the compiler still can’t parse the module, it logs an error. In this case, the error might be error TS2307: Cannot find module ‘moduleA’.
Relative vs. non-relative module import
Module imports are resolved differently depending on whether module references are relative or non-relative.
Relative imports start with /,./ or.. / at the beginning. Here are some examples:
- import Entry from “./components/Entry”;
- import { DefaultHeaders } from “.. /constants/http”;
- import “/mod”;
All other forms of imports are treated as non-relative. Here are some examples:
- import * as $ from “jQuery”;
- import { Component } from “@angular/core”;
A relative import is resolved relative to the file to which it was imported, and cannot be resolved as an external module declaration. You should use relative imports for your own modules to ensure that they are in relative positions at run time.
Imports of non-relative modules can be resolved relative to baseUrl or through pathmaps described below. They can also be resolved into external module declarations. Use non-relative paths to import your external dependencies.
Module resolution strategy
There are two module resolution strategies available: Node and Classic. You can use the –moduleResolution tag to specify which moduleResolution strategy to use. If not specified, then in use, the module of AMD | System | ES2015 when the default value is Classic and other situations for the Node.
Classic
This policy used to be the default TypeScript parsing policy. Right now, its raison d ‘etre is mostly for backward compatibility.
A relative imported module is resolved relative to the file into which it was imported. So import {b} from “./moduleB” in /root/src/folder/ a.ts will use the following search flow:
- /root/src/folder/moduleB.ts
- /root/src/folder/moduleB.d.ts
For imports of non-relative modules, the compiler attempts to locate a matching declaration file by iterating from the directory containing the import file to the upper directory.
Such as:
There is a non-relative import of moduleB import {b} from “moduleB”, which is in the /root/src/ Folder/a.ts file, and will locate “moduleB” as follows:
- /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
Node
This parsing strategy attempts to mimic the Node.js module parsing mechanism at runtime. The complete Node.js parsing algorithm can be found in Node.js Module documentation.
How does Node.js parse modules
To understand the parsing steps that TypeScript compiles follow, it’s important to understand node.js modules first. Typically, imports in Node.js are done through the require function call. Node.js behaves differently depending on whether it requires a relative or non-relative path.
The relative path is simple. For example, suppose there is a file with a path of /root/src/ modulea.js that contains an import var x = require(“./moduleB”); Node.js parses the imports in the following order:
-
Check whether the /root/src/ moduleb. js file exists.
-
Check whether the /root/src/moduleB directory contains a package.json file that specifies a “main” module. In our example, if the Node. Js find file/root/SRC/moduleB/package. The json contains {” main “: “Lib/mainModule. Js”}, then the Node. Js will refer to/root/SRC/moduleB/lib/mainModule js.
-
Check whether the /root/src/moduleB directory contains an index.js file. This file is implicitly referred to as the “main” module in that folder.
You can read the Node.js documentation for more details: File Modules and Folder Modules.
However, resolving non-relative module names is an entirely different process. Node will look for your module in a special folder called node_modules. Node_modules may be in the same directory as the current file, or in the upper directory. Node traverses the parent directory, looking for each node_modules until it finds the module to load.
Var x = require(“moduleB”); var x = require(“moduleB”); . Node will parse moduleB in the following order until one matches.
-
/root/src/node_modules/moduleB.js
-
/ root/SRC/node_modules/moduleB/package. The json (if you specify the “main” attribute)
-
/root/src/node_modules/moduleB/index.js
-
/root/node_modules/moduleB.js
-
/ root/node_modules/moduleB/package. Json (if you specify the “main” attribute)
-
/root/node_modules/moduleB/index.js
-
/node_modules/moduleB.js
-
/ node_modules/moduleB/package. Json (if you specify the “main” attribute)
-
/node_modules/moduleB/index.js
Note that Node.js jumps up one directory in steps (4) and (7).
You can read the node.js documentation for more details: Loading modules from node_modules.
How does TypeScript parse modules
TypeScript mimics the Parsing strategy of the Node.js runtime to locate module definition files at compile time. Therefore, TypeScript adds extensions to TypeScript source files (.ts,.tsx, and.d.ts) on top of Node parsing logic. Also, TypeScript uses the field “types” in package.json to mean something like “main” – which the compiler uses to find the “main” definition file to use.
For example, if there is an import statement import {b} from “./moduleB” in /root/src/ modulea.ts, the following process will be used to locate “./moduleB” :
- /root/src/moduleB.ts
- /root/src/moduleB.tsx
- /root/src/moduleB.d.ts
- / root/SRC/moduleB/package. Json (if you specify the “types” attribute)
- /root/src/moduleB/index.ts
- /root/src/moduleB/index.tsx
- /root/src/moduleB/index.d.ts
Recall that Node.js looks for the moduleb. js file, then for the appropriate package.json, and then for index.js.
Similarly, non-relative imports follow Node.js parsing logic, first looking for the file, then the appropriate folder. Import {b} from “moduleB” in /root/src/ modulea. ts will be resolved in the following order:
/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. The json (if you specify the “types” attribute)/root/SRC/node_modules/moduleB/index. The 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 (if you specify the “types” attribute)/root/node_modules/moduleB/index. The 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 (if you specify the “types” attribute)/node_modules/moduleB/index. The ts/node_modules/moduleB/index. The TSX/node_modules/moduleB/index, which s
Don’t be alarmed by the number of steps here – TypeScript just jumps up the directory twice in steps (8) and (15). This is no more complicated than the flow in Node.js.
Additional module resolution tags
Sometimes the project source code structure is different from the output structure. It usually goes through a system build process and generates output. They include compiling.ts to.js and copying dependencies at different locations to an output location. The end result is that module names at runtime are different from those in the source file that contains their declarations. Or the module path in the final output file is different from the source file path at compile time.
The TypeScript compiler has additional tags that inform the compiler of which transformations have taken place as the source code is compiled into the final output.
It is important to note that the compiler does not perform these conversions; It simply uses this information to guide the import of modules.
Base URL
It is common practice to use baseUrl in applications that utilize the AMD module loader, which requires modules to be placed in a folder at runtime. The source code for these modules can be in different directories, but the build script will keep them all together.
Set baseUrl to tell the compiler where to look for modules. All non-relative module imports are treated as relative to baseUrl.
The value of baseUrl is determined by one of the following:
- The value of baseUrl on the command line (evaluated against the current path if the given path is relative)
- BaseUrl property in ‘tsconfig.json’ (if the given path is relative, it will be evaluated against the ‘tsconfig.json’ path)
Note that the import of relative modules is not affected by the baseUrl setting, because they are always relative to the file from which they were imported.
Read more about baseUrl RequireJS and SystemJS.
Route map
Sometimes modules are not placed directly under baseUrl. Fully “jquery” module to import, for example, in the runtime may be interpreted as “node_modules/jquery/dist/jquery. Slim. Min. Js”. The loader uses the mapping configuration to map module names to runtime files, see RequireJs Documentation and SystemJS Documentation.
The TypeScript compiler supports such declarative mapping by using “paths” in the tsconfig.json file. Here is an example of how to specify “paths” in jquery.
{
"compilerOptions": {
"baseUrl": ".".// This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // the mapping is relative to "baseUrl"}}}Copy the code
Note that “paths” is parsed relative to “baseUrl”. If “baseUrl” is set to a value other than “.”, such as the tsconfig.json directory, then the mapping must be changed accordingly. If you set “baseUrl”: “./ SRC “in the previous example, jquery should map to “.. / node_modules/jquery/dist/jquery “.
“Paths” also allows you to specify complex mappings, including specifying multiple fallback locations. Suppose that in a project configuration, some modules are in one place and others are in another. The build process brings them all together. The engineering structure may be as follows:
├─ ├─ └ └ (imports' ├ 1/ ├ 2' and '├ 2/ ├ 3') │ ├─ ├ 2 ├ ─ ├ ─ garbage (2 files, 1 files, 2 files, 2 filesCopy the code
The corresponding tsconfig.json file is as follows:
{
"compilerOptions": {
"baseUrl": "."."paths": {
"*": [
"*"."generated/*"]}}}Copy the code
It tells the compiler that all module imports that match the “*” (all values) pattern will be looked up in two places:
- “*” : indicates that the name is not changed, so it is mapped to
<moduleName> => <baseUrl>/<moduleName>
- “Generated /*” indicates that the prefix “generated” is added to the module name, so it is mapped to
<moduleName> => <baseUrl>/generated/<moduleName>
Following this logic, the compiler will try to resolve the two imports as follows:
Import ‘folder1 / file2’
- Matches the ‘*’ pattern and wildcards capture the entire name.
- Try the first substitution in the list: ‘*’ -> folder1/file2.
- Replace the result is a relative name – a merger with baseUrl – > projectRoot/folder1 / file2. Ts.
- The file exists. To complete.
Import ‘folder2 / file3’
- Matches the ‘*’ pattern and wildcards capture the entire name.
- Try the first substitution in the list: ‘*’ -> folder2/file3.
- Replace the result is a relative name – a merger with baseUrl – > projectRoot folder2 / file3. Ts.
- File does not exist, skip to second replacement.
- Second substitution: ‘generated/*’ -> generated/folder2/file3.
- Replace the result is a relative name – a merger with baseUrl – > projectRoot/generated/folder2 / file3. Ts.
- The file exists. To complete.
Specify a virtual directory using rootDirs
Sometimes project source files in multiple directories are merged into an output directory during compilation. This can be seen as some source directory creating a “virtual” directory.
With rootDirs, you can tell the compiler the roots that generated this virtual directory; The compiler can therefore resolve relative module imports in a “virtual” directory as if they had been merged together.
For example, there is the following engineering structure:
TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT (imports './view2')Copy the code
The file in SRC/Views is the user code that controls the UI. Generated/Templates are UI templates that are generated automatically at build time through the template generator. Will build the step/SRC/views and generated/templates/copy the output of the views to the same directory. At run time, a view can assume that its templates are in the same directory as it, so relative imports of “./template” can be used.
You can use “rootDirs” to tell the compiler. “RootDirs” specifies a list of roots whose contents are merged at run time. So, for this example, tsconfig.json looks like this:
{
"compilerOptions": {
"rootDirs": [
"src/views"."generated/templates/views"]}}Copy the code
Whenever the compiler finds relative module imports in a subdirectory of a rootDirs, it tries to import from each rootDirs.
The flexibility of rootDirs goes beyond specifying a list of physical directories to logically merge. It provides an array that can contain any number of directories of any name, whether they exist or not. This allows the compiler to handle complex bundles and runtime features such as conditional introductions and project-specific loader plug-ins in a type-safe manner.
Imagine an internationalized scenario where the build tool automatically inserts specific path tokens to generate location-specific bundles, such as #{locale} as part of the relative module path./#{locale}/messages. With this hypothetical setup, the tool enumerates supported areas, mapping the extraction path to./zh/messages,./de/messages, and so on.
Suppose each module exports an array of strings. For example./zh/messages might contain:
export default [
"How are you?"."Nice to meet you."
];
Copy the code
With rootDirs we can make the compiler aware of this mapping, and thus allow the compiler to safely resolve./#{locale}/messages, even if the directory never exists. For example, use the following tsconfig.json:
{
"compilerOptions": {
"rootDirs": [
"src/zh"."src/de"."src/#{locale}"]}}Copy the code
The compiler can now resolve import messages from ‘./#{locale}/messages’ to Import messages from ‘./zh/messages’ for tool support purposes and allows development without knowing the locale information.
Trace module parsing
As discussed earlier, the compiler may access files outside the current folder when parsing a module. This can make it difficult to diagnose why the module is not being parsed or is being parsed to the wrong place. Enable the compiler’s module resolution trace with –traceResolution, which tells us what happened during module resolution.
Suppose we have a simple application that uses typescript modules. There is an import like import * as ts from “typescript” in app.ts.
│ tsconfig. Json ├ ─ ─ ─ node_modules │ └ ─ ─ ─ typescript │ └ ─ ─ ─ the lib │ typescript. Which s └ ─ ─ ─ the SRC app. TsCopy the code
Call the compiler with –traceResolution.
tsc --traceResolution
Copy the code
The following output is displayed:
======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========
Copy the code
Something to watch out for
Import name and location
======== Resolving module ‘typescript’ from ‘src/app.ts’. ========
The policy used by the compiler
Module resolution kind is not specified, using ‘NodeJs’.
Load types from NPM
‘package.json’ has ‘types’ field ‘./lib/typescript.d.ts’ that references ‘node_modules/typescript/lib/typescript.d.ts’.
The final result
======== Module name ‘typescript’ was successfully resolved to ‘node_modules/typescript/lib/typescript.d.ts’. ========
Using the — noResolve
Normally, the compiler will parse module imports before starting compilation. Whenever it successfully resolves the import of a file, the file is added to a list of files for the compiler to process later.
The noResolve compilation option tells the compiler not to add any files to the compilation list that are not passed in on the command line. The compiler will still try to parse the module, but as long as this file is not specified, it will not be included.
Such as
app.ts
import * as A from "moduleA" // OK, moduleA passed on the command-line
import * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
Copy the code
tsc app.ts moduleA.ts –noResolve
Build app.ts with –noResolve:
- ModuleA was probably found correctly because it was specified on the command line.
- ModuleB could not be found because it was not passed on the command line.
Q&A
Why is a module in the exclude list still used by the compiler
If you do not specify any “exclude” or “files”, all files in the folder including tsconfig.json and all subdirectories will be included in the compilation list. If you want to use “exclude” to exclude certain files, or even if you want to specify a list of all files to compile, use “files”.
Some are added automatically by tsconfig.json. It does not involve module resolution as discussed above. If the compiler identifies a file as a module import target, it is added to the compilation list, whether it is excluded or not.
Therefore, to exclude a file from the compiled list, you need to exclude it as well as any files that import it or use ///
Welcome to wechat public account: Front Reading Room