Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 41af927

Browse filesBrowse files
guybedfordMylesBorins
authored andcommitted
module: exports pattern support
Backport-PR-URL: #35757 PR-URL: #34718 Reviewed-By: Jan Krems <jan.krems@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent d6a13a9 commit 41af927
Copy full SHA for 41af927

File tree

Expand file treeCollapse file tree

6 files changed

+118
-49
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

6 files changed

+118
-49
lines changed
Open diff view settings
Collapse file

‎doc/api/esm.md‎

Copy file name to clipboardExpand all lines: doc/api/esm.md
+35-14Lines changed: 35 additions & 14 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,8 @@ The resolver can throw the following errors:
10381038
> 1. Set _mainExport_ to _exports_\[_"."_\].
10391039
> 1. If _mainExport_ is not **undefined**, then
10401040
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1041-
> _packageURL_, _mainExport_, _""_, **false**, _conditions_).
1041+
> _packageURL_, _mainExport_, _""_, **false**, **false**,
1042+
> _conditions_).
10421043
> 1. If _resolved_ is not **null** or **undefined**, then
10431044
> 1. Return _resolved_.
10441045
> 1. Otherwise, if _exports_ is an Object and all keys of _exports_ start with
@@ -1072,29 +1073,43 @@ _isImports_, _conditions_)
10721073
> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
10731074
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
10741075
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1075-
> _packageURL_, _target_, _""_, _isImports_, _conditions_).
1076+
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
10761077
> 1. Return the object _{ resolved, exact: **true** }_.
1077-
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_,
1078-
> sorted by length descending.
1078+
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
1079+
> or _"*"_, sorted by length descending.
10791080
> 1. For each key _expansionKey_ in _expansionKeys_, do
1081+
> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is
1082+
> not equal to the substring of _expansionKey_ excluding the last _"*"_
1083+
> character, then
1084+
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1085+
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1086+
> index of the length of _expansionKey_ minus one.
1087+
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1088+
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1089+
> _conditions_).
1090+
> 1. Return the object _{ resolved, exact: **true** }_.
10801091
> 1. If _matchKey_ starts with _expansionKey_, then
10811092
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
10821093
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
10831094
> index of the length of _expansionKey_.
10841095
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1085-
> _packageURL_, _target_, _subpath_, _isImports_, _conditions_).
1096+
> _packageURL_, _target_, _subpath_, **false**, _isImports_,
1097+
> _conditions_).
10861098
> 1. Return the object _{ resolved, exact: **false** }_.
10871099
> 1. Return the object _{ resolved: **null**, exact: **true** }_.
10881100

1089-
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_,
1090-
_conditions_)
1101+
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
1102+
_internal_, _conditions_)
10911103

10921104
> 1. If _target_ is a String, then
1093-
> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_,
1094-
> throw an _Invalid Module Specifier_ error.
1105+
> 1. If _pattern_ is **false**, _subpath_ has non-zero length and _target_
1106+
> does not end with _"/"_, throw an _Invalid Module Specifier_ error.
10951107
> 1. If _target_ does not start with _"./"_, then
10961108
> 1. If _internal_ is **true** and _target_ does not start with _"../"_ or
10971109
> _"/"_ and is not a valid URL, then
1110+
> 1. If _pattern_ is **true**, then
1111+
> 1. Return **PACKAGE_RESOLVE**(_target_ with every instance of
1112+
> _"*"_ replaced by _subpath_, _packageURL_ + _"/"_)_.
10981113
> 1. Return **PACKAGE_RESOLVE**(_target_ + _subpath_,
10991114
> _packageURL_ + _"/"_)_.
11001115
> 1. Otherwise, throw an _Invalid Package Target_ error.
@@ -1106,8 +1121,12 @@ _conditions_)
11061121
> 1. Assert: _resolvedTarget_ is contained in _packageURL_.
11071122
> 1. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
11081123
> _"node_modules"_ segments, throw an _Invalid Module Specifier_ error.
1109-
> 1. Return the URL resolution of the concatenation of _subpath_ and
1110-
> _resolvedTarget_.
1124+
> 1. If _pattern_ is **true**, then
1125+
> 1. Return the URL resolution of _resolvedTarget_ with every instance of
1126+
> _"*"_ replaced with _subpath_.
1127+
> 1. Otherwise,
1128+
> 1. Return the URL resolution of the concatenation of _subpath_ and
1129+
> _resolvedTarget_.
11111130
> 1. Otherwise, if _target_ is a non-null Object, then
11121131
> 1. If _exports_ contains any index property keys, as defined in ECMA-262
11131132
> [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error.
@@ -1116,16 +1135,18 @@ _conditions_)
11161135
> then
11171136
> 1. Let _targetValue_ be the value of the _p_ property in _target_.
11181137
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1119-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_).
1138+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1139+
> _conditions_).
11201140
> 1. If _resolved_ is equal to **undefined**, continue the loop.
11211141
> 1. Return _resolved_.
11221142
> 1. Return **undefined**.
11231143
> 1. Otherwise, if _target_ is an Array, then
11241144
> 1. If _target.length is zero, return **null**.
11251145
> 1. For each item _targetValue_ in _target_, do
11261146
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1127-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_),
1128-
> continuing the loop on any _Invalid Package Target_ error.
1147+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1148+
> _conditions_), continuing the loop on any _Invalid Package Target_
1149+
> error.
11291150
> 1. If _resolved_ is **undefined**, continue the loop.
11301151
> 1. Return _resolved_.
11311152
> 1. Return or throw the last fallback resolution **null** return or error.
Collapse file

