Skip to content

Commit bbb9771

Browse files
committed
module: support pattern trailers
PR-URL: #39635 Reviewed-By: Bradley Farias <[email protected]>
1 parent 84ed5b0 commit bbb9771

File tree

5 files changed

+100
-32
lines changed

5 files changed

+100
-32
lines changed

doc/api/esm.md

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,25 +1159,36 @@ The resolver can throw the following errors:
11591159
**PACKAGE_IMPORTS_EXPORTS_RESOLVE**(_matchKey_, _matchObj_, _packageURL_,
11601160
_isImports_, _conditions_)
11611161

1162-
> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
1162+
> 1. If _matchKey_ is a key of _matchObj_ and does not end in _"/"_ or contain
1163+
> _"*"_, then
11631164
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
11641165
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
11651166
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
11661167
> 1. Return the object _{ resolved, exact: **true** }_.
1167-
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
1168-
> or _"*"_, sorted by length descending.
1168+
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ either ending in
1169+
> _"/"_ or containing only a single _"*"_, sorted by the sorting function
1170+
> **PATTERN_KEY_COMPARE** which orders in descending order of specificity.
11691171
> 1. For each key _expansionKey_ in _expansionKeys_, do
1170-
> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is
1171-
> not equal to the substring of _expansionKey_ excluding the last _"*"_
1172-
> character, then
1173-
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1174-
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1175-
> index of the length of _expansionKey_ minus one.
1176-
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1177-
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1178-
> _conditions_).
1179-
> 1. Return the object _{ resolved, exact: **true** }_.
1180-
> 1. If _matchKey_ starts with _expansionKey_, then
1172+
> 1. Let _patternBase_ be **null**.
1173+
> 1. If _expansionKey_ contains _"*"_, set _patternBase_ to the substring of
1174+
> _expansionKey_ up to but excluding the first _"*"_ character.
1175+
> 1. If _patternBase_ is not **null** and _matchKey_ starts with but is not
1176+
> equal to _patternBase_, then
1177+
> 1. Let _patternTrailer_ be the substring of _expansionKey_ from the
1178+
> index after the first _"*"_ character.
1179+
> 1. If _patternTrailer_ has zero length, or if _matchKey_ ends with
1180+
> _patternTrailer_ and the length of _matchKey_ is greater than or
1181+
> equal to the length of _expansionKey_, then
1182+
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1183+
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1184+
> index of the length of _patternBase_ up to the length of
1185+
> _matchKey_ minus the length of _patternTrailer_.
1186+
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1187+
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1188+
> _conditions_).
1189+
> 1. Return the object _{ resolved, exact: **true** }_.
1190+
> 1. Otherwise if _patternBase_ is **null** and _matchKey_ starts with
1191+
> _expansionKey_, then
11811192
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
11821193
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
11831194
> index of the length of _expansionKey_.
@@ -1187,6 +1198,22 @@ _isImports_, _conditions_)
11871198
> 1. Return the object _{ resolved, exact: **false** }_.
11881199
> 1. Return the object _{ resolved: **null**, exact: **true** }_.
11891200

1201+
**PATTERN_KEY_COMPARE**(_keyA_, _keyB_)
1202+
1203+
> 1. Assert: _keyA_ ends with _"/"_ or contains only a single _"*"_.
1204+
> 1. Assert: _keyB_ ends with _"/"_ or contains only a single _"*"_.
1205+
> 1. Let _baseLengthA_ be the index of _"*"_ in _keyA_ plus one, if _keyA_
1206+
> contains _"*"_, or the length of _keyA_ otherwise.
1207+
> 1. Let _baseLengthB_ be the index of _"*"_ in _keyB_ plus one, if _keyB_
1208+
> contains _"*"_, or the length of _keyB_ otherwise.
1209+
> 1. If _baseLengthA_ is greater than _baseLengthB_, return -1.
1210+
> 1. If _baseLengthB_ is greater than _baseLengthA_, return 1.
1211+
> 1. If _keyA_ does not contain _"*"_, return 1.
1212+
> 1. If _keyB_ does not contain _"*"_, return -1.
1213+
> 1. If the length of _keyA_ is greater than the length of _keyB_, return -1.
1214+
> 1. If the length of _keyB_ is greater than the length of _keyA_, return 1.
1215+
> 1. Return 0.
1216+
11901217
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
11911218
_internal_, _conditions_)
11921219

doc/api/packages.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,8 @@ For these use cases, subpath export patterns can be used instead:
360360
**`*` maps expose nested subpaths as it is a string replacement syntax
361361
only.**
362362

363-
The left hand matching pattern must always end in `*`. All instances of `*` on
364-
the right hand side will then be replaced with this value, including if it
365-
contains any `/` separators.
363+
All instances of `*` on the right hand side will then be replaced with this
364+
value, including if it contains any `/` separators.
366365

