Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b5cb423

Browse files
committedOct 18, 2021
module: support pattern trailers
PR-URL: nodejs#39635 Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
1 parent c019fa9 commit b5cb423

File tree

5 files changed

+103
-31
lines changed

5 files changed

+103
-31
lines changed
 

‎doc/api/esm.md

+41-14
Original file line numberDiff line numberDiff line change
@@ -1122,25 +1122,36 @@ The resolver can throw the following errors:
11221122
**PACKAGE_IMPORTS_EXPORTS_RESOLVE**(_matchKey_, _matchObj_, _packageURL_,
11231123
_isImports_, _conditions_)
11241124

1125-
> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
1125+
> 1. If _matchKey_ is a key of _matchObj_ and does not end in _"/"_ or contain
1126+
> _"*"_, then
11261127
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
11271128
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
11281129
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
11291130
> 1. Return the object _{ resolved, exact: **true** }_.
1130-
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
1131-
> or _"*"_, sorted by length descending.
1131+
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ either ending in
1132+
> _"/"_ or containing only a single _"*"_, sorted by the sorting function
1133+
> **PATTERN_KEY_COMPARE** which orders in descending order of specificity.
11321134
> 1. For each key _expansionKey_ in _expansionKeys_, do
1133-
> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is
1134-
> not equal to the substring of _expansionKey_ excluding the last _"*"_
1135-
> character, then
1136-
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1137-
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1138-
> index of the length of _expansionKey_ minus one.
1139-
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1140-
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1141-
> _conditions_).
1142-
> 1. Return the object _{ resolved, exact: **true** }_.
1143-
> 1. If _matchKey_ starts with _expansionKey_, then
1135+
> 1. Let _patternBase_ be **null**.
1136+
> 1. If _expansionKey_ contains _"*"_, set _patternBase_ to the substring of
1137+
> _expansionKey_ up to but excluding the first _"*"_ character.
1138+
> 1. If _patternBase_ is not **null** and _matchKey_ starts with but is not
1139+
> equal to _patternBase_, then
1140+
> 1. Let _patternTrailer_ be the substring of _expansionKey_ from the
1141+
> index after the first _"*"_ character.
1142+
> 1. If _patternTrailer_ has zero length, or if _matchKey_ ends with
1143+
> _patternTrailer_ and the length of _matchKey_ is greater than or
1144+
> equal to the length of _expansionKey_, then
1145+
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1146+
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1147+
> index of the length of _patternBase_ up to the length of
1148+
> _matchKey_ minus the length of _patternTrailer_.
1149+
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1150+
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1151+
> _conditions_).
1152+
> 1. Return the object _{ resolved, exact: **true** }_.
1153+
> 1. Otherwise if _patternBase_ is **null** and _matchKey_ starts with
1154+
> _expansionKey_, then
11441155
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
11451156
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
11461157
> index of the length of _expansionKey_.
@@ -1150,6 +1161,22 @@ _isImports_, _conditions_)
11501161
> 1. Return the object _{ resolved, exact: **false** }_.
11511162
> 1. Return the object _{ resolved: **null**, exact: **true** }_.
11521163

1164+
**PATTERN_KEY_COMPARE**(_keyA_, _keyB_)
1165+
1166+
> 1. Assert: _keyA_ ends with _"/"_ or contains only a single _"*"_.
1167+
> 1. Assert: _keyB_ ends with _"/"_ or contains only a single _"*"_.
1168+
> 1. Let _baseLengthA_ be the index of _"*"_ in _keyA_ plus one, if _keyA_
1169+
> contains _"*"_, or the length of _keyA_ otherwise.
1170+
> 1. Let _baseLengthB_ be the index of _"*"_ in _keyB_ plus one, if _keyB_
1171+
> contains _"*"_, or the length of _keyB_ otherwise.
1172+
> 1. If _baseLengthA_ is greater than _baseLengthB_, return -1.
1173+
> 1. If _baseLengthB_ is greater than _baseLengthA_, return 1.
1174+
> 1. If _keyA_ does not contain _"*"_, return 1.
1175+
> 1. If _keyB_ does not contain _"*"_, return -1.
1176+
> 1. If the length of _keyA_ is greater than the length of _keyB_, return -1.
1177+
> 1. If the length of _keyB_ is greater than the length of _keyA_, return 1.
1178+
> 1. Return 0.
1179+
11531180
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
11541181
_internal_, _conditions_)
11551182

