Preface
[Modular mechanism](/articles/模块-typescript 笔记 13/) allows us to split code into multiple modules (files), and during compilation we need to know the exact type of dependent modules, so first we need to find it (establish mapping from module name to module file path)
Actually, in TypeScript, a module name may correspond to a .ts/.tsx or .d.ts file (if --allowJs is enabled, may also correspond to .js/.jsx files)
Basic idea is:
- First try to find the file corresponding to the module (
.ts/.tsx) - If not found, and it's not a relative module import (
non-relative), try to find external module declaration (ambient module declaration), i.e.,d.ts - If still not found, error
Cannot find module 'ModuleA'.
I. Relative vs Non-Relative Module Imports
Relative module imports (relative import) start with /, ./, or ../, for example:
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
Note, relative module imports are not equivalent to relative path imports (e.g., /mod)
All other forms are non-relative module imports (non-relative import), for example:
import * as $ from "jquery";
import { Component } from "@angular/core";
Differences between the two:
-
Relative module imports: Search for modules relative to the file being imported, and won't be resolved as external module declarations. Used to import (can maintain relative positions at runtime) custom modules
-
Non-relative module imports: Search for modules relative to baseUrl or according to path mapping, may be resolved as external module declarations. Used to import external dependency modules
II. Module Resolution Strategies
Specifically, there are 2 module resolution strategies:
-
Classic: TypeScript's default resolution strategy, currently only used for backward compatibility
-
Node: Resolution strategy consistent with NodeJS module mechanism
These 2 strategies can be specified via --moduleResolution compilation option, defaults based on target module form (module === "AMD" or "System" or "ES6" ? "Classic" : "Node")
Classic
Under Classic strategy, relative module imports are resolved relative to the file being imported, for example:
// Source file /root/src/folder/A.ts
import { b } from "./moduleB"
Will try to find:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
For non-relative module imports, start from the directory containing the file being imported and traverse up the directory tree, trying to find matching definition files, for example:
// Source file /root/src/folder/A.ts
import { b } from "moduleB"
Will try to find following files:
/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
NodeJS Module Resolution
In NodeJS, modules are imported via require, specific behavior of module resolution depends on whether parameter is relative path or non-relative path
Relative path handling strategy is quite simple, for:
// Source file /root/src/moduleA.js
var x = require("./moduleB");
Matching order is as follows:
-
Try to match
/root/src/moduleB.js -
Then try to match
/root/src/moduleB/package.json, then find main module (e.g., if specified{ "main": "lib/mainModule.js" }, import/root/src/moduleB/lib/mainModule.js) -
Otherwise try to match
/root/src/moduleB/index.js, becauseindex.jsis implicitly treated as main module under that directory
P.S. Refer to NodeJS documentation: File Modules and Folders as Modules
Non-relative module imports search from node_modules (node_modules may be in same-level directory as current file, or in ancestor directory), NodeJS will search up through each node_modules, looking for module to import, for example:
// Source file /root/src/moduleA.js
var x = require("moduleB");
NodeJS will try to match in order:
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json
/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json
/node_modules/moduleB/index.js
P.S. For package.json, actually load module pointed to by its main field
P.S. For more information about how NodeJS loads modules from node_modules, see Loading from node_modules Folders
TypeScript Emulates NodeJS Strategy
(When module resolution strategy is "Node") TypeScript also simulates NodeJS runtime module resolution mechanism, to find module definition files at compile time
Specifically, adds TypeScript source file suffixes to NodeJS module resolution logic, and also finds declaration files via types field in package.json (equivalent to simulating NodeJS's main field), for example:
// Source file /root/src/moduleA.ts
import { b } from "./moduleB"
Will try to match:
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
P.S. For package.json, TypeScript loads module pointed to by its types field
This process is very similar to NodeJS (first moduleB.js, then package.json, finally index.js), just with TypeScript source file suffixes
Similarly, non-relative module imports also follow NodeJS resolution logic, first find files, then find applicable folders:
// Source file /root/src/moduleA.ts
import { b } from "moduleB
Module search order is as follows:
/root/src/node_modules/moduleB.ts|tsx|d.ts
/root/src/node_modules/moduleB/package.json
/root/src/node_modules/@types/moduleB.d.ts
/root/src/node_modules/moduleB/index.ts|tsx|d.ts
/root/node_modules/moduleB.ts|tsx|d.ts
/root/node_modules/moduleB/package.json
/root/node_modules/@types/moduleB.d.ts
/root/node_modules/moduleB/index.ts|tsx|d.ts
/node_modules/moduleB.ts|tsx|d.ts
/node_modules/moduleB/package.json
/node_modules/@types/moduleB.d.ts
/node_modules/moduleB/index.ts|tsx|d.ts
Almost identical to NodeJS search logic, just additionally searches for d.ts declaration files from node_modules/@types
III. Additional Module Resolution Flags
During build, .ts is compiled to .js, and dependencies are copied from different source locations to same output location. Therefore, at runtime modules may have different names from source files, or compiled output module paths may not match corresponding source files
For these issues, TypeScript provides a series of flags to inform compiler about expected transformations on source paths, to generate final output
P.S. Note, compiler doesn't perform any transformations, only uses this information to guide resolution of module imports to their definition files
Base URL
baseUrl is common in applications following AMD module pattern, module source files can be in different directories, build scripts put them together. At runtime, these modules are "deployed" to single directory
In TypeScript, set baseUrl to inform compiler where to find modules, all non-relative module imports are relative to baseUrl, two ways to specify:
-
Command line argument
--baseUrl(if specifying relative path, calculated based on current directory) -
baseUrlfield intsconfig.json(if relative path, calculated based on directory wheretsconfig.jsonis located)
Note, relative module imports are not affected by baseUrl, because always resolved relative to files importing them
Path Mapping
Some modules are not under baseUrl, e.g., jquery module at runtime may come from node_modules/jquery/dist/jquery.slim.min.js, at this point, module loader maps module names to runtime files via path mapping
TypeScript also supports similar mapping configuration (paths field in tsconfig.json), for example:
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
}
}
}
Note, paths in paths are also relative to baseUrl, if baseUrl changes, paths must also change accordingly
Actually, also supports more complex mapping rules, e.g., multiple alternative locations, see Path mapping for details
rootDirs Specifies Virtual Directories
During compilation, sometimes need to integrate project source code from multiple directories into single output directory, equivalent to creating a "virtual" directory from a set of source directories
rootDirs can inform compiler about those "root" paths that compose the "virtual" directory, allowing compiler to resolve those relative module imports pointing to "virtual" directory, as if they've been integrated into same directory, for example:
src # Source code
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated # Auto-generated template files
└── templates
└── views
└── template1.ts (imports './view2')
Assuming build tool will integrate them into same output directory (i.e., at runtime view1 and template1 are together), therefore can import via ./xxx方式。Can inform compiler of this relationship via rootDirs, list all source directories:
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
Thereafter, whenever encountering relative module imports pointing to rootDirs subdirectories, will try to find in each item of rootDirs
Actually, rootDirs is very flexible, array can contain any number of directory names, regardless of whether directories actually exist. This allows compiler to "catch" complex build/runtime features in type-safe way, such as conditional imports and project-specific loader plugins
For example, internationalization scenario, build tool automatically generates locale-specific bundles by inserting special path identifiers (e.g., #{locale}), e.g., mapping ./#{locale}/messages to concrete ./zh/messages, ./de/messages, etc. Also easily solved via rootDirs:
{
"compilerOptions": {
"rootDirs": [
"src/zh",
"src/de",
"src/#{locale}"
]
}
}
If local has zh language pack, at compile time will import files under src/zh, e.g., import messages from './#{locale}/messages will be resolved to import messages from './zh/messages'
IV. Tracing Resolution Process
Modules can reference files outside current directory, if wanting to locate module resolution related issues (e.g., can't find module, or found wrong one), it's not easy
At this point can enable --traceResolution option to trace compiler's internal module resolution process, for example:
$ tsc --traceResolution
# Imported module name and location
======== Resolving module './math-lib' from '/proj/src/index.ts'. ========
# Module resolution strategy
Explicitly specified module resolution kind: 'NodeJs'.
# Specific process
Loading module as file / folder, candidate module location '/proj/src/math-lib', target file type 'TypeScript'.
File '/proj/src/math-lib.ts' does not exist.
File '/proj/src/math-lib.tsx' does not exist.
File '/proj/src/math-lib.d.ts' exist - use it as a name resolution result.
# Final result
======== Module name './math-lib' was successfully resolved to '/proj/src/math-lib.d.ts'. ========
V. Related Options
--noResolve
Normally, compiler tries to resolve all module imports before starting, each successfully resolved module import adds corresponding file to set of source files to be processed
--noResolve compilation option can prevent compiler from adding any files (except those passed via command line), at this point still tries to resolve files corresponding to modules, but no longer adds them, for example source file:
// Source file app.ts
import * as A from "moduleA"
import * as B from "moduleB"
tsc app.ts moduleA.ts --noResolve will correctly import moduleA, while moduleB will error not found (because --noResolve doesn't allow adding other files)
exclude
By default, directory where tsconfig.json is located is TypeScript project directory, if not specifying files or exclude, all files under that directory and its subdirectories will be added to compilation process. Can exclude certain files via exclude option (blacklist), or specify source files wanting to compile via files option (whitelist)
Additionally, during compilation when encountering imported modules, they will also be added, regardless of whether excluded or not. Therefore, to completely exclude a file at compile time, besides exclude itself, must also exclude all files referencing it
No comments yet. Be the first to share your thoughts.