Proposed set of rules is an extension to baseUrl based mechanics used by RequireJS or SystemJS.
Core elements of the system are baseUrl and path mappings. Resolution process describes how they are used together.
All non-rooted paths are computed relative to baseUrl. Value of baseUrl is determined as either:
- value of baseUrl command line argument (if given path is relative it is computed based on current directory)
- value of baseUrl propery in 'tsconfig.json' (if given path is relative it is computed based on location of 'tsconfig.json')
- location of 'tsconfig.json' file
Sometimes modules are not directly located under baseUrl. It is possible to control how locations are computed in such cases using path mappings. Path mappings are specified using the following JSON structure:
{
"paths": {
"pattern-1": "substitution" | ["list of substitutions"],
"pattern-2": "substitution" | ["list of substitutions"],
...
"pattern-N": "substitution" | ["list of substitutions"]
}
}
Patterns and substitutions are strings that can have zero or one asteriks ('*'). Interpretation of both patterns and substitutions will be described in Resolution process section.
Non-relative module names are resolved slightly differently comparing to relative (start with "./" or "../") and rooted module names (start with "/", drive name or schema).
// mimics path mappings in SystemJS
// NOTE: fileExists checks if file with any supported extension exists on disk
function resolveNonRelativeModuleName(moduleName: string): string {
// check if module name should be used as-is or it should be mapped to different value
let longestMatchedPrefixLength = 0;
let matchedPattern: string;
let matchedWildcard: string;
for (let pattern in config.paths) {
assert(pattern.countOf('*') <= 1);
let indexOfWildcard = pattern.indexOf('*');
if (indexOfWildcard !== -1) {
// if pattern contains asterisk then asterisk acts as a capture group with a greedy matching
// i.e. for the string 'abbb' pattern 'a*b' will get 'bb' as '*'
// check if module name starts with prefix, ends with suffix and these two don't overlap
let prefix = pattern.substr(0, indexOfWildcard);
let suffix = pattern.substr(indexOfWildcard + 1);
if (moduleName.length >= prefix.length + suffix.length &&
moduleName.startsWith(prefix) &&
moduleName.endsWith(suffix)) {
// use length of matched prefix as betterness criteria
if (longestMatchedPrefixLength < prefix.length) {
// save length of the prefix
longestMatchedPrefixLength = prefix.length;
// save matched pattern
matchedPattern = pattern;
// save matched wildcard content
matchedWildcard = moduleName.substr(prefix.length, moduleName.length - suffix.length);
}
}
}
else {
// pattern does not contain asterisk - module name should exactly match pattern to succeed
if (pattern === moduleName) {
// save pattern
matchedPattern = pattern;
// drop saved wildcard match
matchedWildcard = undefined;
// exact match is found - can exit early
break;
}
}
}
if (!matchedPattern) {
// no pattern was matched so module name can be used as-is
let path = combine(baseUrl, moduleName);
return fileExists(path) ? path : undefined;
}
// some pattern was matched - module name needs to be substituted
let substitutions = config.paths[matchedPattern].asArray();
for (let subst of substitutions) {
assert(substs.countOf('*') <= 1);
// replace * in substitution with matched wildcard
let path = matchedWildcard ? subst.replace("*", matchedWildcard) : subst;
// if substituion is a relative path - combine it with baseUrl
path = isRelative(path) ? combine(baseUrl, path) : path;
if (fileExists(path)) {
return path;
}
}
return undefined;
}
Relative module names are computed treating location of source file that contains the import as base folder. Path mappings are not applied.
function resolveRelativeModuleName(moduleName: string, containingFile: string): string {
let path = combine(getDirectoryName(containingFile), moduleName);
return fileExists(path) ? path : undefined;
}
In some cases it also will be useful to apply path mappings to relative module names, for example when project uses source files that are spreaded across file system (not under the same root) but still prefers to use relative module names because in runtime such names can be successfully resolved due to bundling. For example consider this project structure:
userFiles
|-project
|-src
|-views
|-view1.ts (imports './view2')
shared
|-projects
|-project
|-src
|-views
| |-view2.ts (imports './view1')
|-viewManager.ts (imports './views/view1')
Logically files in userFiles/project
and shared/projects/project
belong to the same project and
after build they indeed will be bundled together.
In order to support this we'll add configuration property "rootDirs":
{
"rootDirs": [
"rootDir-1/",
"rootDir-2/",
...
"rootDir-n/"
]
}
Elements in rootDirs
that represent non-absolute paths will be converted to absolute using location of tsconfig.json as a base folder - this is the common approach for all paths defined in tsconfig.json
This property stores list of base folders, every folder name can be either absolute or relative (then it will be computed using baseUrl).
///Algorithm for resolving relative module name
function resolveRelativeModuleName(moduleName: string, containingFile: string): string {
// convert relative module name to absolute using location of containing file
// this step is exactly the same as when doing resolution without path mapping
let path = combine(getDirectoryName(containingFile), moduleName);
if (fileExists(path)) {
// target module is located next to containingFile
return path;
}
// convert absolute module name to non-relative
// try to find element in 'rootDirs' that is the longest prefix for "path' and return path.substr(prefix.length) as non-relative name
let nonRelativeName = tryFindLongestPrefixAndReturnSuffix(rootDirs, path);
if (!nonRelativeName) {
// cannot extract non relative name
return undefined;
}
// run normal resolution for non-relative module names
return resolveNonRelativeModuleName(nonRelativeName);
}
Configuration for the example above:
{
"rootDirs": [
"userFiles/project/",
"/shared/projects/project/"
],
"paths": {
"*": [
"userFiles/project/*",
"/shared/projects/project/*"
]
}
}
projectRoot
|-folder1
|-file1.ts // imports 'folder2/file2'
|-folder2
|-file2.ts // imports './file3'
|-file3.ts
|-tsconfig.json // has no path mappings/baseUrl/rootDirs
-
import 'folder2/file2'
- baseUrl is not specified in configuration -> baseUrl = location of tsconfig.json
- path mappings are not available -> path = moduleName
- resolved module file name = combine(baseUrl, path) ->
projectRoot/folder2/file2.ts
-
import './file3'
- moduleName is relative and rootDirs are not specified in configuration - compute module name
relative to the location of containing file: resolved module file name =
projectRoot/folder2/file3.ts
- moduleName is relative and rootDirs are not specified in configuration - compute module name
relative to the location of containing file: resolved module file name =
projectRoot
|-folder1
|-file1.ts // imports 'folder1/file2' and 'folder2/file3'
|-file2.ts
|-generated
|-folder1
|-...
|-folder2
|-file3.ts
|-tsconfig.json
// configuration in tsconfig.json
{
"paths": {
"*": [
"*",
"generated/*"
]
}
}
-
import 'folder1/file2'
- baseUrl is not specified in configuration -> baseUrl = location of tsconfig.json
- configuration contains path mappings.
- pattern '*' is matched and wildcard captures the whole module name
- try first substitution in the list: '*' ->
folder1/file2
- result of substitution is relative name - combine it with baseUrl ->
projectRoot/folder1/file2.ts
. This file exists.
-
import 'folder2/file2'
- baseUrl is not specified in configuration -> baseUrl = location of tsconfig.json
- configuration contains path mappings.
- pattern '*' is matched and wildcard captures the whole module name
- try first substitution in the list: '*' ->
folder2/file3
- result of substitution is relative name - combine it with baseUrl ->
projectRoot/folder2/file3.ts
. File does not exists, move to the second substitution - second substitution 'generated/*' ->
generated/folder2/file3
- result of substitution is relative name - combine it with baseUrl ->
projectRoot/generated/folder2/file3.ts
. File exists
projectRoot
|-folder1
|-file1.ts // imports './file2'
|-generated
|-folder1
|-file2.ts
|-folder2
|-file3.ts // imports '../folder1/file1'
|-tsconfig.json
// configuration in tsconfig.json
{
"rootDirs": [
"./",
"./generated/"
],
"paths": {
"*": [
"*",
"generated/*"
]
}
}
-
import './file2'
- baseUrl is not specified in configuration -> baseUrl = location of tsconfig.json
projectRoot
- non-absolute rootDirs are converted to absolute using location of tsconfig.json as base folder ->
"rootDirs": ["projectRoot/", "projectRoot/generated"]
- module name is relative and configuration contains set of rootDirs to convert relative name to non-relative.
- convert relative module name to absolute:
projectRoot/folder1/file2
- use
rootDirs
to obtain non-relative module name:folder1/file2
(matchesprojectRoot/
) - resolve non-relative module name:
projectRoot/generated/folder1/file2.ts
(by matching '*' and applying second substitution)
- baseUrl is not specified in configuration -> baseUrl = location of tsconfig.json
-
import '../folder1/file1'
- baseUrl is not specified in configuration -> baseUrl = location of tsconfig.json
projectRoot
- non-absolute rootDirs are converted to absolute using location of tsconfig.json as base folder ->
"rootDirs": ["projectRoot/", "projectRoot/generated"]
- module name is relative and configuration contains set of rootDirs to convert relative name to non-relative.
- convert relative module name to absolute:
projectRoot/generated/folder1/file1
- use
rootDirs
to obtain non-relative module name:folder1/file1
(matchesprojectRoot/generated/
) - resolve non-relative module name:
projectRoot/folder1/file1.ts
(by matching '*' and applying first substitution)
- baseUrl is not specified in configuration -> baseUrl = location of tsconfig.json