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
|
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
|
## Experimental Loader hooks
|
||||||
|
|
||||||
**Note: This API is currently being redesigned and will still change.**
|
**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 |
|
| `format` | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `'module'` | Load a standard JavaScript module |
|
|
||||||
| `'commonjs'` | Load a Node.js CommonJS module |
|
|
||||||
| `'builtin'` | Load a Node.js builtin 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][] |
|
| `'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
|
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
|
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
|
The _"module"_ format is returned for an ECMAScript Module, while the
|
||||||
_"commonjs"_ format is used to indicate loading through the legacy
|
_"commonjs"_ format is used to indicate loading through the legacy
|
||||||
CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be
|
CommonJS loader. Additional formats such as _"addon"_ can be extended in future
|
||||||
extended in future updates.
|
updates.
|
||||||
|
|
||||||
In the following algorithms, all subroutine errors are propagated as errors
|
In the following algorithms, all subroutine errors are propagated as errors
|
||||||
of these top-level routines.
|
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
|
[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
|
[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
|
[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
|
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
|
||||||
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
|
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
|
||||||
|
@ -658,7 +658,7 @@ Module.prototype.load = function(filename) {
|
|||||||
url,
|
url,
|
||||||
new ModuleJob(ESMLoader, url, async () => {
|
new ModuleJob(ESMLoader, url, async () => {
|
||||||
return createDynamicModule(
|
return createDynamicModule(
|
||||||
['default'], url, (reflect) => {
|
[], ['default'], url, (reflect) => {
|
||||||
reflect.exports.default.set(exports);
|
reflect.exports.default.set(exports);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { ArrayPrototype } = primordials;
|
const { ArrayPrototype, JSON, Object } = primordials;
|
||||||
|
|
||||||
const debug = require('internal/util/debuglog').debuglog('esm');
|
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);
|
debug('creating ESM facade for %s with exports: %j', url, exports);
|
||||||
const names = ArrayPrototype.map(exports, (name) => `${name}`);
|
const names = ArrayPrototype.map(exports, (name) => `${name}`);
|
||||||
|
|
||||||
const source = `
|
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) =>
|
${ArrayPrototype.join(ArrayPrototype.map(names, (name) =>
|
||||||
`let $${name};
|
`let $${name};
|
||||||
export { $${name} as ${name} };
|
export { $${name} as ${name} };
|
||||||
@ -22,19 +26,21 @@ import.meta.done();
|
|||||||
`;
|
`;
|
||||||
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
|
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
|
||||||
const m = new ModuleWrap(source, `${url}`);
|
const m = new ModuleWrap(source, `${url}`);
|
||||||
m.link(() => 0);
|
|
||||||
m.instantiate();
|
|
||||||
|
|
||||||
const readyfns = new Set();
|
const readyfns = new Set();
|
||||||
const reflect = {
|
const reflect = {
|
||||||
namespace: m.namespace(),
|
exports: Object.create(null),
|
||||||
exports: {},
|
|
||||||
onReady: (cb) => { readyfns.add(cb); },
|
onReady: (cb) => { readyfns.add(cb); },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (imports.length)
|
||||||
|
reflect.imports = Object.create(null);
|
||||||
|
|
||||||
callbackMap.set(m, {
|
callbackMap.set(m, {
|
||||||
initializeImportMeta: (meta, wrap) => {
|
initializeImportMeta: (meta, wrap) => {
|
||||||
meta.exports = reflect.exports;
|
meta.exports = reflect.exports;
|
||||||
|
if (reflect.imports)
|
||||||
|
meta.imports = reflect.imports;
|
||||||
meta.done = () => {
|
meta.done = () => {
|
||||||
evaluate(reflect);
|
evaluate(reflect);
|
||||||
reflect.onReady = (cb) => cb(reflect);
|
reflect.onReady = (cb) => cb(reflect);
|
||||||
|
@ -10,17 +10,14 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks');
|
|||||||
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
||||||
const experimentalJsonModules = getOptionValue('--experimental-json-modules');
|
const experimentalJsonModules = getOptionValue('--experimental-json-modules');
|
||||||
const typeFlag = getOptionValue('--input-type');
|
const typeFlag = getOptionValue('--input-type');
|
||||||
|
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
|
||||||
const { resolve: moduleWrapResolve,
|
const { resolve: moduleWrapResolve,
|
||||||
getPackageType } = internalBinding('module_wrap');
|
getPackageType } = internalBinding('module_wrap');
|
||||||
const { pathToFileURL, fileURLToPath } = require('internal/url');
|
const { pathToFileURL, fileURLToPath } = require('internal/url');
|
||||||
const { ERR_INPUT_TYPE_NOT_ALLOWED,
|
const { ERR_INPUT_TYPE_NOT_ALLOWED,
|
||||||
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
|
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
|
||||||
|
|
||||||
const {
|
const { SafeMap } = primordials;
|
||||||
Object,
|
|
||||||
SafeMap
|
|
||||||
} = primordials;
|
|
||||||
|
|
||||||
const realpathCache = new SafeMap();
|
const realpathCache = new SafeMap();
|
||||||
|
|
||||||
@ -44,15 +41,11 @@ const legacyExtensionFormatMap = {
|
|||||||
'.node': 'commonjs'
|
'.node': 'commonjs'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (experimentalJsonModules) {
|
if (experimentalWasmModules)
|
||||||
// This is a total hack
|
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
|
||||||
Object.assign(extensionFormatMap, {
|
|
||||||
'.json': 'json'
|
if (experimentalJsonModules)
|
||||||
});
|
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
|
||||||
Object.assign(legacyExtensionFormatMap, {
|
|
||||||
'.json': 'json'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolve(specifier, parentURL) {
|
function resolve(specifier, parentURL) {
|
||||||
if (NativeModule.canBeRequiredByUsers(specifier)) {
|
if (NativeModule.canBeRequiredByUsers(specifier)) {
|
||||||
|
@ -153,7 +153,7 @@ class Loader {
|
|||||||
loaderInstance = async (url) => {
|
loaderInstance = async (url) => {
|
||||||
debug(`Translating dynamic ${url}`);
|
debug(`Translating dynamic ${url}`);
|
||||||
const { exports, execute } = await this._dynamicInstantiate(url);
|
const { exports, execute } = await this._dynamicInstantiate(url);
|
||||||
return createDynamicModule(exports, url, (reflect) => {
|
return createDynamicModule([], exports, url, (reflect) => {
|
||||||
debug(`Loading dynamic ${url}`);
|
debug(`Loading dynamic ${url}`);
|
||||||
execute(reflect.exports);
|
execute(reflect.exports);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/* global WebAssembly */
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
JSON,
|
||||||
|
Object,
|
||||||
SafeMap,
|
SafeMap,
|
||||||
StringPrototype,
|
StringPrototype
|
||||||
JSON
|
|
||||||
} = primordials;
|
} = primordials;
|
||||||
|
|
||||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||||
@ -72,11 +75,11 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) {
|
|||||||
];
|
];
|
||||||
if (module && module.loaded) {
|
if (module && module.loaded) {
|
||||||
const exports = module.exports;
|
const exports = module.exports;
|
||||||
return createDynamicModule(['default'], url, (reflect) => {
|
return createDynamicModule([], ['default'], url, (reflect) => {
|
||||||
reflect.exports.default.set(exports);
|
reflect.exports.default.set(exports);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return createDynamicModule(['default'], url, () => {
|
return createDynamicModule([], ['default'], url, () => {
|
||||||
debug(`Loading CJSModule ${url}`);
|
debug(`Loading CJSModule ${url}`);
|
||||||
// We don't care about the return val of _load here because Module#load
|
// 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
|
// 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);
|
module.compileForPublicLoader(true);
|
||||||
return createDynamicModule(
|
return createDynamicModule(
|
||||||
[...module.exportKeys, 'default'], url, (reflect) => {
|
[], [...module.exportKeys, 'default'], url, (reflect) => {
|
||||||
debug(`Loading BuiltinModule ${url}`);
|
debug(`Loading BuiltinModule ${url}`);
|
||||||
module.reflect = reflect;
|
module.reflect = reflect;
|
||||||
for (const key of module.exportKeys)
|
for (const key of module.exportKeys)
|
||||||
@ -116,7 +119,7 @@ translators.set('json', async function jsonStrategy(url) {
|
|||||||
let module = CJSModule._cache[modulePath];
|
let module = CJSModule._cache[modulePath];
|
||||||
if (module && module.loaded) {
|
if (module && module.loaded) {
|
||||||
const exports = module.exports;
|
const exports = module.exports;
|
||||||
return createDynamicModule(['default'], url, (reflect) => {
|
return createDynamicModule([], ['default'], url, (reflect) => {
|
||||||
reflect.exports.default.set(exports);
|
reflect.exports.default.set(exports);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -136,8 +139,32 @@ translators.set('json', async function jsonStrategy(url) {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
CJSModule._cache[modulePath] = module;
|
CJSModule._cache[modulePath] = module;
|
||||||
return createDynamicModule(['default'], url, (reflect) => {
|
return createDynamicModule([], ['default'], url, (reflect) => {
|
||||||
debug(`Parsing JSONModule ${url}`);
|
debug(`Parsing JSONModule ${url}`);
|
||||||
reflect.exports.default.set(module.exports);
|
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");
|
"--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 (!es_module_specifier_resolution.empty()) {
|
||||||
if (!experimental_modules) {
|
if (!experimental_modules) {
|
||||||
errors->push_back("--es-module-specifier-resolution requires "
|
errors->push_back("--es-module-specifier-resolution requires "
|
||||||
@ -274,6 +279,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
|||||||
"experimental ES Module support and caching modules",
|
"experimental ES Module support and caching modules",
|
||||||
&EnvironmentOptions::experimental_modules,
|
&EnvironmentOptions::experimental_modules,
|
||||||
kAllowedInEnvironment);
|
kAllowedInEnvironment);
|
||||||
|
AddOption("--experimental-wasm-modules",
|
||||||
|
"experimental ES Module support for webassembly modules",
|
||||||
|
&EnvironmentOptions::experimental_wasm_modules,
|
||||||
|
kAllowedInEnvironment);
|
||||||
AddOption("--experimental-policy",
|
AddOption("--experimental-policy",
|
||||||
"use the specified file as a "
|
"use the specified file as a "
|
||||||
"security policy",
|
"security policy",
|
||||||
|
@ -94,6 +94,7 @@ class EnvironmentOptions : public Options {
|
|||||||
bool experimental_json_modules = false;
|
bool experimental_json_modules = false;
|
||||||
bool experimental_modules = false;
|
bool experimental_modules = false;
|
||||||
std::string es_module_specifier_resolution;
|
std::string es_module_specifier_resolution;
|
||||||
|
bool experimental_wasm_modules = false;
|
||||||
std::string module_type;
|
std::string module_type;
|
||||||
std::string experimental_policy;
|
std::string experimental_policy;
|
||||||
bool experimental_repl_await = false;
|
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