Skip to content

Commit 5057318

Browse files
guybedfordMylesBorins
authored andcommitted
module: exports pattern support
PR-URL: #34718 Reviewed-By: Jan Krems <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 1d1ce1f commit 5057318

File tree

6 files changed

+118
-49
lines changed

6 files changed

+118
-49
lines changed

doc/api/esm.md

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,8 @@ The resolver can throw the following errors:
976976
> 1. Set _mainExport_ to _exports_\[_"."_\].
977977
> 1. If _mainExport_ is not **undefined**, then
978978
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
979-
> _packageURL_, _mainExport_, _""_, **false**, _conditions_).
979+
> _packageURL_, _mainExport_, _""_, **false**, **false**,
980+
> _conditions_).
980981
> 1. If _resolved_ is not **null** or **undefined**, then
981982
> 1. Return _resolved_.
982983
> 1. Otherwise, if _exports_ is an Object and all keys of _exports_ start with
@@ -1010,29 +1011,43 @@ _isImports_, _conditions_)
10101011
> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
10111012
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
10121013
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1013-
> _packageURL_, _target_, _""_, _isImports_, _conditions_).
1014+
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
10141015
> 1. Return the object _{ resolved, exact: **true** }_.
1015-
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_,
1016-
> sorted by length descending.
1016+
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
1017+
> or _"*"_, sorted by length descending.
10171018
> 1. For each key _expansionKey_ in _expansionKeys_, do
1019+
> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is
1020+
> not equal to the substring of _expansionKey_ excluding the last _"*"_
1021+
> character, then
1022+
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1023+
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1024+
> index of the length of _expansionKey_ minus one.
1025+
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1026+
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1027+
> _conditions_).
1028+
> 1. Return the object _{ resolved, exact: **true** }_.
10181029
> 1. If _matchKey_ starts with _expansionKey_, then
10191030
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
10201031
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
10211032
> index of the length of _expansionKey_.
10221033
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1023-
> _packageURL_, _target_, _subpath_, _isImports_, _conditions_).
1034+
> _packageURL_, _target_, _subpath_, **false**, _isImports_,
1035+
> _conditions_).
10241036
> 1. Return the object _{ resolved, exact: **false** }_.
10251037
> 1. Return the object _{ resolved: **null**, exact: **true** }_.
10261038

1027-
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_,
1028-
_conditions_)
1039+
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
1040+
_internal_, _conditions_)
10291041

10301042
> 1. If _target_ is a String, then
1031-
> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_,
1032-
> throw an _Invalid Module Specifier_ error.
1043+
> 1. If _pattern_ is **false**, _subpath_ has non-zero length and _target_
1044+
> does not end with _"/"_, throw an _Invalid Module Specifier_ error.
10331045
> 1. If _target_ does not start with _"./"_, then
10341046
> 1. If _internal_ is **true** and _target_ does not start with _"../"_ or
10351047
> _"/"_ and is not a valid URL, then
1048+
> 1. If _pattern_ is **true**, then
1049+
> 1. Return **PACKAGE_RESOLVE**(_target_ with every instance of
1050+
> _"*"_ replaced by _subpath_, _packageURL_ + _"/"_)_.
10361051
> 1. Return **PACKAGE_RESOLVE**(_target_ + _subpath_,
10371052
> _packageURL_ + _"/"_)_.
10381053
> 1. Otherwise, throw an _Invalid Package Target_ error.
@@ -1044,8 +1059,12 @@ _conditions_)
10441059
> 1. Assert: _resolvedTarget_ is contained in _packageURL_.
10451060
> 1. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
10461061
> _"node_modules"_ segments, throw an _Invalid Module Specifier_ error.
1047-
> 1. Return the URL resolution of the concatenation of _subpath_ and
1048-
> _resolvedTarget_.
1062+
> 1. If _pattern_ is **true**, then
1063+
> 1. Return the URL resolution of _resolvedTarget_ with every instance of
1064+
> _"*"_ replaced with _subpath_.
1065+
> 1. Otherwise,
1066+
> 1. Return the URL resolution of the concatenation of _subpath_ and
1067+
> _resolvedTarget_.
10491068
> 1. Otherwise, if _target_ is a non-null Object, then
10501069
> 1. If _exports_ contains any index property keys, as defined in ECMA-262
10511070
> [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error.
@@ -1054,16 +1073,18 @@ _conditions_)
10541073
> then
10551074
> 1. Let _targetValue_ be the value of the _p_ property in _target_.
10561075
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1057-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_).
1076+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1077+
> _conditions_).
10581078
> 1. If _resolved_ is equal to **undefined**, continue the loop.
10591079
> 1. Return _resolved_.
10601080
> 1. Return **undefined**.
10611081
> 1. Otherwise, if _target_ is an Array, then
10621082
> 1. If _target.length is zero, return **null**.
10631083
> 1. For each item _targetValue_ in _target_, do
10641084
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1065-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_),
1066-
> continuing the loop on any _Invalid Package Target_ error.
1085+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1086+
> _conditions_), continuing the loop on any _Invalid Package Target_
1087+
> error.
10671088
> 1. If _resolved_ is **undefined**, continue the loop.
10681089
> 1. Return _resolved_.
10691090
> 1. Return or throw the last fallback resolution **null** return or error.

