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‎

Lines changed: 69 additions & 33 deletions
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
importsubmodulefrom'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-
importfeaturefrom'es-module-package/features/x.js';
356+
importfeatureXfrom'es-module-package/features/x';
350357
// Loads ./node_modules/es-module-package/src/features/x.js
358+
359+
importfeatureYfrom'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_isnot**undefined**, then
17431758
>1.Let_resolved_betheresultof**PACKAGE_TARGET_RESOLVE**(
1744-
> _packageURL_, _mainExport_, _""_, **false**, _conditions_).
1759+
> _packageURL_, _mainExport_, _""_, **false**, **false**,
1760+
> _conditions_).
17451761
>1.If_resolved_isnot**null**or**undefined**, then
17461762
>1.Return_resolved_.
17471763
>1.Otherwise, if_exports_isanObjectandallkeysof_exports_startwith
@@ -1775,29 +1791,43 @@ _isImports_, _conditions_)
17751791
>1.If_matchKey_isakeyof_matchObj_, anddoesnotendin_"*"_, then
17761792
>1.Let_target_bethevalueof_matchObj_\[_matchKey_\].
17771793
>1.Let_resolved_betheresultof**PACKAGE_TARGET_RESOLVE**(
1778-
> _packageURL_, _target_, _""_, _isImports_, _conditions_).
1794+
> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_).
17791795
>1.Returntheobject_{resolved, exact:**true** }_.
1780-
>1.Let_expansionKeys_bethelistofkeysof_matchObj_endingin_"/"_,
1781-
>sortedbylengthdescending.
1796+
>1.Let_expansionKeys_bethelistofkeysof_matchObj_endingin_"/"_
1797+
>or_"*"_, sortedbylengthdescending.
17821798
>1.Foreachkey_expansionKey_in_expansionKeys_, do
1799+
>1.If_expansionKey_endsin_"*"_and_matchKey_startswithbutis
1800+
>notequaltothesubstringof_expansionKey_excludingthelast_"*"_
1801+
>character, then
1802+
>1.Let_target_bethevalueof_matchObj_\[_expansionKey_\].
1803+
>1.Let_subpath_bethesubstringof_matchKey_startingatthe
1804+
>indexofthelengthof_expansionKey_minusone.
1805+
>1.Let_resolved_betheresultof**PACKAGE_TARGET_RESOLVE**(
1806+
> _packageURL_, _target_, _subpath_, **true**, _isImports_,
1807+
> _conditions_).
1808+
>1.Returntheobject_{resolved, exact:**true** }_.
17831809
>1.If_matchKey_startswith_expansionKey_, then
17841810
>1.Let_target_bethevalueof_matchObj_\[_expansionKey_\].
17851811
>1.Let_subpath_bethesubstringof_matchKey_startingatthe
17861812
>indexofthelengthof_expansionKey_.
17871813
>1.Let_resolved_betheresultof**PACKAGE_TARGET_RESOLVE**(
1788-
> _packageURL_, _target_, _subpath_, _isImports_, _conditions_).
1814+
> _packageURL_, _target_, _subpath_, **false**, _isImports_,
1815+
> _conditions_).
17891816
>1.Returntheobject_{resolved, exact:**false** }_.
17901817
>1.Returntheobject_{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_isaString, then
1796-
>1.If_subpath_hasnon-zerolengthand_target_doesnotendwith_"/"_,
1797-
>throwan_InvalidModuleSpecifier_error.
1823+
>1.If_pattern_is**false**, _subpath_hasnon-zerolengthand_target_
1824+
>doesnotendwith_"/"_, throwan_InvalidModuleSpecifier_error.
17981825
>1.If_target_doesnotstartwith_"./"_, then
17991826
>1.If_internal_is**true**and_target_doesnotstartwith_"../"_or
18001827
>_"/"_andisnotavalidURL, 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, throwan_InvalidPackageTarget_error.
@@ -1809,8 +1839,12 @@ _conditions_)
18091839
>1.Assert:_resolvedTarget_iscontainedin_packageURL_.
18101840
>1.If_subpath_spliton_"/"_or_"\\"_containsany_"."_, _".."_or
18111841
>_"node_modules"_segments, throwan_InvalidModuleSpecifier_error.
1812-
>1.ReturntheURLresolutionoftheconcatenationof_subpath_and
1813-
>_resolvedTarget_.
1842+
>1.If_pattern_is**true**, then
1843+
>1.ReturntheURLresolutionof_resolvedTarget_witheveryinstanceof
1844+
>_"*"_replacedwith_subpath_.
1845+
>1.Otherwise,
1846+
>1.ReturntheURLresolutionoftheconcatenationof_subpath_and
1847+
>_resolvedTarget_.
18141848
>1.Otherwise, if_target_isanon-nullObject, then
18151849
>1.If_exports_containsanyindexpropertykeys, asdefinedinECMA-262
18161850
> [6.1.7ArrayIndex][], throwan_InvalidPackageConfiguration_error.
@@ -1819,16 +1853,18 @@ _conditions_)
18191853
>then
18201854
>1.Let_targetValue_bethevalueofthe_p_propertyin_target_.
18211855
>1.Let_resolved_betheresultof**PACKAGE_TARGET_RESOLVE**(
1822-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_).
1856+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1857+
> _conditions_).
18231858
>1.If_resolved_isequalto**undefined**, continuetheloop.
18241859
>1.Return_resolved_.
18251860
>1.Return**undefined**.
18261861
>1.Otherwise, if_target_isanArray, then
18271862
>1.If_target.lengthiszero, return**null**.
18281863
>1.Foreachitem_targetValue_in_target_, do
18291864
>1.Let_resolved_betheresultof**PACKAGE_TARGET_RESOLVE**(
1830-
> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_),
1831-
>continuingthelooponany_InvalidPackageTarget_error.
1865+
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1866+
> _conditions_), continuingthelooponany_InvalidPackageTarget_
1867+
>error.
18321868
>1.If_resolved_is**undefined**, continuetheloop.
18331869
>1.Return_resolved_.
18341870
>1.Returnorthrowthelastfallbackresolution**null**returnorerror.

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

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,11 @@ function throwInvalidPackageTarget(
307307
}
308308

