esm: --experimental-wasm-modules integration support
PR-URL: https://github.com/nodejs/node/pull/27659 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
parent
6982dc7198
commit
bbc254db5d
@ -441,6 +441,30 @@ node --experimental-modules index.mjs # fails
|
||||
node --experimental-modules --experimental-json-modules index.mjs # works
|
||||
```
|
||||
|
||||
## Experimental Wasm Modules
|
||||
|
||||
Importing Web Assembly 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.
|
||||
|
||||
This integration is in line with the
|
||||
[ES Module Integration Proposal for Web Assembly][].
|
||||
|
||||
For example, an `index.mjs` containing:
|
||||
|
||||
```js
|
||||
import * as M from './module.wasm';
|
||||
console.log(M);
|
||||
```
|
||||
|
||||
executed under:
|
||||
|
||||
```bash
|
||||
node --experimental-modules --experimental-wasm-modules index.mjs
|
||||
```
|
||||
|
||||
would provide the exports interface for the instantiation of `module.wasm`.
|
||||
|
||||
## Experimental Loader hooks
|
||||
|
||||
**Note: This API is currently being redesigned and will still change.**
|
||||
@ -484,11 +508,12 @@ module. This can be one of the following:
|
||||
|
||||
| `format` | Description |
|
||||
| --- | --- |
|
||||
| `'module'` | Load a standard JavaScript module |
|
||||
| `'commonjs'` | Load a Node.js CommonJS module |
|
||||
| `'builtin'` | Load a Node.js builtin module |
|
||||
| `'json'` | Load a JSON file |
|
||||
| `'commonjs'` | Load a Node.js CommonJS module |
|
||||
| `'dynamic'` | Use a [dynamic instantiate hook][] |
|
||||
| `'json'` | Load a JSON file |
|
||||
| `'module'` | Load a standard JavaScript module |
|
||||
| `'wasm'` | Load a WebAssembly module |
|
||||
|
||||
For example, a dummy loader to load JavaScript restricted to browser resolution
|
||||
rules with only JS file extension and Node.js builtin modules support could
|
||||
@ -585,8 +610,8 @@ format for that resolved URL given by the **ESM_FORMAT** routine.
|
||||
|
||||
The _"module"_ format is returned for an ECMAScript Module, while the
|
||||
_"commonjs"_ format is used to indicate loading through the legacy
|
||||
CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be
|
||||
extended in future updates.
|
||||
CommonJS loader. Additional formats such as _"addon"_ can be extended in future
|
||||
updates.
|
||||
|
||||
In the following algorithms, all subroutine errors are propagated as errors
|
||||
of these top-level routines.
|
||||
@ -739,5 +764,6 @@ success!
|
||||
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
|
||||
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
|
||||
[WHATWG JSON modules]: https://github.com/whatwg/html/issues/4315
|
||||
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
|
||||
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
|
||||
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
|
||||
|
@ -658,7 +658,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);
|
||||
});
|
||||
})
|
||||
|
@ -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);
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
@ -122,6 +122,11 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
|
||||
"--experimental-modules be enabled");
|
||||
}
|
||||
|
||||
if (experimental_wasm_modules && !experimental_modules) {
|
||||
errors->push_back("--experimental-wasm-modules requires "
|
||||
"--experimental-modules be enabled");
|
||||
}
|
||||
|
||||
if (!es_module_specifier_resolution.empty()) {
|
||||
if (!experimental_modules) {
|
||||
errors->push_back("--es-module-specifier-resolution requires "
|
||||
@ -274,6 +279,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",
|
||||
|
@ -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;
|
||||
|
15
test/es-module/test-esm-wasm.mjs
Normal file
15
test/es-module/test-esm-wasm.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
// Flags: --experimental-modules --experimental-wasm-modules
|
||||
import '../common/index.mjs';
|
||||
import { add, addImported } from '../fixtures/es-modules/simple.wasm';
|
||||
import { state } from '../fixtures/es-modules/wasm-dep.mjs';
|
||||
import { strictEqual } from 'assert';
|
||||
|
||||
strictEqual(state, 'WASM Start Executed');
|
||||
|
||||
strictEqual(add(10, 20), 30);
|
||||
|
||||
strictEqual(addImported(0), 42);
|
||||
|
||||
strictEqual(state, 'WASM JS Function Executed');
|
||||
|
||||
strictEqual(addImported(1), 43);
|
BIN
test/fixtures/es-modules/simple.wasm
vendored
Normal file
BIN
test/fixtures/es-modules/simple.wasm
vendored
Normal file
Binary file not shown.
23
test/fixtures/es-modules/simple.wat
vendored
Normal file
23
test/fixtures/es-modules/simple.wat
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
;; Compiled using the WebAssembly Tootkit (https://github.com/WebAssembly/wabt)
|
||||
;; $ wat2wasm simple.wat -o simple.wasm
|
||||
|
||||
(module
|
||||
(import "./wasm-dep.mjs" "jsFn" (func $jsFn (result i32)))
|
||||
(import "./wasm-dep.mjs" "jsInitFn" (func $jsInitFn))
|
||||
(export "add" (func $add))
|
||||
(export "addImported" (func $addImported))
|
||||
(start $startFn)
|
||||
(func $startFn
|
||||
call $jsInitFn
|
||||
)
|
||||
(func $add (param $a i32) (param $b i32) (result i32)
|
||||
local.get $a
|
||||
local.get $b
|
||||
i32.add
|
||||
)
|
||||
(func $addImported (param $a i32) (result i32)
|
||||
local.get $a
|
||||
call $jsFn
|
||||
i32.add
|
||||
)
|
||||
)
|
13
test/fixtures/es-modules/wasm-dep.mjs
vendored
Normal file
13
test/fixtures/es-modules/wasm-dep.mjs
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import { strictEqual } from 'assert';
|
||||
|
||||
export function jsFn () {
|
||||
state = 'WASM JS Function Executed';
|
||||
return 42;
|
||||
}
|
||||
|
||||
export let state = 'JS Function Executed';
|
||||
|
||||
export function jsInitFn () {
|
||||
strictEqual(state, 'JS Function Executed');
|
||||
state = 'WASM Start Executed';
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user