doc/api/packages.md

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -181,17 +181,17 @@ Alternatively a project could choose to export entire folders:
181181
"exports": {
182182
".": "./lib/index.js",
183183
"./lib": "./lib/index.js",
184-
"./lib/": "./lib/",
184+
"./lib/*": "./lib/*.js",
185185
"./feature": "./feature/index.js",
186-
"./feature/": "./feature/",
186+
"./feature/*": "./feature/*.js",
187187
"./package.json": "./package.json"
188188
}
189189
}
190190
```
191191

192192
As a last resort, package encapsulation can be disabled entirely by creating an
193-
export for the root of the package `"./": "./"`. This will expose every file in
194-
the package at the cost of disabling the encapsulation and potential tooling
193+
export for the root of the package `"./*": "./*"`. This will expose every file
194+
in the package at the cost of disabling the encapsulation and potential tooling
195195
benefits this provides. As the ES Module loader in Node.js enforces the use of
196196
[the full specifier path][], exporting the root rather than being explicit
197197
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';
254254
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
255255
```
256256

257-
Entire folders can also be mapped with package exports:
257+
### Subpath export patterns
258+
259+
> Stability: 1 - Experimental
260+
261+
Explicitly listing each exports subpath entry is recommended for packages with
262+
a small number of exports. But for packages that have very large numbers of
263+
subpaths this can start to cause package.json bloat and maintenance issues.
264+
265+
For these use cases, subpath export patterns can be used instead:
258266

259267
```json
260268
// ./node_modules/es-module-package/package.json
261269
{
262270
"exports": {
263-
"./features/": "./src/features/"
271+
"./features/*": "./src/features/*.js"
264272
}
265273
}
266274
```
267275

268-
With the preceding, all modules within the `./src/features/` folder
269-
are exposed deeply to `import` and `require`:
276+
The left hand matching pattern must always end in `*`. All instances of `*` on
277+
the right hand side will then be replaced with this value, including if it
278+
contains any `/` separators.
270279

271280
```js
272-
import feature from 'es-module-package/features/x.js';
281+
import featureX from 'es-module-package/features/x';
273282
// Loads ./node_modules/es-module-package/src/features/x.js
283+
284+
import featureY from 'es-module-package/features/y/y';
285+
// Loads ./node_modules/es-module-package/src/features/y/y.js
274286
```
275287

