Skip to content

Commit 5f7fad2

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. In this case it is similar to dynamic `import()` but is run synchronously and returns the name space object directly. ```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 const required = require('./point.mjs'); // [Module: null prototype] { // default: [class Point], // distance: [Function: distance] // } console.log(required); (async () => { const imported = await import('./point.mjs'); console.log(imported === required); // true })(); ``` 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. In this case, users should load the asynchronous module using `import()`. 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. PR-URL: #51977 Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 80f86e5 commit 5f7fad2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1170
-79
lines changed

doc/api/cli.md

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

878878
Use the specified file as a security policy.
879879

880+
### `--experimental-require-module`
881+
882+
<!-- YAML
883+
added: REPLACEME
884+
-->
885+
886+
> Stability: 1.1 - Active Developement
887+
888+
Supports loading a synchronous ES module graph in `require()`.
889+
890+
See [Loading ECMAScript modules using `require()`][].
891+
880892
### `--experimental-sea-config`
881893

882894
<!-- YAML
@@ -1584,6 +1596,18 @@ changes:
15841596

15851597
Identical to `-e` but prints the result.
15861598

1599+
### `--experimental-print-required-tla`
1600+
1601+
<!-- YAML
1602+
added: REPLACEME
1603+
-->
1604+
1605+
This flag is only useful when `--experimental-require-module` is enabled.
1606+
1607+
If the ES module being `require()`'d contains top-level await, this flag
1608+
allows Node.js to evaluate the module, try to locate the
1609+
top-level awaits, and print their location to help users find them.
1610+
15871611
### `--prof`
15881612

15891613
<!-- YAML
@@ -2534,6 +2558,8 @@ Node.js options that are allowed are:
25342558
* `--experimental-network-imports`
25352559
* `--experimental-permission`
25362560
* `--experimental-policy`
2561+
* `--experimental-print-required-tla`
2562+
* `--experimental-require-module`
25372563
* `--experimental-shadow-realm`
25382564
* `--experimental-specifier-resolution`
25392565
* `--experimental-top-level-await`
@@ -3038,6 +3064,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
30383064
[ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions
30393065
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
30403066
[File System Permissions]: permissions.md#file-system-permissions
3067+
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
30413068
[Module customization hooks]: module.md#customization-hooks
30423069
[Module customization hooks: enabling]: module.md#enabling
30433070
[Modules loaders]: packages.md#modules-loaders

doc/api/errors.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2487,6 +2487,19 @@ 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_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).
2502+
24902503
<a id="ERR_REQUIRE_ESM"></a>
24912504

24922505
### `ERR_REQUIRE_ESM`
@@ -2495,6 +2508,9 @@ object.
24952508
24962509
An attempt was made to `require()` an [ES Module][].
24972510

2511+
To enable `require()` for synchronous module graphs (without
2512+
top-level `await`), use `--experimental-require-module`.
2513+
24982514
<a id="ERR_SCRIPT_EXECUTION_INTERRUPTED"></a>
24992515

25002516
### `ERR_SCRIPT_EXECUTION_INTERRUPTED`

doc/api/esm.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,11 +466,10 @@ compatibility.
466466
467467
### `require`
468468
469-
The CommonJS module `require` always treats the files it references as CommonJS.
469+
The CommonJS module `require` currently only supports loading synchronous ES
470+
modules when `--experimental-require-module` is enabled.
470471
471-
Using `require` to load an ES module is not supported because ES modules have
472-
asynchronous execution. Instead, use [`import()`][] to load an ES module
473-
from a CommonJS module.
472+
See [Loading ECMAScript modules using `require()`][] for details.
474473
475474
### CommonJS Namespaces
476475
@@ -1142,6 +1141,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11421141
[Import Attributes]: #import-attributes
11431142
[Import Attributes proposal]: https://github.com/tc39/proposal-import-attributes
11441143
[JSON modules]: #json-modules
1144+
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
11451145
[Module customization hooks]: module.md#customization-hooks
11461146
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
11471147
[Terminology]: #terminology

doc/api/modules.md

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,60 @@ 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+
The `.mjs` extension is reserved for [ECMAScript Modules][].
173+
Currently, if the flag `--experimental-require-module` is not used, loading
174+
an ECMAScript module using `require()` will throw a [`ERR_REQUIRE_ESM`][]
175+
error, and users need to use [`import()`][] instead. See
176+
[Determining module system][] section for more info
178177
regarding which files are parsed as ECMAScript modules.
179178

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

