Skip to content

Commit 20da0a5

Browse files
guybedfordtargos
authored andcommitted
module: support pattern trailers
PR-URL: #39635 Reviewed-By: Bradley Farias <[email protected]>
1 parent bc236a6 commit 20da0a5

File tree

5 files changed

+100
-31
lines changed

5 files changed

+100
-31
lines changed

doc/api/esm.md

+41-14
Original file line numberDiff line numberDiff line change
@@ -1175,25 +1175,36 @@ The resolver can throw the following errors:
11751175
**PACKAGE_IMPORTS_EXPORTS_RESOLVE**(_matchKey_, _matchObj_, _packageURL_,
11761176
_isImports_, _conditions_)
11771177

1178-
> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
1178+
> 1. If _matchKey_ is a key of _matchObj_ and does not end in _"/"_ or contain
1179+
> _"*"_, then
11791180
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
11801181
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
11811182
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
11821183
> 1. Return the object _{ resolved, exact: **true** }_.
1183-
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
1184-
> or _"*"_, sorted by length descending.
1184+
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ either ending in
1185+
> _"/"_ or containing only a single _"*"_, sorted by the sorting function
1186+
> **PATTERN_KEY_COMPARE** which orders in descending order of specificity.
11851187
> 1. For each key _expansionKey_ in _expansionKeys_, do
1186-
> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is
1187-
> not equal to the substring of _expansionKey_ excluding the last _"*"_
1188-
> character, then
1189-
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1190-
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1191-
> index of the length of _expansionKey_ minus one.
1192-
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1193-
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1194-
> _conditions_).
1195-
> 1. Return the object _{ resolved, exact: **true** }_.
1196-
> 1. If _matchKey_ starts with _expansionKey_, then
1188+
> 1. Let _patternBase_ be **null**.
1189+
> 1. If _expansionKey_ contains _"*"_, set _patternBase_ to the substring of
1190+
> _expansionKey_ up to but excluding the first _"*"_ character.
1191+
> 1. If _patternBase_ is not **null** and _matchKey_ starts with but is not
1192+
> equal to _patternBase_, then
1193+
> 1. Let _patternTrailer_ be the substring of _expansionKey_ from the
1194+
> index after the first _"*"_ character.
1195+
> 1. If _patternTrailer_ has zero length, or if _matchKey_ ends with
1196+
> _patternTrailer_ and the length of _matchKey_ is greater than or
1197+
> equal to the length of _expansionKey_, then
1198+
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1199+
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1200+
> index of the length of _patternBase_ up to the length of
1201+
> _matchKey_ minus the length of _patternTrailer_.
1202+
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1203+
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1204+
> _conditions_).
1205+
> 1. Return the object _{ resolved, exact: **true** }_.
1206+
> 1. Otherwise if _patternBase_ is **null** and _matchKey_ starts with
1207+
> _expansionKey_, then
11971208
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
11981209
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
11991210
> index of the length of _expansionKey_.
@@ -1203,6 +1214,22 @@ _isImports_, _conditions_)
12031214
> 1. Return the object _{ resolved, exact: **false** }_.
12041215
> 1. Return the object _{ resolved: **null**, exact: **true** }_.
12051216

1217+
**PATTERN_KEY_COMPARE**(_keyA_, _keyB_)
1218+
1219+
> 1. Assert: _keyA_ ends with _"/"_ or contains only a single _"*"_.
1220+
> 1. Assert: _keyB_ ends with _"/"_ or contains only a single _"*"_.
1221+
> 1. Let _baseLengthA_ be the index of _"*"_ in _keyA_ plus one, if _keyA_
1222+
> contains _"*"_, or the length of _keyA_ otherwise.
1223+
> 1. Let _baseLengthB_ be the index of _"*"_ in _keyB_ plus one, if _keyB_
1224+
> contains _"*"_, or the length of _keyB_ otherwise.
1225+
> 1. If _baseLengthA_ is greater than _baseLengthB_, return -1.
1226+
> 1. If _baseLengthB_ is greater than _baseLengthA_, return 1.
1227+
> 1. If _keyA_ does not contain _"*"_, return 1.
1228+
> 1. If _keyB_ does not contain _"*"_, return -1.
1229+
> 1. If the length of _keyA_ is greater than the length of _keyB_, return -1.
1230+
> 1. If the length of _keyB_ is greater than the length of _keyA_, return 1.
1231+
> 1. Return 0.
1232+
12061233
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
12071234
_internal_, _conditions_)
12081235

doc/api/packages.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,8 @@ For these use cases, subpath export patterns can be used instead:
376376
**`*` maps expose nested subpaths as it is a string replacement syntax
377377
only.**
378378

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