367366
```js
368367
import featureX from 'es-module-package/features/x';

lib/internal/modules/esm/resolve.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ const {
1515
SafeSet,
1616
String,
1717
StringPrototypeEndsWith,
18+
StringPrototypeIncludes,
1819
StringPrototypeIndexOf,
20+
StringPrototypeLastIndexOf,
1921
StringPrototypeReplace,
2022
StringPrototypeSlice,
2123
StringPrototypeSplit,
@@ -59,7 +61,6 @@ const userConditions = getOptionValue('--conditions');
5961
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
6062
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
6163

62-
6364
function getConditionsSet(conditions) {
6465
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
6566
if (!ArrayIsArray(conditions)) {
@@ -479,7 +480,9 @@ function packageExportsResolve(
479480
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base))
480481
exports = { '.': exports };
481482

482-
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
483+
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
484+
!StringPrototypeIncludes(packageSubpath, '*') &&
485+
!StringPrototypeEndsWith(packageSubpath, '/')) {
483486
const target = exports[packageSubpath];
484487
const resolved = resolvePackageTarget(
485488
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
@@ -490,30 +493,38 @@ function packageExportsResolve(
490493
}
491494

492495
let bestMatch = '';
496+
let bestMatchSubpath;
493497
const keys = ObjectGetOwnPropertyNames(exports);
494498
for (let i = 0; i < keys.length; i++) {
495499
const key = keys[i];
496-
if (key[key.length - 1] === '*' &&
500+
const patternIndex = StringPrototypeIndexOf(key, '*');
501+
if (patternIndex !== -1 &&
497502
StringPrototypeStartsWith(packageSubpath,
498-
StringPrototypeSlice(key, 0, -1)) &&
499-
packageSubpath.length >= key.length &&
500-
key.length > bestMatch.length) {
501-
bestMatch = key;
503+
StringPrototypeSlice(key, 0, patternIndex))) {
504+
const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
505+
if (packageSubpath.length >= key.length &&
506+
StringPrototypeEndsWith(packageSubpath, patternTrailer) &&
507+
patternKeyCompare(bestMatch, key) === 1 &&
508+
StringPrototypeLastIndexOf(key, '*') === patternIndex) {
509+
bestMatch = key;
510+
bestMatchSubpath = StringPrototypeSlice(
511+
packageSubpath, patternIndex,
512+
packageSubpath.length - patternTrailer.length);
513+
}
502514
} else if (key[key.length - 1] === '/' &&
503515
StringPrototypeStartsWith(packageSubpath, key) &&
504-
key.length > bestMatch.length) {
516+
patternKeyCompare(bestMatch, key) === 1) {
505517
bestMatch = key;
518+
bestMatchSubpath = StringPrototypeSlice(packageSubpath, key.length);
506519
}
507520
}
508521

509522
if (bestMatch) {
510523
const target = exports[bestMatch];
511-
const pattern = bestMatch[bestMatch.length - 1] === '*';
512-
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length -
513-
(pattern ? 1 : 0));
514-
const resolved = resolvePackageTarget(packageJSONUrl, target, subpath,
515-
bestMatch, base, pattern, false,
516-
conditions);
524+
const pattern = StringPrototypeIncludes(bestMatch, '*');
525+
const resolved = resolvePackageTarget(packageJSONUrl, target,
526+
bestMatchSubpath, bestMatch, base,
527+
pattern, false, conditions);
517528
if (resolved === null || resolved === undefined)
518529
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
519530
return { resolved, exact: pattern };
@@ -522,6 +533,20 @@ function packageExportsResolve(
522533
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
523534
}
524535

536+
function patternKeyCompare(a, b) {
537+
const aPatternIndex = StringPrototypeIndexOf(a, '*');
538+
const bPatternIndex = StringPrototypeIndexOf(b, '*');
539+
const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
540+
const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
541+
if (baseLenA > baseLenB) return -1;
542+
if (baseLenB > baseLenA) return 1;
543+
if (aPatternIndex === -1) return 1;
544+
if (bPatternIndex === -1) return -1;
545+
if (a.length > b.length) return -1;
546+
if (b.length > a.length) return 1;
547+
return 0;
548+
}
549+
525550
function packageImportsResolve(name, base, conditions) {
526551
if (name === '#' || StringPrototypeStartsWith(name, '#/')) {
527552
const reason = 'is not a valid internal imports specifier name';

test/es-module/test-esm-exports.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
3535
['pkgexports-sugar', { default: 'main' }],
3636
// Path patterns
3737
['pkgexports/subpath/sub-dir1', { default: 'main' }],
38-
['pkgexports/features/dir1', { default: 'main' }]
38+
['pkgexports/subpath/sub-dir1.js', { default: 'main' }],
39+
['pkgexports/features/dir1', { default: 'main' }],
40+
['pkgexports/dir1/dir1/trailer', { default: 'main' }],
41+
['pkgexports/dir2/dir2/trailer', { default: 'index' }],
42+
['pkgexports/a/dir1/dir1', { default: 'main' }],
43+
['pkgexports/a/b/dir1/dir1', { default: 'main' }],
3944
]);
4045

4146
if (isRequire) {
@@ -72,6 +77,8 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
7277
['pkgexports/null/subpath', './null/subpath'],
7378
// Empty fallback
7479
['pkgexports/nofallback1', './nofallback1'],
80+
// Non pattern matches
81+
['pkgexports/trailer', './trailer'],
7582
]);
7683

7784
const invalidExports = new Map([
@@ -142,6 +149,8 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
142149
['pkgexports/sub/not-a-file.js', `pkgexports${sep}not-a-file.js`],
143150
// No extension lookups
144151
['pkgexports/no-ext', `pkgexports${sep}asdf`],
152+
// Pattern specificity
153+
['pkgexports/dir2/trailer', `subpath${sep}dir2.js`],
145154
]);
146155

147156
if (!isRequire) {

test/fixtures/node_modules/pkgexports/package.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)