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 2f3ffc0

Browse filesBrowse files
guybedfordcodebytere
authored andcommitted
module: exports pattern support
PR-URL: #34718 Backport-PR-URL: #35385 Reviewed-By: Jan Krems <jan.krems@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent ed3278d commit 2f3ffc0
Copy full SHA for 2f3ffc0

File tree

Expand file treeCollapse file tree

5 files changed

+122
-55
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+122
-55
lines changed
Open diff view settings
Collapse file

‎doc/api/esm.md‎

Copy file name to clipboardExpand all lines: doc/api/esm.md
+69-33Lines changed: 69 additions & 33 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,14 @@ package. It is not a strong encapsulation since a direct require of any
298298
absolute subpath of the package such as
299299
`require('/path/to/node_modules/pkg/subpath.js')` will still load `subpath.js`.
300300

301-
#### Subpath exports
301+
### Subpath exports
302302

303-
When using the `"exports"` field, custom subpaths can be defined along
304-
with the main entry point by treating the main entry point as the
305-
`"."` subpath:
303+
> Stability: 1 - Experimental
306304
307-
<!-- eslint-skip -->
308-
```js
305+
When using the `"exports"` field, custom subpaths can be defined along with the
306+
main entry point by treating the main entry point as the `"."` subpath:
307+
308+
```json
309309
{
310310
"main": "./main.js",
311311
"exports": {
@@ -315,8 +315,7 @@ with the main entry point by treating the main entry point as the
315315
}
316316
```
317317

318-
Now only the defined subpath in `"exports"` can be imported by a
319-
consumer:
318+
Now only the defined subpath in `"exports"` can be imported by a consumer:
320319

321320
```js
322321
import submodule from 'es-module-package/submodule';
@@ -330,30 +329,46 @@ import submodule from 'es-module-package/private-module.js';
330329
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
331330
```
332331

333-
Entire folders can also be mapped with package exports:
332+
### Subpath export patterns
334333

335-
<!-- eslint-skip -->
336-
```js
334+
> Stability: 1 - Experimental
335+
336+
Explicitly listing each exports subpath entry is recommended for packages with
337+
a small number of exports. But for packages that have very large numbers of
338+
subpaths this can start to cause package.json bloat and maintenance issues.
339+
340+
For these use cases, subpath export patterns can be used instead:
341+
342+
```json
337343
// ./node_modules/es-module-package/package.json
338344
{
339345
"exports": {
340-
"./features/": "./src/features/"
346+
"./features/*": "./src/features/*.js"
341347
}
342348
}
343349
```
344350

345-
With the above, all modules within the `./src/features/` folder
346-
are exposed deeply to `import` and `require`:
351+
The left hand matching pattern must always end in `*`. All instances of `*` on
352+
the right hand side will then be replaced with this value, including if it
353+
contains any `/` separators.
347354

348355
```js
349-
import feature from 'es-module-package/features/x.js';
356+
import featureX from 'es-module-package/features/x';
350357
// Loads ./node_modules/es-module-package/src/features/x.js
358+
359+
import featureY from 'es-module-package/features/y/y';
360+
// Loads ./node_modules/es-module-package/src/features/y/y.js
351361
```
352362

353-
When using folder mappings, ensure that you do want to expose every
354-
module inside the subfolder. Any modules which are not public
355-
should be moved to another folder to retain the encapsulation
356-
benefits of exports.
363+
This is a direct static replacement without any special handling for file
364+
extensions. In the previous example, `pkg/features/x.json` would be resolved to
365+
`./src/features/x.json.js` in the mapping.
366+
367+
The property of exports being statically enumerable is maintained with exports
368+
patterns since the individual exports for a package can be determined by
369+
treating the right hand side target pattern as a `**` glob against the list of
370+
files within the package. Because `node_modules` paths are forbidden in exports
371+
targets, this expansion is dependent on only the files of the package itself.
357372

358373
#### Package exports fallbacks
359374

@@ -1741,7 +1756,8 @@ The resolver can throw the following errors:
17411756
> 1. Set _mainExport_ to _exports_\[_"."_\].
17421757
> 1. If _mainExport_ is not **undefined**, then
17431758
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1744-
> _packageURL_, _mainExport_, _""_, **false**, _conditions_).
1759+
> _packageURL_, _mainExport_, _""_, **false**, **false**,
1760+
> _conditions_).
17451761
> 1. If _resolved_ is not **null** or **undefined**, then
17461762
> 1. Return _resolved_.
17471763
> 1. Otherwise, if _exports_ is an Object and all keys of _exports_ start with
@@ -1775,29 +1791,43 @@ _isImports_, _conditions_)
17751791
> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then
17761792
> 1. Let _target_ be the value of _matchObj_\[_matchKey_\].
17771793
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1778-
> _packageURL_, _target_, _""_, _isImports_, _conditions_).
1794+
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
17791795
> 1. Return the object _{ resolved, exact: **true** }_.
1780-
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_,
1781-
> sorted by length descending.
1796+
> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_
1797+
> or _"*"_, sorted by length descending.
17821798
> 1. For each key _expansionKey_ in _expansionKeys_, do
1799+
> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is
1800+
> not equal to the substring of _expansionKey_ excluding the last _"*"_
1801+
> character, then
1802+
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
1803+
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
1804+
> index of the length of _expansionKey_ minus one.
1805+
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1806+
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1807+
> _conditions_).
1808+
> 1. Return the object _{ resolved, exact: **true** }_.
17831809
> 1. If _matchKey_ starts with _expansionKey_, then
17841810
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\].
17851811
> 1. Let _subpath_ be the substring of _matchKey_ starting at the
17861812
> index of the length of _expansionKey_.
17871813
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1788-
> _packageURL_, _target_, _subpath_, _isImports_, _conditions_).
1814+
> _packageURL_, _target_, _subpath_, **false**, _isImports_,
1815+
> _conditions_).
17891816
> 1. Return the object _{ resolved, exact: **false** }_.
17901817
> 1. Return the object _{ resolved: **null**, exact: **true** }_.
17911818

