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 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 11 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2726,6 +2726,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`
Expand Down
45 changes: 38 additions & 7 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```

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

Expand Down Expand Up @@ -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
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
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 @@ -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);
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