diff --git a/doc/api/esm.md b/doc/api/esm.md index f4493ee5ea8c1a..eb9811ada8b1a5 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1175,25 +1175,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_. @@ -1203,6 +1214,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_) diff --git a/doc/api/packages.md b/doc/api/packages.md index 09acf5a549033c..8817350b646ad7 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -376,9 +376,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'; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 0a0da107682469..ca0e1b316fc5b6 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -15,7 +15,9 @@ const { SafeSet, String, StringPrototypeEndsWith, + StringPrototypeIncludes, StringPrototypeIndexOf, + StringPrototypeLastIndexOf, StringPrototypeReplace, StringPrototypeSlice, StringPrototypeSplit, @@ -114,7 +116,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' @@ -607,7 +609,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 @@ -618,30 +622,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); if (!pattern) @@ -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 diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index f153b46e321638..a99814b0950d10 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -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) { @@ -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([ @@ -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${sep}dir2.js`], ]); if (!isRequire) { diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json index 240122d4aaec95..6c80d5cb15a67e 100644 --- a/test/fixtures/node_modules/pkgexports/package.json +++ b/test/fixtures/node_modules/pkgexports/package.json @@ -47,8 +47,16 @@ "require": "./resolve-self-invalid.js", "import": "./resolve-self-invalid.mjs" }, + "./*/trailer": "./subpath/*.js", + "./*/*railer": "never", + "./*trailer": "never", + "./*/dir2/trailer": "./subpath/*/index.js", + "./a/*": "./subpath/*.js", + "./a/b/": "./nomatch/", + "./a/b*": "./subpath*.js", "./subpath/": "./subpath/", "./subpath/sub-*": "./subpath/dir1/*.js", + "./subpath/sub-*.js": "./subpath/dir1/*.js", "./features/*": "./subpath/*/*.js" } }