diff --git a/src/presets/cloudflare.ts b/src/presets/cloudflare.ts index 37f226339f..e22102c8c0 100644 --- a/src/presets/cloudflare.ts +++ b/src/presets/cloudflare.ts @@ -11,6 +11,9 @@ export const cloudflare = defineNitroPreset({ preview: "npx wrangler dev ./server/index.mjs --site ./public --local", deploy: "npx wrangler deploy", }, + wasm: { + esmImport: true, + }, hooks: { async compiled(nitro: Nitro) { await writeFile( diff --git a/src/rollup/config.ts b/src/rollup/config.ts index 8d18fe0cc2..094dd0b339 100644 --- a/src/rollup/config.ts +++ b/src/rollup/config.ts @@ -4,11 +4,9 @@ import { dirname, join, normalize, relative, resolve } from "pathe"; import type { InputOptions, OutputOptions, Plugin } from "rollup"; import { defu } from "defu"; // import terser from "@rollup/plugin-terser"; // TODO: Investigate jiti issue -import type { RollupWasmOptions } from "@rollup/plugin-wasm"; import commonjs from "@rollup/plugin-commonjs"; import alias from "@rollup/plugin-alias"; import json from "@rollup/plugin-json"; -import wasmPlugin from "@rollup/plugin-wasm"; import inject from "@rollup/plugin-inject"; import { nodeResolve } from "@rollup/plugin-node-resolve"; import { isWindows } from "std-env"; @@ -24,6 +22,7 @@ import { runtimeDir } from "../dirs"; import { version } from "../../package.json"; import { replace } from "./plugins/replace"; import { virtual } from "./plugins/virtual"; +import { wasm } from "./plugins/wasm"; import { dynamicRequire } from "./plugins/dynamic-require"; import { NodeExternalsOptions, externals } from "./plugins/externals"; import { externals as legacyExternals } from "./plugins/externals-legacy"; @@ -147,12 +146,9 @@ export const getRollupConfig = (nitro: Nitro): RollupConfig => { // Raw asset loader rollupConfig.plugins.push(raw()); - // WASM import support + // WASM support if (nitro.options.experimental.wasm) { - const options = { - ...(nitro.options.experimental.wasm as RollupWasmOptions), - }; - rollupConfig.plugins.push(wasmPlugin(options)); + rollupConfig.plugins.push(wasm(nitro.options.wasm || {})); } // Build-time environment variables @@ -365,7 +361,10 @@ export const plugins = [ return { id: _resolved, external: false }; } } - if (!resolved || resolved.external) { + if ( + !resolved || + (resolved.external && resolved.resolvedBy !== "nitro:wasm-import") + ) { throw new Error( `Cannot resolve ${JSON.stringify(id)} from ${JSON.stringify( from diff --git a/src/rollup/plugins/wasm.ts b/src/rollup/plugins/wasm.ts new file mode 100644 index 0000000000..d15c1dabc7 --- /dev/null +++ b/src/rollup/plugins/wasm.ts @@ -0,0 +1,69 @@ +import { createHash } from "node:crypto"; +import { extname, basename } from "node:path"; +import { promises as fs } from "node:fs"; +import type { Plugin } from "rollup"; +import wasmBundle from "@rollup/plugin-wasm"; +import { WasmOptions } from "../../types"; + +const PLUGIN_NAME = "nitro:wasm-import"; +const wasmRegex = /\.wasm$/; + +export function wasm(options: WasmOptions): Plugin { + return options.esmImport ? wasmImport() : wasmBundle(options.rollup); +} + +export function wasmImport(): Plugin { + const copies = Object.create(null); + + return { + name: PLUGIN_NAME, + async resolveId(id: string, importer: string) { + if (copies[id]) { + return { + id: copies[id].publicFilepath, + external: true, + }; + } + if (wasmRegex.test(id)) { + const { id: filepath } = + (await this.resolve(id, importer, { skipSelf: true })) || {}; + if (!filepath || filepath === id) { + return null; + } + const buffer = await fs.readFile(filepath); + const hash = createHash("sha1") + .update(buffer) + .digest("hex") + .slice(0, 16); + const ext = extname(filepath); + const name = basename(filepath, ext); + + const outputFileName = `wasm/${name}-${hash}${ext}`; + const publicFilepath = `./${outputFileName}`; + + copies[id] = { + filename: outputFileName, + publicFilepath, + buffer, + }; + + return { + id: publicFilepath, + external: true, + }; + } + }, + async generateBundle() { + await Promise.all( + Object.keys(copies).map(async (name) => { + const copy = copies[name]; + await this.emitFile({ + type: "asset", + source: copy.buffer, + fileName: copy.filename, + }); + }) + ); + }, + }; +} diff --git a/src/types/nitro.ts b/src/types/nitro.ts index 953a3d67b5..5d694bd3d5 100644 --- a/src/types/nitro.ts +++ b/src/types/nitro.ts @@ -180,6 +180,20 @@ export interface NitroRouteRules proxy?: { to: string } & ProxyOptions; } +export interface WasmOptions { + /** + * Direct import the wasm file instead of bundling, required in Cloudflare Workers + * + * @default false + */ + esmImport?: boolean; + + /** + * Options for `@rollup/plugin-wasm`, only used when `esmImport` is `false` + */ + rollup?: RollupWasmOptions; +} + export interface NitroOptions extends PresetOptions { // Internal _config: NitroConfig; @@ -215,8 +229,9 @@ export interface NitroOptions extends PresetOptions { renderer?: string; serveStatic: boolean | "node" | "deno"; noPublicDir: boolean; + /** @experimental Requires `experimental.wasm` to be effective */ + wasm?: WasmOptions; experimental?: { - wasm?: boolean | RollupWasmOptions; legacyExternals?: boolean; openAPI?: boolean; /** @@ -227,6 +242,10 @@ export interface NitroOptions extends PresetOptions { * Enable native async context support for useEvent() */ asyncContext?: boolean; + /** + * Enable Experimental WebAssembly Support + */ + wasm?: boolean; }; future: { nativeSWR: boolean;