Skip to content

Commit 551c77a

Browse files
module: use CJS conditions for hook-loaded require
Pass CommonJS package export conditions through synchronous resolution that blocks on async loader hooks when the request originates from require() in a CommonJS module loaded by module.register(). This keeps async resolve hooks from seeing ESM import conditions for require.resolve(). Fixes: #51327 Signed-off-by: Bob Put <bobu.work@gmail.com>
1 parent 7b08df7 commit 551c77a

4 files changed

Lines changed: 107 additions & 18 deletions

File tree

lib/internal/modules/esm/hooks.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,11 @@ function defineImportAssertionAlias(context) {
115115
* Calling the asynchronous `load` hook asynchronously.
116116
* @property {(url: string, context: object, defaultLoad: Function) => LoadResult} [loadSync]
117117
* Calling the asynchronous `load` hook synchronously.
118-
* @property {(originalSpecifier: string, parentURL: string,
119-
* importAttributes: Record<string, string>) => Promise<ResolveResult>} resolve
118+
* @property {(originalSpecifier: string, parentURL: string, importAttributes:
119+
* Record<string, string>, conditions?: string[]) => Promise<ResolveResult>} resolve
120120
* Calling the asynchronous `resolve` hook asynchronously.
121-
* @property {(originalSpecifier: string, parentURL: string,
122-
* importAttributes: Record<string, string>) => ResolveResult} [resolveSync]
121+
* @property {(originalSpecifier: string, parentURL: string, importAttributes:
122+
* Record<string, string>, conditions?: string[]) => ResolveResult} [resolveSync]
123123
* Calling the asynchronous `resolve` hook synchronously.
124124
* @property {(specifier: string, parentURL: string) => any} register Register asynchronous loader hooks
125125
* @property {() => void} waitForLoaderHookInitialization Force loading of hooks.
@@ -219,18 +219,20 @@ class AsyncLoaderHooksOnLoaderHookWorker {
219219
* @param {string} [parentURL] The URL path of the module's parent.
220220
* @param {ImportAttributes} [importAttributes] Attributes from the import
221221
* statement or expression.
222+
* @param {string[]} [conditions] Package export conditions for this request.
222223
* @returns {Promise<{ format: string, url: URL['href'] }>}
223224
*/
224225
async resolve(
225226
originalSpecifier,
226227
parentURL,
227228
importAttributes = { __proto__: null },
229+
conditions = getDefaultConditions(),
228230
) {
229231
throwIfInvalidParentURL(parentURL);
230232

231233
const chain = this.#chains.resolve;
232234
const context = {
233-
conditions: getDefaultConditions(),
235+
conditions,
234236
importAttributes,
235237
parentURL,
236238
};
@@ -834,15 +836,30 @@ class AsyncLoaderHooksProxiedToLoaderHookWorker {
834836
* @param {string} [parentURL] The URL path of the module's parent.
835837
* @param {ImportAttributes} importAttributes Attributes from the import
836838
* statement or expression.
839+
* @param {string[]} [conditions] Package export conditions for this request.
837840
* @returns {{ format: string, url: URL['href'] }}
838841
*/
839-
resolve(originalSpecifier, parentURL, importAttributes) {
840-
return asyncLoaderHookWorker.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
842+
resolve(originalSpecifier, parentURL, importAttributes, conditions) {
843+
return asyncLoaderHookWorker.makeAsyncRequest(
844+
'resolve',
845+
undefined,
846+
originalSpecifier,
847+
parentURL,
848+
importAttributes,
849+
conditions,
850+
);
841851
}
842852

843-
resolveSync(originalSpecifier, parentURL, importAttributes) {
853+
resolveSync(originalSpecifier, parentURL, importAttributes, conditions) {
844854
// This happens only as a result of `import.meta.resolve` calls, which must be sync per spec.
845-
return asyncLoaderHookWorker.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
855+
return asyncLoaderHookWorker.makeSyncRequest(
856+
'resolve',
857+
undefined,
858+
originalSpecifier,
859+
parentURL,
860+
importAttributes,
861+
conditions,
862+
);
846863
}
847864

848865
/**

lib/internal/modules/esm/loader.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const {
6161
setImportMetaResolveInitializer,
6262
} = internalBinding('module_wrap');
6363
const {
64+
getCjsConditionsArray,
6465
urlToFilename,
6566
} = require('internal/modules/helpers');
6667
const {
@@ -552,7 +553,9 @@ class ModuleLoader {
552553
return job;
553554
}
554555

555-
const context = { format, importAttributes, __proto__: null };
556+
const conditions = requestType === kRequireInImportedCJS ?
557+
getCjsConditionsArray() : this.#defaultConditions;
558+
const context = { format, importAttributes, conditions, __proto__: null };
556559

557560
let moduleOrModulePromise;
558561
if (requestType === kRequireInImportedCJS) {
@@ -600,7 +603,9 @@ class ModuleLoader {
600603
let maybePromise;
601604
if (requestType === kRequireInImportedCJS || requestType === kImportInRequiredESM) {
602605
// In these two cases, resolution must be synchronous.
603-
maybePromise = this.resolveSync(parentURL, request);
606+
const conditions = requestType === kRequireInImportedCJS ?
607+
getCjsConditionsArray() : this.#defaultConditions;
608+
maybePromise = this.resolveSync(parentURL, request, false, conditions);
604609
assert(!isPromise(maybePromise));
605610
} else {
606611
maybePromise = this.#resolve(parentURL, request);
@@ -680,7 +685,7 @@ class ModuleLoader {
680685
const specifier = `${request.specifier}`;
681686
const importAttributes = request.attributes ?? kEmptyObject;
682687
// TODO(joyeecheung): invoke the synchronous hooks in the default step on loader thread.
683-
return this.#asyncLoaderHooks.resolve(specifier, parentURL, importAttributes);
688+
return this.#asyncLoaderHooks.resolve(specifier, parentURL, importAttributes, request.conditions);
684689
}
685690

686691
return this.resolveSync(parentURL, request);
@@ -718,7 +723,12 @@ class ModuleLoader {
718723
*/
719724
#resolveAndMaybeBlockOnLoaderThread(out, specifier, context) {
720725
if (this.#asyncLoaderHooks?.resolveSync) {
721-
return this.#asyncLoaderHooks.resolveSync(specifier, context.parentURL, context.importAttributes);
726+
return this.#asyncLoaderHooks.resolveSync(
727+
specifier,
728+
context.parentURL,
729+
context.importAttributes,
730+
context.conditions,
731+
);
722732
}
723733
out.isResolvedByDefaultResolve = true;
724734
return this.#cachedDefaultResolve(specifier, context);
@@ -735,9 +745,15 @@ class ModuleLoader {
735745
* @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks().
736746
* This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized
737747
* by module.register()`) which invokes the CJS resolution separately from the hook chain.
748+
* @param {string[]} [conditions] Package export conditions for this request.
738749
* @returns {ResolveResult}
739750
*/
740-
resolveSync(parentURL, request, shouldSkipSyncHooks = false) {
751+
resolveSync(
752+
parentURL,
753+
request,
754+
shouldSkipSyncHooks = false,
755+
conditions = request.conditions ?? this.#defaultConditions,
756+
) {
741757
const specifier = `${request.specifier}`;
742758
const importAttributes = request.attributes ?? kEmptyObject;
743759
// Use an output parameter to track the state and avoid polluting the user-visible resolve results.
@@ -747,14 +763,14 @@ class ModuleLoader {
747763
let isResolvedBySyncHooks = false;
748764
if (!shouldSkipSyncHooks && syncResolveHooks.length) {
749765
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
750-
result = resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
766+
result = resolveWithSyncHooks(specifier, parentURL, importAttributes, conditions,
751767
this.#resolveAndMaybeBlockOnLoaderThread.bind(this, out));
752768
// If the default step ran, sync hooks did not short-circuit the resolution.
753769
isResolvedBySyncHooks = !out.isResolvedByDefaultResolve;
754770
} else {
755771
const context = {
756772
...request,
757-
conditions: this.#defaultConditions,
773+
conditions,
758774
parentURL,
759775
importAttributes,
760776
__proto__: null,
@@ -829,7 +845,7 @@ class ModuleLoader {
829845
url,
830846
context.format,
831847
context.importAttributes,
832-
this.#defaultConditions,
848+
context.conditions ?? this.#defaultConditions,
833849
this.#loadAndMaybeBlockOnLoaderThread.bind(this, out),
834850
validateLoadSloppy,
835851
);

lib/internal/modules/esm/translators.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const assert = require('internal/assert');
2525
const { dirname, extname } = require('path');
2626
const {
2727
assertBufferSource,
28+
getCjsConditionsArray,
2829
loadBuiltinModule,
2930
stringify,
3031
stripBOM,
@@ -178,7 +179,12 @@ function loadCJSModuleWithSpecialRequire(module, source, url, filename, isMain,
178179
}
179180
}
180181

181-
const request = { specifier, __proto__: null, attributes: kEmptyObject };
182+
const request = {
183+
specifier,
184+
__proto__: null,
185+
attributes: kEmptyObject,
186+
conditions: getCjsConditionsArray(),
187+
};
182188
// Skip sync hooks in resolveSync since resolveForCJSWithHooks already ran them above.
183189
const { url: resolvedURL } = cascadedLoader.resolveSync(url, request, /* shouldSkipSyncHooks */ true);
184190
return urlToFilename(resolvedURL);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import '../common/index.mjs';
2+
import assert from 'node:assert';
3+
import { register } from 'node:module';
4+
5+
const loader = `
6+
import assert from 'node:assert';
7+
8+
function assertCjsConditions(context) {
9+
assert.ok(
10+
context.conditions.includes('require'),
11+
\`Conditions should include "require": \${JSON.stringify(context.conditions)}\`,
12+
);
13+
assert.ok(
14+
!context.conditions.includes('import'),
15+
\`Conditions should not include "import": \${JSON.stringify(context.conditions)}\`,
16+
);
17+
}
18+
19+
export async function resolve(specifier, context, nextResolve) {
20+
if (specifier === 'custom:cjs') {
21+
return { url: 'custom:cjs', format: 'commonjs', shortCircuit: true };
22+
}
23+
24+
if (specifier === 'node:target') {
25+
assertCjsConditions(context);
26+
return { url: 'node:fs', shortCircuit: true };
27+
}
28+
29+
return nextResolve(specifier, context);
30+
}
31+
32+
export async function load(url, context, nextLoad) {
33+
if (url === 'custom:cjs') {
34+
return {
35+
format: 'commonjs',
36+
source: \`
37+
module.exports = require.resolve('node:target');
38+
\`,
39+
shortCircuit: true,
40+
};
41+
}
42+
43+
return nextLoad(url, context);
44+
}
45+
`;
46+
47+
register(`data:text/javascript,${encodeURIComponent(loader)}`);
48+
49+
const ns = await import('custom:cjs');
50+
assert.strictEqual(ns.default, 'node:fs');

0 commit comments

Comments
 (0)