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
55 changes: 37 additions & 18 deletions packages/cloudflare/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import { pathToFileURL } from "node:url";
import { runPrerender } from "vinext/internal/build/run-prerender";
import { loadDotenv } from "vinext/internal/config/dotenv";
import { loadNextConfig, resolveNextConfig } from "vinext/internal/config/next-config";
import {
formatVinextPrerenderLabel,
loadVinextPrerenderConfigFromViteConfig,
resolveVinextPrerenderDecision,
} from "vinext/internal/config/prerender";
import {
detectProject,
findInNodeModules,
Expand Down Expand Up @@ -63,6 +68,8 @@ export type DeployOptions = {
tprWindow?: number;
};

type ProjectViteApi = Pick<typeof import("vite"), "createBuilder" | "loadConfigFromFile">;

function parsePositiveIntegerArg(raw: string, flag: string): number {
if (raw === "") {
throw new Error(`${flag} requires a value, but none was provided.`);
Expand Down Expand Up @@ -159,23 +166,25 @@ export async function withCloudflareEnv<T>(
}
}

async function runBuild(info: ProjectInfo, env: string | undefined): Promise<void> {
console.log("\n Building for Cloudflare Workers...\n");

async function loadProjectViteApi(root: string): Promise<ProjectViteApi> {
// Resolve Vite from the project root so that symlinked vinext installs
// (bun link / npm link) use the project's Vite, not the monorepo copy.
// This mirrors the loadVite() pattern in cli.ts.
let vitePath: string;
try {
const req = createRequire(path.join(info.root, "package.json"));
const req = createRequire(path.join(root, "package.json"));
vitePath = req.resolve("vite");
} catch {
vitePath = "vite";
}
const viteUrl = vitePath === "vite" ? vitePath : pathToFileURL(vitePath).href;
const { createBuilder } = (await import(/* @vite-ignore */ viteUrl)) as {
createBuilder: typeof import("vite").createBuilder;
};
return (await import(/* @vite-ignore */ viteUrl)) as ProjectViteApi;
}

async function runBuild(info: ProjectInfo, env: string | undefined): Promise<void> {
console.log("\n Building for Cloudflare Workers...\n");

const { createBuilder } = await loadProjectViteApi(info.root);

// Use Vite's JS API for the build. The Vite config prepared by `vinext init`
// has the cloudflare() plugin which handles the Worker output format.
Expand Down Expand Up @@ -378,26 +387,36 @@ export async function deploy(options: DeployOptions): Promise<void> {
}

// Step 5: Build
const buildEnv = deployEnv === "production" && !options.env ? undefined : deployEnv;
if (!options.skipBuild) {
await runBuild(info, deployEnv === "production" && !options.env ? undefined : deployEnv);
await runBuild(info, buildEnv);
} else {
console.log("\n Skipping build (--skip-build)");
}

// Step 6a: prerender — render every discovered route into dist.
// Triggered by --prerender-all, or automatically when next.config.js
// sets `output: 'export'` (every route must be statically exportable).
// Triggered by --prerender-all, vinext({ prerender: true }), or automatically
// when next.config.js sets `output: 'export'` (every route must be statically
// exportable). The CLI flag wins when more than one trigger is present.
{
const rawNextConfig = await loadNextConfig(info.root);
const nextConfig = await resolveNextConfig(rawNextConfig, info.root);
const isStaticExport = nextConfig.output === "export";

if (options.prerenderAll || isStaticExport) {
const label =
isStaticExport && !options.prerenderAll
? "Pre-rendering all routes (output: 'export')..."
: "Pre-rendering all routes...";
console.log(`\n ${label}`);

const shouldLoadVinextPrerenderConfig = !options.prerenderAll && nextConfig.output !== "export";
const vinextPrerenderConfig = shouldLoadVinextPrerenderConfig
? await withCloudflareEnv(buildEnv, async () => {
const vite = await loadProjectViteApi(info.root);
return loadVinextPrerenderConfigFromViteConfig(vite, info.root);
})
: null;
const prerenderDecision = resolveVinextPrerenderDecision({
prerenderAllFlag: options.prerenderAll,
vinextPrerenderConfig,
nextOutput: nextConfig.output,
});

if (prerenderDecision) {
console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
if (nextConfig.enablePrerenderSourceMaps) {
process.setSourceMapsEnabled(true);
Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 50);
Expand Down
4 changes: 4 additions & 0 deletions packages/vinext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@
"types": "./dist/config/next-config.d.ts",
"import": "./dist/config/next-config.js"
},
"./internal/config/prerender": {
"types": "./dist/config/prerender.d.ts",
"import": "./dist/config/prerender.js"
},
"./server/app-rsc-handler": {
"types": "./dist/server/app-rsc-handler.d.ts",
"import": "./dist/server/app-rsc-handler.js"
Expand Down
13 changes: 11 additions & 2 deletions packages/vinext/src/cache/cache-adapters-virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ export function generateCacheAdaptersModule(cache?: VinextCacheConfig): string {
"",
"// A factory that throws (e.g. a missing binding on an incompatible runtime)",
"// is logged and skipped so the default handler stays in place.",
"function __vinextFormatAdapterError(error) {",
" if (error instanceof Error && error.message) return error.message;",
" try {",
" return String(error);",
" } catch {",
" return '<unknown error>';",
" }",
"}",
"",
"let __vinextCacheAdaptersRegistered = false;",
"",
"export function registerConfiguredCacheAdapters(env) {",
Expand All @@ -113,7 +122,7 @@ export function generateCacheAdaptersModule(cache?: VinextCacheConfig): string {
)} }));`,
" } catch (error) {",
' console.warn("[vinext] failed to initialize the configured data cache adapter; ' +
'using the default handler.", error);',
'using the default handler.\\n" + __vinextFormatAdapterError(error));',
" }",
);
}
Expand All @@ -126,7 +135,7 @@ export function generateCacheAdaptersModule(cache?: VinextCacheConfig): string {
)} }));`,
" } catch (error) {",
' console.warn("[vinext] failed to initialize the configured CDN cache adapter; ' +
'using the default adapter.", error);',
'using the default adapter.\\n" + __vinextFormatAdapterError(error));',
" }",
);
}
Expand Down
51 changes: 38 additions & 13 deletions packages/vinext/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { deploy as runDeploy, parseDeployArgs } from "@vinext/cloudflare/interna
import { printDeployHelp } from "@vinext/cloudflare/internal/deploy-help";
import { runCheck, formatReport } from "./check.js";
import { init as runInit, getReactUpgradeDeps } from "./init.js";
import { INIT_PLATFORMS, resolveInitPlatform } from "./init-platform.js";
import { INIT_PLATFORMS, resolveInitPlatform, resolveInitPrerender } from "./init-platform.js";
import { loadDotenv } from "./config/dotenv.js";
import {
createRscCompatibilityId,
Expand All @@ -53,6 +53,12 @@ import {
import { generateRouteTypes } from "./typegen.js";
import { normalizePathSeparators } from "./utils/path.js";
import { createDevServerConfigPlugin, normalizeDevServerHostname } from "./cli-dev-config.js";
import {
findVinextPrerenderConfigInPlugins,
formatVinextPrerenderLabel,
resolveVinextPrerenderDecision,
type ResolvedVinextPrerenderConfig,
} from "./config/prerender.js";

// ─── Resolve Vite from the project root ────────────────────────────────────────
//
Expand Down Expand Up @@ -220,8 +226,16 @@ function hasPagesDir(): boolean {
);
}

async function loadBuildEmptyOutDir(vite: ViteModule, root: string): Promise<boolean | undefined> {
if (!hasViteConfig(root)) return undefined;
type BuildViteConfigMetadata = {
emptyOutDir?: boolean;
prerenderConfig: ResolvedVinextPrerenderConfig | null;
};

async function loadBuildViteConfigMetadata(
vite: ViteModule,
root: string,
): Promise<BuildViteConfigMetadata> {
if (!hasViteConfig(root)) return { prerenderConfig: null };

// Read the raw user config before the multi-environment build so
// `build.emptyOutDir: false` remains an escape hatch for vinext's upfront clean.
Expand All @@ -231,7 +245,10 @@ async function loadBuildEmptyOutDir(vite: ViteModule, root: string): Promise<boo
root,
);
const emptyOutDir = loaded?.config.build?.emptyOutDir;
return typeof emptyOutDir === "boolean" ? emptyOutDir : undefined;
return {
emptyOutDir: typeof emptyOutDir === "boolean" ? emptyOutDir : undefined,
prerenderConfig: findVinextPrerenderConfigInPlugins(loaded?.config.plugins),
};
}

/**
Expand Down Expand Up @@ -544,10 +561,12 @@ async function buildApp() {
}
}

const buildConfigMetadata = await loadBuildViteConfigMetadata(vite, root);

cleanBuildOutput({
root,
outDir: distDir,
emptyOutDir: await loadBuildEmptyOutDir(vite, root),
emptyOutDir: buildConfigMetadata.emptyOutDir,
});

// All paths (App Router, Pages Router + Cloudflare, Pages Router plain Node)
Expand Down Expand Up @@ -655,21 +674,22 @@ async function buildApp() {
}

let prerenderResult;
const shouldPrerender = parsed.prerenderAll || resolvedNextConfig.output === "export";
const prerenderDecision = resolveVinextPrerenderDecision({
prerenderAllFlag: parsed.prerenderAll,
vinextPrerenderConfig: buildConfigMetadata.prerenderConfig,
nextOutput: resolvedNextConfig.output,
});

if (shouldPrerender) {
if (prerenderDecision) {
// Enable Node.js built-in sourcemap support so prerender error stack
// traces resolve through the server bundle's sourcemaps to show original
// source files. Matches Next.js's enablePrerenderSourceMaps default.
if (resolvedNextConfig.enablePrerenderSourceMaps) {
process.setSourceMapsEnabled(true);
Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 50);
}
const label = parsed.prerenderAll
? "Pre-rendering all routes..."
: "Pre-rendering all routes (output: 'export')...";
process.stdout.write("\x1b[0m");
console.log(` ${label}`);
console.log(` ${formatVinextPrerenderLabel(prerenderDecision)}`);
prerenderResult = await runPrerender({
root: normalizePathSeparators(process.cwd()),
concurrency: parsed.prerenderConcurrency,
Expand Down Expand Up @@ -854,13 +874,15 @@ async function initCommand() {
const force = rawArgs.includes("--force");
const platform = await resolveInitPlatform(rawArgs);
const platformOptions = await INIT_PLATFORMS[platform].options(rawArgs);
const prerender = await resolveInitPrerender(rawArgs);

await runInit({
root: process.cwd(),
port,
skipCheck,
force,
platform,
prerender,
cloudflare: platform === "cloudflare" ? platformOptions : undefined,
});
}
Expand Down Expand Up @@ -895,8 +917,8 @@ function printHelp(cmd?: string) {

Options:
--verbose Show full Vite/Rollup build output (suppressed by default)
--prerender-all Pre-render discovered routes after building (future releases
will serve these files in vinext start)
--prerender-all Pre-render discovered routes after building. Equivalent
to vinext({ prerender: true }) and takes priority.
--prerender-concurrency <count>
Maximum number of routes to pre-render in parallel
--precompress Precompress static assets at build time (.br, .gz, .zst)
Expand Down Expand Up @@ -960,6 +982,8 @@ function printHelp(cmd?: string) {
--skip-check Skip the compatibility check step
--force Overwrite existing vite.config.ts
--platform <target> Deployment target: cloudflare or node
--prerender Configure vinext build to pre-render all static routes
(default: prompt, with No selected by default)
--data-cache <type> Cloudflare data cache: kv or none (default: kv)
--image-optimization <type>
Cloudflare image optimization: cloudflare-images or none
Expand All @@ -972,6 +996,7 @@ function printHelp(cmd?: string) {
Configure the default Cloudflare cache handlers
vinext init --platform=cloudflare --image-optimization=none
Do not configure Cloudflare Images
vinext init --prerender Add prerender: { routes: "*" } to vite.config.ts
vinext init --platform=node Configure a Node deployment
vinext init -p 4000 Use port 4000 for dev:vinext
vinext init --force Overwrite existing vite.config.ts
Expand Down
Loading
Loading