Skip to content

Commit db791a0

Browse files
committed
module: handle null source from async loader hooks in sync hooks
This relaxes the validation in sync hooks so that it accepts the quirky nullish source returned by the default step of the async loader when the module being loaded is CommonJS. When there are no customization hooks registered, a saner synchronous default load step is used to use a property instead of a reset nullish source to signify that the module should go through the CJS monkey patching routes and reduce excessive reloading from disk.
1 parent f6f8eb7 commit db791a0

File tree

15 files changed

+245
-103
lines changed

15 files changed

+245
-103
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ const {
177177
registerHooks,
178178
resolveHooks,
179179
resolveWithHooks,
180+
validateLoadStrict,
180181
} = require('internal/modules/customization_hooks');
181182
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
182183
const packageJsonReader = require('internal/modules/package_json_reader');
@@ -1175,7 +1176,7 @@ function loadBuiltinWithHooks(id, url, format) {
11751176
url ??= `node:${id}`;
11761177
// TODO(joyeecheung): do we really want to invoke the load hook for the builtins?
11771178
const loadResult = loadWithHooks(url, format || 'builtin', /* importAttributes */ undefined,
1178-
getCjsConditionsArray(), getDefaultLoad(url, id));
1179+
getCjsConditionsArray(), getDefaultLoad(url, id), validateLoadStrict);
11791180
if (loadResult.format && loadResult.format !== 'builtin') {
11801181
return undefined; // Format has been overridden, return undefined for the caller to continue loading.
11811182
}
@@ -1791,10 +1792,11 @@ function loadSource(mod, filename, formatFromNode) {
17911792
mod[kURL] = convertCJSFilenameToURL(filename);
17921793
}
17931794