276-
When using folder mappings, ensure that you do want to expose every
277-
module inside the subfolder. Any modules which are not public
278-
should be moved to another folder to retain the encapsulation
279-
benefits of exports.
288+
This is a direct static replacement without any special handling for file
289+
extensions. In the previous example, `pkg/features/x.json` would be resolved to
290+
`./src/features/x.json.js` in the mapping.
291+
292+
The property of exports being statically enumerable is maintained with exports
293+
patterns since the individual exports for a package can be determined by
294+
treating the right hand side target pattern as a `**` glob against the list of
295+
files within the package. Because `node_modules` paths are forbidden in exports
296+
targets, this expansion is dependent on only the files of the package itself.
280297

281298
### Package exports fallbacks
282299

lib/internal/modules/esm/resolve.js

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,11 @@ function throwInvalidPackageTarget(
312312
}
313313

314314
const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
315+
const patternRegEx = /\*/g;
315316

316317
function resolvePackageTargetString(
317-
target, subpath, match, packageJSONUrl, base, internal, conditions) {
318-
if (subpath !== '' && target[target.length - 1] !== '/')
318+
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {
319+
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
319320
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
320321

321322
if (!StringPrototypeStartsWith(target, './')) {
@@ -326,8 +327,12 @@ function resolvePackageTargetString(
326327
new URL(target);
327328
isURL = true;
328329
} catch {}
329-
if (!isURL)
330-
return packageResolve(target + subpath, packageJSONUrl, conditions);
330+
if (!isURL) {
331+
const exportTarget = pattern ?
332+
StringPrototypeReplace(target, patternRegEx, subpath) :
333+
target + subpath;
334+
return packageResolve(exportTarget, packageJSONUrl, conditions);
335+
}
331336
}
332337
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
333338
}
@@ -347,6 +352,9 @@ function resolvePackageTargetString(
347352
if (RegExpPrototypeTest(invalidSegmentRegEx, subpath))
348353
throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base);
349354

355+
if (pattern)
356+
return new URL(StringPrototypeReplace(resolved.href, patternRegEx,
357+
subpath));
350358
return new URL(subpath, resolved);
351359
}
352360

@@ -361,10 +369,10 @@ function isArrayIndex(key) {
361369
}
362370

