-
-
Notifications
You must be signed in to change notification settings - Fork 529
/
cjs-resolve-filename-hook.ts
146 lines (138 loc) · 4.13 KB
/
cjs-resolve-filename-hook.ts
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
134
135
136
137
138
139
140
141
142
143
144
145
146
import type Module = require('module');
import type { Service } from '.';
/** @internal */
export type ModuleConstructorWithInternals = typeof Module & {
_resolveFilename(
request: string,
parent?: Module,
isMain?: boolean,
options?: ModuleResolveFilenameOptions,
...rest: any[]
): string;
_preloadModules(requests?: string[]): void;
};
interface ModuleResolveFilenameOptions {
paths?: Array<string>;
}
/**
* @internal
*/
export function installCommonjsResolveHookIfNecessary(tsNodeService: Service) {
const Module = require('module') as ModuleConstructorWithInternals;
const originalResolveFilename = Module._resolveFilename;
const shouldInstallHook = tsNodeService.options.experimentalResolverFeatures;
if (shouldInstallHook) {
Module._resolveFilename = _resolveFilename;
}
function _resolveFilename(
this: any,
request: string,
parent?: Module,
isMain?: boolean,
options?: ModuleResolveFilenameOptions,
...rest: any[]
): string {
if (!tsNodeService.enabled())
return originalResolveFilename.call(
this,
request,
parent,
isMain,
options,
...rest
);
// #region path-mapping
// Note: [SYNC-PATH-MAPPING] keep this logic synced with the corresponding ESM implementation.
let candidateSpecifiers: string[] = [request];
const attemptPathMapping =
tsNodeService.commonjsPathMapping &&
parent?.filename &&
!tsNodeService.ignored(parent.filename);
if (attemptPathMapping) {
const mappedSpecifiers = tsNodeService.mapPath(request);
if (mappedSpecifiers) {
candidateSpecifiers = mappedSpecifiers;
}
}
// Attempt all resolutions. Collect resolution failures and throw an
// aggregated error if they all fail.
const moduleNotFoundErrors = [];
for (let i = 0; i < candidateSpecifiers.length; i++) {
try {
// TODO does this break if `options.paths` is passed? Should we bail if
// we receive `options.paths`?
return originalResolveFilename.call(
this,
candidateSpecifiers[i],
parent,
isMain,
options
);
} catch (err: any) {
const isNotFoundError = err.code === 'MODULE_NOT_FOUND';
if (!isNotFoundError) {
throw err;
}
moduleNotFoundErrors.push(err);
}
}
// If only one candidate, no need to wrap it.
if (candidateSpecifiers.length === 1) {
throw moduleNotFoundErrors[0];
} else {
throw new MappedCommonJSModuleNotFoundError(
request,
parent!.filename,
candidateSpecifiers,
moduleNotFoundErrors
);
}
// #endregion
// This is a stub to support other pull requests that will be merged in the near future
// Right now, it does nothing.
return originalResolveFilename.call(
this,
request,
parent,
isMain,
options,
...rest
);
}
}
interface NodeCommonJSModuleNotFoundError extends Error {
requireStack?: string[];
}
class MappedCommonJSModuleNotFoundError extends Error {
// Same code as other module not found errors.
readonly code = 'MODULE_NOT_FOUND' as const;
readonly errors!: ReadonlyArray<Error>;
readonly requireStack?: string[];
constructor(
specifier: string,
parentFilename: string,
candidates: string[],
moduleNotFoundErrors: Error[]
) {
super(
[
`Cannot find '${specifier}' imported from ${parentFilename} using TypeScript path mapping`,
'Candidates attempted:',
...candidates.map((candidate) => `- ${candidate}`),
].join('\n')
);
// TODO this differs slightly from nodejs errors; see if we can match them
this.name = `Error [${this.code}]`;
// Match shape of `AggregateError`
Object.defineProperty(this, 'errors', {
value: moduleNotFoundErrors,
configurable: true,
writable: true,
});
// Assume every `requireStack` is identical, and maybe downstream code is doing
// something with it
this.requireStack = (
moduleNotFoundErrors[0] as NodeCommonJSModuleNotFoundError | undefined
)?.requireStack;
}
}