module: fix conditions override in synchronous resolve hooks

1. Make sure that the conditions are converted into arrays when
  being passed into user hooks.
2. Pass the conditions from user hooks into the ESM resolution
  so that it takes effect.

PR-URL: https://github.com/nodejs/node/pull/59011
Fixes: https://github.com/nodejs/node/issues/59003
Reviewed-By: Zeyu "Alex" Yang <himself65@outlook.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
This commit is contained in:
Joyee Cheung 2025-07-26 11:13:11 +02:00 committed by GitHub
parent a7999c602c
commit 6ea421a3d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 269 additions and 33 deletions

View file

@ -0,0 +1,2 @@
exports.result = 'default';

View file

@ -0,0 +1,2 @@
export const result = 'foo-esm';

View file

@ -0,0 +1,2 @@
exports.result = 'foo';

View file

@ -0,0 +1,28 @@
{
"exports": {
".": {
"foo": "./foo.cjs",
"foo-esm": "./foo-esm.mjs",
"default": "./default.cjs"
},
"./second": {
"foo": "./foo.cjs",
"foo-esm": "./foo-esm.mjs",
"default": "./default.cjs"
},
"./third": {
"foo": "./foo.cjs",
"foo-esm": "./foo-esm.mjs",
"default": "./default.cjs"
},
"./fourth": {
"foo": "./foo.cjs",
"foo-esm": "./foo-esm.mjs",
"default": "./default.cjs"
},
"./no-default": {
"foo": "./foo.cjs",
"foo-esm": "./foo-esm.mjs"
}
}
}

View file

@ -0,0 +1,57 @@
// Similar to test-module-hooks-custom-conditions.mjs, but checking the
// real require() instead of the re-invented one for imported CJS.
'use strict';
const common = require('../common');
const { registerHooks } = require('node:module');
const assert = require('node:assert');
const { cjs, esm } = require('../fixtures/es-modules/custom-condition/load.cjs');
(async () => {
// Without hooks, the default condition is used.
assert.strictEqual(cjs('foo').result, 'default');
assert.strictEqual((await esm('foo')).result, 'default');
// Prepending 'foo' to the conditions array in the resolve hook should
// allow a CJS to be resolved with that condition.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
assert(Array.isArray(context.conditions));
context.conditions = ['foo', ...context.conditions];
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo/second').result, 'foo');
assert.strictEqual((await esm('foo/second')).result, 'foo');
hooks.deregister();
}
// Prepending 'foo-esm' to the conditions array in the resolve hook should
// allow a ESM to be resolved with that condition.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
assert(Array.isArray(context.conditions));
context.conditions = ['foo-esm', ...context.conditions];
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo/third').result, 'foo-esm');
assert.strictEqual((await esm('foo/third')).result, 'foo-esm');
hooks.deregister();
}
// Duplicating the 'foo' condition in the resolve hook should not change the result.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
assert(Array.isArray(context.conditions));
context.conditions = ['foo', ...context.conditions, 'foo'];
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo/fourth').result, 'foo');
assert.strictEqual((await esm('foo/fourth')).result, 'foo');
hooks.deregister();
}
})().then(common.mustCall());

View file

@ -0,0 +1,70 @@
// Check various special values of `conditions` in the context object
// when using synchronous module hooks to override the loaders in a
// CJS module.
'use strict';
const common = require('../common');
const { registerHooks } = require('node:module');
const assert = require('node:assert');
const { cjs, esm } = require('../fixtures/es-modules/custom-condition/load.cjs');
(async () => {
// Setting it to undefined would lead to the default conditions being used.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
context.conditions = undefined;
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo').result, 'default');
assert.strictEqual((await esm('foo')).result, 'default');
hooks.deregister();
}
// Setting it to an empty array would lead to the default conditions being used.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
context.conditions = [];
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo/second').result, 'default');
assert.strictEqual((await esm('foo/second')).result, 'default');
hooks.deregister();
}
// If the exports have no default export, it should error.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
context.conditions = [];
return nextResolve(specifier, context);
},
});
assert.throws(() => cjs('foo/no-default'), {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
});
await assert.rejects(esm('foo/no-default'), {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
});
hooks.deregister();
}
// If the exports have no default export, it should error.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
context.conditions = 'invalid';
return nextResolve(specifier, context);
},
});
assert.throws(() => cjs('foo/third'), {
code: 'ERR_INVALID_ARG_VALUE',
});
await assert.rejects(esm('foo/third'), {
code: 'ERR_INVALID_ARG_VALUE',
});
hooks.deregister();
}
})().then(common.mustCall());

View file

@ -0,0 +1,53 @@
// This tests that custom conditions can be used in module resolution hooks.
import '../common/index.mjs';
import { registerHooks } from 'node:module';
import assert from 'node:assert';
import { cjs, esm } from '../fixtures/es-modules/custom-condition/load.cjs';
// Without hooks, the default condition is used.
assert.strictEqual(cjs('foo').result, 'default');
assert.strictEqual((await esm('foo')).result, 'default');
// Prepending 'foo' to the conditions array in the resolve hook should
// allow a CJS to be resolved with that condition.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
assert(Array.isArray(context.conditions));
context.conditions = ['foo', ...context.conditions];
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo/second').result, 'foo');
assert.strictEqual((await esm('foo/second')).result, 'foo');
hooks.deregister();
}
// Prepending 'foo-esm' to the conditions array in the resolve hook should
// allow a ESM to be resolved with that condition.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
assert(Array.isArray(context.conditions));
context.conditions = ['foo-esm', ...context.conditions];
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo/third').result, 'foo-esm');
assert.strictEqual((await esm('foo/third')).result, 'foo-esm');
hooks.deregister();
}
// Duplicating the 'foo' condition in the resolve hook should not change the result.
{
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
assert(Array.isArray(context.conditions));
context.conditions = ['foo', ...context.conditions, 'foo'];
return nextResolve(specifier, context);
},
});
assert.strictEqual(cjs('foo/fourth').result, 'foo');
assert.strictEqual((await esm('foo/fourth')).result, 'foo');
hooks.deregister();
}