module: fix require(esm) on the module.register() hooks thread

On the async loader hooks thread, resolve kImportInRequiredESM and
kRequireInImportedCJS requests synchronously again, and always use
ModuleJobSync for kImportInRequiredESM. Otherwise require() of an ES
module with imports from a hooks module hit an internal assertion in
ModuleJobSync.syncLink().

Fixes: https://github.com/nodejs/node/issues/66767
Refs: https://github.com/nodejs/node/pull/62530
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/66770
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
Matteo Collina authored and GitHub committed 2026-10-17 16:26:39 +00:00
1 parent 269cab6320
commit c526694ae9
5 files changed
+46 -5

No files matched your search

+9 -5
View File
@@ -613,12 +613,13 @@ class ModuleLoader {
const isMain = (parentURL === undefined);
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
// Use ModuleJobSync whenever we're on the main thread (not the async loader hook worker),
// Use ModuleJobSync for kImportInRequiredESM, which must always be linked synchronously,
// and whenever we're on the main thread (not the async loader hook worker),
// except for kRequireInImportedCJS (TODO: consolidate that case too).
// TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too.
const ModuleJobCtor = (this.isForAsyncLoaderHookWorker ||
requestType === kRequireInImportedCJS) ?
ModuleJob : ModuleJobSync;
const ModuleJobCtor = (requestType === kImportInRequiredESM ||
(!this.isForAsyncLoaderHookWorker && requestType !== kRequireInImportedCJS)) ?
ModuleJobSync : ModuleJob;
job = new ModuleJobCtor(
this,
url,
@@ -645,9 +646,12 @@ class ModuleLoader {
*/
getOrCreateModuleJob(parentURL, request, requestType) {
let maybePromise;
if (!this.isForAsyncLoaderHookWorker) {
if (!this.isForAsyncLoaderHookWorker ||
requestType === kRequireInImportedCJS || requestType === kImportInRequiredESM) {
// On the main thread, always resolve synchronously;
// `resolveSync` coordinates with the async loader hook worker if needed.
// On the async loader hook worker, require(esm) and require() in imported CJS
// must still be resolved synchronously.
maybePromise = this.resolveSync(parentURL, request);
assert(!isPromise(maybePromise));
} else {
@@ -0,0 +1 @@
export const value = 'dep:';
@@ -0,0 +1,4 @@
import { sep } from 'node:path';
import { value } from './dep.mjs';
export const result = `${value}${typeof sep}`;
@@ -0,0 +1,9 @@
// Loaded on the module.register() hooks thread. It defines no hooks: require()-ing
// an ES module with imports while the hooks module is being loaded is enough.
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { result } = require('./esm-with-import.mjs');
if (result !== 'dep:string') {
throw new Error(`Unexpected result: ${result}`);
}
@@ -0,0 +1,23 @@
// Test that require(esm) of a module with imports works while loading a module.register() hooks module.
// Refs: https://github.com/nodejs/node/issues/66767
import '../common/index.mjs';
import { execPath } from 'node:process';
import fixtures from '../common/fixtures.js';
import { spawnSyncAndAssert } from '../common/child_process.js';
spawnSyncAndAssert(
execPath,
[
'--no-warnings',
'--input-type=module',
'--eval',
`import { register } from 'node:module';
register(${JSON.stringify(fixtures.fileURL('module-hooks/require-esm-in-hooks-thread/hooks.mjs'))});
console.log('ok');`,
],
{
stdout: 'ok',
stderr: '',
trim: true,
},
);