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>
This commit is contained in:
Guy Bedford
2019-10-13 19:27:39 -04:00
parent c73ef32d35
commit 2367474db4
21 changed files with 497 additions and 221 deletions

View File

@@ -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

View File

@@ -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,38 +316,11 @@ 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:
<!-- eslint-skip -->
```js
// ./node_modules/es-module-package/package.json
{
"exports": {
".": "./main.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.
For packages with only a main entry point, an `"exports"` value of just
a string is also supported:
<!-- 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.
Array fallback support is provided for exports, similarly to import maps
in order to be forward-compatible with fallback workflows in future:
in order to be forwards-compatible with possible fallback workflows in future:
<!-- eslint-skip -->
```js
@@ -358,6 +334,137 @@ in order to be forward-compatible with fallback workflows in future:
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`.
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
{
"exports": "./main.js"
}
```
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.
<!-- eslint-skip -->
```js
{
"exports": {
".": {
"require": "./main.cjs",
"default": "./main.js"
}
}
}
```
can be written:
<!-- eslint-skip -->
```js
{
"exports": {
"require": "./main.cjs",
"default": "./main.js"
}
}
```
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
### Terminology
@@ -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, 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_, "")_.
> _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_,
> _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

View File

@@ -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
```

View File

@@ -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.
.

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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") \

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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)}`);
}

View File

@@ -0,0 +1 @@
module.exports = 'main';

View File

@@ -0,0 +1,3 @@
'use strict';
module.exports = 'not-exported';

View File

@@ -0,0 +1,6 @@
{
"exports": {
"default": "./main.js",
"./main": "./main.js"
}
}

1
test/fixtures/node_modules/pkgexports-sugar/main.js generated vendored Normal file
View File

@@ -0,0 +1 @@
module.exports = 'main';

View File

@@ -0,0 +1,3 @@
'use strict';
module.exports = 'not-exported';

View File

@@ -0,0 +1,3 @@
{
"exports": "./main.js"
}

1
test/fixtures/node_modules/pkgexports-sugar2/main.js generated vendored Normal file
View File

@@ -0,0 +1 @@
module.exports = 'main';

View File

@@ -0,0 +1,3 @@
'use strict';
module.exports = 'not-exported';

View File

@@ -0,0 +1,6 @@
{
"exports": {
"require": "./not-exported.js",
"default": "./main.js"
}
}

View File

@@ -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"]
}
}