Skip to content

Commit e4dadea

Browse files
committed
module: implement the "module-sync" exports condition
This patch implements a "module-sync" exports condition for packages to supply a sycnrhonous ES module to the Node.js module loader, no matter it's being required or imported. This is similar to the "module" condition that bundlers have been using to support `require(esm)` in Node.js, and allows dual-package authors to opt into ESM-first only newer versions of Node.js that supports require(esm) while avoiding the dual-package hazard. ```json { "type": "module", "exports": { "node": { // On new version of Node.js, both require() and import get // the ESM version "module-sync": "./index.js", // On older version of Node.js, where "module" and // require(esm) are not supported, use the transpiled CJS version // to avoid dual-package hazard. Library authors can decide // to drop support for older versions of Node.js when they think // it's time. "default": "./dist/index.cjs" }, // On any other environment, use the ESM version. "default": "./index.js" } } ``` We end up implementing a condition with a different name instead of reusing "module", because existing code in the ecosystem using the "module" condition sometimes also expect the module resolution for these ESM files to work in CJS style, which is supported by bundlers, but the native Node.js loader has intentionally made ESM resolution different from CJS resolution (e.g. forbidding `import './noext'` or `import './directory'`), so it would be semver-major to implement a `"module"` condition without implementing the forbidden ESM resolution rules. For now, this just implments a new condition as semver-minor so it can be backported to older LTS. Refs: https://webpack.js.org/guides/package-exports/#target-environment-independent-packages PR-URL: nodejs#54648 Fixes: nodejs#52173 Refs: https://github.com/joyeecheung/test-module-condition Refs: nodejs#52697 Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Jan Krems <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]>
1 parent 88e7838 commit e4dadea

34 files changed

+180
-9
lines changed

doc/api/modules.md

+12-6
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,12 @@ LOAD_PACKAGE_IMPORTS(X, DIR)
342342
1. Find the closest package scope SCOPE to DIR.
343343
2. If no scope was found, return.
344344
3. If the SCOPE/package.json "imports" is null or undefined, return.
345-
4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
346-
["node", "require"]) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
347-
5. RESOLVE_ESM_MATCH(MATCH).
345+
4. If `--experimental-require-module` is enabled
346+
a. let CONDITIONS = ["node", "require", "module-sync"]
347+
b. Else, let CONDITIONS = ["node", "require"]
348+
5. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
349+
CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
350+
6. RESOLVE_ESM_MATCH(MATCH).
348351
349352
LOAD_PACKAGE_EXPORTS(X, DIR)
350353
1. Try to interpret X as a combination of NAME and SUBPATH where the name
@@ -353,9 +356,12 @@ LOAD_PACKAGE_EXPORTS(X, DIR)
353356
return.
354357
3. Parse DIR/NAME/package.json, and look for "exports" field.
355358
4. If "exports" is null or undefined, return.
356-
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
357-
`package.json` "exports", ["node", "require"]) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
358-
6. RESOLVE_ESM_MATCH(MATCH)
359+
5. If `--experimental-require-module` is enabled
360+
a. let CONDITIONS = ["node", "require", "module-sync"]
361+
b. Else, let CONDITIONS = ["node", "require"]
362+
6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
363+
`package.json` "exports", CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
364+
7. RESOLVE_ESM_MATCH(MATCH)
359365
360366
LOAD_PACKAGE_SELF(X, DIR)
361367
1. Find the closest package scope SCOPE to DIR.

doc/api/packages.md

+17-2
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,10 @@ specific to least specific as conditions should be defined:
665665
formats include CommonJS, JSON, native addons, and ES modules
666666
if `--experimental-require-module` is enabled. _Always mutually
667667
exclusive with `"import"`._
668+
* `"module-sync"` - matches no matter the package is loaded via `import`,
669+
`import()` or `require()`. The format is expected to be ES modules that does
670+
not contain top-level await in its module graph - if it does,
671+
`ERR_REQUIRE_ASYNC_MODULE` will be thrown when the module is `require()`-ed.
668672
* `"default"` - the generic fallback that always matches. Can be a CommonJS
669673
or ES module file. _This condition should always come last._
670674

@@ -769,7 +773,7 @@ In node, conditions have very few restrictions, but specifically these include:
769773

770774
### Community Conditions Definitions
771775

