Skip to content

Commit e98d89c

Browse files
guybedfordMylesBorins
authored andcommitted
module: conditional exports with flagged conditions
PR-URL: #29978 Reviewed-By: Jan Krems <jan.krems@gmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com>
1 parent 085af30 commit e98d89c

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
@@ -170,6 +170,15 @@ the ability to import a directory that has an index file.
170170

171171
Please see [customizing esm specifier resolution][] for example usage.
172172

173+
### `--experimental-conditional-exports
174+
<!-- YAML
175+
added: REPLACEME
176+
-->
177+
178+
Enable experimental support for the `"require"` and `"node"` conditional
179+
package export resolutions.
180+
See [Conditional Exports][] for more information.
181+
173182
### `--experimental-json-modules`
174183
<!-- YAML
175184
added: v12.9.0
@@ -1021,6 +1030,7 @@ Node.js options that are allowed are:
10211030
* `--enable-fips`
10221031
* `--enable-source-maps`
10231032
* `--es-module-specifier-resolution`
1033+
* `--experimental-conditional-exports`
10241034
* `--experimental-json-modules`
10251035
* `--experimental-loader`
10261036
* `--experimental-modules`
@@ -1324,3 +1334,4 @@ greater than `4` (its current default value). For more information, see the
13241334
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
13251335
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
13261336
[context-aware]: addons.html#addons_context_aware_addons
1337+
[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
## <code>import</code> Specifiers
362469

@@ -806,6 +913,9 @@ of these top-level routines unless stated otherwise.
806913
807914
_isMain_ is **true** when resolving the Node.js application entry point.
808915
916+
_defaultEnv_ is the conditional environment name priority array,
917+
`["node", "default"]`.
918+
809919
<details>
810920
<summary>Resolver algorithm specification</summary>
811921
@@ -905,14 +1015,16 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9051015
> 1. If _pjson_ is **null**, then
9061016
> 1. Throw a _Module Not Found_ error.
9071017
> 1. If _pjson.exports_ is not **null** or **undefined**, then
908-
> 1. If _pjson.exports_ is a String or Array, then
1018+
> 1. If _exports_ is an Object with both a key starting with _"."_ and a key
1019+
> not starting with _"."_, throw a "Invalid Package Configuration" error.
1020+
> 1. If _pjson.exports_ is a String or Array, or an Object containing no
1021+
> keys starting with _"."_, then
1022+
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
1023+
> _pjson.exports_, _""_).
1024+
> 1. If _pjson.exports_ is an Object containing a _"."_ property, then
1025+
> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_.
9091026
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
910-
> _pjson.exports_, "")_.
911-
> 1. If _pjson.exports is an Object, then
912-
> 1. If _pjson.exports_ contains a _"."_ property, then
913-
> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_.
914-
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
915-
> _mainExport_, "")_.
1027+
> _mainExport_, _""_).
9161028
> 1. If _pjson.main_ is a String, then
9171029
> 1. Let _resolvedMain_ be the URL resolution of _packageURL_, "/", and
9181030
> _pjson.main_.
@@ -926,13 +1038,14 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9261038
> 1. Return _legacyMainURL_.
9271039
9281040
**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packagePath_, _exports_)
929-
930-
> 1. If _exports_ is an Object, then
1041+
> 1. If _exports_ is an Object with both a key starting with _"."_ and a key not
1042+
> starting with _"."_, throw an "Invalid Package Configuration" error.
1043+
> 1. If _exports_ is an Object and all keys of _exports_ start with _"."_, then
9311044
> 1. Set _packagePath_ to _"./"_ concatenated with _packagePath_.
9321045
> 1. If _packagePath_ is a key of _exports_, then
9331046
> 1. Let _target_ be the value of _exports\[packagePath\]_.
9341047
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
935-
> _""_).
1048+
> _""_, _defaultEnv_).
9361049
> 1. Let _directoryKeys_ be the list of keys of _exports_ ending in
9371050
> _"/"_, sorted by length descending.
9381051
> 1. For each key _directory_ in _directoryKeys_, do
@@ -941,10 +1054,10 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9411054
> 1. Let _subpath_ be the substring of _target_ starting at the index
9421055
> of the length of _directory_.
9431056
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
944-
> _subpath_).
1057+
> _subpath_, _defaultEnv_).
9451058
> 1. Throw a _Module Not Found_ error.
9461059
947-
**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_)
1060+
**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _env_)
9481061
9491062
> 1. If _target_ is a String, then
9501063
> 1. If _target_ does not start with _"./"_, throw a _Module Not Found_
@@ -960,12 +1073,20 @@ _isMain_ is **true** when resolving the Node.js application entry point.
9601073
> _subpath_ and _resolvedTarget_.
9611074
> 1. If _resolved_ is contained in _resolvedTarget_, then
9621075
> 1. Return _resolved_.
1076+
> 1. Otherwise, if _target_ is a non-null Object, then
1077+
> 1. If _target_ has an object key matching one of the names in _env_, then
1078+
> 1. Let _targetValue_ be the corresponding value of the first object key
1079+
> of _target_ in _env_.
1080+
> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**
1081+
> (_packageURL_, _targetValue_, _subpath_, _env_).
1082+
> 1. Assert: _resolved_ is a String.
1083+
> 1. Return _resolved_.
9631084
> 1. Otherwise, if _target_ is an Array, then
9641085
> 1. For each item _targetValue_ in _target_, do
965-
> 1. If _targetValue_ is not a String, continue the loop.
1086+
> 1. If _targetValue_ is an Array, continue the loop.
9661087
> 1. Let _resolved_ be the result of
9671088
> **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _targetValue_,
968-
> _subpath_), continuing the loop on abrupt completion.
1089+
> _subpath_, _env_), continuing the loop on abrupt completion.
9691090
> 1. Assert: _resolved_ is a String.
9701091
> 1. Return _resolved_.
9711092
> 1. Throw a _Module Not Found_ error.
@@ -1033,6 +1154,7 @@ success!
10331154
```
10341155
10351156
[CommonJS]: modules.html
1157+
[Conditional Exports]: #esm_conditional_exports
10361158
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
10371159
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
10381160
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
@@ -1045,7 +1167,7 @@ success!
10451167
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
10461168
[`module.createRequire()`]: modules.html#modules_module_createrequire_filename
10471169
[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports
1048-
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
10491170
[package exports]: #esm_package_exports
1171+
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
10501172
[special scheme]: https://url.spec.whatwg.org/#special-scheme
10511173
[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
@@ -113,6 +113,9 @@ Requires Node.js to be built with
113113
.It Fl -es-module-specifier-resolution
114114
Select extension resolution algorithm for ES Modules; either 'explicit' (default) or 'node'
115115
.
116+
.It Fl -experimental-conditional-exports
117+
Enable experimental support for "require" and "node" conditional export targets.
118+
.
116119
.It Fl -experimental-json-modules
117120
Enable experimental JSON interop support for the ES Module loader.
118121
.

lib/internal/errors.js

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

0 commit comments

Comments
 (0)