182226
<!-- type=misc -->
@@ -206,12 +250,24 @@ require(X) from module at path Y
206250

207251
LOAD_AS_FILE(X)
208252
1. If X is a file, load X as its file extension format. STOP
209-
2. If X.js is a file, load X.js as JavaScript text. STOP
210-
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
253+
2. If X.js is a file,
254+
a. Find the closest package scope SCOPE to X.
255+
b. If no scope was found, load X.js as a CommonJS module. STOP.
256+
c. If the SCOPE/package.json contains "type" field,
257+
1. If the "type" field is "module", load X.js as an ECMAScript module. STOP.
258+
2. Else, load X.js as an CommonJS module. STOP.
259+
3. If X.json is a file, load X.json to a JavaScript Object. STOP
211260
4. If X.node is a file, load X.node as binary addon. STOP
261+
5. If X.mjs is a file, and `--experimental-require-module` is enabled,
262+
load X.mjs as an ECMAScript module. STOP
212263

213264
LOAD_INDEX(X)
214-
1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
265+
1. If X/index.js is a file
266+
a. Find the closest package scope SCOPE to X.
267+
b. If no scope was found, load X/index.js as a CommonJS module. STOP.
268+
c. If the SCOPE/package.json contains "type" field,
269+
1. If the "type" field is "module", load X/index.js as an ECMAScript module. STOP.
270+
2. Else, load X/index.js as an CommonJS module. STOP.
215271
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
216272
3. If X/index.node is a file, load X/index.node as binary addon. STOP
217273

@@ -1085,6 +1141,7 @@ This section was moved to
10851141
[GLOBAL_FOLDERS]: #loading-from-the-global-folders
10861142
[`"main"`]: packages.md#main
10871143
[`"type"`]: packages.md#type
1144+
[`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module
10881145
[`ERR_REQUIRE_ESM`]: errors.md#err_require_esm
10891146
[`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import
10901147
[`MODULE_NOT_FOUND`]: errors.md#module_not_found

doc/api/packages.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,15 @@ There is the CommonJS module loader:
133133
`process.dlopen()`.
134134
* It treats all files that lack `.json` or `.node` extensions as JavaScript
135135
text files.
136-
* It cannot be used to load ECMAScript modules (although it is possible to
137-
[load ECMASCript modules from CommonJS modules][]). When used to load a
138-
JavaScript text file that is not an ECMAScript module, it loads it as a
139-
CommonJS module.
136+
* It can only be used to [load ECMASCript modules from CommonJS modules][] if
137+
the module graph is synchronous (that contains no top-level `await`) when
138+
`--experimental-require-module` is enabled.
139+
When used to load a JavaScript text file that is not an ECMAScript module,
140+
the file will be loaded as a CommonJS module.
140141

141142
There is the ECMAScript module loader:
142143

143-
* It is asynchronous.
144+
* It is asynchronous, unless it's being used to load modules for `require()`.
144145
* It is responsible for handling `import` statements and `import()` expressions.
145146
* It is not monkey patchable, can be customized using [loader hooks][].
146147
* It does not support folders as modules, directory indexes (e.g.
@@ -623,9 +624,9 @@ specific to least specific as conditions should be defined:
623624
* `"require"` - matches when the package is loaded via `require()`. The
624625
referenced file should be loadable with `require()` although the condition
625626
matches regardless of the module format of the target file. Expected
626-
formats include CommonJS, JSON, and native addons but not ES modules as
627-
`require()` doesn't support them. _Always mutually exclusive with
628-
`"import"`._
627+
formats include CommonJS, JSON, native addons, and ES modules
628+
if `--experimental-require-module` is enabled. _Always mutually
629+
exclusive with `"import"`._
629630
* `"default"` - the generic fallback that always matches. Can be a CommonJS
630631
or ES module file. _This condition should always come last._
631632

@@ -1371,7 +1372,7 @@ This field defines [subpath imports][] for the current package.
13711372
[entry points]: #package-entry-points
13721373
[folders as modules]: modules.md#folders-as-modules
13731374
[import maps]: https://github.com/WICG/import-maps
1374-
[load ECMASCript modules from CommonJS modules]: modules.md#the-mjs-extension
1375+
[load ECMASCript modules from CommonJS modules]: modules.md#loading-ecmascript-modules-using-require
13751376
[loader hooks]: esm.md#loaders
13761377
[packages folder mapping]: https://github.com/WICG/import-maps#packages-via-trailing-slashes
13771378
[self-reference]: #self-referencing-a-package-using-its-name

0 commit comments

Comments
 (0)