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: https://github.com/nodejs/node/pull/51977
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
This commit is contained in:
Joyee Cheung committed 2024-03-18 18:10:06 +08:00
1 parent 80f86e5d02
commit 5f7fad2605
63 files changed
+1170 -79

No files matched your search

+27
View File
@@ -877,6 +877,18 @@ 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()`.
See [Loading ECMAScript modules using `require()`][].
### `--experimental-sea-config`
<!-- YAML
@@ -1584,6 +1596,18 @@ changes:
Identical to `-e` but prints the result.
### `--experimental-print-required-tla`
<!-- YAML
added: REPLACEME
-->
This flag is only useful when `--experimental-require-module` is enabled.
If the ES module being `require()`'d contains top-level await, this flag
allows Node.js to evaluate the module, try to locate the
top-level awaits, and print their location to help users find them.
### `--prof`
<!-- YAML
@@ -2534,6 +2558,8 @@ Node.js options that are allowed are:
* `--experimental-network-imports`
* `--experimental-permission`
* `--experimental-policy`
* `--experimental-print-required-tla`
* `--experimental-require-module`
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`
* `--experimental-top-level-await`
@@ -3038,6 +3064,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
[File System Permissions]: permissions.md#file-system-permissions
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
[Module customization hooks]: module.md#customization-hooks
[Module customization hooks: enabling]: module.md#enabling
[Modules loaders]: packages.md#modules-loaders
+16
View File
@@ -2487,6 +2487,19 @@ 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
`--experimental-print-required-tla` (this would execute the modules
before looking for the top-level awaits).
<a id="ERR_REQUIRE_ESM"></a>
### `ERR_REQUIRE_ESM`
@@ -2495,6 +2508,9 @@ object.
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>
### `ERR_SCRIPT_EXECUTION_INTERRUPTED`
+4 -4
View File
@@ -466,11 +466,10 @@ compatibility.
### `require`
The CommonJS module `require` always treats the files it references as CommonJS.
The CommonJS module `require` currently only supports loading synchronous ES
modules when `--experimental-require-module` is enabled.
Using `require` to load an ES module is not supported because ES modules have
asynchronous execution. Instead, use [`import()`][] to load an ES module
from a CommonJS module.
See [Loading ECMAScript modules using `require()`][] for details.
### CommonJS Namespaces
@@ -1142,6 +1141,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[Import Attributes]: #import-attributes
[Import Attributes proposal]: https://github.com/tc39/proposal-import-attributes
[JSON modules]: #json-modules
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
[Module customization hooks]: module.md#customization-hooks
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
[Terminology]: #terminology
+67 -10
View File
@@ -167,16 +167,60 @@ variable. Since the module lookups using `node_modules` folders are all
relative, and based on the real path of the files making the calls to
`require()`, the packages themselves can be anywhere.
## The `.mjs` extension
## Loading ECMAScript modules using `require()`
Due to the synchronous nature of `require()`, it is not possible to use it to
load ECMAScript module files. Attempting to do so will throw a
[`ERR_REQUIRE_ESM`][] error. Use [`import()`][] instead.
The `.mjs` extension is reserved for [ECMAScript Modules][] which cannot be
loaded via `require()`. See [Determining module system][] section for more info
The `.mjs` extension is reserved for [ECMAScript Modules][].
Currently, if the flag `--experimental-require-module` is not used, loading
an ECMAScript module using `require()` will throw a [`ERR_REQUIRE_ESM`][]
error, and users need to use [`import()`][] instead. See
[Determining module system][] section for more info
regarding which files are parsed as ECMAScript modules.
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.
## All together
<!-- type=misc -->
@@ -206,12 +250,24 @@ require(X) from module at path Y
LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
2. If X.js is a file,
a. Find the closest package scope SCOPE to X.
b. If no scope was found, load X.js as a CommonJS module. STOP.
c. If the SCOPE/package.json contains "type" field,
1. If the "type" field is "module", load X.js as an ECMAScript module. STOP.
2. Else, load X.js as an CommonJS module. STOP.
3. If X.json is a file, load X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP
5. If X.mjs is a file, and `--experimental-require-module` is enabled,
load X.mjs as an ECMAScript module. STOP
LOAD_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
1. If X/index.js is a file
a. Find the closest package scope SCOPE to X.
b. If no scope was found, load X/index.js as a CommonJS module. STOP.
c. If the SCOPE/package.json contains "type" field,
1. If the "type" field is "module", load X/index.js as an ECMAScript module. STOP.
2. Else, load X/index.js as an CommonJS module. STOP.
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.node as binary addon. STOP
@@ -1085,6 +1141,7 @@ This section was moved to
[GLOBAL_FOLDERS]: #loading-from-the-global-folders
[`"main"`]: packages.md#main
[`"type"`]: packages.md#type
[`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module
[`ERR_REQUIRE_ESM`]: errors.md#err_require_esm
[`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import
[`MODULE_NOT_FOUND`]: errors.md#module_not_found
+10 -9
View File
@@ -133,14 +133,15 @@ There is the CommonJS module loader:
`process.dlopen()`.
* It treats all files that lack `.json` or `.node` extensions as JavaScript
text files.
* It cannot be used to load ECMAScript modules (although it is possible to
[load ECMASCript modules from CommonJS modules][]). When used to load a
JavaScript text file that is not an ECMAScript module, it loads it as a
CommonJS module.
* It can only be used to [load ECMASCript modules from CommonJS modules][] if
the module graph is synchronous (that contains no top-level `await`) when
`--experimental-require-module` is enabled.
When used to load a JavaScript text file that is not an ECMAScript module,
the file will be loaded as a CommonJS module.
There is the ECMAScript module loader:
* It is asynchronous.
* It is asynchronous, unless it's being used to load modules for `require()`.
* It is responsible for handling `import` statements and `import()` expressions.
* It is not monkey patchable, can be customized using [loader hooks][].
* It does not support folders as modules, directory indexes (e.g.
@@ -623,9 +624,9 @@ specific to least specific as conditions should be defined:
* `"require"` - matches when the package is loaded via `require()`. The
referenced file should be loadable with `require()` although the condition
matches regardless of the module format of the target file. Expected
formats include CommonJS, JSON, and native addons but not ES modules as
`require()` doesn't support them. _Always mutually exclusive with
`"import"`._
formats include CommonJS, JSON, native addons, and ES modules
if `--experimental-require-module` is enabled. _Always mutually
exclusive with `"import"`._
* `"default"` - the generic fallback that always matches. Can be a CommonJS
or ES module file. _This condition should always come last._
@@ -1371,7 +1372,7 @@ This field defines [subpath imports][] for the current package.
[entry points]: #package-entry-points
[folders as modules]: modules.md#folders-as-modules
[import maps]: https://github.com/WICG/import-maps
[load ECMASCript modules from CommonJS modules]: modules.md#the-mjs-extension
[load ECMASCript modules from CommonJS modules]: modules.md#loading-ecmascript-modules-using-require
[loader hooks]: esm.md#loaders
[packages folder mapping]: https://github.com/WICG/import-maps#packages-via-trailing-slashes
[self-reference]: #self-referencing-a-package-using-its-name
+95 -19
View File
@@ -60,10 +60,11 @@ const {
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
Symbol,
} = primordials;
// Map used to store CJS parsing data.
const cjsParseCache = new SafeWeakMap();
// Map used to store CJS parsing data or for ESM loading.
const cjsSourceCache = new SafeWeakMap();
/**
* Map of already-loaded CJS modules to use.
*/
@@ -72,12 +73,14 @@ const cjsExportsCache = new SafeWeakMap();
// Set first due to cycle with ESM loader functions.
module.exports = {
cjsExportsCache,
cjsParseCache,
cjsSourceCache,
initializeCJS,
Module,
wrapSafe,
};
const kIsMainSymbol = Symbol('kIsMainSymbol');
const { BuiltinModule } = require('internal/bootstrap/realm');
const {
maybeCacheSourceMap,
@@ -395,6 +398,11 @@ function initializeCJS() {
// TODO(joyeecheung): deprecate this in favor of a proper hook?
Module.runMain =
require('internal/modules/run_main').executeUserEntryPoint;
if (getOptionValue('--experimental-require-module')) {
emitExperimentalWarning('Support for loading ES Module in require()');
Module._extensions['.mjs'] = loadESMFromCJS;
}
}
// Given a module name, and a list of paths to test, returns the first
@@ -600,6 +608,19 @@ function resolveExports(nmPath, request) {
}
}
// We don't cache this in case user extends the extensions.
function getDefaultExtensions() {
const extensions = ObjectKeys(Module._extensions);
if (!getOptionValue('--experimental-require-module')) {
return extensions;
}
// If the .mjs extension is added by --experimental-require-module,
// remove it from the supported default extensions to maintain
// compatibility.
// TODO(joyeecheung): allow both .mjs and .cjs?
return ArrayPrototypeFilter(extensions, (ext) => ext !== '.mjs' || Module._extensions['.mjs'] !== loadESMFromCJS);
}
/**
* Get the absolute path to a module.
* @param {string} request Relative or absolute file path
@@ -701,7 +722,7 @@ Module._findPath = function(request, paths, isMain) {
if (!filename) {
// Try it with each of the extensions
if (exts === undefined) {
exts = ObjectKeys(Module._extensions);
exts = getDefaultExtensions();
}
filename = tryExtensions(basePath, exts, isMain);
}
@@ -710,7 +731,7 @@ Module._findPath = function(request, paths, isMain) {
if (!filename && rc === 1) { // Directory.
// try it with each of the extensions at "index"
if (exts === undefined) {
exts = ObjectKeys(Module._extensions);
exts = getDefaultExtensions();
}
filename = tryPackage(basePath, exts, isMain, request);
}
@@ -987,7 +1008,7 @@ Module._load = function(request, parent, isMain) {
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded) {
const parseCachedModule = cjsParseCache.get(cachedModule);
const parseCachedModule = cjsSourceCache.get(cachedModule);
if (!parseCachedModule || parseCachedModule.loaded) {
return getExportsForCircularRequire(cachedModule);
}
@@ -1009,6 +1030,9 @@ Module._load = function(request, parent, isMain) {
setOwnProperty(process, 'mainModule', module);
setOwnProperty(module.require, 'main', process.mainModule);
module.id = '.';
module[kIsMainSymbol] = true;
} else {
module[kIsMainSymbol] = false;
}
reportModuleToWatchMode(filename);
@@ -1244,6 +1268,20 @@ let resolvedArgv;
let hasPausedEntry = false;
/** @type {import('vm').Script} */
/**
* Resolve and evaluate it synchronously as ESM if it's ESM.
* @param {Module} mod CJS module instance
* @param {string} filename Absolute path of the file.
*/
function loadESMFromCJS(mod, filename) {
const source = getMaybeCachedSource(mod, filename);
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const isMain = mod[kIsMainSymbol];
// TODO(joyeecheung): we may want to invent optional special handling for default exports here.
// For now, it's good enough to be identical to what `import()` returns.
mod.exports = cascadedLoader.importSyncForRequire(filename, source, isMain);
}
/**
* Wraps the given content in a script and runs it in a new context.
* @param {string} filename The name of the file being loaded
@@ -1269,11 +1307,16 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
);
// Cache the source map for the module if present.
if (script.sourceMapURL) {
maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL);
const { sourceMapURL } = script;
if (sourceMapURL) {
maybeCacheSourceMap(filename, content, this, false, undefined, sourceMapURL);
}
return runScriptInThisContext(script, true, false);
return {
__proto__: null,
function: runScriptInThisContext(script, true, false),
sourceMapURL,
};
}
try {
@@ -1291,7 +1334,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);
}
return result.function;
return result;
} catch (err) {
if (process.mainModule === cjsModuleInstance) {
const { enrichCJSError } = require('internal/modules/esm/translators');
@@ -1306,8 +1349,9 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
* `exports`) to the file. Returns exception, if any.
* @param {string} content The source code of the module
* @param {string} filename The file path of the module
* @param {boolean} loadAsESM Whether it's known to be ESM via .mjs or "type" in package.json.
*/
Module.prototype._compile = function(content, filename) {
Module.prototype._compile = function(content, filename, loadAsESM = false) {
let moduleURL;
let redirects;
const manifest = policy()?.manifest;
@@ -1317,8 +1361,20 @@ Module.prototype._compile = function(content, filename) {
manifest.assertIntegrity(moduleURL, content);
}
const compiledWrapper = wrapSafe(filename, content, this);
// TODO(joyeecheung): when the module is the entry point, consider allowing TLA.
// Only modules being require()'d really need to avoid TLA.
if (loadAsESM) {
// Pass the source into the .mjs extension handler indirectly through the cache.
cjsSourceCache.set(this, { source: content });
loadESMFromCJS(this, filename);
return;
}
const { function: compiledWrapper } = wrapSafe(filename, content, this);
// TODO(joyeecheung): the detection below is unnecessarily complex. Using the
// kIsMainSymbol, or a kBreakOnStartSymbol that gets passed from
// higher level instead of doing hacky detection here.
let inspectorWrapper = null;
if (getOptionValue('--inspect-brk') && process._eval == null) {
if (!resolvedArgv) {
@@ -1362,24 +1418,43 @@ Module.prototype._compile = function(content, filename) {
};
/**
* Native handler for `.js` files.
* @param {Module} module The module to compile
* @param {string} filename The file path of the module
* Get the source code of a module, using cached ones if it's cached.
* @param {Module} mod Module instance whose source is potentially already cached.
* @param {string} filename Absolute path to the file of the module.
* @returns {string}
*/
Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
const cached = cjsParseCache.get(module);
function getMaybeCachedSource(mod, filename) {
const cached = cjsSourceCache.get(mod);
let content;
if (cached?.source) {
content = cached.source;
cached.source = undefined;
} else {
// TODO(joyeecheung): we can read a buffer instead to speed up
// compilation.
content = fs.readFileSync(filename, 'utf8');
}
return content;
}
/**
* Built-in handler for `.js` files.
* @param {Module} module The module to compile
* @param {string} filename The file path of the module
*/
Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
const content = getMaybeCachedSource(module, filename);
if (StringPrototypeEndsWith(filename, '.js')) {
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
// Function require shouldn't be used in ES modules.
if (pkg?.data.type === 'module') {
if (getOptionValue('--experimental-require-module')) {
module._compile(content, filename, true);
return;
}
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
const parent = moduleParentCache.get(module);
const parentPath = parent?.filename;
@@ -1412,7 +1487,8 @@ Module._extensions['.js'] = function(module, filename) {
throw err;
}
}
module._compile(content, filename);
module._compile(content, filename, false);
};
/**
+128 -8
View File
@@ -15,16 +15,25 @@ const {
hardenRegExp,
} = primordials;
const assert = require('internal/assert');
const {
ERR_REQUIRE_ESM,
ERR_NETWORK_IMPORT_DISALLOWED,
ERR_UNKNOWN_MODULE_FORMAT,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const { isURL } = require('internal/url');
const { emitExperimentalWarning } = require('internal/util');
const { isURL, pathToFileURL, URL } = require('internal/url');
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
const {
registerModule,
getDefaultConditions,
} = require('internal/modules/esm/utils');
const { kImplicitAssertType } = require('internal/modules/esm/assert');
const {
maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache');
const { canParse } = internalBinding('url');
const { ModuleWrap } = internalBinding('module_wrap');
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer;
/**
@@ -184,8 +193,6 @@ class ModuleLoader {
async eval(source, url, isEntryPoint = false) {
const evalInstance = (url) => {
const { ModuleWrap } = internalBinding('module_wrap');
const { registerModule } = require('internal/modules/esm/utils');
const module = new ModuleWrap(url, undefined, source, 0, 0);
registerModule(module, {
__proto__: null,
@@ -197,7 +204,7 @@ class ModuleLoader {
return module;
};
const ModuleJob = require('internal/modules/esm/module_job');
const { ModuleJob } = require('internal/modules/esm/module_job');
const job = new ModuleJob(
this, url, undefined, evalInstance, false, false);
this.loadCache.set(url, undefined, job);
@@ -250,6 +257,118 @@ class ModuleLoader {
return job;
}
/**
* This constructs (creates, instantiates and evaluates) a module graph that
* is require()'d.
* @param {string} filename Resolved filename of the module being require()'d
* @param {string} source Source code. TODO(joyeecheung): pass the raw buffer.
* @param {string} isMain Whether this module is a main module.
* @returns {ModuleNamespaceObject}
*/
importSyncForRequire(filename, source, isMain) {
const url = pathToFileURL(filename).href;
let job = this.loadCache.get(url, kImplicitAssertType);
// This module is already loaded, check whether it's synchronous and return the
// namespace.
if (job !== undefined) {
return job.module.getNamespaceSync();
}
// TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the
// cache here, or use a carrier object to carry the compiled module script
// into the constructor to ensure cache hit.
const wrap = new ModuleWrap(url, undefined, source, 0, 0);
// Cache the source map for the module if present.
if (wrap.sourceMapURL) {
maybeCacheSourceMap(url, source, null, false, undefined, wrap.sourceMapURL);
}
const { registerModule } = require('internal/modules/esm/utils');
// TODO(joyeecheung): refactor so that the default options are shared across
// the built-in loaders.
registerModule(wrap, {
__proto__: null,
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
importModuleDynamically: (specifier, wrap, importAttributes) => {
return this.import(specifier, url, importAttributes);
},
});
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
const { ModuleJobSync } = require('internal/modules/esm/module_job');
job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk);
this.loadCache.set(url, kImplicitAssertType, job);
return job.runSync().namespace;
}
/**
* Resolve individual module requests and create or get the cached ModuleWraps for
* each of them. This is only used to create a module graph being require()'d.
* @param {string} specifier Specifier of the the imported module.
* @param {string} parentURL Where the import comes from.
* @param {object} importAttributes import attributes from the import statement.
* @returns {ModuleWrap}
*/
getModuleWrapForRequire(specifier, parentURL, importAttributes) {
assert(getOptionValue('--experimental-require-module'));
if (canParse(specifier)) {
const protocol = new URL(specifier).protocol;
if (protocol === 'https:' || protocol === 'http:') {
throw new ERR_NETWORK_IMPORT_DISALLOWED(specifier, parentURL,
'ES modules cannot be loaded by require() from the network');
}
assert(protocol === 'file:' || protocol === 'node:' || protocol === 'data:');
}
const requestKey = this.#resolveCache.serializeKey(specifier, importAttributes);
let resolveResult = this.#resolveCache.get(requestKey, parentURL);
if (resolveResult == null) {
resolveResult = this.defaultResolve(specifier, parentURL, importAttributes);
this.#resolveCache.set(requestKey, parentURL, resolveResult);
}
const { url, format } = resolveResult;
const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
let job = this.loadCache.get(url, resolvedImportAttributes.type);
if (job !== undefined) {
// This module is previously imported before. We will return the module now and check
// asynchronicity of the entire graph later, after the graph is instantiated.
return job.module;
}
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
const loadResult = defaultLoadSync(url, { format, importAttributes });
const { responseURL, source } = loadResult;
let { format: finalFormat } = loadResult;
this.validateLoadResult(url, finalFormat);
if (finalFormat === 'commonjs') {
finalFormat = 'commonjs-sync';
} else if (finalFormat === 'wasm') {
assert.fail('WASM is currently unsupported by require(esm)');
}
const translator = getTranslators().get(finalFormat);
if (!translator) {
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
}
const isMain = (parentURL === undefined);
const wrap = FunctionPrototypeCall(translator, this, responseURL, source, isMain);
assert(wrap instanceof ModuleWrap); // No asynchronous translators should be called.
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
process.send({ 'watch:import': [url] });
}
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
const { ModuleJobSync } = require('internal/modules/esm/module_job');
job = new ModuleJobSync(this, url, importAttributes, wrap, isMain, inspectBrk);
this.loadCache.set(url, importAttributes.type, job);
return job.module;
}
/**
* Create and cache an object representing a loaded module.
* @param {string} url The absolute URL that was resolved for this module
@@ -277,8 +396,9 @@ class ModuleLoader {
(url, isMain) => callTranslator(this.loadSync(url, context), isMain) :
async (url, isMain) => callTranslator(await this.load(url, context), isMain);
const isMain = parentURL === undefined;
const inspectBrk = (
parentURL === undefined &&
isMain &&
getOptionValue('--inspect-brk')
);
@@ -286,13 +406,13 @@ class ModuleLoader {
process.send({ 'watch:import': [url] });
}
const ModuleJob = require('internal/modules/esm/module_job');
const { ModuleJob } = require('internal/modules/esm/module_job');
const job = new ModuleJob(
this,
url,
importAttributes,
moduleProvider,
parentURL === undefined,
isMain,
inspectBrk,
sync,
);
+54 -13
View File
@@ -8,9 +8,9 @@ const {
ObjectSetPrototypeOf,
PromiseResolve,
PromisePrototypeThen,
ReflectApply,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
ReflectApply,
SafePromiseAllReturnArrayLike,
SafePromiseAllReturnVoid,
SafeSet,
@@ -20,7 +20,7 @@ const {
globalThis,
} = primordials;
const { ModuleWrap } = internalBinding('module_wrap');
const { ModuleWrap, kEvaluated } = internalBinding('module_wrap');
const {
privateSymbols: {
entry_point_module_private_symbol,
@@ -52,24 +52,29 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
(globalLike) => errorMessage === `${globalLike} is not defined`,
);
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of
* its dependencies, over time. */
class ModuleJob {
// `loader` is the Loader instance used for loading dependencies.
// `moduleProvider` is a function
constructor(loader, url, importAttributes = { __proto__: null },
moduleProvider, isMain, inspectBrk, sync = false) {
class ModuleJobBase {
constructor(loader, url, importAttributes, moduleWrapMaybePromise, isMain, inspectBrk) {
this.loader = loader;
this.importAttributes = importAttributes;
this.isMain = isMain;
this.inspectBrk = inspectBrk;
this.url = url;
this.module = moduleWrapMaybePromise;
}
}
this.module = undefined;
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of
* its dependencies, over time. */
class ModuleJob extends ModuleJobBase {
// `loader` is the Loader instance used for loading dependencies.
constructor(loader, url, importAttributes = { __proto__: null },
moduleProvider, isMain, inspectBrk, sync = false) {
const modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
super(loader, url, importAttributes, modulePromise, isMain, inspectBrk);
// Expose the promise to the ModuleWrap directly for linking below.
// `this.module` is also filled in below.
this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
this.modulePromise = modulePromise;
if (sync) {
this.module = this.modulePromise;
@@ -255,5 +260,41 @@ class ModuleJob {
return { __proto__: null, module: this.module };
}
}
ObjectSetPrototypeOf(ModuleJob.prototype, null);
module.exports = ModuleJob;
// This is a fully synchronous job and does not spawn additional threads in any way.
// All the steps are ensured to be synchronous and it throws on instantiating
// an asynchronous graph.
class ModuleJobSync extends ModuleJobBase {
constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) {
super(loader, url, importAttributes, moduleWrap, isMain, inspectBrk, true);
assert(this.module instanceof ModuleWrap);
const moduleRequests = this.module.getModuleRequestsSync();
for (let i = 0; i < moduleRequests.length; ++i) {
const { 0: specifier, 1: attributes } = moduleRequests[i];
const wrap = this.loader.getModuleWrapForRequire(specifier, url, attributes);
const isLast = (i === moduleRequests.length - 1);
// TODO(joyeecheung): make the resolution callback deal with both promisified
// an raw module wraps, then we don't need to wrap it with a promise here.
this.module.cacheResolvedWrapsSync(specifier, PromiseResolve(wrap), isLast);
}
}
async run() {
const status = this.module.getStatus();
assert(status === kEvaluated,
`A require()-d module that is imported again must be evaluated. Status = ${status}`);
return { __proto__: null, module: this.module };
}
runSync() {
this.module.instantiateSync();
setHasStartedUserESMExecution();
const namespace = this.module.evaluateSync();
return { __proto__: null, module: this.module, namespace };
}
}
ObjectSetPrototypeOf(ModuleJobBase.prototype, null);
module.exports = {
ModuleJob, ModuleJobSync, ModuleJobBase,
};
+2 -2
View File
@@ -97,8 +97,8 @@ class LoadCache extends SafeMap {
validateString(url, 'url');
validateString(type, 'type');
const ModuleJob = require('internal/modules/esm/module_job');
if (job instanceof ModuleJob !== true &&
const { ModuleJobBase } = require('internal/modules/esm/module_job');
if (job instanceof ModuleJobBase !== true &&
typeof job !== 'function') {
throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job);
}
+26 -6
View File
@@ -44,7 +44,7 @@ const {
} = require('internal/modules/helpers');
const {
Module: CJSModule,
cjsParseCache,
cjsSourceCache,
cjsExportsCache,
} = require('internal/modules/cjs/loader');
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
@@ -81,7 +81,7 @@ let cjsParse;
*/
async function initCJSParse() {
if (typeof WebAssembly === 'undefined') {
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
initCJSParseSync();
} else {
const { parse, init } =
require('internal/deps/cjs-module-lexer/dist/lexer');
@@ -89,11 +89,19 @@ async function initCJSParse() {
await init();
cjsParse = parse;
} catch {
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
initCJSParseSync();
}
}
}
function initCJSParseSync() {
// TODO(joyeecheung): implement a binding that directly compiles using
// v8::WasmModuleObject::Compile() synchronously.
if (cjsParse === undefined) {
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
}
}
const translators = new SafeMap();
exports.translators = translators;
exports.enrichCJSError = enrichCJSError;
@@ -162,7 +170,7 @@ async function importModuleDynamically(specifier, { url }, attributes) {
}
// Strategy for loading a standard JavaScript module.
translators.set('module', async function moduleStrategy(url, source, isMain) {
translators.set('module', function moduleStrategy(url, source, isMain) {
assertBufferSource(source, true, 'load');
source = stringify(source);
debug(`Translating StandardModule ${url}`);
@@ -323,6 +331,16 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
}
translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) {
initCJSParseSync();
assert(!isMain); // This is only used by imported CJS modules.
return createCJSModuleWrap(url, source, isMain, (module, source, url, filename) => {
assert(module === CJSModule._cache[filename]);
CJSModule._load(filename, null, false);
});
});
// Handle CommonJS modules referenced by `require` calls.
// This translator function must be sync, as `require` is sync.
translators.set('require-commonjs', (url, source, isMain) => {
@@ -371,7 +389,7 @@ function cjsPreparseModuleExports(filename, source) {
// TODO: Do we want to keep hitting the user mutable CJS loader here?
let module = CJSModule._cache[filename];
if (module) {
const cached = cjsParseCache.get(module);
const cached = cjsSourceCache.get(module);
if (cached) {
return { module, exportNames: cached.exportNames };
}
@@ -395,7 +413,7 @@ function cjsPreparseModuleExports(filename, source) {
const exportNames = new SafeSet(new SafeArrayIterator(exports));
// Set first for cycles.
cjsParseCache.set(module, { source, exportNames });
cjsSourceCache.set(module, { source, exportNames });
if (reexports.length) {
module.filename = filename;
@@ -522,6 +540,8 @@ translators.set('wasm', async function(url, source) {
let compiled;
try {
// TODO(joyeecheung): implement a binding that directly compiles using
// v8::WasmModuleObject::Compile() synchronously.
compiled = await WebAssembly.compile(source);
} catch (err) {
err.message = errPath(url) + ': ' + err.message;
+1 -1
View File
@@ -16,7 +16,7 @@ const { getCodePath, isSea } = internalBinding('sea');
function embedderRunCjs(contents) {
const filename = process.execPath;
const compiledWrapper = wrapSafe(
const { function: compiledWrapper } = wrapSafe(
isSea() ? getCodePath() : filename,
contents);
+189
View File
@@ -321,6 +321,61 @@ static Local<Object> createImportAttributesContainer(
return attributes;
}
void ModuleWrap::GetModuleRequestsSync(
const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
Local<Object> that = args.This();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, that);
CHECK(!obj->linked_);
Local<Module> module = obj->module_.Get(isolate);
Local<FixedArray> module_requests = module->GetModuleRequests();
const int module_requests_length = module_requests->Length();
std::vector<Local<Value>> requests;
requests.reserve(module_requests_length);
// call the dependency resolve callbacks
for (int i = 0; i < module_requests_length; i++) {
Local<ModuleRequest> module_request =
module_requests->Get(realm->context(), i).As<ModuleRequest>();
Local<FixedArray> raw_attributes = module_request->GetImportAssertions();
std::vector<Local<Value>> request = {
module_request->GetSpecifier(),
createImportAttributesContainer(realm, isolate, raw_attributes, 3),
};
requests.push_back(Array::New(isolate, request.data(), request.size()));
}
args.GetReturnValue().Set(
Array::New(isolate, requests.data(), requests.size()));
}
void ModuleWrap::CacheResolvedWrapsSync(
const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
CHECK_EQ(args.Length(), 3);
CHECK(args[0]->IsString());
CHECK(args[1]->IsPromise());
CHECK(args[2]->IsBoolean());
ModuleWrap* dependent;
ASSIGN_OR_RETURN_UNWRAP(&dependent, args.This());
Utf8Value specifier(isolate, args[0]);
dependent->resolve_cache_[specifier.ToString()].Reset(isolate,
args[1].As<Promise>());
if (args[2].As<v8::Boolean>()->Value()) {
dependent->linked_ = true;
}
}
void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
@@ -486,6 +541,129 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result.ToLocalChecked());
}
void ModuleWrap::InstantiateSync(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
Local<Context> context = obj->context();
Local<Module> module = obj->module_.Get(isolate);
Environment* env = realm->env();
{
TryCatchScope try_catch(env);
USE(module->InstantiateModule(context, ResolveModuleCallback));
// clear resolve cache on instantiate
obj->resolve_cache_.clear();
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
AppendExceptionLine(env,
try_catch.Exception(),
try_catch.Message(),
ErrorHandlingMode::MODULE_ERROR);
try_catch.ReThrow();
return;
}
}
// If --experimental-print-required-tla is true, proceeds to evaluation even
// if it's async because we want to search for the TLA and help users locate
// them.
if (module->IsGraphAsync() && !env->options()->print_required_tla) {
THROW_ERR_REQUIRE_ASYNC_MODULE(env);
return;
}
}
void ModuleWrap::EvaluateSync(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
Local<Context> context = obj->context();
Local<Module> module = obj->module_.Get(isolate);
Environment* env = realm->env();
Local<Value> result;
{
TryCatchScope try_catch(env);
if (!module->Evaluate(context).ToLocal(&result)) {
if (try_catch.HasCaught()) {
if (!try_catch.HasTerminated()) {
try_catch.ReThrow();
}
return;
}
}
}
CHECK(result->IsPromise());
Local<Promise> promise = result.As<Promise>();
if (promise->State() == Promise::PromiseState::kRejected) {
Local<Value> exception = promise->Result();
Local<v8::Message> message =
v8::Exception::CreateMessage(isolate, exception);
AppendExceptionLine(
env, exception, message, ErrorHandlingMode::MODULE_ERROR);
isolate->ThrowException(exception);
return;
}
if (module->IsGraphAsync()) {
CHECK(env->options()->print_required_tla);
auto stalled = module->GetStalledTopLevelAwaitMessage(isolate);
if (stalled.size() != 0) {
for (auto pair : stalled) {
Local<v8::Message> message = std::get<1>(pair);
std::string reason = "Error: unexpected top-level await at ";
std::string info =
FormatErrorMessage(isolate, context, "", message, true);
reason += info;
FPrintF(stderr, "%s\n", reason);
}
}
THROW_ERR_REQUIRE_ASYNC_MODULE(env);
return;
}
CHECK_EQ(promise->State(), Promise::PromiseState::kFulfilled);
args.GetReturnValue().Set(module->GetModuleNamespace());
}
void ModuleWrap::GetNamespaceSync(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
Local<Module> module = obj->module_.Get(isolate);
switch (module->GetStatus()) {
case v8::Module::Status::kUninstantiated:
case v8::Module::Status::kInstantiating:
return realm->env()->ThrowError(
"cannot get namespace, module has not been instantiated");
case v8::Module::Status::kEvaluating:
return THROW_ERR_REQUIRE_ASYNC_MODULE(realm->env());
case v8::Module::Status::kInstantiated:
case v8::Module::Status::kEvaluated:
case v8::Module::Status::kErrored:
break;
default:
UNREACHABLE();
}
if (module->IsGraphAsync()) {
return THROW_ERR_REQUIRE_ASYNC_MODULE(realm->env());
}
Local<Value> result = module->GetModuleNamespace();
args.GetReturnValue().Set(result);
}
void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
@@ -818,6 +996,12 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
ModuleWrap::kInternalFieldCount);
SetProtoMethod(isolate, tpl, "link", Link);
SetProtoMethod(isolate, tpl, "getModuleRequestsSync", GetModuleRequestsSync);
SetProtoMethod(
isolate, tpl, "cacheResolvedWrapsSync", CacheResolvedWrapsSync);
SetProtoMethod(isolate, tpl, "instantiateSync", InstantiateSync);
SetProtoMethod(isolate, tpl, "evaluateSync", EvaluateSync);
SetProtoMethod(isolate, tpl, "getNamespaceSync", GetNamespaceSync);
SetProtoMethod(isolate, tpl, "instantiate", Instantiate);
SetProtoMethod(isolate, tpl, "evaluate", Evaluate);
SetProtoMethod(isolate, tpl, "setExport", SetSyntheticExport);
@@ -869,6 +1053,11 @@ void ModuleWrap::RegisterExternalReferences(
registry->Register(New);
registry->Register(Link);
registry->Register(GetModuleRequestsSync);
registry->Register(CacheResolvedWrapsSync);
registry->Register(InstantiateSync);
registry->Register(EvaluateSync);
registry->Register(GetNamespaceSync);
registry->Register(Instantiate);
registry->Register(Evaluate);
registry->Register(SetSyntheticExport);
+8
View File
@@ -79,6 +79,14 @@ class ModuleWrap : public BaseObject {
~ModuleWrap() override;
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetModuleRequestsSync(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void CacheResolvedWrapsSync(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void InstantiateSync(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EvaluateSync(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetNamespaceSync(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Link(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Instantiate(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Evaluate(const v8::FunctionCallbackInfo<v8::Value>& args);
+5
View File
@@ -92,6 +92,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_MODULE_NOT_FOUND, Error) \
V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \
V(ERR_OUT_OF_RANGE, RangeError) \
V(ERR_REQUIRE_ASYNC_MODULE, Error) \
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \
V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \
V(ERR_STRING_TOO_LONG, Error) \
@@ -188,6 +189,10 @@ ERRORS_WITH_CODE(V)
"creating Workers") \
V(ERR_NON_CONTEXT_AWARE_DISABLED, \
"Loading non context-aware native addons has been disabled") \
V(ERR_REQUIRE_ASYNC_MODULE, \
"require() cannot be used on an ESM graph with top-level await. Use " \
"import() instead. To see where the top-level await comes from, use " \
"--experimental-print-required-tla.") \
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, \
"Script execution was interrupted by `SIGINT`") \
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, "Failed to set PSK identity hint") \
+11
View File
@@ -355,6 +355,17 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"ES module syntax, try again to evaluate them as ES modules",
&EnvironmentOptions::detect_module,
kAllowedInEnvvar);
AddOption("--experimental-print-required-tla",
"Print pending top-level await. If --experimental-require-module "
"is true, evaluate asynchronous graphs loaded by `require()` but "
"do not run the microtasks, in order to to find and print "
"top-level await in the graph",
&EnvironmentOptions::print_required_tla,
kAllowedInEnvvar);
AddOption("--experimental-require-module",
"Allow loading explicit ES Modules in require().",
&EnvironmentOptions::require_module,
kAllowedInEnvvar);
AddOption("--diagnostic-dir",
"set dir for all output files"
" (default: current working directory)",
+2
View File
@@ -105,6 +105,8 @@ class EnvironmentOptions : public Options {
bool abort_on_uncaught_exception = false;
std::vector<std::string> conditions;
bool detect_module = false;
bool print_required_tla = false;
bool require_module = false;
std::string dns_result_order;
bool enable_source_maps = false;
bool experimental_fetch = true;
@@ -6,16 +6,16 @@ import { describe, it } from 'node:test';
// Expect note to be included in the error output
const expectedNote = 'To load an ES module, ' +
'set "type": "module" in the package.json ' +
'or use the .mjs extension.';
// Don't match the following sentence because it can change as features are
// added.
const expectedNote = 'Warning: To load an ES module';
const mustIncludeMessage = {
getMessage: () => (stderr) => `${expectedNote} not found in ${stderr}`,
getMessage: (stderr) => `${expectedNote} not found in ${stderr}`,
includeNote: true,
};
const mustNotIncludeMessage = {
getMessage: () => (stderr) => `${expectedNote} must not be included in ${stderr}`,
getMessage: (stderr) => `${expectedNote} must not be included in ${stderr}`,
includeNote: false,
};
+1 -1
View File
@@ -6,7 +6,7 @@ require('../common');
const { strictEqual, throws } = require('assert');
const { createModuleLoader } = require('internal/modules/esm/loader');
const { LoadCache, ResolveCache } = require('internal/modules/esm/module_map');
const ModuleJob = require('internal/modules/esm/module_job');
const { ModuleJob } = require('internal/modules/esm/module_job');
const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
@@ -0,0 +1,14 @@
// Flags: --experimental-require-module
'use strict';
const common = require('../common');
const assert = require('assert');
(async () => {
await import('../fixtures/es-modules/tla/resolved.mjs');
assert.throws(() => {
require('../fixtures/es-modules/tla/resolved.mjs');
}, {
code: 'ERR_REQUIRE_ASYNC_MODULE',
});
})().then(common.mustCall());
@@ -0,0 +1,35 @@
// Flags: --experimental-require-module
'use strict';
require('../common');
const assert = require('assert');
const { isModuleNamespaceObject } = require('util/types');
// If only "require" exports are defined, return "require" exports.
{
const mod = require('../fixtures/es-modules/exports-require-only/load.cjs');
assert.deepStrictEqual({ ...mod }, { type: 'cjs' });
assert(!isModuleNamespaceObject(mod));
}
// If only "import" exports are defined, throw ERR_PACKAGE_PATH_NOT_EXPORTED
// instead of falling back to it, because the request comes from require().
assert.throws(() => {
require('../fixtures/es-modules/exports-import-only/load.cjs');
}, {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
});
// If both are defined, "require" is used.
{
const mod = require('../fixtures/es-modules/exports-both/load.cjs');
assert.deepStrictEqual({ ...mod }, { type: 'cjs' });
assert(!isModuleNamespaceObject(mod));
}
// If "import" and "default" are defined, "default" is used.
{
const mod = require('../fixtures/es-modules/exports-import-default/load.cjs');
assert.deepStrictEqual({ ...mod }, { type: 'cjs' });
assert(!isModuleNamespaceObject(mod));
}
@@ -0,0 +1,17 @@
// Flags: --experimental-require-module
'use strict';
require('../common');
const assert = require('assert');
const { isModuleNamespaceObject } = require('util/types');
const mod = require('../fixtures/es-modules/package-default-extension/index.mjs');
assert.deepStrictEqual({ ...mod }, { entry: 'mjs' });
assert(isModuleNamespaceObject(mod));
assert.throws(() => {
const mod = require('../fixtures/es-modules/package-default-extension');
console.log(mod); // In case it succeeds, log the result for debugging.
}, {
code: 'MODULE_NOT_FOUND',
});
@@ -0,0 +1,32 @@
// Flags: --experimental-require-module
'use strict';
// Tests that previously dynamically import()'ed results are reference equal to
// require()'d results.
const common = require('../common');
const assert = require('assert');
const path = require('path');
const { pathToFileURL } = require('url');
(async () => {
const modules = [
'../fixtures/es-module-loaders/module-named-exports.mjs',
'../fixtures/es-modules/import-esm.mjs',
'../fixtures/es-modules/require-cjs.mjs',
'../fixtures/es-modules/cjs-exports.mjs',
'../common/index.mjs',
'../fixtures/es-modules/package-type-module/index.js',
];
for (const id of modules) {
const url = pathToFileURL(path.resolve(__dirname, id));
const imported = await import(url);
const required = require(id);
assert.strictEqual(imported, required,
`import()'ed and require()'ed result of ${id} was not reference equal`);
}
const id = '../fixtures/es-modules/data-import.mjs';
const imported = await import(id);
const required = require(id);
assert.strictEqual(imported.data, required.data);
})().then(common.mustCall());
@@ -0,0 +1,32 @@
// Flags: --experimental-require-module
'use strict';
// Tests that previously dynamically require()'ed results are reference equal to
// import()'d results.
const common = require('../common');
const assert = require('assert');
const { pathToFileURL } = require('url');
const path = require('path');
(async () => {
const modules = [
'../fixtures/es-module-loaders/module-named-exports.mjs',
'../fixtures/es-modules/import-esm.mjs',
'../fixtures/es-modules/require-cjs.mjs',
'../fixtures/es-modules/cjs-exports.mjs',
'../common/index.mjs',
'../fixtures/es-modules/package-type-module/index.js',
];
for (const id of modules) {
const url = pathToFileURL(path.resolve(__dirname, id));
const required = require(id);
const imported = await import(url);
assert.strictEqual(imported, required,
`import()'ed and require()'ed result of ${id} was not reference equal`);
}
const id = '../fixtures/es-modules/data-import.mjs';
const required = require(id);
const imported = await import(id);
assert.strictEqual(imported.data, required.data);
})().then(common.mustCall());
@@ -0,0 +1,48 @@
// Flags: --experimental-require-module
'use strict';
require('../common');
const assert = require('assert');
const { spawnSyncAndExit } = require('../common/child_process');
const fixtures = require('../common/fixtures');
spawnSyncAndExit(process.execPath, [
'--experimental-require-module',
fixtures.path('es-modules/require-syntax-error.cjs'),
], {
status: 1,
signal: null,
stderr(output) {
assert.match(output, /var foo bar;/);
assert.match(output, /SyntaxError: Unexpected identifier 'bar'/);
return true;
},
});
spawnSyncAndExit(process.execPath, [
'--experimental-require-module',
fixtures.path('es-modules/require-reference-error.cjs'),
], {
status: 1,
signal: null,
trim: true,
stdout: 'executed',
stderr(output) {
assert.match(output, /module\.exports = { hello: 'world' };/);
assert.match(output, /ReferenceError: module is not defined/);
return true;
},
});
spawnSyncAndExit(process.execPath, [
'--experimental-require-module',
fixtures.path('es-modules/require-throw-error.cjs'),
], {
status: 1,
signal: null,
stderr(output) {
assert.match(output, /throw new Error\('test'\);/);
assert.match(output, /Error: test/);
return true;
},
});
@@ -0,0 +1,33 @@
// Flags: --experimental-require-module
'use strict';
// Tests that require()ing modules without explicit module type information
// warns and errors.
require('../common');
const assert = require('assert');
const { isModuleNamespaceObject } = require('util/types');
assert.throws(() => {
require('../fixtures/es-modules/package-without-type/noext-esm');
}, {
message: /Unexpected token 'export'/
});
assert.throws(() => {
require('../fixtures/es-modules/loose.js');
}, {
message: /Unexpected token 'export'/
});
{
// .mjs should not be matched as default extensions.
const id = '../fixtures/es-modules/should-not-be-resolved';
assert.throws(() => {
require(id);
}, {
code: 'MODULE_NOT_FOUND'
});
const mod = require(`${id}.mjs`);
assert.deepStrictEqual({ ...mod }, { hello: 'world' });
assert(isModuleNamespaceObject(mod));
}
@@ -0,0 +1,72 @@
'use strict';
require('../common');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
const fixtures = require('../common/fixtures');
const stderr = /ExperimentalWarning: Support for loading ES Module in require/;
// Test named exports.
{
spawnSyncAndExitWithoutError(
process.execPath,
[ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-module-loaders/module-named-exports.mjs') ],
{
stderr,
}
);
}
// Test ESM that import ESM.
{
spawnSyncAndExitWithoutError(
process.execPath,
[ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/import-esm.mjs') ],
{
stderr,
stdout: 'world',
trim: true,
}
);
}
// Test ESM that import CJS.
{
spawnSyncAndExitWithoutError(
process.execPath,
[ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/cjs-exports.mjs') ],
{
stdout: 'ok',
stderr,
trim: true,
}
);
}
// Test ESM that require() CJS.
// Can't use the common/index.mjs here because that checks the globals, and
// -r injects a bunch of globals.
{
spawnSyncAndExitWithoutError(
process.execPath,
[ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/require-cjs.mjs') ],
{
stdout: 'world',
stderr,
trim: true,
}
);
}
// Test "type": "module" and "main" field in package.json.
{
spawnSyncAndExitWithoutError(
process.execPath,
[ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/package-type-module') ],
{
stdout: 'package-type-module',
stderr,
trim: true,
}
);
}
@@ -0,0 +1,11 @@
// Flags: --experimental-require-module
'use strict';
require('../common');
const assert = require('assert');
assert.throws(() => {
require('../fixtures/es-modules/network-import.mjs');
}, {
code: 'ERR_NETWORK_IMPORT_DISALLOWED'
});
+63
View File
@@ -0,0 +1,63 @@
// Flags: --experimental-require-module
'use strict';
require('../common');
const assert = require('assert');
const { spawnSyncAndExit } = require('../common/child_process');
const fixtures = require('../common/fixtures');
const message = /require\(\) cannot be used on an ESM graph with top-level await/;
const code = 'ERR_REQUIRE_ASYNC_MODULE';
assert.throws(() => {
require('../fixtures/es-modules/tla/rejected.mjs');
}, { message, code });
assert.throws(() => {
require('../fixtures/es-modules/tla/unresolved.mjs');
}, { message, code });
assert.throws(() => {
require('../fixtures/es-modules/tla/resolved.mjs');
}, { message, code });
// Test TLA in inner graphs.
assert.throws(() => {
require('../fixtures/es-modules/tla/parent.mjs');
}, { message, code });
{
spawnSyncAndExit(process.execPath, [
'--experimental-require-module',
fixtures.path('es-modules/tla/require-execution.js'),
], {
signal: null,
status: 1,
stderr(output) {
assert.doesNotMatch(output, /I am executed/);
assert.match(output, message);
return true;
},
stdout: ''
});
}
{
spawnSyncAndExit(process.execPath, [
'--experimental-require-module',
'--experimental-print-required-tla',
fixtures.path('es-modules/tla/require-execution.js'),
], {
signal: null,
status: 1,
stderr(output) {
assert.match(output, /I am executed/);
assert.match(output, /Error: unexpected top-level await at.*execution\.mjs:3/);
assert.match(output, /await Promise\.resolve\('hi'\)/);
assert.match(output, message);
return true;
},
stdout: ''
});
}
@@ -0,0 +1,21 @@
// Flags: --experimental-require-module
'use strict';
require('../common');
const assert = require('assert');
const modules = [
'../fixtures/es-module-loaders/module-named-exports.mjs',
'../fixtures/es-modules/import-esm.mjs',
'../fixtures/es-modules/require-cjs.mjs',
'../fixtures/es-modules/cjs-exports.mjs',
'../common/index.mjs',
'../fixtures/es-modules/package-type-module/index.js',
];
for (const id of modules) {
const first = require(id);
const second = require(id);
assert.strictEqual(first, second,
`the results of require('${id}') twice are not reference equal`);
}
+62
View File
@@ -0,0 +1,62 @@
// Flags: --experimental-require-module
'use strict';
const common = require('../common');
const assert = require('assert');
const { isModuleNamespaceObject } = require('util/types');
common.expectWarning(
'ExperimentalWarning',
'Support for loading ES Module in require() is an experimental feature ' +
'and might change at any time'
);
// Test named exports.
{
const mod = require('../fixtures/es-module-loaders/module-named-exports.mjs');
assert.deepStrictEqual({ ...mod }, { foo: 'foo', bar: 'bar' });
assert(isModuleNamespaceObject(mod));
}
// Test ESM that import ESM.
{
const mod = require('../fixtures/es-modules/import-esm.mjs');
assert.deepStrictEqual({ ...mod }, { hello: 'world' });
assert(isModuleNamespaceObject(mod));
}
// Test ESM that import CJS.
{
const mod = require('../fixtures/es-modules/cjs-exports.mjs');
assert.deepStrictEqual({ ...mod }, {});
assert(isModuleNamespaceObject(mod));
}
// Test ESM that require() CJS.
{
const mjs = require('../common/index.mjs');
// Only comparing a few properties because the ESM version of test/common doesn't
// re-export everything from the CJS version.
assert.strictEqual(common.mustCall, mjs.mustCall);
assert.strictEqual(common.localIPv6Hosts, mjs.localIPv6Hosts);
assert(!isModuleNamespaceObject(common));
assert(isModuleNamespaceObject(mjs));
}
// Test "type": "module" and "main" field in package.json.
// Also, test default export.
{
const mod = require('../fixtures/es-modules/package-type-module');
assert.deepStrictEqual({ ...mod }, { default: 'package-type-module' });
assert(isModuleNamespaceObject(mod));
}
// Test data: import.
{
const mod = require('../fixtures/es-modules/data-import.mjs');
assert.deepStrictEqual({ ...mod }, {
data: { hello: 'world' },
id: 'data:text/javascript,export default %7B%20hello%3A%20%22world%22%20%7D'
});
assert(isModuleNamespaceObject(mod));
}
+2
View File
@@ -0,0 +1,2 @@
export { default as data } from 'data:text/javascript,export default %7B%20hello%3A%20%22world%22%20%7D';
export const id = 'data:text/javascript,export default %7B%20hello%3A%20%22world%22%20%7D';
@@ -1,4 +1,3 @@
{
"type": "module"
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('dep');
+1
View File
@@ -0,0 +1 @@
module.exports = { type: "cjs" };
+2
View File
@@ -0,0 +1,2 @@
export const type = "mjs";
+8
View File
@@ -0,0 +1,8 @@
{
"exports": {
".": {
"import": "./mod.mjs",
"require": "./mod.cjs"
}
}
}
@@ -0,0 +1 @@
module.exports = require('dep');
@@ -0,0 +1 @@
module.exports = { type: "cjs" };
@@ -0,0 +1,2 @@
export const type = "mjs";
@@ -0,0 +1,8 @@
{
"exports": {
".": {
"import": "./mod.mjs",
"default": "./mod.js"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
module.exports = require('dep');
+2
View File
@@ -0,0 +1,2 @@
export const type = 'mjs';
@@ -0,0 +1,8 @@
{
"type": "module",
"exports": {
".": {
"import": "./mod.js"
}
}
}
@@ -0,0 +1 @@
module.exports = require('dep');
@@ -0,0 +1 @@
module.exports = { type: "cjs" };
@@ -0,0 +1,7 @@
{
"exports": {
".": {
"require": "./mod.js"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
import { hello } from './imported-esm.mjs';
console.log(hello);
export { hello };
+1
View File
@@ -0,0 +1 @@
export const hello = 'world';
+1
View File
@@ -0,0 +1 @@
import 'http://example.com/foo.js';
@@ -0,0 +1 @@
module.exports = { entry: 'cjs' };
@@ -0,0 +1 @@
export const entry = 'mjs';
+3
View File
@@ -0,0 +1,3 @@
// Reference errors are not thrown until reference happens.
console.log('executed');
module.exports = { hello: 'world' };
+5
View File
@@ -0,0 +1,5 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const exports = require('./required-cjs');
console.log(exports.hello);
export default exports;
+2
View File
@@ -0,0 +1,2 @@
'use strict';
require('./reference-error.mjs');
+2
View File
@@ -0,0 +1,2 @@
'use strict';
require('./syntax-error.mjs');
+2
View File
@@ -0,0 +1,2 @@
'use strict';
require('./throw-error.mjs');
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
hello: 'world',
};
+1
View File
@@ -0,0 +1 @@
export const hello = 'world';
+1
View File
@@ -0,0 +1 @@
var foo bar;
+1
View File
@@ -0,0 +1 @@
throw new Error('test');
+3
View File
@@ -0,0 +1,3 @@
import process from 'node:process';
process._rawDebug('I am executed');
await Promise.resolve('hi');
+1
View File
@@ -0,0 +1 @@
require('./execution.mjs');
+1
View File
@@ -0,0 +1 @@
await Promise.resolve('hello');