Skip to content

Commit 9c1c17a

Browse files
committed
esm: loader chaining
This patch adds the ability to chain loaders together. Hooks still need auditing on the best way to behave in the context of chaining, and that will be addressed in future patches.
1 parent dc00a07 commit 9c1c17a

39 files changed

+485
-416
lines changed

doc/api/esm.md

Lines changed: 107 additions & 93 deletions
Large diffs are not rendered by default.

lib/internal/main/repl.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const {
99

1010
const esmLoader = require('internal/process/esm_loader');
1111
const {
12-
evalScript
12+
evalScript,
13+
uncaughtException,
1314
} = require('internal/process/execution');
1415

1516
const console = require('internal/console/global');
@@ -33,7 +34,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) {
3334
process.exit(1);
3435
}
3536

36-
esmLoader.loadESM(() => {
37+
esmLoader.getLoader().then(() => {
3738
console.log(`Welcome to Node.js ${process.version}.\n` +
3839
'Type ".help" for more information.');
3940

@@ -61,5 +62,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) {
6162
getOptionValue('--inspect-brk'),
6263
getOptionValue('--print'));
6364
}
65+
}).catch((e) => {
66+
uncaughtException(e, true /* fromPromise */);
6467
});
6568
}

lib/internal/modules/cjs/loader.js

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ const {
102102
const { validateString } = require('internal/validators');
103103
const pendingDeprecation = getOptionValue('--pending-deprecation');
104104

105+
const originalModuleExports = new SafeWeakMap();
106+
module.exports = {
107+
wrapSafe, Module, toRealPath, readPackageScope,
108+
originalModuleExports,
109+
get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; }
110+
};
111+
105112
const {
106113
CHAR_FORWARD_SLASH,
107114
CHAR_BACKWARD_SLASH,
@@ -113,8 +120,6 @@ const {
113120
} = require('internal/util/types');
114121

115122
const asyncESM = require('internal/process/esm_loader');
116-
const ModuleJob = require('internal/modules/esm/module_job');
117-
const { ModuleWrap, kInstantiated } = internalBinding('module_wrap');
118123
const {
119124
encodedSepRegEx,
120125
packageInternalResolve
@@ -1119,30 +1124,7 @@ Module.prototype.load = function(filename) {
11191124
Module._extensions[extension](this, filename);
11201125
this.loaded = true;
11211126

1122-
const ESMLoader = asyncESM.ESMLoader;
1123-
const url = `${pathToFileURL(filename)}`;
1124-
const module = ESMLoader.moduleMap.get(url);
1125-
// Create module entry at load time to snapshot exports correctly
1126-
const exports = this.exports;
1127-
// Called from cjs translator
1128-
if (module !== undefined && module.module !== undefined) {
1129-
if (module.module.getStatus() >= kInstantiated)
1130-
module.module.setExport('default', exports);
1131-
} else {
1132-
// Preemptively cache
1133-
// We use a function to defer promise creation for async hooks.
1134-
ESMLoader.moduleMap.set(
1135-
url,
1136-
// Module job creation will start promises.
1137-
// We make it a function to lazily trigger those promises
1138-
// for async hooks compatibility.
1139-
() => new ModuleJob(ESMLoader, url, () =>
1140-
new ModuleWrap(url, undefined, ['default'], function() {
1141-
this.setExport('default', exports);
1142-
})
1143-
, false /* isMain */, false /* inspectBrk */)
1144-
);
1145-
}
1127+
originalModuleExports.set(this, this.exports);
11461128
};
11471129

11481130

@@ -1176,7 +1158,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
11761158
lineOffset: 0,
11771159
displayErrors: true,
11781160
importModuleDynamically: async (specifier) => {
1179-
const loader = asyncESM.ESMLoader;
1161+
const loader = await asyncESM.getLoader();
11801162
return loader.import(specifier, normalizeReferrerURL(filename));
11811163
},
11821164
});
@@ -1209,7 +1191,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
12091191
const { callbackMap } = internalBinding('module_wrap');
12101192
callbackMap.set(compiled.cacheKey, {
12111193
importModuleDynamically: async (specifier) => {
1212-
const loader = asyncESM.ESMLoader;
1194+
const loader = await asyncESM.getLoader();
12131195
return loader.import(specifier, normalizeReferrerURL(filename));
12141196
}
12151197
});

