1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133 | 1x
1x
1x
1x
1x
11x
11x
14x
11x
12x
1x
12x
12x
19x
12x
14x
14x
14x
15x
15x
15x
15x
11x
1x
15x
1x
49x
3x
11x
11x
3x
3x
3x
3x
8x
28x
12x
11x
22x
22x
| import { readPackage } from './package-reader';
import * as fs from "fs";
import * as path from "path";
import { matchStar } from "./match-star";
export type MatchPath = (
absoluteSourceFileName: string,
requestedModule: string,
readPackageJson?: (packageJsonPath: string) => any,
fileExists?: any, extensions?: Array<string>) => string | undefined;
/**
* Creates a function that can resolve paths according to tsconfig paths property.
* @param tsConfigPath The paths where tsconfig.json is located.
* @param baseUrl The baseUrl specified in tsconfig.
* @param paths The paths specified in tsconfig.
*/
export function createMatchPath(
absoluteBaseUrl: string,
paths: { [key: string]: Array<string> }
): MatchPath {
// Resolve all paths to absolute form once here, this saves time on each request later.
// We also add the baseUrl as a base which will be replace if specified in paths. This is how typescript works
const absolutePaths: { [key: string]: Array<string> } = Object.keys(paths)
.reduce((soFar, key) => ({
...soFar,
[key]: paths[key]
.map((pathToResolve) => path.join(absoluteBaseUrl, pathToResolve))
}), {
"*": [`${absoluteBaseUrl.replace(/\/$/, "")}/*`],
});
return (sourceFileName: string, requestedModule: string, readPackageJson: (packageJsonPath: string) => any, fileExists: any, extensions?: Array<string>) =>
matchFromAbsolutePaths(absolutePaths, sourceFileName, requestedModule, readPackageJson, fileExists, extensions);
}
/**
* Finds a path from tsconfig that matches a module load request.
* @param absolutePathMappings The paths to try as specified in tsconfig but resolved to absolute form.
* @param absoluteSourceFileName Absolute path to the file that requested the module.
* @param requestedModule The required module name.
* @param readPackageJson Function that returns parsed package.json if exists or undefined(useful for testing).
* @param fileExists Function that checks for existance of a file (useful for testing).
* @param extensions File extensions to probe for (useful for testing).
* @returns the found path, or undefined if no path was found.
*/
export function matchFromAbsolutePaths(
absolutePathMappings: { [key: string]: Array<string> },
absoluteSourceFileName: string,
requestedModule: string,
IreadPackageJson: (packageJsonPath: string) => any = (packageJsonPath: string) => readPackage(packageJsonPath),
IfileExists = fs.existsSync,
extensions = Object.keys(require.extensions)
): string | undefined {
Eif (requestedModule[0] !== '.'
&& requestedModule[0] !== path.sep
&& absolutePathMappings
&& absoluteSourceFileName
&& requestedModule
&& fileExists) {
for (const virtualPathPattern of sortByLongestPrefix(Object.keys(absolutePathMappings))) {
const starMatch = virtualPathPattern === requestedModule ? '' : matchStar(virtualPathPattern, requestedModule);
if (starMatch !== undefined) {
for (const physicalPathPattern of absolutePathMappings[virtualPathPattern]) {
const physicalPath = physicalPathPattern.replace('*', starMatch);
const resolved = tryResolve(physicalPath, fileExists, readPackageJson, extensions);
if (resolved) {
return resolved;
}
}
}
}
}
return undefined;
}
/**
* Tries to resolve a physical path by:
* 1. Check for files named as last part of request and ending in any of the extensions.
* 2. Check for file specified in package.json's main property.
* 3. Check for a file named index ending in any of the extensions.
* @param physicalPath The path to check.
* @param fileExists Function that checks for existance of a file (useful for testing).
* @param readPackageJson Function that returns parsed package.json if exists or undefined(useful for testing).
* @param extensions File extensions to probe for (useful for testing).
* @returns {string}
*/
function tryResolve(
physicalPath: string,
fileExists: any,
readPackageJson: (packageJsonPath: string) => any, extensions: Array<string>
): string | undefined {
if (path.extname(path.basename(physicalPath)).length > 0 && fileExists(physicalPath)) {
return physicalPath;
}
if (extensions.reduce((prev, curr) => prev || fileExists(physicalPath + curr), false)) {
return physicalPath;
}
const packageJson = readPackageJson(path.join(physicalPath, "/package.json"));
if (packageJson && packageJson.main && fileExists(path.join(physicalPath, packageJson.main))) {
const file = path.join(physicalPath, packageJson.main);
const fileExtension = path.extname(file).replace(/^\./, "");
const fileExtensionRegex = new RegExp(`\.${fileExtension}$`);
return fileExtension ? file.replace(fileExtensionRegex, "") : file;
}
const indexPath = path.join(physicalPath, "/index");
return extensions.reduce((prev, curr) => prev || fileExists(indexPath + curr) && physicalPath, "");
}
/**
* Sort path patterns.
* If module name can be matches with multiple patterns then pattern with the longest prefix will be picked.
*/
function sortByLongestPrefix(arr: Array<string>): Array<string> {
return arr
.concat()
.sort((a: string, b: string) => getPrefixLength(b) - getPrefixLength(a));
}
function getPrefixLength(pattern: string): number {
const prefixLength = pattern.indexOf("*");
return pattern.substr(0, prefixLength).length;
}
|