1792-
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_,
1793-
_conditions_)
1819+
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
1820+
_internal_, _conditions_)
17941821

17951822
> 1. If _target_ is a String, then
1796-
> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_,
1797-
> throw an _Invalid Module Specifier_ error.
1823+
> 1. If _pattern_ is **false**, _subpath_ has non-zero length and _target_
1824+
> does not end with _"/"_, throw an _Invalid Module Specifier_ error.
17981825
> 1. If _target_ does not start with _"./"_, then
17991826
> 1. If _internal_ is **true** and _target_ does not start with _"../"_ or
18001827
> _"/"_ and is not a valid URL, then
1828+
> 1. If _pattern_ is **true**, then
1829+
> 1. Return **PACKAGE_RESOLVE**(_target_ with every instance of
1830+
> _"*"_ replaced by _subpath_, _packageURL_ + _"/"_)_.
18011831
> 1. Return **PACKAGE_RESOLVE**(_target_ + _subpath_,
18021832
> _packageURL_ + _"/"_)_.
18031833
> 1. Otherwise, throw an _Invalid Package Target_ error.
@@ -1809,8 +1839,12 @@ _conditions_)
18091839
> 1. Assert: _resolvedTarget_ is contained in _packageURL_.
18101840
> 1. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or
18111841
> _"node_modules"_ segments, throw an _Invalid Module Specifier_ error.
1812-
> 1. Return the URL resolution of the concatenation of _subpath_ and
1813-
> _resolvedTarget_.
1842+
> 1. If _pattern_ is **true**, then
1843+
> 1. Return the URL resolution of _resolvedTarget_ with every instance of
1844+
> _"*"_ replaced with _subpath_.
1845+
> 1. Otherwise,
1846+
> 1. Return the URL resolution of the concatenation of _subpath_ and
1847+
> _resolvedTarget_.
18141848
> 1. Otherwise, if _target_ is a non-null Object, then
18151849
> 1. If _exports_ contains any index property keys, as defined in ECMA-262
18161850
> [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error.
@@ -1819,16 +1853,18 @@ _conditions_)
18191853
> then
18201854
> 1. Let _targetValue_ be the value of the _p_ property in _target_.
18211855
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1822-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_).
1856+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1857+
> _conditions_).
18231858
> 1. If _resolved_ is equal to **undefined**, continue the loop.
18241859
> 1. Return _resolved_.
18251860
> 1. Return **undefined**.
18261861
> 1. Otherwise, if _target_ is an Array, then
18271862
> 1. If _target.length is zero, return **null**.
18281863
> 1. For each item _targetValue_ in _target_, do
18291864
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
1830-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_),
1831-
> continuing the loop on any _Invalid Package Target_ error.
1865+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1866+
> _conditions_), continuing the loop on any _Invalid Package Target_
1867+
> error.
18321868
> 1. If _resolved_ is **undefined**, continue the loop.
18331869
> 1. Return _resolved_.
18341870
> 1. Return or throw the last fallback resolution **null** return or error.
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.