Skip to content

Commit 7e1f050

Browse files
guybedfordMylesBorins
authored andcommitted
module: conditional exports with flagged conditions
PR-URL: #29978 Reviewed-By: Jan Krems <[email protected]> Reviewed-By: Myles Borins <[email protected]>
1 parent 5679b61 commit 7e1f050

File tree

21 files changed

+485
-209
lines changed

21 files changed

+485
-209
lines changed

doc/api/cli.md

+11
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,15 @@ the ability to import a directory that has an index file.
161161

162162
Please see [customizing esm specifier resolution][] for example usage.
163163

164+
### `--experimental-conditional-exports
165+
<!-- YAML
166+
added: REPLACEME
167+
-->
168+
169+
Enable experimental support for the `"require"` and `"node"` conditional
170+
package export resolutions.
171+
See [Conditional Exports][] for more information.
172+
164173
### `--experimental-json-modules`
165174
<!-- YAML
166175
added: v12.9.0
@@ -1055,6 +1064,7 @@ Node.js options that are allowed are:
10551064
* `--enable-fips`
10561065
* `--enable-source-maps`
10571066
* `--es-module-specifier-resolution`
1067+
* `--experimental-conditional-exports`
10581068
* `--experimental-json-modules`
10591069
* `--experimental-loader`
10601070
* `--experimental-modules`
@@ -1359,3 +1369,4 @@ greater than `4` (its current default value). For more information, see the
13591369
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
13601370
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
13611371
[context-aware]: addons.html#addons_context_aware_addons
1372+
[Conditional Exports]: esm.html#esm_conditional_exports

doc/api/esm.md

+153-31
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ that would only be supported in ES module-supporting versions of Node.js (and
260260
other runtimes). New packages could be published containing only ES module
261261
sources, and would be compatible only with ES module-supporting runtimes.
262262

263+
To define separate package entry points for use by `require` and by `import`,
264+
see [Conditional Exports][].
265+
263266
### Package Exports
264267

265268
By default, all subpaths from a package can be imported (`import 'pkg/x.js'`).
@@ -313,50 +316,154 @@ If a package has no exports, setting `"exports": false` can be used instead of
313316
`"exports": {}` to indicate the package does not intend for submodules to be
314317
exposed.
315318

316-
Exports can also be used to map the main entry point of a package:
319+
Any invalid exports entries will be ignored. This includes exports not
320+
starting with `"./"` or a missing trailing `"/"` for directory exports.
321+
322+
Array fallback support is provided for exports, similarly to import maps
323+
in order to be forwards-compatible with possible fallback workflows in future:
317324

318325
<!-- eslint-skip -->
319326
```js
320-
// ./node_modules/es-module-package/package.json
321327
{
322328
"exports": {
323-
".": "./main.js"
329+
"./submodule": ["not:valid", "./submodule.js"]
324330
}
325331
}
326332
```
327333

328-
where the "." indicates loading the package without any subpath. Exports will
329-
always override any existing `"main"` value for both CommonJS and
330-
ES module packages.
334+
Since `"not:valid"` is not a supported target, `"./submodule.js"` is used
335+
instead as the fallback, as if it were the only target.
336+
337+
Defining a `"."` export will define the main entry point for the package,
338+
and will always take precedence over the `"main"` field in the `package.json`.
331339

332-
For packages with only a main entry point, an `"exports"` value of just
333-
a string is also supported:
340+
This allows defining a different entry point for Node.js versions that support
341+
ECMAScript modules and versions that don't, for example:
342+
343+
<!-- eslint-skip -->
344+
```js
345+
{
346+
"main": "./main-legacy.cjs",
347+
"exports": {
348+
".": "./main-modern.cjs"
349+
}
350+
}
351+
```
352+
353+
#### Conditional Exports
354+
355+
Conditional exports provide a way to map to different paths depending on
356+
certain conditions. They are supported for both CommonJS and ES module imports.
357+
358+
For example, a package that wants to provide different ES module exports for
359+
Node.js and the browser can be written:
360+
361+
<!-- eslint-skip -->
362+
```js
363+
// ./node_modules/pkg/package.json
364+
{
365+
"type": "module",
366+
"main": "./index.js",
367+
"exports": {
368+
"./feature": {
369+
"browser": "./feature-browser.js",
370+
"default": "./feature-default.js"
371+
}
372+
}
373+
}
374+
```
375+
376+
When resolving the `"."` export, if no matching target is found, the `"main"`
377+
will be used as the final fallback.
378+
379+
The conditions supported in Node.js are matched in the following order:
380+
381+
1. `"require"` - matched when the package is loaded via `require()`.
382+
_This is currently only supported behind the
383+
`--experimental-conditional-exports` flag._
384+
2. `"node"` - matched for any Node.js environment. Can be a CommonJS or ES
385+
module file. _This is currently only supported behind the
386+
`--experimental-conditional-exports` flag._
387+
3. `"default"` - the generic fallback that will always match if no other
388+
more specific condition is matched first. Can be a CommonJS or ES module
389+
file.
390+
391+
Using the `"require"` condition it is possible to define a package that will
392+
have a different exported value for CommonJS and ES modules, which can be a
393+
hazard in that it can result in having two separate instances of the same
394+
package in use in an application, which can cause a number of bugs.
395+
396+
Other conditions such as `"browser"`, `"electron"`, `"deno"`, `"react-native"`,
397+
etc. could be defined in other runtimes or tools.
398+
399+
#### Exports Sugar
400+
401+
If the `"."` export is the only export, the `"exports"` field provides sugar
402+
for this case being the direct `"exports"` field value.
403+
404+
If the `"."` export has a fallback array or string value, then the `"exports"`
405+
field can be set to this value directly.
406+
407+
<!-- eslint-skip -->
408+
```js
409+
{
410+
"exports": {
411+
".": "./main.js"
412+
}
413+
}
414+
```
415+
416+
can be written:
334417

