From 8b3ce055bae1c8a12271433470df906b6a722cf3 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 4 Feb 2025 11:31:10 -0800 Subject: [PATCH 1/2] esm: support source phase imports for WebAssembly --- doc/api/errors.md | 7 + doc/api/esm.md | 47 ++++- doc/api/vm.md | 1 + lib/internal/modules/cjs/loader.js | 2 +- lib/internal/modules/esm/loader.js | 47 +++-- lib/internal/modules/esm/module_job.js | 120 +++++++++--- lib/internal/modules/esm/translators.js | 8 +- lib/internal/modules/esm/utils.js | 33 ++-- lib/internal/modules/run_main.js | 2 +- lib/internal/process/execution.js | 9 +- lib/internal/vm.js | 17 +- lib/internal/vm/module.js | 23 ++- lib/repl.js | 6 +- src/env_properties.h | 1 + src/module_wrap.cc | 185 +++++++++++++++--- src/module_wrap.h | 15 ++ src/node.cc | 4 + src/node_errors.h | 10 + test/es-module/test-esm-wasm.mjs | 162 ++++++++++++++- test/fixtures/es-modules/unimportable.wasm | Bin 0 -> 30 bytes test/fixtures/es-modules/wasm-source-phase.js | 7 + .../parallel/test-vm-module-dynamic-import.js | 3 +- 22 files changed, 596 insertions(+), 113 deletions(-) create mode 100644 test/fixtures/es-modules/unimportable.wasm create mode 100644 test/fixtures/es-modules/wasm-source-phase.js diff --git a/doc/api/errors.md b/doc/api/errors.md index e02624f3675558..0817d2572b3ae5 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2724,6 +2724,13 @@ 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. + + +### `ERR_SOURCE_PHASE_NOT_DEFINED` + +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)`. + ### `ERR_SQLITE_ERROR` diff --git a/doc/api/esm.md b/doc/api/esm.md index b3cf2eade965f8..c8d6e23de5b477 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -667,17 +667,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); ``` @@ -687,7 +689,37 @@ 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 + + + +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 +}); +``` @@ -1124,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 diff --git a/doc/api/vm.md b/doc/api/vm.md index a5dd038070a498..c7ca77fac93673 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -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. diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 2e0492151a29bc..acec19221de5ff 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1501,7 +1501,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); diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index a3b437ade87c75..302011a4d2be7a 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -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} */ - 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} */ - 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; @@ -360,7 +366,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().namespace }; @@ -372,9 +378,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; @@ -405,6 +412,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 @@ -446,7 +454,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; @@ -526,13 +534,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; @@ -558,6 +567,7 @@ class ModuleLoader { url, importAttributes, moduleOrModulePromise, + phase, isMain, inspectBrk, isForRequireInImportedCJS, @@ -575,11 +585,18 @@ class ModuleLoader { * @param {string} parentURL Path of the parent importing the module. * @param {Record} 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} */ - 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(); }, { diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 846a336d27547e..0cdb78d2f0097d 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -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, @@ -58,8 +58,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; @@ -77,14 +79,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} 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. @@ -96,22 +99,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} Dependency module jobs. */ - async _link() { + async #link() { this.module = await this.modulePromise; assert(this.module instanceof ModuleWrap); @@ -122,23 +140,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; @@ -148,17 +176,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); @@ -246,6 +274,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 }; @@ -261,7 +290,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; } @@ -316,40 +346,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); } } @@ -358,6 +412,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) { @@ -381,6 +436,7 @@ class ModuleJobSync extends ModuleJobBase { } runSync() { + 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 diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 678659aacaad3e..c300b0305318bd 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -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 diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 99061e62976e7c..b0bd8a449e84f4 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -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, @@ -106,6 +106,7 @@ function getConditionsSet(conditions) { * @param {string} specifier * @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer * @param {Record} attributes + * @param {number} phase * @returns { Promise } */ @@ -206,58 +207,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} attributes - The import attributes object. * @param {string|null|undefined} referrerName - name of the referrer. * @returns {Promise} - 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} attributes - The import attributes object. * @param {string|null|undefined} referrerName - name of the referrer. * @returns {Promise} - 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} attributes - The import attributes object. * @param {string|null|undefined} referrerName - name of the referrer. * @returns {Promise} - 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) { diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 9c90f0f6d3e33b..044926b4138d79 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -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); }); } } diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index d4d7a604851ef1..c7f35759805c55 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -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 diff --git a/lib/internal/vm.js b/lib/internal/vm.js index 0b9865ea9a0cf6..7c28b640bd47f1 100644 --- a/lib/internal/vm.js +++ b/lib/internal/vm.js @@ -31,6 +31,15 @@ const { }, } = internalBinding('util'); +/** + * @callback VmImportModuleDynamicallyCallback + * @param {string} specifier + * @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer + * @param {Record} attributes + * @param {string} phase + * @returns { Promise } + */ + /** * 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. diff --git a/lib/internal/vm/module.js b/lib/internal/vm/module.js index 16f6422c9b8ee9..5c20f51cf481ad 100644 --- a/lib/internal/vm/module.js +++ b/lib/internal/vm/module.js @@ -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} attributes + * @param {number} phase + * @returns { Promise } + */ + +/** + * @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; diff --git a/lib/repl.js b/lib/repl.js index fd8b51b9547e8a..6cfceacac620f0 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -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. diff --git a/src/env_properties.h b/src/env_properties.h index 43725de9b51237..e64d92f4b54996 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -290,6 +290,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") \ diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 649ec428e2dd6f..460ec673262b23 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -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); @@ -102,8 +105,7 @@ Local ModuleWrap::context() const { return obj.As()->GetCreationContextChecked(); } -ModuleWrap* ModuleWrap::GetFromModule(Environment* env, - Local module) { +ModuleWrap* ModuleWrap::GetFromModule(Environment* env, Local module) { auto range = env->hash_to_module_map.equal_range(module->GetIdentityHash()); for (auto it = range.first; it != range.second; ++it) { if (it->second->module_ == module) { @@ -408,6 +410,16 @@ MaybeLocal ModuleWrap::CompileSourceTextModule( return scope.Escape(module); } +ModulePhase to_phase_constant(ModuleImportPhase phase) { + switch (phase) { + case ModuleImportPhase::kEvaluation: + return kEvaluationPhase; + case ModuleImportPhase::kSource: + return kSourcePhase; + } + CHECK(false); +} + static Local createImportAttributesContainer( Realm* realm, Isolate* isolate, @@ -443,14 +455,17 @@ static Local createModuleRequestsContainer( Local raw_attributes = module_request->GetImportAttributes(); Local attributes = createImportAttributesContainer(realm, isolate, raw_attributes, 3); + ModuleImportPhase phase = module_request->GetPhase(); Local names[] = { realm->isolate_data()->specifier_string(), realm->isolate_data()->attributes_string(), + realm->isolate_data()->phase_string(), }; Local values[] = { specifier, attributes, + v8::Integer::New(isolate, to_phase_constant(phase)), }; DCHECK_EQ(arraysize(names), arraysize(values)); @@ -523,7 +538,8 @@ void ModuleWrap::Instantiate(const FunctionCallbackInfo& args) { Local context = obj->context(); Local 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(); @@ -551,7 +567,7 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { ContextifyContext* contextify_context = obj->contextify_context_; MicrotaskQueue* microtask_queue = nullptr; if (contextify_context != nullptr) - microtask_queue = contextify_context->microtask_queue(); + microtask_queue = contextify_context->microtask_queue(); // module.evaluate(timeout, breakOnSigint) CHECK_EQ(args.Length(), 2); @@ -607,8 +623,7 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { } if (try_catch.HasCaught()) { - if (!try_catch.HasTerminated()) - try_catch.ReThrow(); + if (!try_catch.HasTerminated()) try_catch.ReThrow(); return; } @@ -626,7 +641,8 @@ void ModuleWrap::InstantiateSync(const FunctionCallbackInfo& 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(); @@ -772,6 +788,40 @@ void ModuleWrap::GetNamespace(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } +void ModuleWrap::SetModuleSourceObject( + const FunctionCallbackInfo& 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() + ->IsUndefined()); + + obj->object()->SetInternalField(kModuleSourceObjectSlot, args[0]); +} + +void ModuleWrap::GetModuleSourceObject( + const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + + CHECK_EQ(args.Length(), 0); + Local module_source_object = + obj->object()->GetInternalField(kModuleSourceObjectSlot).As(); + + if (module_source_object->IsUndefined()) { + Local url = obj->object()->GetInternalField(kURLSlot).As(); + THROW_ERR_SOURCE_PHASE_NOT_DEFINED(isolate, url); + return; + } + + args.GetReturnValue().Set(module_source_object); +} + void ModuleWrap::GetStatus(const FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); ModuleWrap* obj; @@ -832,11 +882,63 @@ MaybeLocal ModuleWrap::ResolveModuleCallback( return module->module_.Get(isolate); } -static MaybeLocal ImportModuleDynamically( +MaybeLocal ModuleWrap::ResolveSourceCallback( + Local context, + Local specifier, + Local import_attributes, + Local referrer) { + Isolate* isolate = context->GetIsolate(); + Environment* env = Environment::GetCurrent(context); + if (env == nullptr) { + THROW_ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE(isolate); + return MaybeLocal(); + } + + 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(); + } + + 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(); + } + + Local 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(); + } + + ModuleWrap* module; + ASSIGN_OR_RETURN_UNWRAP(&module, module_object, MaybeLocal()); + + Local module_source_object = + module->object()->GetInternalField(kModuleSourceObjectSlot).As(); + if (module_source_object->IsUndefined()) { + Local url = + module->object()->GetInternalField(kURLSlot).As(); + THROW_ERR_SOURCE_PHASE_NOT_DEFINED(isolate, url); + return MaybeLocal(); + } + CHECK(module_source_object->IsObject()); + return module_source_object.As(); +} + +static MaybeLocal ImportModuleDynamicallyWithPhase( Local context, Local host_defined_options, Local resource_name, Local specifier, + ModuleImportPhase phase, Local import_attributes) { Isolate* isolate = context->GetIsolate(); Environment* env = Environment::GetCurrent(context); @@ -874,16 +976,16 @@ static MaybeLocal ImportModuleDynamically( Local import_args[] = { id, Local(specifier), + v8::Integer::New(isolate, to_phase_constant(phase)), attributes, resource_name, }; Local result; - if (import_callback->Call( - context, - Undefined(isolate), - arraysize(import_args), - import_args).ToLocal(&result)) { + if (import_callback + ->Call( + context, Undefined(isolate), arraysize(import_args), import_args) + .ToLocal(&result)) { CHECK(result->IsPromise()); return handle_scope.Escape(result.As()); } @@ -891,6 +993,20 @@ static MaybeLocal ImportModuleDynamically( return MaybeLocal(); } +static MaybeLocal ImportModuleDynamically( + Local context, + Local host_defined_options, + Local resource_name, + Local specifier, + Local import_attributes) { + return ImportModuleDynamicallyWithPhase(context, + host_defined_options, + resource_name, + specifier, + ModuleImportPhase::kEvaluation, + import_attributes); +} + void ModuleWrap::SetImportModuleDynamicallyCallback( const FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); @@ -903,13 +1019,17 @@ 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( - Local context, Local module, Local meta) { +void ModuleWrap::HostInitializeImportMetaObjectCallback(Local context, + Local module, + Local meta) { Environment* env = Environment::GetCurrent(context); - if (env == nullptr) - return; + if (env == nullptr) return; ModuleWrap* module_wrap = GetFromModule(env, module); if (module_wrap == nullptr) { @@ -966,10 +1086,10 @@ MaybeLocal ModuleWrap::SyntheticModuleEvaluationStepsCallback( ->GetInternalField(kSyntheticEvaluationStepsSlot) .As() .As(); - obj->object()->SetInternalField( - kSyntheticEvaluationStepsSlot, Undefined(isolate)); - MaybeLocal ret = synthetic_evaluation_steps->Call(context, - obj->object(), 0, nullptr); + obj->object()->SetInternalField(kSyntheticEvaluationStepsSlot, + Undefined(isolate)); + MaybeLocal ret = + synthetic_evaluation_steps->Call(context, obj->object(), 0, nullptr); if (ret.IsEmpty()) { CHECK(try_catch.HasCaught()); } @@ -1117,6 +1237,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); @@ -1145,18 +1267,21 @@ void ModuleWrap::CreatePerContextProperties(Local 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 } @@ -1172,6 +1297,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); diff --git a/src/module_wrap.h b/src/module_wrap.h index 83b5793013cbc4..304110865f8528 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -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& args); static void EvaluateSync(const v8::FunctionCallbackInfo& args); static void GetNamespaceSync(const v8::FunctionCallbackInfo& args); + static void SetModuleSourceObject( + const v8::FunctionCallbackInfo& args); + static void GetModuleSourceObject( + const v8::FunctionCallbackInfo& args); static void Link(const v8::FunctionCallbackInfo& args); static void Instantiate(const v8::FunctionCallbackInfo& args); @@ -129,6 +139,11 @@ class ModuleWrap : public BaseObject { v8::Local specifier, v8::Local import_attributes, v8::Local referrer); + static v8::MaybeLocal ResolveSourceCallback( + v8::Local context, + v8::Local specifier, + v8::Local import_attributes, + v8::Local referrer); static ModuleWrap* GetFromModule(node::Environment*, v8::Local); v8::Global module_; diff --git a/src/node.cc b/src/node.cc index 480681d0b02ff8..2836d0fa9d67f4 100644 --- a/src/node.cc +++ b/src/node.cc @@ -766,6 +766,10 @@ static ExitCode ProcessGlobalArgsInternal(std::vector* 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. diff --git a/src/node_errors.h b/src/node_errors.h index 801b19fc91810b..cc61cfc810f165 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -105,6 +105,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) \ @@ -247,6 +248,15 @@ inline v8::Local 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 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 ERR_STRING_TOO_LONG(v8::Isolate* isolate) { char message[128]; snprintf(message, sizeof(message), diff --git a/test/es-module/test-esm-wasm.mjs b/test/es-module/test-esm-wasm.mjs index da56e221edc1e9..71b247411429b0 100644 --- a/test/es-module/test-esm-wasm.mjs +++ b/test/es-module/test-esm-wasm.mjs @@ -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,164 @@ 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);', + ].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";', + 'try {', + ` await import.source(${JSON.stringify(fileUrl)});`, + ' ok(false);', + '}', + 'catch (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)};`, + ]); + ok(stderr.includes('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'), + ]); + ok(stderr.includes('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'), + ]); + ok(stderr.includes('Source phase import object is not defined for module')); + strictEqual(stdout, ''); + notStrictEqual(code, 0); + }); }); diff --git a/test/fixtures/es-modules/unimportable.wasm b/test/fixtures/es-modules/unimportable.wasm new file mode 100644 index 0000000000000000000000000000000000000000..74f97158e9cec1491d6f2c78f4b0af9865cee273 GIT binary patch literal 30 hcmZQbEY4+QU|?WmWlUgTtY_k7WN~x!^Z^qL3;;uy1bP4f literal 0 HcmV?d00001 diff --git a/test/fixtures/es-modules/wasm-source-phase.js b/test/fixtures/es-modules/wasm-source-phase.js new file mode 100644 index 00000000000000..0485caa8c7c88b --- /dev/null +++ b/test/fixtures/es-modules/wasm-source-phase.js @@ -0,0 +1,7 @@ +import source mod from './simple.wasm'; + +export function dyn (specifier) { + return import.source(specifier); +} + +export { mod }; diff --git a/test/parallel/test-vm-module-dynamic-import.js b/test/parallel/test-vm-module-dynamic-import.js index bd542ca9202513..a90013f3676b15 100644 --- a/test/parallel/test-vm-module-dynamic-import.js +++ b/test/parallel/test-vm-module-dynamic-import.js @@ -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; }), }); From 301b0677caaa42ad5479af788af5165bc9ff719e Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 5 Feb 2025 10:16:11 -0800 Subject: [PATCH 2/2] fixup: code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antoine du Hamel Co-authored-by: Michaƫl Zasso --- doc/api/errors.md | 4 ++++ doc/api/esm.md | 6 +++--- src/module_wrap.cc | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 0817d2572b3ae5..0fdb5488f08088 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2728,6 +2728,10 @@ A file imported from a source map was not found. ### `ERR_SOURCE_PHASE_NOT_DEFINED` + + 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)`. diff --git a/doc/api/esm.md b/doc/api/esm.md index c8d6e23de5b477..97f3223fa7f128 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -710,14 +710,14 @@ into a new instance of `library.wasm`: ```js -import source libraryModule from './library.wasm`; +import source libraryModule from './library.wasm'; const instance1 = await WebAssembly.instantiate(libraryModule, { - custom: import1 + custom: import1, }); const instance2 = await WebAssembly.instantiate(libraryModule, { - custom: import2 + custom: import2, }); ``` diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 460ec673262b23..56073e0b2dde9a 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -417,7 +417,7 @@ ModulePhase to_phase_constant(ModuleImportPhase phase) { case ModuleImportPhase::kSource: return kSourcePhase; } - CHECK(false); + UNREACHABLE(); } static Local createImportAttributesContainer( @@ -465,7 +465,7 @@ static Local createModuleRequestsContainer( Local values[] = { specifier, attributes, - v8::Integer::New(isolate, to_phase_constant(phase)), + Integer::New(isolate, to_phase_constant(phase)), }; DCHECK_EQ(arraysize(names), arraysize(values)); @@ -976,7 +976,7 @@ static MaybeLocal ImportModuleDynamicallyWithPhase( Local import_args[] = { id, Local(specifier), - v8::Integer::New(isolate, to_phase_constant(phase)), + Integer::New(isolate, to_phase_constant(phase)), attributes, resource_name, };