summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuy Bedford <guybedford@gmail.com>2019-10-13 19:27:39 -0400
committerGuy Bedford <guybedford@gmail.com>2019-11-08 17:26:26 -0500
commit2367474db46136aecb87b27d82bfa597a95aeedd (patch)
tree58fd8eb4ad143677a791b00f994965d9573e562b
parentc73ef32d355aa58672c15e89534c375dd2246f3c (diff)
downloadandroid-node-v8-2367474db46136aecb87b27d82bfa597a95aeedd.tar.gz
android-node-v8-2367474db46136aecb87b27d82bfa597a95aeedd.tar.bz2
android-node-v8-2367474db46136aecb87b27d82bfa597a95aeedd.zip
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>
-rw-r--r--doc/api/cli.md11
-rw-r--r--doc/api/esm.md184
-rw-r--r--doc/api/modules.md13
-rw-r--r--doc/node.13
-rw-r--r--lib/internal/errors.js2
-rw-r--r--lib/internal/modules/cjs/loader.js103
-rw-r--r--src/env.h3
-rw-r--r--src/module_wrap.cc307
-rw-r--r--src/node_options.cc4
-rw-r--r--src/node_options.h1
-rw-r--r--test/es-module/test-esm-exports.mjs31
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar-fail/main.js1
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar-fail/not-exported.js3
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar-fail/package.json6
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar/main.js1
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar/not-exported.js3
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar/package.json3
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar2/main.js1
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar2/not-exported.js3
-rw-r--r--test/fixtures/node_modules/pkgexports-sugar2/package.json6
-rw-r--r--test/fixtures/node_modules/pkgexports/package.json5
21 files changed, 485 insertions, 209 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
```
diff --git a/doc/node.1 b/doc/node.1
index 30d63b216d..99ff358df9 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -113,6 +113,9 @@ Requires Node.js to be built with
.It Fl -es-module-specifier-resolution
Select extension resolution algorithm for ES Modules; either 'explicit' (default) or 'node'
.
+.It Fl -experimental-conditional-exports
+Enable experimental support for "require" and "node" conditional export targets.
+.
.It Fl -experimental-json-modules
Enable experimental JSON interop support for the ES Module loader.
.
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 2684931a77..6d4a582631 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -981,7 +981,7 @@ E('ERR_INVALID_OPT_VALUE', (name, value) =>
E('ERR_INVALID_OPT_VALUE_ENCODING',
'The value "%s" is invalid for option "encoding"', TypeError);
E('ERR_INVALID_PACKAGE_CONFIG',
- 'Invalid package config in \'%s\' imported from %s', Error);
+ 'Invalid package config for \'%s\', %s', Error);
E('ERR_INVALID_PERFORMANCE_MARK',
'The "%s" performance mark has not been set', Error);
E('ERR_INVALID_PROTOCOL',
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index 8cec27e6c4..243b9867cd 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -59,6 +59,8 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const experimentalModules = getOptionValue('--experimental-modules');
const experimentalSelf = getOptionValue('--experimental-resolve-self');
+const experimentalConditionalExports =
+ getOptionValue('--experimental-conditional-exports');
const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
@@ -67,6 +69,7 @@ const { compileFunction } = internalBinding('contextify');
const {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_OPT_VALUE,
+ ERR_INVALID_PACKAGE_CONFIG,
ERR_REQUIRE_ESM
} = require('internal/errors').codes;
const { validateString } = require('internal/validators');
@@ -441,7 +444,6 @@ function trySelf(paths, exts, isMain, trailingSlash, request) {
if (expansion) {
// Use exports
const fromExports = applyExports(basePath, expansion);
- if (!fromExports) return false;
return resolveBasePath(fromExports, exts, isMain, trailingSlash, request);
} else {
// Use main field
@@ -449,17 +451,51 @@ function trySelf(paths, exts, isMain, trailingSlash, request) {
}
}
+function isConditionalDotExportSugar(exports, basePath) {
+ if (typeof exports === 'string')
+ return true;
+ if (Array.isArray(exports))
+ return true;
+ if (typeof exports !== 'object')
+ return false;
+ let isConditional = false;
+ let firstCheck = true;
+ for (const key of Object.keys(exports)) {
+ const curIsConditional = key[0] !== '.';
+ if (firstCheck) {
+ firstCheck = false;
+ isConditional = curIsConditional;
+ } else if (isConditional !== curIsConditional) {
+ throw new ERR_INVALID_PACKAGE_CONFIG(basePath, '"exports" cannot ' +
+ 'contain some keys starting with \'.\' and some not. The exports ' +
+ 'object must either be an object of package subpath keys or an ' +
+ 'object of main entry condition name keys only.');
+ }
+ }
+ return isConditional;
+}
+
function applyExports(basePath, expansion) {
- const pkgExports = readPackageExports(basePath);
const mappingKey = `.${expansion}`;
- if (typeof pkgExports === 'object' && pkgExports !== null) {
+ let pkgExports = readPackageExports(basePath);
+ if (pkgExports === undefined || pkgExports === null || !experimentalModules)
+ return path.resolve(basePath, mappingKey);
+
+ if (isConditionalDotExportSugar(pkgExports, basePath))
+ pkgExports = { '.': pkgExports };
+
+ if (typeof pkgExports === 'object') {
if (ObjectPrototype.hasOwnProperty(pkgExports, mappingKey)) {
const mapping = pkgExports[mappingKey];
return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '',
basePath, mappingKey);
}
+ // Fallback to CJS main lookup when no main export is defined
+ if (mappingKey === '.')
+ return basePath;
+
let dirMatch = '';
for (const candidateKey of Object.keys(pkgExports)) {
if (candidateKey[candidateKey.length - 1] !== '/') continue;
@@ -476,19 +512,15 @@ function applyExports(basePath, expansion) {
subpath, basePath, mappingKey);
}
}
- if (mappingKey === '.' && typeof pkgExports === 'string') {
- return resolveExportsTarget(pathToFileURL(basePath + '/'), pkgExports,
- '', basePath, mappingKey);
- }
- if (pkgExports != null) {
- // eslint-disable-next-line no-restricted-syntax
- const e = new Error(`Package exports for '${basePath}' do not define ` +
- `a '${mappingKey}' subpath`);
- e.code = 'MODULE_NOT_FOUND';
- throw e;
- }
+ // Fallback to CJS main lookup when no main export is defined
+ if (mappingKey === '.')
+ return basePath;
- return path.resolve(basePath, mappingKey);
+ // eslint-disable-next-line no-restricted-syntax
+ const e = new Error(`Package exports for '${basePath}' do not define ` +
+ `a '${mappingKey}' subpath`);
+ e.code = 'MODULE_NOT_FOUND';
+ throw e;
}
// This only applies to requests of a specific form:
@@ -532,7 +564,7 @@ function resolveExportsTarget(pkgPath, target, subpath, basePath, mappingKey) {
}
} else if (Array.isArray(target)) {
for (const targetValue of target) {
- if (typeof targetValue !== 'string') continue;
+ if (Array.isArray(targetValue)) continue;
try {
return resolveExportsTarget(pkgPath, targetValue, subpath, basePath,
mappingKey);
@@ -540,10 +572,43 @@ function resolveExportsTarget(pkgPath, target, subpath, basePath, mappingKey) {
if (e.code !== 'MODULE_NOT_FOUND') throw e;
}
}
+ } else if (typeof target === 'object' && target !== null) {
+ if (experimentalConditionalExports &&
+ ObjectPrototype.hasOwnProperty(target, 'require')) {
+ try {
+ return resolveExportsTarget(pkgPath, target.require, subpath,
+ basePath, mappingKey);
+ } catch (e) {
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
+ }
+ }
+ if (experimentalConditionalExports &&
+ ObjectPrototype.hasOwnProperty(target, 'node')) {
+ try {
+ return resolveExportsTarget(pkgPath, target.node, subpath,
+ basePath, mappingKey);
+ } catch (e) {
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
+ }
+ }
+ if (ObjectPrototype.hasOwnProperty(target, 'default')) {
+ try {
+ return resolveExportsTarget(pkgPath, target.default, subpath,
+ basePath, mappingKey);
+ } catch (e) {
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
+ }
+ }
+ }
+ let e;
+ if (mappingKey !== '.') {
+ // eslint-disable-next-line no-restricted-syntax
+ e = new Error(`Package exports for '${basePath}' do not define a ` +
+ `valid '${mappingKey}' target${subpath ? ' for ' + subpath : ''}`);
+ } else {
+ // eslint-disable-next-line no-restricted-syntax
+ e = new Error(`No valid exports main found for '${basePath}'`);
}
- // eslint-disable-next-line no-restricted-syntax
- const e = new Error(`Package exports for '${basePath}' do not define a ` +
- `valid '${mappingKey}' target${subpath ? 'for ' + subpath : ''}`);
e.code = 'MODULE_NOT_FOUND';
throw e;
}
diff --git a/src/env.h b/src/env.h
index 83eb227053..5f5188d49d 100644
--- a/src/env.h
+++ b/src/env.h
@@ -200,6 +200,7 @@ constexpr size_t kFsStatsBufferLength =
V(crypto_rsa_pss_string, "rsa-pss") \
V(cwd_string, "cwd") \
V(data_string, "data") \
+ V(default_string, "default") \
V(dest_string, "dest") \
V(destroyed_string, "destroyed") \
V(detached_string, "detached") \
@@ -214,6 +215,7 @@ constexpr size_t kFsStatsBufferLength =
V(dns_srv_string, "SRV") \
V(dns_txt_string, "TXT") \
V(done_string, "done") \
+ V(dot_string, ".") \
V(duration_string, "duration") \
V(emit_warning_string, "emitWarning") \
V(empty_object_string, "{}") \
@@ -278,6 +280,7 @@ constexpr size_t kFsStatsBufferLength =
V(netmask_string, "netmask") \
V(next_string, "next") \
V(nistcurve_string, "nistCurve") \
+ V(node_string, "node") \
V(nsname_string, "nsname") \
V(ocsp_request_string, "OCSPRequest") \
V(oncertcb_string, "oncertcb") \
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index 4c4a1ce863..5745cce9e0 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -835,10 +835,16 @@ void ThrowExportsInvalid(Environment* env,
const std::string& target,
const URL& pjson_url,
const URL& base) {
- const std::string msg = "Cannot resolve package exports target '" + target +
- "' matched for '" + subpath + "' in " + pjson_url.ToFilePath() +
- ", imported from " + base.ToFilePath();
- node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
+ if (subpath.length()) {
+ const std::string msg = "Cannot resolve package exports target '" + target +
+ "' matched for '" + subpath + "' in " + pjson_url.ToFilePath() +
+ ", imported from " + base.ToFilePath();
+ node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
+ } else {
+ const std::string msg = "Cannot resolve package main '" + target + "' in" +
+ pjson_url.ToFilePath() + ", imported from " + base.ToFilePath();
+ node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
+ }
}
void ThrowExportsInvalid(Environment* env,
@@ -857,13 +863,13 @@ void ThrowExportsInvalid(Environment* env,
}
}
-Maybe<URL> ResolveExportsTarget(Environment* env,
- const std::string& target,
- const std::string& subpath,
- const std::string& match,
- const URL& pjson_url,
- const URL& base,
- bool throw_invalid = true) {
+Maybe<URL> ResolveExportsTargetString(Environment* env,
+ const std::string& target,
+ const std::string& subpath,
+ const std::string& match,
+ const URL& pjson_url,
+ const URL& base,
+ bool throw_invalid = true) {
if (target.substr(0, 2) != "./") {
if (throw_invalid) {
ThrowExportsInvalid(env, match, target, pjson_url, base);
@@ -901,68 +907,142 @@ Maybe<URL> ResolveExportsTarget(Environment* env,
return Just(subpath_resolved);
}
+Maybe<URL> ResolveExportsTarget(Environment* env,
+ const URL& pjson_url,
+ Local<Value> target,
+ const std::string& subpath,
+ const std::string& pkg_subpath,
+ const URL& base,
+ bool throw_invalid = true) {
+ Isolate* isolate = env->isolate();
+ Local<Context> context = env->context();
+ if (target->IsString()) {
+ Utf8Value target_utf8(isolate, target.As<v8::String>());
+ std::string target_str(*target_utf8, target_utf8.length());
+ Maybe<URL> resolved = ResolveExportsTargetString(env, target_str, subpath,
+ pkg_subpath, pjson_url, base, throw_invalid);
+ if (resolved.IsNothing()) {
+ return Nothing<URL>();
+ }
+ return FinalizeResolution(env, resolved.FromJust(), base);
+ } else if (target->IsArray()) {
+ Local<Array> target_arr = target.As<Array>();
+ const uint32_t length = target_arr->Length();
+ if (length == 0) {
+ if (throw_invalid) {
+ ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
+ }
+ return Nothing<URL>();
+ }
+ for (uint32_t i = 0; i < length; i++) {
+ auto target_item = target_arr->Get(context, i).ToLocalChecked();
+ if (!target_item->IsArray()) {
+ Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url,
+ target_item, subpath, pkg_subpath, base, false);
+ if (resolved.IsNothing()) continue;
+ return FinalizeResolution(env, resolved.FromJust(), base);
+ }
+ }
+ if (throw_invalid) {
+ auto invalid = target_arr->Get(context, length - 1).ToLocalChecked();
+ Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, invalid,
+ subpath, pkg_subpath, base, true);
+ CHECK(resolved.IsNothing());
+ }
+ return Nothing<URL>();
+ } else if (target->IsObject()) {
+ Local<Object> target_obj = target.As<Object>();
+ bool matched = false;
+ Local<Value> conditionalTarget;
+ if (env->options()->experimental_conditional_exports &&
+ target_obj->HasOwnProperty(context, env->node_string()).FromJust()) {
+ matched = true;
+ conditionalTarget =
+ target_obj->Get(context, env->node_string()).ToLocalChecked();
+ Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url,
+ conditionalTarget, subpath, pkg_subpath, base, false);
+ if (!resolved.IsNothing()) {
+ return resolved;
+ }
+ }
+ if (target_obj->HasOwnProperty(context, env->default_string()).FromJust()) {
+ matched = true;
+ conditionalTarget =
+ target_obj->Get(context, env->default_string()).ToLocalChecked();
+ Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url,
+ conditionalTarget, subpath, pkg_subpath, base, false);
+ if (!resolved.IsNothing()) {
+ return resolved;
+ }
+ }
+ if (matched && throw_invalid) {
+ Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url,
+ conditionalTarget, subpath, pkg_subpath, base, true);
+ CHECK(resolved.IsNothing());
+ return Nothing<URL>();
+ }
+ }
+ if (throw_invalid) {
+ ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
+ }
+ return Nothing<URL>();
+}
+
+Maybe<bool> IsConditionalExportsMainSugar(Environment* env,
+ Local<Value> exports,
+ const URL& pjson_url,
+ const URL& base) {
+ if (exports->IsString() || exports->IsArray()) return Just(true);
+ if (!exports->IsObject()) return Just(false);
+ Local<Context> context = env->context();
+ Local<Object> exports_obj = exports.As<Object>();
+ Local<Array> keys =
+ exports_obj->GetOwnPropertyNames(context).ToLocalChecked();
+ bool isConditionalSugar = false;
+ for (uint32_t i = 0; i < keys->Length(); ++i) {
+ Local<String> key = keys->Get(context, i).ToLocalChecked().As<String>();
+ Utf8Value key_utf8(env->isolate(), key);
+ bool curIsConditionalSugar = key_utf8.length() == 0 || key_utf8[0] != '.';
+ if (i == 0) {
+ isConditionalSugar = curIsConditionalSugar;
+ } else if (isConditionalSugar != curIsConditionalSugar) {
+ const std::string msg = "Cannot resolve package exports in " +
+ pjson_url.ToFilePath() + ", imported from " + base.ToFilePath() + ". " +
+ "\"exports\" cannot contain some keys starting with '.' and some not." +
+ " The exports object must either be an object of package subpath keys" +
+ " or an object of main entry condition name keys only.";
+ node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str());
+ return Nothing<bool>();
+ }
+ }
+ return Just(isConditionalSugar);
+}
+
Maybe<URL> PackageMainResolve(Environment* env,
const URL& pjson_url,
const PackageConfig& pcfg,
const URL& base) {
if (pcfg.exists == Exists::Yes) {
Isolate* isolate = env->isolate();
- Local<Context> context = env->context();
+
if (!pcfg.exports.IsEmpty()) {
Local<Value> exports = pcfg.exports.Get(isolate);
- if (exports->IsString() || exports->IsObject() || exports->IsArray()) {
- Local<Value> target;
- if (!exports->IsObject()) {
- target = exports;
- } else {
- Local<Object> exports_obj = exports.As<Object>();
- Local<String> dot_string = String::NewFromUtf8(env->isolate(), ".",
- v8::NewStringType::kNormal).ToLocalChecked();
- target =
- exports_obj->Get(env->context(), dot_string).ToLocalChecked();
- }
- if (target->IsString()) {
- Utf8Value target_utf8(isolate, target.As<v8::String>());
- std::string target(*target_utf8, target_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, target, "", ".",
- pjson_url, base);
- if (resolved.IsNothing()) {
- ThrowExportsInvalid(env, ".", target, pjson_url, base);
- return Nothing<URL>();
- }
- return FinalizeResolution(env, resolved.FromJust(), base);
- } else if (target->IsArray()) {
- Local<Array> target_arr = target.As<Array>();
- const uint32_t length = target_arr->Length();
- if (length == 0) {
- ThrowExportsInvalid(env, ".", target, pjson_url, base);
- return Nothing<URL>();
- }
- for (uint32_t i = 0; i < length; i++) {
- auto target_item = target_arr->Get(context, i).ToLocalChecked();
- if (target_item->IsString()) {
- Utf8Value target_utf8(isolate, target_item.As<v8::String>());
- std::string target_str(*target_utf8, target_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, target_str, "",
- ".", pjson_url, base, false);
- if (resolved.IsNothing()) continue;
- return FinalizeResolution(env, resolved.FromJust(), base);
- }
- }
- auto invalid = target_arr->Get(context, length - 1).ToLocalChecked();
- if (!invalid->IsString()) {
- ThrowExportsInvalid(env, ".", invalid, pjson_url, base);
- return Nothing<URL>();
- }
- Utf8Value invalid_utf8(isolate, invalid.As<v8::String>());
- std::string invalid_str(*invalid_utf8, invalid_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, invalid_str, "",
- ".", pjson_url, base);
- CHECK(resolved.IsNothing());
- return Nothing<URL>();
- } else {
- ThrowExportsInvalid(env, ".", target, pjson_url, base);
- return Nothing<URL>();
+ Maybe<bool> isConditionalExportsMainSugar =
+ IsConditionalExportsMainSugar(env, exports, pjson_url, base);
+ if (isConditionalExportsMainSugar.IsNothing())
+ return Nothing<URL>();
+ if (isConditionalExportsMainSugar.FromJust()) {
+ return ResolveExportsTarget(env, pjson_url, exports, "", "", base,
+ true);
+ } else if (exports->IsObject()) {
+ Local<Object> exports_obj = exports.As<Object>();
+ if (exports_obj->HasOwnProperty(env->context(), env->dot_string())
+ .FromJust()) {
+ Local<Value> target =
+ exports_obj->Get(env->context(), env->dot_string())
+ .ToLocalChecked();
+ return ResolveExportsTarget(env, pjson_url, target, "", "", base,
+ true);
}
}
}
@@ -1002,7 +1082,11 @@ Maybe<URL> PackageExportsResolve(Environment* env,
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
Local<Value> exports = pcfg.exports.Get(isolate);
- if (!exports->IsObject()) {
+ Maybe<bool> isConditionalExportsMainSugar =
+ IsConditionalExportsMainSugar(env, exports, pjson_url, base);
+ if (isConditionalExportsMainSugar.IsNothing())
+ return Nothing<URL>();
+ if (!exports->IsObject() || isConditionalExportsMainSugar.FromJust()) {
ThrowExportsNotFound(env, pkg_subpath, pjson_url, base);
return Nothing<URL>();
}
@@ -1012,49 +1096,12 @@ Maybe<URL> PackageExportsResolve(Environment* env,
if (exports_obj->HasOwnProperty(context, subpath).FromJust()) {
Local<Value> target = exports_obj->Get(context, subpath).ToLocalChecked();
- if (target->IsString()) {
- Utf8Value target_utf8(isolate, target.As<v8::String>());
- std::string target_str(*target_utf8, target_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, target_str, "",
- pkg_subpath, pjson_url, base);
- if (resolved.IsNothing()) {
- ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
- return Nothing<URL>();
- }
- return FinalizeResolution(env, resolved.FromJust(), base);
- } else if (target->IsArray()) {
- Local<Array> target_arr = target.As<Array>();
- const uint32_t length = target_arr->Length();
- if (length == 0) {
- ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
- return Nothing<URL>();
- }
- for (uint32_t i = 0; i < length; i++) {
- auto target_item = target_arr->Get(context, i).ToLocalChecked();
- if (target_item->IsString()) {
- Utf8Value target_utf8(isolate, target_item.As<v8::String>());
- std::string target(*target_utf8, target_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, target, "",
- pkg_subpath, pjson_url, base, false);
- if (resolved.IsNothing()) continue;
- return FinalizeResolution(env, resolved.FromJust(), base);
- }
- }
- auto invalid = target_arr->Get(context, length - 1).ToLocalChecked();
- if (!invalid->IsString()) {
- ThrowExportsInvalid(env, pkg_subpath, invalid, pjson_url, base);
- return Nothing<URL>();
- }
- Utf8Value invalid_utf8(isolate, invalid.As<v8::String>());
- std::string invalid_str(*invalid_utf8, invalid_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, invalid_str, "",
- pkg_subpath, pjson_url, base);
- CHECK(resolved.IsNothing());
- return Nothing<URL>();
- } else {
- ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
+ Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, target, "",
+ pkg_subpath, base);
+ if (resolved.IsNothing()) {
return Nothing<URL>();
}
+ return FinalizeResolution(env, resolved.FromJust(), base);
}
Local<String> best_match;
@@ -1076,49 +1123,13 @@ Maybe<URL> PackageExportsResolve(Environment* env,
if (best_match_str.length() > 0) {
auto target = exports_obj->Get(context, best_match).ToLocalChecked();
std::string subpath = pkg_subpath.substr(best_match_str.length());
- if (target->IsString()) {
- Utf8Value target_utf8(isolate, target.As<v8::String>());
- std::string target(*target_utf8, target_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, target, subpath,
- pkg_subpath, pjson_url, base);
- if (resolved.IsNothing()) {
- ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
- return Nothing<URL>();
- }
- return FinalizeResolution(env, URL(subpath, resolved.FromJust()), base);
- } else if (target->IsArray()) {
- Local<Array> target_arr = target.As<Array>();
- const uint32_t length = target_arr->Length();
- if (length == 0) {
- ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
- return Nothing<URL>();
- }
- for (uint32_t i = 0; i < length; i++) {
- auto target_item = target_arr->Get(context, i).ToLocalChecked();
- if (target_item->IsString()) {
- Utf8Value target_utf8(isolate, target_item.As<v8::String>());
- std::string target_str(*target_utf8, target_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, target_str, subpath,
- pkg_subpath, pjson_url, base, false);
- if (resolved.IsNothing()) continue;
- return FinalizeResolution(env, resolved.FromJust(), base);
- }
- }
- auto invalid = target_arr->Get(context, length - 1).ToLocalChecked();
- if (!invalid->IsString()) {
- ThrowExportsInvalid(env, pkg_subpath, invalid, pjson_url, base);
- return Nothing<URL>();
- }
- Utf8Value invalid_utf8(isolate, invalid.As<v8::String>());
- std::string invalid_str(*invalid_utf8, invalid_utf8.length());
- Maybe<URL> resolved = ResolveExportsTarget(env, invalid_str, subpath,
- pkg_subpath, pjson_url, base);
- CHECK(resolved.IsNothing());
- return Nothing<URL>();
- } else {
- ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
+
+ Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, target, subpath,
+ pkg_subpath, base);
+ if (resolved.IsNothing()) {
return Nothing<URL>();
}
+ return FinalizeResolution(env, resolved.FromJust(), base);
}
ThrowExportsNotFound(env, pkg_subpath, pjson_url, base);
diff --git a/src/node_options.cc b/src/node_options.cc
index 8d97791f79..92ee79b583 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -331,6 +331,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"experimental ES Module support and caching modules",
&EnvironmentOptions::experimental_modules,
kAllowedInEnvironment);
+ AddOption("--experimental-conditional-exports",
+ "experimental support for conditional exports targets",
+ &EnvironmentOptions::experimental_conditional_exports,
+ kAllowedInEnvironment);
AddOption("--experimental-resolve-self",
"experimental support for require/import of the current package",
&EnvironmentOptions::experimental_resolve_self,
diff --git a/src/node_options.h b/src/node_options.h
index a4af15e3e0..af69d67ce0 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -101,6 +101,7 @@ class EnvironmentOptions : public Options {
public:
bool abort_on_uncaught_exception = false;
bool enable_source_maps = false;
+ bool experimental_conditional_exports = false;
bool experimental_json_modules = false;
bool experimental_modules = false;
bool experimental_resolve_self = false;
diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs
index d8c3399418..2683b5df68 100644
--- a/test/es-module/test-esm-exports.mjs
+++ b/test/es-module/test-esm-exports.mjs
@@ -1,4 +1,4 @@
-// Flags: --experimental-modules --experimental-resolve-self
+// Flags: --experimental-modules --experimental-resolve-self --experimental-conditional-exports
import { mustCall } from '../common/index.mjs';
import { ok, deepStrictEqual, strictEqual } from 'assert';
@@ -23,7 +23,16 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
['pkgexports/fallbackfile', { default: 'asdf' }],
// Dot main
['pkgexports', { default: 'asdf' }],
+ // Conditional split for require
+ ['pkgexports/condition', isRequire ? { default: 'encoded path' } :
+ { default: 'asdf' }],
+ // String exports sugar
+ ['pkgexports-sugar', { default: 'main' }],
+ // Conditional object exports sugar
+ ['pkgexports-sugar2', isRequire ? { default: 'not-exported' } :
+ { default: 'main' }]
]);
+
for (const [validSpecifier, expected] of validSpecifiers) {
if (validSpecifier === null) continue;
@@ -39,6 +48,9 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
// The file exists but isn't exported. The exports is a number which counts
// as a non-null value without any properties, just like `{}`.
['pkgexports-number/hidden.js', './hidden.js'],
+ // Sugar cases still encapsulate
+ ['pkgexports-sugar/not-exported.js', './not-exported.js'],
+ ['pkgexports-sugar2/not-exported.js', './not-exported.js']
]);
const invalidExports = new Map([
@@ -79,7 +91,7 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
assertStartsWith(err.message, (isRequire ? 'Package exports' :
'Cannot resolve'));
assertIncludes(err.message, isRequire ?
- `do not define a valid '${subpath}' subpath` :
+ `do not define a valid '${subpath}' target` :
`matched for '${subpath}'`);
}));
}
@@ -93,11 +105,22 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
'Cannot find module');
}));
- // THe use of %2F escapes in paths fails loading
+ // The use of %2F escapes in paths fails loading
loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => {
strictEqual(err.code, isRequire ? 'ERR_INVALID_FILE_URL_PATH' :
'ERR_MODULE_NOT_FOUND');
}));
+
+ // Sugar conditional exports main mixed failure case
+ loadFixture('pkgexports-sugar-fail').catch(mustCall((err) => {
+ strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
+ assertStartsWith(err.message, (isRequire ? 'Invalid package' :
+ 'Cannot resolve'));
+ assertIncludes(err.message, '"exports" cannot contain some keys starting ' +
+ 'with \'.\' and some not. The exports object must either be an object of ' +
+ 'package subpath keys or an object of main entry condition name keys ' +
+ 'only.');
+ }));
});
const { requireFromInside, importFromInside } = fromInside;
@@ -124,6 +147,6 @@ function assertStartsWith(actual, expected) {
}
function assertIncludes(actual, expected) {
- ok(actual.toString().indexOf(expected),
+ ok(actual.toString().indexOf(expected) !== -1,
`${JSON.stringify(actual)} includes ${JSON.stringify(expected)}`);
}
diff --git a/test/fixtures/node_modules/pkgexports-sugar-fail/main.js b/test/fixtures/node_modules/pkgexports-sugar-fail/main.js
new file mode 100644
index 0000000000..dfdd47b877
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar-fail/main.js
@@ -0,0 +1 @@
+module.exports = 'main';
diff --git a/test/fixtures/node_modules/pkgexports-sugar-fail/not-exported.js b/test/fixtures/node_modules/pkgexports-sugar-fail/not-exported.js
new file mode 100644
index 0000000000..02e146dbe9
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar-fail/not-exported.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = 'not-exported';
diff --git a/test/fixtures/node_modules/pkgexports-sugar-fail/package.json b/test/fixtures/node_modules/pkgexports-sugar-fail/package.json
new file mode 100644
index 0000000000..0fb05a427a
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar-fail/package.json
@@ -0,0 +1,6 @@
+{
+ "exports": {
+ "default": "./main.js",
+ "./main": "./main.js"
+ }
+}
diff --git a/test/fixtures/node_modules/pkgexports-sugar/main.js b/test/fixtures/node_modules/pkgexports-sugar/main.js
new file mode 100644
index 0000000000..dfdd47b877
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar/main.js
@@ -0,0 +1 @@
+module.exports = 'main';
diff --git a/test/fixtures/node_modules/pkgexports-sugar/not-exported.js b/test/fixtures/node_modules/pkgexports-sugar/not-exported.js
new file mode 100644
index 0000000000..02e146dbe9
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar/not-exported.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = 'not-exported';
diff --git a/test/fixtures/node_modules/pkgexports-sugar/package.json b/test/fixtures/node_modules/pkgexports-sugar/package.json
new file mode 100644
index 0000000000..5ebad0b4bd
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar/package.json
@@ -0,0 +1,3 @@
+{
+ "exports": "./main.js"
+}
diff --git a/test/fixtures/node_modules/pkgexports-sugar2/main.js b/test/fixtures/node_modules/pkgexports-sugar2/main.js
new file mode 100644
index 0000000000..dfdd47b877
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar2/main.js
@@ -0,0 +1 @@
+module.exports = 'main';
diff --git a/test/fixtures/node_modules/pkgexports-sugar2/not-exported.js b/test/fixtures/node_modules/pkgexports-sugar2/not-exported.js
new file mode 100644
index 0000000000..02e146dbe9
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar2/not-exported.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = 'not-exported';
diff --git a/test/fixtures/node_modules/pkgexports-sugar2/package.json b/test/fixtures/node_modules/pkgexports-sugar2/package.json
new file mode 100644
index 0000000000..139b06665d
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports-sugar2/package.json
@@ -0,0 +1,6 @@
+{
+ "exports": {
+ "require": "./not-exported.js",
+ "default": "./main.js"
+ }
+}
diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json
index 38e2fc1a5c..37c28cdc1a 100644
--- a/test/fixtures/node_modules/pkgexports/package.json
+++ b/test/fixtures/node_modules/pkgexports/package.json
@@ -1,7 +1,7 @@
{
"name": "@pkgexports/name",
+ "main": "./asdf.js",
"exports": {
- ".": "./asdf.js",
"./hole": "./lib/hole.js",
"./space": "./sp%20ce.js",
"./valid-cjs": "./asdf.js",
@@ -18,6 +18,7 @@
"./fallbackfile": [[], null, {}, "builtin:x", "./asdf.js"],
"./nofallback1": [],
"./nofallback2": [null, {}, "builtin:x"],
- "./nodemodules": "./node_modules/internalpkg/x.js"
+ "./nodemodules": "./node_modules/internalpkg/x.js",
+ "./condition": [{ "require": "./sp ce.js" }, "./asdf.js"]
}
}