‎doc/api/packages.md‎

Copy file name to clipboardExpand all lines: doc/api/packages.md
+30-13Lines changed: 30 additions & 13 deletions
  • Display the source diff
  • Display the rich diff
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

Collapse file

‎lib/internal/modules/esm/resolve.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/resolve.js
+45-19Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,11 @@ function throwInvalidPackageTarget(
307307
}
308308

309309
const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
310+
const patternRegEx = /\*/g;
310311

311312
function resolvePackageTargetString(
312-
target, subpath, match, packageJSONUrl, base, internal, conditions) {
313-
if (subpath !== '' && target[target.length - 1] !== '/')
313+
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {
314+
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
314315
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
315316

316317
if (!StringPrototypeStartsWith(target, './')) {
@@ -321,8 +322,12 @@ function resolvePackageTargetString(
321322
new URL(target);
322323
isURL = true;
323324
} catch {}
324-
if (!isURL)
325-
return packageResolve(target + subpath, packageJSONUrl, conditions);
325+
if (!isURL) {
326+
const exportTarget = pattern ?
327+
StringPrototypeReplace(target, patternRegEx, subpath) :
328+
target + subpath;
329+
return packageResolve(exportTarget, packageJSONUrl, conditions);
330+
}
326331
}
327332
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
328333
}
@@ -342,6 +347,9 @@ function resolvePackageTargetString(
342347
if (RegExpPrototypeTest(invalidSegmentRegEx, subpath))
343348
throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base);
344349

350+
if (pattern)
351+
return new URL(StringPrototypeReplace(resolved.href, patternRegEx,
352+
subpath));
345353
return new URL(subpath, resolved);
346354
}
347355

@@ -356,10 +364,10 @@ function isArrayIndex(key) {
356364
}
357365

