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 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>
### `ERR_SQLITE_ERROR`

View File

@ -669,17 +669,19 @@ imported from the same path.
> Stability: 1 - Experimental
Importing WebAssembly modules is supported under the
`--experimental-wasm-modules` flag, allowing any `.wasm` files to be
imported as normal modules while also supporting their module imports.
Importing both WebAssembly module instances and WebAssembly source phase
imports are supported under the `--experimental-wasm-modules` flag.
This integration is in line with the
Both of these integrations are in line with the
[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
import * as M from './module.wasm';
import * as M from './library.wasm';
console.log(M);
```
@ -689,7 +691,35 @@ executed under:
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>
@ -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
[Module customization hooks]: module.md#customization-hooks
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
[Terminology]: #terminology
[URL]: https://url.spec.whatwg.org/
[`"exports"`]: packages.md#exports

View File

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

View File

@ -1508,7 +1508,7 @@ function loadESMFromCJS(mod, filename, format, source) {
if (isMain) {
require('internal/modules/run_main').runEntryPointWithESMLoader((cascadedLoader) => {
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.
setOwnProperty(process, 'mainModule', undefined);

View File

@ -38,7 +38,7 @@ const {
forceDefaultLoader,
} = require('internal/modules/esm/utils');
const { kImplicitTypeAttribute } = require('internal/modules/esm/assert');
const { ModuleWrap, kEvaluating, kEvaluated } = internalBinding('module_wrap');
const { ModuleWrap, kEvaluating, kEvaluated, kEvaluationPhase, kSourcePhase } = internalBinding('module_wrap');
const {
urlToFilename,
} = require('internal/modules/helpers');
@ -236,8 +236,7 @@ class ModuleLoader {
async executeModuleJob(url, wrap, isEntryPoint = false) {
const { ModuleJob } = require('internal/modules/esm/module_job');
const module = await onImport.tracePromise(async () => {
const job = new ModuleJob(
this, url, undefined, wrap, false, false);
const job = new ModuleJob(this, url, undefined, wrap, kEvaluationPhase, false, false);
this.loadCache.set(url, undefined, job);
const { module } = await job.run(isEntryPoint);
return module;
@ -273,11 +272,12 @@ class ModuleLoader {
* @param {string} [parentURL] The URL of the module where the module request is initiated.
* It's undefined if it's from the root module.
* @param {ImportAttributes} importAttributes Attributes from the import statement or expression.
* @param {number} phase Import phase.
* @returns {Promise<ModuleJobBase>}
*/
async getModuleJobForImport(specifier, parentURL, importAttributes) {
async getModuleJobForImport(specifier, parentURL, importAttributes, phase) {
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} [parentURL] See {@link getModuleJobForImport}
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
* @param {number} phase Import phase.
* @returns {Promise<ModuleJobBase>}
*/
getModuleJobForRequireInImportedCJS(specifier, parentURL, importAttributes) {
getModuleJobForRequireInImportedCJS(specifier, parentURL, importAttributes, phase) {
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 {string} [parentURL] 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.
* @returns {ModuleJobBase}
*/
#getJobFromResolveResult(resolveResult, parentURL, importAttributes, isForRequireInImportedCJS = false) {
#getJobFromResolveResult(resolveResult, parentURL, importAttributes, phase,
isForRequireInImportedCJS = false) {
const { url, format } = resolveResult;
const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
let job = this.loadCache.get(url, resolvedImportAttributes.type);
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;
@ -377,7 +383,7 @@ class ModuleLoader {
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
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);
mod[kRequiredModuleSymbol] = job.module;
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} parentURL Where the import comes from.
* @param {object} importAttributes import attributes from the import statement.
* @param {number} phase The import phase.
* @returns {ModuleJobBase}
*/
getModuleJobForRequire(specifier, parentURL, importAttributes) {
getModuleJobForRequire(specifier, parentURL, importAttributes, phase) {
const parsed = URLParse(specifier);
if (parsed != null) {
const protocol = parsed.protocol;
@ -422,6 +429,7 @@ class ModuleLoader {
}
throw new ERR_REQUIRE_CYCLE_MODULE(message);
}
job.ensurePhase(phase);
// 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
// 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 { 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);
return job;
@ -543,13 +551,14 @@ class ModuleLoader {
* 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 {ImportAttributes} importAttributes See {@link getModuleJobForImport}
* @param {number} phase Import phase.
* @param {string} [parentURL] See {@link getModuleJobForImport}
* @param {string} [format] The format hint possibly returned by the `resolve` hook
* @param {boolean} isForRequireInImportedCJS Whether this module job is created for require()
* in imported CJS.
* @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 isMain = parentURL === undefined;
@ -575,6 +584,7 @@ class ModuleLoader {
url,
importAttributes,
moduleOrModulePromise,
phase,
isMain,
inspectBrk,
isForRequireInImportedCJS,
@ -592,11 +602,18 @@ class ModuleLoader {
* @param {string} parentURL Path of the parent importing the module.
* @param {Record<string, string>} importAttributes Validations for the
* module import.
* @param {number} [phase] The phase of the import.
* @param {boolean} [isEntryPoint] Whether this is the realm-level entry point.
* @returns {Promise<ModuleExports>}
*/
async import(specifier, parentURL, importAttributes, isEntryPoint = false) {
async import(specifier, parentURL, importAttributes, phase = kEvaluationPhase, isEntryPoint = false) {
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);
return module.getNamespace();
}, {

View File

@ -22,7 +22,7 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
const { ModuleWrap, kInstantiated } = internalBinding('module_wrap');
const { ModuleWrap, kInstantiated, kEvaluationPhase } = internalBinding('module_wrap');
const {
privateSymbols: {
entry_point_module_private_symbol,
@ -59,8 +59,10 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
);
class ModuleJobBase {
constructor(url, importAttributes, isMain, inspectBrk) {
constructor(url, importAttributes, phase, isMain, inspectBrk) {
assert(typeof phase === 'number');
this.importAttributes = importAttributes;
this.phase = phase;
this.isMain = isMain;
this.inspectBrk = inspectBrk;
@ -78,14 +80,15 @@ class ModuleJob extends ModuleJobBase {
* @param {string} url URL of the module to be wrapped in ModuleJob.
* @param {ImportAttributes} importAttributes Import attributes from the import statement.
* @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} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed).
* @param {boolean} isForRequireInImportedCJS Whether this is created for require() in imported CJS.
*/
constructor(loader, url, importAttributes = { __proto__: null },
moduleOrModulePromise, isMain, inspectBrk, isForRequireInImportedCJS = false) {
super(url, importAttributes, isMain, inspectBrk);
constructor(loader, url, importAttributes = { __proto__: null }, moduleOrModulePromise,
phase = kEvaluationPhase, isMain, inspectBrk, isForRequireInImportedCJS = false) {
super(url, importAttributes, phase, isMain, inspectBrk);
this.#loader = loader;
// Expose the promise to the ModuleWrap directly for linking below.
@ -97,22 +100,37 @@ class ModuleJob extends ModuleJobBase {
this.modulePromise = moduleOrModulePromise;
}
// Promise for the list of all dependencyJobs.
this.linked = this._link();
// This promise is awaited later anyway, so silence
// 'unhandled rejection' warnings.
PromisePrototypeThen(this.linked, undefined, noop);
if (this.phase === kEvaluationPhase) {
// Promise for the list of all dependencyJobs.
this.linked = this.#link();
// This promise is awaited later anyway, so silence
// 'unhandled rejection' warnings.
PromisePrototypeThen(this.linked, undefined, noop);
}
// instantiated == deep dependency jobs wrappers are instantiated,
// and module wrapper is instantiated.
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.
* @returns {Promise<ModuleJob[]>} Dependency module jobs.
*/
async _link() {
async #link() {
this.module = await this.modulePromise;
assert(this.module instanceof ModuleWrap);
@ -123,23 +141,33 @@ class ModuleJob extends ModuleJobBase {
// these `link` callbacks depending on each other.
// Create an ArrayLike to avoid calling into userspace with `.then`
// when returned from the async function.
const dependencyJobs = Array(moduleRequests.length);
ObjectSetPrototypeOf(dependencyJobs, null);
const evaluationDepJobs = Array(moduleRequests.length);
ObjectSetPrototypeOf(evaluationDepJobs, null);
// Specifiers should be aligned with the moduleRequests array in order.
const specifiers = 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`.
for (let idx = 0; idx < moduleRequests.length; idx++) {
const { specifier, attributes } = moduleRequests[idx];
for (
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
// loop so that hooks can pre-fetch sources off-thread.
const dependencyJobPromise = this.#loader.getModuleJobForImport(
specifier, this.url, attributes,
specifier, this.url, attributes, phase,
);
const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
debug(`async link() ${this.url} -> ${specifier}`, job);
dependencyJobs[idx] = job;
if (phase === kEvaluationPhase) {
evaluationDepJobs[eidx] = job;
}
return job.modulePromise;
});
modulePromises[idx] = modulePromise;
@ -149,17 +177,17 @@ class ModuleJob extends ModuleJobBase {
const modules = await SafePromiseAllReturnArrayLike(modulePromises);
this.module.link(specifiers, modules);
return dependencyJobs;
return evaluationDepJobs;
}
instantiate() {
#instantiate() {
if (this.instantiated === undefined) {
this.instantiated = this._instantiate();
this.instantiated = this.#_instantiate();
}
return this.instantiated;
}
async _instantiate() {
async #_instantiate() {
const jobsInGraph = new SafeSet();
const addJobsToDependencyGraph = async (moduleJob) => {
debug(`async addJobsToDependencyGraph() ${this.url}`, moduleJob);
@ -247,6 +275,7 @@ class ModuleJob extends ModuleJobBase {
}
runSync() {
assert(this.phase === kEvaluationPhase);
assert(this.module instanceof ModuleWrap);
if (this.instantiated !== undefined) {
return { __proto__: null, module: this.module };
@ -262,7 +291,8 @@ class ModuleJob extends ModuleJobBase {
}
async run(isEntryPoint = false) {
await this.instantiate();
assert(this.phase === kEvaluationPhase);
await this.#instantiate();
if (isEntryPoint) {
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 {ImportAttributes} importAttributes Import attributes from the import statement.
* @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} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed).
*/
constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) {
super(url, importAttributes, isMain, inspectBrk, true);
constructor(loader, url, importAttributes, moduleWrap, phase = kEvaluationPhase, isMain,
inspectBrk) {
super(url, importAttributes, phase, isMain, inspectBrk, true);
this.#loader = loader;
this.module = 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
// references in the linking.
loader.loadCache.set(url, importAttributes.type, this);
this.#loader.loadCache.set(this.url, this.type, this);
try {
const moduleRequests = this.module.getModuleRequests();
// Specifiers should be aligned with the moduleRequests array in order.
const specifiers = 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) {
const { specifier, attributes } = moduleRequests[i];
const job = this.#loader.getModuleJobForRequire(specifier, url, attributes);
const { specifier, attributes, phase } = moduleRequests[i];
const job = this.#loader.getModuleJobForRequire(specifier, this.url, attributes, phase);
specifiers[i] = specifier;
modules[i] = job.module;
jobs[i] = job;
if (phase === kEvaluationPhase) {
evaluationDepJobs[j++] = job;
}
}
evaluationDepJobs.length = j;
this.module.link(specifiers, modules);
this.linked = jobs;
this.linked = evaluationDepJobs;
} finally {
// 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.
loader.loadCache.delete(url, importAttributes.type);
this.#loader.loadCache.delete(this.url, this.type);
}
}
@ -359,6 +413,7 @@ class ModuleJobSync extends ModuleJobBase {
}
async run() {
assert(this.phase === kEvaluationPhase);
// This path is hit by a require'd module that is imported again.
const status = this.module.getStatus();
if (status > kInstantiated) {
@ -382,6 +437,7 @@ class ModuleJobSync extends ModuleJobBase {
}
runSync(parent) {
assert(this.phase === kEvaluationPhase);
// TODO(joyeecheung): add the error decoration logic from the async instantiate.
this.module.async = this.module.instantiateSync();
// 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(
'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);
for (const expt of ObjectKeys(exports)) {
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

View File

@ -20,7 +20,11 @@ const {
vm_dynamic_import_no_callback,
} = internalBinding('symbols');
const { ModuleWrap } = internalBinding('module_wrap');
const {
ModuleWrap,
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback,
} = internalBinding('module_wrap');
const {
maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache');
@ -39,10 +43,6 @@ const {
emitExperimentalWarning,
getCWDURL,
} = require('internal/util');
const {
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback,
} = internalBinding('module_wrap');
const assert = require('internal/assert');
const {
normalizeReferrerURL,
@ -112,6 +112,7 @@ function getConditionsSet(conditions) {
* @param {string} specifier
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
* @param {Record<string, string>} attributes
* @param {number} phase
* @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.
* @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @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();
return cascadedLoader.import(specifier, referrerName, attributes);
return cascadedLoader.import(specifier, referrerName, attributes, phase);
}
/**
* Proxy the dynamic import to the default loader for classic scripts.
* @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @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 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.
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
* @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @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.
*/
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
// and fall back to the default loader.
if (referrerSymbol === vm_dynamic_import_main_context_default) {
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
// import, proxy the request to the default loader without the warning.
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.
if (referrerSymbol === source_text_module_default_hdo) {
return defaultImportModuleDynamicallyForModule(specifier, attributes, referrerName);
return defaultImportModuleDynamicallyForModule(specifier, phase, attributes, referrerName);
}
if (moduleRegistries.has(referrerSymbol)) {
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
if (importModuleDynamically !== undefined) {
return importModuleDynamically(specifier, callbackReferrer, attributes);
return importModuleDynamically(specifier, callbackReferrer, attributes, phase);
}
}
if (referrerSymbol === vm_dynamic_import_missing_flag) {

View File

@ -156,7 +156,7 @@ function executeUserEntryPoint(main = process.argv[1]) {
runEntryPointWithESMLoader((cascadedLoader) => {
// Note that if the graph contains unsettled TLA, this may never resolve
// 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');
const { pathToFileURL } = require('internal/url');
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const {
kSourcePhase,
kEvaluationPhase,
} = internalBinding('module_wrap');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const {
@ -379,9 +383,10 @@ function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, sho
*/
function compileScript(name, body, baseUrl) {
const hostDefinedOptionId = Symbol(name);
async function importModuleDynamically(specifier, _, importAttributes) {
async function importModuleDynamically(specifier, _, importAttributes, phase) {
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(
body, // code

View File

@ -31,6 +31,15 @@ const {
},
} = 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.
* @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.
* @param {import('internal/modules/esm/utils').ImportModuleDynamicallyCallback | undefined} importModuleDynamically -
* @param {VmImportModuleDynamicallyCallback | undefined} importModuleDynamically -
* The importModuleDynamically function or undefined.
* @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.
*/
function getHostDefinedOptionId(importModuleDynamically, hint) {
@ -82,7 +91,7 @@ function getHostDefinedOptionId(importModuleDynamically, hint) {
/**
* Registers a dynamically imported module for customization.
* @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.
*/
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 {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 {import('internal/modules/esm/utils').ImportModuleDynamicallyCallback} [importModuleDynamically] -
* @param {VmImportModuleDynamicallyCallback} [importModuleDynamically] -
* A function to use for dynamically importing modules.
* @returns {object} An object containing the compiled function and any associated data.
* @throws {TypeError} If any of the arguments are of the wrong type.

View File

@ -61,6 +61,7 @@ const {
kEvaluating,
kEvaluated,
kErrored,
kSourcePhase,
} = binding;
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) {
const importModuleDynamicallyWrapper = async (...args) => {
const m = await ReflectApply(importModuleDynamically, this, args);
const importModuleDynamicallyWrapper = async (specifier, referrer, attributes, phase) => {
const phaseString = phase === kSourcePhase ? 'source' : 'evaluation';
const m = await ReflectApply(importModuleDynamically, this, [specifier, referrer, attributes,
phaseString]);
if (isModuleNamespaceObject(m)) {
if (phase === kSourcePhase) throw new ERR_VM_MODULE_NOT_MODULE();
return m;
}
if (!isModule(m)) {
@ -443,6 +460,8 @@ function importModuleDynamicallyWrap(importModuleDynamically) {
if (m.status === 'errored') {
throw m.error;
}
if (phase === kSourcePhase)
return m[kWrap].getModuleSourceObject();
return m.namespace;
};
return importModuleDynamicallyWrapper;

View File

@ -457,9 +457,11 @@ function REPLServer(prompt,
} catch {
// Continue regardless of error.
}
async function importModuleDynamically(specifier, _, importAttributes) {
async function importModuleDynamically(specifier, _, importAttributes, phase) {
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.
// Shall be false in case `--no-experimental-repl-await` flag is used.

View File

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

View File

@ -44,6 +44,7 @@ using v8::MemorySpan;
using v8::Message;
using v8::MicrotaskQueue;
using v8::Module;
using v8::ModuleImportPhase;
using v8::ModuleRequest;
using v8::Name;
using v8::Null;
@ -73,6 +74,8 @@ ModuleWrap::ModuleWrap(Realm* realm,
object->SetInternalField(kModuleSlot, module);
object->SetInternalField(kURLSlot, url);
object->SetInternalField(kModuleSourceObjectSlot,
v8::Undefined(realm->isolate()));
object->SetInternalField(kSyntheticEvaluationStepsSlot,
synthetic_evaluation_step);
object->SetInternalField(kContextObjectSlot, context_object);
@ -410,6 +413,16 @@ MaybeLocal<Module> ModuleWrap::CompileSourceTextModule(
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(
Realm* realm,
Isolate* isolate,
@ -445,14 +458,17 @@ static Local<Array> createModuleRequestsContainer(
Local<FixedArray> raw_attributes = module_request->GetImportAttributes();
Local<Object> attributes =
createImportAttributesContainer(realm, isolate, raw_attributes, 3);
ModuleImportPhase phase = module_request->GetPhase();
Local<Name> names[] = {
realm->isolate_data()->specifier_string(),
realm->isolate_data()->attributes_string(),
realm->isolate_data()->phase_string(),
};
Local<Value> values[] = {
specifier,
attributes,
Integer::New(isolate, to_phase_constant(phase)),
};
DCHECK_EQ(arraysize(names), arraysize(values));
@ -525,7 +541,8 @@ void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) {
Local<Context> context = obj->context();
Local<Module> module = obj->module_.Get(isolate);
TryCatchScope try_catch(realm->env());
USE(module->InstantiateModule(context, ResolveModuleCallback));
USE(module->InstantiateModule(
context, ResolveModuleCallback, ResolveSourceCallback));
// clear resolve cache on instantiate
obj->resolve_cache_.clear();
@ -631,7 +648,8 @@ void ModuleWrap::InstantiateSync(const FunctionCallbackInfo<Value>& args) {
{
TryCatchScope try_catch(env);
USE(module->InstantiateModule(context, ResolveModuleCallback));
USE(module->InstantiateModule(
context, ResolveModuleCallback, ResolveSourceCallback));
// clear resolve cache on instantiate
obj->resolve_cache_.clear();
@ -777,6 +795,40 @@ void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
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) {
Isolate* isolate = args.GetIsolate();
ModuleWrap* obj;
@ -837,11 +889,63 @@ MaybeLocal<Module> ModuleWrap::ResolveModuleCallback(
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<Data> host_defined_options,
Local<Value> resource_name,
Local<String> specifier,
ModuleImportPhase phase,
Local<FixedArray> import_attributes) {
Isolate* isolate = context->GetIsolate();
Environment* env = Environment::GetCurrent(context);
@ -879,6 +983,7 @@ static MaybeLocal<Promise> ImportModuleDynamically(
Local<Value> import_args[] = {
id,
Local<Value>(specifier),
Integer::New(isolate, to_phase_constant(phase)),
attributes,
resource_name,
};
@ -896,6 +1001,20 @@ static MaybeLocal<Promise> ImportModuleDynamically(
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(
const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
@ -908,6 +1027,10 @@ void ModuleWrap::SetImportModuleDynamicallyCallback(
realm->set_host_import_module_dynamically_callback(import_callback);
isolate->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically);
// TODO(guybedford): Enable this once
// https://github.com/nodejs/node/pull/56842 lands.
// isolate->SetHostImportModuleWithPhaseDynamicallyCallback(
// ImportModuleDynamicallyWithPhase);
}
void ModuleWrap::HostInitializeImportMetaObjectCallback(
@ -1132,6 +1255,8 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
SetProtoMethod(isolate, tpl, "instantiate", Instantiate);
SetProtoMethod(isolate, tpl, "evaluate", Evaluate);
SetProtoMethod(isolate, tpl, "setExport", SetSyntheticExport);
SetProtoMethod(isolate, tpl, "setModuleSourceObject", SetModuleSourceObject);
SetProtoMethod(isolate, tpl, "getModuleSourceObject", GetModuleSourceObject);
SetProtoMethodNoSideEffect(
isolate, tpl, "createCachedData", CreateCachedData);
SetProtoMethodNoSideEffect(isolate, tpl, "getNamespace", GetNamespace);
@ -1160,18 +1285,21 @@ void ModuleWrap::CreatePerContextProperties(Local<Object> target,
void* priv) {
Realm* realm = Realm::GetCurrent(context);
Isolate* isolate = realm->isolate();
#define V(name) \
#define V(enum_type, name) \
target \
->Set(context, \
FIXED_ONE_BYTE_STRING(isolate, #name), \
Integer::New(isolate, Module::Status::name)) \
Integer::New(isolate, enum_type::name)) \
.FromJust()
V(kUninstantiated);
V(kInstantiating);
V(kInstantiated);
V(kEvaluating);
V(kEvaluated);
V(kErrored);
V(Module::Status, kUninstantiated);
V(Module::Status, kInstantiating);
V(Module::Status, kInstantiated);
V(Module::Status, kEvaluating);
V(Module::Status, kEvaluated);
V(Module::Status, kErrored);
V(ModulePhase, kEvaluationPhase);
V(ModulePhase, kSourcePhase);
#undef V
}
@ -1187,6 +1315,8 @@ void ModuleWrap::RegisterExternalReferences(
registry->Register(Instantiate);
registry->Register(Evaluate);
registry->Register(SetSyntheticExport);
registry->Register(SetModuleSourceObject);
registry->Register(GetModuleSourceObject);
registry->Register(CreateCachedData);
registry->Register(GetNamespace);
registry->Register(GetStatus);

View File

@ -33,11 +33,17 @@ enum HostDefinedOptions : int {
kLength = 9,
};
enum ModulePhase : int {
kSourcePhase = 1,
kEvaluationPhase = 2,
};
class ModuleWrap : public BaseObject {
public:
enum InternalFields {
kModuleSlot = BaseObject::kInternalFieldCount,
kURLSlot,
kModuleSourceObjectSlot,
kSyntheticEvaluationStepsSlot,
kContextObjectSlot, // Object whose creation context is the target Context
kInternalFieldCount
@ -106,6 +112,10 @@ class ModuleWrap : public BaseObject {
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 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 Instantiate(const v8::FunctionCallbackInfo<v8::Value>& args);
@ -129,6 +139,11 @@ class ModuleWrap : public BaseObject {
v8::Local<v8::String> specifier,
v8::Local<v8::FixedArray> import_attributes,
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>);
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;
}
if (env_opts->experimental_wasm_modules) {
v8_args.emplace_back("--js-source-phase-imports");
}
#ifdef __POSIX__
// Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the
// 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_SCRIPT_EXECUTION_INTERRUPTED, Error) \
V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \
V(ERR_SOURCE_PHASE_NOT_DEFINED, SyntaxError) \
V(ERR_STRING_TOO_LONG, Error) \
V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \
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);
}
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) {
char message[128];
snprintf(message, sizeof(message),

View File

@ -1,6 +1,6 @@
import { spawnPromisified } from '../common/index.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 { describe, it } from 'node:test';
@ -90,4 +90,161 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () =>
match(stderr, /ExperimentalWarning/);
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" } })', {
importModuleDynamically: common.mustCall((specifier, wrap, attributes) => {
importModuleDynamically: common.mustCall((specifier, wrap, attributes, phase) => {
assert.strictEqual(specifier, 'foo');
assert.strictEqual(wrap, s);
assert.deepStrictEqual(attributes, { __proto__: null, key: 'value' });
assert.strictEqual(phase, 'evaluation');
return foo;
}),
});