diff --git a/README.md b/README.md index 7e0bb02..cc3ba7a 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ const instance = initSync(imports) ### `wasm-bindgen` Support +#### Target `web` (Recommended) + ```ts import init, { add } from 'some-pkg' import wasm from 'some-pkg/add_bg.wasm' // import wasm file @@ -65,6 +67,23 @@ await init({ add(1, 2) ``` +> [!NOTE] +> Other targets such as `nodejs` and `no-modules` are not supported. + +#### Target `bundler` (default) + +Enable `wasmBindgen` option in the plugin config. + +```ts +// rolldown.config.ts +import { defineConfig } from 'rolldown' +import { wasm } from 'rolldown-plugin-wasm' + +export default defineConfig({ + plugins: [wasm({ wasmBindgen: true })], +}) +``` + ### TypeScript Support ```jsonc @@ -122,6 +141,10 @@ export interface Options { * Configures what code is emitted to instantiate the Wasm (both inline and separate) */ targetEnv?: TargetEnv + /** + * Enable `wasm-bindgen` support + */ + wasmBindgen?: boolean } ``` diff --git a/src/index.ts b/src/index.ts index e067131..b146a0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ export function wasm(options: Options = {}): Plugin { publicPath = '', targetEnv, fileName = '[hash][extname]', + wasmBindgen, } = options const copies: Record< @@ -79,7 +80,15 @@ export function wasm(options: Options = {}): Plugin { this.addWatchFile(id) + const mod = this.getModuleInfo(id)! const buffer = await readFile(id) + + if (wasmBindgen) { + mod.meta.wasmExportNames = WebAssembly.Module.exports( + await WebAssembly.compile(buffer), + ).map((e) => e.name) + } + if (targetEnv === 'auto-inline') { return buffer.toString('binary') } @@ -100,8 +109,7 @@ export function wasm(options: Options = {}): Plugin { const publicFilepath = `${publicPath}${outputFileName}` // only copy if the file is not marked `sync`, `sync` files are always inlined - const isSync = !!this.getModuleInfo(id)?.meta?.wasmSync - if (!isSync) { + if (!mod.meta.wasmSync) { copies[id] = { filename: outputFileName, publicFilepath, @@ -117,7 +125,8 @@ export function wasm(options: Options = {}): Plugin { transform: { filter: { id: { include, exclude } }, handler(code, id) { - const isSync = !!this.getModuleInfo(id)?.meta?.wasmSync + const mod = this.getModuleInfo(id)! + const isSync = !!mod.meta.wasmSync const publicFilepath = copies[id] ? `'${copies[id].publicFilepath}'` : null @@ -132,12 +141,26 @@ export function wasm(options: Options = {}): Plugin { src = null } + let codegen = `import { loadWasmModule } from ${JSON.stringify(HELPERS_ID)} +function init(imports) { + return loadWasmModule(${isSync}, ${publicFilepath}, ${src}, imports) +}\n` + + if (wasmBindgen) { + const names = mod.meta.wasmExportNames as string[] + codegen += 'const instance = /* @__PURE__ */ await init({})\n' + codegen += names + .map((name) => { + return `export const ${name} = instance.exports[${JSON.stringify(name)}]` + }) + .join('\n') + } + + codegen += `\nexport default init` + return { - map: { - mappings: '', - }, - code: `import { loadWasmModule } from ${JSON.stringify(HELPERS_ID)}; -export default function(imports){return loadWasmModule(${isSync}, ${publicFilepath}, ${src}, imports)}`, + map: { mappings: '' }, + code: codegen, } }, }, diff --git a/src/options.ts b/src/options.ts index fa4fbae..ec2fbd2 100644 --- a/src/options.ts +++ b/src/options.ts @@ -43,4 +43,8 @@ export interface Options { * Configures what code is emitted to instantiate the Wasm (both inline and separate) */ targetEnv?: TargetEnv + /** + * Enable wasm-bindgen support + */ + wasmBindgen?: boolean } diff --git a/tests/__snapshots__/rolldown.test.ts.snap b/tests/__snapshots__/rolldown.test.ts.snap index 3124cdf..d0d63a0 100644 --- a/tests/__snapshots__/rolldown.test.ts.snap +++ b/tests/__snapshots__/rolldown.test.ts.snap @@ -35,9 +35,10 @@ function loadWasmModule(sync, filepath, src, imports) { //#endregion //#region tests/fixtures/add.wasm -function add_default(imports) { +function init(imports) { return loadWasmModule(false, "b62665e669ab1545.wasm", null, imports); } +var add_default = init; //#endregion //#region tests/fixtures/basic.js @@ -92,9 +93,10 @@ function loadWasmModule(sync, filepath, src, imports) { //#endregion //#region tests/fixtures/add.wasm -function add_default(imports) { +function init(imports) { return loadWasmModule(false, "b62665e669ab1545.wasm", null, imports); } +var add_default = init; //#endregion //#region tests/fixtures/basic.js @@ -137,9 +139,10 @@ function loadWasmModule(sync, filepath, src, imports) { //#endregion //#region tests/fixtures/add.wasm -function add_default(imports) { +function init(imports) { return loadWasmModule(false, "b62665e669ab1545.wasm", null, imports); } +var add_default = init; //#endregion //#region tests/fixtures/basic.js @@ -182,9 +185,10 @@ function loadWasmModule(sync, filepath, src, imports) { //#endregion //#region tests/fixtures/add.wasm -function add_default(imports) { +function init(imports) { return loadWasmModule(true, null, "AGFzbQEAAAABh4CAgAABYAJ/fwF/A4KAgIAAAQAFg4CAgAABABEHkICAgAACBm1lbW9yeQIAA2FkZAAAComAgIAAAQcAIAEgAGoLC7aEgIAAAgBBgIDAAAsIAQAAAAIAAAAAQYiAwAALnAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYCAgAAEbmFtZQGGgICAAAEAA2FkZAD+gICAAAlwcm9kdWNlcnMCCGxhbmd1YWdlAQRSdXN0BDIwMTgMcHJvY2Vzc2VkLWJ5AwVydXN0Yx0xLjM0LjEgKGZjNTBmMzI4YiAyMDE5LTA0LTI0KQZ3YWxydXMFMC43LjAMd2FzbS1iaW5kZ2VuEjAuMi40NSAoNmZhNmFmMjNiKQ==", imports); } +var add_default = init; //#endregion //#region tests/fixtures/sync.js @@ -237,9 +241,10 @@ function loadWasmModule(sync, filepath, src, imports) { //#endregion //#region tests/fixtures/add.wasm -function add_default(imports) { +function init(imports) { return loadWasmModule(true, null, "AGFzbQEAAAABh4CAgAABYAJ/fwF/A4KAgIAAAQAFg4CAgAABABEHkICAgAACBm1lbW9yeQIAA2FkZAAAComAgIAAAQcAIAEgAGoLC7aEgIAAAgBBgIDAAAsIAQAAAAIAAAAAQYiAwAALnAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYCAgAAEbmFtZQGGgICAAAEAA2FkZAD+gICAAAlwcm9kdWNlcnMCCGxhbmd1YWdlAQRSdXN0BDIwMTgMcHJvY2Vzc2VkLWJ5AwVydXN0Yx0xLjM0LjEgKGZjNTBmMzI4YiAyMDE5LTA0LTI0KQZ3YWxydXMFMC43LjAMd2FzbS1iaW5kZ2VuEjAuMi40NSAoNmZhNmFmMjNiKQ==", imports); } +var add_default = init; //#endregion //#region tests/fixtures/sync.js @@ -280,9 +285,10 @@ function loadWasmModule(sync, filepath, src, imports) { //#endregion //#region tests/fixtures/add.wasm -function add_default(imports) { +function init(imports) { return loadWasmModule(true, null, "AGFzbQEAAAABh4CAgAABYAJ/fwF/A4KAgIAAAQAFg4CAgAABABEHkICAgAACBm1lbW9yeQIAA2FkZAAAComAgIAAAQcAIAEgAGoLC7aEgIAAAgBBgIDAAAsIAQAAAAIAAAAAQYiAwAALnAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYCAgAAEbmFtZQGGgICAAAEAA2FkZAD+gICAAAlwcm9kdWNlcnMCCGxhbmd1YWdlAQRSdXN0BDIwMTgMcHJvY2Vzc2VkLWJ5AwVydXN0Yx0xLjM0LjEgKGZjNTBmMzI4YiAyMDE5LTA0LTI0KQZ3YWxydXMFMC43LjAMd2FzbS1iaW5kZ2VuEjAuMi40NSAoNmZhNmFmMjNiKQ==", imports); } +var add_default = init; //#endregion //#region tests/fixtures/sync.js @@ -291,3 +297,232 @@ var sync_default = add_default; //#endregion export { sync_default as default };" `; + +exports[`rolldown > wasm-bindgen.js > platform = browser 1`] = ` +"// b62665e669ab1545.wasm +[BINARY] +// wasm-bindgen.js +//#region rolldown:runtime +var __defProp = Object.defineProperty; +var __exportAll = (all, symbols) => { + let target = {}; + for (var name in all) { + __defProp(target, name, { + get: all[name], + enumerable: true + }); + } + if (symbols) { + __defProp(target, Symbol.toStringTag, { value: "Module" }); + } + return target; +}; + +//#endregion +//#region \\0wasm-helpers.js +function loadWasmModule(sync, filepath, src, imports) { + function instantiateOrCompile(source, imports$1, stream) { + const instantiate = stream ? WebAssembly.instantiateStreaming : WebAssembly.instantiate; + const compile = stream ? WebAssembly.compileStreaming : WebAssembly.compile; + if (imports$1) { + return instantiate(source, imports$1).then(({ instance: instance$1 }) => instance$1); + } else { + return compile(source); + } + } + let buf = null; + if (filepath) { + return instantiateOrCompile(fetch(filepath), imports, true); + } + const raw = globalThis.atob(src); + const len = raw.length; + buf = new Uint8Array(new ArrayBuffer(len)); + for (let i = 0; i < len; i++) { + buf[i] = raw.charCodeAt(i); + } + if (sync) { + const mod = new WebAssembly.Module(buf); + return imports ? new WebAssembly.Instance(mod, imports) : mod; + } else { + return instantiateOrCompile(buf, imports); + } +} + +//#endregion +//#region tests/fixtures/add.wasm +var add_exports = /* @__PURE__ */ __exportAll({ + add: () => add, + default: () => add_default, + memory: () => memory +}); +function init(imports) { + return loadWasmModule(false, "b62665e669ab1545.wasm", null, imports); +} +const instance = await init({}); +const memory = instance.exports["memory"]; +const add = instance.exports["add"]; +var add_default = init; + +//#endregion +//#region tests/fixtures/wasm-bindgen.js +var wasm_bindgen_default = add_exports; + +//#endregion +export { wasm_bindgen_default as default };" +`; + +exports[`rolldown > wasm-bindgen.js > platform = neutral 1`] = ` +"// b62665e669ab1545.wasm +[BINARY] +// wasm-bindgen.js +//#region rolldown:runtime +var __defProp = Object.defineProperty; +var __exportAll = (all, symbols) => { + let target = {}; + for (var name in all) { + __defProp(target, name, { + get: all[name], + enumerable: true + }); + } + if (symbols) { + __defProp(target, Symbol.toStringTag, { value: "Module" }); + } + return target; +}; + +//#endregion +//#region \\0wasm-helpers.js +function loadWasmModule(sync, filepath, src, imports) { + function instantiateOrCompile(source, imports$1, stream) { + const instantiate = stream ? WebAssembly.instantiateStreaming : WebAssembly.instantiate; + const compile = stream ? WebAssembly.compileStreaming : WebAssembly.compile; + if (imports$1) { + return instantiate(source, imports$1).then(({ instance: instance$1 }) => instance$1); + } else { + return compile(source); + } + } + let buf = null; + const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null; + if (filepath && isNode) { + const { readFile } = process.getBuiltinModule("fs/promises"); + const path = process.getBuiltinModule("path"); + return readFile(path.resolve(import.meta.dirname, filepath)).then((buffer) => instantiateOrCompile(buffer, imports)); + } else if (filepath) { + return instantiateOrCompile(fetch(filepath), imports, true); + } + if (isNode) { + const { Buffer: Buffer$1 } = process.getBuiltinModule("buffer"); + buf = Buffer$1.from(src, "base64"); + } else { + const raw = globalThis.atob(src); + const len = raw.length; + buf = new Uint8Array(new ArrayBuffer(len)); + for (let i = 0; i < len; i++) { + buf[i] = raw.charCodeAt(i); + } + } + if (sync) { + const mod = new WebAssembly.Module(buf); + return imports ? new WebAssembly.Instance(mod, imports) : mod; + } else { + return instantiateOrCompile(buf, imports); + } +} + +//#endregion +//#region tests/fixtures/add.wasm +var add_exports = /* @__PURE__ */ __exportAll({ + add: () => add, + default: () => add_default, + memory: () => memory +}); +function init(imports) { + return loadWasmModule(false, "b62665e669ab1545.wasm", null, imports); +} +const instance = await init({}); +const memory = instance.exports["memory"]; +const add = instance.exports["add"]; +var add_default = init; + +//#endregion +//#region tests/fixtures/wasm-bindgen.js +var wasm_bindgen_default = add_exports; + +//#endregion +export { wasm_bindgen_default as default };" +`; + +exports[`rolldown > wasm-bindgen.js > platform = node 1`] = ` +"// b62665e669ab1545.wasm +[BINARY] +// wasm-bindgen.js +import "node:module"; + +//#region rolldown:runtime +var __defProp = Object.defineProperty; +var __exportAll = (all, symbols) => { + let target = {}; + for (var name in all) { + __defProp(target, name, { + get: all[name], + enumerable: true + }); + } + if (symbols) { + __defProp(target, Symbol.toStringTag, { value: "Module" }); + } + return target; +}; + +//#endregion +//#region \\0wasm-helpers.js +function loadWasmModule(sync, filepath, src, imports) { + function instantiateOrCompile(source, imports$1, stream) { + const instantiate = stream ? WebAssembly.instantiateStreaming : WebAssembly.instantiate; + const compile = stream ? WebAssembly.compileStreaming : WebAssembly.compile; + if (imports$1) { + return instantiate(source, imports$1).then(({ instance: instance$1 }) => instance$1); + } else { + return compile(source); + } + } + let buf = null; + if (filepath) { + const { readFile } = process.getBuiltinModule("fs/promises"); + const path = process.getBuiltinModule("path"); + return readFile(path.resolve(import.meta.dirname, filepath)).then((buffer) => instantiateOrCompile(buffer, imports)); + } + const { Buffer: Buffer$1 } = process.getBuiltinModule("buffer"); + buf = Buffer$1.from(src, "base64"); + if (sync) { + const mod = new WebAssembly.Module(buf); + return imports ? new WebAssembly.Instance(mod, imports) : mod; + } else { + return instantiateOrCompile(buf, imports); + } +} + +//#endregion +//#region tests/fixtures/add.wasm +var add_exports = /* @__PURE__ */ __exportAll({ + add: () => add, + default: () => add_default, + memory: () => memory +}); +function init(imports) { + return loadWasmModule(false, "b62665e669ab1545.wasm", null, imports); +} +const instance = await init({}); +const memory = instance.exports["memory"]; +const add = instance.exports["add"]; +var add_default = init; + +//#endregion +//#region tests/fixtures/wasm-bindgen.js +var wasm_bindgen_default = add_exports; + +//#endregion +export { wasm_bindgen_default as default };" +`; diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 8319b0a..9fec441 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -21,6 +21,10 @@ describe('e2e', () => { test('maxFileSize = 0', async () => { await e2e(platform, false, 0) }) + + test('with wasmBindgen', async () => { + await e2e(platform, false, undefined, true) + }) }) }) @@ -28,17 +32,28 @@ async function e2e( platform: 'node' | 'browser', sync: boolean, maxFileSize?: number, + wasmBindgen?: boolean, ) { const { chunks } = await rolldownBuild( path.resolve( import.meta.dirname, - sync ? 'fixtures/sync.js' : 'fixtures/basic.js', + sync + ? 'fixtures/sync.js' + : wasmBindgen + ? 'fixtures/wasm-bindgen.js' + : 'fixtures/basic.js', ), - [wasm({ maxFileSize })], - { platform }, + [wasm({ maxFileSize, wasmBindgen })], + { + platform, + treeshake: { + moduleSideEffects: false, + }, + }, ) - const mod = new SourceTextModule(chunks[0].code, { + const code = chunks[0].code + const mod = new SourceTextModule(code, { context: createContext({ atob: platform === 'browser' ? atob : undefined, fetch: @@ -69,14 +84,24 @@ async function e2e( meta.dirname = process.cwd() }, }) - await mod.link(() => { - throw new Error('No imports expected') + await mod.link((spec) => { + throw new Error(`No imports expected: ${spec}`) }) await mod.evaluate() + const exported = (mod.namespace as any).default + + let init: (imports?: any) => any + if (wasmBindgen) { + const { add } = (mod.namespace as any).default + expect(add(1, 2)).toBe(3) + init = exported.default + } else { + init = exported + } { // module - const ret = (mod.namespace as any).default() + const ret = init() expect(ret).a(sync ? 'WebAssembly.Module' : 'Promise') const wasmModule: WebAssembly.Module = await ret @@ -86,7 +111,7 @@ async function e2e( { // instance - const ret = (mod.namespace as any).default({}) + const ret = init({}) expect(ret).a(sync ? 'WebAssembly.Instance' : 'Promise') const instance = await ret diff --git a/tests/fixtures/wasm-bindgen.js b/tests/fixtures/wasm-bindgen.js new file mode 100644 index 0000000..af4f53a --- /dev/null +++ b/tests/fixtures/wasm-bindgen.js @@ -0,0 +1,2 @@ +import * as wasm from './add.wasm' +export default wasm diff --git a/tests/rolldown.test.ts b/tests/rolldown.test.ts index ba2e2a8..c1a4aff 100644 --- a/tests/rolldown.test.ts +++ b/tests/rolldown.test.ts @@ -9,9 +9,16 @@ describe('rolldown', async () => { await testFixtures( '*.js', async (args, id) => { - const { snapshot } = await rolldownBuild(id, [wasm({ maxFileSize: 0 })], { - platform: args.platform, - }) + const { snapshot } = await rolldownBuild( + id, + [ + wasm({ + maxFileSize: 0, + wasmBindgen: id.includes('wasm-bindgen'), + }), + ], + { platform: args.platform }, + ) return snapshot }, {