mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
module: exports pattern support
PR-URL: https://github.com/nodejs/node/pull/34718 Reviewed-By: Jan Krems <jan.krems@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
c9506a8f3e
commit
2d868fe822
@ -976,7 +976,8 @@ The resolver can throw the following errors:
|
||||
> 1. Set _mainExport_ to _exports_\[_"."_\].
|
||||
> 1. If _mainExport_ is not **undefined**, then
|
||||
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
|
||||
> _packageURL_, _mainExport_, _""_, **false**, _conditions_).
|
||||
> _packageURL_, _mainExport_, _""_, **false**, **false**,
|
||||
> _conditions_).
|
||||
> 1. If _resolved_ is not **null** or **undefined**, then
|
||||
> 1. Return _resolved_.
|
||||
> 1. Otherwise, if _exports_ is an Object and all keys of _exports_ start with
|
||||
@ -1010,29 +1011,43 @@ _isImports_, _conditions_)
|
||||
> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
|
||||
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
|
||||
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
|
||||
> _packageURL_, _target_, _""_, _isImports_, _conditions_).
|
||||
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
|
||||
> 1. Return the object _{ resolved, exact: **true** }_.
|
||||
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_,
|
||||
> sorted by length descending.
|
||||
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
|
||||
> or _"*"_, sorted by length descending.
|
||||
> 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 _target_ be the value of _matchObj_\[_expansionKey_\].
|
||||
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
|
||||
> index of the length of _expansionKey_.
|
||||
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
|
||||
> _packageURL_, _target_, _subpath_, _isImports_, _conditions_).
|
||||
> _packageURL_, _target_, _subpath_, **false**, _isImports_,
|
||||
> _conditions_).
|
||||
> 1. Return the object _{ resolved, exact: **false** }_.
|
||||
> 1. Return the object _{ resolved: **null**, exact: **true** }_.
|
||||
|
||||
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_,
|
||||
_conditions_)
|
||||
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
|
||||
_internal_, _conditions_)
|
||||
|
||||
> 1. If _target_ is a String, then
|
||||
> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_,
|
||||
> throw an _Invalid Module Specifier_ error.
|
||||
> 1. If _pattern_ is **false**, _subpath_ has non-zero length and _target_
|
||||
> does not end with _"/"_, throw an _Invalid Module Specifier_ error.
|
||||
> 1. If _target_ does not start with _"./"_, then
|
||||
> 1. If _internal_ is **true** and _target_ does not start with _"../"_ or
|
||||
> _"/"_ and is not a valid URL, then
|
||||
> 1. If _pattern_ is **true**, then
|
||||
> 1. Return **PACKAGE_RESOLVE**(_target_ with every instance of
|
||||
> _"*"_ replaced by _subpath_, _packageURL_ + _"/"_)_.
|
||||
> 1. Return **PACKAGE_RESOLVE**(_target_ + _subpath_,
|
||||
> _packageURL_ + _"/"_)_.
|
||||
> 1. Otherwise, throw an _Invalid Package Target_ error.
|
||||
@ -1044,8 +1059,12 @@ _conditions_)
|
||||
> 1. Assert: _resolvedTarget_ is contained in _packageURL_.
|
||||
> 1. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
|
||||
> _"node_modules"_ segments, throw an _Invalid Module Specifier_ error.
|
||||
> 1. Return the URL resolution of the concatenation of _subpath_ and
|
||||
> _resolvedTarget_.
|
||||
> 1. If _pattern_ is **true**, then
|
||||
> 1. Return the URL resolution of _resolvedTarget_ with every instance of
|
||||
> _"*"_ replaced with _subpath_.
|
||||
> 1. Otherwise,
|
||||
> 1. Return the URL resolution of the concatenation of _subpath_ and
|
||||
> _resolvedTarget_.
|
||||
> 1. Otherwise, if _target_ is a non-null Object, then
|
||||
> 1. If _exports_ contains any index property keys, as defined in ECMA-262
|
||||
> [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error.
|
||||
@ -1054,7 +1073,8 @@ _conditions_)
|
||||
> then
|
||||
> 1. Let _targetValue_ be the value of the _p_ property in _target_.
|
||||
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
|
||||
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_).
|
||||
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
|
||||
> _conditions_).
|
||||
> 1. If _resolved_ is equal to **undefined**, continue the loop.
|
||||
> 1. Return _resolved_.
|
||||
> 1. Return **undefined**.
|
||||
@ -1062,8 +1082,9 @@ _conditions_)
|
||||
> 1. If _target.length is zero, return **null**.
|
||||
> 1. For each item _targetValue_ in _target_, do
|
||||
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
|
||||
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_),
|
||||
> continuing the loop on any _Invalid Package Target_ error.
|
||||
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
|
||||
> _conditions_), continuing the loop on any _Invalid Package Target_
|
||||
> error.
|
||||
> 1. If _resolved_ is **undefined**, continue the loop.
|
||||
> 1. Return _resolved_.
|
||||
> 1. Return or throw the last fallback resolution **null** return or error.
|
||||
|
@ -181,17 +181,17 @@ Alternatively a project could choose to export entire folders:
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./lib": "./lib/index.js",
|
||||
"./lib/": "./lib/",
|
||||
"./lib/*": "./lib/*.js",
|
||||
"./feature": "./feature/index.js",
|
||||
"./feature/": "./feature/",
|
||||
"./feature/*": "./feature/*.js",
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As a last resort, package encapsulation can be disabled entirely by creating an
|
||||
export for the root of the package `"./": "./"`. This will expose every file in
|
||||
the package at the cost of disabling the encapsulation and potential tooling
|
||||
export for the root of the package `"./*": "./*"`. This will expose every file
|
||||
in the package at the cost of disabling the encapsulation and potential tooling
|
||||
benefits this provides. As the ES Module loader in Node.js enforces the use of
|
||||
[the full specifier path][], exporting the root rather than being explicit
|
||||
about entry is less expressive than either of the prior examples. Not only
|
||||
@ -254,29 +254,46 @@ import submodule from 'es-module-package/private-module.js';
|
||||
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
|
||||
```
|
||||
|
||||
Entire folders can also be mapped with package exports:
|
||||
### Subpath export patterns
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Explicitly listing each exports subpath entry is recommended for packages with
|
||||
a small number of exports. But for packages that have very large numbers of
|
||||
subpaths this can start to cause package.json bloat and maintenance issues.
|
||||
|
||||
For these use cases, subpath export patterns can be used instead:
|
||||
|
||||
```json
|
||||
// ./node_modules/es-module-package/package.json
|
||||
{
|
||||
"exports": {
|
||||
"./features/": "./src/features/"
|
||||
"./features/*": "./src/features/*.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With the preceding, all modules within the `./src/features/` folder
|
||||
are exposed deeply to `import` and `require`:
|
||||
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.
|
||||
|
||||
```js
|
||||
import feature from 'es-module-package/features/x.js';
|
||||
import featureX from 'es-module-package/features/x';
|
||||
// Loads ./node_modules/es-module-package/src/features/x.js
|
||||
|
||||
import featureY from 'es-module-package/features/y/y';
|
||||
// Loads ./node_modules/es-module-package/src/features/y/y.js
|
||||
```
|
||||
|
||||
When using folder mappings, ensure that you do want to expose every
|
||||
module inside the subfolder. Any modules which are not public
|
||||
should be moved to another folder to retain the encapsulation
|
||||
benefits of exports.
|
||||
This is a direct static replacement without any special handling for file
|
||||
extensions. In the previous example, `pkg/features/x.json` would be resolved to
|
||||
`./src/features/x.json.js` in the mapping.
|
||||
|
||||
The property of exports being statically enumerable is maintained with exports
|
||||
patterns since the individual exports for a package can be determined by
|
||||
treating the right hand side target pattern as a `**` glob against the list of
|
||||
files within the package. Because `node_modules` paths are forbidden in exports
|
||||
targets, this expansion is dependent on only the files of the package itself.
|
||||
|
||||
### Package exports fallbacks
|
||||
|
||||
|
@ -312,10 +312,11 @@ function throwInvalidPackageTarget(
|
||||
}
|
||||
|
||||
const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
|
||||
const patternRegEx = /\*/g;
|
||||
|
||||
function resolvePackageTargetString(
|
||||
target, subpath, match, packageJSONUrl, base, internal, conditions) {
|
||||
if (subpath !== '' && target[target.length - 1] !== '/')
|
||||
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {
|
||||
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
|
||||
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
||||
|
||||
if (!StringPrototypeStartsWith(target, './')) {
|
||||
@ -326,8 +327,12 @@ function resolvePackageTargetString(
|
||||
new URL(target);
|
||||
isURL = true;
|
||||
} catch {}
|
||||
if (!isURL)
|
||||
return packageResolve(target + subpath, packageJSONUrl, conditions);
|
||||
if (!isURL) {
|
||||
const exportTarget = pattern ?
|
||||
StringPrototypeReplace(target, patternRegEx, subpath) :
|
||||
target + subpath;
|
||||
return packageResolve(exportTarget, packageJSONUrl, conditions);
|
||||
}
|
||||
}
|
||||
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
||||
}
|
||||
@ -347,6 +352,9 @@ function resolvePackageTargetString(
|
||||
if (RegExpPrototypeTest(invalidSegmentRegEx, subpath))
|
||||
throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base);
|
||||
|
||||
if (pattern)
|
||||
return new URL(StringPrototypeReplace(resolved.href, patternRegEx,
|
||||
subpath));
|
||||
return new URL(subpath, resolved);
|
||||
}
|
||||
|
||||
@ -361,10 +369,10 @@ function isArrayIndex(key) {
|
||||
}
|
||||
|
||||
function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
|
||||
base, internal, conditions) {
|
||||
base, pattern, internal, conditions) {
|
||||
if (typeof target === 'string') {
|
||||
return resolvePackageTargetString(
|
||||
target, subpath, packageSubpath, packageJSONUrl, base, internal,
|
||||
target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal,
|
||||
conditions);
|
||||
} else if (ArrayIsArray(target)) {
|
||||
if (target.length === 0)
|
||||
@ -376,8 +384,8 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
|
||||
let resolved;
|
||||
try {
|
||||
resolved = resolvePackageTarget(
|
||||
packageJSONUrl, targetItem, subpath, packageSubpath, base, internal,
|
||||
conditions);
|
||||
packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern,
|
||||
internal, conditions);
|
||||
} catch (e) {
|
||||
lastException = e;
|
||||
if (e.code === 'ERR_INVALID_PACKAGE_TARGET')
|
||||
@ -411,7 +419,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
|
||||
const conditionalTarget = target[key];
|
||||
const resolved = resolvePackageTarget(
|
||||
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
|
||||
internal, conditions);
|
||||
pattern, internal, conditions);
|
||||
if (resolved === undefined)
|
||||
continue;
|
||||
return resolved;
|
||||
@ -465,7 +473,7 @@ function packageExportsResolve(
|
||||
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
|
||||
const target = exports[packageSubpath];
|
||||
const resolved = resolvePackageTarget(
|
||||
packageJSONUrl, target, '', packageSubpath, base, false, conditions
|
||||
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
|
||||
);
|
||||
if (resolved === null || resolved === undefined)
|
||||
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
||||
@ -476,7 +484,13 @@ function packageExportsResolve(
|
||||
const keys = ObjectGetOwnPropertyNames(exports);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (key[key.length - 1] === '/' &&
|
||||
if (key[key.length - 1] === '*' &&
|
||||
StringPrototypeStartsWith(packageSubpath,
|
||||
StringPrototypeSlice(key, 0, -1)) &&
|
||||
packageSubpath.length >= key.length &&
|
||||
key.length > bestMatch.length) {
|
||||
bestMatch = key;
|
||||
} else if (key[key.length - 1] === '/' &&
|
||||
StringPrototypeStartsWith(packageSubpath, key) &&
|
||||
key.length > bestMatch.length) {
|
||||
bestMatch = key;
|
||||
@ -485,12 +499,15 @@ function packageExportsResolve(
|
||||
|
||||
if (bestMatch) {
|
||||
const target = exports[bestMatch];
|
||||
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length);
|
||||
const pattern = bestMatch[bestMatch.length - 1] === '*';
|
||||
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length -
|
||||
(pattern ? 1 : 0));
|
||||
const resolved = resolvePackageTarget(packageJSONUrl, target, subpath,
|
||||
bestMatch, base, false, conditions);
|
||||
bestMatch, base, pattern, false,
|
||||
conditions);
|
||||
if (resolved === null || resolved === undefined)
|
||||
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
||||
return { resolved, exact: false };
|
||||
return { resolved, exact: pattern };
|
||||
}
|
||||
|
||||
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
||||
@ -509,7 +526,7 @@ function packageImportsResolve(name, base, conditions) {
|
||||
if (imports) {
|
||||
if (ObjectPrototypeHasOwnProperty(imports, name)) {
|
||||
const resolved = resolvePackageTarget(
|
||||
packageJSONUrl, imports[name], '', name, base, true, conditions
|
||||
packageJSONUrl, imports[name], '', name, base, false, true, conditions
|
||||
);
|
||||
if (resolved !== null)
|
||||
return { resolved, exact: true };
|
||||
@ -518,7 +535,13 @@ function packageImportsResolve(name, base, conditions) {
|
||||
const keys = ObjectGetOwnPropertyNames(imports);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (key[key.length - 1] === '/' &&
|
||||
if (key[key.length - 1] === '*' &&
|
||||
StringPrototypeStartsWith(name,
|
||||
StringPrototypeSlice(key, 0, -1)) &&
|
||||
name.length >= key.length &&
|
||||
key.length > bestMatch.length) {
|
||||
bestMatch = key;
|
||||
} else if (key[key.length - 1] === '/' &&
|
||||
StringPrototypeStartsWith(name, key) &&
|
||||
key.length > bestMatch.length) {
|
||||
bestMatch = key;
|
||||
@ -527,11 +550,14 @@ function packageImportsResolve(name, base, conditions) {
|
||||
|
||||
if (bestMatch) {
|
||||
const target = imports[bestMatch];
|
||||
const subpath = StringPrototypeSubstr(name, bestMatch.length);
|
||||
const pattern = bestMatch[bestMatch.length - 1] === '*';
|
||||
const subpath = StringPrototypeSubstr(name, bestMatch.length -
|
||||
(pattern ? 1 : 0));
|
||||
const resolved = resolvePackageTarget(
|
||||
packageJSONUrl, target, subpath, bestMatch, base, true, conditions);
|
||||
packageJSONUrl, target, subpath, bestMatch, base, pattern, true,
|
||||
conditions);
|
||||
if (resolved !== null)
|
||||
return { resolved, exact: false };
|
||||
return { resolved, exact: pattern };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,9 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
|
||||
{ default: 'self-cjs' } : { default: 'self-mjs' }],
|
||||
// Resolve self sugar
|
||||
['pkgexports-sugar', { default: 'main' }],
|
||||
// Path patterns
|
||||
['pkgexports/subpath/sub-dir1', { default: 'main' }],
|
||||
['pkgexports/features/dir1', { default: 'main' }]
|
||||
]);
|
||||
|
||||
if (isRequire) {
|
||||
|
@ -5,9 +5,9 @@
|
||||
"import": "./importbranch.js",
|
||||
"require": "./requirebranch.js"
|
||||
},
|
||||
"#subpath/": "./sub/",
|
||||
"#subpath/*": "./sub/*",
|
||||
"#external": "pkgexports/valid-cjs",
|
||||
"#external/subpath/": "pkgexports/sub/",
|
||||
"#external/subpath/*": "pkgexports/sub/*",
|
||||
"#external/invalidsubpath/": "pkgexports/sub",
|
||||
"#belowbase": "../belowbase",
|
||||
"#url": "some:url",
|
||||
|
4
test/fixtures/node_modules/pkgexports/package.json
generated
vendored
4
test/fixtures/node_modules/pkgexports/package.json
generated
vendored
@ -47,6 +47,8 @@
|
||||
"require": "./resolve-self-invalid.js",
|
||||
"import": "./resolve-self-invalid.mjs"
|
||||
},
|
||||
"./subpath/": "./subpath/"
|
||||
"./subpath/": "./subpath/",
|
||||
"./subpath/sub-*": "./subpath/dir1/*.js",
|
||||
"./features/*": "./subpath/*/*.js"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user