Skip to content

Commit 170848b

Browse files
authored
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. PR-URL: #59929 Fixes: #59384 Fixes: #57327 Refs: #59666 Refs: https://github.com/dygabo/load_module_test Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent 0c35aaf commit 170848b

File tree

14 files changed

+227
-107
lines changed

14 files changed

+227
-107
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 4 additions & 4 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,9 @@ 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,
1795-
getCjsConditionsArray(),
1796-
getDefaultLoad(mod[kURL], filename));
1797-
1797+
getCjsConditionsArray(), defaultLoad, validateLoadStrict);
17981798
// Reset the module properties with load hook results.
17991799
if (loadResult.format !== undefined) {
18001800
mod[kFormat] = loadResult.format;

lib/internal/modules/customization_hooks.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -262,29 +262,56 @@ 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+
validateSourcePermissive(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 validateSourcePermissive(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+
// See: https://github.com/nodejs/node/issues/57327#issuecomment-2701382020
308+
return;
309+
}
310+
validateSourceStrict(url, context, result);
311+
}
312+
313+
function validateFormat(url, context, result) {
314+
const { format } = result;
288315
if (typeof format !== 'string' && format !== undefined) {
289316
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
290317
'a string',
@@ -293,12 +320,6 @@ function validateLoad(url, context, result) {
293320
format,
294321
);
295322
}
296-
297-
return {
298-
__proto__: null,
299-
format,
300-
source,
301-
};
302323
}
303324

304325
class ModuleResolveContext {
@@ -338,9 +359,10 @@ let decoder;
338359
* @param {ImportAttributes|undefined} importAttributes
339360
* @param {string[]} conditions
340361
* @param {(url: string, context: ModuleLoadContext) => ModuleLoadResult} defaultLoad
362+
* @param {(url: string, context: ModuleLoadContext, result: ModuleLoadResult) => ModuleLoadResult} validateLoad
341363
* @returns {ModuleLoadResult}
342364
*/
343-
function loadWithHooks(url, originalFormat, importAttributes, conditions, defaultLoad) {
365+
function loadWithHooks(url, originalFormat, importAttributes, conditions, defaultLoad, validateLoad) {
344366
debug('loadWithHooks', url, originalFormat);
345367
const context = new ModuleLoadContext(originalFormat, importAttributes, conditions);
346368
if (loadHooks.length === 0) {
@@ -403,4 +425,6 @@ module.exports = {
403425
registerHooks,
404426
resolveHooks,
405427
resolveWithHooks,
428+
validateLoadStrict,
429+
validateLoadSloppy,
406430
};

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: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ const {
5555
resolveWithHooks,
5656
loadHooks,
5757
loadWithHooks,
58+
validateLoadSloppy,
5859
} = require('internal/modules/customization_hooks');
59-
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer;
60+
let defaultResolve, defaultLoadSync, importMetaInitializer;
6061

6162
const { tracingChannel } = require('diagnostics_channel');
6263
const onImport = tracingChannel('module.import');
@@ -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,18 +508,19 @@ 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 =
508-
loadResult.format === 'commonjs' ||
509-
loadResult.format === 'commonjs-typescript' ? 'commonjs-sync' : loadResult.format;
513+
const translatorKey = (formatFromLoad === 'commonjs' || formatFromLoad === 'commonjs-typescript') ?
514+
'commonjs-sync' : formatFromLoad;
510515

511-
if (finalFormat === 'wasm') {
516+
if (translatorKey === 'wasm') {
512517
assert.fail('WASM is currently unsupported by require(esm)');
513518
}
514519

515520
const { source } = loadResult;
516521
const isMain = (parentURL === undefined);
517-
const wrap = this.#translate(url, finalFormat, source, parentURL);
522+
const translateContext = { format: formatFromLoad, source, translatorKey, __proto__: null };
523+
const wrap = this.#translate(url, translateContext, parentURL);
518524
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
519525

520526
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
@@ -523,7 +529,7 @@ class ModuleLoader {
523529

524530
const cjsModule = wrap[imported_cjs_symbol];
525531
if (cjsModule) {
526-
assert(finalFormat === 'commonjs-sync');
532+
assert(translatorKey === 'commonjs-sync');
527533
// Check if the ESM initiating import CJS is being required by the same CJS module.
528534
if (cjsModule?.[kIsExecuting]) {
529535
const parentFilename = urlToFilename(parentURL);
@@ -547,22 +553,22 @@ class ModuleLoader {
547553
* Translate a loaded module source into a ModuleWrap. This is run synchronously,
548554
* but the translator may return the ModuleWrap in a Promise.
549555
* @param {string} url URL of the module to be translated.
550-
* @param {string} format Format of the module to be translated. This is used to find
551-
* matching translators.
552-
* @param {ModuleSource} source Source of the module to be translated.
553-
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
556+
* @param {TranslateContext} translateContext Context for the translator
557+
* @param {string|undefined} parentURL URL of the module initiating the module loading for the first time.
558+
* Undefined if it's the entry point.
554559
* @returns {ModuleWrap}
555560
*/
556-
#translate(url, format, source, parentURL) {
561+
#translate(url, translateContext, parentURL) {
562+
const { translatorKey, format } = translateContext;
557563
this.validateLoadResult(url, format);
558-
const translator = getTranslators().get(format);
564+
const translator = getTranslators().get(translatorKey);
559565

560566
if (!translator) {
561-
throw new ERR_UNKNOWN_MODULE_FORMAT(format, url);
567+
throw new ERR_UNKNOWN_MODULE_FORMAT(translatorKey, url);
562568
}
563569

564-
const result = FunctionPrototypeCall(translator, this, url, source, parentURL === undefined);
565-
assert(result instanceof ModuleWrap);
570+
const result = FunctionPrototypeCall(translator, this, url, translateContext, parentURL);
571+
assert(result instanceof ModuleWrap, `The ${format} module returned is not a ModuleWrap`);
566572
return result;
567573
}
568574

@@ -575,7 +581,8 @@ class ModuleLoader {
575581
* @returns {ModuleWrap}
576582
*/
577583
loadAndTranslateForRequireInImportedCJS(url, loadContext, parentURL) {
578-
const { format: formatFromLoad, source } = this.#loadSync(url, loadContext);
584+
const loadResult = this.#loadSync(url, loadContext);
585+
const formatFromLoad = loadResult.format;
579586

580587
if (formatFromLoad === 'wasm') { // require(wasm) is not supported.
581588
throw new ERR_UNKNOWN_MODULE_FORMAT(formatFromLoad, url);
@@ -587,15 +594,16 @@ class ModuleLoader {
587594
}
588595
}
589596

590-
let finalFormat = formatFromLoad;
597+
let translatorKey = formatFromLoad;
591598
if (formatFromLoad === 'commonjs') {
592-
finalFormat = 'require-commonjs';
599+
translatorKey = 'require-commonjs';
593600
}
594601
if (formatFromLoad === 'commonjs-typescript') {
595-
finalFormat = 'require-commonjs-typescript';
602+
translatorKey = 'require-commonjs-typescript';
596603
}
597604

598-
const wrap = this.#translate(url, finalFormat, source, parentURL);
605+
const translateContext = { ...loadResult, translatorKey, __proto__: null };
606+
const wrap = this.#translate(url, translateContext, parentURL);
599607
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
600608
return wrap;
601609
}
@@ -610,8 +618,9 @@ class ModuleLoader {
610618
*/
611619
loadAndTranslate(url, loadContext, parentURL) {
612620
const maybePromise = this.load(url, loadContext);
613-
const afterLoad = ({ format, source }) => {
614-
return this.#translate(url, format, source, parentURL);
621+
const afterLoad = (loadResult) => {
622+
const translateContext = { ...loadResult, translatorKey: loadResult.format, __proto__: null };
623+
return this.#translate(url, translateContext, parentURL);
615624
};
616625
if (isPromise(maybePromise)) {
617626
return maybePromise.then(afterLoad);
@@ -837,8 +846,8 @@ class ModuleLoader {
837846
return this.#customizations.load(url, context);
838847
}
839848

840-
defaultLoad ??= require('internal/modules/esm/load').defaultLoad;
841-
return defaultLoad(url, context);
849+
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
850+
return defaultLoadSync(url, context);
842851
}
843852

844853
/**
@@ -873,7 +882,7 @@ class ModuleLoader {
873882
// TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead
874883
// of converting them from plain objects in the hooks.
875884
return loadWithHooks(url, context.format, context.importAttributes, this.#defaultConditions,
876-
this.#loadAndMaybeBlockOnLoaderThread.bind(this));
885+
this.#loadAndMaybeBlockOnLoaderThread.bind(this), validateLoadSloppy);
877886
}
878887
return this.#loadAndMaybeBlockOnLoaderThread(url, context);
879888
}

0 commit comments

Comments
 (0)