363371
function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
364-
base, internal, conditions) {
372+
base, pattern, internal, conditions) {
365373
if (typeof target === 'string') {
366374
return resolvePackageTargetString(
367-
target, subpath, packageSubpath, packageJSONUrl, base, internal,
375+
target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal,
368376
conditions);
369377
} else if (ArrayIsArray(target)) {
370378
if (target.length === 0)
@@ -376,8 +384,8 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
376384
let resolved;
377385
try {
378386
resolved = resolvePackageTarget(
379-
packageJSONUrl, targetItem, subpath, packageSubpath, base, internal,
380-
conditions);
387+
packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern,
388+
internal, conditions);
381389
} catch (e) {
382390
lastException = e;
383391
if (e.code === 'ERR_INVALID_PACKAGE_TARGET')
@@ -411,7 +419,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
411419
const conditionalTarget = target[key];
412420
const resolved = resolvePackageTarget(
413421
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
414-
internal, conditions);
422+
pattern, internal, conditions);
415423
if (resolved === undefined)
416424
continue;
417425
return resolved;
@@ -465,7 +473,7 @@ function packageExportsResolve(
465473
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
466474
const target = exports[packageSubpath];
467475
const resolved = resolvePackageTarget(
468-
packageJSONUrl, target, '', packageSubpath, base, false, conditions
476+
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
469477
);
470478
if (resolved === null || resolved === undefined)
471479
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
@@ -476,7 +484,13 @@ function packageExportsResolve(
476484
const keys = ObjectGetOwnPropertyNames(exports);
477485
for (let i = 0; i < keys.length; i++) {
478486
const key = keys[i];
479-
if (key[key.length - 1] === '/' &&
487+
if (key[key.length - 1] === '*' &&
488+
StringPrototypeStartsWith(packageSubpath,
489+
StringPrototypeSlice(key, 0, -1)) &&
490+
packageSubpath.length >= key.length &&
491+
key.length > bestMatch.length) {
492+
bestMatch = key;
493+
} else if (key[key.length - 1] === '/' &&
480494
StringPrototypeStartsWith(packageSubpath, key) &&
481495
key.length > bestMatch.length) {
482496
bestMatch = key;
@@ -485,12 +499,15 @@ function packageExportsResolve(
485499

486500
if (bestMatch) {
487501
const target = exports[bestMatch];
488-
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length);
502+
const pattern = bestMatch[bestMatch.length - 1] === '*';
503+
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length -
504+
(pattern ? 1 : 0));
489505
const resolved = resolvePackageTarget(packageJSONUrl, target, subpath,
490-
bestMatch, base, false, conditions);
506+
bestMatch, base, pattern, false,
507+
conditions);
491508
if (resolved === null || resolved === undefined)
492509
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
493-
return { resolved, exact: false };
510+
return { resolved, exact: pattern };
494511
}
495512

496513
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
@@ -509,7 +526,7 @@ function packageImportsResolve(name, base, conditions) {
509526
if (imports) {
510527
if (ObjectPrototypeHasOwnProperty(imports, name)) {
511528
const resolved = resolvePackageTarget(
512-
packageJSONUrl, imports[name], '', name, base, true, conditions
529+
packageJSONUrl, imports[name], '', name, base, false, true, conditions
513530
);
514531
if (resolved !== null)
515532
return { resolved, exact: true };
@@ -518,7 +535,13 @@ function packageImportsResolve(name, base, conditions) {
518535
const keys = ObjectGetOwnPropertyNames(imports);
519536
for (let i = 0; i < keys.length; i++) {
520537
const key = keys[i];
521-
if (key[key.length - 1] === '/' &&
538+
if (key[key.length - 1] === '*' &&
539+
StringPrototypeStartsWith(name,
540+
StringPrototypeSlice(key, 0, -1)) &&
541+
name.length >= key.length &&
542+
key.length > bestMatch.length) {
543+
bestMatch = key;
544+
} else if (key[key.length - 1] === '/' &&
522545
StringPrototypeStartsWith(name, key) &&
523546
key.length > bestMatch.length) {
524547
bestMatch = key;
@@ -527,11 +550,14 @@ function packageImportsResolve(name, base, conditions) {
527550

528551
if (bestMatch) {
529552
const target = imports[bestMatch];
530-
const subpath = StringPrototypeSubstr(name, bestMatch.length);
553+
const pattern = bestMatch[bestMatch.length - 1] === '*';
554+
const subpath = StringPrototypeSubstr(name, bestMatch.length -
555+
(pattern ? 1 : 0));
531556
const resolved = resolvePackageTarget(
532-
packageJSONUrl, target, subpath, bestMatch, base, true, conditions);
557+
packageJSONUrl, target, subpath, bestMatch, base, pattern, true,
558+
conditions);
533559
if (resolved !== null)
534-
return { resolved, exact: false };
560+
return { resolved, exact: pattern };
535561
}
536562
}
537563
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
3333
{ default: 'self-cjs' } : { default: 'self-mjs' }],
3434
// Resolve self sugar
3535
['pkgexports-sugar', { default: 'main' }],
36+
// Path patterns
37+
['pkgexports/subpath/sub-dir1', { default: 'main' }],
38+
['pkgexports/features/dir1', { default: 'main' }]
3639
]);
3740

3841
if (isRequire) {

test/fixtures/es-modules/pkgimports/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
"import": "./importbranch.js",
66
"require": "./requirebranch.js"
77
},
8-
"#subpath/": "./sub/",
8+
"#subpath/*": "./sub/*",
99
"#external": "pkgexports/valid-cjs",
10-
"#external/subpath/": "pkgexports/sub/",
10+
"#external/subpath/*": "pkgexports/sub/*",
1111
"#external/invalidsubpath/": "pkgexports/sub",
1212
"#belowbase": "../belowbase",
1313
"#url": "some:url",

test/fixtures/node_modules/pkgexports/package.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)