‎doc/api/packages.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,11 @@ For these use cases, subpath export patterns can be used instead:
341341
}
342342
```
343343

344-
The left hand matching pattern must always end in `*`. All instances of `*` on
345-
the right hand side will then be replaced with this value, including if it
346-
contains any `/` separators.
344+
**`*` maps expose nested subpaths as it is a string replacement syntax
345+
only.**
346+
347+
All instances of `*` on the right hand side will then be replaced with this
348+
value, including if it contains any `/` separators.
347349

348350
```js
349351
import featureX from 'es-module-package/features/x';

‎lib/internal/modules/esm/resolve.js

+39-13
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,
@@ -479,7 +481,9 @@ function packageExportsResolve(
479481
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base))
480482
exports = { '.': exports };
481483

482-
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
484+
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
485+
!StringPrototypeIncludes(packageSubpath, '*') &&
486+
!StringPrototypeEndsWith(packageSubpath, '/')) {
483487
const target = exports[packageSubpath];
484488
const resolved = resolvePackageTarget(
485489
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
@@ -490,30 +494,38 @@ function packageExportsResolve(
490494
}
491495

492496
let bestMatch = '';
497+
let bestMatchSubpath;
493498
const keys = ObjectGetOwnPropertyNames(exports);
494499
for (let i = 0; i < keys.length; i++) {
495500
const key = keys[i];
496-
if (key[key.length - 1] === '*' &&
501+
const patternIndex = StringPrototypeIndexOf(key, '*');
502+
if (patternIndex !== -1 &&
497503
StringPrototypeStartsWith(packageSubpath,
498-
StringPrototypeSlice(key, 0, -1)) &&
499-
packageSubpath.length >= key.length &&
500-
key.length > bestMatch.length) {
501-
bestMatch = key;
504+
StringPrototypeSlice(key, 0, patternIndex))) {
505+
const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
506+
if (packageSubpath.length >= key.length &&
507+
StringPrototypeEndsWith(packageSubpath, patternTrailer) &&
508+
patternKeyCompare(bestMatch, key) === 1 &&
509+
StringPrototypeLastIndexOf(key, '*') === patternIndex) {
510+
bestMatch = key;
511+
bestMatchSubpath = StringPrototypeSlice(
512+
packageSubpath, patternIndex,
513+
packageSubpath.length - patternTrailer.length);
514+
}
502515
} else if (key[key.length - 1] === '/' &&
503516
StringPrototypeStartsWith(packageSubpath, key) &&
504-
key.length > bestMatch.length) {
517+
patternKeyCompare(bestMatch, key) === 1) {
505518
bestMatch = key;
519+
bestMatchSubpath = StringPrototypeSlice(packageSubpath, key.length);
506520
}
507521
}
508522

509523
if (bestMatch) {
510524
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);
525+
const pattern = StringPrototypeIncludes(bestMatch, '*');
526+
const resolved = resolvePackageTarget(packageJSONUrl, target,
527+
bestMatchSubpath, bestMatch, base,
528+
pattern, false, conditions);
517529
if (resolved === null || resolved === undefined)
518530
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
519531
return { resolved, exact: pattern };
@@ -522,6 +534,20 @@ function packageExportsResolve(
522534
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
523535
}
524536

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

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

+10-1
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

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

0 commit comments

Comments
 (0)
Please sign in to comment.