Skip to content
Merged
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
9 changes: 6 additions & 3 deletions packages/void/src/cli/cf-build-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import ignore from 'ignore';
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'pathslash';
import { migrateBuiltWranglerConfig, record } from './cf-config-migrate.ts';
import { buildContainerImage } from './container-image.ts';
import { readGeneratedWranglerConfigFile } from './generated-wrangler-config.ts';

export interface CfBuildOutput {
directory: string;
Expand Down Expand Up @@ -388,9 +389,11 @@ export async function prepareCfFrameworkBuildOutput(options: {
configPath: string;
accountId: string;
}): Promise<CfBuildOutput | null> {
const raw = record(JSON.parse(readFileSync(options.configPath, 'utf8')));
const assets = record(raw?.assets);
if (!raw || typeof raw.main !== 'string' || typeof assets?.directory !== 'string') {
// The root config is JSONC: it may be hand-written, or restored from void.lock.json with Void's
// published-traffic comment.
const raw = readGeneratedWranglerConfigFile(options.configPath);
const assets = record(raw.assets);
if (typeof raw.main !== 'string' || typeof assets?.directory !== 'string') {
return null;
}
const configDirectory = dirname(options.configPath);
Expand Down
102 changes: 101 additions & 1 deletion packages/void/test/unit/cf-build-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import {
writeContainerConfig,
} from '@cloudflare/build-output-utils';
import { cloudflare } from '@cloudflare/vite-plugin';
import { renderCloudflareStaticEntry } from '@void/deploy-cloudflare';
import {
renderCloudflareStaticEntry,
writeCloudflareFrameworkDeployConfig,
} from '@void/deploy-cloudflare';
import { toSlash } from 'pathslash';
import { createBuilder } from 'vite';
import { afterEach, expect, it, vi } from 'vite-plus/test';
Expand All @@ -33,6 +36,10 @@ import {
prepareCfFrameworkBuildOutput,
prepareCfStaticBuildOutput,
} from '../../src/cli/cf-build-output.ts';
import { resolveGeneratedDeployConfigPathDefault } from '../../src/cli/deploy-artifact-validation.ts';
import { deployPointerPath } from '../../src/cli/deploy-binding-snapshot.ts';
import { writeWranglerPublishedTrafficSnapshot } from '../../src/cli/wrangler.ts';
import { materializeCloudflareConfig } from '../../src/cloudflare-config.ts';
import { prepareCfBuildOutput } from '../../src/deploy.ts';

const roots: Array<string> = [];
Expand Down Expand Up @@ -828,6 +835,99 @@ it('bundles a generated static Worker and staged assets into prebuilt cf output'
);
});

/** The comment line `void domain add` writes, in the format of writeWranglerPublishedTrafficSnapshot. */
const PUBLISHED_TRAFFIC_COMMENT = `// void:published-traffic:v1:${Buffer.from(
JSON.stringify({ workers_dev: false, preview_urls: false, routes: [] }),
'utf8',
).toString('base64url')}`;

it(
'bundles framework output whose restored generated config keeps the published traffic comment',
async () => {
const root = mkdtempSync(join(tmpdir(), 'void-cf-framework-jsonc-test-'));
roots.push(root);
const output = join(root, 'framework');
mkdirSync(output);
writeFileSync(join(root, 'package.json'), JSON.stringify({ type: 'module' }));
writeFileSync(
join(root, 'void.config.ts'),
`export default { cloudflare: ${JSON.stringify({
name: 'void-framework-test',
account_id: 'account-id',
compatibility_date: '2026-09-29',
})} };\n`,
);
writeFileSync(
join(output, '_worker.js'),
'export default { fetch() { return new Response("ok") } };\n',
);
writeFileSync(join(output, 'index.html'), '<h1>public</h1>');
writeFileSync(join(output, '.assetsignore'), '_worker.js\n');

// `void domain add` records the traffic snapshot into the lock; a fresh checkout restores it.
const configPath = materializeCloudflareConfig(root);
writeWranglerPublishedTrafficSnapshot(configPath, {
workers_dev: false,
preview_urls: false,
routes: [{ pattern: 'app.example.com', custom_domain: true }],
});
rmSync(configPath);
expect(materializeCloudflareConfig(root)).toBe(configPath);
// `void deploy` then points the restored config at the framework output.
writeCloudflareFrameworkDeployConfig({
configPath,
deployPointerPath: deployPointerPath(root),
name: 'void-framework-test',
accountId: 'account-id',
main: 'framework/_worker.js',
assetsDirectory: 'framework',
compatibilityDate: '2026-09-29',
});
const deployConfigPath = resolveGeneratedDeployConfigPathDefault(root);
expect(readFileSync(deployConfigPath, 'utf8').startsWith('// void:published-traffic:v1:')).toBe(
true,
);

await withPrepared(
prepareCfFrameworkBuildOutput({
projectRoot: root,
configPath: deployConfigPath,
accountId: 'account-id',
}),
async (prepared) => {
const built = await readBuildOutput(prepared.directory);
expect(built.workers.default.config.manifest?.mainModule).toBe('index.js');
const assets = getWorkerAssetsDir(prepared.directory);
expect(readFileSync(join(assets, 'index.html'), 'utf8')).toContain('public');
expect(existsSync(join(assets, '_worker.js'))).toBe(false);
},
);
},
CF_BUILD_TIMEOUT_MS,
);

it('reads a framework config as JSONC and still rejects one that is not an object', async () => {
const output = worker({});
const configPath = join(output, 'wrangler.jsonc');
const prepare = () =>
prepareCfFrameworkBuildOutput({
projectRoot: dirname(output),
configPath,
accountId: 'account-id',
});
// A hand-written root config may have a BOM and trailing commas. It has no Worker entry here.
writeFileSync(configPath, `\uFEFF${PUBLISHED_TRAFFIC_COMMENT}\n{ "name": "void-test", }`);
await expect(prepare()).resolves.toBeNull();

for (const text of [
`${PUBLISHED_TRAFFIC_COMMENT}\n{ "main": `,
`${PUBLISHED_TRAFFIC_COMMENT}\n[]`,
]) {
writeFileSync(configPath, text);
await expect(prepare()).rejects.toThrow('Invalid Wrangler JSON/JSONC config.');
}
});

it.skipIf(process.platform === 'win32')(
'builds a named Sandbox image into cf prebuilt output and cleans up its local tag',
async () => {
Expand Down
Loading