diff --git a/.eslintrc.js b/.eslintrc.js index 5fb535c3d83759..914566d309c498 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -251,6 +251,7 @@ module.exports = { 'node-core/no-unescaped-regexp-dot': 'error', }, globals: { + WebAssembly: false, COUNTER_HTTP_CLIENT_REQUEST: false, COUNTER_HTTP_CLIENT_RESPONSE: false, COUNTER_HTTP_SERVER_REQUEST: false, diff --git a/lib/internal/loader/CreateDynamicModule.js b/lib/internal/loader/CreateDynamicModule.js index f2596de04bfcb3..87fdfaf4813cdd 100644 --- a/lib/internal/loader/CreateDynamicModule.js +++ b/lib/internal/loader/CreateDynamicModule.js @@ -4,8 +4,9 @@ const { ModuleWrap } = internalBinding('module_wrap'); const debug = require('util').debuglog('esm'); const ArrayJoin = Function.call.bind(Array.prototype.join); const ArrayMap = Function.call.bind(Array.prototype.map); +const { Buffer } = require('buffer'); -const createDynamicModule = (exports, url = '', evaluate) => { +const createDynamicModule = (exports, url = '', evaluate, imports = []) => { debug( `creating ESM facade for ${url} with exports: ${ArrayJoin(exports, ', ')}` ); @@ -32,28 +33,39 @@ const createDynamicModule = (exports, url = '', evaluate) => { const reflectiveModule = new ModuleWrap(src, `cjs-facade:${url}`); reflectiveModule.instantiate(); const { setExecutor, reflect } = reflectiveModule.evaluate()(); + + const importLookup = {}; + for (const name of imports) + importLookup[name] = `$${Buffer.from(name).toString('hex')}`; + // public exposed ESM const reexports = ` import { executor, ${ArrayMap(names, (name) => `$${name}`)} } from ""; + ${ArrayJoin(ArrayMap(imports, (i) => + `import * as ${importLookup[i]} from '${i}';`), '\n')} export { ${ArrayJoin(ArrayMap(names, (name) => `$${name} as ${name}`), ', ')} } if (typeof executor === "function") { // add await to this later if top level await comes along - executor() + executor({ ${ + ArrayJoin(ArrayMap(imports, (i) => `'${i}': ${importLookup[i]}, `), '\n')} + }); }`; if (typeof evaluate === 'function') { - setExecutor(() => evaluate(reflect)); + setExecutor((imports) => { + reflect.imports = imports; + evaluate(reflect); + }); } const module = new ModuleWrap(reexports, `${url}`); - module.link(async () => reflectiveModule); - module.instantiate(); return { module, - reflect + reflect, + reflectiveModule, }; }; diff --git a/lib/internal/loader/DefaultResolve.js b/lib/internal/loader/DefaultResolve.js index d815be87dd8954..9d0dc764a96a54 100644 --- a/lib/internal/loader/DefaultResolve.js +++ b/lib/internal/loader/DefaultResolve.js @@ -44,7 +44,8 @@ const extensionFormatMap = { '.mjs': 'esm', '.json': 'json', '.node': 'addon', - '.js': 'cjs' + '.js': 'cjs', + '.wasm': 'wasm', }; function resolve(specifier, parentURL) { diff --git a/lib/internal/loader/ModuleJob.js b/lib/internal/loader/ModuleJob.js index db37765b20bd0c..de39d1c0aa4de2 100644 --- a/lib/internal/loader/ModuleJob.js +++ b/lib/internal/loader/ModuleJob.js @@ -21,11 +21,13 @@ class ModuleJob { this.modulePromise = moduleProvider(url); this.module = undefined; this.reflect = undefined; + let reflectiveModule; // Wait for the ModuleWrap instance being linked with all dependencies. const link = async () => { ({ module: this.module, - reflect: this.reflect } = await this.modulePromise); + reflect: this.reflect, + reflectiveModule } = await this.modulePromise); if (inspectBrk) { const initWrapper = process.binding('inspector').callAndPauseOnStart; initWrapper(this.module.instantiate, this.module); @@ -34,6 +36,11 @@ class ModuleJob { const dependencyJobs = []; const promises = this.module.link(async (specifier) => { + if (specifier === '' && reflectiveModule !== undefined) { + const r = reflectiveModule; + reflectiveModule = undefined; + return r; + } const jobPromise = this.loader.getModuleJob(specifier, url); dependencyJobs.push(jobPromise); return (await (await jobPromise).modulePromise).module; @@ -53,10 +60,10 @@ class ModuleJob { } async instantiate() { - if (this.instantiated) { - return this.instantiated; - } - return this.instantiated = this._instantiate(); + if (!this.instantiated) + this.instantiated = this._instantiate(); + await this.instantiated; + return this.module; } // This method instantiates the module associated with this job and its diff --git a/lib/internal/loader/Translators.js b/lib/internal/loader/Translators.js index 18b1b12fd15854..4c8ca676a2d9a0 100644 --- a/lib/internal/loader/Translators.js +++ b/lib/internal/loader/Translators.js @@ -90,3 +90,16 @@ translators.set('json', async (url) => { } }); }); + +translators.set('wasm', async (url) => { + debug(`Translating WASMModule ${url}`); + const bytes = await readFileAsync(new URL(url)); + const module = await WebAssembly.compile(bytes); + const imports = WebAssembly.Module.imports(module).map((i) => i.module); + const exports = WebAssembly.Module.exports(module).map((e) => e.name); + return createDynamicModule(exports, url, (reflect) => { + const instance = new WebAssembly.Instance(module, reflect.imports); + for (const name of exports) + reflect.exports[name].set(instance.exports[name]); + }, imports); +}); diff --git a/test/common/index.mjs b/test/common/index.mjs index 6d6fe4997bdb71..bada28cf40931d 100644 --- a/test/common/index.mjs +++ b/test/common/index.mjs @@ -100,6 +100,11 @@ export function leakedGlobals() { // Turn this off if the test should not check for global leaks. export let globalCheck = true; // eslint-disable-line +export function crashOnUnhandledRejection() { + process.on('unhandledRejection', + (err) => process.nextTick(() => { throw err; })); +} + process.on('exit', function() { if (!globalCheck) return; const leaked = leakedGlobals(); diff --git a/test/es-module/test-esm-import-wasm-errors.mjs b/test/es-module/test-esm-import-wasm-errors.mjs new file mode 100644 index 00000000000000..856237988fd971 --- /dev/null +++ b/test/es-module/test-esm-import-wasm-errors.mjs @@ -0,0 +1,32 @@ +// Flags: --experimental-modules + +import { crashOnUnhandledRejection } from '../common'; +import assert from 'assert'; + +crashOnUnhandledRejection(); + +async function expectsRejection(fn, settings) { + // Retain async context. + const storedError = new Error('Thrown from:'); + try { + await fn(); + } catch (err) { + try { + assert(err instanceof settings.type, + `${err.name} is not instance of ${settings.type.name}`); + assert.strictEqual(err.name, settings.type.name); + } catch (validationError) { + console.error(validationError); + console.error('Original error:'); + console.error(err); + throw storedError; + } + return; + } + assert.fail('Missing expected exception'); +} + +expectsRejection(() => + import('../fixtures/es-modules/invalid.wasm'), { + type: WebAssembly.CompileError, +}); diff --git a/test/es-module/test-esm-import-wasm.mjs b/test/es-module/test-esm-import-wasm.mjs new file mode 100644 index 00000000000000..971497c99b48c5 --- /dev/null +++ b/test/es-module/test-esm-import-wasm.mjs @@ -0,0 +1,15 @@ +// Flags: --experimental-modules --harmony-dynamic-import + +import { crashOnUnhandledRejection } from '../common'; +import assert from 'assert'; + +crashOnUnhandledRejection(); + +import { add } from '../fixtures/es-modules/add.wasm'; + +assert.strictEqual(add(2, 3), 5); + +import('../fixtures/es-modules/add.wasm').then((ns) => { + assert.strictEqual(ns.add(2, 3), 5); + assert.strictEqual(ns.default(2), 12); +}); diff --git a/test/fixtures/es-modules/add.mjs b/test/fixtures/es-modules/add.mjs new file mode 100644 index 00000000000000..d0f87bf2ef282c --- /dev/null +++ b/test/fixtures/es-modules/add.mjs @@ -0,0 +1 @@ +export const add = (a, b) => a + b; diff --git a/test/fixtures/es-modules/add.wasm b/test/fixtures/es-modules/add.wasm new file mode 100644 index 00000000000000..ceb1f52d74548e Binary files /dev/null and b/test/fixtures/es-modules/add.wasm differ diff --git a/test/fixtures/es-modules/add.wat b/test/fixtures/es-modules/add.wat new file mode 100644 index 00000000000000..99f8bc6a2b40cf --- /dev/null +++ b/test/fixtures/es-modules/add.wat @@ -0,0 +1,18 @@ +;; source of add.wasm + +(module + (import "./add.mjs" "add" (func $add_i32 (param i32 i32) (result i32))) + + (func $add (param $p0 i32) (param $p1 i32) (result i32) + get_local $p0 + get_local $p1 + call $add_i32) + + (func $add_10 (param $p0 i32) (result i32) + get_local $p0 + i32.const 10 + i32.add) + + (export "add" (func $add)) + (export "default" (func $add_10))) + diff --git a/test/fixtures/es-modules/invalid.wasm b/test/fixtures/es-modules/invalid.wasm new file mode 100644 index 00000000000000..78981922613b2a --- /dev/null +++ b/test/fixtures/es-modules/invalid.wasm @@ -0,0 +1 @@ +a