From 441ffe4701d880806b4f406ed17b5c1d691fc266 Mon Sep 17 00:00:00 2001 From: Pleasurecruise <3196812536@qq.com> Date: Wed, 21 Oct 2026 21:50:17 +0100 Subject: [PATCH 1/2] fix(deploy): emit additional modules in framework Worker bundles (#535) --- packages/void/src/cli/cf-build-output.ts | 33 +++++++++ .../void/test/unit/cf-build-output.test.ts | 73 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/packages/void/src/cli/cf-build-output.ts b/packages/void/src/cli/cf-build-output.ts index e67ee17e4..4985adc16 100644 --- a/packages/void/src/cli/cf-build-output.ts +++ b/packages/void/src/cli/cf-build-output.ts @@ -26,6 +26,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 +322,37 @@ async function prepareNativeBuildOutput( } } +// The Cloudflare Vite plugin's additional module rules (CompiledWasm, Data and Text). +const additionalModuleRE = /\.(?:wasm(?:\?module)?|bin|txt|html|sql)$/; +const moduleReferenceRE = /__VOID_MODULE__(.*?)__VOID_MODULE__/g; + +/** Emit additional module imports beside the bundle, like `@cloudflare/vite-plugin`. */ +function additionalModules(): Plugin { + return { + name: 'void:additional-modules', + enforce: 'pre', + async resolveId(source, importer, options) { + if (!additionalModuleRE.test(source) && !source.startsWith('#')) { + return null; + } + const resolved = await this.resolve(source, importer, { ...options, skipSelf: true }); + if (!resolved || !additionalModuleRE.test(resolved.id)) { + return resolved; + } + return { id: `__VOID_MODULE__${resolved.id.split('?')[0]}__VOID_MODULE__`, external: true }; + }, + renderChunk(code, chunk) { + return code.replace(moduleReferenceRE, (_, path: string) => { + const fileName = this.getFileName( + this.emitFile({ type: 'asset', name: basename(path), source: readFileSync(path) }), + ); + const importPath = relative(dirname(chunk.fileName), fileName); + return importPath.startsWith('.') ? importPath : `./${importPath}`; + }); + }, + }; +} + /** Bundle a generated Worker entry with Vite, then hand the result to `prepareCfBuildOutput`. */ async function prepareBundledBuildOutput(options: { projectRoot: string; @@ -339,6 +371,7 @@ async function prepareBundledBuildOutput(options: { root: options.projectRoot, publicDir: false, logLevel: 'error', + plugins: [additionalModules()], 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..b057efd4d 100644 --- a/packages/void/test/unit/cf-build-output.test.ts +++ b/packages/void/test/unit/cf-build-output.test.ts @@ -562,6 +562,79 @@ it( CF_BUILD_TIMEOUT_MS, ); +it( + 'keeps framework Wasm, text and data imports as Worker 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', imports: { '#plain': './framework/plain.wasm' } }), + ); + 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, 'message.txt'), 'text module'); + writeFileSync(join(output, 'data.bin'), Buffer.from([0, 255])); + writeFileSync( + join(output, '_worker.js'), + "import plain from './plain.wasm'; import message from './message.txt'; import data from './data.bin';\n" + + "import { render } from './chunks/render.js';\n" + + 'export default { async fetch() { return new Response(String([plain, message, data, await render()])) } };\n', + ); + writeFileSync( + join(output, 'chunks/render.js'), + "import plain from '#plain';\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 bundle = getWorkerBundleDir(prepared.directory); + const additional = Object.keys(modules) + .filter((name) => modules[name].type !== 'esm') + .map((name) => [modules[name].type, readFileSync(join(bundle, name))]); + expect(additional).toHaveLength(4); + expect(additional).toEqual( + expect.arrayContaining([ + ['wasm', bindgenWasm], + ['wasm', plainWasm], + ['text', Buffer.from('text module')], + ['data', Buffer.from([0, 255])], + ]), + ); + expectCfDryRun(prepared.directory); + }, + ); + }, + CF_BUILD_TIMEOUT_MS, +); + it( 'accepts a nested hashed Worker entry with a relative module import', async () => { From 35512d218163181b467f1e8ef1ca072bbf727eb5 Mon Sep 17 00:00:00 2001 From: Pleasurecruise <3196812536@qq.com> Date: Wed, 21 Oct 2026 22:13:01 +0100 Subject: [PATCH 2/2] refactor(deploy): follow plugin naming and drop the manifest fallback in the test --- packages/void/src/cli/cf-build-output.ts | 8 ++++---- packages/void/test/unit/cf-build-output.test.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/void/src/cli/cf-build-output.ts b/packages/void/src/cli/cf-build-output.ts index 4985adc16..6f3080b67 100644 --- a/packages/void/src/cli/cf-build-output.ts +++ b/packages/void/src/cli/cf-build-output.ts @@ -324,10 +324,10 @@ async function prepareNativeBuildOutput( // The Cloudflare Vite plugin's additional module rules (CompiledWasm, Data and Text). const additionalModuleRE = /\.(?:wasm(?:\?module)?|bin|txt|html|sql)$/; -const moduleReferenceRE = /__VOID_MODULE__(.*?)__VOID_MODULE__/g; +const modulePlaceholderRE = /__VOID_MODULE__(.*?)__VOID_MODULE__/g; /** Emit additional module imports beside the bundle, like `@cloudflare/vite-plugin`. */ -function additionalModules(): Plugin { +function additionalModulesPlugin(): Plugin { return { name: 'void:additional-modules', enforce: 'pre', @@ -342,7 +342,7 @@ function additionalModules(): Plugin { return { id: `__VOID_MODULE__${resolved.id.split('?')[0]}__VOID_MODULE__`, external: true }; }, renderChunk(code, chunk) { - return code.replace(moduleReferenceRE, (_, path: string) => { + return code.replace(modulePlaceholderRE, (_, path: string) => { const fileName = this.getFileName( this.emitFile({ type: 'asset', name: basename(path), source: readFileSync(path) }), ); @@ -371,7 +371,7 @@ async function prepareBundledBuildOutput(options: { root: options.projectRoot, publicDir: false, logLevel: 'error', - plugins: [additionalModules()], + plugins: [additionalModulesPlugin()], 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 b057efd4d..cfe0e96e0 100644 --- a/packages/void/test/unit/cf-build-output.test.ts +++ b/packages/void/test/unit/cf-build-output.test.ts @@ -614,7 +614,7 @@ it( }), async (prepared) => { const built = await readBuildOutput(prepared.directory); - const modules = built.workers.default.config.manifest?.modules ?? {}; + const { modules } = built.workers.default.config.manifest!; const bundle = getWorkerBundleDir(prepared.directory); const additional = Object.keys(modules) .filter((name) => modules[name].type !== 'esm')