Skip to content

Commit

Permalink
module: support pattern trailers
Browse files Browse the repository at this point in the history
PR-URL: #39635
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
  • Loading branch information
guybedford committed Aug 25, 2021
1 parent 84ed5b0 commit bbb9771
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 32 deletions.
55 changes: 41 additions & 14 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1159,25 +1159,36 @@ The resolver can throw the following errors:
**PACKAGE_IMPORTS_EXPORTS_RESOLVE**(_matchKey_, _matchObj_, _packageURL_,
_isImports_, _conditions_)

> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
> 1. If _matchKey_ is a key of _matchObj_ and does not end in _"/"_ or contain
> _"*"_, then
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
> 1. Return the object _{ resolved, exact: **true** }_.
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
> or _"*"_, sorted by length descending.
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ either ending in
> _"/"_ or containing only a single _"*"_, sorted by the sorting function
> **PATTERN_KEY_COMPARE** which orders in descending order of specificity.
> 1. For each key _expansionKey_ in _expansionKeys_, do
> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is
> not equal to the substring of _expansionKey_ excluding the last _"*"_
> character, then
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
> index of the length of _expansionKey_ minus one.
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
> _conditions_).
> 1. Return the object _{ resolved, exact: **true** }_.
> 1. If _matchKey_ starts with _expansionKey_, then
> 1. Let _patternBase_ be **null**.
> 1. If _expansionKey_ contains _"*"_, set _patternBase_ to the substring of
> _expansionKey_ up to but excluding the first _"*"_ character.
> 1. If _patternBase_ is not **null** and _matchKey_ starts with but is not
> equal to _patternBase_, then
> 1. Let _patternTrailer_ be the substring of _expansionKey_ from the
> index after the first _"*"_ character.
> 1. If _patternTrailer_ has zero length, or if _matchKey_ ends with
> _patternTrailer_ and the length of _matchKey_ is greater than or
> equal to the length of _expansionKey_, then
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
> index of the length of _patternBase_ up to the length of
> _matchKey_ minus the length of _patternTrailer_.
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
> _conditions_).
> 1. Return the object _{ resolved, exact: **true** }_.
> 1. Otherwise if _patternBase_ is **null** and _matchKey_ starts with
> _expansionKey_, then
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
> index of the length of _expansionKey_.
Expand All @@ -1187,6 +1198,22 @@ _isImports_, _conditions_)
> 1. Return the object _{ resolved, exact: **false** }_.
> 1. Return the object _{ resolved: **null**, exact: **true** }_.

**PATTERN_KEY_COMPARE**(_keyA_, _keyB_)

> 1. Assert: _keyA_ ends with _"/"_ or contains only a single _"*"_.
> 1. Assert: _keyB_ ends with _"/"_ or contains only a single _"*"_.
> 1. Let _baseLengthA_ be the index of _"*"_ in _keyA_ plus one, if _keyA_
> contains _"*"_, or the length of _keyA_ otherwise.
> 1. Let _baseLengthB_ be the index of _"*"_ in _keyB_ plus one, if _keyB_
> contains _"*"_, or the length of _keyB_ otherwise.
> 1. If _baseLengthA_ is greater than _baseLengthB_, return -1.
> 1. If _baseLengthB_ is greater than _baseLengthA_, return 1.
> 1. If _keyA_ does not contain _"*"_, return 1.
> 1. If _keyB_ does not contain _"*"_, return -1.
> 1. If the length of _keyA_ is greater than the length of _keyB_, return -1.
> 1. If the length of _keyB_ is greater than the length of _keyA_, return 1.
> 1. Return 0.

**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
_internal_, _conditions_)

Expand Down
5 changes: 2 additions & 3 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,8 @@ For these use cases, subpath export patterns can be used instead:
**`*` maps expose nested subpaths as it is a string replacement syntax
only.**

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

