Skip to content

Commit 91e63ff

Browse files
committed
fix(build): honor configured prerender output roots
1 parent e754ca0 commit 91e63ff

7 files changed

Lines changed: 89 additions & 13 deletions

File tree

packages/cloudflare/src/deploy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1992,6 +1992,7 @@ export async function deploy(options: DeployOptions): Promise<void> {
19921992
root: info.root,
19931993
concurrency: options.prerenderConcurrency,
19941994
nextConfig,
1995+
routeRootConfig: viteConfigMetadata.routeRootConfig,
19951996
});
19961997
ranPrerender = true;
19971998
}

packages/vinext/src/build/inject-pregenerated-paths.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ const VINEXT_PREGEN_RE = new RegExp(
1818
export function injectPregeneratedConcretePaths(
1919
root: string,
2020
workerEntry = path.resolve(root, "dist", "server", "index.js"),
21+
serverOutputDir = path.resolve(root, "dist", "server"),
2122
): void {
22-
const manifestDir = path.resolve(root, "dist", "server");
2323
const runtimeDir = path.dirname(workerEntry);
24-
const manifest = readPrerenderManifest(path.join(manifestDir, "vinext-prerender.json"));
24+
const manifest = readPrerenderManifest(path.join(serverOutputDir, "vinext-prerender.json"));
2525
const table = manifest?.pregeneratedConcretePaths ?? [];
2626

2727
// Response-stage entries can be deployed independently of index.js. Keep the
@@ -30,14 +30,14 @@ export function injectPregeneratedConcretePaths(
3030
// The file is emitted during the server build; writing it after prerendering
3131
// updates the artifact without rebuilding or coupling core to a host
3232
// transport.
33-
const runtimeModule = path.join(runtimeDir, PREGENERATED_CONCRETE_PATHS_MODULE);
34-
fs.mkdirSync(runtimeDir, { recursive: true });
35-
fs.writeFileSync(
36-
runtimeModule,
33+
const runtimeModuleCode =
3734
table.length > 0
3835
? `globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = ${JSON.stringify(table)};\n`
39-
: "delete globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS;\n",
40-
);
36+
: "delete globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS;\n";
37+
for (const outputDir of new Set([serverOutputDir, runtimeDir])) {
38+
fs.mkdirSync(outputDir, { recursive: true });
39+
fs.writeFileSync(path.join(outputDir, PREGENERATED_CONCRETE_PATHS_MODULE), runtimeModuleCode);
40+
}
4141

4242
if (!fs.existsSync(workerEntry)) {
4343
if (table.length > 0) globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = table;

packages/vinext/src/build/run-prerender.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { rememberCurrentServerEntryImportMtime, startProdServer } from "../serve
3838
import { enterPrerenderPhase } from "./prerender-phase.js";
3939
import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
4040
import { resolveBuiltRscEntryPath } from "./server-entry.js";
41+
import type { VinextRouteRootConfig } from "../config/prerender.js";
4142

4243
// ─── Progress UI ──────────────────────────────────────────────────────────────
4344

@@ -107,6 +108,8 @@ type RunPrerenderOptions = {
107108
* Intended for tests that build to a custom outDir.
108109
*/
109110
rscBundlePath?: string;
111+
/** Build output roots captured from the vinext Vite plugin. */
112+
routeRootConfig?: VinextRouteRootConfig | null;
110113
/**
111114
* Maximum number of routes rendered in parallel.
112115
* Defaults to prerenderApp/prerenderPages internal defaults when omitted.
@@ -167,7 +170,10 @@ export async function runPrerender(options: RunPrerenderOptions): Promise<Preren
167170
// The manifest lands in dist/server/ alongside the server bundle so it's
168171
// cleaned with the rest of vinext's build output on rebuild and co-located
169172
// with server artifacts.
170-
const manifestDir = path.join(root, "dist", "server");
173+
const manifestDir = path.resolve(
174+
root,
175+
options.routeRootConfig?.rscOutDir ?? path.join("dist", "server"),
176+
);
171177
const rscBundlePath = options.rscBundlePath ?? resolveBuiltRscEntryPath(manifestDir);
172178
// The emitted entry may live below dist/server (for example entries/app.js).
173179
// Build metadata and prerender artifacts remain rooted at dist/server.
@@ -221,7 +227,7 @@ export async function runPrerender(options: RunPrerenderOptions): Promise<Preren
221227
const outDir =
222228
mode === "export"
223229
? path.join(root, "dist", "client")
224-
: path.join(root, "dist", "server", "prerendered-routes");
230+
: path.join(manifestDir, "prerendered-routes");
225231

226232
// For hybrid builds (both app/ and pages/ present), start a single shared
227233
// prod server and pass it to both phases. This avoids spinning up two servers
@@ -311,8 +317,7 @@ export async function runPrerender(options: RunPrerenderOptions): Promise<Preren
311317
...(sharedProdServer
312318
? { _prodServer: sharedProdServer, _prerenderSecret: sharedPrerenderSecret }
313319
: {
314-
pagesBundlePath:
315-
options.pagesBundlePath ?? path.join(root, "dist", "server", "entry.js"),
320+
pagesBundlePath: options.pagesBundlePath ?? path.join(manifestDir, "entry.js"),
316321
}),
317322
onProgress: ({ total, route }) => {
318323
if (pagesTotal === 0) {
@@ -383,7 +388,7 @@ export async function runPrerender(options: RunPrerenderOptions): Promise<Preren
383388
);
384389
}
385390

386-
injectPregeneratedConcretePaths(root, rscBundlePath);
391+
injectPregeneratedConcretePaths(root, rscBundlePath, manifestDir);
387392
if (fs.existsSync(rscBundlePath)) {
388393
rememberCurrentServerEntryImportMtime(rscBundlePath);
389394
}

packages/vinext/src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ async function buildApp() {
736736
root,
737737
concurrency: parsed.prerenderConcurrency,
738738
nextConfig: resolvedNextConfig,
739+
routeRootConfig: buildConfigMetadata.routeRootConfig,
739740
});
740741
await emitPrerenderPathManifest({
741742
root,

tests/deploy-prerender-config.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,29 @@ describe("deploy prerender config wiring", () => {
264264
);
265265
});
266266

267+
it("passes configured build output roots to deploy prerendering", async () => {
268+
writeProject("true");
269+
const viteConfigPath = path.join(tmpDir, "vite.config.ts");
270+
fs.writeFileSync(
271+
viteConfigPath,
272+
fs
273+
.readFileSync(viteConfigPath, "utf-8")
274+
.replace(
275+
"vinext({ prerender: true",
276+
'vinext({ rscOutDir: "build/application", prerender: true',
277+
),
278+
);
279+
const { deploy } = await import("../packages/cloudflare/src/deploy.js");
280+
281+
await deploy({ root: tmpDir, skipBuild: true });
282+
283+
expect(runPrerenderMock).toHaveBeenCalledWith(
284+
expect.objectContaining({
285+
routeRootConfig: expect.objectContaining({ rscOutDir: "build/application" }),
286+
}),
287+
);
288+
});
289+
267290
it("loads Vite config even when the prerender-all flag already decides prerendering", async () => {
268291
writeProject("true");
269292
fs.appendFileSync(

tests/inject-pregenerated-paths.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ describe("injectPregeneratedConcretePaths", () => {
204204
expect(
205205
fs.existsSync(path.join(path.dirname(entryPath), PREGENERATED_CONCRETE_PATHS_MODULE)),
206206
).toBe(true);
207+
expect(
208+
fs.readFileSync(
209+
path.join(tmpDir, "dist/server", PREGENERATED_CONCRETE_PATHS_MODULE),
210+
"utf-8",
211+
),
212+
).toContain("/blog/post-a");
207213
const applicationEntry: unknown = await import(
208214
`${pathToFileURL(entryPath).href}?t=${Date.now()}`
209215
);

tests/run-prerender-concurrency.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,44 @@ describe("runPrerender concurrency", () => {
141141
fs.rmSync(root, { recursive: true, force: true });
142142
}
143143
});
144+
145+
it("resolves App artifacts from the configured RSC output root", async () => {
146+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-run-prerender-custom-rsc-"));
147+
const serverDir = path.join(root, "build", "application");
148+
const applicationEntry = path.join(serverDir, "entries", "application-entry.js");
149+
fs.mkdirSync(path.join(root, "app"));
150+
fs.mkdirSync(path.join(serverDir, ".vite"), { recursive: true });
151+
fs.mkdirSync(path.dirname(applicationEntry), { recursive: true });
152+
fs.writeFileSync(path.join(serverDir, "BUILD_ID"), "custom-build\n");
153+
fs.writeFileSync(applicationEntry, "export {};\n");
154+
fs.writeFileSync(
155+
path.join(serverDir, ".vite", "manifest.json"),
156+
JSON.stringify({
157+
"virtual:vinext-rsc-entry": {
158+
file: "entries/application-entry.js",
159+
isDynamicEntry: true,
160+
},
161+
}),
162+
);
163+
164+
try {
165+
const { runPrerender } = await import("../packages/vinext/src/build/run-prerender.js");
166+
167+
await runPrerender({
168+
root,
169+
routeRootConfig: { rscOutDir: "build/application" },
170+
});
171+
172+
expect(prerenderAppMock).toHaveBeenCalledWith(
173+
expect.objectContaining({
174+
rscBundlePath: applicationEntry,
175+
serverDir,
176+
outDir: path.join(serverDir, "prerendered-routes"),
177+
config: expect.objectContaining({ buildId: "custom-build" }),
178+
}),
179+
);
180+
} finally {
181+
fs.rmSync(root, { recursive: true, force: true });
182+
}
183+
});
144184
});

0 commit comments

Comments
 (0)