Skip to content

Commit db4dcc0

Browse files
committed
esm: support source phase imports for WebAssembly
PR-URL: nodejs#56919 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Stephen Belanger <[email protected]>
1 parent ac5afbc commit db4dcc0

22 files changed

+580
-95
lines changed

doc/api/errors.md

+11
Original file line numberDiff line numberDiff line change
@@ -2736,6 +2736,17 @@ The source map could not be parsed because it does not exist, or is corrupt.
27362736

27372737
A file imported from a source map was not found.
27382738

2739+
<a id="ERR_SOURCE_PHASE_NOT_DEFINED"></a>
2740+
2741+
### `ERR_SOURCE_PHASE_NOT_DEFINED`
2742+
2743+
<!-- YAML
2744+
added: REPLACEME
2745+
-->
2746+
2747+
The provided module import does not provide a source phase imports representation for source phase
2748+
import syntax `import source x from 'x'` or `import.source(x)`.
2749+
27392750
<a id="ERR_SQLITE_ERROR"></a>
27402751

27412752
### `ERR_SQLITE_ERROR`

doc/api/esm.md

+38-7
Original file line numberDiff line numberDiff line change
@@ -669,17 +669,19 @@ imported from the same path.
669669
670670
> Stability: 1 - Experimental
671671
672-
Importing WebAssembly modules is supported under the
673-
`--experimental-wasm-modules` flag, allowing any `.wasm` files to be
674-
imported as normal modules while also supporting their module imports.
672+
Importing both WebAssembly module instances and WebAssembly source phase
673+
imports are supported under the `--experimental-wasm-modules` flag.
675674
676-
This integration is in line with the
675+
Both of these integrations are in line with the
677676
[ES Module Integration Proposal for WebAssembly][].
678677
679-
For example, an `index.mjs` containing:
678+
Instance imports allow any `.wasm` files to be imported as normal modules,
679+
supporting their module imports in turn.
680+
681+
For example, an `index.js` containing:
680682
681683
```js
682-
import * as M from './module.wasm';
684+
import * as M from './library.wasm';
683685
console.log(M);
684686
```
685687
@@ -689,7 +691,35 @@ executed under:
689691
node --experimental-wasm-modules index.mjs
690692
```
691693
692-
would provide the exports interface for the instantiation of `module.wasm`.
694+
would provide the exports interface for the instantiation of `library.wasm`.
695+
696+
### Wasm Source Phase Imports
697+
698+
<!-- YAML
699+
added: REPLACEME
700+
-->
701+
702+
The [Source Phase Imports][] proposal allows the `import source` keyword
703+
combination to import a `WebAssembly.Module` object directly, instead of getting
704+
a module instance already instantiated with its dependencies.
705+
706+
This is useful when needing custom instantiations for Wasm, while still
707+
resolving and loading it through the ES module integration.
708+
709+
For example, to create multiple instances of a module, or to pass custom imports
710+
into a new instance of `library.wasm`:
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+
```
693723
694724
<i id="esm_experimental_top_level_await"></i>
695725
@@ -1126,6 +1156,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11261156
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
11271157
[Module customization hooks]: module.md#customization-hooks
11281158
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
1159+
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
11291160
[Terminology]: #terminology
11301161
[URL]: https://url.spec.whatwg.org/
11311162
[`"exports"`]: packages.md#exports

doc/api/vm.md

+1
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1508,7 +1508,7 @@ function loadESMFromCJS(mod, filename, format, source) {
15081508
if (isMain) {
15091509
require('internal/modules/run_main').runEntryPointWithESMLoader((cascadedLoader) => {
15101510
const mainURL = pathToFileURL(filename).href;
1511-
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, true);
1511+
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, undefined, true);
15121512
});
15131513
// ESM won't be accessible via process.mainModule.
15141514
setOwnProperty(process, 'mainModule', undefined);

lib/internal/modules/esm/loader.js

