These resolution semantics are taken directly from the node
API documentation and changed to be appropriate for the TypeScript compiler. Some parts of the resolution strategy are defined in the TypeScript Language Specification and therefore cannot be changed in the compiler implementation. Luckily these largely agree between the node
and TypeScript definitions, to make the TypeScript compiler follow these semantics will just require changing the host specific external module resolution.
If the import declaration specifies a top-level external module name and the program contains no AmbientExternalModuleDeclaration (§12.1.6) with a string literal that specifies that exact name, the name is resolved in a host dependent manner (for example by considering the name relative to a module name space root). If a matching module cannot be found an error occurs.
TypeScript Language Specification §11.2.1
TypeScript will attempt to load the required filename with the added extension of .ts
then .d.ts
.
A module prefixed with '/'
is an absolute path to the file. For example, require('/home/marco/foo')
will load the file at /home/marco/foo.ts
.
A module prefixed with './'
or '../'
is relative to the file calling require()
. That is, circle.ts
must be in the same directory as foo.ts
for require('./circle')
to find it.
Without a leading '/'
, './'
or '../'
to indicate a file, the module is loaded from a node_modules
or typings
folder.
If the module identifier passed to require()
does not begin with '/'
, '../'
, or './'
, then TypeScript starts at the parent directory of the current module, adds /node_modules
, and attempts to load the module from that location.
If it is not found there then it instead trys adding /typings
, and attempts to load the module from that location.
If it is not found there, then it moves to the parent directory and repeats the two load attempts, and so on, until the root of the file system or a directory called node_modules
or typings
is reached.
For example, if the file at '/home/ry/node_modules/projects/foo.ts'
called require('bar')
, then TypeScript would look in the following locations, in this order (each of these locations would expand to multiple files as specified in Folders as Modules):
/home/ry/node_modules/projects/node_modules/bar
/home/ry/node_modules/projects/typings/bar
/home/ry/node_modules/node_modules/bar
/home/ry/node_modules/typings/bar
/home/ry/node_modules/bar
This allows programs to localize their dependencies, so that they do not clash.
It is convenient to organize programs and libraries into self-contained directories, and then provide a single entry point to that library. There are three ways in which a folder may be passed to require()
as an argument.
The first is to create a package.json
file in the root of the folder, which specifies a main
module or definition
file. An example package.json
file might look like this:
{
"name" : "some-library",
"typescript": {
"main": "./src/some-library.ts",
"definition": "./dist/some-library.d.ts"
}
}
If this was in a folder at ./some-library
, then require('./some-library')
would attempt to load ./some-library/src/some-library.ts
.
If there is no typescript.main
value in the package.json
then TypeScript will fallback to loading the file specified by typescript.definition
, e.g. if the example did not have typescript.main
then require('./some-library')
would attempt to load ./some-library/dist/some-library.d.ts
.
This is the extent of TypeScripts awareness of package.json
files.
If there is no package.json
file present in the directory, or neither of typescript.main
and typescript.definition
are set, then TypeScript will attempt to load an index.ts
or index.d.ts
file out of that directory. For example, if there was no package.json
file in the above example, then require('./some-library')
would attempt to load:
./some-library/index.ts
./some-library/index.d.ts
This extends TypeScript Language Specification §11.2.1. Specifically this extends the final bullet point of the section, defining a host dependent manner in which to resolve top-level external module names where there is no AmbientExternalModuleDeclaration that matches. This also adds in an extension to allow external modules to have multiple file paths associated with them, allowing for Folders as Modules.
- Starting at the parent directory of the module requiring the external module the following steps will be followed:
- The current directory has
'/node_modules'
appended to it followed by the external module name, this is attempted to be resolved as an (absolute) relative external module name. If resolved this is returned as the resolution of the module. - The current directory has
'/typings'
appended to it followed by the external module name, this is attempted to be resolved as an (absolute) relative external module name. If resolved this is returned as the resolution of the module. - If the current directory is the root of the file system or is a directory called
'node_modules'
or'typings'
then the module is not found and an error occurs. - The current directory is changed to the parent of the current directory.
- The current directory has
For example, if the file at '/home/ry/node_modules/projects/foo.ts'
called require('bar')
, then TypeScript would attempt to resolve the following relative external module names, in this order (each of these locations would expand to multiple files as specified in Extended File Paths):
/home/ry/node_modules/projects/node_modules/bar
/home/ry/node_modules/projects/typings/bar
/home/ry/node_modules/node_modules/bar
/home/ry/node_modules/typings/bar
/home/ry/node_modules/bar
This allows programs to localize their dependencies, so that they do not clash.
In addition to associating the module's source file path without extension there are multiple special case paths that must be associated with source files to ensure the Folders as Modules section above works. They are:
- If the file is named
'index.ts'
and there is no'package.json'
next to the file it also gains its parent directory's path as a file path. - If the file is named
'index.ts'
and there is a'package.json'
next to the file, but that'package.json'
does not contain either oftypescript.main
ortypescript.definition
fields then the file also gains its parent directory's path as a file path. - If the file is named
'index.d.ts'
and there is no'package.json'
or'index.ts'
next to the file it also gains its parent directory's path as a file path. - If the file is named
'index.d.ts'
and there is no'index.ts'
and there is a'package.json'
next to the file, but that'package.json'
does not contain either oftypescript.main
ortypescript.definition
fields then the file also gains its parent directory's path as a file path. - If at some point in the file system hierarchy above the file there is a
'package.json'
that contains atypescript.main
which references the file when resolved relative to the'package.json'
then the file also gains the'package.json'
s parent directory's path as a file path. - If at some point in the file system hierarchy above the file there is a
'package.json'
that contains atypescript.definition
which references the file when resolved relative to the'package.json'
and does not contain atypescript.main
then the file also gains the'package.json'
s parent directory's path as a file path.
The following directory tree (plus package.json
contents)
/
├── some-lib
│ └── index.ts
├── other-lib
│ ├── package.json
│ │ { typescript: { main: 'main.ts' } }
│ └── main.ts
└── weird-lib
├── package.json
│ { typescript: { main: './dist/bundle.ts' } }
└── dist
└── bundle.ts
will have the following mapping of files to file paths
{
"/some-lib/index.ts": [ "/some-lib/index", "/some-lib" ],
"/other-lib/main.ts": [ "/other-lib/main", "/other-lib" ],
"/weird-lib/dist/bundle.ts": [ "/weird-lib/dist/bundle", "/weird-lib" ]
}
function require(X) { // from module at path Y
if (X.begins_with_any('./', '/', '../')) {
return load_as_file(Y + X)
|| load_as_directory(Y + X);
} else {
return load_node_modules_or_typings(X, dirname(Y))
|| throw "Not found";
}
}
function load_as_file(X) {
if (file.exists(X)) {
return load(X);
} else if (file.exists(X + '.ts')) {
return load(X + '.ts');
} else if (file.exists(X + '.d.ts')) {
return load(X + '.d.ts');
}
}
function load_as_directory(X) {
if (file.exists(X + '/package.json')) {
var pkg = load(X + '/package.json');
if (pkg.typescript) {
if (pkg.typescript.main) {
return load_as_file(X + pkg.typescript.main);
} else if (pkg.typescript.definition) {
return load_as_file(X + pkg.typescript.definition)
}
}
}
return load_as_file(X + '/index');
}
function load_node_modules_or_typings(X, start) {
var result;
node_modules_paths(start).any(function (dir) {
return (result = (load_as_file(dir + X) || load_as_directory(dir + X)) )
} return result;
}
function node_modules_paths(start) {
var parts = path.split(start);
var dirs = [];
parts.reverse().any(function (part) {
if (part === "node_modules" || part === "typings") {
return true;
}
dirs.push(dir + "/node_modules");
dirs.push(dir + "/typings");
});
return dirs;
}
👍