Skip to content

Commit 8b3ce05

Browse files
author
Guy Bedford
committed
esm: support source phase imports for WebAssembly
1 parent 3207fda commit 8b3ce05

22 files changed

+596
-113
lines changed

doc/api/errors.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2724,6 +2724,13 @@ The source map could not be parsed because it does not exist, or is corrupt.
27242724

27252725
A file imported from a source map was not found.
27262726

2727+
<a id="ERR_SOURCE_PHASE_NOT_DEFINED"></a>
2728+
2729+
### `ERR_SOURCE_PHASE_NOT_DEFINED`
2730+
2731+
The provided module import does not provide a source phase imports representation for source phase
2732+
import syntax `import source x from 'x'` or `import.source(x)`.
2733+
27272734
<a id="ERR_SQLITE_ERROR"></a>
27282735

27292736
### `ERR_SQLITE_ERROR`

doc/api/esm.md

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -667,17 +667,19 @@ imported from the same path.
667667
668668
> Stability: 1 - Experimental
669669
670-
Importing WebAssembly modules is supported under the
671-
`--experimental-wasm-modules` flag, allowing any `.wasm` files to be
672-
imported as normal modules while also supporting their module imports.
670+
Importing both WebAssembly module instances and WebAssembly source phase
671+
imports are supported under the `--experimental-wasm-modules` flag.
673672
674-
This integration is in line with the
673+
Both of these integrations are in line with the
675674
[ES Module Integration Proposal for WebAssembly][].
676675
677-
For example, an `index.mjs` containing:
676+
Instance imports allow any `.wasm` files to be imported as normal modules,
677+
supporting their module imports in turn.
678+
679+
For example, an `index.js` containing:
678680
679681
```js
680-
import * as M from './module.wasm';
682+
import * as M from './library.wasm';
681683
console.log(M);
682684
```
683685
@@ -687,7 +689,37 @@ executed under:
687689
node --experimental-wasm-modules index.mjs
688690
```
689691
690-
would provide the exports interface for the instantiation of `module.wasm`.
692+
would provide the exports interface for the instantiation of `library.wasm`.
693+
694+
### Wasm Source Phase Imports
695+
696+
<!-- YAML
697+
added: REPLACEME
698+
-->
699+
700+
The [Source Phase Imports][] proposal allows the `import source` keyword
701+
combination to import a `WebAssembly.Module` object directly, instead of getting
702+
a module instance already instantiated with its dependencies.
703+
704+
This is useful when needing custom instantiations for Wasm, while still
705+
resolving and loading it through the ES module integration.
706+
707+
For example, to create multiple instances of a module, or to pass custom imports
708+
into a new instance of `library.wasm`:
709+
710+
<!-- eslint-skip -->
711+
712+
```js
713+
import source libraryModule from './library.wasm`;
714+
715+
const instance1 = await WebAssembly.instantiate(libraryModule, {
716+
custom: import1
717+
});
718+
719+
const instance2 = await WebAssembly.instantiate(libraryModule, {
720+
custom: import2
721+
});
722+
```
691723
692724
<i id="esm_experimental_top_level_await"></i>
693725
@@ -1124,6 +1156,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11241156
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
11251157
[Module customization hooks]: module.md#customization-hooks
11261158
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
1159+
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
11271160
[Terminology]: #terminology
11281161
[URL]: https://url.spec.whatwg.org/
11291162
[`"exports"`]: packages.md#exports

doc/api/vm.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,6 +1908,7 @@ has the following signature:
19081908
* `importAttributes` {Object} The `"with"` value passed to the
19091909
[`optionsExpression`][] optional parameter, or an empty object if no value was
19101910
provided.
1911+
* `phase` {string} The phase of the dynamic import (`"source"` or `"evaluation"`).
19111912
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
19121913
recommended in order to take advantage of error tracking, and to avoid issues
19131914
with namespaces that contain `then` function exports.

lib/internal/modules/cjs/loader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,7 @@ function loadESMFromCJS(mod, filename, format, source) {
15011501
if (isMain) {
15021502
require('internal/modules/run_main').runEntryPointWithESMLoader((cascadedLoader) => {
15031503
const mainURL = pathToFileURL(filename).href;
1504-
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, true);
1504+
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, undefined, true);
15051505
});
15061506
// ESM won't be accessible via process.mainModule.
15071507
setOwnProperty(process, 'mainModule', undefined);

lib/internal/modules/esm/loader.js

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const {
3838
forceDefaultLoader,
3939
} = require('internal/modules/esm/utils');
4040
const { kImplicitTypeAttribute } = require('internal/modules/esm/assert');
41-
const { ModuleWrap, kEvaluating, kEvaluated } = internalBinding('module_wrap');
41+
const { ModuleWrap, kEvaluating, kEvaluated, kEvaluationPhase, kSourcePhase } = internalBinding('module_wrap');
4242
const {
4343
urlToFilename,
4444
} = require('internal/modules/helpers');
@@ -236,8 +236,7 @@ class ModuleLoader {
236236
async executeModuleJob(url, wrap, isEntryPoint = false) {
237237
const { ModuleJob } = require('internal/modules/esm/module_job');
238238
const module = await onImport.tracePromise(async () => {
239-
const job = new ModuleJob(
240-
this, url, undefined, wrap, false, false);
239+
const job = new ModuleJob(this, url, undefined, wrap, kEvaluationPhase, false, false);
241240
this.loadCache.set(url, undefined, job);
242241
const { module } = await job.run(isEntryPoint);
243242
return module;
@@ -273,11 +272,12 @@ class ModuleLoader {
273272
* @param {string} [parentURL] The URL of the module where the module request is initiated.
274273
* It's undefined if it's from the root module.
275274
* @param {ImportAttributes} importAttributes Attributes from the import statement or expression.
275+
* @param {number} phase Import phase.
276276
* @returns {Promise<ModuleJobBase>}
277277
*/
278-
async getModuleJobForImport(specifier, parentURL, importAttributes) {
278+
async getModuleJobForImport(specifier, parentURL, importAttributes, phase) {
279279
const resolveResult = await this.resolve(specifier, parentURL, importAttributes);
280-
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, false);
280+
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, phase, false);
281281
}
282282

283283
/**
@@ -287,11 +287,12 @@ class ModuleLoader {
287287
* @param {string} specifier See {@link getModuleJobForImport}
288288
* @param {string} [parentURL] See {@link getModuleJobForImport}
289289
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
290+
* @param {number} phase Import phase.
290291
* @returns {Promise<ModuleJobBase>}
291292
*/
292-
getModuleJobForRequireInImportedCJS(specifier, parentURL, importAttributes) {
293+
getModuleJobForRequireInImportedCJS(specifier, parentURL, importAttributes, phase) {
293294
const resolveResult = this.resolveSync(specifier, parentURL, importAttributes);
294-
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, true);
295+
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, phase, true);
295296
}
296297

297298
/**
@@ -300,16 +301,21 @@ class ModuleLoader {
300301
* @param {{ format: string, url: string }} resolveResult Resolved module request.
301302
* @param {string} [parentURL] See {@link getModuleJobForImport}
302303
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
304+
* @param {number} phase Import phase.
303305
* @param {boolean} isForRequireInImportedCJS Whether this is done for require() in imported CJS.
304306
* @returns {ModuleJobBase}
305307
*/
306-
#getJobFromResolveResult(resolveResult, parentURL, importAttributes, isForRequireInImportedCJS = false) {
308+
#getJobFromResolveResult(resolveResult, parentURL, importAttributes, phase,
309+
isForRequireInImportedCJS = false) {
307310
const { url, format } = resolveResult;
308311
const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
309312
let job = this.loadCache.get(url, resolvedImportAttributes.type);
310313

311314
if (job === undefined) {
312-
job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format, isForRequireInImportedCJS);
315+
job = this.#createModuleJob(url, resolvedImportAttributes, phase, parentURL, format,
316+
isForRequireInImportedCJS);
317+
} else {
318+
job.ensurePhase(phase);
313319
}
314320

315321
return job;
@@ -360,7 +366,7 @@ class ModuleLoader {
360366
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
361367

362368
const { ModuleJobSync } = require('internal/modules/esm/module_job');
363-
job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk);
369+
job = new ModuleJobSync(this, url, kEmptyObject, wrap, kEvaluationPhase, isMain, inspectBrk);
364370
this.loadCache.set(url, kImplicitTypeAttribute, job);
365371
mod[kRequiredModuleSymbol] = job.module;
366372
return { wrap: job.module, namespace: job.runSync().namespace };
@@ -372,9 +378,10 @@ class ModuleLoader {
372378
* @param {string} specifier Specifier of the the imported module.
373379
* @param {string} parentURL Where the import comes from.
374380
* @param {object} importAttributes import attributes from the import statement.
381+
* @param {number} phase The import phase.
375382
* @returns {ModuleJobBase}
376383
*/
377-
getModuleJobForRequire(specifier, parentURL, importAttributes) {
384+
getModuleJobForRequire(specifier, parentURL, importAttributes, phase) {
378385
const parsed = URLParse(specifier);
379386
if (parsed != null) {
380387
const protocol = parsed.protocol;
@@ -405,6 +412,7 @@ class ModuleLoader {
405412
}
406413
throw new ERR_REQUIRE_CYCLE_MODULE(message);
407414
}
415+
job.ensurePhase(phase);
408416
// Otherwise the module could be imported before but the evaluation may be already
409417
// completed (e.g. the require call is lazy) so it's okay. We will return the
410418
// module now and check asynchronicity of the entire graph later, after the
@@ -446,7 +454,7 @@ class ModuleLoader {
446454

447455
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
448456
const { ModuleJobSync } = require('internal/modules/esm/module_job');
449-
job = new ModuleJobSync(this, url, importAttributes, wrap, isMain, inspectBrk);
457+
job = new ModuleJobSync(this, url, importAttributes, wrap, phase, isMain, inspectBrk);
450458

451459
this.loadCache.set(url, importAttributes.type, job);
452460
return job;
@@ -526,13 +534,14 @@ class ModuleLoader {
526534
* by the time this returns. Otherwise it may still have pending module requests.
527535
* @param {string} url The URL that was resolved for this module.
528536
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
537+
* @param {number} phase Import phase.
529538
* @param {string} [parentURL] See {@link getModuleJobForImport}
530539
* @param {string} [format] The format hint possibly returned by the `resolve` hook
531540
* @param {boolean} isForRequireInImportedCJS Whether this module job is created for require()
532541
* in imported CJS.
533542
* @returns {ModuleJobBase} The (possibly pending) module job
534543
*/
535-
#createModuleJob(url, importAttributes, parentURL, format, isForRequireInImportedCJS) {
544+
#createModuleJob(url, importAttributes, phase, parentURL, format, isForRequireInImportedCJS) {
536545
const context = { format, importAttributes };
537546

538547
const isMain = parentURL === undefined;
@@ -558,6 +567,7 @@ class ModuleLoader {
558567
url,
559568
importAttributes,
560569
moduleOrModulePromise,
570+
phase,
561571
isMain,
562572
inspectBrk,
563573
isForRequireInImportedCJS,
@@ -575,11 +585,18 @@ class ModuleLoader {
575585
* @param {string} parentURL Path of the parent importing the module.
576586
* @param {Record<string, string>} importAttributes Validations for the
577587
* module import.
588+
* @param {number} [phase] The phase of the import.
589+
* @param {boolean} [isEntryPoint] Whether this is the realm-level entry point.
578590
* @returns {Promise<ModuleExports>}
579591
*/
580-
async import(specifier, parentURL, importAttributes, isEntryPoint = false) {
592+
async import(specifier, parentURL, importAttributes, phase = kEvaluationPhase, isEntryPoint = false) {
581593
return onImport.tracePromise(async () => {
582-
const moduleJob = await this.getModuleJobForImport(specifier, parentURL, importAttributes);
594+
const moduleJob = await this.getModuleJobForImport(specifier, parentURL, importAttributes,
595+
phase);
596+
if (phase === kSourcePhase) {
597+
const module = await moduleJob.modulePromise;
598+
return module.getModuleSourceObject();
599+
}
583600
const { module } = await moduleJob.run(isEntryPoint);
584601
return module.getNamespace();
585602
}, {

0 commit comments

Comments
 (0)