diff options
Diffstat (limited to 'packages/integrations/cloudflare/src/utils/wasm-module-loader.ts')
-rw-r--r-- | packages/integrations/cloudflare/src/utils/wasm-module-loader.ts | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts b/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts new file mode 100644 index 000000000..7d34d48c3 --- /dev/null +++ b/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts @@ -0,0 +1,119 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { type Plugin } from 'vite'; + +/** + * Loads '*.wasm?module' imports as WebAssembly modules, which is the only way to load WASM in cloudflare workers. + * Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration + * Cloudflare worker WASM from javascript support: https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/ + * @param disabled - if true throws a helpful error message if wasm is encountered and wasm imports are not enabled, + * otherwise it will error obscurely in the esbuild and vite builds + * @param assetsDirectory - the folder name for the assets directory in the build directory. Usually '_astro' + * @returns Vite plugin to load WASM tagged with '?module' as a WASM modules + */ +export function wasmModuleLoader({ + disabled, + assetsDirectory, +}: { + disabled: boolean; + assetsDirectory: string; +}): Plugin { + const postfix = '.wasm?module'; + let isDev = false; + + return { + name: 'vite:wasm-module-loader', + enforce: 'pre', + configResolved(config) { + isDev = config.command === 'serve'; + }, + config(_, __) { + // let vite know that file format and the magic import string is intentional, and will be handled in this plugin + return { + assetsInclude: ['**/*.wasm?module'], + build: { rollupOptions: { external: /^__WASM_ASSET__.+\.wasm\.mjs$/i } }, + }; + }, + + load(id, _) { + if (!id.endsWith(postfix)) { + return; + } + if (disabled) { + throw new Error( + `WASM module's cannot be loaded unless you add \`wasmModuleImports: true\` to your astro config.` + ); + } + + const filePath = id.slice(0, -1 * '?module'.length); + + const data = fs.readFileSync(filePath); + const base64 = data.toString('base64'); + + const base64Module = ` +const wasmModule = new WebAssembly.Module(Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0))); +export default wasmModule +`; + if (isDev) { + // no need to wire up the assets in dev mode, just rewrite + return base64Module; + } else { + // just some shared ID + let hash = hashString(base64); + // emit the wasm binary as an asset file, to be picked up later by the esbuild bundle for the worker. + // give it a shared deterministic name to make things easy for esbuild to switch on later + const assetName = path.basename(filePath).split('.')[0] + '.' + hash + '.wasm'; + this.emitFile({ + type: 'asset', + // put it explicitly in the _astro assets directory with `fileName` rather than `name` so that + // vite doesn't give it a random id in its name. We need to be able to easily rewrite from + // the .mjs loader and the actual wasm asset later in the ESbuild for the worker + fileName: path.join(assetsDirectory, assetName), + source: fs.readFileSync(filePath), + }); + + // however, by default, the SSG generator cannot import the .wasm as a module, so embed as a base64 string + const chunkId = this.emitFile({ + type: 'prebuilt-chunk', + fileName: assetName + '.mjs', + code: base64Module, + }); + + return ` +import wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs"; +export default wasmModule; + `; + } + }, + + // output original wasm file relative to the chunk + renderChunk(code, chunk, _) { + if (isDev) return; + + if (!/__WASM_ASSET__/g.test(code)) return; + + const final = code.replaceAll(/__WASM_ASSET__([a-z\d]+).wasm.mjs/g, (s, assetId) => { + const fileName = this.getFileName(assetId); + const relativePath = path + .relative(path.dirname(chunk.fileName), fileName) + .replaceAll('\\', '/'); // fix windows paths for import + return `./${relativePath}`; + }); + + return { code: final }; + }, + }; +} + +/** + * Returns a deterministic 32 bit hash code from a string + */ +function hashString(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash &= hash; // Convert to 32bit integer + } + return new Uint32Array([hash])[0].toString(36); +} |