From 3570df2da6cea395c50b9434900ab6af64e8063c Mon Sep 17 00:00:00 2001 From: Pleasurecruise <3196812536@qq.com> Date: Wed, 21 Oct 2026 21:45:10 +0100 Subject: [PATCH] fix(deploy): keep framework Worker Wasm imports as Wasm modules (#535) --- packages/void/src/cli/cf-build-output.ts | 25 +++++++ .../void/test/unit/cf-build-output.test.ts | 67 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/packages/void/src/cli/cf-build-output.ts b/packages/void/src/cli/cf-build-output.ts index e67ee17e4..9bb57026e 100644 --- a/packages/void/src/cli/cf-build-output.ts +++ b/packages/void/src/cli/cf-build-output.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { cpSync, copyFileSync, @@ -26,6 +27,7 @@ import { import { type ModuleType, OutputContainerSchema } from '@cloudflare/config'; import ignore from 'ignore'; import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'pathslash'; +import type { Plugin } from 'vite'; import { migrateBuiltWranglerConfig, record } from './cf-config-migrate.ts'; import { buildContainerImage } from './container-image.ts'; import { readGeneratedWranglerConfigFile } from './generated-wrangler-config.ts'; @@ -321,6 +323,28 @@ async function prepareNativeBuildOutput( } } +/** Emit `.wasm` imports beside the bundle so cf uploads them as Worker Wasm modules. */ +function workerWasmModules(): Plugin { + return { + name: 'void:worker-wasm-modules', + enforce: 'pre', + async resolveId(source, importer) { + if (!source.endsWith('.wasm')) { + return null; + } + const resolved = await this.resolve(source, importer, { skipSelf: true }); + if (!resolved) { + return null; + } + const wasm = readFileSync(resolved.id); + const hash = createHash('sha256').update(wasm).digest('hex').slice(0, 8); + const fileName = `${basename(resolved.id, '.wasm')}-${hash}.wasm`; + this.emitFile({ type: 'asset', fileName, source: wasm }); + return { id: `./${fileName}`, external: true }; + }, + }; +} + /** Bundle a generated Worker entry with Vite, then hand the result to `prepareCfBuildOutput`. */ async function prepareBundledBuildOutput(options: { projectRoot: string; @@ -339,6 +363,7 @@ async function prepareBundledBuildOutput(options: { root: options.projectRoot, publicDir: false, logLevel: 'error', + plugins: [workerWasmModules()], build: { outDir: scratch, emptyOutDir: true, diff --git a/packages/void/test/unit/cf-build-output.test.ts b/packages/void/test/unit/cf-build-output.test.ts index fd3b4e4f3..1ff77a053 100644 --- a/packages/void/test/unit/cf-build-output.test.ts +++ b/packages/void/test/unit/cf-build-output.test.ts @@ -562,6 +562,73 @@ it( CF_BUILD_TIMEOUT_MS, ); +it( + 'keeps framework Wasm imports as Worker Wasm modules', + async () => { + const root = mkdtempSync(join(tmpdir(), 'void-cf-framework-wasm-test-')); + roots.push(root); + const output = join(root, 'framework'); + mkdirSync(join(output, 'chunks'), { recursive: true }); + writeFileSync(join(root, 'package.json'), JSON.stringify({ type: 'module' })); + writeFileSync( + join(root, 'wrangler.json'), + JSON.stringify({ + name: 'void-framework-test', + account_id: 'account-id', + main: 'framework/_worker.js', + compatibility_date: '2026-09-29', + assets: { directory: 'framework', binding: 'ASSETS' }, + }), + ); + // Imports `wbg`, like wasm-bindgen output. + const bindgenWasm = Buffer.from([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 9, 1, 3, 119, 98, 103, 1, 102, 0, 0, + ]); + const plainWasm = Buffer.from([0, 97, 115, 109, 1, 0, 0, 0]); + mkdirSync(join(root, 'node_modules/bindgen'), { recursive: true }); + writeFileSync(join(root, 'node_modules/bindgen/index_bg.wasm'), bindgenWasm); + writeFileSync(join(output, 'plain.wasm'), plainWasm); + writeFileSync( + join(output, '_worker.js'), + "import plain from './plain.wasm'; import { render } from './chunks/render.js';\n" + + 'export default { async fetch() { return new Response(String(plain) + await render()) } };\n', + ); + writeFileSync( + join(output, 'chunks/render.js'), + "import plain from '../plain.wasm';\n" + + "export async function render() { return String(plain) + String(await import('bindgen/index_bg.wasm')); }\n", + ); + writeFileSync(join(output, '.assetsignore'), '_worker.js\n'); + + await withPrepared( + prepareCfFrameworkBuildOutput({ + projectRoot: root, + configPath: join(root, 'wrangler.json'), + accountId: 'account-id', + }), + async (prepared) => { + const built = await readBuildOutput(prepared.directory); + const modules = built.workers.default.config.manifest?.modules ?? {}; + const wasm = Object.keys(modules).filter((name) => modules[name].type === 'wasm'); + const bundle = getWorkerBundleDir(prepared.directory); + expect(wasm.map((name) => readFileSync(join(bundle, name)))).toEqual( + expect.arrayContaining([bindgenWasm, plainWasm]), + ); + expect(wasm).toHaveLength(2); + const code = readdirSync(bundle) + .filter((name) => name.endsWith('.js')) + .map((name) => readFileSync(join(bundle, name), 'utf8')) + .join('\n'); + for (const name of wasm) { + expect(code).toContain(`./${name}`); + } + expectCfDryRun(prepared.directory); + }, + ); + }, + CF_BUILD_TIMEOUT_MS, +); + it( 'accepts a nested hashed Worker entry with a relative module import', async () => {