esm: support source phase imports for WebAssembly

PR-URL: https://github.com/nodejs/node/pull/56919
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
Guy Bedford 2025-02-04 11:31:10 -08:00
parent ac5afbc83a
commit db4dcc05ac
22 changed files with 580 additions and 95 deletions

View File

@ -2736,6 +2736,17 @@ The source map could not be parsed because it does not exist, or is corrupt.
A file imported from a source map was not found. A file imported from a source map was not found.
<a id="ERR_SOURCE_PHASE_NOT_DEFINED"></a>
### `ERR_SOURCE_PHASE_NOT_DEFINED`
<!-- YAML
added: REPLACEME
-->
The provided module import does not provide a source phase imports representation for source phase
import syntax `import source x from 'x'` or `import.source(x)`.
<a id="ERR_SQLITE_ERROR"></a> <a id="ERR_SQLITE_ERROR"></a>
### `ERR_SQLITE_ERROR` ### `ERR_SQLITE_ERROR`

View File

@ -669,17 +669,19 @@ imported from the same path.
> Stability: 1 - Experimental > Stability: 1 - Experimental
Importing WebAssembly modules is supported under the Importing both WebAssembly module instances and WebAssembly source phase
`--experimental-wasm-modules` flag, allowing any `.wasm` files to be imports are supported under the `--experimental-wasm-modules` flag.
imported as normal modules while also supporting their module imports.
This integration is in line with the Both of these integrations are in line with the
[ES Module Integration Proposal for WebAssembly][]. [ES Module Integration Proposal for WebAssembly][].
For example, an `index.mjs` containing: Instance imports allow any `.wasm` files to be imported as normal modules,
supporting their module imports in turn.
For example, an `index.js` containing:
```js ```js
import * as M from './module.wasm'; import * as M from './library.wasm';
console.log(M); console.log(M);
``` ```
@ -689,7 +691,35 @@ executed under:
node --experimental-wasm-modules index.mjs node --experimental-wasm-modules index.mjs
``` ```
would provide the exports interface for the instantiation of `module.wasm`. would provide the exports interface for the instantiation of `library.wasm`.
### Wasm Source Phase Imports
<!-- YAML
added: REPLACEME
-->
The [Source Phase Imports][] proposal allows the `import source` keyword
combination to import a `WebAssembly.Module` object directly, instead of getting
a module instance already instantiated with its dependencies.
This is useful when needing custom instantiations for Wasm, while still
resolving and loading it through the ES module integration.
For example, to create multiple instances of a module, or to pass custom imports
into a new instance of `library.wasm`:
```js
import source libraryModule from './library.wasm';
const instance1 = await WebAssembly.instantiate(libraryModule, {
custom: import1,
});
const instance2 = await WebAssembly.instantiate(libraryModule, {
custom: import2,
});
```
<i id="esm_experimental_top_level_await"></i> <i id="esm_experimental_top_level_await"></i>
@ -1126,6 +1156,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require [Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
[Module customization hooks]: module.md#customization-hooks [Module customization hooks]: module.md#customization-hooks
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification [Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
[Terminology]: #terminology [Terminology]: #terminology
[URL]: https://url.spec.whatwg.org/ [URL]: https://url.spec.whatwg.org/
[`"exports"`]: packages.md#exports [`"exports"`]: packages.md#exports

View File

@ -1908,6 +1908,7 @@ has the following signature:
* `importAttributes` {Object} The `"with"` value passed to the * `importAttributes` {Object} The `"with"` value passed to the
[`optionsExpression`][] optional parameter, or an empty object if no value was [`optionsExpression`][] optional parameter, or an empty object if no value was
provided. provided.
* `phase` {string} The phase of the dynamic import (`"source"` or `"evaluation"`).
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is * Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
recommended in order to take advantage of error tracking, and to avoid issues recommended in order to take advantage of error tracking, and to avoid issues
with namespaces that contain `then` function exports. with namespaces that contain `then` function exports.

View File

@ -1508,7 +1508,7 @@ function loadESMFromCJS(mod, filename, format, source) {
if (isMain) { if (isMain) {
require('internal/modules/run_main').runEntryPointWithESMLoader((cascadedLoader) => { require('internal/modules/run_main').runEntryPointWithESMLoader((cascadedLoader) => {
const mainURL = pathToFileURL(filename).href; const mainURL = pathToFileURL(filename).href;
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, true); return cascadedLoader.import(mainURL, undefined, { __proto__: null }, undefined, true);
}); });
// ESM won't be accessible via process.mainModule. // ESM won't be accessible via process.mainModule.
setOwnProperty(process, 'mainModule', undefined); setOwnProperty(process, 'mainModule', undefined);

View File

@ -38,7 +38,7 @@ const {
forceDefaultLoader, forceDefaultLoader,
} = require('internal/modules/esm/utils'); } = require('internal/modules/esm/utils');
const { kImplicitTypeAttribute } = require('internal/modules/esm/assert'); const { kImplicitTypeAttribute } = require('internal/modules/esm/assert');
const { ModuleWrap, kEvaluating, kEvaluated } = internalBinding('module_wrap'); const { ModuleWrap, kEvaluating, kEvaluated, kEvaluationPhase, kSourcePhase } = internalBinding('module_wrap');
const { const {
urlToFilename, urlToFilename,
} = require('internal/modules/helpers'); } = require('internal/modules/helpers');
@ -236,8 +236,7 @@ class ModuleLoader {
async executeModuleJob(url, wrap, isEntryPoint = false) { async executeModuleJob(url, wrap, isEntryPoint = false) {
const { ModuleJob } = require('internal/modules/esm/module_job'); const { ModuleJob } = require('internal/modules/esm/module_job');
const module = await onImport.tracePromise(async () => { const module = await onImport.tracePromise(async () => {
const job = new ModuleJob( const job = new ModuleJob(this, url, undefined, wrap, kEvaluationPhase, false, false);
this, url, undefined, wrap, false, false);
this.loadCache.set(url, undefined, job); this.loadCache.set(url, undefined, job);
const { module } = await job.run(isEntryPoint); const { module } = await job.run(isEntryPoint);
return module; return module;
@ -273,11 +272,12 @@ class ModuleLoader {
* @param {string} [parentURL] The URL of the module where the module request is initiated. * @param {string} [parentURL] The URL of the module where the module request is initiated.
* It's undefined if it's from the root module. * It's undefined if it's from the root module.
* @param {ImportAttributes} importAttributes Attributes from the import statement or expression. * @param {ImportAttributes} importAttributes Attributes from the import statement or expression.
* @param {number} phase Import phase.
* @returns {Promise<ModuleJobBase>} * @returns {Promise<ModuleJobBase>}
*/ */
async getModuleJobForImport(specifier, parentURL, importAttributes) { async getModuleJobForImport(specifier, parentURL, importAttributes, phase) {
const resolveResult = await this.resolve(specifier, parentURL, importAttributes); const resolveResult = await this.resolve(specifier, parentURL, importAttributes);
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, false); return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, phase, false);
} }
/** /**
@ -287,11 +287,12 @@ class ModuleLoader {
* @param {string} specifier See {@link getModuleJobForImport} * @param {string} specifier See {@link getModuleJobForImport}
* @param {string} [parentURL] See {@link getModuleJobForImport} * @param {string} [parentURL] See {@link getModuleJobForImport}
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport} * @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
* @param {number} phase Import phase.
* @returns {Promise<ModuleJobBase>} * @returns {Promise<ModuleJobBase>}
*/ */
getModuleJobForRequireInImportedCJS(specifier, parentURL, importAttributes) { getModuleJobForRequireInImportedCJS(specifier, parentURL, importAttributes, phase) {
const resolveResult = this.resolveSync(specifier, parentURL, importAttributes); const resolveResult = this.resolveSync(specifier, parentURL, importAttributes);
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, true); return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, phase, true);
} }
/** /**
@ -300,16 +301,21 @@ class ModuleLoader {
* @param {{ format: string, url: string }} resolveResult Resolved module request. * @param {{ format: string, url: string }} resolveResult Resolved module request.
* @param {string} [parentURL] See {@link getModuleJobForImport} * @param {string} [parentURL] See {@link getModuleJobForImport}
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport} * @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
* @param {number} phase Import phase.
* @param {boolean} isForRequireInImportedCJS Whether this is done for require() in imported CJS. * @param {boolean} isForRequireInImportedCJS Whether this is done for require() in imported CJS.
* @returns {ModuleJobBase} * @returns {ModuleJobBase}
*/ */
#getJobFromResolveResult(resolveResult, parentURL, importAttributes, isForRequireInImportedCJS = false) { #getJobFromResolveResult(resolveResult, parentURL, importAttributes, phase,
isForRequireInImportedCJS = false) {
const { url, format } = resolveResult; const { url, format } = resolveResult;
const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes; const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
let job = this.loadCache.get(url, resolvedImportAttributes.type); let job = this.loadCache.get(url, resolvedImportAttributes.type);
if (job === undefined) { if (job === undefined) {
job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format, isForRequireInImportedCJS); job = this.#createModuleJob(url, resolvedImportAttributes, phase, parentURL, format,
isForRequireInImportedCJS);
} else {
job.ensurePhase(phase);
} }
return job; return job;
@ -377,7 +383,7 @@ class ModuleLoader {
const inspectBrk = (isMain && getOptionValue('--inspect-brk')); const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
const { ModuleJobSync } = require('internal/modules/esm/module_job'); const { ModuleJobSync } = require('internal/modules/esm/module_job');
job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk); job = new ModuleJobSync(this, url, kEmptyObject, wrap, kEvaluationPhase, isMain, inspectBrk);
this.loadCache.set(url, kImplicitTypeAttribute, job); this.loadCache.set(url, kImplicitTypeAttribute, job);
mod[kRequiredModuleSymbol] = job.module; mod[kRequiredModuleSymbol] = job.module;
return { wrap: job.module, namespace: job.runSync(parent).namespace }; return { wrap: job.module, namespace: job.runSync(parent).namespace };
@ -389,9 +395,10 @@ class ModuleLoader {
* @param {string} specifier Specifier of the the imported module. * @param {string} specifier Specifier of the the imported module.
* @param {string} parentURL Where the import comes from. * @param {string} parentURL Where the import comes from.
* @param {object} importAttributes import attributes from the import statement. * @param {object} importAttributes import attributes from the import statement.
* @param {number} phase The import phase.
* @returns {ModuleJobBase} * @returns {ModuleJobBase}
*/ */
getModuleJobForRequire(specifier, parentURL, importAttributes) { getModuleJobForRequire(specifier, parentURL, importAttributes, phase) {
const parsed = URLParse(specifier); const parsed = URLParse(specifier);
if (parsed != null) { if (parsed != null) {
const protocol = parsed.protocol; const protocol = parsed.protocol;
@ -422,6 +429,7 @@ class ModuleLoader {
} }
throw new ERR_REQUIRE_CYCLE_MODULE(message); throw new ERR_REQUIRE_CYCLE_MODULE(message);
} }
job.ensurePhase(phase);
// Otherwise the module could be imported before but the evaluation may be already // Otherwise the module could be imported before but the evaluation may be already
// completed (e.g. the require call is lazy) so it's okay. We will return the // completed (e.g. the require call is lazy) so it's okay. We will return the
// module now and check asynchronicity of the entire graph later, after the // module now and check asynchronicity of the entire graph later, after the
@ -463,7 +471,7 @@ class ModuleLoader {
const inspectBrk = (isMain && getOptionValue('--inspect-brk')); const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
const { ModuleJobSync } = require('internal/modules/esm/module_job'); const { ModuleJobSync } = require('internal/modules/esm/module_job');
job = new ModuleJobSync(this, url, importAttributes, wrap, isMain, inspectBrk); job = new ModuleJobSync(this, url, importAttributes, wrap, phase, isMain, inspectBrk);
this.loadCache.set(url, importAttributes.type, job); this.loadCache.set(url, importAttributes.type, job);
return job; return job;
@ -543,13 +551,14 @@ class ModuleLoader {
* by the time this returns. Otherwise it may still have pending module requests. * by the time this returns. Otherwise it may still have pending module requests.
* @param {string} url The URL that was resolved for this module. * @param {string} url The URL that was resolved for this module.
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport} * @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
* @param {number} phase Import phase.
* @param {string} [parentURL] See {@link getModuleJobForImport} * @param {string} [parentURL] See {@link getModuleJobForImport}
* @param {string} [format] The format hint possibly returned by the `resolve` hook * @param {string} [format] The format hint possibly returned by the `resolve` hook
* @param {boolean} isForRequireInImportedCJS Whether this module job is created for require() * @param {boolean} isForRequireInImportedCJS Whether this module job is created for require()
* in imported CJS. * in imported CJS.
* @returns {ModuleJobBase} The (possibly pending) module job * @returns {ModuleJobBase} The (possibly pending) module job
*/ */
#createModuleJob(url, importAttributes, parentURL, format, isForRequireInImportedCJS) { #createModuleJob(url, importAttributes, phase, parentURL, format, isForRequireInImportedCJS) {
const context = { format, importAttributes }; const context = { format, importAttributes };
const isMain = parentURL === undefined; const isMain = parentURL === undefined;
@ -575,6 +584,7 @@ class ModuleLoader {
url, url,
importAttributes, importAttributes,
moduleOrModulePromise, moduleOrModulePromise,
phase,
isMain, isMain,
inspectBrk, inspectBrk,
isForRequireInImportedCJS, isForRequireInImportedCJS,
@ -592,11 +602,18 @@ class ModuleLoader {
* @param {string} parentURL Path of the parent importing the module. * @param {string} parentURL Path of the parent importing the module.
* @param {Record<string, string>} importAttributes Validations for the * @param {Record<string, string>} importAttributes Validations for the
* module import. * module import.
* @param {number} [phase] The phase of the import.
* @param {boolean} [isEntryPoint] Whether this is the realm-level entry point.
* @returns {Promise<ModuleExports>} * @returns {Promise<ModuleExports>}
*/ */
async import(specifier, parentURL, importAttributes, isEntryPoint = false) { async import(specifier, parentURL, importAttributes, phase = kEvaluationPhase, isEntryPoint = false) {
return onImport.tracePromise(async () => { return onImport.tracePromise(async () => {
const moduleJob = await this.getModuleJobForImport(specifier, parentURL, importAttributes); const moduleJob = await this.getModuleJobForImport(specifier, parentURL, importAttributes,
phase);
if (phase === kSourcePhase) {
const module = await moduleJob.modulePromise;
return module.getModuleSourceObject();
}
const { module } = await moduleJob.run(isEntryPoint); const { module } = await moduleJob.run(isEntryPoint);
return module.getNamespace(); return module.getNamespace();
}, { }, {

View File

@ -22,7 +22,7 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn; debug = fn;
}); });
const { ModuleWrap, kInstantiated } = internalBinding('module_wrap'); const { ModuleWrap, kInstantiated, kEvaluationPhase } = internalBinding('module_wrap');
const { const {
privateSymbols: { privateSymbols: {
entry_point_module_private_symbol, entry_point_module_private_symbol,
@ -59,8 +59,10 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
); );
class ModuleJobBase { class ModuleJobBase {
constructor(url, importAttributes, isMain, inspectBrk) { constructor(url, importAttributes, phase, isMain, inspectBrk) {
assert(typeof phase === 'number');
this.importAttributes = importAttributes; this.importAttributes = importAttributes;
this.phase = phase;
this.isMain = isMain; this.isMain = isMain;
this.inspectBrk = inspectBrk; this.inspectBrk = inspectBrk;
@ -78,14 +80,15 @@ class ModuleJob extends ModuleJobBase {
* @param {string} url URL of the module to be wrapped in ModuleJob. * @param {string} url URL of the module to be wrapped in ModuleJob.
* @param {ImportAttributes} importAttributes Import attributes from the import statement. * @param {ImportAttributes} importAttributes Import attributes from the import statement.
* @param {ModuleWrap|Promise<ModuleWrap>} moduleOrModulePromise Translated ModuleWrap for the module. * @param {ModuleWrap|Promise<ModuleWrap>} moduleOrModulePromise Translated ModuleWrap for the module.
* @param {number} phase The phase to load the module to.
* @param {boolean} isMain Whether the module is the entry point. * @param {boolean} isMain Whether the module is the entry point.
* @param {boolean} inspectBrk Whether this module should be evaluated with the * @param {boolean} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed). * first line paused in the debugger (because --inspect-brk is passed).
* @param {boolean} isForRequireInImportedCJS Whether this is created for require() in imported CJS. * @param {boolean} isForRequireInImportedCJS Whether this is created for require() in imported CJS.
*/ */
constructor(loader, url, importAttributes = { __proto__: null }, constructor(loader, url, importAttributes = { __proto__: null }, moduleOrModulePromise,
moduleOrModulePromise, isMain, inspectBrk, isForRequireInImportedCJS = false) { phase = kEvaluationPhase, isMain, inspectBrk, isForRequireInImportedCJS = false) {
super(url, importAttributes, isMain, inspectBrk); super(url, importAttributes, phase, isMain, inspectBrk);
this.#loader = loader; this.#loader = loader;
// Expose the promise to the ModuleWrap directly for linking below. // Expose the promise to the ModuleWrap directly for linking below.
@ -97,22 +100,37 @@ class ModuleJob extends ModuleJobBase {
this.modulePromise = moduleOrModulePromise; this.modulePromise = moduleOrModulePromise;
} }
// Promise for the list of all dependencyJobs. if (this.phase === kEvaluationPhase) {
this.linked = this._link(); // Promise for the list of all dependencyJobs.
// This promise is awaited later anyway, so silence this.linked = this.#link();
// 'unhandled rejection' warnings. // This promise is awaited later anyway, so silence
PromisePrototypeThen(this.linked, undefined, noop); // 'unhandled rejection' warnings.
PromisePrototypeThen(this.linked, undefined, noop);
}
// instantiated == deep dependency jobs wrappers are instantiated, // instantiated == deep dependency jobs wrappers are instantiated,
// and module wrapper is instantiated. // and module wrapper is instantiated.
this.instantiated = undefined; this.instantiated = undefined;
} }
/**
* Ensure that this ModuleJob is moving towards the required phase
* (does not necessarily mean it is ready at that phase - run does that)
* @param {number} phase
*/
ensurePhase(phase) {
if (this.phase < phase) {
this.phase = phase;
this.linked = this.#link();
PromisePrototypeThen(this.linked, undefined, noop);
}
}
/** /**
* Iterates the module requests and links with the loader. * Iterates the module requests and links with the loader.
* @returns {Promise<ModuleJob[]>} Dependency module jobs. * @returns {Promise<ModuleJob[]>} Dependency module jobs.
*/ */
async _link() { async #link() {
this.module = await this.modulePromise; this.module = await this.modulePromise;
assert(this.module instanceof ModuleWrap); assert(this.module instanceof ModuleWrap);
@ -123,23 +141,33 @@ class ModuleJob extends ModuleJobBase {
// these `link` callbacks depending on each other. // these `link` callbacks depending on each other.
// Create an ArrayLike to avoid calling into userspace with `.then` // Create an ArrayLike to avoid calling into userspace with `.then`
// when returned from the async function. // when returned from the async function.
const dependencyJobs = Array(moduleRequests.length); const evaluationDepJobs = Array(moduleRequests.length);
ObjectSetPrototypeOf(dependencyJobs, null); ObjectSetPrototypeOf(evaluationDepJobs, null);
// Specifiers should be aligned with the moduleRequests array in order. // Specifiers should be aligned with the moduleRequests array in order.
const specifiers = Array(moduleRequests.length); const specifiers = Array(moduleRequests.length);
const modulePromises = Array(moduleRequests.length); const modulePromises = Array(moduleRequests.length);
// Track each loop for whether it is an evaluation phase or source phase request.
let isEvaluation;
// Iterate with index to avoid calling into userspace with `Symbol.iterator`. // Iterate with index to avoid calling into userspace with `Symbol.iterator`.
for (let idx = 0; idx < moduleRequests.length; idx++) { for (
const { specifier, attributes } = moduleRequests[idx]; let idx = 0, eidx = 0;
// Use the let-scoped eidx value to update the executionDepJobs length at the end of the loop.
idx < moduleRequests.length || (evaluationDepJobs.length = eidx, false);
idx++, eidx += isEvaluation
) {
const { specifier, phase, attributes } = moduleRequests[idx];
isEvaluation = phase === kEvaluationPhase;
// TODO(joyeecheung): resolve all requests first, then load them in another // TODO(joyeecheung): resolve all requests first, then load them in another
// loop so that hooks can pre-fetch sources off-thread. // loop so that hooks can pre-fetch sources off-thread.
const dependencyJobPromise = this.#loader.getModuleJobForImport( const dependencyJobPromise = this.#loader.getModuleJobForImport(
specifier, this.url, attributes, specifier, this.url, attributes, phase,
); );
const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => { const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
debug(`async link() ${this.url} -> ${specifier}`, job); debug(`async link() ${this.url} -> ${specifier}`, job);
dependencyJobs[idx] = job; if (phase === kEvaluationPhase) {
evaluationDepJobs[eidx] = job;
}
return job.modulePromise; return job.modulePromise;
}); });
modulePromises[idx] = modulePromise; modulePromises[idx] = modulePromise;
@ -149,17 +177,17 @@ class ModuleJob extends ModuleJobBase {
const modules = await SafePromiseAllReturnArrayLike(modulePromises); const modules = await SafePromiseAllReturnArrayLike(modulePromises);
this.module.link(specifiers, modules); this.module.link(specifiers, modules);
return dependencyJobs; return evaluationDepJobs;
} }
instantiate() { #instantiate() {
if (this.instantiated === undefined) { if (this.instantiated === undefined) {
this.instantiated = this._instantiate(); this.instantiated = this.#_instantiate();
} }
return this.instantiated; return this.instantiated;
} }
async _instantiate() { async #_instantiate() {
const jobsInGraph = new SafeSet(); const jobsInGraph = new SafeSet();
const addJobsToDependencyGraph = async (moduleJob) => { const addJobsToDependencyGraph = async (moduleJob) => {
debug(`async addJobsToDependencyGraph() ${this.url}`, moduleJob); debug(`async addJobsToDependencyGraph() ${this.url}`, moduleJob);
@ -247,6 +275,7 @@ class ModuleJob extends ModuleJobBase {
} }
runSync() { runSync() {
assert(this.phase === kEvaluationPhase);
assert(this.module instanceof ModuleWrap); assert(this.module instanceof ModuleWrap);
if (this.instantiated !== undefined) { if (this.instantiated !== undefined) {
return { __proto__: null, module: this.module }; return { __proto__: null, module: this.module };
@ -262,7 +291,8 @@ class ModuleJob extends ModuleJobBase {
} }
async run(isEntryPoint = false) { async run(isEntryPoint = false) {
await this.instantiate(); assert(this.phase === kEvaluationPhase);
await this.#instantiate();
if (isEntryPoint) { if (isEntryPoint) {
globalThis[entry_point_module_private_symbol] = this.module; globalThis[entry_point_module_private_symbol] = this.module;
} }
@ -317,40 +347,64 @@ class ModuleJobSync extends ModuleJobBase {
* @param {string} url URL of the module to be wrapped in ModuleJob. * @param {string} url URL of the module to be wrapped in ModuleJob.
* @param {ImportAttributes} importAttributes Import attributes from the import statement. * @param {ImportAttributes} importAttributes Import attributes from the import statement.
* @param {ModuleWrap} moduleWrap Translated ModuleWrap for the module. * @param {ModuleWrap} moduleWrap Translated ModuleWrap for the module.
* @param {number} phase The phase to load the module to.
* @param {boolean} isMain Whether the module is the entry point. * @param {boolean} isMain Whether the module is the entry point.
* @param {boolean} inspectBrk Whether this module should be evaluated with the * @param {boolean} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed). * first line paused in the debugger (because --inspect-brk is passed).
*/ */
constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) { constructor(loader, url, importAttributes, moduleWrap, phase = kEvaluationPhase, isMain,
super(url, importAttributes, isMain, inspectBrk, true); inspectBrk) {
super(url, importAttributes, phase, isMain, inspectBrk, true);
this.#loader = loader; this.#loader = loader;
this.module = moduleWrap; this.module = moduleWrap;
assert(this.module instanceof ModuleWrap); assert(this.module instanceof ModuleWrap);
this.linked = undefined;
this.type = importAttributes.type;
if (phase === kEvaluationPhase) {
this.#link();
}
}
/**
* Ensure that this ModuleJob is at the required phase
* @param {number} phase
*/
ensurePhase(phase) {
if (this.phase < phase) {
this.phase = phase;
this.#link();
}
}
#link() {
// Store itself into the cache first before linking in case there are circular // Store itself into the cache first before linking in case there are circular
// references in the linking. // references in the linking.
loader.loadCache.set(url, importAttributes.type, this); this.#loader.loadCache.set(this.url, this.type, this);
try { try {
const moduleRequests = this.module.getModuleRequests(); const moduleRequests = this.module.getModuleRequests();
// Specifiers should be aligned with the moduleRequests array in order. // Specifiers should be aligned with the moduleRequests array in order.
const specifiers = Array(moduleRequests.length); const specifiers = Array(moduleRequests.length);
const modules = Array(moduleRequests.length); const modules = Array(moduleRequests.length);
const jobs = Array(moduleRequests.length); const evaluationDepJobs = Array(moduleRequests.length);
let j = 0;
for (let i = 0; i < moduleRequests.length; ++i) { for (let i = 0; i < moduleRequests.length; ++i) {
const { specifier, attributes } = moduleRequests[i]; const { specifier, attributes, phase } = moduleRequests[i];
const job = this.#loader.getModuleJobForRequire(specifier, url, attributes); const job = this.#loader.getModuleJobForRequire(specifier, this.url, attributes, phase);
specifiers[i] = specifier; specifiers[i] = specifier;
modules[i] = job.module; modules[i] = job.module;
jobs[i] = job; if (phase === kEvaluationPhase) {
evaluationDepJobs[j++] = job;
}
} }
evaluationDepJobs.length = j;
this.module.link(specifiers, modules); this.module.link(specifiers, modules);
this.linked = jobs; this.linked = evaluationDepJobs;
} finally { } finally {
// Restore it - if it succeeds, we'll reset in the caller; Otherwise it's // Restore it - if it succeeds, we'll reset in the caller; Otherwise it's
// not cached and if the error is caught, subsequent attempt would still fail. // not cached and if the error is caught, subsequent attempt would still fail.
loader.loadCache.delete(url, importAttributes.type); this.#loader.loadCache.delete(this.url, this.type);
} }
} }
@ -359,6 +413,7 @@ class ModuleJobSync extends ModuleJobBase {
} }
async run() { async run() {
assert(this.phase === kEvaluationPhase);
// This path is hit by a require'd module that is imported again. // This path is hit by a require'd module that is imported again.
const status = this.module.getStatus(); const status = this.module.getStatus();
if (status > kInstantiated) { if (status > kInstantiated) {
@ -382,6 +437,7 @@ class ModuleJobSync extends ModuleJobBase {
} }
runSync(parent) { runSync(parent) {
assert(this.phase === kEvaluationPhase);
// TODO(joyeecheung): add the error decoration logic from the async instantiate. // TODO(joyeecheung): add the error decoration logic from the async instantiate.
this.module.async = this.module.instantiateSync(); this.module.async = this.module.instantiateSync();
// If --experimental-print-required-tla is true, proceeds to evaluation even // If --experimental-print-required-tla is true, proceeds to evaluation even

View File

@ -506,12 +506,16 @@ translators.set('wasm', async function(url, source) {
const createDynamicModule = require( const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module'); 'internal/modules/esm/create_dynamic_module');
return createDynamicModule(imports, exports, url, (reflect) => { const { module } = createDynamicModule(imports, exports, url, (reflect) => {
const { exports } = new WebAssembly.Instance(compiled, reflect.imports); const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
for (const expt of ObjectKeys(exports)) { for (const expt of ObjectKeys(exports)) {
reflect.exports[expt].set(exports[expt]); reflect.exports[expt].set(exports[expt]);
} }
}).module; });
// WebAssembly modules support source phase imports, to import the compiled module
// separate from the linked instance.
module.setModuleSourceObject(compiled);
return module;
}); });
// Strategy for loading a addon // Strategy for loading a addon

View File

@ -20,7 +20,11 @@ const {
vm_dynamic_import_no_callback, vm_dynamic_import_no_callback,
} = internalBinding('symbols'); } = internalBinding('symbols');
const { ModuleWrap } = internalBinding('module_wrap'); const {
ModuleWrap,
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback,
} = internalBinding('module_wrap');
const { const {
maybeCacheSourceMap, maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache'); } = require('internal/source_map/source_map_cache');
@ -39,10 +43,6 @@ const {
emitExperimentalWarning, emitExperimentalWarning,
getCWDURL, getCWDURL,
} = require('internal/util'); } = require('internal/util');
const {
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback,
} = internalBinding('module_wrap');
const assert = require('internal/assert'); const assert = require('internal/assert');
const { const {
normalizeReferrerURL, normalizeReferrerURL,
@ -112,6 +112,7 @@ function getConditionsSet(conditions) {
* @param {string} specifier * @param {string} specifier
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer * @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
* @param {Record<string, string>} attributes * @param {Record<string, string>} attributes
* @param {number} phase
* @returns {Promise<ModuleNamespaceObject>} * @returns {Promise<ModuleNamespaceObject>}
*/ */
@ -212,58 +213,62 @@ function initializeImportMetaObject(symbol, meta, wrap) {
/** /**
* Proxy the dynamic import handling to the default loader for source text modules. * Proxy the dynamic import handling to the default loader for source text modules.
* @param {string} specifier - The module specifier string. * @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object. * @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer. * @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object. * @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
*/ */
function defaultImportModuleDynamicallyForModule(specifier, attributes, referrerName) { function defaultImportModuleDynamicallyForModule(specifier, phase, attributes, referrerName) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, referrerName, attributes); return cascadedLoader.import(specifier, referrerName, attributes, phase);
} }
/** /**
* Proxy the dynamic import to the default loader for classic scripts. * Proxy the dynamic import to the default loader for classic scripts.
* @param {string} specifier - The module specifier string. * @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object. * @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer. * @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object. * @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
*/ */
function defaultImportModuleDynamicallyForScript(specifier, attributes, referrerName) { function defaultImportModuleDynamicallyForScript(specifier, phase, attributes, referrerName) {
const parentURL = normalizeReferrerURL(referrerName); const parentURL = normalizeReferrerURL(referrerName);
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, parentURL, attributes); return cascadedLoader.import(specifier, parentURL, attributes, phase);
} }
/** /**
* Asynchronously imports a module dynamically using a callback function. The native callback. * Asynchronously imports a module dynamically using a callback function. The native callback.
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object. * @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
* @param {string} specifier - The module specifier string. * @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object. * @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer. * @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object. * @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing. * @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
*/ */
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes, referrerName) { async function importModuleDynamicallyCallback(referrerSymbol, specifier, phase, attributes,
referrerName) {
// For user-provided vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, emit the warning // For user-provided vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, emit the warning
// and fall back to the default loader. // and fall back to the default loader.
if (referrerSymbol === vm_dynamic_import_main_context_default) { if (referrerSymbol === vm_dynamic_import_main_context_default) {
emitExperimentalWarning('vm.USE_MAIN_CONTEXT_DEFAULT_LOADER'); emitExperimentalWarning('vm.USE_MAIN_CONTEXT_DEFAULT_LOADER');
return defaultImportModuleDynamicallyForScript(specifier, attributes, referrerName); return defaultImportModuleDynamicallyForScript(specifier, phase, attributes, referrerName);
} }
// For script compiled internally that should use the default loader to handle dynamic // For script compiled internally that should use the default loader to handle dynamic
// import, proxy the request to the default loader without the warning. // import, proxy the request to the default loader without the warning.
if (referrerSymbol === vm_dynamic_import_default_internal) { if (referrerSymbol === vm_dynamic_import_default_internal) {
return defaultImportModuleDynamicallyForScript(specifier, attributes, referrerName); return defaultImportModuleDynamicallyForScript(specifier, phase, attributes, referrerName);
} }
// For SourceTextModules compiled internally, proxy the request to the default loader. // For SourceTextModules compiled internally, proxy the request to the default loader.
if (referrerSymbol === source_text_module_default_hdo) { if (referrerSymbol === source_text_module_default_hdo) {
return defaultImportModuleDynamicallyForModule(specifier, attributes, referrerName); return defaultImportModuleDynamicallyForModule(specifier, phase, attributes, referrerName);
} }
if (moduleRegistries.has(referrerSymbol)) { if (moduleRegistries.has(referrerSymbol)) {
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol); const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
if (importModuleDynamically !== undefined) { if (importModuleDynamically !== undefined) {
return importModuleDynamically(specifier, callbackReferrer, attributes); return importModuleDynamically(specifier, callbackReferrer, attributes, phase);
} }
} }
if (referrerSymbol === vm_dynamic_import_missing_flag) { if (referrerSymbol === vm_dynamic_import_missing_flag) {

View File

@ -156,7 +156,7 @@ function executeUserEntryPoint(main = process.argv[1]) {
runEntryPointWithESMLoader((cascadedLoader) => { runEntryPointWithESMLoader((cascadedLoader) => {
// Note that if the graph contains unsettled TLA, this may never resolve // Note that if the graph contains unsettled TLA, this may never resolve
// even after the event loop stops running. // even after the event loop stops running.
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, true); return cascadedLoader.import(mainURL, undefined, { __proto__: null }, undefined, true);
}); });
} }
} }

View File

@ -19,6 +19,10 @@ const {
} = require('internal/errors'); } = require('internal/errors');
const { pathToFileURL } = require('internal/url'); const { pathToFileURL } = require('internal/url');
const { exitCodes: { kGenericUserError } } = internalBinding('errors'); const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const {
kSourcePhase,
kEvaluationPhase,
} = internalBinding('module_wrap');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript'); const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const { const {
@ -379,9 +383,10 @@ function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, sho
*/ */
function compileScript(name, body, baseUrl) { function compileScript(name, body, baseUrl) {
const hostDefinedOptionId = Symbol(name); const hostDefinedOptionId = Symbol(name);
async function importModuleDynamically(specifier, _, importAttributes) { async function importModuleDynamically(specifier, _, importAttributes, phase) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, baseUrl, importAttributes); return cascadedLoader.import(specifier, baseUrl, importAttributes,
phase === 'source' ? kSourcePhase : kEvaluationPhase);
} }
return makeContextifyScript( return makeContextifyScript(
body, // code body, // code

View File

@ -31,6 +31,15 @@ const {
}, },
} = internalBinding('util'); } = internalBinding('util');
/**
* @callback VmImportModuleDynamicallyCallback
* @param {string} specifier
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
* @param {Record<string, string>} attributes
* @param {string} phase
* @returns { Promise<void> }
*/
/** /**
* Checks if the given object is a context object. * Checks if the given object is a context object.
* @param {object} object - The object to check. * @param {object} object - The object to check.
@ -42,10 +51,10 @@ function isContext(object) {
/** /**
* Retrieves the host-defined option ID based on the provided importModuleDynamically and hint. * Retrieves the host-defined option ID based on the provided importModuleDynamically and hint.
* @param {import('internal/modules/esm/utils').ImportModuleDynamicallyCallback | undefined} importModuleDynamically - * @param {VmImportModuleDynamicallyCallback | undefined} importModuleDynamically -
* The importModuleDynamically function or undefined. * The importModuleDynamically function or undefined.
* @param {string} hint - The hint for the option ID. * @param {string} hint - The hint for the option ID.
* @returns {symbol | import('internal/modules/esm/utils').ImportModuleDynamicallyCallback} - The host-defined option * @returns {symbol | VmImportModuleDynamicallyCallback} - The host-defined option
* ID. * ID.
*/ */
function getHostDefinedOptionId(importModuleDynamically, hint) { function getHostDefinedOptionId(importModuleDynamically, hint) {
@ -82,7 +91,7 @@ function getHostDefinedOptionId(importModuleDynamically, hint) {
/** /**
* Registers a dynamically imported module for customization. * Registers a dynamically imported module for customization.
* @param {string} referrer - The path of the referrer module. * @param {string} referrer - The path of the referrer module.
* @param {import('internal/modules/esm/utils').ImportModuleDynamicallyCallback} importModuleDynamically - The * @param {VmImportModuleDynamicallyCallback} importModuleDynamically - The
* dynamically imported module function to be registered. * dynamically imported module function to be registered.
*/ */
function registerImportModuleDynamically(referrer, importModuleDynamically) { function registerImportModuleDynamically(referrer, importModuleDynamically) {
@ -115,7 +124,7 @@ function registerImportModuleDynamically(referrer, importModuleDynamically) {
* @param {object[]} [contextExtensions=[]] - An array of context extensions to use for the compiled function. * @param {object[]} [contextExtensions=[]] - An array of context extensions to use for the compiled function.
* @param {string[]} [params] - An optional array of parameter names for the compiled function. * @param {string[]} [params] - An optional array of parameter names for the compiled function.
* @param {symbol} hostDefinedOptionId - A symbol referenced by the field `host_defined_option_symbol`. * @param {symbol} hostDefinedOptionId - A symbol referenced by the field `host_defined_option_symbol`.
* @param {import('internal/modules/esm/utils').ImportModuleDynamicallyCallback} [importModuleDynamically] - * @param {VmImportModuleDynamicallyCallback} [importModuleDynamically] -
* A function to use for dynamically importing modules. * A function to use for dynamically importing modules.
* @returns {object} An object containing the compiled function and any associated data. * @returns {object} An object containing the compiled function and any associated data.
* @throws {TypeError} If any of the arguments are of the wrong type. * @throws {TypeError} If any of the arguments are of the wrong type.

View File

@ -61,6 +61,7 @@ const {
kEvaluating, kEvaluating,
kEvaluated, kEvaluated,
kErrored, kErrored,
kSourcePhase,
} = binding; } = binding;
const STATUS_MAP = { const STATUS_MAP = {
@ -431,10 +432,26 @@ class SyntheticModule extends Module {
} }
} }
/**
* @callback ImportModuleDynamicallyCallback
* @param {string} specifier
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
* @param {Record<string, string>} attributes
* @param {number} phase
* @returns { Promise<void> }
*/
/**
* @param {import('internal/vm').VmImportModuleDynamicallyCallback} importModuleDynamically
* @returns {ImportModuleDynamicallyCallback}
*/
function importModuleDynamicallyWrap(importModuleDynamically) { function importModuleDynamicallyWrap(importModuleDynamically) {
const importModuleDynamicallyWrapper = async (...args) => { const importModuleDynamicallyWrapper = async (specifier, referrer, attributes, phase) => {
const m = await ReflectApply(importModuleDynamically, this, args); const phaseString = phase === kSourcePhase ? 'source' : 'evaluation';
const m = await ReflectApply(importModuleDynamically, this, [specifier, referrer, attributes,
phaseString]);
if (isModuleNamespaceObject(m)) { if (isModuleNamespaceObject(m)) {
if (phase === kSourcePhase) throw new ERR_VM_MODULE_NOT_MODULE();
return m; return m;
} }
if (!isModule(m)) { if (!isModule(m)) {
@ -443,6 +460,8 @@ function importModuleDynamicallyWrap(importModuleDynamically) {
if (m.status === 'errored') { if (m.status === 'errored') {
throw m.error; throw m.error;
} }
if (phase === kSourcePhase)
return m[kWrap].getModuleSourceObject();
return m.namespace; return m.namespace;
}; };
return importModuleDynamicallyWrapper; return importModuleDynamicallyWrapper;

View File

@ -457,9 +457,11 @@ function REPLServer(prompt,
} catch { } catch {
// Continue regardless of error. // Continue regardless of error.
} }
async function importModuleDynamically(specifier, _, importAttributes) { async function importModuleDynamically(specifier, _, importAttributes, phase) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, parentURL, importAttributes); return cascadedLoader.import(specifier, parentURL, importAttributes,
phase === 'evaluation' ? cascadedLoader.kEvaluationPhase :
cascadedLoader.kSourcePhase);
} }
// `experimentalREPLAwait` is set to true by default. // `experimentalREPLAwait` is set to true by default.
// Shall be false in case `--no-experimental-repl-await` flag is used. // Shall be false in case `--no-experimental-repl-await` flag is used.

View File

@ -295,6 +295,7 @@
V(pathname_string, "pathname") \ V(pathname_string, "pathname") \
V(pending_handle_string, "pendingHandle") \ V(pending_handle_string, "pendingHandle") \
V(permission_string, "permission") \ V(permission_string, "permission") \
V(phase_string, "phase") \
V(pid_string, "pid") \ V(pid_string, "pid") \
V(ping_rtt_string, "pingRTT") \ V(ping_rtt_string, "pingRTT") \
V(pipe_source_string, "pipeSource") \ V(pipe_source_string, "pipeSource") \

View File

@ -44,6 +44,7 @@ using v8::MemorySpan;
using v8::Message; using v8::Message;
using v8::MicrotaskQueue; using v8::MicrotaskQueue;
using v8::Module; using v8::Module;
using v8::ModuleImportPhase;
using v8::ModuleRequest; using v8::ModuleRequest;
using v8::Name; using v8::Name;
using v8::Null; using v8::Null;
@ -73,6 +74,8 @@ ModuleWrap::ModuleWrap(Realm* realm,
object->SetInternalField(kModuleSlot, module); object->SetInternalField(kModuleSlot, module);
object->SetInternalField(kURLSlot, url); object->SetInternalField(kURLSlot, url);
object->SetInternalField(kModuleSourceObjectSlot,
v8::Undefined(realm->isolate()));
object->SetInternalField(kSyntheticEvaluationStepsSlot, object->SetInternalField(kSyntheticEvaluationStepsSlot,
synthetic_evaluation_step); synthetic_evaluation_step);
object->SetInternalField(kContextObjectSlot, context_object); object->SetInternalField(kContextObjectSlot, context_object);
@ -410,6 +413,16 @@ MaybeLocal<Module> ModuleWrap::CompileSourceTextModule(
return scope.Escape(module); return scope.Escape(module);
} }
ModulePhase to_phase_constant(ModuleImportPhase phase) {
switch (phase) {
case ModuleImportPhase::kEvaluation:
return kEvaluationPhase;
case ModuleImportPhase::kSource:
return kSourcePhase;
}
UNREACHABLE();
}
static Local<Object> createImportAttributesContainer( static Local<Object> createImportAttributesContainer(
Realm* realm, Realm* realm,
Isolate* isolate, Isolate* isolate,
@ -445,14 +458,17 @@ static Local<Array> createModuleRequestsContainer(
Local<FixedArray> raw_attributes = module_request->GetImportAttributes(); Local<FixedArray> raw_attributes = module_request->GetImportAttributes();
Local<Object> attributes = Local<Object> attributes =
createImportAttributesContainer(realm, isolate, raw_attributes, 3); createImportAttributesContainer(realm, isolate, raw_attributes, 3);
ModuleImportPhase phase = module_request->GetPhase();
Local<Name> names[] = { Local<Name> names[] = {
realm->isolate_data()->specifier_string(), realm->isolate_data()->specifier_string(),
realm->isolate_data()->attributes_string(), realm->isolate_data()->attributes_string(),
realm->isolate_data()->phase_string(),
}; };
Local<Value> values[] = { Local<Value> values[] = {
specifier, specifier,
attributes, attributes,
Integer::New(isolate, to_phase_constant(phase)),
}; };
DCHECK_EQ(arraysize(names), arraysize(values)); DCHECK_EQ(arraysize(names), arraysize(values));
@ -525,7 +541,8 @@ void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) {
Local<Context> context = obj->context(); Local<Context> context = obj->context();
Local<Module> module = obj->module_.Get(isolate); Local<Module> module = obj->module_.Get(isolate);
TryCatchScope try_catch(realm->env()); TryCatchScope try_catch(realm->env());
USE(module->InstantiateModule(context, ResolveModuleCallback)); USE(module->InstantiateModule(
context, ResolveModuleCallback, ResolveSourceCallback));
// clear resolve cache on instantiate // clear resolve cache on instantiate
obj->resolve_cache_.clear(); obj->resolve_cache_.clear();
@ -631,7 +648,8 @@ void ModuleWrap::InstantiateSync(const FunctionCallbackInfo<Value>& args) {
{ {
TryCatchScope try_catch(env); TryCatchScope try_catch(env);
USE(module->InstantiateModule(context, ResolveModuleCallback)); USE(module->InstantiateModule(
context, ResolveModuleCallback, ResolveSourceCallback));
// clear resolve cache on instantiate // clear resolve cache on instantiate
obj->resolve_cache_.clear(); obj->resolve_cache_.clear();
@ -777,6 +795,40 @@ void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result); args.GetReturnValue().Set(result);
} }
void ModuleWrap::SetModuleSourceObject(
const FunctionCallbackInfo<Value>& args) {
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsObject());
CHECK(obj->object()
->GetInternalField(kModuleSourceObjectSlot)
.As<Value>()
->IsUndefined());
obj->object()->SetInternalField(kModuleSourceObjectSlot, args[0]);
}
void ModuleWrap::GetModuleSourceObject(
const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
CHECK_EQ(args.Length(), 0);
Local<Value> module_source_object =
obj->object()->GetInternalField(kModuleSourceObjectSlot).As<Value>();
if (module_source_object->IsUndefined()) {
Local<String> url = obj->object()->GetInternalField(kURLSlot).As<String>();
THROW_ERR_SOURCE_PHASE_NOT_DEFINED(isolate, url);
return;
}
args.GetReturnValue().Set(module_source_object);
}
void ModuleWrap::GetStatus(const FunctionCallbackInfo<Value>& args) { void ModuleWrap::GetStatus(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate(); Isolate* isolate = args.GetIsolate();
ModuleWrap* obj; ModuleWrap* obj;
@ -837,11 +889,63 @@ MaybeLocal<Module> ModuleWrap::ResolveModuleCallback(
return module->module_.Get(isolate); return module->module_.Get(isolate);
} }
static MaybeLocal<Promise> ImportModuleDynamically( MaybeLocal<Object> ModuleWrap::ResolveSourceCallback(
Local<Context> context,
Local<String> specifier,
Local<FixedArray> import_attributes,
Local<Module> referrer) {
Isolate* isolate = context->GetIsolate();
Environment* env = Environment::GetCurrent(context);
if (env == nullptr) {
THROW_ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE(isolate);
return MaybeLocal<Object>();
}
Utf8Value specifier_utf8(isolate, specifier);
std::string specifier_std(*specifier_utf8, specifier_utf8.length());
ModuleWrap* dependent = GetFromModule(env, referrer);
if (dependent == nullptr) {
THROW_ERR_VM_MODULE_LINK_FAILURE(
env, "request for '%s' is from invalid module", specifier_std);
return MaybeLocal<Object>();
}
if (dependent->resolve_cache_.count(specifier_std) != 1) {
THROW_ERR_VM_MODULE_LINK_FAILURE(
env, "request for '%s' is not in cache", specifier_std);
return MaybeLocal<Object>();
}
Local<Object> module_object =
dependent->resolve_cache_[specifier_std].Get(isolate);
if (module_object.IsEmpty() || !module_object->IsObject()) {
THROW_ERR_VM_MODULE_LINK_FAILURE(
env, "request for '%s' did not return an object", specifier_std);
return MaybeLocal<Object>();
}
ModuleWrap* module;
ASSIGN_OR_RETURN_UNWRAP(&module, module_object, MaybeLocal<Object>());
Local<Value> module_source_object =
module->object()->GetInternalField(kModuleSourceObjectSlot).As<Value>();
if (module_source_object->IsUndefined()) {
Local<String> url =
module->object()->GetInternalField(kURLSlot).As<String>();
THROW_ERR_SOURCE_PHASE_NOT_DEFINED(isolate, url);
return MaybeLocal<Object>();
}
CHECK(module_source_object->IsObject());
return module_source_object.As<Object>();
}
static MaybeLocal<Promise> ImportModuleDynamicallyWithPhase(
Local<Context> context, Local<Context> context,
Local<Data> host_defined_options, Local<Data> host_defined_options,
Local<Value> resource_name, Local<Value> resource_name,
Local<String> specifier, Local<String> specifier,
ModuleImportPhase phase,
Local<FixedArray> import_attributes) { Local<FixedArray> import_attributes) {
Isolate* isolate = context->GetIsolate(); Isolate* isolate = context->GetIsolate();
Environment* env = Environment::GetCurrent(context); Environment* env = Environment::GetCurrent(context);
@ -879,6 +983,7 @@ static MaybeLocal<Promise> ImportModuleDynamically(
Local<Value> import_args[] = { Local<Value> import_args[] = {
id, id,
Local<Value>(specifier), Local<Value>(specifier),
Integer::New(isolate, to_phase_constant(phase)),
attributes, attributes,
resource_name, resource_name,
}; };
@ -896,6 +1001,20 @@ static MaybeLocal<Promise> ImportModuleDynamically(
return MaybeLocal<Promise>(); return MaybeLocal<Promise>();
} }
static MaybeLocal<Promise> ImportModuleDynamically(
Local<Context> context,
Local<Data> host_defined_options,
Local<Value> resource_name,
Local<String> specifier,
Local<FixedArray> import_attributes) {
return ImportModuleDynamicallyWithPhase(context,
host_defined_options,
resource_name,
specifier,
ModuleImportPhase::kEvaluation,
import_attributes);
}
void ModuleWrap::SetImportModuleDynamicallyCallback( void ModuleWrap::SetImportModuleDynamicallyCallback(
const FunctionCallbackInfo<Value>& args) { const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate(); Isolate* isolate = args.GetIsolate();
@ -908,6 +1027,10 @@ void ModuleWrap::SetImportModuleDynamicallyCallback(
realm->set_host_import_module_dynamically_callback(import_callback); realm->set_host_import_module_dynamically_callback(import_callback);
isolate->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically); isolate->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically);
// TODO(guybedford): Enable this once
// https://github.com/nodejs/node/pull/56842 lands.
// isolate->SetHostImportModuleWithPhaseDynamicallyCallback(
// ImportModuleDynamicallyWithPhase);
} }
void ModuleWrap::HostInitializeImportMetaObjectCallback( void ModuleWrap::HostInitializeImportMetaObjectCallback(
@ -1132,6 +1255,8 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
SetProtoMethod(isolate, tpl, "instantiate", Instantiate); SetProtoMethod(isolate, tpl, "instantiate", Instantiate);
SetProtoMethod(isolate, tpl, "evaluate", Evaluate); SetProtoMethod(isolate, tpl, "evaluate", Evaluate);
SetProtoMethod(isolate, tpl, "setExport", SetSyntheticExport); SetProtoMethod(isolate, tpl, "setExport", SetSyntheticExport);
SetProtoMethod(isolate, tpl, "setModuleSourceObject", SetModuleSourceObject);
SetProtoMethod(isolate, tpl, "getModuleSourceObject", GetModuleSourceObject);
SetProtoMethodNoSideEffect( SetProtoMethodNoSideEffect(
isolate, tpl, "createCachedData", CreateCachedData); isolate, tpl, "createCachedData", CreateCachedData);
SetProtoMethodNoSideEffect(isolate, tpl, "getNamespace", GetNamespace); SetProtoMethodNoSideEffect(isolate, tpl, "getNamespace", GetNamespace);
@ -1160,18 +1285,21 @@ void ModuleWrap::CreatePerContextProperties(Local<Object> target,
void* priv) { void* priv) {
Realm* realm = Realm::GetCurrent(context); Realm* realm = Realm::GetCurrent(context);
Isolate* isolate = realm->isolate(); Isolate* isolate = realm->isolate();
#define V(name) \ #define V(enum_type, name) \
target \ target \
->Set(context, \ ->Set(context, \
FIXED_ONE_BYTE_STRING(isolate, #name), \ FIXED_ONE_BYTE_STRING(isolate, #name), \
Integer::New(isolate, Module::Status::name)) \ Integer::New(isolate, enum_type::name)) \
.FromJust() .FromJust()
V(kUninstantiated); V(Module::Status, kUninstantiated);
V(kInstantiating); V(Module::Status, kInstantiating);
V(kInstantiated); V(Module::Status, kInstantiated);
V(kEvaluating); V(Module::Status, kEvaluating);
V(kEvaluated); V(Module::Status, kEvaluated);
V(kErrored); V(Module::Status, kErrored);
V(ModulePhase, kEvaluationPhase);
V(ModulePhase, kSourcePhase);
#undef V #undef V
} }
@ -1187,6 +1315,8 @@ void ModuleWrap::RegisterExternalReferences(
registry->Register(Instantiate); registry->Register(Instantiate);
registry->Register(Evaluate); registry->Register(Evaluate);
registry->Register(SetSyntheticExport); registry->Register(SetSyntheticExport);
registry->Register(SetModuleSourceObject);
registry->Register(GetModuleSourceObject);
registry->Register(CreateCachedData); registry->Register(CreateCachedData);
registry->Register(GetNamespace); registry->Register(GetNamespace);
registry->Register(GetStatus); registry->Register(GetStatus);

View File

@ -33,11 +33,17 @@ enum HostDefinedOptions : int {
kLength = 9, kLength = 9,
}; };
enum ModulePhase : int {
kSourcePhase = 1,
kEvaluationPhase = 2,
};
class ModuleWrap : public BaseObject { class ModuleWrap : public BaseObject {
public: public:
enum InternalFields { enum InternalFields {
kModuleSlot = BaseObject::kInternalFieldCount, kModuleSlot = BaseObject::kInternalFieldCount,
kURLSlot, kURLSlot,
kModuleSourceObjectSlot,
kSyntheticEvaluationStepsSlot, kSyntheticEvaluationStepsSlot,
kContextObjectSlot, // Object whose creation context is the target Context kContextObjectSlot, // Object whose creation context is the target Context
kInternalFieldCount kInternalFieldCount
@ -106,6 +112,10 @@ class ModuleWrap : public BaseObject {
static void InstantiateSync(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 EvaluateSync(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetNamespaceSync(const v8::FunctionCallbackInfo<v8::Value>& args); static void GetNamespaceSync(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetModuleSourceObject(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetModuleSourceObject(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void Link(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 Instantiate(const v8::FunctionCallbackInfo<v8::Value>& args);
@ -129,6 +139,11 @@ class ModuleWrap : public BaseObject {
v8::Local<v8::String> specifier, v8::Local<v8::String> specifier,
v8::Local<v8::FixedArray> import_attributes, v8::Local<v8::FixedArray> import_attributes,
v8::Local<v8::Module> referrer); v8::Local<v8::Module> referrer);
static v8::MaybeLocal<v8::Object> ResolveSourceCallback(
v8::Local<v8::Context> context,
v8::Local<v8::String> specifier,
v8::Local<v8::FixedArray> import_attributes,
v8::Local<v8::Module> referrer);
static ModuleWrap* GetFromModule(node::Environment*, v8::Local<v8::Module>); static ModuleWrap* GetFromModule(node::Environment*, v8::Local<v8::Module>);
v8::Global<v8::Module> module_; v8::Global<v8::Module> module_;

View File

@ -770,6 +770,10 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
env_opts->abort_on_uncaught_exception = true; env_opts->abort_on_uncaught_exception = true;
} }
if (env_opts->experimental_wasm_modules) {
v8_args.emplace_back("--js-source-phase-imports");
}
#ifdef __POSIX__ #ifdef __POSIX__
// Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the // Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the
// performance penalty of frequent EINTR wakeups when the profiler is running. // performance penalty of frequent EINTR wakeups when the profiler is running.

View File

@ -107,6 +107,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_REQUIRE_ASYNC_MODULE, Error) \ V(ERR_REQUIRE_ASYNC_MODULE, Error) \
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \
V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \
V(ERR_SOURCE_PHASE_NOT_DEFINED, SyntaxError) \
V(ERR_STRING_TOO_LONG, Error) \ V(ERR_STRING_TOO_LONG, Error) \
V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \ V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \
V(ERR_TLS_PSK_SET_IDENTITY_HINT_FAILED, Error) \ V(ERR_TLS_PSK_SET_IDENTITY_HINT_FAILED, Error) \
@ -278,6 +279,15 @@ inline v8::Local<v8::Object> ERR_BUFFER_TOO_LARGE(v8::Isolate* isolate) {
return ERR_BUFFER_TOO_LARGE(isolate, message); return ERR_BUFFER_TOO_LARGE(isolate, message);
} }
inline void THROW_ERR_SOURCE_PHASE_NOT_DEFINED(v8::Isolate* isolate,
v8::Local<v8::String> url) {
std::string message = std::string(*v8::String::Utf8Value(isolate, url));
return THROW_ERR_SOURCE_PHASE_NOT_DEFINED(
isolate,
"Source phase import object is not defined for module %s",
message.c_str());
}
inline v8::Local<v8::Object> ERR_STRING_TOO_LONG(v8::Isolate* isolate) { inline v8::Local<v8::Object> ERR_STRING_TOO_LONG(v8::Isolate* isolate) {
char message[128]; char message[128];
snprintf(message, sizeof(message), snprintf(message, sizeof(message),

View File

@ -1,6 +1,6 @@
import { spawnPromisified } from '../common/index.mjs'; import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs'; import * as fixtures from '../common/fixtures.mjs';
import { strictEqual, match } from 'node:assert'; import { ok, strictEqual, notStrictEqual, match } from 'node:assert';
import { execPath } from 'node:process'; import { execPath } from 'node:process';
import { describe, it } from 'node:test'; import { describe, it } from 'node:test';
@ -90,4 +90,161 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () =>
match(stderr, /ExperimentalWarning/); match(stderr, /ExperimentalWarning/);
match(stderr, /WebAssembly/); match(stderr, /WebAssembly/);
}); });
it('should support static source phase imports', async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--input-type=module',
'--eval',
[
'import { strictEqual } from "node:assert";',
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/wasm-source-phase.js'))};`,
'strictEqual(wasmExports.mod instanceof WebAssembly.Module, true);',
'const AbstractModuleSourceProto = Object.getPrototypeOf(Object.getPrototypeOf(wasmExports.mod));',
'const toStringTag = Object.getOwnPropertyDescriptor(AbstractModuleSourceProto, Symbol.toStringTag).get;',
'strictEqual(toStringTag.call(wasmExports.mod), "WebAssembly.Module");',
].join('\n'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
});
// TODO: Enable this once https://github.com/nodejs/node/pull/56842 lands.
it.skip('should support dynamic source phase imports', async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--input-type=module',
'--eval',
[
'import { strictEqual } from "node:assert";',
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/wasm-source-phase.js'))};`,
'strictEqual(wasmExports.mod instanceof WebAssembly.Module, true);',
'strictEqual(await wasmExports.dyn("./simple.wasm") instanceof WebAssembly.Module, true);',
'const AbstractModuleSourceProto = Object.getPrototypeOf(Object.getPrototypeOf(wasmExports.mod));',
'const toStringTag = Object.getOwnPropertyDescriptor(AbstractModuleSourceProto, Symbol.toStringTag).get;',
'strictEqual(toStringTag.call(wasmExports.mod), "WebAssembly.Module");',
].join('\n'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
});
it('should not execute source phase imports', async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--input-type=module',
'--eval',
[
'import { strictEqual } from "node:assert";',
`import source mod from ${JSON.stringify(fixtures.fileURL('es-modules/unimportable.wasm'))};`,
'assert.strictEqual(mod instanceof WebAssembly.Module, true);',
`await assert.rejects(import(${JSON.stringify(fixtures.fileURL('es-modules/unimportable.wasm'))}));`,
].join('\n'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
});
// TODO: Enable this once https://github.com/nodejs/node/pull/56842 lands.
it.skip('should not execute dynamic source phase imports', async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--input-type=module',
'--eval',
`await import.source(${JSON.stringify(fixtures.fileURL('es-modules/unimportable.wasm'))})`,
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
});
// TODO: Enable this once https://github.com/nodejs/node/pull/56842 lands.
it.skip('should throw for dynamic source phase imports not defined', async () => {
const fileUrl = fixtures.fileURL('es-modules/wasm-source-phase.js');
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--input-type=module',
'--eval',
[
'import { ok, strictEqual } from "node:assert";',
`await assert.rejects(import.source(${JSON.stringify(fileUrl)}), (e) => {`,
' strictEqual(e instanceof SyntaxError, true);',
' strictEqual(e.message.includes("Source phase import object is not defined for module"), true);',
` strictEqual(e.message.includes(${JSON.stringify(fileUrl)}), true);`,
'});',
].join('\n'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
});
it('should throw for static source phase imports not defined', async () => {
const fileUrl = fixtures.fileURL('es-modules/wasm-source-phase.js');
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--input-type=module',
'--eval',
`import source nosource from ${JSON.stringify(fileUrl)};`,
]);
match(stderr, /Source phase import object is not defined for module/);
ok(stderr.includes(fileUrl));
strictEqual(stdout, '');
notStrictEqual(code, 0);
});
it('should throw for vm source phase static import', async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--experimental-vm-modules',
'--input-type=module',
'--eval',
[
'const m1 = new vm.SourceTextModule("import source x from \\"y\\";");',
'const m2 = new vm.SourceTextModule("export var p = 5;");',
'await m1.link(() => m2);',
'await m1.evaluate();',
].join('\n'),
]);
match(stderr, /Source phase import object is not defined for module/);
strictEqual(stdout, '');
notStrictEqual(code, 0);
});
// TODO: Enable this once https://github.com/nodejs/node/pull/56842 lands.
it.skip('should throw for vm source phase dynamic import', async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-wasm-modules',
'--experimental-vm-modules',
'--input-type=module',
'--eval',
[
'import { constants } from "node:vm";',
'const opts = { importModuleDynamically: () => m2 };',
'const m1 = new vm.SourceTextModule("await import.source(\\"y\\");", opts);',
'const m2 = new vm.SourceTextModule("export var p = 5;");',
'await m1.link(() => m2);',
'await m1.evaluate();',
].join('\n'),
]);
match(stderr, /Source phase import object is not defined for module/);
strictEqual(stdout, '');
notStrictEqual(code, 0);
});
}); });

Binary file not shown.

View File

@ -0,0 +1,7 @@
import source mod from './simple.wasm';
export function dyn (specifier) {
return import.source(specifier);
}
export { mod };

View File

@ -59,10 +59,11 @@ async function test() {
{ {
const s = new Script('import("foo", { with: { key: "value" } })', { const s = new Script('import("foo", { with: { key: "value" } })', {
importModuleDynamically: common.mustCall((specifier, wrap, attributes) => { importModuleDynamically: common.mustCall((specifier, wrap, attributes, phase) => {
assert.strictEqual(specifier, 'foo'); assert.strictEqual(specifier, 'foo');
assert.strictEqual(wrap, s); assert.strictEqual(wrap, s);
assert.deepStrictEqual(attributes, { __proto__: null, key: 'value' }); assert.deepStrictEqual(attributes, { __proto__: null, key: 'value' });
assert.strictEqual(phase, 'evaluation');
return foo; return foo;
}), }),
}); });