335418
<!-- eslint-skip -->
336419
```js
337-
// ./node_modules/es-module-package/package.json
338420
{
339421
"exports": "./main.js"
340422
}
341423
```
342424

343-
Any invalid exports entries will be ignored. This includes exports not
344-
starting with `"./"` or a missing trailing `"/"` for directory exports.
425+
When using conditional exports, the rule is that all keys in the object mapping
426+
must not start with a `"."` otherwise they would be indistinguishable from
427+
exports subpaths.
345428

346-
Array fallback support is provided for exports, similarly to import maps
347-
in order to be forward-compatible with fallback workflows in future:
429+
<!-- eslint-skip -->
430+
```js
431+
{
432+
"exports": {
433+
".": {
434+
"require": "./main.cjs",
435+
"default": "./main.js"
436+
}
437+
}
438+
}
439+
```
440+
441+
can be written:
348442

349443
<!-- eslint-skip -->
350444
```js
351445
{
352446
"exports": {
353-
"./submodule": ["not:valid", "./submodule.js"]
447+
"require": "./main.cjs",
448+
"default": "./main.js"
354449
}
355450
}
356451
```
357452

358-
Since `"not:valid"` is not a supported target, `"./submodule.js"` is used
359-
instead as the fallback, as if it were the only target.
453+
If writing any exports value that mixes up these two forms, an error will be
454+
thrown:
455+
456+
<!-- eslint-skip -->
457+
```js
458+
{
459+
// Throws on resolution!
460+
"exports": {
461+
"./feature": "./lib/feature.js",
462+
"require": "./main.cjs",
463+
"default": "./main.js"
464+
}
465+
}
466+
```
360467

361468
## `import` Specifiers
362469

