Skip to content

Commit

Permalink
module: support require()ing synchronous ESM graphs
Browse files Browse the repository at this point in the history
This patch adds `require()` support for synchronous ESM graphs under
the flag --experimental-require-module.

This is based on the the following design aspect of ESM:

- The resolution can be synchronous (up to the host)
- The evaluation of a synchronous graph (without top-level await)
  is also synchronous, and, by the time the module graph is
  instantiated (before evaluation starts), this is is already known.

When --experimental-require-module is enabled, and require() resolves
to an ES module, if the module is fully synchronous (contains no
top-level await), the `require()` call returns the module name space
object. Otherwise, Node.js throws an error without executing the module.
If `--print-required-tla` is passed, Node.js will evaluate the module,
try to locate the top-level awaits, and print their location to help
users fix them.

With this option alone, the module being `require()`'d should be explicitly
marked as an ES module either using the `"type": "module"` field in
`package.json` or the `.mjs` extension. To load implicit ES modules ending
with `.js` using automatic syntax-based module type detection, use
`--experimental-require-module-with-detection`.
  • Loading branch information
joyeecheung committed Mar 9, 2024
1 parent b209b83 commit a1b3cd4
Show file tree
Hide file tree
Showing 37 changed files with 1,030 additions and 146 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ doc/changelogs/CHANGELOG_v1*.md
!doc/changelogs/CHANGELOG_v18.md
!doc/api_assets/*.js
!.eslintrc.js
test/es-module/test-require-module-detect-entry-point.js
test/es-module/test-require-module-detect-entry-point-aou.js
35 changes: 35 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,39 @@ added: v11.8.0

Use the specified file as a security policy.

### `--experimental-require-module`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active Developement
Supports loading a synchronous ES module graph in `require()`. If the module
graph is not synchronous (contains top-level await), Node.js throws an error
without executing the module. If `--print-pending-tla` is passed, Node.js
will evaluate the module, try to locate the top-level awaits, and print
their location to help users fix them.

With this option alone, the module being `require()`'d should be explicitly
marked as an ES module either using the `"type": "module"` field in
`package.json` or the `.mjs` extension. To load implicit ES modules with
automatic syntax-based module type detection, use
`--experimental-require-module-with-detection`.

### `--experimental-require-module-with-detection`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active Developement
In addition to what `--experimental-require-module` supports, when the module
being `require()`'d is not explicitly marked as an ES Module using the
`"type": "module"` field in `package.json` or the `.mjs` extension, it will
try to detect module type based on the syntax of the module.

### `--experimental-sea-config`

<!-- YAML
Expand Down Expand Up @@ -2523,6 +2556,8 @@ Node.js options that are allowed are:
* `--experimental-network-imports`
* `--experimental-permission`
* `--experimental-policy`
* `--experimental-require-module-with-detection`
* `--experimental-require-module`
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`
* `--experimental-top-level-await`
Expand Down
16 changes: 15 additions & 1 deletion doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2487,13 +2487,27 @@ Accessing `Object.prototype.__proto__` has been forbidden using
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
object.

<a id="ERR_REQUIRE_ASYNC_MODULE"></a>

### `ERR_REQUIRE_ASYNC_MODULE`

> Stability: 1 - Experimental
When trying to `require()` a [ES Module][] under `--experimental-require-module`,
the module turns out to be asynchronous. That is, it contains top-level await.
To see where the top-level await is, use
`--print-pending-tla` (this would execute the modules before looking for
the top-level awaits).

<a id="ERR_REQUIRE_ESM"></a>

### `ERR_REQUIRE_ESM`

> Stability: 1 - Experimental
An attempt was made to `require()` an [ES Module][].
An attempt was made to `require()` an [ES Module][]. To enable `require()` for
synchronous module graphs (without top-level await), use
`--experimental-require-module`.

<a id="ERR_SCRIPT_EXECUTION_INTERRUPTED"></a>

Expand Down
7 changes: 7 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ class NodeAggregateError extends AggregateError {
}

const assert = require('internal/assert');
const { getOptionValue } = require('internal/options');

// Lazily loaded
let util;
Expand Down Expand Up @@ -1686,12 +1687,16 @@ E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
E('ERR_REQUIRE_ESM',
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {
hideInternalStackFrames(this);
assert(!getOptionValue('--experimental-require-module'));
let msg = `require() of ES Module ${filename}${parentPath ? ` from ${
parentPath}` : ''} not supported.`;
const hint = '\nOr use --experimental-require-module if the module is synchronous ' +
'(contains no top-level await)';
if (!packageJsonPath) {
if (StringPrototypeEndsWith(filename, '.mjs'))
msg += `\nInstead change the require of ${filename} to a dynamic ` +
'import() which is available in all CommonJS modules.';
msg += hint;
return msg;
}
const path = require('path');
Expand All @@ -1700,6 +1705,7 @@ E('ERR_REQUIRE_ESM',
if (hasEsmSyntax) {
msg += `\nInstead change the require of ${basename} in ${parentPath} to` +
' a dynamic import() which is available in all CommonJS modules.';
msg += hint;
return msg;
}
msg += `\n${basename} is treated as an ES module file as it is a .js ` +
Expand All @@ -1710,6 +1716,7 @@ E('ERR_REQUIRE_ESM',
'modules, or change "type": "module" to "type": "commonjs" in ' +
`${packageJsonPath} to treat all .js files as CommonJS (using .mjs for ` +
'all ES modules instead).\n';
msg += hint;
return msg;
}, Error);
E('ERR_SCRIPT_EXECUTION_INTERRUPTED',
Expand Down
Loading

0 comments on commit a1b3cd4

Please sign in to comment.