```js
import featureX from 'es-module-package/features/x';
Expand Down
53 changes: 39 additions & 14 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const {
SafeSet,
String,
StringPrototypeEndsWith,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeLastIndexOf,
StringPrototypeReplace,
StringPrototypeSlice,
StringPrototypeSplit,
Expand Down Expand Up @@ -59,7 +61,6 @@ const userConditions = getOptionValue('--conditions');
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);


function getConditionsSet(conditions) {
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
if (!ArrayIsArray(conditions)) {
Expand Down Expand Up @@ -479,7 +480,9 @@ function packageExportsResolve(
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base))
exports = { '.': exports };

if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
!StringPrototypeIncludes(packageSubpath, '*') &&
!StringPrototypeEndsWith(packageSubpath, '/')) {
const target = exports[packageSubpath];
const resolved = resolvePackageTarget(
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
Expand All @@ -490,30 +493,38 @@ function packageExportsResolve(
}

let bestMatch = '';
let bestMatchSubpath;
const keys = ObjectGetOwnPropertyNames(exports);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key[key.length - 1] === '*' &&
const patternIndex = StringPrototypeIndexOf(key, '*');
if (patternIndex !== -1 &&
StringPrototypeStartsWith(packageSubpath,
StringPrototypeSlice(key, 0, -1)) &&
packageSubpath.length >= key.length &&
key.length > bestMatch.length) {
bestMatch = key;
StringPrototypeSlice(key, 0, patternIndex))) {
const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
if (packageSubpath.length >= key.length &&
StringPrototypeEndsWith(packageSubpath, patternTrailer) &&
patternKeyCompare(bestMatch, key) === 1 &&
StringPrototypeLastIndexOf(key, '*') === patternIndex) {
bestMatch = key;
bestMatchSubpath = StringPrototypeSlice(
packageSubpath, patternIndex,
packageSubpath.length - patternTrailer.length);
}
} else if (key[key.length - 1] === '/' &&
StringPrototypeStartsWith(packageSubpath, key) &&
key.length > bestMatch.length) {
patternKeyCompare(bestMatch, key) === 1) {
bestMatch = key;
bestMatchSubpath = StringPrototypeSlice(packageSubpath, key.length);
}
}

if (bestMatch) {
const target = exports[bestMatch];
const pattern = bestMatch[bestMatch.length - 1] === '*';
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length -
(pattern ? 1 : 0));
const resolved = resolvePackageTarget(packageJSONUrl, target, subpath,
bestMatch, base, pattern, false,
conditions);
const pattern = StringPrototypeIncludes(bestMatch, '*');
const resolved = resolvePackageTarget(packageJSONUrl, target,
bestMatchSubpath, bestMatch, base,
pattern, false, conditions);
if (resolved === null || resolved === undefined)
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
return { resolved, exact: pattern };
Expand All @@ -522,6 +533,20 @@ function packageExportsResolve(
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
}

function patternKeyCompare(a, b) {
const aPatternIndex = StringPrototypeIndexOf(a, '*');
const bPatternIndex = StringPrototypeIndexOf(b, '*');
const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
if (baseLenA > baseLenB) return -1;
if (baseLenB > baseLenA) return 1;
if (aPatternIndex === -1) return 1;
if (bPatternIndex === -1) return -1;
if (a.length > b.length) return -1;
if (b.length > a.length) return 1;
return 0;
}

function packageImportsResolve(name, base, conditions) {
if (name === '#' || StringPrototypeStartsWith(name, '#/')) {
const reason = 'is not a valid internal imports specifier name';
Expand Down
11 changes: 10 additions & 1 deletion test/es-module/test-esm-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
['pkgexports-sugar', { default: 'main' }],
// Path patterns
['pkgexports/subpath/sub-dir1', { default: 'main' }],
['pkgexports/features/dir1', { default: 'main' }]
['pkgexports/subpath/sub-dir1.js', { default: 'main' }],
['pkgexports/features/dir1', { default: 'main' }],
['pkgexports/dir1/dir1/trailer', { default: 'main' }],
['pkgexports/dir2/dir2/trailer', { default: 'index' }],
['pkgexports/a/dir1/dir1', { default: 'main' }],
['pkgexports/a/b/dir1/dir1', { default: 'main' }],
]);

if (isRequire) {
Expand Down Expand Up @@ -72,6 +77,8 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
['pkgexports/null/subpath', './null/subpath'],
// Empty fallback
['pkgexports/nofallback1', './nofallback1'],
// Non pattern matches
['pkgexports/trailer', './trailer'],
]);

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

if (!isRequire) {
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/node_modules/pkgexports/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit bbb9771

Please sign in to comment.