Skip to content

Commit 2f3ffc0

Browse files
guybedfordcodebytere
authored andcommitted
module: exports pattern support
PR-URL: #34718 Backport-PR-URL: #35385 Reviewed-By: Jan Krems <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent ed3278d commit 2f3ffc0

File tree

5 files changed

+122
-55
lines changed

5 files changed

+122
-55
lines changed

doc/api/esm.md

+69-33
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.

lib/internal/modules/esm/resolve.js

+45-19
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
}

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

+3
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

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

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)