lib/internal/modules/esm/get_format.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ if (experimentalWasmModules)
3333
if (experimentalJsonModules)
3434
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
3535

36-
function defaultGetFormat(url, context, defaultGetFormatUnused) {
36+
function defaultGetFormat(url, context, nextGetFormat) {
3737
if (StringPrototypeStartsWith(url, 'nodejs:')) {
3838
return { format: 'builtin' };
3939
}

lib/internal/modules/esm/loader.js

Lines changed: 100 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ require('internal/modules/cjs/loader');
66
const {
77
FunctionPrototypeBind,
88
ObjectSetPrototypeOf,
9-
SafeMap,
109
} = primordials;
1110

1211
const {
1312
ERR_INVALID_ARG_VALUE,
1413
ERR_INVALID_RETURN_PROPERTY,
1514
ERR_INVALID_RETURN_PROPERTY_VALUE,
1615
ERR_INVALID_RETURN_VALUE,
17-
ERR_UNKNOWN_MODULE_FORMAT
16+
ERR_UNKNOWN_MODULE_FORMAT,
1817
} = require('internal/errors').codes;
1918
const { URL, pathToFileURL } = require('internal/url');
2019
const { validateString } = require('internal/validators');
@@ -28,11 +27,30 @@ const {
2827
const { defaultGetFormat } = require('internal/modules/esm/get_format');
2928
const { defaultGetSource } = require(
3029
'internal/modules/esm/get_source');
31-
const { defaultTransformSource } = require(
32-
'internal/modules/esm/transform_source');
3330
const { translators } = require(
3431
'internal/modules/esm/translators');
3532
const { getOptionValue } = require('internal/options');
33+
const {
34+
isArrayBufferView,
35+
isAnyArrayBuffer,
36+
} = require('internal/util/types');
37+
38+
let cwd; // Initialized in importLoader
39+
40+
function validateSource(source, hookName, allowString) {
41+
if (allowString && typeof source === 'string') {
42+
return;
43+
}
44+
if (isArrayBufferView(source) || isAnyArrayBuffer(source)) {
45+
return;
46+
}
47+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
48+
`${allowString ? 'string, ' : ''}array buffer, or typed array`,
49+
hookName,
50+
'source',
51+
source,
52+
);
53+
}
3654

3755
/* A Loader instance is used as the main entry point for loading ES modules.
3856
* Currently, this is a singleton -- there is only one used for loading
@@ -46,33 +64,16 @@ class Loader {
4664
// Registry of loaded modules, akin to `require.cache`
4765
this.moduleMap = new ModuleMap();
4866

49-
// Map of already-loaded CJS modules to use
50-
this.cjsCache = new SafeMap();
51-
52-
// This hook is called before the first root module is imported. It's a
53-
// function that returns a piece of code that runs as a sloppy-mode script.
54-
// The script may evaluate to a function that can be called with a
55-
// `getBuiltin` helper that can be used to retrieve builtins.
56-
// If the hook returns `null` instead of a source string, it opts out of
57-
// running any preload code.
58-
// The preload code runs as soon as the hook module has finished evaluating.
59-
this._getGlobalPreloadCode = null;
60-
// The resolver has the signature
61-
// (specifier : string, parentURL : string, defaultResolve)
62-
// -> Promise<{ url : string }>
63-
// where defaultResolve is ModuleRequest.resolve (having the same
64-
// signature itself).
67+
// Preload code is provided by loaders to be run after hook initialization.
68+
this.globalPreloadCode = [];
69+
// Loader resolve hook.
6570
this._resolve = defaultResolve;
66-
// This hook is called after the module is resolved but before a translator
67-
// is chosen to load it; the format returned by this function is the name
68-
// of a translator.
71+
// Loader getFormat hook.
6972
this._getFormat = defaultGetFormat;
70-
// This hook is called just before the source code of an ES module file
71-
// is loaded.
73+
// Loader getSource hook.
7274
this._getSource = defaultGetSource;
73-
// This hook is called just after the source code of an ES module file
74-
// is loaded, but before anything is done with the string.
75-
this._transformSource = defaultTransformSource;
75+
// Transform source hooks.
76+
this.transformSourceHooks = [];
7677
// The index for assigning unique URLs to anonymous module evaluation
7778
this.evalIndex = 0;
7879
}
@@ -83,7 +84,7 @@ class Loader {
8384
validateString(parentURL, 'parentURL');
8485

8586
const resolveResponse = await this._resolve(
86-
specifier, { parentURL, conditions: DEFAULT_CONDITIONS }, defaultResolve);
87+
specifier, { parentURL, conditions: DEFAULT_CONDITIONS });
8788
if (typeof resolveResponse !== 'object') {
8889
throw new ERR_INVALID_RETURN_VALUE(
8990
'object', 'loader resolve', resolveResponse);
@@ -98,8 +99,7 @@ class Loader {
9899
}
99100

100101
async getFormat(url) {
101-
const getFormatResponse = await this._getFormat(
102-
url, {}, defaultGetFormat);
102+
const getFormatResponse = await this._getFormat(url, {});
103103
if (typeof getFormatResponse !== 'object') {
104104
throw new ERR_INVALID_RETURN_VALUE(
105105
'object', 'loader getFormat', getFormatResponse);
@@ -137,6 +137,22 @@ class Loader {
137137
return format;
138138
}
139139

140+
async getSource(url, format) {
141+
const { source: originalSource } = await this._getSource(url, { format });
142+
143+
const allowString = format !== 'wasm';
144+
validateSource(originalSource, 'getSource', allowString);
145+
146+
let source = originalSource;
147+
for (let i = 0; i < this.transformSourceHooks.length; i += 1) {
148+
const hook = this.transformSourceHooks[i];
149+
({ source } = await hook(source, { url, format, originalSource }));
150+
validateSource(source, 'transformSource', allowString);
151+
}
152+
153+
return source;
154+
}
155+
140156
async eval(
141157
source,
142158
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
@@ -166,72 +182,84 @@ class Loader {
166182
return module.getNamespace();
167183
}
168184

185+
async importLoader(specifier) {
186+
if (cwd === undefined) {
187+
try {
188+
// `process.cwd()` can fail.
189+
cwd = process.cwd() + '/';
190+
} catch {
191+
cwd = 'file:///';
192+
}
193+
cwd = pathToFileURL(cwd).href;
194+
}
195+
196+
const { url } = await defaultResolve(specifier, cwd,
197+
{ conditions: DEFAULT_CONDITIONS });
198+
const { format } = await defaultGetFormat(url, {});
199+
200+
// !!! CRITICAL SECTION !!!
201+
// NO AWAIT OPS BETWEEN HERE AND SETTING JOB IN MODULE MAP!
202+
// YIELDING CONTROL COULD RESULT IN MAP BEING OVERRIDDEN!
203+
let job = this.moduleMap.get(url);
204+
if (job === undefined) {
205+
if (!translators.has(format))
206+
throw new ERR_UNKNOWN_MODULE_FORMAT(format);
207+
208+
const loaderInstance = translators.get(format);
209+
210+
job = new ModuleJob(this, url, loaderInstance, false, false);
211+
this.moduleMap.set(url, job);
212+
// !!! END CRITICAL SECTION !!!
213+
}
214+
215+
const { module } = await job.run();
216+
return module.getNamespace();
217+
}
218+
169219
hook(hooks) {
170220
const {
171221
resolve,
172-
dynamicInstantiate,
173222
getFormat,
174223
getSource,
175-
transformSource,
176-
getGlobalPreloadCode,
177224
} = hooks;
178225

179226
// Use .bind() to avoid giving access to the Loader instance when called.
180227
if (resolve !== undefined)
181228
this._resolve = FunctionPrototypeBind(resolve, null);
182-
if (dynamicInstantiate !== undefined) {
183-
process.emitWarning(
184-
'The dynamicInstantiate loader hook has been removed.');
185-
}
186229
if (getFormat !== undefined) {
187230
this._getFormat = FunctionPrototypeBind(getFormat, null);
188231
}
189232
if (getSource !== undefined) {
190233
this._getSource = FunctionPrototypeBind(getSource, null);
191234
}
192-
if (transformSource !== undefined) {
193-
this._transformSource = FunctionPrototypeBind(transformSource, null);
194-
}
195-
if (getGlobalPreloadCode !== undefined) {
196-
this._getGlobalPreloadCode =
197-
FunctionPrototypeBind(getGlobalPreloadCode, null);
198-
}
199235
}
200236

201237
runGlobalPreloadCode() {
202-
if (!this._getGlobalPreloadCode) {
203-
return;
204-
}
205-
const preloadCode = this._getGlobalPreloadCode();
206-
if (preloadCode === null) {
207-
return;
208-
}
238+
for (let i = 0; i < this.globalPreloadCode.length; i += 1) {
239+
const preloadCode = this.globalPreloadCode[i];
209240

210-
if (typeof preloadCode !== 'string') {
211-
throw new ERR_INVALID_RETURN_VALUE(
212-
'string', 'loader getGlobalPreloadCode', preloadCode);
241+
const { compileFunction } = require('vm');
242+
const preloadInit = compileFunction(preloadCode, ['getBuiltin'], {
243+
filename: '<preload>',
244+
});
245+
const { NativeModule } = require('internal/bootstrap/loaders');
246+
247+
preloadInit.call(globalThis, (builtinName) => {
248+
if (NativeModule.canBeRequiredByUsers(builtinName)) {
249+
return require(builtinName);
250+
}
251+
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
252+
});
213253
}
214-
const { compileFunction } = require('vm');
215-
const preloadInit = compileFunction(preloadCode, ['getBuiltin'], {
216-
filename: '<preload>',
217-
});
218-
const { NativeModule } = require('internal/bootstrap/loaders');
219-
220-
preloadInit.call(globalThis, (builtinName) => {
221-
if (NativeModule.canBeRequiredByUsers(builtinName)) {
222-
return require(builtinName);
223-
}
224-
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
225-
});
226254
}
227255

228256
async getModuleJob(specifier, parentURL) {
229257
const url = await this.resolve(specifier, parentURL);
230258
const format = await this.getFormat(url);
259+
260+
// !!! CRITICAL SECTION !!!
261+
// NO AWAIT OPS BETWEEN HERE AND SETTING JOB IN MODULE MAP
231262
let job = this.moduleMap.get(url);
232-
// CommonJS will set functions for lazy job evaluation.
233-
if (typeof job === 'function')
234-
this.moduleMap.set(url, job = job());
235263
if (job !== undefined)
236264
return job;
237265

@@ -245,6 +273,8 @@ class Loader {
245273
job = new ModuleJob(this, url, loaderInstance, parentURL === undefined,
246274
inspectBrk);
247275
this.moduleMap.set(url, job);
276+
// !!! END CRITICAL SECTION !!!
277+
248278
return job;
249279
}
250280
}

lib/internal/modules/esm/resolve.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,8 +764,7 @@ function resolveAsCommonJS(specifier, parentURL) {
764764
}
765765
}
766766

767-
function defaultResolve(specifier, context = {}, defaultResolveUnused) {
768-
let { parentURL, conditions } = context;
767+
function defaultResolve(specifier, { parentURL, conditions } = {}) {
769768
let parsed;
770769
try {
771770
parsed = new URL(specifier);

lib/internal/modules/esm/transform_source.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)