Skip to content

Commit 875dd5b

Browse files
joyeecheungaduh95
authored andcommitted
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 44bd38b commit 875dd5b

File tree

5 files changed

+59
-11
lines changed

5 files changed

+59
-11
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ let statCache = null;
235235
* See more {@link Module._load}
236236
* @returns {object}
237237
*/
238-
function wrapModuleLoad(request, parent, isMain) {
238+
function wrapModuleLoad(request, parent, isMain, options) {
239239
const logLabel = `[${parent?.id || ''}] [${request}]`;
240240
const traceLabel = `require('${request}')`;
241241
const channel = onRequire();
@@ -248,11 +248,11 @@ function wrapModuleLoad(request, parent, isMain) {
248248
__proto__: null,
249249
parentFilename: parent?.filename,
250250
id: request,
251-
}, Module, request, parent, isMain);
251+
}, Module, request, parent, isMain, options);
252252
}
253253
// No subscribers, skip the wrapping to avoid clobbering stack traces
254254
// and debugging steps.
255-
return Module._load(request, parent, isMain);
255+
return Module._load(request, parent, isMain, options);
256256
} finally {
257257
endTimer(logLabel, traceLabel);
258258
}
@@ -1041,9 +1041,10 @@ function getExportsForCircularRequire(module) {
10411041
* @param {string} specifier
10421042
* @param {Module|undefined} parent
10431043
* @param {boolean} isMain
1044+
* @param {boolean} shouldSkipModuleHooks
10441045
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
10451046
*/
1046-
function resolveForCJSWithHooks(specifier, parent, isMain) {
1047+
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks) {
10471048
let defaultResolvedURL;
10481049
let defaultResolvedFilename;
10491050
let format;
@@ -1066,7 +1067,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain) {
10661067
}
10671068

10681069
// Fast path: no hooks, just return simple results.
1069-
if (!resolveHooks.length) {
1070+
if (!resolveHooks.length || shouldSkipModuleHooks) {
10701071
const filename = defaultResolveImpl(specifier, parent, isMain);
10711072
return { __proto__: null, url: defaultResolvedURL, filename, format };
10721073
}
@@ -1119,7 +1120,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain) {
11191120
}
11201121

11211122
const result = { __proto__: null, url, format, filename, parentURL };
1122-
debug('resolveForCJSWithHooks', specifier, parent?.id, '->', result);
1123+
debug('resolveForCJSWithHooks', specifier, parent?.id, isMain, shouldSkipModuleHooks, '->', result);
11231124
return result;
11241125
}
11251126

@@ -1212,9 +1213,10 @@ function loadBuiltinWithHooks(id, url, format) {
12121213
* @param {string} request Specifier of module to load via `require`
12131214
* @param {Module} parent Absolute path of the module importing the child
12141215
* @param {boolean} isMain Whether the module is the main entry point
1216+
* @param {object|undefined} options Additional options for loading the module
12151217
* @returns {object}
12161218
*/
1217-
Module._load = function(request, parent, isMain) {
1219+
Module._load = function(request, parent, isMain, options = kEmptyObject) {
12181220
let relResolveCacheIdentifier;
12191221
if (parent) {
12201222
debug('Module._load REQUEST %s parent: %s', request, parent.id);
@@ -1237,7 +1239,7 @@ Module._load = function(request, parent, isMain) {
12371239
}
12381240
}
12391241

1240-
const resolveResult = resolveForCJSWithHooks(request, parent, isMain);
1242+
const resolveResult = resolveForCJSWithHooks(request, parent, isMain, options.shouldSkipModuleHooks);
12411243
let { format } = resolveResult;
12421244
const { url, filename } = resolveResult;
12431245

lib/internal/modules/esm/translators.js

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

115115
const { requestTypes: { kRequireInImportedCJS } } = require('internal/modules/esm/utils');
116+
const kShouldSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: true };
116117
/**
117118
* Loads a CommonJS module via the ESM Loader sync CommonJS translator.
118119
* This translator creates its own version of the `require` function passed into CommonJS modules.
@@ -145,7 +146,9 @@ function loadCJSModule(module, source, url, filename, isMain) {
145146
importAttributes = { __proto__: null, type: 'json' };
146147
break;
147148
case '.node':
148-
return wrapModuleLoad(specifier, module);
149+
// If it gets here in the translators, the hooks must have already been invoked
150+
// in the loader. Skip them in the synthetic module evaluation step.
151+
return wrapModuleLoad(specifier, module, false, kShouldSkipModuleHooks);
149152
default:
150153
// fall through
151154
}
@@ -293,7 +296,9 @@ function createCJSNoSourceModuleWrap(url, parentURL) {
293296
debug(`Loading CJSModule ${url}`);
294297

295298
if (!module.loaded) {
296-
wrapModuleLoad(filename, null, isMain);
299+
// If it gets here in the translators, the hooks must have already been invoked
300+
// in the loader. Skip them in the synthetic module evaluation step.
301+
wrapModuleLoad(filename, null, isMain, kShouldSkipModuleHooks);
297302
}
298303

299304
/** @type {import('./loader').ModuleExports} */
@@ -336,7 +341,9 @@ translators.set('require-commonjs-typescript', (url, translateContext, parentURL
336341
// This goes through Module._load to accommodate monkey-patchers.
337342
function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain) {
338343
assert(module === CJSModule._cache[filename]);
339-
wrapModuleLoad(filename, undefined, isMain);
344+
// If it gets here in the translators, the hooks must have already been invoked
345+
// in the loader. Skip them in the synthetic module evaluation step.
346+
wrapModuleLoad(filename, undefined, isMain, kShouldSkipModuleHooks);
340347
}
341348

342349
// 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)