Skip to content
Open
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
5 changes: 1 addition & 4 deletions packages/cloudflare/src/cache/kv-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ function buildStorageKey(prefix: string, categoryPrefix: string, logicalKey: str
return `${prefix}${categoryPrefix}${HASHED_KEY_PREFIX}${fnv1a64(logicalKey)}`;
}

/**
* Create the deterministic key namespace shared by runtime cache operations
* and deploy-time prerender population.
*/
/** Create the deterministic key namespace for runtime cache operations. */
export function createKvKeySpace(appPrefix: string | undefined): KvKeySpace {
const prefix = normalizeAppPrefix(appPrefix);
return {
Expand Down
48 changes: 0 additions & 48 deletions packages/cloudflare/src/deploy-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import type { VinextCacheConfig } from "vinext/internal/cache-adapters";
import { findViteConfigPath } from "vinext/internal/utils/project";
import {
DEFAULT_KV_DATA_CACHE_BINDING,
type KvDataAdapterOptions,
} from "./cache/kv-data-adapter.js";
import {
DEFAULT_CDN_VERSION_METADATA_BINDING,
type CdnAdapterOptions,
Expand Down Expand Up @@ -118,12 +114,6 @@ export function viteConfigHasCacheAdapter(root: string): boolean {
return cacheFieldAssigned(block, "cdn") || cacheFieldAssigned(block, "data");
}

export type ResolvedKvDataAdapterConfig = {
binding: string;
appPrefix?: string;
ttlSeconds?: number;
};

export type ResolvedCdnAdapterConfig = {
versionMetadataBinding: string;
};
Expand Down Expand Up @@ -159,44 +149,6 @@ export function resolveCdnAdapterConfig(
};
}

function isCloudflareKvDataAdapterPath(adapter: string): boolean {
const normalized = adapter.replace(/\\/g, "/");
return (
normalized === "@vinext/cloudflare/cache/kv-data-adapter.runtime" ||
normalized === "@vinext/cloudflare/cache/kv-data-adapter.runtime.js" ||
normalized.endsWith("/cache/kv-data-adapter.runtime.js")
);
}

function readPositiveNumberOption(
options: KvDataAdapterOptions | undefined,
field: "ttlSeconds",
): number | undefined {
const value = options?.[field];
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
}

export function resolveKvDataAdapterConfig(
cache: VinextCacheConfig | null | undefined,
): ResolvedKvDataAdapterConfig | null {
const data = cache?.data;
if (!data?.adapter || !isCloudflareKvDataAdapterPath(data.adapter)) return null;

const options = data.options as KvDataAdapterOptions | undefined;
return {
binding:
typeof options?.binding === "string" && options.binding.length > 0
? options.binding
: DEFAULT_KV_DATA_CACHE_BINDING,
...(typeof options?.appPrefix === "string" && options.appPrefix.length > 0
? { appPrefix: options.appPrefix }
: {}),
...(readPositiveNumberOption(options, "ttlSeconds") !== undefined
? { ttlSeconds: readPositiveNumberOption(options, "ttlSeconds") }
: {}),
};
}

export function viteConfigHasImageAdapter(root: string): boolean {
const configPath = findViteConfigPath(root);
if (!configPath) return true;
Expand Down
7 changes: 4 additions & 3 deletions packages/cloudflare/src/deploy-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ export function formatDeployHelp(): string {
--verbose Print raw output from internal Wrangler commands
--no-promote Do not promote the uploaded Worker version to 100%
traffic
--prerender-all Pre-render discovered routes after building (future
releases will auto-populate the remote cache)
--prerender-all Deprecated for Worker deployments; use
--experimental-warm-cdn-cache instead (still
honored with next.config output: "export")
--prerender-concurrency <count>
Maximum number of routes to pre-render in parallel
Maximum parallel routes for output: "export"
--experimental-warm-cdn-cache
Upload a Worker version, warm build-discovered paths
through the production URL, then promote it (experimental)
Expand Down
150 changes: 14 additions & 136 deletions packages/cloudflare/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
*/

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { spawn, type SpawnOptions } from "node:child_process";
Expand All @@ -35,7 +34,6 @@ import {
findVinextCacheConfigInPlugins,
findVinextPrerenderConfigInPlugins,
findVinextRouteRootConfigInPlugins,
formatVinextPrerenderLabel,
isConfiguredCdnResponsePolicyHeader,
hasBuildIdentityResponseHeader,
hasUncachedRequestRouting,
Expand Down Expand Up @@ -73,7 +71,6 @@ import {
formatMissingCacheAdapterError,
formatImageOptimizationHint,
resolveCdnAdapterConfig,
resolveKvDataAdapterConfig,
viteConfigHasCacheAdapter,
viteConfigHasCloudflarePlugin,
viteConfigHasImageAdapter,
Expand All @@ -93,7 +90,6 @@ import { parseWorkerDeploymentUrl } from "./worker-deployment-url.js";
import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
import { normalizePathTrailingSlash } from "vinext/shims/url-utils";
import { cacheabilityRoutePathname } from "vinext/internal/server/cacheability-manifest";
import { buildPrerenderKVPairs, type KVBulkPair } from "./prerender-kv-populate.js";
import { writeCacheabilityManifestArtifact } from "./cacheability-artifact.js";
import {
DEFAULT_CACHEABILITY_PROBE_PHASE_TIMEOUT_MS,
Expand Down Expand Up @@ -546,52 +542,13 @@ async function runBuild(info: ProjectInfo, env: string | undefined): Promise<voi
});
}

async function populateKVCacheFromPrerenderedArtifacts(
root: string,
wranglerEnv: string | undefined,
cacheConfig: VinextCacheConfig | null,
): Promise<void> {
// `loadDeployViteConfigMetadata` returns null unless a cache adapter is declared.
const kvConfig = resolveKvDataAdapterConfig(cacheConfig);
if (!kvConfig) return;

const { routeCount, pairs } = buildPrerenderKVPairs(path.join(root, "dist", "server"), {
appPrefix: kvConfig.appPrefix,
ttlSeconds: kvConfig.ttlSeconds,
});

if (pairs.length === 0) {
console.log(
" KV cache: Skipping prerender upload (no App Router prerendered cache entries found).",
);
return;
}

await runWranglerKVBulkPut(root, {
binding: kvConfig.binding,
env: wranglerEnv,
pairs,
});

console.log(
` KV cache: Uploaded ${pairs.length} entr${pairs.length === 1 ? "y" : "ies"} for ${routeCount} prerendered route${routeCount === 1 ? "" : "s"}.`,
);
}

// ─── Deploy ──────────────────────────────────────────────────────────────────

type WranglerDeployArgs = {
args: string[];
env: string | undefined;
};

type WranglerKVBulkPutArgs = {
args: string[];
env: string | undefined;
};

const KV_BULK_PUT_CHUNK_SIZE = 25;

export function validateWranglerEnvName(env: string): string {
if (env.includes("\0")) {
throw new Error("Wrangler environment names cannot contain null bytes.");
Expand All @@ -616,19 +573,6 @@ export function buildWranglerDeployArgs(
return { args, env };
}

export function buildWranglerKVBulkPutArgs(options: {
binding: string;
env?: string;
filePath: string;
}): WranglerKVBulkPutArgs {
const env = options.env || undefined;
const args = ["kv", "bulk", "put", options.filePath, "--binding", options.binding, "--remote"];
if (env) {
args.push("--env", validateWranglerEnvName(env));
}
return { args, env };
}

/**
* Resolve Wrangler's JavaScript CLI entrypoint in node_modules.
*
Expand Down Expand Up @@ -675,58 +619,6 @@ export function buildWranglerInvocation(
return { ...buildNodeCliInvocation(wranglerBin, args, nodeExecutable), env };
}

export async function runWranglerKVBulkPut(
root: string,
options: {
binding: string;
env?: string;
pairs: KVBulkPair[];
tempDir?: string;
},
execute: typeof spawn = spawn,
nodeExecutable: string = process.execPath,
): Promise<void> {
const tempDir = fs.mkdtempSync(path.join(options.tempDir ?? os.tmpdir(), "vinext-kv-bulk-"));

try {
const wranglerBin = resolveWranglerBin(root);
const totalChunks = Math.ceil(options.pairs.length / KV_BULK_PUT_CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
const filePath = path.join(tempDir, `prerender-kv-${i}.json`);
const chunk = options.pairs.slice(
i * KV_BULK_PUT_CHUNK_SIZE,
(i + 1) * KV_BULK_PUT_CHUNK_SIZE,
);
fs.writeFileSync(filePath, JSON.stringify(chunk), "utf-8");
const { args } = buildWranglerKVBulkPutArgs({
binding: options.binding,
env: options.env,
filePath,
});
const invocation = buildNodeCliInvocation(wranglerBin, args, nodeExecutable);
const child = execute(invocation.file, invocation.args, {
cwd: root,
stdio: "inherit",
shell: false,
});
await new Promise<void>((resolve, reject) => {
child.once("error", reject);
child.once("close", (code, signal) => {
if (code === 0) {
resolve();
return;
}

const exitReason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
reject(new Error(`Wrangler KV bulk put failed with ${exitReason}.`));
});
});
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}

export async function runWranglerDeploy(
root: string,
options: Pick<DeployOptions, "preview" | "env" | "name" | "config" | "verbose"> & {
Expand Down Expand Up @@ -2057,6 +1949,7 @@ export async function deploy(options: DeployOptions): Promise<void> {
vinextPrerenderConfig,
nextOutput: nextConfig.output,
});
const shouldPrerenderLocally = prerenderDecision?.reason === "next-export";
const hasStrictResponseVary = hasVerbatimResponseVary(viteConfigMetadata.cacheConfig);
const warmupStatusSource = cacheWarmupStatusSource(viteConfigMetadata.cacheConfig);
const hasStagedRequestRouting =
Expand All @@ -2069,15 +1962,23 @@ export async function deploy(options: DeployOptions): Promise<void> {
viteConfigMetadata.cacheConfig,
);
const shouldEmitPrerenderPathManifest = !options.skipBuild && prerenderDecision;
if (prerenderDecision && !shouldPrerenderLocally) {
const trigger =
prerenderDecision.reason === "flag" ? "--prerender-all" : "vinext prerender config";
const replacement = options.warmCdnCache
? "Routes will be rendered and warmed through the staged Worker instead."
: "Use --experimental-warm-cdn-cache to render and warm routes through the deployed Worker instead.";
console.warn(`\n Warning: ${trigger} is ignored by Cloudflare deploy. ${replacement}`);
}
// Step 5: Build
if (!options.skipBuild) {
await runBuild(info, buildEnv);
} else {
console.log("\n Skipping build (--skip-build)");
}

const canWarmTpr = options.experimentalTPR && !prerenderDecision && hasBuildIdentityHeader;
if (options.experimentalTPR && prerenderDecision) {
const canWarmTpr = options.experimentalTPR && !shouldPrerenderLocally && hasBuildIdentityHeader;
if (options.experimentalTPR && shouldPrerenderLocally) {
console.log(" TPR: Skipping route selection (all-route prerendering configured)");
} else if (options.experimentalTPR && !hasBuildIdentityHeader) {
console.log(
Expand Down Expand Up @@ -2118,11 +2019,6 @@ export async function deploy(options: DeployOptions): Promise<void> {
const shouldWarmCdnCache = options.warmCdnCache || shouldWarmTpr;
const shouldSelectTpr = shouldWarmTpr && !options.warmCdnCache;
const candidatePathsOnly = shouldSelectTpr && !needsCacheabilityProbeManifest;
// Static export still needs local artifacts. Other pre-warm deploys render
// through the staged Worker so runtime-backed adapters populate themselves.
const shouldPrerenderLocally =
prerenderDecision && (!shouldWarmCdnCache || prerenderDecision.reason === "next-export");

if (shouldWarmCdnCache && cdnAdapterConfig) {
const wrangler = await loadProjectWranglerApi(info.root);
const previousCwd = process.cwd();
Expand Down Expand Up @@ -2160,13 +2056,10 @@ export async function deploy(options: DeployOptions): Promise<void> {
});
}

// Step 6a: prerender — render every discovered route into dist.
// Triggered only by --prerender-all, vinext({ prerender: true }), or
// output: 'export'. CDN warmup performs path discovery above, but relies on
// the deployed Worker to render and classify each response.
let ranPrerender = false;
// Step 6a: static export still requires local prerendered artifacts. Worker
// deployments render through the deployed Worker during CDN pre-warming.
if (shouldPrerenderLocally) {
console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
console.log("\n Pre-rendering all routes (output: 'export')...");
if (nextConfig.enablePrerenderSourceMaps) {
process.setSourceMapsEnabled(true);
Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 50);
Expand All @@ -2177,21 +2070,6 @@ export async function deploy(options: DeployOptions): Promise<void> {
nextConfig,
routeRootConfig: viteConfigMetadata.routeRootConfig,
});
ranPrerender = true;
}

if (ranPrerender) {
try {
await populateKVCacheFromPrerenderedArtifacts(
root,
deployEnv === "production" && !options.env ? undefined : deployEnv,
viteConfigMetadata.cacheConfig,
);
} catch (error) {
console.log(
` KV cache: Skipping prerender upload (${formatUnknownError(error)}). Continuing with deploy.`,
);
}
}

// Step 7: Deploy via wrangler
Expand Down
Loading
Loading