Skip to content

Commit 2a79e56

Browse files
committed
fix: load user config in collectPrerenderPaths for virtual module support
collectPrerenderPaths() now loads the user's full vite.config via loadConfigFromFile() so that user-defined plugins (virtual modules, resolve aliases, etc.) are available when ssrLoadModule() runs getPrerenderPaths(). - Use process.env.NODE_ENV for mode detection - Throw on failed config load instead of silent fallback - Throw on prerender path resolution errors instead of console.warn - Replace categories test fixture with tags/virtual:tags fixture
1 parent c242c6f commit 2a79e56

5 files changed

Lines changed: 102 additions & 17 deletions

File tree

‎packages/void/src/prerender-paths.ts‎

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
11
import { existsSync } from "node:fs";
22
import { join } from "node:path";
3-
import { createServer } from "vite";
3+
import { createServer, loadConfigFromFile, type Plugin } from "vite";
44
import { scanPages } from "./pages/scan";
55
import { detectSsrPrerender, getSsrServerEntryPath } from "./ssr/entry";
66
import type { VoidConfig } from "./config";
77

8+
/**
9+
* Create a temporary Vite dev server for ssrLoadModule().
10+
*
11+
* Loads the user's full Vite config (plugins, resolve aliases, SSR options,
12+
* etc.) but filters out @cloudflare/vite-plugin plugins which replace the
13+
* SSR environment with a FetchableDevEnvironment that requires a running
14+
* Miniflare backend, making ssrLoadModule() unusable.
15+
*/
16+
async function createSsrLoadServer(root: string) {
17+
const loaded = await loadConfigFromFile(
18+
{
19+
command: "serve",
20+
mode: process.env.NODE_ENV === "development" ? "development" : "production",
21+
},
22+
undefined,
23+
root,
24+
);
25+
26+
if (!loaded) {
27+
throw new Error("[void] Failed to load Vite config — expected a vite.config.{ts,js,mjs} file");
28+
}
29+
30+
const { plugins: rawPlugins, ...restConfig } = loaded.config;
31+
const plugins = ((rawPlugins ?? []) as unknown[]).flat(Infinity).filter((p): p is Plugin => {
32+
if (!p || typeof p !== "object") return false;
33+
const name = (p as Plugin).name;
34+
if (!name) return true;
35+
return !name.startsWith("vite-plugin-cloudflare");
36+
});
37+
38+
return createServer({
39+
...restConfig,
40+
root,
41+
configFile: false,
42+
server: { middlewareMode: true },
43+
logLevel: "silent",
44+
plugins,
45+
});
46+
}
47+
848
/**
949
* Collect all paths that should be prerendered.
1050
*
@@ -39,12 +79,7 @@ export async function collectPrerenderPaths(options: {
3979
(p) => p.prerender && p.hasGetPrerenderPaths && (p.params.length > 0 || p.catchAll),
4080
);
4181
if (dynamicPrerenderPages.length > 0) {
42-
const viteServer = await createServer({
43-
root,
44-
configFile: false,
45-
server: { middlewareMode: true },
46-
logLevel: "silent",
47-
});
82+
const viteServer = await createSsrLoadServer(root);
4883
try {
4984
for (const page of dynamicPrerenderPages) {
5085
try {
@@ -65,8 +100,9 @@ export async function collectPrerenderPaths(options: {
65100
}
66101
}
67102
} catch (err) {
68-
console.warn(
103+
throw new Error(
69104
`[void] Failed to resolve prerender paths for ${page.pattern}: ${err instanceof Error ? err.message : String(err)}`,
105+
{ cause: err },
70106
);
71107
}
72108
}
@@ -79,12 +115,7 @@ export async function collectPrerenderPaths(options: {
79115
// Custom SSR: getPrerenderPaths() in src/main.ssr.ts
80116
const ssrPrerender = detectSsrPrerender(root);
81117
if (ssrPrerender?.hasGetPrerenderPaths) {
82-
const viteServer = await createServer({
83-
root,
84-
configFile: false,
85-
server: { middlewareMode: true },
86-
logLevel: "silent",
87-
});
118+
const viteServer = await createSsrLoadServer(root);
88119
try {
89120
const entryPath = getSsrServerEntryPath(root)!;
90121
const mod = await viteServer.ssrLoadModule(entryPath);
@@ -97,8 +128,9 @@ export async function collectPrerenderPaths(options: {
97128
}
98129
}
99130
} catch (err) {
100-
console.warn(
131+
throw new Error(
101132
`[void] Failed to resolve SSR prerender paths: ${err instanceof Error ? err.message : String(err)}`,
133+
{ cause: err },
102134
);
103135
} finally {
104136
await viteServer.close();

‎packages/void/test/integration/static-pages.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ describe("playground/static-pages build (output: static)", () => {
7373
expect(html).toContain("Post hello");
7474
});
7575

76+
it("prerenders dynamic pages with getPrerenderPaths using virtual modules", () => {
77+
// tags/[tag] exports getPrerenderPaths that imports from virtual:tags plugin
78+
const vuePath = join(DIST, "client/tags/vue.html");
79+
const vitePath = join(DIST, "client/tags/vite.html");
80+
expect(existsSync(vuePath)).toBe(true);
81+
expect(existsSync(vitePath)).toBe(true);
82+
83+
const html = readFileSync(vuePath, "utf-8");
84+
expect(html).toContain('data-testid="tag-title"');
85+
expect(html).toContain("Tag vue");
86+
});
87+
7688
it("does not prerender dynamic pages without getPrerenderPaths", () => {
7789
// items/[id] has no getPrerenderPaths, so it should NOT be prerendered
7890
expect(existsSync(join(DIST, "client/items"))).toBe(false);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineHandler } from "void";
2+
import { tags } from "virtual:tags";
3+
4+
export interface Props {
5+
tag: string;
6+
}
7+
8+
export const prerender = true;
9+
10+
export function getPrerenderPaths() {
11+
return tags.map((tag: string) => ({ tag }));
12+
}
13+
14+
export const loader = defineHandler<Props>((c) => {
15+
return { tag: c.req.param("tag") };
16+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script setup lang="ts">
2+
defineProps<{ tag: string }>();
3+
</script>
4+
5+
<template>
6+
<div>
7+
<h1 data-testid="tag-title">Tag {{ tag }}</h1>
8+
</div>
9+
</template>
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
import { defineConfig } from "vite";
1+
import { defineConfig, type Plugin } from "vite";
22
import { voidPlugin } from "void";
33
import { voidVue } from "@void-x/vue";
44

5+
function virtualTagsPlugin(): Plugin {
6+
const VIRTUAL_ID = "virtual:tags";
7+
const RESOLVED_ID = "\0virtual:tags";
8+
return {
9+
name: "virtual-tags",
10+
resolveId(id) {
11+
if (id === VIRTUAL_ID) return RESOLVED_ID;
12+
},
13+
load(id) {
14+
if (id === RESOLVED_ID) {
15+
return `export const tags = ["vue", "vite"];`;
16+
}
17+
},
18+
};
19+
}
20+
521
export default defineConfig({
6-
plugins: [voidPlugin(), voidVue()],
22+
plugins: [voidPlugin(), voidVue(), virtualTagsPlugin()],
723
});

0 commit comments

Comments
 (0)