@@ -805,6 +912,9 @@ of these top-level routines unless stated otherwise.
805912
806913
_isMain_ is **true** when resolving the Node.js application entry point.
807914
915+
_defaultEnv_ is the conditional environment name priority array,
916+
`["node", "default"]`.
917+
808918
<details>
809919
<summary>Resolver algorithm specification</summary>
810920
@@ -904,14 +1014,16 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9041014
> 1. If _pjson_ is **null**, then
9051015
> 1. Throw a _Module Not Found_ error.
9061016
> 1. If _pjson.exports_ is not **null** or **undefined**, then
907-
> 1. If _pjson.exports_ is a String or Array, then
1017+
> 1. If _exports_ is an Object with both a key starting with _"."_ and a key
1018+
> not starting with _"."_, throw a "Invalid Package Configuration" error.
1019+
> 1. If _pjson.exports_ is a String or Array, or an Object containing no
1020+
> keys starting with _"."_, then
1021+
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
1022+
> _pjson.exports_, _""_).
1023+
> 1. If _pjson.exports_ is an Object containing a _"."_ property, then
1024+
> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_.
9081025
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
909-
> _pjson.exports_, "")_.
910-
> 1. If _pjson.exports is an Object, then
911-
> 1. If _pjson.exports_ contains a _"."_ property, then
912-
> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_.
913-
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
914-
> _mainExport_, "")_.
1026+
> _mainExport_, _""_).
9151027
> 1. If _pjson.main_ is a String, then
9161028
> 1. Let _resolvedMain_ be the URL resolution of _packageURL_, "/", and
9171029
> _pjson.main_.
@@ -925,13 +1037,14 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9251037
> 1. Return _legacyMainURL_.
9261038
9271039
**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packagePath_, _exports_)
928-
929-
> 1. If _exports_ is an Object, then
1040+
> 1. If _exports_ is an Object with both a key starting with _"."_ and a key not
1041+
> starting with _"."_, throw an "Invalid Package Configuration" error.
1042+
> 1. If _exports_ is an Object and all keys of _exports_ start with _"."_, then
9301043
> 1. Set _packagePath_ to _"./"_ concatenated with _packagePath_.
9311044
> 1. If _packagePath_ is a key of _exports_, then
9321045
> 1. Let _target_ be the value of _exports\[packagePath\]_.
9331046
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
934-
> _""_).
1047+
> _""_, _defaultEnv_).
9351048
> 1. Let _directoryKeys_ be the list of keys of _exports_ ending in
9361049
> _"/"_, sorted by length descending.
9371050
> 1. For each key _directory_ in _directoryKeys_, do
@@ -940,10 +1053,10 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9401053
> 1. Let _subpath_ be the substring of _target_ starting at the index
9411054
> of the length of _directory_.
9421055
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
943-
> _subpath_).
1056+
> _subpath_, _defaultEnv_).
9441057
> 1. Throw a _Module Not Found_ error.
9451058
946-
**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_)
1059+
**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _env_)
9471060
9481061
> 1. If _target_ is a String, then
9491062
> 1. If _target_ does not start with _"./"_, throw a _Module Not Found_
@@ -959,12 +1072,20 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9591072
> _subpath_ and _resolvedTarget_.
9601073
> 1. If _resolved_ is contained in _resolvedTarget_, then
9611074
> 1. Return _resolved_.
1075+
> 1. Otherwise, if _target_ is a non-null Object, then
1076+
> 1. If _target_ has an object key matching one of the names in _env_, then
1077+
> 1. Let _targetValue_ be the corresponding value of the first object key
1078+
> of _target_ in _env_.
1079+
> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**
1080+
> (_packageURL_, _targetValue_, _subpath_, _env_).
1081+
> 1. Assert: _resolved_ is a String.
1082+
> 1. Return _resolved_.
9621083
> 1. Otherwise, if _target_ is an Array, then
9631084
> 1. For each item _targetValue_ in _target_, do
964-
> 1. If _targetValue_ is not a String, continue the loop.
1085+
> 1. If _targetValue_ is an Array, continue the loop.
9651086
> 1. Let _resolved_ be the result of
9661087
> **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _targetValue_,
967-
> _subpath_), continuing the loop on abrupt completion.
1088+
> _subpath_, _env_), continuing the loop on abrupt completion.
9681089
> 1. Assert: _resolved_ is a String.
9691090
> 1. Return _resolved_.
9701091
> 1. Throw a _Module Not Found_ error.
@@ -1032,6 +1153,7 @@ success!
10321153
```
10331154
10341155
[CommonJS]: modules.html
1156+
[Conditional Exports]: #esm_conditional_exports
10351157
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
10361158
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
10371159
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
@@ -1044,7 +1166,7 @@ success!
10441166
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
10451167
[`module.createRequire()`]: modules.html#modules_module_createrequire_filename
10461168
[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports
1047-
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
10481169
[package exports]: #esm_package_exports
1170+
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
10491171
[special scheme]: https://url.spec.whatwg.org/#special-scheme
10501172
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules

doc/api/modules.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,17 @@ RESOLVE_BARE_SPECIFIER(DIR, X)
232232
2. If X matches this pattern and DIR/name/package.json is a file:
233233
a. Parse DIR/name/package.json, and look for "exports" field.
234234
b. If "exports" is null or undefined, GOTO 3.
235-
c. Find the longest key in "exports" that the subpath starts with.
236-
d. If no such key can be found, throw "not found".
237-
e. let RESOLVED_URL =
235+
c. If "exports" is an object with some keys starting with "." and some keys
236+
not starting with ".", throw "invalid config".
237+
c. If "exports" is a string, or object with no keys starting with ".", treat
238+
it as having that value as its "." object property.
239+
d. If subpath is "." and "exports" does not have a "." entry, GOTO 3.
240+
e. Find the longest key in "exports" that the subpath starts with.
241+
f. If no such key can be found, throw "not found".
242+
g. let RESOLVED_URL =
238243
PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), exports[key],
239244
subpath.slice(key.length)), as defined in the esm resolver.
240-
f. return fileURLToPath(RESOLVED_URL)
245+
h. return fileURLToPath(RESOLVED_URL)
241246
3. return DIR/X
242247
```
243248

doc/node.1

+3
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ Requires Node.js to be built with
108108
.It Fl -es-module-specifier-resolution
109109
Select extension resolution algorithm for ES Modules; either 'explicit' (default) or 'node'
110110
.
111+
.It Fl -experimental-conditional-exports
112+
Enable experimental support for "require" and "node" conditional export targets.
113+
.
111114
.It Fl -experimental-json-modules
112115
Enable experimental JSON interop support for the ES Module loader.
113116
.

lib/internal/errors.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ E('ERR_INVALID_OPT_VALUE', (name, value) =>
976976
E('ERR_INVALID_OPT_VALUE_ENCODING',
977977
'The value "%s" is invalid for option "encoding"', TypeError);
978978
E('ERR_INVALID_PACKAGE_CONFIG',
979-
'Invalid package config in \'%s\' imported from %s', Error);
979+
'Invalid package config for \'%s\', %s', Error);
980980
E('ERR_INVALID_PERFORMANCE_MARK',
981981
'The "%s" performance mark has not been set', Error);
982982
E('ERR_INVALID_PROTOCOL',

0 commit comments

Comments
 (0)