309309
constinvalidSegmentRegEx=/(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
310+
constpatternRegEx=/\*/g;
310311

311312
functionresolvePackageTargetString(
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
newURL(target);
322323
isURL=true;
323324
}catch{}
324-
if(!isURL)
325-
returnpackageResolve(target+subpath,packageJSONUrl,conditions);
325+
if(!isURL){
326+
constexportTarget=pattern ?
327+
StringPrototypeReplace(target,patternRegEx,subpath) :
328+
target+subpath;
329+
returnpackageResolve(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+
returnnewURL(StringPrototypeReplace(resolved.href,patternRegEx,
352+
subpath));
345353
returnnewURL(subpath,resolved);
346354
}
347355

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

358366
functionresolvePackageTarget(packageJSONUrl,target,subpath,packageSubpath,
359-
base,internal,conditions){
367+
base,pattern,internal,conditions){
360368
if(typeoftarget==='string'){
361369
returnresolvePackageTargetString(
362-
target,subpath,packageSubpath,packageJSONUrl,base,internal,
370+
target,subpath,packageSubpath,packageJSONUrl,base,pattern,internal,
363371
conditions);
364372
}elseif(ArrayIsArray(target)){
365373
if(target.length===0)
@@ -371,8 +379,8 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
371379
letresolved;
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
constconditionalTarget=target[key];
407415
constresolved=resolvePackageTarget(
408416
packageJSONUrl,conditionalTarget,subpath,packageSubpath,base,
409-
internal,conditions);
417+
pattern,internal,conditions);
410418
if(resolved===undefined)
411419
continue;
412420
returnresolved;
@@ -460,7 +468,7 @@ function packageExportsResolve(
460468
if(ObjectPrototypeHasOwnProperty(exports,packageSubpath)){
461469
consttarget=exports[packageSubpath];
462470
constresolved=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
constkeys=ObjectGetOwnPropertyNames(exports);
472480
for(leti=0;i<keys.length;i++){
473481
constkey=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+
}elseif(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
consttarget=exports[bestMatch];
483-
constsubpath=StringPrototypeSubstr(packageSubpath,bestMatch.length);
497+
constpattern=bestMatch[bestMatch.length-1]==='*';
498+
constsubpath=StringPrototypeSubstr(packageSubpath,bestMatch.length-
499+
(pattern ? 1 : 0));
484500
constresolved=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
constresolved=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
constkeys=ObjectGetOwnPropertyNames(imports);
514531
for(leti=0;i<keys.length;i++){
515532
constkey=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+
}elseif(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
consttarget=imports[bestMatch];
525-
constsubpath=StringPrototypeSubstr(name,bestMatch.length);
548+
constpattern=bestMatch[bestMatch.length-1]==='*';
549+
constsubpath=StringPrototypeSubstr(name,bestMatch.length-
550+
(pattern ? 1 : 0));
526551
constresolved=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‎

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)