Skip to content

module: allow cycles in require() in the CJS handling in ESM loader #58598

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
ModuleWrap,
kErrored,
kEvaluated,
kEvaluating,
kEvaluationPhase,
kInstantiated,
kUninstantiated,
Expand Down Expand Up @@ -338,8 +339,14 @@ class ModuleJob extends ModuleJobBase {
return { __proto__: null, module: this.module, namespace };
}
throw this.module.getError();

} else if (status === kEvaluated) {
} else if (status === kEvaluating || status === kEvaluated) {
// kEvaluating can show up when this is being used to deal with CJS <-> CJS cycles.
// Allow it for now, since we only need to ban ESM <-> CJS cycles which would be
// detected earlier during the linking phase, though the CJS handling in the ESM
// loader won't be able to emit warnings on pending circular exports like what
// the CJS loader does.
// TODO(joyeecheung): remove the re-invented require() in the ESM loader and
// always handle CJS using the CJS loader to eliminate the quirks.
return { __proto__: null, module: this.module, namespace: this.module.getNamespaceSync() };
}
assert.fail(`Unexpected module status ${status}.`);
Expand Down
3 changes: 1 addition & 2 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -783,11 +783,10 @@ void ModuleWrap::GetNamespaceSync(const FunctionCallbackInfo<Value>& args) {
return realm->env()->ThrowError(
"Cannot get namespace, module has not been instantiated");
case Module::Status::kInstantiated:
case Module::Status::kEvaluating:
case Module::Status::kEvaluated:
case Module::Status::kErrored:
break;
case Module::Status::kEvaluating:
UNREACHABLE();
}

if (module->IsGraphAsync()) {
Expand Down
20 changes: 20 additions & 0 deletions test/es-module/test-import-preload-require-cycle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

// This tests that --import preload does not break CJS entry points that contains
// require cycles.

require('../common');
const fixtures = require('../common/fixtures');
const { spawnSyncAndAssert } = require('../common/child_process');

spawnSyncAndAssert(
process.execPath,
[
'--import',
fixtures.fileURL('import-require-cycle/preload.mjs'),
fixtures.path('import-require-cycle/c.js'),
],
{
stdout: /cycle equality true/,
}
);
1 change: 1 addition & 0 deletions test/fixtures/import-require-cycle/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports.b = require('./b.js');
1 change: 1 addition & 0 deletions test/fixtures/import-require-cycle/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports.a = require('./a.js');
3 changes: 3 additions & 0 deletions test/fixtures/import-require-cycle/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const obj = require('./b.js');

console.log('cycle equality', obj.a.b === obj);
7 changes: 7 additions & 0 deletions test/fixtures/import-require-cycle/preload.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as mod from "module";

mod.registerHooks({
load(url, context, nextLoad) {
return nextLoad(url, context);
},
});
Loading