358366
function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
359-
base, internal, conditions) {
367+
base, pattern, internal, conditions) {
360368
if (typeof target === 'string') {
361369
return resolvePackageTargetString(
362-
target, subpath, packageSubpath, packageJSONUrl, base, internal,
370+
target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal,
363371
conditions);
364372
} else if (ArrayIsArray(target)) {
365373
if (target.length === 0)
@@ -371,8 +379,8 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
371379
let resolved;
372380
try {
373381
resolved = resolvePackageTarget(
374-
packageJSONUrl, targetItem, subpath, packageSubpath, base, internal,
375-
conditions);
382+
packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern,
383+
internal, conditions);
376384
} catch (e) {
377385
lastException = e;
378386
if (e.code === 'ERR_INVALID_PACKAGE_TARGET')
@@ -406,7 +414,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
406414
const conditionalTarget = target[key];
407415
const resolved = resolvePackageTarget(
408416
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
409-
internal, conditions);
417+
pattern, internal, conditions);
410418
if (resolved === undefined)
411419
continue;
412420
return resolved;
@@ -460,7 +468,7 @@ function packageExportsResolve(
460468
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) {
461469
const target = exports[packageSubpath];
462470
const resolved = resolvePackageTarget(
463-
packageJSONUrl, target, '', packageSubpath, base, false, conditions
471+
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
464472
);
465473
if (resolved === null || resolved === undefined)
466474
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
@@ -471,7 +479,13 @@ function packageExportsResolve(
471479
const keys = ObjectGetOwnPropertyNames(exports);
472480
for (let i = 0; i < keys.length; i++) {
473481
const key = keys[i];
474-
if (key[key.length - 1] === '/' &&
482+
if (key[key.length - 1] === '*' &&
483+
StringPrototypeStartsWith(packageSubpath,
484+
StringPrototypeSlice(key, 0, -1)) &&
485+
packageSubpath.length >= key.length &&
486+
key.length > bestMatch.length) {
487+
bestMatch = key;
488+
} else if (key[key.length - 1] === '/' &&
475489
StringPrototypeStartsWith(packageSubpath, key) &&
476490
key.length > bestMatch.length) {
477491
bestMatch = key;
@@ -480,12 +494,15 @@ function packageExportsResolve(
480494

481495
if (bestMatch) {
482496
const target = exports[bestMatch];
483-
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length);
497+
const pattern = bestMatch[bestMatch.length - 1] === '*';
498+
const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length -
499+
(pattern ? 1 : 0));
484500
const resolved = resolvePackageTarget(packageJSONUrl, target, subpath,
485-
bestMatch, base, false, conditions);
501+
bestMatch, base, pattern, false,
502+
conditions);
486503
if (resolved === null || resolved === undefined)
487504
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
488-
return { resolved, exact: false };
505+
return { resolved, exact: pattern };
489506
}
490507

491508
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
@@ -504,7 +521,7 @@ function packageImportsResolve(name, base, conditions) {
504521
if (imports) {
505522
if (ObjectPrototypeHasOwnProperty(imports, name)) {
506523
const resolved = resolvePackageTarget(
507-
packageJSONUrl, imports[name], '', name, base, true, conditions
524+
packageJSONUrl, imports[name], '', name, base, false, true, conditions
508525
);
509526
if (resolved !== null)
510527
return { resolved, exact: true };
@@ -513,7 +530,13 @@ function packageImportsResolve(name, base, conditions) {
513530
const keys = ObjectGetOwnPropertyNames(imports);
514531
for (let i = 0; i < keys.length; i++) {
515532
const key = keys[i];
516-
if (key[key.length - 1] === '/' &&
533+
if (key[key.length - 1] === '*' &&
534+
StringPrototypeStartsWith(name,
535+
StringPrototypeSlice(key, 0, -1)) &&
536+
name.length >= key.length &&
537+
key.length > bestMatch.length) {
538+
bestMatch = key;
539+
} else if (key[key.length - 1] === '/' &&
517540
StringPrototypeStartsWith(name, key) &&
518541
key.length > bestMatch.length) {
519542
bestMatch = key;
@@ -522,11 +545,14 @@ function packageImportsResolve(name, base, conditions) {
522545

523546
if (bestMatch) {
524547
const target = imports[bestMatch];
525-
const subpath = StringPrototypeSubstr(name, bestMatch.length);
548+
const pattern = bestMatch[bestMatch.length - 1] === '*';
549+
const subpath = StringPrototypeSubstr(name, bestMatch.length -
550+
(pattern ? 1 : 0));
526551
const resolved = resolvePackageTarget(
527-
packageJSONUrl, target, subpath, bestMatch, base, true, conditions);
552+
packageJSONUrl, target, subpath, bestMatch, base, pattern, true,
553+
conditions);
528554
if (resolved !== null)
529-
return { resolved, exact: false };
555+
return { resolved, exact: pattern };
530556
}
531557
}
532558
}
Collapse file

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

Copy file name to clipboardExpand all lines: test/es-module/test-esm-exports.mjs
+3Lines 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) {
Collapse file

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

Copy file name to clipboardExpand all lines: test/fixtures/es-modules/pkgimports/package.json
+2-2Lines 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",
Collapse file

‎test/fixtures/node_modules/pkgexports/package.json‎

Copy file name to clipboardExpand all lines: test/fixtures/node_modules/pkgexports/package.json
+3-1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.