Skip to content

Commit cad46af

Browse files
joyeecheungmarco-ippolito
authored andcommitted
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 Backport-PR-URL: #53500 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 30b859f commit cad46af

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

+27
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,18 @@ added: v11.8.0
974974
975975
Use the specified file as a security policy.
976976

977+
### `--experimental-require-module`
978+
979+
<!-- YAML
980+
added: REPLACEME
981+
-->
982+
983+
> Stability: 1.1 - Active Developement
984+
985+
Supports loading a synchronous ES module graph in `require()`.
986+
987+
See [Loading ECMAScript modules using `require()`][].
988+
977989
### `--experimental-sea-config`
978990

979991
<!-- YAML
@@ -1695,6 +1707,18 @@ changes:
16951707

16961708
Identical to `-e` but prints the result.
16971709

1710+
### `--experimental-print-required-tla`
1711+
1712+
<!-- YAML
1713+
added: REPLACEME
1714+
-->
1715+
1716+
This flag is only useful when `--experimental-require-module` is enabled.
1717+
1718+
If the ES module being `require()`'d contains top-level await, this flag
1719+
allows Node.js to evaluate the module, try to locate the
1720+
top-level awaits, and print their location to help users find them.
1721+
16981722
### `--prof`
16991723

17001724
<!-- YAML
@@ -2642,6 +2666,8 @@ one is included in the list below.
26422666
* `--experimental-network-imports`
26432667
* `--experimental-permission`
26442668
* `--experimental-policy`
2669+
* `--experimental-print-required-tla`
2670+
* `--experimental-require-module`
26452671
* `--experimental-shadow-realm`
26462672
* `--experimental-specifier-resolution`
26472673
* `--experimental-top-level-await`
@@ -3111,6 +3137,7 @@ done
31113137
[ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions
31123138
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
31133139
[File System Permissions]: permissions.md#file-system-permissions
3140+
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
31143141
[Module customization hooks]: module.md#customization-hooks
31153142
[Module customization hooks: enabling]: module.md#enabling
31163143
[Modules loaders]: packages.md#modules-loaders

doc/api/errors.md

+16
Original file line numberDiff line numberDiff line change
@@ -2521,6 +2521,19 @@ Accessing `Object.prototype.__proto__` has been forbidden using
25212521
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
25222522
object.
25232523

2524+
<a id="ERR_REQUIRE_ASYNC_MODULE"></a>
2525+
2526+
### `ERR_REQUIRE_ASYNC_MODULE`
2527+
2528+
> Stability: 1 - Experimental
2529+
2530+
When trying to `require()` a [ES Module][] under `--experimental-require-module`,
2531+
the module turns out to be asynchronous. That is, it contains top-level await.
2532+
2533+
To see where the top-level await is, use
2534+
`--experimental-print-required-tla` (this would execute the modules
2535+
before looking for the top-level awaits).
2536+
25242537
<a id="ERR_REQUIRE_ESM"></a>
25252538

25262539
### `ERR_REQUIRE_ESM`
@@ -2529,6 +2542,9 @@ object.
25292542
25302543
An attempt was made to `require()` an [ES Module][].
25312544

2545+
To enable `require()` for synchronous module graphs (without
2546+
top-level `await`), use `--experimental-require-module`.
2547+
25322548
<a id="ERR_SCRIPT_EXECUTION_INTERRUPTED"></a>
25332549

25342550
### `ERR_SCRIPT_EXECUTION_INTERRUPTED`

doc/api/esm.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -449,11 +449,10 @@ compatibility.
449449
450450
### `require`
451451
452-
The CommonJS module `require` always treats the files it references as CommonJS.
452+
The CommonJS module `require` currently only supports loading synchronous ES
453+
modules when `--experimental-require-module` is enabled.
453454
454-
Using `require` to load an ES module is not supported because ES modules have
455-
asynchronous execution. Instead, use [`import()`][] to load an ES module
456-
from a CommonJS module.
455+
See [Loading ECMAScript modules using `require()`][] for details.
457456
458457
### CommonJS Namespaces
459458
@@ -1132,6 +1131,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11321131
[Import Attributes]: #import-attributes
11331132
[Import Attributes proposal]: https://github.com/tc39/proposal-import-attributes
11341133
[JSON modules]: #json-modules
1134+
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
11351135
[Module customization hooks]: module.md#customization-hooks
11361136
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
11371137
[Terminology]: #terminology

doc/api/modules.md

+67-10
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,60 @@ variable. Since the module lookups using `node_modules` folders are all
168168
relative, and based on the real path of the files making the calls to
169169
`require()`, the packages themselves can be anywhere.
170170

171-
## The `.mjs` extension
171+
## Loading ECMAScript modules using `require()`
172172

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

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

183227
<!-- type=misc -->
@@ -207,12 +251,24 @@ require(X) from module at path Y
207251

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

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

@@ -1097,6 +1153,7 @@ This section was moved to
10971153
[GLOBAL_FOLDERS]: #loading-from-the-global-folders
10981154
[`"main"`]: packages.md#main
10991155
[`"type"`]: packages.md#type
1156+
[`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module
11001157
[`ERR_REQUIRE_ESM`]: errors.md#err_require_esm
11011158
[`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import
11021159
[`MODULE_NOT_FOUND`]: errors.md#module_not_found

doc/api/packages.md

+10-9
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)