Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esm: Source Phase Imports for WebAssembly #56919

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="ERR_SOURCE_PHASE_NOT_DEFINED"></a>

### `ERR_SOURCE_PHASE_NOT_DEFINED`
guybedford marked this conversation as resolved.
Show resolved Hide resolved

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`
Expand Down
47 changes: 40 additions & 7 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```

Expand All @@ -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

<!-- 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`:

<!-- eslint-skip -->
Copy link
Contributor

@aduh95 aduh95 Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we instead add the required plugin? Can happen in a follow-up PR

diff --git a/eslint.config.mjs b/eslint.config.mjs
index ef195d0c60..912d326557 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -17,6 +17,7 @@ import nodeCore from './tools/eslint/eslint-plugin-node-core.js';
 const js = requireEslintTool('@eslint/js');
 const babelEslintParser = requireEslintTool('@babel/eslint-parser');
 const babelPluginSyntaxImportAttributes = resolveEslintTool('@babel/plugin-syntax-import-attributes');
+const babelPluginSyntaxImportSource = resolveEslintTool('@babel/plugin-syntax-import-source');
 const jsdoc = requireEslintTool('eslint-plugin-jsdoc');
 const markdown = requireEslintTool('eslint-plugin-markdown');
 const stylisticJs = requireEslintTool('@stylistic/eslint-plugin-js');
@@ -74,6 +75,7 @@ export default [
         babelOptions: {
           plugins: [
             babelPluginSyntaxImportAttributes,
+            babelPluginSyntaxImportSource,
           ],
         },
         requireConfigFile: false,
diff --git a/tools/eslint/package.json b/tools/eslint/package.json
index 68bedee0cb..0b6244654e 100644
--- a/tools/eslint/package.json
+++ b/tools/eslint/package.json
@@ -6,6 +6,7 @@
     "@babel/core": "^7.26.0",
     "@babel/eslint-parser": "^7.25.9",
     "@babel/plugin-syntax-import-attributes": "^7.26.0",
+    "@babel/plugin-syntax-import-source": "^7.25.9",
     "@stylistic/eslint-plugin-js": "^2.12.1",
     "eslint": "^9.17.0",
     "eslint-formatter-tap": "^8.40.0",


```js
import source libraryModule from './library.wasm`;

const instance1 = await WebAssembly.instantiate(libraryModule, {
custom: import1
});

const instance2 = await WebAssembly.instantiate(libraryModule, {
custom: import2
});
guybedford marked this conversation as resolved.
Show resolved Hide resolved
```

<i id="esm_experimental_top_level_await"></i>

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that WebAssembly.Module is the only type of ModuleSource, we should document it here.

Suggested change
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
* Returns: {Module Namespace Object|vm.Module|WebAssembly.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.
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
47 changes: 32 additions & 15 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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;
Expand Down Expand Up @@ -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 };
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -558,6 +567,7 @@ class ModuleLoader {
url,
importAttributes,
moduleOrModulePromise,
phase,
isMain,
inspectBrk,
isForRequireInImportedCJS,
Expand All @@ -575,11 +585,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();
}, {
Expand Down
Loading
Loading