Skip to content

Commit 80678e1

Browse files
committed
module: support require()ing synchronous ESM graphs
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. If `--experimental-require-module` is enabled, and the ECMAScript module being loaded by `require()` meets the following requirements: - Explicitly marked as an ES module with a `"type": "module"` field in the closest package.json or a `.mjs` extension. - Fully synchronous (contains no top-level `await`). `require()` will load the requested module as an ES Module, and return the module name space object. ```mjs // point.mjs export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; } class Point { constructor(x, y) { this.x = x; this.y = y; } } export default Point; ` ``` ```cjs // [Module: null prototype] { // default: [class Point], // distance: [Function: distance] // } console.log(require('./point.mjs')); ``` If the module being `require()`'d contains top-level `await`, or the module graph it `import`s contains top-level `await`, `ERR_REQUIRE_ASYNC_MODULE` will be thrown. If `--experimental-print-required-tla` is enabled, instead of throwing `ERR_REQUIRE_ASYNC_MODULE` before evaluation, Node.js will evaluate the module, try to locate the top-level awaits, and print their location to help users fix them.
1 parent 604f6f8 commit 80678e1

35 files changed

+1011
-160
lines changed

doc/api/cli.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,18 @@ added: v11.8.0
871871

872872
Use the specified file as a security policy.
873873

874+
### `--experimental-require-module`
875+
876+
<!-- YAML
877+
added: REPLACEME
878+
-->
879+
880+
> Stability: 1.1 - Active Developement
881+
882+
Supports loading a synchronous ES module graph in `require()`.
883+
884+
See [Loading ECMAScript modules using `require()`][].
885+
874886
### `--experimental-sea-config`
875887

876888
<!-- YAML
@@ -1578,6 +1590,18 @@ changes:
15781590

15791591
Identical to `-e` but prints the result.
15801592

1593+
### `--experimental-print-required-tla`
1594+
1595+
<!-- YAML
1596+
added: REPLACEME
1597+
-->
1598+
1599+
This flag is only useful when `--experimental-require-module` is enabled.
1600+
1601+
If the ES module being `require()`'d contains top-level await, this flag
1602+
allows Node.js to evaluate the module, try to locate the
1603+
top-level awaits, and print their location to help users fix them.
1604+
15811605
### `--prof`
15821606

