Skip to content

Commit

Permalink
module: support pattern trailers
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Aug 2, 2021
1 parent f2d02ab commit 83ccf1a
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 28 deletions.
55 changes: 41 additions & 14 deletions doc/api/esm.md
Expand Up @@ -1176,25 +1176,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
> but is not equal to _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 @@ -1204,6 +1215,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
54 changes: 40 additions & 14 deletions lib/internal/modules/esm/resolve.js
Expand Up @@ -16,6 +16,7 @@ const {
String,
StringPrototypeEndsWith,
StringPrototypeIndexOf,
StringPrototypeLastIndexOf,
StringPrototypeReplace,
StringPrototypeSlice,
StringPrototypeSplit,
Expand Down Expand Up @@ -114,7 +115,7 @@ function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) {
`Package ${pkgPath} has a "main" field set to ${JSONStringify(main)}, ` +
`excluding the full filename and extension to the resolved file at "${
StringPrototypeSlice(path, pkgPath.length)}", imported from ${
basePath}.\n Automatic extension resolution of the "main" field is` +
basePath}.\n Automatic extension resolution of the "main" field is ` +
'deprecated for ES modules.',
'DeprecationWarning',
'DEP0151'
Expand Down Expand Up @@ -607,7 +608,9 @@ function packageExportsResolve(
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base))
exports = { '.': exports };

if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
StringPrototypeIndexOf(packageSubpath, '*') === -1 &&
!StringPrototypeEndsWith(packageSubpath, '/')) {
const target = exports[packageSubpath];
const resolved = resolvePackageTarget(
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
Expand All @@ -618,30 +621,39 @@ 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) {
packageSubpath.length > key.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 = bestMatch.indexOf('*') !== -1;
const resolved = resolvePackageTarget(packageJSONUrl, target,
bestMatchSubpath, bestMatch, base,
pattern, false, conditions);
if (resolved === null || resolved === undefined)
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
if (!pattern)
Expand All @@ -652,6 +664,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;
}

/**
* @param {string} name
* @param {string | URL | undefined} base
Expand Down
9 changes: 9 additions & 0 deletions test/es-module/test-esm-exports.mjs
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/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 @@ -77,6 +82,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 @@ -147,6 +154,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/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 83ccf1a

Please sign in to comment.