Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/void/src/cli/cf-build-output.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from 'node:crypto';
import {
cpSync,
copyFileSync,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -339,6 +363,7 @@ async function prepareBundledBuildOutput(options: {
root: options.projectRoot,
publicDir: false,
logLevel: 'error',
plugins: [workerWasmModules()],
build: {
outDir: scratch,
emptyOutDir: true,
Expand Down
67 changes: 67 additions & 0 deletions packages/void/test/unit/cf-build-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down