15831607
<!-- YAML
@@ -2512,6 +2536,8 @@ Node.js options that are allowed are:
25122536
* `--experimental-network-imports`
25132537
* `--experimental-permission`
25142538
* `--experimental-policy`
2539+
* `--experimental-print-required-tla`
2540+
* `--experimental-require-module`
25152541
* `--experimental-shadow-realm`
25162542
* `--experimental-specifier-resolution`
25172543
* `--experimental-top-level-await`
@@ -3016,6 +3042,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
30163042
[ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions
30173043
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
30183044
[File System Permissions]: permissions.md#file-system-permissions
3045+
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
30193046
[Module customization hooks]: module.md#customization-hooks
30203047
[Module customization hooks: enabling]: module.md#enabling
30213048
[Modules loaders]: packages.md#modules-loaders

doc/api/errors.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2487,14 +2487,28 @@ Accessing `Object.prototype.__proto__` has been forbidden using
24872487
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
24882488
object.
24892489

2490-
<a id="ERR_REQUIRE_ESM"></a>
2490+
<a id="ERR_REQUIRE_ASYNC_MODULE"></a>
2491+
2492+
### `ERR_REQUIRE_ASYNC_MODULE`
2493+
2494+
> Stability: 1 - Experimental
2495+
2496+
When trying to `require()` a [ES Module][] under `--experimental-require-module`,
2497+
the module turns out to be asynchronous. That is, it contains top-level await.
2498+
2499+
To see where the top-level await is, use
2500+
`--experimental-print-required-tla` (this would execute the modules
2501+
before looking for the top-level awaits). <a id="ERR_REQUIRE_ESM"></a>
24912502

24922503
### `ERR_REQUIRE_ESM`
24932504

24942505
> Stability: 1 - Experimental
24952506
24962507
An attempt was made to `require()` an [ES Module][].
24972508

2509+
To enable `require()` for synchronous module graphs (without
2510+
top-level `await`), use `--experimental-require-module`.
2511+
24982512
<a id="ERR_SCRIPT_EXECUTION_INTERRUPTED"></a>
24992513

25002514
### `ERR_SCRIPT_EXECUTION_INTERRUPTED`

doc/api/modules.md

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,50 @@ variable. Since the module lookups using `node_modules` folders are all
167167
relative, and based on the real path of the files making the calls to
168168
`require()`, the packages themselves can be anywhere.
169169

170-
## The `.mjs` extension
170+
## Loading ECMAScript modules using `require()`
171171

172-
Due to the synchronous nature of `require()`, it is not possible to use it to
173-
load ECMAScript module files. Attempting to do so will throw a
174-
[`ERR_REQUIRE_ESM`][] error. Use [`import()`][] instead.
175-
176-
The `.mjs` extension is reserved for [ECMAScript Modules][] which cannot be
177-
loaded via `require()`. See [Determining module system][] section for more info
172+
Currently, if the flag `--experimental-require-module` is not used, loading
173+
an ECMAScript module using `require()` will throw a [`ERR_REQUIRE_ESM`][]
174+
error, and users need to use [`import()`][] instead. See
175+
[Determining module system][] section for more info
178176
regarding which files are parsed as ECMAScript modules.
179177

178+
If `--experimental-require-module` is enabled, and the ECMAScript module being
179+
loaded by `require()` meets the following requirements:
180+
181+
* Explicitly marked as an ES module with a `"type": "module"` field in
182+
the closest package.json or a `.mjs` extension.
183+
* Fully synchronous (contains no top-level `await`).
184+
185+
`require()` will load the requested module as an ES Module, and return
186+
the module name space object.
187+
188+
```mjs
189+
// point.mjs
190+
export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; }
191+
class Point {
192+
constructor(x, y) { this.x = x; this.y = y; }
193+
}
194+
export default Point;
195+
```
196+
197+
```cjs
198+
// [Module: null prototype] {
199+
// default: [class Point],
200+
// distance: [Function: distance]
201+
// }
202+
console.log(require('./point.mjs'));
203+
```
204+
205+
If the module being `require()`'d contains top-level `await`, or the module
206+
graph it `import`s contains top-level `await`,
207+
[`ERR_REQUIRE_ASYNC_MODULE`][] will be thrown.
208+
209+
If `--experimental-print-required-tla` is enabled, instead of throwing
210+
`ERR_REQUIRE_ASYNC_MODULE` before evaluation, Node.js will evaluate the
211+
module, try to locate the top-level awaits, and print their location to
212+
help users fix them.
213+
180214
## All together
181215

182216
<!-- type=misc -->
@@ -187,6 +221,8 @@ the `require.resolve()` function.
187221
Putting together all of the above, here is the high-level algorithm
188222
in pseudocode of what `require()` does:
189223

224+
<!-- TODO(joyeecheung): update this for --experimental-require-module-->
225+
190226
<pre>
191227
require(X) from module at path Y
192228
1. If X is a core module,
@@ -1085,6 +1121,7 @@ This section was moved to
10851121
[GLOBAL_FOLDERS]: #loading-from-the-global-folders
10861122
[`"main"`]: packages.md#main
10871123
[`"type"`]: packages.md#type
1124+
[`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module
10881125
[`ERR_REQUIRE_ESM`]: errors.md#err_require_esm
10891126
[`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import
10901127
[`MODULE_NOT_FOUND`]: errors.md#module_not_found

doc/api/packages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1371,7 +1371,7 @@ This field defines [subpath imports][] for the current package.
13711371
[entry points]: #package-entry-points
13721372
[folders as modules]: modules.md#folders-as-modules
13731373
[import maps]: https://github.com/WICG/import-maps
1374-
[load ECMASCript modules from CommonJS modules]: modules.md#the-mjs-extension
1374+
[load ECMASCript modules from CommonJS modules]: modules.md#loading-ecmascript-modules-using-require
13751375
[loader hooks]: esm.md#loaders
13761376
[packages folder mapping]: https://github.com/WICG/import-maps#packages-via-trailing-slashes
13771377
[self-reference]: #self-referencing-a-package-using-its-name

lib/internal/errors.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ class NodeAggregateError extends AggregateError {
210210
}
211211

212212
const assert = require('internal/assert');
213+
const { getOptionValue } = require('internal/options');
213214

214215
// Lazily loaded
215216
let util;
@@ -1686,12 +1687,16 @@ E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
16861687
E('ERR_REQUIRE_ESM',
16871688
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {
16881689
hideInternalStackFrames(this);
1690+
assert(!getOptionValue('--experimental-require-module'));
16891691
let msg = `require() of ES Module ${filename}${parentPath ? ` from ${
16901692
parentPath}` : ''} not supported.`;
1693+
const hint = '\nOr use --experimental-require-module if the module is synchronous ' +
1694+
'(contains no top-level await)';
16911695
if (!packageJsonPath) {
16921696
if (StringPrototypeEndsWith(filename, '.mjs'))
16931697
msg += `\nInstead change the require of ${filename} to a dynamic ` +
16941698
'import() which is available in all CommonJS modules.';
1699+
msg += hint;
16951700
return msg;
16961701
}
16971702
const path = require('path');
@@ -1700,6 +1705,7 @@ E('ERR_REQUIRE_ESM',
17001705
if (hasEsmSyntax) {
17011706
msg += `\nInstead change the require of ${basename} in ${parentPath} to` +
17021707
' a dynamic import() which is available in all CommonJS modules.';
1708+
msg += hint;
17031709
return msg;
17041710
}
17051711
msg += `\n${basename} is treated as an ES module file as it is a .js ` +
@@ -1710,6 +1716,7 @@ E('ERR_REQUIRE_ESM',
17101716
'modules, or change "type": "module" to "type": "commonjs" in ' +
17111717
`${packageJsonPath} to treat all .js files as CommonJS (using .mjs for ` +
17121718
'all ES modules instead).\n';
1719+
msg += hint;
17131720
return msg;
17141721
}, Error);
17151722
E('ERR_SCRIPT_EXECUTION_INTERRUPTED',

0 commit comments

Comments
 (0)