1795+
const defaultLoad = getDefaultLoad(mod[kURL], filename);
17941796
const loadResult = loadWithHooks(mod[kURL], mod[kFormat], /* importAttributes */ undefined,
17951797
getCjsConditionsArray(),
1796-
getDefaultLoad(mod[kURL], filename));
1797-
1798+
defaultLoad,
1799+
validateLoadStrict);
17981800
// Reset the module properties with load hook results.
17991801
if (loadResult.format !== undefined) {
18001802
mod[kFormat] = loadResult.format;

lib/internal/modules/customization_hooks.js

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -262,29 +262,55 @@ function validateResolve(specifier, context, result) {
262262
*/
263263

264264
/**
265-
* Validate the result returned by a chain of resolve hook.
265+
* Validate the result returned by a chain of load hook.
266266
* @param {string} url URL passed into the hooks.
267267
* @param {ModuleLoadContext} context Context passed into the hooks.
268268
* @param {ModuleLoadResult} result Result produced by load hooks.
269269
* @returns {ModuleLoadResult}
270270
*/
271-
function validateLoad(url, context, result) {
271+
function validateLoadStrict(url, context, result) {
272+
validateSourceStrict(url, context, result);
273+
validateFormat(url, context, result);
274+
return result;
275+
}
276+
277+
function validateLoadSloppy(url, context, result) {
278+
validateSourceSloppy(url, context, result);
279+
validateFormat(url, context, result);
280+
return result;
281+
}
282+
283+
function validateSourceStrict(url, context, result) {
272284
const { source, format } = result;
273285
// To align with module.register(), the load hooks are still invoked for
274286
// the builtins even though the default load step only provides null as source,
275287
// and any source content for builtins provided by the user hooks are ignored.
276288
if (!StringPrototypeStartsWith(url, 'node:') &&
277289
typeof result.source !== 'string' &&
278290
!isAnyArrayBuffer(source) &&
279-
!isArrayBufferView(source)) {
291+
!isArrayBufferView(source) &&
292+
format !== 'addon') {
280293
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
281294
'a string, an ArrayBuffer, or a TypedArray',
282295
'load',
283296
'source',
284297
source,
285298
);
286299
}
300+
}
287301

302+
function validateSourceSloppy(url, context, result) {
303+
const { source, format } = result;
304+
if (format === 'commonjs' && source == null) {
305+
// Accommodate the quirk in defaultLoad used by asynchronous loader hooks
306+
// which sets source to null for commonjs.
307+
return;
308+
}
309+
validateSourceStrict(url, context, result);
310+
}
311+
312+
function validateFormat(url, context, result) {
313+
const { format } = result;
288314
if (typeof format !== 'string' && format !== undefined) {
289315
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
290316
'a string',
@@ -293,12 +319,6 @@ function validateLoad(url, context, result) {
293319
format,
294320
);
295321
}
296-
297-
return {
298-
__proto__: null,
299-
format,
300-
source,
301-
};
302322
}
303323

304324
class ModuleResolveContext {
@@ -338,9 +358,10 @@ let decoder;
338358
* @param {ImportAttributes|undefined} importAttributes
339359
* @param {string[]} conditions
340360
* @param {(url: string, context: ModuleLoadContext) => ModuleLoadResult} defaultLoad
361+
* @param {(url: string, context: ModuleLoadContext, result: ModuleLoadResult) => ModuleLoadResult} validateLoad
341362
* @returns {ModuleLoadResult}
342363
*/
343-
function loadWithHooks(url, originalFormat, importAttributes, conditions, defaultLoad) {
364+
function loadWithHooks(url, originalFormat, importAttributes, conditions, defaultLoad, validateLoad) {
344365
debug('loadWithHooks', url, originalFormat);
345366
const context = new ModuleLoadContext(originalFormat, importAttributes, conditions);
346367
if (loadHooks.length === 0) {
@@ -403,4 +424,6 @@ module.exports = {
403424
registerHooks,
404425
resolveHooks,
405426
resolveWithHooks,
427+
validateLoadStrict,
428+
validateLoadSloppy,
406429
};

lib/internal/modules/esm/hooks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const {
6161
SHARED_MEMORY_BYTE_LENGTH,
6262
WORKER_TO_MAIN_THREAD_NOTIFICATION,
6363
} = require('internal/modules/esm/shared_constants');
64-
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
64+
let debug = require('internal/util/debuglog').debuglog('async_loader_worker', (fn) => {
6565
debug = fn;
6666
});
6767
let importMetaInitializer;

lib/internal/modules/esm/load.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,34 @@ function defaultLoadSync(url, context = kEmptyObject) {
141141

142142
throwIfUnsupportedURLScheme(urlInstance, false);
143143

144+
let shouldBeReloadedByCJSLoader = false;
144145
if (urlInstance.protocol === 'node:') {
145146
source = null;
146-
} else if (source == null) {
147-
({ responseURL, source } = getSourceSync(urlInstance, context));
148-
context.source = source;
149-
}
147+
format ??= 'builtin';
148+
} else if (format === 'addon') {
149+
// Skip loading addon file content. It must be loaded with dlopen from file system.
150+
source = null;
151+
} else {
152+
if (source == null) {
153+
({ responseURL, source } = getSourceSync(urlInstance, context));
154+
context = { __proto__: context, source };
155+
}
150156

151-
format ??= defaultGetFormat(urlInstance, context);
157+
// Now that we have the source for the module, run `defaultGetFormat` to detect its format.
158+
format ??= defaultGetFormat(urlInstance, context);
152159

160+
// For backward compatibility reasons, we need to let go through Module._load
161+
// again.
162+
shouldBeReloadedByCJSLoader = (format === 'commonjs');
163+
}
153164
validateAttributes(url, format, importAttributes);
154165

155166
return {
156167
__proto__: null,
157168
format,
158169
responseURL,
159170
source,
171+
shouldBeReloadedByCJSLoader,
160172
};
161173
}
162174

lib/internal/modules/esm/loader.js

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const {
5555
resolveWithHooks,
5656
loadHooks,
5757
loadWithHooks,
58+
validateLoadSloppy,
5859
} = require('internal/modules/customization_hooks');
5960
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer;
6061

@@ -146,6 +147,10 @@ let hooksProxy;
146147
* @typedef {ArrayBuffer|TypedArray|string} ModuleSource
147148
*/
148149

150+
/**
151+
* @typedef { format: ModuleFormat, source: ModuleSource, translatorKey: string } TranslateContext
152+
*/
153+
149154
/**
150155
* This class covers the base machinery of module loading. To add custom
151156
* behavior you can pass a customizations object and this object will be
@@ -503,16 +508,18 @@ class ModuleLoader {
503508

504509
const loadResult = this.#loadSync(url, { format, importAttributes });
505510

511+
const formatFromLoad = loadResult.format;
506512
// Use the synchronous commonjs translator which can deal with cycles.
507-
const finalFormat = loadResult.format === 'commonjs' ? 'commonjs-sync' : loadResult.format;
513+
const translatorKey = formatFromLoad === 'commonjs' ? 'commonjs-sync' : formatFromLoad;
508514

509-
if (finalFormat === 'wasm') {
515+
if (translatorKey === 'wasm') {
510516
assert.fail('WASM is currently unsupported by require(esm)');
511517
}
512518

513519
const { source } = loadResult;
514520
const isMain = (parentURL === undefined);
515-
const wrap = this.#translate(url, finalFormat, source, parentURL);
521+
const translateContext = { format: formatFromLoad, source, translatorKey, __proto__: null };
522+
const wrap = this.#translate(url, translateContext, parentURL);
516523
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
517524

518525
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
@@ -521,7 +528,7 @@ class ModuleLoader {
521528

522529
const cjsModule = wrap[imported_cjs_symbol];
523530
if (cjsModule) {
524-
assert(finalFormat === 'commonjs-sync');
531+
assert(translatorKey === 'commonjs-sync');
525532
// Check if the ESM initiating import CJS is being required by the same CJS module.
526533
if (cjsModule?.[kIsExecuting]) {
527534
const parentFilename = urlToFilename(parentURL);
@@ -545,22 +552,22 @@ class ModuleLoader {
545552
* Translate a loaded module source into a ModuleWrap. This is run synchronously,
546553
* but the translator may return the ModuleWrap in a Promise.
547554
* @param {string} url URL of the module to be translated.
548-
* @param {string} format Format of the module to be translated. This is used to find
549-
* matching translators.
550-
* @param {ModuleSource} source Source of the module to be translated.
551-
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
555+
* @param {TranslateContext} translateContext Context for the translator
556+
* @param {string|undefined} parentURL URL of the module initiating the module loading for the first time.
557+
* Undefined if it's the entry point.
552558
* @returns {ModuleWrap}
553559
*/
554-
#translate(url, format, source, parentURL) {
560+
#translate(url, translateContext, parentURL) {
561+
const { translatorKey, format } = translateContext;
555562
this.validateLoadResult(url, format);
556-
const translator = getTranslators().get(format);
563+
const translator = getTranslators().get(translatorKey);
557564

558565
if (!translator) {
559-
throw new ERR_UNKNOWN_MODULE_FORMAT(format, url);
566+
throw new ERR_UNKNOWN_MODULE_FORMAT(translatorKey, url);
560567
}
561568

562-
const result = FunctionPrototypeCall(translator, this, url, source, parentURL === undefined);
563-
assert(result instanceof ModuleWrap);
569+
const result = FunctionPrototypeCall(translator, this, url, translateContext, parentURL);
570+
assert(result instanceof ModuleWrap, `The ${format} module returned is not a ModuleWrap`);
564571
return result;
565572
}
566573

@@ -573,7 +580,8 @@ class ModuleLoader {
573580
* @returns {ModuleWrap}
574581
*/
575582
loadAndTranslateForRequireInImportedCJS(url, loadContext, parentURL) {
576-
const { format: formatFromLoad, source } = this.#loadSync(url, loadContext);
583+
const loadResult = this.#loadSync(url, loadContext);
584+
const formatFromLoad = loadResult.format;
577585

578586
if (formatFromLoad === 'wasm') { // require(wasm) is not supported.
579587
throw new ERR_UNKNOWN_MODULE_FORMAT(formatFromLoad, url);
@@ -585,15 +593,16 @@ class ModuleLoader {
585593
}
586594
}
587595

588-
let finalFormat = formatFromLoad;
596+
let translatorKey = formatFromLoad;
589597
if (formatFromLoad === 'commonjs') {
590-
finalFormat = 'require-commonjs';
598+
translatorKey = 'require-commonjs';
591599
}
592600
if (formatFromLoad === 'commonjs-typescript') {
593-
finalFormat = 'require-commonjs-typescript';
601+
translatorKey = 'require-commonjs-typescript';
594602
}
595603

596-
const wrap = this.#translate(url, finalFormat, source, parentURL);
604+
const translateContext = { ...loadResult, translatorKey, __proto__: null };
605+
const wrap = this.#translate(url, translateContext, parentURL);
597606
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
598607
return wrap;
599608
}
@@ -608,8 +617,9 @@ class ModuleLoader {
608617
*/
609618
loadAndTranslate(url, loadContext, parentURL) {
610619
const maybePromise = this.load(url, loadContext);
611-
const afterLoad = ({ format, source }) => {
612-
return this.#translate(url, format, source, parentURL);
620+
const afterLoad = (loadResult) => {
621+
const translateContext = { ...loadResult, translatorKey: loadResult.format, __proto__: null };
622+
return this.#translate(url, translateContext, parentURL);
613623
};
614624
if (isPromise(maybePromise)) {
615625
return maybePromise.then(afterLoad);
@@ -835,8 +845,8 @@ class ModuleLoader {
835845
return this.#customizations.load(url, context);
836846
}
837847

838-
defaultLoad ??= require('internal/modules/esm/load').defaultLoad;
839-
return defaultLoad(url, context);
848+
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
849+
return defaultLoadSync(url, context);
840850
}
841851

842852
/**
@@ -871,7 +881,7 @@ class ModuleLoader {
871881
// TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead
872882
// of converting them from plain objects in the hooks.
873883
return loadWithHooks(url, context.format, context.importAttributes, this.#defaultConditions,
874-
this.#loadAndMaybeBlockOnLoaderThread.bind(this));
884+
this.#loadAndMaybeBlockOnLoaderThread.bind(this), validateLoadSloppy);
875885
}
876886
return this.#loadAndMaybeBlockOnLoaderThread(url, context);
877887
}

0 commit comments

Comments
 (0)