+32-15
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;
@@ -377,7 +383,7 @@ class ModuleLoader {
377383
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
378384

379385
const { ModuleJobSync } = require('internal/modules/esm/module_job');
380-
job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk);
386+
job = new ModuleJobSync(this, url, kEmptyObject, wrap, kEvaluationPhase, isMain, inspectBrk);
381387
this.loadCache.set(url, kImplicitTypeAttribute, job);
382388
mod[kRequiredModuleSymbol] = job.module;
383389
return { wrap: job.module, namespace: job.runSync(parent).namespace };
@@ -389,9 +395,10 @@ class ModuleLoader {
389395
* @param {string} specifier Specifier of the the imported module.
390396
* @param {string} parentURL Where the import comes from.
391397
* @param {object} importAttributes import attributes from the import statement.
398+
* @param {number} phase The import phase.
392399
* @returns {ModuleJobBase}
393400
*/
394-
getModuleJobForRequire(specifier, parentURL, importAttributes) {
401+
getModuleJobForRequire(specifier, parentURL, importAttributes, phase) {
395402
const parsed = URLParse(specifier);
396403
if (parsed != null) {
397404
const protocol = parsed.protocol;
@@ -422,6 +429,7 @@ class ModuleLoader {
422429
}
423430
throw new ERR_REQUIRE_CYCLE_MODULE(message);
424431
}
432+
job.ensurePhase(phase);
425433
// Otherwise the module could be imported before but the evaluation may be already
426434
// completed (e.g. the require call is lazy) so it's okay. We will return the
427435
// module now and check asynchronicity of the entire graph later, after the
@@ -463,7 +471,7 @@ class ModuleLoader {
463471

464472
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
465473
const { ModuleJobSync } = require('internal/modules/esm/module_job');
466-
job = new ModuleJobSync(this, url, importAttributes, wrap, isMain, inspectBrk);
474+
job = new ModuleJobSync(this, url, importAttributes, wrap, phase, isMain, inspectBrk);
467475

468476
this.loadCache.set(url, importAttributes.type, job);
469477
return job;
@@ -543,13 +551,14 @@ class ModuleLoader {
543551
* by the time this returns. Otherwise it may still have pending module requests.
544552
* @param {string} url The URL that was resolved for this module.
545553
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
554+
* @param {number} phase Import phase.
546555
* @param {string} [parentURL] See {@link getModuleJobForImport}
547556
* @param {string} [format] The format hint possibly returned by the `resolve` hook
548557
* @param {boolean} isForRequireInImportedCJS Whether this module job is created for require()
549558
* in imported CJS.
550559
* @returns {ModuleJobBase} The (possibly pending) module job
551560
*/
552-
#createModuleJob(url, importAttributes, parentURL, format, isForRequireInImportedCJS) {
561+
#createModuleJob(url, importAttributes, phase, parentURL, format, isForRequireInImportedCJS) {
553562
const context = { format, importAttributes };
554563

555564
const isMain = parentURL === undefined;
@@ -575,6 +584,7 @@ class ModuleLoader {
575584
url,
576585
importAttributes,
577586
moduleOrModulePromise,
587+
phase,
578588
isMain,
579589
inspectBrk,
580590
isForRequireInImportedCJS,
@@ -592,11 +602,18 @@ class ModuleLoader {
592602
* @param {string} parentURL Path of the parent importing the module.
593603
* @param {Record<string, string>} importAttributes Validations for the
594604
* module import.
605+
* @param {number} [phase] The phase of the import.
606+
* @param {boolean} [isEntryPoint] Whether this is the realm-level entry point.
595607
* @returns {Promise<ModuleExports>}
596608
*/
597-
async import(specifier, parentURL, importAttributes, isEntryPoint = false) {
609+
async import(specifier, parentURL, importAttributes, phase = kEvaluationPhase, isEntryPoint = false) {
598610
return onImport.tracePromise(async () => {
599-
const moduleJob = await this.getModuleJobForImport(specifier, parentURL, importAttributes);
611+
const moduleJob = await this.getModuleJobForImport(specifier, parentURL, importAttributes,
612+
phase);
613+
if (phase === kSourcePhase) {
614+
const module = await moduleJob.modulePromise;
615+
return module.getModuleSourceObject();
616+
}
600617
const { module } = await moduleJob.run(isEntryPoint);
601618
return module.getNamespace();
602619
}, {

0 commit comments

Comments
 (0)