diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 91b89860e7..29dce8c0a7 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -643,7 +643,7 @@ Module.prototype.load = function(filename) { url, new ModuleJob(ESMLoader, url, async () => { return createDynamicModule( - ['default'], url, (reflect) => { + [], ['default'], url, (reflect) => { reflect.exports.default.set(exports); }); }) diff --git a/lib/internal/modules/esm/create_dynamic_module.js b/lib/internal/modules/esm/create_dynamic_module.js index eea01bed31..45f964d5ad 100644 --- a/lib/internal/modules/esm/create_dynamic_module.js +++ b/lib/internal/modules/esm/create_dynamic_module.js @@ -1,14 +1,18 @@ 'use strict'; -const { ArrayPrototype } = primordials; +const { ArrayPrototype, JSON, Object } = primordials; const debug = require('internal/util/debuglog').debuglog('esm'); -const createDynamicModule = (exports, url = '', evaluate) => { +const createDynamicModule = (imports, exports, url = '', evaluate) => { debug('creating ESM facade for %s with exports: %j', url, exports); const names = ArrayPrototype.map(exports, (name) => `${name}`); const source = ` +${ArrayPrototype.join(ArrayPrototype.map(imports, (impt, index) => + `import * as $import_${index} from ${JSON.stringify(impt)}; +import.meta.imports[${JSON.stringify(impt)}] = $import_${index};`), '\n') +} ${ArrayPrototype.join(ArrayPrototype.map(names, (name) => `let $${name}; export { $${name} as ${name} }; @@ -22,19 +26,21 @@ import.meta.done(); `; const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const m = new ModuleWrap(source, `${url}`); - m.link(() => 0); - m.instantiate(); const readyfns = new Set(); const reflect = { - namespace: m.namespace(), - exports: {}, + exports: Object.create(null), onReady: (cb) => { readyfns.add(cb); }, }; + if (imports.length) + reflect.imports = Object.create(null); + callbackMap.set(m, { initializeImportMeta: (meta, wrap) => { meta.exports = reflect.exports; + if (reflect.imports) + meta.imports = reflect.imports; meta.done = () => { evaluate(reflect); reflect.onReady = (cb) => cb(reflect); diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js index a83cf9c675..67b8db716c 100644 --- a/lib/internal/modules/esm/default_resolve.js +++ b/lib/internal/modules/esm/default_resolve.js @@ -10,17 +10,14 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const experimentalJsonModules = getOptionValue('--experimental-json-modules'); const typeFlag = getOptionValue('--input-type'); - +const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); const { resolve: moduleWrapResolve, getPackageType } = internalBinding('module_wrap'); const { pathToFileURL, fileURLToPath } = require('internal/url'); const { ERR_INPUT_TYPE_NOT_ALLOWED, ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; -const { - Object, - SafeMap -} = primordials; +const { SafeMap } = primordials; const realpathCache = new SafeMap(); @@ -44,15 +41,11 @@ const legacyExtensionFormatMap = { '.node': 'commonjs' }; -if (experimentalJsonModules) { - // This is a total hack - Object.assign(extensionFormatMap, { - '.json': 'json' - }); - Object.assign(legacyExtensionFormatMap, { - '.json': 'json' - }); -} +if (experimentalWasmModules) + extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm'; + +if (experimentalJsonModules) + extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; function resolve(specifier, parentURL) { if (NativeModule.canBeRequiredByUsers(specifier)) { diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index f752550d12..0ea1e6f4e5 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -153,7 +153,7 @@ class Loader { loaderInstance = async (url) => { debug(`Translating dynamic ${url}`); const { exports, execute } = await this._dynamicInstantiate(url); - return createDynamicModule(exports, url, (reflect) => { + return createDynamicModule([], exports, url, (reflect) => { debug(`Loading dynamic ${url}`); execute(reflect.exports); }); diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 72350fb2b2..4ca9be4d62 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -1,9 +1,12 @@ 'use strict'; +/* global WebAssembly */ + const { + JSON, + Object, SafeMap, - StringPrototype, - JSON + StringPrototype } = primordials; const { NativeModule } = require('internal/bootstrap/loaders'); @@ -72,11 +75,11 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) { ]; if (module && module.loaded) { const exports = module.exports; - return createDynamicModule(['default'], url, (reflect) => { + return createDynamicModule([], ['default'], url, (reflect) => { reflect.exports.default.set(exports); }); } - return createDynamicModule(['default'], url, () => { + return createDynamicModule([], ['default'], url, () => { debug(`Loading CJSModule ${url}`); // We don't care about the return val of _load here because Module#load // will handle it for us by checking the loader registry and filling the @@ -97,7 +100,7 @@ translators.set('builtin', async function builtinStrategy(url) { } module.compileForPublicLoader(true); return createDynamicModule( - [...module.exportKeys, 'default'], url, (reflect) => { + [], [...module.exportKeys, 'default'], url, (reflect) => { debug(`Loading BuiltinModule ${url}`); module.reflect = reflect; for (const key of module.exportKeys) @@ -116,7 +119,7 @@ translators.set('json', async function jsonStrategy(url) { let module = CJSModule._cache[modulePath]; if (module && module.loaded) { const exports = module.exports; - return createDynamicModule(['default'], url, (reflect) => { + return createDynamicModule([], ['default'], url, (reflect) => { reflect.exports.default.set(exports); }); } @@ -136,8 +139,32 @@ translators.set('json', async function jsonStrategy(url) { throw err; } CJSModule._cache[modulePath] = module; - return createDynamicModule(['default'], url, (reflect) => { + return createDynamicModule([], ['default'], url, (reflect) => { debug(`Parsing JSONModule ${url}`); reflect.exports.default.set(module.exports); }); }); + +// Strategy for loading a wasm module +translators.set('wasm', async function(url) { + const pathname = fileURLToPath(url); + const buffer = await readFileAsync(pathname); + debug(`Translating WASMModule ${url}`); + let compiled; + try { + compiled = await WebAssembly.compile(buffer); + } catch (err) { + err.message = pathname + ': ' + err.message; + throw err; + } + + const imports = + WebAssembly.Module.imports(compiled).map(({ module }) => module); + const exports = WebAssembly.Module.exports(compiled).map(({ name }) => name); + + return createDynamicModule(imports, exports, url, (reflect) => { + const { exports } = new WebAssembly.Instance(compiled, reflect.imports); + for (const expt of Object.keys(exports)) + reflect.exports[expt].set(exports[expt]); + }); +}); diff --git a/src/node_options.cc b/src/node_options.cc index b2f14d2056..000ec45598 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -269,6 +269,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "experimental ES Module support and caching modules", &EnvironmentOptions::experimental_modules, kAllowedInEnvironment); + AddOption("--experimental-wasm-modules", + "experimental ES Module support for webassembly modules", + &EnvironmentOptions::experimental_wasm_modules, + kAllowedInEnvironment); AddOption("--experimental-policy", "use the specified file as a " "security policy", diff --git a/src/node_options.h b/src/node_options.h index 2aca1f327c..4ddd97f966 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -94,6 +94,7 @@ class EnvironmentOptions : public Options { bool experimental_json_modules = false; bool experimental_modules = false; std::string es_module_specifier_resolution; + bool experimental_wasm_modules = false; std::string module_type; std::string experimental_policy; bool experimental_repl_await = false; diff --git a/test/es-module/test-esm-wasm.mjs b/test/es-module/test-esm-wasm.mjs new file mode 100644 index 0000000000..62154bbf3c --- /dev/null +++ b/test/es-module/test-esm-wasm.mjs @@ -0,0 +1,9 @@ +// Flags: --experimental-modules --experimental-wasm-modules +/* eslint-disable node-core/required-modules */ +import { add, addImported } from '../fixtures/es-modules/simple.wasm'; +import { strictEqual } from 'assert'; + +strictEqual(add(10, 20), 30); + +strictEqual(addImported(0), 42); +strictEqual(addImported(1), 43); diff --git a/test/fixtures/es-modules/simple.wasm b/test/fixtures/es-modules/simple.wasm new file mode 100644 index 0000000000..889c1a5865 Binary files /dev/null and b/test/fixtures/es-modules/simple.wasm differ diff --git a/test/fixtures/es-modules/wasm-dep.mjs b/test/fixtures/es-modules/wasm-dep.mjs new file mode 100644 index 0000000000..4a2e5dc1ae --- /dev/null +++ b/test/fixtures/es-modules/wasm-dep.mjs @@ -0,0 +1,3 @@ +export function jsFn () { + return 42; +}