772-
Condition strings other than the `"import"`, `"require"`, `"node"`,
776+
Condition strings other than the `"import"`, `"require"`, `"node"`, `"module-sync"`,
773777
`"node-addons"` and `"default"` conditions
774778
[implemented in Node.js core](#conditional-exports) are ignored by default.
775779

@@ -900,6 +904,17 @@ $ node other.js
900904

901905
## Dual CommonJS/ES module packages
902906

907+
<!-- This section should not be in the API documentation:
908+
909+
1. It teaches opinionated practices that some consider dangerous, see
910+
https://github.com/nodejs/node/issues/52174
911+
2. It will soon be obsolete when we unflag --experimental-require-module.
912+
3. It's difficult to understand a multi-file structure via long texts and snippets in
913+
a markdown document.
914+
915+
TODO(?): Move this section to its own repository with example folders.
916+
-->
917+
903918
Prior to the introduction of support for ES modules in Node.js, it was a common
904919
pattern for package authors to include both CommonJS and ES module JavaScript
905920
sources in their package, with `package.json` [`"main"`][] specifying the
@@ -912,7 +927,7 @@ ignores) the top-level `"module"` field.
912927
Node.js can now run ES module entry points, and a package can contain both
913928
CommonJS and ES module entry points (either via separate specifiers such as
914929
`'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional
915-
exports][]). Unlike in the scenario where `"module"` is only used by bundlers,
930+
exports][]). Unlike in the scenario where top-level `"module"` field is only used by bundlers,
916931
or ES module files are transpiled into CommonJS on the fly before evaluation by
917932
Node.js, the files referenced by the ES module entry point are evaluated as ES
918933
modules.

lib/internal/modules/esm/get_format.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function underNodeModules(url) {
9797
let typelessPackageJsonFilesWarnedAbout;
9898
function warnTypelessPackageJsonFile(pjsonPath, url) {
9999
typelessPackageJsonFilesWarnedAbout ??= new SafeSet();
100-
if (!typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
100+
if (!underNodeModules(url) && !typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
101101
const warning = `Module type of ${url} is not specified and it doesn't parse as CommonJS.\n` +
102102
'Reparsing as ES module because module syntax was detected. This incurs a performance overhead.\n' +
103103
`To eliminate this warning, add "type": "module" to ${pjsonPath}.`;

lib/internal/modules/esm/utils.js

+3
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ function initializeDefaultConditions() {
8383
...userConditions,
8484
]);
8585
defaultConditionsSet = new SafeSet(defaultConditions);
86+
if (getOptionValue('--experimental-require-module')) {
87+
defaultConditionsSet.add('module-sync');
88+
}
8689
}
8790

8891
/**

lib/internal/modules/helpers.js

+3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ function initializeCjsConditions() {
7676
...addonConditions,
7777
...userConditions,
7878
]);
79+
if (getOptionValue('--experimental-require-module')) {
80+
cjsConditions.add('module-sync');
81+
}
7982
}
8083

8184
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Flags: --experimental-require-module
2+
3+
import '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import * as staticImport from '../fixtures/es-modules/module-condition/import.mjs';
6+
import { import as _import } from '../fixtures/es-modules/module-condition/dynamic_import.js';
7+
8+
async function dynamicImport(id) {
9+
const result = await _import(id);
10+
return result.resolved;
11+
}
12+
13+
assert.deepStrictEqual({ ...staticImport }, {
14+
import_module_require: 'import',
15+
module_and_import: 'module',
16+
module_and_require: 'module',
17+
module_import_require: 'module',
18+
module_only: 'module',
19+
module_require_import: 'module',
20+
require_module_import: 'module',
21+
});
22+
23+
assert.strictEqual(await dynamicImport('import-module-require'), 'import');
24+
assert.strictEqual(await dynamicImport('module-and-import'), 'module');
25+
assert.strictEqual(await dynamicImport('module-and-require'), 'module');
26+
assert.strictEqual(await dynamicImport('module-import-require'), 'module');
27+
assert.strictEqual(await dynamicImport('module-only'), 'module');
28+
assert.strictEqual(await dynamicImport('module-require-import'), 'module');
29+
assert.strictEqual(await dynamicImport('require-module-import'), 'module');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Flags: --experimental-require-module
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('assert');
6+
7+
const loader = require('../fixtures/es-modules/module-condition/require.cjs');
8+
9+
assert.strictEqual(loader.require('import-module-require').resolved, 'module');
10+
assert.strictEqual(loader.require('module-and-import').resolved, 'module');
11+
assert.strictEqual(loader.require('module-and-require').resolved, 'module');
12+
assert.strictEqual(loader.require('module-import-require').resolved, 'module');
13+
assert.strictEqual(loader.require('module-only').resolved, 'module');
14+
assert.strictEqual(loader.require('module-require-import').resolved, 'module');
15+
assert.strictEqual(loader.require('require-module-import').resolved, 'require');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function load(id) {
2+
return import(id);
3+
}
4+
5+
export { load as import };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { resolved as import_module_require } from 'import-module-require';
2+
export { resolved as module_and_import } from 'module-and-import';
3+
export { resolved as module_and_require } from 'module-and-require';
4+
export { resolved as module_import_require } from 'module-import-require';
5+
export { resolved as module_only } from 'module-only';
6+
export { resolved as module_require_import } from 'module-require-import';
7+
export { resolved as require_module_import } from 'require-module-import';

test/fixtures/es-modules/module-condition/node_modules/import-module-require/import.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/import-module-require/module.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/import-module-require/package.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/import-module-require/require.cjs

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-and-import/import.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-and-import/module.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-and-import/package.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-and-require/module.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-and-require/package.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-and-require/require.cjs

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-import-require/import.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-import-require/module.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-import-require/package.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-import-require/require.cjs

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-only/module.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-only/package.json

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-require-import/import.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-require-import/module.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-require-import/package.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/module-require-import/require.cjs

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/require-module-import/import.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/require-module-import/module.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/require-module-import/package.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/module-condition/node_modules/require-module-import/require.cjs

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports.require = require;

0 commit comments

Comments
 (0)