Skip to content

Commit 7d1b217

Browse files
committed
module: do not invoke resolve hooks twice for imported cjs
Previously the resolve hook can be invoked twice from the synthetic module evaluation step of imported CJS in the extra module._load() call that's invoked on the resolved full path. Add an option to avoid it, since the resolution and loading has already been done before. PR-URL: #61529 Fixes: #57125 Refs: #55808 Refs: #56241 Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent 6df0cdf commit 7d1b217

File tree

5 files changed

+60
-11
lines changed

5 files changed

+60
-11
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ let statCache = null;
234234
*
235235
* See more {@link Module._load}
236236
*/
237-
function wrapModuleLoad(request, parent, isMain) {
237+
function wrapModuleLoad(request, parent, isMain, options) {
238238
const logLabel = `[${parent?.id || ''}] [${request}]`;
239239
const traceLabel = `require('${request}')`;
240240
const channel = onRequire();
@@ -247,11 +247,11 @@ function wrapModuleLoad(request, parent, isMain) {
247247
__proto__: null,
248248
parentFilename: parent?.filename,
249249
id: request,
250-
}, Module, request, parent, isMain);
250+
}, Module, request, parent, isMain, options);
251251
}
252252
// No subscribers, skip the wrapping to avoid clobbering stack traces
253253
// and debugging steps.
254-
return Module._load(request, parent, isMain);
254+
return Module._load(request, parent, isMain, options);
255255
} finally {
256256
endTimer(logLabel, traceLabel);
257257
}
@@ -1015,9 +1015,10 @@ function getExportsForCircularRequire(module) {
10151015
* @param {string} specifier
10161016
* @param {Module|undefined} parent
10171017
* @param {boolean} isMain
1018+
* @param {boolean} shouldSkipModuleHooks
10181019
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
10191020
*/
1020-
function resolveForCJSWithHooks(specifier, parent, isMain) {
1021+
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks) {
10211022
let defaultResolvedURL;
10221023
let defaultResolvedFilename;
10231024
let format;
@@ -1040,7 +1041,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain) {
10401041
}
10411042

10421043
// Fast path: no hooks, just return simple results.
1043-
if (!resolveHooks.length) {
1044+
if (!resolveHooks.length || shouldSkipModuleHooks) {
10441045
const filename = defaultResolveImpl(specifier, parent, isMain);
10451046
return { __proto__: null, url: defaultResolvedURL, filename, format };
10461047
}
@@ -1093,7 +1094,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain) {
10931094
}
10941095

10951096
const result = { __proto__: null, url, format, filename, parentURL };
1096-
debug('resolveForCJSWithHooks', specifier, parent?.id, '->', result);
1097+
debug('resolveForCJSWithHooks', specifier, parent?.id, isMain, shouldSkipModuleHooks, '->', result);
10971098
return result;
10981099
}
10991100

@@ -1186,8 +1187,10 @@ function loadBuiltinWithHooks(id, url, format) {
11861187
* @param {string} request Specifier of module to load via `require`
11871188
* @param {Module} parent Absolute path of the module importing the child
11881189
* @param {boolean} isMain Whether the module is the main entry point
1190+
* @param {object|undefined} options Additional options for loading the module
1191+
* @returns {object}
11891192
*/
1190-
Module._load = function(request, parent, isMain) {
1193+
Module._load = function(request, parent, isMain, options = kEmptyObject) {
11911194
let relResolveCacheIdentifier;
11921195
if (parent) {
11931196
debug('Module._load REQUEST %s parent: %s', request, parent.id);
@@ -1210,7 +1213,7 @@ Module._load = function(request, parent, isMain) {
12101213
}
12111214
}
12121215

1213-
const resolveResult = resolveForCJSWithHooks(request, parent, isMain);
1216+
const resolveResult = resolveForCJSWithHooks(request, parent, isMain, options.shouldSkipModuleHooks);
12141217
let { format } = resolveResult;
12151218
const { url, filename } = resolveResult;
12161219

lib/internal/modules/esm/translators.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ translators.set('module', function moduleStrategy(url, translateContext, parentU
111111
return module;
112112
});
113113

114+
const kShouldSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: true };
114115
/**
115116
* Loads a CommonJS module via the ESM Loader sync CommonJS translator.
116117
* This translator creates its own version of the `require` function passed into CommonJS modules.
@@ -144,7 +145,9 @@ function loadCJSModule(module, source, url, filename, isMain) {
144145
importAttributes = { __proto__: null, type: 'json' };
145146
break;
146147
case '.node':
147-
return wrapModuleLoad(specifier, module);
148+
// If it gets here in the translators, the hooks must have already been invoked
149+
// in the loader. Skip them in the synthetic module evaluation step.
150+
return wrapModuleLoad(specifier, module, false, kShouldSkipModuleHooks);
148151
default:
149152
// fall through
150153
}
@@ -282,7 +285,9 @@ function createCJSNoSourceModuleWrap(url, parentURL) {
282285
debug(`Loading CJSModule ${url}`);
283286

284287
if (!module.loaded) {
285-
wrapModuleLoad(filename, null, isMain);
288+
// If it gets here in the translators, the hooks must have already been invoked
289+
// in the loader. Skip them in the synthetic module evaluation step.
290+
wrapModuleLoad(filename, null, isMain, kShouldSkipModuleHooks);
286291
}
287292

288293
/** @type {import('./loader').ModuleExports} */
@@ -325,7 +330,9 @@ translators.set('require-commonjs-typescript', (url, translateContext, parentURL
325330
// This goes through Module._load to accommodate monkey-patchers.
326331
function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain) {
327332
assert(module === CJSModule._cache[filename]);
328-
wrapModuleLoad(filename, undefined, isMain);
333+
// If it gets here in the translators, the hooks must have already been invoked
334+
// in the loader. Skip them in the synthetic module evaluation step.
335+
wrapModuleLoad(filename, undefined, isMain, kShouldSkipModuleHooks);
329336
}
330337

331338
// Handle CommonJS modules referenced by `import` statements or expressions,

test/fixtures/value.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports.value = 42;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
// Test that load hook in imported CJS only gets invoked once.
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const { registerHooks } = require('module');
7+
const path = require('path');
8+
const { pathToFileURL } = require('url');
9+
10+
const hook = registerHooks({
11+
load: common.mustCall(function(url, context, nextLoad) {
12+
assert.strictEqual(url, pathToFileURL(path.resolve(__dirname, '../fixtures/value.cjs')).href);
13+
return nextLoad(url, context);
14+
}, 1),
15+
});
16+
17+
import('../fixtures/value.cjs').then(common.mustCall((result) => {
18+
assert.strictEqual(result.value, 42);
19+
hook.deregister();
20+
}));
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
// Test that resolve hook in imported CJS only gets invoked once.
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const { registerHooks } = require('module');
7+
8+
const hook = registerHooks({
9+
resolve: common.mustCall(function(specifier, context, nextResolve) {
10+
assert.strictEqual(specifier, '../fixtures/value.cjs');
11+
return nextResolve(specifier, context);
12+
}, 1),
13+
});
14+
15+
import('../fixtures/value.cjs').then(common.mustCall((result) => {
16+
assert.strictEqual(result.value, 42);
17+
hook.deregister();
18+
}));

0 commit comments

Comments
 (0)