Skip to content

Commit

Permalink
esm: Support source phase imports for WebAssembly
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Feb 5, 2025
1 parent 3207fda commit aa33f53
Show file tree
Hide file tree
Showing 22 changed files with 596 additions and 113 deletions.
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`

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 -->
```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 @@ -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
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

0 comments on commit aa33f53

Please sign in to comment.