Skip to content

Commit 537d2c1

Browse files
jkremstargos
authored andcommitted
module: expose exports conditions to loaders
PR-URL: #31303 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent a7b6a10 commit 537d2c1

File tree

8 files changed

+114
-23
lines changed

8 files changed

+114
-23
lines changed

doc/api/esm.md

+19
Original file line numberDiff line numberDiff line change
@@ -1050,11 +1050,22 @@ and parent URL. The module specifier is the string in an `import` statement or
10501050
`import()` expression, and the parent URL is the URL of the module that imported
10511051
this one, or `undefined` if this is the main entry point for the application.
10521052

1053+
The `conditions` property on the `context` is an array of conditions for
1054+
[Conditional Exports][] that apply to this resolution request. They can be used
1055+
for looking up conditional mappings elsewhere or to modify the list when calling
1056+
the default resolution logic.
1057+
1058+
The [current set of Node.js default conditions][Conditional Exports] will always
1059+
be in the `context.conditions` list passed to the hook. If the hook wants to
1060+
ensure Node.js-compatible resolution logic, all items from this default
1061+
condition list **must** be passed through to the `defaultResolve` function.
1062+
10531063
```js
10541064
/**
10551065
* @param {string} specifier
10561066
* @param {object} context
10571067
* @param {string} context.parentURL
1068+
* @param {string[]} context.conditions
10581069
* @param {function} defaultResolve
10591070
* @returns {object} response
10601071
* @returns {string} response.url
@@ -1069,6 +1080,14 @@ export async function resolve(specifier, context, defaultResolve) {
10691080
new URL(specifier, parentURL).href : new URL(specifier).href
10701081
};
10711082
}
1083+
if (anotherCondition) {
1084+
// When calling the defaultResolve, the arguments can be modified. In this
1085+
// case it's adding another value for matching conditional exports.
1086+
return defaultResolve(specifier, {
1087+
...context,
1088+
conditions: [...context.conditions, 'another-condition'],
1089+
});
1090+
}
10721091
// Defer to Node.js for all other specifiers.
10731092
return defaultResolve(specifier, context, defaultResolve);
10741093
}

lib/internal/modules/esm/loader.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ const { validateString } = require('internal/validators');
1919
const ModuleMap = require('internal/modules/esm/module_map');
2020
const ModuleJob = require('internal/modules/esm/module_job');
2121

22-
const { defaultResolve } = require('internal/modules/esm/resolve');
22+
const {
23+
defaultResolve,
24+
DEFAULT_CONDITIONS,
25+
} = require('internal/modules/esm/resolve');
2326
const { defaultGetFormat } = require('internal/modules/esm/get_format');
2427
const { defaultGetSource } = require(
2528
'internal/modules/esm/get_source');
@@ -92,7 +95,7 @@ class Loader {
9295
validateString(parentURL, 'parentURL');
9396

9497
const resolveResponse = await this._resolve(
95-
specifier, { parentURL }, defaultResolve);
98+
specifier, { parentURL, conditions: DEFAULT_CONDITIONS }, defaultResolve);
9699
if (typeof resolveResponse !== 'object') {
97100
throw new ERR_INVALID_RETURN_VALUE(
98101
'object', 'loader resolve', resolveResponse);

lib/internal/modules/esm/resolve.js

+70-21
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ const {
44
ArrayIsArray,
55
JSONParse,
66
JSONStringify,
7+
ObjectFreeze,
78
ObjectGetOwnPropertyNames,
89
ObjectPrototypeHasOwnProperty,
910
SafeMap,
11+
SafeSet,
1012
StringPrototypeEndsWith,
1113
StringPrototypeIncludes,
1214
StringPrototypeIndexOf,
@@ -35,6 +37,7 @@ const typeFlag = getOptionValue('--input-type');
3537
const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
3638
const {
3739
ERR_INPUT_TYPE_NOT_ALLOWED,
40+
ERR_INVALID_ARG_VALUE,
3841
ERR_INVALID_MODULE_SPECIFIER,
3942
ERR_INVALID_PACKAGE_CONFIG,
4043
ERR_INVALID_PACKAGE_TARGET,
@@ -43,6 +46,20 @@ const {
4346
ERR_UNSUPPORTED_ESM_URL_SCHEME,
4447
} = require('internal/errors').codes;
4548

49+
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']);
50+
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
51+
52+
function getConditionsSet(conditions) {
53+
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
54+
if (!ArrayIsArray(conditions)) {
55+
throw new ERR_INVALID_ARG_VALUE('conditions', conditions,
56+
'expected an array');
57+
}
58+
return new SafeSet(conditions);
59+
}
60+
return DEFAULT_CONDITIONS_SET;
61+
}
62+
4663
const realpathCache = new SafeMap();
4764
const packageJSONCache = new SafeMap(); /* string -> PackageConfig */
4865

@@ -310,14 +327,18 @@ function resolveExportsTargetString(
310327
return subpathResolved;
311328
}
312329

313-
function isArrayIndex(key /* string */) { /* -> boolean */
330+
/**
331+
* @param {string} key
332+
* @returns {boolean}
333+
*/
334+
function isArrayIndex(key) {
314335
const keyNum = +key;
315336
if (`${keyNum}` !== key) return false;
316337
return keyNum >= 0 && keyNum < 0xFFFF_FFFF;
317338
}
318339

319340
function resolveExportsTarget(
320-
packageJSONUrl, target, subpath, packageSubpath, base) {
341+
packageJSONUrl, target, subpath, packageSubpath, base, conditions) {
321342
if (typeof target === 'string') {
322343
const resolved = resolveExportsTargetString(
323344
target, subpath, packageSubpath, packageJSONUrl, base);
@@ -332,7 +353,8 @@ function resolveExportsTarget(
332353
let resolved;
333354
try {
334355
resolved = resolveExportsTarget(
335-
packageJSONUrl, targetItem, subpath, packageSubpath, base);
356+
packageJSONUrl, targetItem, subpath, packageSubpath, base,
357+
conditions);
336358
} catch (e) {
337359
lastException = e;
338360
if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' ||
@@ -357,11 +379,12 @@ function resolveExportsTarget(
357379
}
358380
for (let i = 0; i < keys.length; i++) {
359381
const key = keys[i];
360-
if (key === 'node' || key === 'import' || key === 'default') {
382+
if (key === 'default' || conditions.has(key)) {
361383
const conditionalTarget = target[key];
362384
try {
363385
return resolveExportsTarget(
364-
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base);
386+
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
387+
conditions);
365388
} catch (e) {
366389
if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') continue;
367390
throw e;
@@ -397,16 +420,18 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
397420
}
398421

399422

400-
function packageMainResolve(packageJSONUrl, packageConfig, base) {
423+
function packageMainResolve(packageJSONUrl, packageConfig, base, conditions) {
401424
if (packageConfig.exists) {
402425
const exports = packageConfig.exports;
403426
if (exports !== undefined) {
404427
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
405-
return resolveExportsTarget(packageJSONUrl, exports, '', '', base);
428+
return resolveExportsTarget(packageJSONUrl, exports, '', '', base,
429+
conditions);
406430
} else if (typeof exports === 'object' && exports !== null) {
407431
const target = exports['.'];
408432
if (target !== undefined)
409-
return resolveExportsTarget(packageJSONUrl, target, '', '', base);
433+
return resolveExportsTarget(packageJSONUrl, target, '', '', base,
434+
conditions);
410435
}
411436

412437
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(packageJSONUrl, '.');
@@ -434,9 +459,16 @@ function packageMainResolve(packageJSONUrl, packageConfig, base) {
434459
fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base));
435460
}
436461

437-
462+
/**
463+
* @param {URL} packageJSONUrl
464+
* @param {string} packageSubpath
465+
* @param {object} packageConfig
466+
* @param {string} base
467+
* @param {Set<string>} conditions
468+
* @returns {URL}
469+
*/
438470
function packageExportsResolve(
439-
packageJSONUrl, packageSubpath, packageConfig, base) /* -> URL */ {
471+
packageJSONUrl, packageSubpath, packageConfig, base, conditions) {
440472
const exports = packageConfig.exports;
441473
if (exports === undefined ||
442474
isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
@@ -447,7 +479,7 @@ function packageExportsResolve(
447479
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
448480
const target = exports[packageSubpath];
449481
const resolved = resolveExportsTarget(
450-
packageJSONUrl, target, '', packageSubpath, base);
482+
packageJSONUrl, target, '', packageSubpath, base, conditions);
451483
return finalizeResolution(resolved, base);
452484
}
453485

@@ -466,7 +498,7 @@ function packageExportsResolve(
466498
const target = exports[bestMatch];
467499
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length);
468500
const resolved = resolveExportsTarget(
469-
packageJSONUrl, target, subpath, packageSubpath, base);
501+
packageJSONUrl, target, subpath, packageSubpath, base, conditions);
470502
return finalizeResolution(resolved, base);
471503
}
472504

@@ -478,7 +510,13 @@ function getPackageType(url) {
478510
return packageConfig.type;
479511
}
480512

481-
function packageResolve(specifier /* string */, base /* URL */) { /* -> URL */
513+
/**
514+
* @param {string} specifier
515+
* @param {URL} base
516+
* @param {Set<string>} conditions
517+
* @returns {URL}
518+
*/
519+
function packageResolve(specifier, base, conditions) {
482520
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
483521
let validPackageName = true;
484522
let isScoped = false;
@@ -530,10 +568,11 @@ function packageResolve(specifier /* string */, base /* URL */) { /* -> URL */
530568
if (packageSubpath === './') {
531569
return new URL('./', packageJSONUrl);
532570
} else if (packageSubpath === '') {
533-
return packageMainResolve(packageJSONUrl, packageConfig, base);
571+
return packageMainResolve(packageJSONUrl, packageConfig, base,
572+
conditions);
534573
} else {
535574
return packageExportsResolve(
536-
packageJSONUrl, packageSubpath, packageConfig, base);
575+
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
537576
}
538577
}
539578
}
@@ -559,10 +598,11 @@ function packageResolve(specifier /* string */, base /* URL */) { /* -> URL */
559598
if (packageSubpath === './') {
560599
return new URL('./', packageJSONUrl);
561600
} else if (packageSubpath === '') {
562-
return packageMainResolve(packageJSONUrl, packageConfig, base);
601+
return packageMainResolve(packageJSONUrl, packageConfig, base,
602+
conditions);
563603
} else if (packageConfig.exports !== undefined) {
564604
return packageExportsResolve(
565-
packageJSONUrl, packageSubpath, packageConfig, base);
605+
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
566606
} else {
567607
return finalizeResolution(
568608
new URL(packageSubpath, packageJSONUrl), base);
@@ -587,7 +627,13 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
587627
return false;
588628
}
589629

590-
function moduleResolve(specifier /* string */, base /* URL */) { /* -> URL */
630+
/**
631+
* @param {string} specifier
632+
* @param {URL} base
633+
* @param {Set<string>} conditions
634+
* @returns {URL}
635+
*/
636+
function moduleResolve(specifier, base, conditions) {
591637
// Order swapped from spec for minor perf gain.
592638
// Ok since relative URLs cannot parse as URLs.
593639
let resolved;
@@ -597,13 +643,14 @@ function moduleResolve(specifier /* string */, base /* URL */) { /* -> URL */
597643
try {
598644
resolved = new URL(specifier);
599645
} catch {
600-
return packageResolve(specifier, base);
646+
return packageResolve(specifier, base, conditions);
601647
}
602648
}
603649
return finalizeResolution(resolved, base);
604650
}
605651

606-
function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) {
652+
function defaultResolve(specifier, context = {}, defaultResolveUnused) {
653+
let { parentURL, conditions } = context;
607654
let parsed;
608655
try {
609656
parsed = new URL(specifier);
@@ -641,7 +688,8 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) {
641688
throw new ERR_INPUT_TYPE_NOT_ALLOWED();
642689
}
643690

644-
let url = moduleResolve(specifier, new URL(parentURL));
691+
conditions = getConditionsSet(conditions);
692+
let url = moduleResolve(specifier, parentURL, conditions);
645693

646694
if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
647695
const urlPath = fileURLToPath(url);
@@ -658,6 +706,7 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) {
658706
}
659707

660708
module.exports = {
709+
DEFAULT_CONDITIONS,
661710
defaultResolve,
662711
getPackageType
663712
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
2+
import '../common/index.mjs';
3+
import assert from 'assert';
4+
5+
import * as ns from '../fixtures/es-modules/conditional-exports.mjs';
6+
7+
assert.deepStrictEqual({ ...ns }, { default: 'from custom condition' });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {ok, deepStrictEqual} from 'assert';
2+
3+
export async function resolve(specifier, context, defaultResolve) {
4+
ok(Array.isArray(context.conditions), 'loader receives conditions array');
5+
deepStrictEqual([...context.conditions].sort(), ['import', 'node']);
6+
return defaultResolve(specifier, {
7+
...context,
8+
conditions: ['custom-condition', ...context.conditions],
9+
});
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from 'pkgexports/condition';

test/fixtures/node_modules/pkgexports/custom-condition.mjs

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/node_modules/pkgexports/package.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)