383382
```js
384383
import featureX from 'es-module-package/features/x';

lib/internal/modules/esm/resolve.js

+40-14
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,
@@ -114,7 +116,7 @@ function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) {
114116
`Package ${pkgPath} has a "main" field set to ${JSONStringify(main)}, ` +
115117
`excluding the full filename and extension to the resolved file at "${
116118
StringPrototypeSlice(path, pkgPath.length)}", imported from ${
117-
basePath}.\n Automatic extension resolution of the "main" field is` +
119+
basePath}.\n Automatic extension resolution of the "main" field is ` +
118120
'deprecated for ES modules.',
119121
'DeprecationWarning',
120122
'DEP0151'
@@ -607,7 +609,9 @@ function packageExportsResolve(
607609
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base))
608610
exports = { '.': exports };
609611

610-
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
612+
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
613+
!StringPrototypeIncludes(packageSubpath, '*') &&
614+
!StringPrototypeEndsWith(packageSubpath, '/')) {
611615
const target = exports[packageSubpath];
612616
const resolved = resolvePackageTarget(
613617
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
@@ -618,30 +622,38 @@ function packageExportsResolve(
618622
}
619623

620624
let bestMatch = '';
625+
let bestMatchSubpath;
621626
const keys = ObjectGetOwnPropertyNames(exports);
622627
for (let i = 0; i < keys.length; i++) {
623628
const key = keys[i];
624-
if (key[key.length - 1] === '*' &&
629+
const patternIndex = StringPrototypeIndexOf(key, '*');
630+
if (patternIndex !== -1 &&
625631
StringPrototypeStartsWith(packageSubpath,
626-
StringPrototypeSlice(key, 0, -1)) &&
627-
packageSubpath.length >= key.length &&
628-
key.length > bestMatch.length) {
629-
bestMatch = key;
632+
StringPrototypeSlice(key, 0, patternIndex))) {
633+
const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
634+
if (packageSubpath.length >= key.length &&
635+
StringPrototypeEndsWith(packageSubpath, patternTrailer) &&
636+
patternKeyCompare(bestMatch, key) === 1 &&
637+
StringPrototypeLastIndexOf(key, '*') === patternIndex) {
638+
bestMatch = key;
639+
bestMatchSubpath = StringPrototypeSlice(
640+
packageSubpath, patternIndex,
641+
packageSubpath.length - patternTrailer.length);
642+
}
630643
} else if (key[key.length - 1] === '/' &&
631644
StringPrototypeStartsWith(packageSubpath, key) &&
632-
key.length > bestMatch.length) {
645+
patternKeyCompare(bestMatch, key) === 1) {
633646
bestMatch = key;
647+
bestMatchSubpath = StringPrototypeSlice(packageSubpath, key.length);
634648
}
635649
}
636650

637651
if (bestMatch) {
638652
const target = exports[bestMatch];
639-
const pattern = bestMatch[bestMatch.length - 1] === '*';
640-
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length -
641-
(pattern ? 1 : 0));
642-
const resolved = resolvePackageTarget(packageJSONUrl, target, subpath,
643-
bestMatch, base, pattern, false,
644-
conditions);
653+
const pattern = StringPrototypeIncludes(bestMatch, '*');
654+
const resolved = resolvePackageTarget(packageJSONUrl, target,
655+
bestMatchSubpath, bestMatch, base,
656+
pattern, false, conditions);
645657
if (resolved === null || resolved === undefined)
646658
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
647659
if (!pattern)
@@ -652,6 +664,20 @@ function packageExportsResolve(
652664
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
653665
}
654666

667+
function patternKeyCompare(a, b) {
668+
const aPatternIndex = StringPrototypeIndexOf(a, '*');
669+
const bPatternIndex = StringPrototypeIndexOf(b, '*');
670+
const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
671+
const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
672+
if (baseLenA > baseLenB) return -1;
673+
if (baseLenB > baseLenA) return 1;
674+
if (aPatternIndex === -1) return 1;
675+
if (bPatternIndex === -1) return -1;
676+
if (a.length > b.length) return -1;
677+
if (b.length > a.length) return 1;
678+
return 0;
679+
}
680+
655681
/**
656682
* @param {string} name
657683
* @param {string | URL | undefined} base

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

+9
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/subpath/sub-dir1.js', { default: 'main' }],
3839
['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) {
@@ -77,6 +82,8 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
7782
['pkgexports/null/subpath', './null/subpath'],
7883
// Empty fallback
7984
['pkgexports/nofallback1', './nofallback1'],
85+
// Non pattern matches
86+
['pkgexports/trailer', './trailer'],
8087
]);
8188

8289
const invalidExports = new Map([
@@ -147,6 +154,8 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
147154
['pkgexports/sub/not-a-file.js', `pkgexports${sep}not-a-file.js`],
148155
// No extension lookups
149156
['pkgexports/no-ext', `pkgexports${sep}asdf`],
157+
// Pattern specificity
158+
['pkgexports/dir2/trailer', `subpath${sep}dir2.js`],
150159
]);
151160

152161
if (!isRequire) {

test/fixtures/node_modules/pkgexports/package.json

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

0 commit comments

Comments
 (0)