Skip to content

Commit

Permalink
module: conditional exports import condition
Browse files Browse the repository at this point in the history
PR-URL: #30799
Reviewed-By: Jan Krems <jan.krems@gmail.com>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
  • Loading branch information
guybedford committed Dec 12, 2019
1 parent edf654d commit 357a992
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 30 deletions.
49 changes: 28 additions & 21 deletions doc/api/esm.md
Expand Up @@ -329,8 +329,8 @@ Node.js and the browser can be written:
"main": "./index.js",
"exports": {
"./feature": {
"browser": "./feature-browser.js",
"default": "./feature-default.js"
"import": "./feature-default.js",
"browser": "./feature-browser.js"
}
}
}
Expand All @@ -341,16 +341,24 @@ 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
1. `"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
`--experimental-conditional-exports` flag._
2. `"require"` - matched when the package is loaded via `require()`.
_This is currently only supported behind the
`--experimental-conditional-exports` flag._
3. `"import"` - matched when the package is loaded via `import` or
`import()`. Can be any module format, this field does not set the type
interpretation. _This is currently only supported behind the
`--experimental-conditional-exports` flag._
4. `"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.

> Setting any of the above flagged conditions for a published package is not
> recommended until they are unflagged to avoid breaking changes to packages in
> future.
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
Expand Down Expand Up @@ -394,8 +402,8 @@ from exports subpaths.
{
"exports": {
".": {
"require": "./main.cjs",
"default": "./main.js"
"import": "./main.js",
"require": "./main.cjs"
}
}
}
Expand All @@ -407,8 +415,8 @@ can be written:
```js
{
"exports": {
"require": "./main.cjs",
"default": "./main.js"
"import": "./main.js",
"require": "./main.cjs"
}
}
```
Expand All @@ -422,8 +430,8 @@ thrown:
// Throws on resolution!
"exports": {
"./feature": "./lib/feature.js",
"require": "./main.cjs",
"default": "./main.js"
"import": "./main.js",
"require": "./main.cjs"
}
}
```
Expand Down Expand Up @@ -508,9 +516,8 @@ ES module wrapper is used for `import` and the CommonJS entry point for
`require`.

> Note: While `--experimental-conditional-exports` is flagged, a package
> using this pattern will throw when loaded via `require()` in modern
> Node.js, unless package consumers use the `--experimental-conditional-exports`
> flag.
> using this pattern will throw when loaded unless package consumers use the
> `--experimental-conditional-exports` flag.
<!-- eslint-skip -->
```js
Expand All @@ -520,7 +527,7 @@ ES module wrapper is used for `import` and the CommonJS entry point for
"main": "./index.cjs",
"exports": {
"require": "./index.cjs",
"default": "./wrapper.mjs"
"import": "./wrapper.mjs"
}
}
```
Expand Down Expand Up @@ -605,8 +612,8 @@ CommonJS and ES module entry points directly (requires
"type": "module",
"main": "./index.cjs",
"exports": {
"require": "./index.cjs",
"default": "./index.mjs"
"import": "./index.mjs",
"require": "./index.cjs"
}
}
```
Expand Down Expand Up @@ -1149,7 +1156,7 @@ 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"]`.
`["node", "import"]`.
<details>
<summary>Resolver algorithm specification</summary>
Expand Down
3 changes: 2 additions & 1 deletion doc/api/modules.md
Expand Up @@ -241,7 +241,8 @@ RESOLVE_BARE_SPECIFIER(DIR, X)
g. If no such key can be found, throw "not found".
h. let RESOLVED_URL =
PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), exports[key],
subpath.slice(key.length)), as defined in the ESM resolver.
subpath.slice(key.length), ["node", "require"]), as defined in the ESM
resolver.
i. return fileURLToPath(RESOLVED_URL)
3. return DIR/X
```
Expand Down
8 changes: 4 additions & 4 deletions lib/internal/modules/cjs/loader.js
Expand Up @@ -585,9 +585,9 @@ function resolveExportsTarget(pkgPath, target, subpath, basePath, mappingKey) {
}
} else if (typeof target === 'object' && target !== null) {
if (experimentalConditionalExports &&
ObjectPrototypeHasOwnProperty(target, 'require')) {
ObjectPrototypeHasOwnProperty(target, 'node')) {
try {
const result = resolveExportsTarget(pkgPath, target.require, subpath,
const result = resolveExportsTarget(pkgPath, target.node, subpath,
basePath, mappingKey);
emitExperimentalWarning('Conditional exports');
return result;
Expand All @@ -596,9 +596,9 @@ function resolveExportsTarget(pkgPath, target, subpath, basePath, mappingKey) {
}
}
if (experimentalConditionalExports &&
ObjectPrototypeHasOwnProperty(target, 'node')) {
ObjectPrototypeHasOwnProperty(target, 'require')) {
try {
const result = resolveExportsTarget(pkgPath, target.node, subpath,
const result = resolveExportsTarget(pkgPath, target.require, subpath,
basePath, mappingKey);
emitExperimentalWarning('Conditional exports');
return result;
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Expand Up @@ -255,6 +255,7 @@ constexpr size_t kFsStatsBufferLength =
V(hostmaster_string, "hostmaster") \
V(http_1_1_string, "http/1.1") \
V(ignore_string, "ignore") \
V(import_string, "import") \
V(infoaccess_string, "infoAccess") \
V(inherit_string, "inherit") \
V(input_string, "input") \
Expand Down
11 changes: 11 additions & 0 deletions src/module_wrap.cc
Expand Up @@ -967,6 +967,17 @@ Maybe<URL> ResolveExportsTarget(Environment* env,
return resolved;
}
}
if (env->options()->experimental_conditional_exports &&
target_obj->HasOwnProperty(context, env->import_string()).FromJust()) {
matched = true;
conditionalTarget =
target_obj->Get(context, env->import_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 =
Expand Down
13 changes: 11 additions & 2 deletions test/es-module/test-esm-exports.mjs
Expand Up @@ -31,7 +31,7 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
['pkgexports-sugar', { default: 'main' }],
// Conditional object exports sugar
['pkgexports-sugar2', isRequire ? { default: 'not-exported' } :
{ default: 'main' }]
{ default: 'main' }],
]);

for (const [validSpecifier, expected] of validSpecifiers) {
Expand All @@ -51,7 +51,7 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
['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']
['pkgexports-sugar2/not-exported.js', './not-exported.js'],
]);

const invalidExports = new Map([
Expand Down Expand Up @@ -97,6 +97,15 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
}));
}

// Conditional export, even with no match, should still be used instead
// of falling back to main
if (isRequire) {
loadFixture('pkgexports-main').catch(mustCall((err) => {
strictEqual(err.code, 'MODULE_NOT_FOUND');
assertStartsWith(err.message, 'No valid export');
}));
}

// Covering out bases - not a file is still not a file after dir mapping.
loadFixture('pkgexports/sub/not-a-file.js').catch(mustCall((err) => {
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports-main/main.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports-main/module.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions test/fixtures/node_modules/pkgexports-main/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/fixtures/node_modules/pkgexports-sugar2/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 357a992

Please sign in to comment.