diff options
author | Guy Bedford <guybedford@gmail.com> | 2019-10-13 19:27:39 -0400 |
---|---|---|
committer | Guy Bedford <guybedford@gmail.com> | 2019-11-08 17:26:26 -0500 |
commit | 2367474db46136aecb87b27d82bfa597a95aeedd (patch) | |
tree | 58fd8eb4ad143677a791b00f994965d9573e562b /doc/api | |
parent | c73ef32d355aa58672c15e89534c375dd2246f3c (diff) | |
download | node-new-2367474db46136aecb87b27d82bfa597a95aeedd.tar.gz |
module: conditional exports with flagged conditions
PR-URL: https://github.com/nodejs/node/pull/29978
Reviewed-By: Jan Krems <jan.krems@gmail.com>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Diffstat (limited to 'doc/api')
-rw-r--r-- | doc/api/cli.md | 11 | ||||
-rw-r--r-- | doc/api/esm.md | 184 | ||||
-rw-r--r-- | doc/api/modules.md | 13 |
3 files changed, 173 insertions, 35 deletions
diff --git a/doc/api/cli.md b/doc/api/cli.md index fa46a00aff..d305644a0f 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -170,6 +170,15 @@ the ability to import a directory that has an index file. Please see [customizing esm specifier resolution][] for example usage. +### `--experimental-conditional-exports +<!-- YAML +added: REPLACEME +--> + +Enable experimental support for the `"require"` and `"node"` conditional +package export resolutions. +See [Conditional Exports][] for more information. + ### `--experimental-json-modules` <!-- YAML added: v12.9.0 @@ -1021,6 +1030,7 @@ Node.js options that are allowed are: * `--enable-fips` * `--enable-source-maps` * `--es-module-specifier-resolution` +* `--experimental-conditional-exports` * `--experimental-json-modules` * `--experimental-loader` * `--experimental-modules` @@ -1324,3 +1334,4 @@ greater than `4` (its current default value). For more information, see the [libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html [remote code execution]: https://www.owasp.org/index.php/Code_Injection [context-aware]: addons.html#addons_context_aware_addons +[Conditional Exports]: esm.html#esm_conditional_exports diff --git a/doc/api/esm.md b/doc/api/esm.md index 5cb44868e8..c1ae58ed6c 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -260,6 +260,9 @@ that would only be supported in ES module-supporting versions of Node.js (and other runtimes). New packages could be published containing only ES module sources, and would be compatible only with ES module-supporting runtimes. +To define separate package entry points for use by `require` and by `import`, +see [Conditional Exports][]. + ### Package Exports 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 `"exports": {}` to indicate the package does not intend for submodules to be exposed. -Exports can also be used to map the main entry point of a package: +Any invalid exports entries will be ignored. This includes exports not +starting with `"./"` or a missing trailing `"/"` for directory exports. + +Array fallback support is provided for exports, similarly to import maps +in order to be forwards-compatible with possible fallback workflows in future: <!-- eslint-skip --> ```js -// ./node_modules/es-module-package/package.json { "exports": { - ".": "./main.js" + "./submodule": ["not:valid", "./submodule.js"] } } ``` -where the "." indicates loading the package without any subpath. Exports will -always override any existing `"main"` value for both CommonJS and -ES module packages. +Since `"not:valid"` is not a supported target, `"./submodule.js"` is used +instead as the fallback, as if it were the only target. + +Defining a `"."` export will define the main entry point for the package, +and will always take precedence over the `"main"` field in the `package.json`. -For packages with only a main entry point, an `"exports"` value of just -a string is also supported: +This allows defining a different entry point for Node.js versions that support +ECMAScript modules and versions that don't, for example: + +<!-- eslint-skip --> +```js +{ + "main": "./main-legacy.cjs", + "exports": { + ".": "./main-modern.cjs" + } +} +``` + +#### Conditional Exports + +Conditional exports provide a way to map to different paths depending on +certain conditions. They are supported for both CommonJS and ES module imports. + +For example, a package that wants to provide different ES module exports for +Node.js and the browser can be written: + +<!-- eslint-skip --> +```js +// ./node_modules/pkg/package.json +{ + "type": "module", + "main": "./index.js", + "exports": { + "./feature": { + "browser": "./feature-browser.js", + "default": "./feature-default.js" + } + } +} +``` + +When resolving the `"."` export, if no matching target is found, the `"main"` +will be used as the final fallback. + +The conditions supported in Node.js are matched in the following order: + +1. `"require"` - matched when the package is loaded via `require()`. + _This is currently only supported behind the + `--experimental-conditional-exports` flag._ +2. `"node"` - matched for any Node.js environment. Can be a CommonJS or ES + module file. _This is currently only supported behind the + `--experimental-conditional-exports` flag._ +3. `"default"` - the generic fallback that will always match if no other + more specific condition is matched first. Can be a CommonJS or ES module + file. + +Using the `"require"` condition it is possible to define a package that will +have a different exported value for CommonJS and ES modules, which can be a +hazard in that it can result in having two separate instances of the same +package in use in an application, which can cause a number of bugs. + +Other conditions such as `"browser"`, `"electron"`, `"deno"`, `"react-native"`, +etc. could be defined in other runtimes or tools. + +#### Exports Sugar + +If the `"."` export is the only export, the `"exports"` field provides sugar +for this case being the direct `"exports"` field value. + +If the `"."` export has a fallback array or string value, then the `"exports"` +field can be set to this value directly. + +<!-- eslint-skip --> +```js +{ + "exports": { + ".": "./main.js" + } +} +``` + +can be written: <!-- eslint-skip --> ```js -// ./node_modules/es-module-package/package.json { "exports": "./main.js" } ``` -Any invalid exports entries will be ignored. This includes exports not -starting with `"./"` or a missing trailing `"/"` for directory exports. +When using conditional exports, the rule is that all keys in the object mapping +must not start with a `"."` otherwise they would be indistinguishable from +exports subpaths. -Array fallback support is provided for exports, similarly to import maps -in order to be forward-compatible with fallback workflows in future: +<!-- eslint-skip --> +```js +{ + "exports": { + ".": { + "require": "./main.cjs", + "default": "./main.js" + } + } +} +``` + +can be written: <!-- eslint-skip --> ```js { "exports": { - "./submodule": ["not:valid", "./submodule.js"] + "require": "./main.cjs", + "default": "./main.js" } } ``` -Since `"not:valid"` is not a supported target, `"./submodule.js"` is used -instead as the fallback, as if it were the only target. +If writing any exports value that mixes up these two forms, an error will be +thrown: + +<!-- eslint-skip --> +```js +{ + // Throws on resolution! + "exports": { + "./feature": "./lib/feature.js", + "require": "./main.cjs", + "default": "./main.js" + } +} +``` ## <code>import</code> Specifiers @@ -806,6 +913,9 @@ of these top-level routines unless stated otherwise. _isMain_ is **true** when resolving the Node.js application entry point. +_defaultEnv_ is the conditional environment name priority array, +`["node", "default"]`. + <details> <summary>Resolver algorithm specification</summary> @@ -905,14 +1015,16 @@ _isMain_ is **true** when resolving the Node.js application entry point. > 1. If _pjson_ is **null**, then > 1. Throw a _Module Not Found_ error. > 1. If _pjson.exports_ is not **null** or **undefined**, then -> 1. If _pjson.exports_ is a String or Array, then +> 1. If _exports_ is an Object with both a key starting with _"."_ and a key +> not starting with _"."_, throw a "Invalid Package Configuration" error. +> 1. If _pjson.exports_ is a String or Array, or an Object containing no +> keys starting with _"."_, then +> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, +> _pjson.exports_, _""_). +> 1. If _pjson.exports_ is an Object containing a _"."_ property, then +> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_. > 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _pjson.exports_, "")_. -> 1. If _pjson.exports is an Object, then -> 1. If _pjson.exports_ contains a _"."_ property, then -> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_. -> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _mainExport_, "")_. +> _mainExport_, _""_). > 1. If _pjson.main_ is a String, then > 1. Let _resolvedMain_ be the URL resolution of _packageURL_, "/", and > _pjson.main_. @@ -926,13 +1038,14 @@ _isMain_ is **true** when resolving the Node.js application entry point. > 1. Return _legacyMainURL_. **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packagePath_, _exports_) - -> 1. If _exports_ is an Object, then +> 1. If _exports_ is an Object with both a key starting with _"."_ and a key not +> starting with _"."_, throw an "Invalid Package Configuration" error. +> 1. If _exports_ is an Object and all keys of _exports_ start with _"."_, then > 1. Set _packagePath_ to _"./"_ concatenated with _packagePath_. > 1. If _packagePath_ is a key of _exports_, then > 1. Let _target_ be the value of _exports\[packagePath\]_. > 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, -> _""_). +> _""_, _defaultEnv_). > 1. Let _directoryKeys_ be the list of keys of _exports_ ending in > _"/"_, sorted by length descending. > 1. For each key _directory_ in _directoryKeys_, do @@ -941,10 +1054,10 @@ _isMain_ is **true** when resolving the Node.js application entry point. > 1. Let _subpath_ be the substring of _target_ starting at the index > of the length of _directory_. > 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, -> _subpath_). +> _subpath_, _defaultEnv_). > 1. Throw a _Module Not Found_ error. -**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_) +**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _env_) > 1. If _target_ is a String, then > 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. > _subpath_ and _resolvedTarget_. > 1. If _resolved_ is contained in _resolvedTarget_, then > 1. Return _resolved_. +> 1. Otherwise, if _target_ is a non-null Object, then +> 1. If _target_ has an object key matching one of the names in _env_, then +> 1. Let _targetValue_ be the corresponding value of the first object key +> of _target_ in _env_. +> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_TARGET_RESOLVE** +> (_packageURL_, _targetValue_, _subpath_, _env_). +> 1. Assert: _resolved_ is a String. +> 1. Return _resolved_. > 1. Otherwise, if _target_ is an Array, then > 1. For each item _targetValue_ in _target_, do -> 1. If _targetValue_ is not a String, continue the loop. +> 1. If _targetValue_ is an Array, continue the loop. > 1. Let _resolved_ be the result of > **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _targetValue_, -> _subpath_), continuing the loop on abrupt completion. +> _subpath_, _env_), continuing the loop on abrupt completion. > 1. Assert: _resolved_ is a String. > 1. Return _resolved_. > 1. Throw a _Module Not Found_ error. @@ -1033,6 +1154,7 @@ success! ``` [CommonJS]: modules.html +[Conditional Exports]: #esm_conditional_exports [ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md [ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md @@ -1045,7 +1167,7 @@ success! [`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import [`module.createRequire()`]: modules.html#modules_module_createrequire_filename [`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports -[dynamic instantiate hook]: #esm_dynamic_instantiate_hook [package exports]: #esm_package_exports +[dynamic instantiate hook]: #esm_dynamic_instantiate_hook [special scheme]: https://url.spec.whatwg.org/#special-scheme [the official standard format]: https://tc39.github.io/ecma262/#sec-modules diff --git a/doc/api/modules.md b/doc/api/modules.md index 8715218b32..d685662921 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -232,12 +232,17 @@ RESOLVE_BARE_SPECIFIER(DIR, X) 2. If X matches this pattern and DIR/name/package.json is a file: a. Parse DIR/name/package.json, and look for "exports" field. b. If "exports" is null or undefined, GOTO 3. - c. Find the longest key in "exports" that the subpath starts with. - d. If no such key can be found, throw "not found". - e. let RESOLVED_URL = + c. If "exports" is an object with some keys starting with "." and some keys + not starting with ".", throw "invalid config". + c. If "exports" is a string, or object with no keys starting with ".", treat + it as having that value as its "." object property. + d. If subpath is "." and "exports" does not have a "." entry, GOTO 3. + e. Find the longest key in "exports" that the subpath starts with. + f. If no such key can be found, throw "not found". + g. let RESOLVED_URL = PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), exports[key], subpath.slice(key.length)), as defined in the esm resolver. - f. return fileURLToPath(RESOLVED_URL) + h. return fileURLToPath(RESOLVED_URL) 3. return DIR/X ``` |