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
33 changes: 33 additions & 0 deletions packages/void/src/cli/cf-build-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 modulePlaceholderRE = /__VOID_MODULE__(.*?)__VOID_MODULE__/g;

/** Emit additional module imports beside the bundle, like `@cloudflare/vite-plugin`. */
function additionalModulesPlugin(): 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(modulePlaceholderRE, (_, 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;
Expand All @@ -339,6 +371,7 @@ async function prepareBundledBuildOutput(options: {
root: options.projectRoot,
publicDir: false,
logLevel: 'error',
plugins: [additionalModulesPlugin()],
build: {
outDir: scratch,
emptyOutDir: true,
Expand Down
73 changes: 73 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,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!;
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 () => {
Expand Down