Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: implement the "module-sync" exports condition #54648

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,12 @@ LOAD_PACKAGE_IMPORTS(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "imports" is null or undefined, return.
4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
["node", "require"]) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
5. RESOLVE_ESM_MATCH(MATCH).
4. If `--experimental-require-module` is enabled
a. let CONDITIONS = ["node", "require", "module-sync"]
b. Else, let CONDITIONS = ["node", "require"]
5. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
6. RESOLVE_ESM_MATCH(MATCH).

LOAD_PACKAGE_EXPORTS(X, DIR)
1. Try to interpret X as a combination of NAME and SUBPATH where the name
Expand All @@ -346,9 +349,12 @@ LOAD_PACKAGE_EXPORTS(X, DIR)
return.
3. Parse DIR/NAME/package.json, and look for "exports" field.
4. If "exports" is null or undefined, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
`package.json` "exports", ["node", "require"]) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
6. RESOLVE_ESM_MATCH(MATCH)
5. If `--experimental-require-module` is enabled
a. let CONDITIONS = ["node", "require", "module-sync"]
b. Else, let CONDITIONS = ["node", "require"]
6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
`package.json` "exports", CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
7. RESOLVE_ESM_MATCH(MATCH)

LOAD_PACKAGE_SELF(X, DIR)
1. Find the closest package scope SCOPE to DIR.
Expand Down
19 changes: 17 additions & 2 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,10 @@ specific to least specific as conditions should be defined:
formats include CommonJS, JSON, native addons, and ES modules
if `--experimental-require-module` is enabled. _Always mutually
exclusive with `"import"`._
* `"module-sync"` - matches no matter the package is loaded via `import`,
`import()` or `require()`. The format is expected to be ES modules that does
not contain top-level await in its module graph - if it does,
`ERR_REQUIRE_ASYNC_MODULE` will be thrown when the module is `require()`-ed.
* `"default"` - the generic fallback that always matches. Can be a CommonJS
or ES module file. _This condition should always come last._

Expand Down Expand Up @@ -755,7 +759,7 @@ Any number of custom conditions can be set with repeat flags.

### Community Conditions Definitions

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

Expand Down Expand Up @@ -886,6 +890,17 @@ $ node other.js

## Dual CommonJS/ES module packages

<!-- This section should not be in the API documentation:

1. It teaches opinionated practices that some consider dangerous, see
https://github.com/nodejs/node/issues/52174
2. It will soon be obsolete when we unflag --experimental-require-module.
3. It's difficult to understand a multi-file structure via long texts and snippets in
a markdown document.

TODO(?): Move this section to its own repository with example folders.
-->

Prior to the introduction of support for ES modules in Node.js, it was a common
pattern for package authors to include both CommonJS and ES module JavaScript
sources in their package, with `package.json` [`"main"`][] specifying the
Expand All @@ -898,7 +913,7 @@ ignores) the top-level `"module"` field.
Node.js can now run ES module entry points, and a package can contain both
CommonJS and ES module entry points (either via separate specifiers such as
`'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional
exports][]). Unlike in the scenario where `"module"` is only used by bundlers,
exports][]). Unlike in the scenario where top-level `"module"` field is only used by bundlers,
or ES module files are transpiled into CommonJS on the fly before evaluation by
Node.js, the files referenced by the ES module entry point are evaluated as ES
modules.
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function underNodeModules(url) {
let typelessPackageJsonFilesWarnedAbout;
function warnTypelessPackageJsonFile(pjsonPath, url) {
typelessPackageJsonFilesWarnedAbout ??= new SafeSet();
if (!typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
if (!underNodeModules(url) && !typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
const warning = `Module type of ${url} is not specified and it doesn't parse as CommonJS.\n` +
'Reparsing as ES module because module syntax was detected. This incurs a performance overhead.\n' +
`To eliminate this warning, add "type": "module" to ${pjsonPath}.`;
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ function initializeDefaultConditions() {
...userConditions,
]);
defaultConditionsSet = new SafeSet(defaultConditions);
if (getOptionValue('--experimental-require-module')) {
defaultConditionsSet.add('module-sync');
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ function initializeCjsConditions() {
...addonConditions,
...userConditions,
]);
if (getOptionValue('--experimental-require-module')) {
cjsConditions.add('module-sync');
}
}

/**
Expand Down
29 changes: 29 additions & 0 deletions test/es-module/test-import-module-conditional-exports-module.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Flags: --experimental-require-module

import '../common/index.mjs';
import assert from 'node:assert';
import * as staticImport from '../fixtures/es-modules/module-condition/import.mjs';
import { import as _import } from '../fixtures/es-modules/module-condition/dynamic_import.js';

async function dynamicImport(id) {
const result = await _import(id);
return result.resolved;
}

assert.deepStrictEqual({ ...staticImport }, {
import_module_require: 'import',
module_and_import: 'module',
module_and_require: 'module',
module_import_require: 'module',
module_only: 'module',
module_require_import: 'module',
require_module_import: 'module',
});

assert.strictEqual(await dynamicImport('import-module-require'), 'import');
assert.strictEqual(await dynamicImport('module-and-import'), 'module');
assert.strictEqual(await dynamicImport('module-and-require'), 'module');
assert.strictEqual(await dynamicImport('module-import-require'), 'module');
assert.strictEqual(await dynamicImport('module-only'), 'module');
assert.strictEqual(await dynamicImport('module-require-import'), 'module');
assert.strictEqual(await dynamicImport('require-module-import'), 'module');
15 changes: 15 additions & 0 deletions test/es-module/test-require-module-conditional-exports-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Flags: --experimental-require-module
'use strict';

require('../common');
const assert = require('assert');

const loader = require('../fixtures/es-modules/module-condition/require.cjs');

assert.strictEqual(loader.require('import-module-require').resolved, 'module');
assert.strictEqual(loader.require('module-and-import').resolved, 'module');
assert.strictEqual(loader.require('module-and-require').resolved, 'module');
assert.strictEqual(loader.require('module-import-require').resolved, 'module');
assert.strictEqual(loader.require('module-only').resolved, 'module');
assert.strictEqual(loader.require('module-require-import').resolved, 'module');
assert.strictEqual(loader.require('require-module-import').resolved, 'require');
5 changes: 5 additions & 0 deletions test/fixtures/es-modules/module-condition/dynamic_import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function load(id) {
return import(id);
}

export { load as import };
7 changes: 7 additions & 0 deletions test/fixtures/es-modules/module-condition/import.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { resolved as import_module_require } from 'import-module-require';
export { resolved as module_and_import } from 'module-and-import';
export { resolved as module_and_require } from 'module-and-require';
export { resolved as module_import_require } from 'module-import-require';
export { resolved as module_only } from 'module-only';
export { resolved as module_require_import } from 'module-require-import';
export { resolved as require_module_import } from 'require-module-import';

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

1 change: 1 addition & 0 deletions test/fixtures/es-modules/module-condition/require.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.require = require;
Loading