From 2e9c5ca6a2728fabafbfd2e5e61c010fdae3be36 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Tue, 3 Mar 2026 16:14:30 +0800 Subject: [PATCH] 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 --- packages/void/src/prerender-paths.ts | 62 ++++++++++++++----- .../test/integration/static-pages.test.ts | 12 ++++ .../static-pages/pages/tags/[tag].server.ts | 16 +++++ playground/static-pages/pages/tags/[tag].vue | 9 +++ playground/static-pages/vite.config.ts | 20 +++++- 5 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 playground/static-pages/pages/tags/[tag].server.ts create mode 100644 playground/static-pages/pages/tags/[tag].vue diff --git a/packages/void/src/prerender-paths.ts b/packages/void/src/prerender-paths.ts index cca2aa516..12867203b 100644 --- a/packages/void/src/prerender-paths.ts +++ b/packages/void/src/prerender-paths.ts @@ -1,10 +1,50 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; -import { createServer } from "vite"; +import { createServer, loadConfigFromFile, type Plugin } from "vite"; import { scanPages } from "./pages/scan"; import { detectSsrPrerender, getSsrServerEntryPath } from "./ssr/entry"; import type { VoidConfig } from "./config"; +/** + * Create a temporary Vite dev server for ssrLoadModule(). + * + * Loads the user's full Vite config (plugins, resolve aliases, SSR options, + * etc.) but filters out @cloudflare/vite-plugin plugins which replace the + * SSR environment with a FetchableDevEnvironment that requires a running + * Miniflare backend, making ssrLoadModule() unusable. + */ +async function createSsrLoadServer(root: string) { + const loaded = await loadConfigFromFile( + { + command: "serve", + mode: process.env.NODE_ENV === "development" ? "development" : "production", + }, + undefined, + root, + ); + + if (!loaded) { + throw new Error("[void] Failed to load Vite config — expected a vite.config.{ts,js,mjs} file"); + } + + const { plugins: rawPlugins, ...restConfig } = loaded.config; + const plugins = ((rawPlugins ?? []) as unknown[]).flat(Infinity).filter((p): p is Plugin => { + if (!p || typeof p !== "object") return false; + const name = (p as Plugin).name; + if (!name) return true; + return !name.startsWith("vite-plugin-cloudflare"); + }); + + return createServer({ + ...restConfig, + root, + configFile: false, + server: { middlewareMode: true }, + logLevel: "silent", + plugins, + }); +} + /** * Collect all paths that should be prerendered. * @@ -39,12 +79,7 @@ export async function collectPrerenderPaths(options: { (p) => p.prerender && p.hasGetPrerenderPaths && (p.params.length > 0 || p.catchAll), ); if (dynamicPrerenderPages.length > 0) { - const viteServer = await createServer({ - root, - configFile: false, - server: { middlewareMode: true }, - logLevel: "silent", - }); + const viteServer = await createSsrLoadServer(root); try { for (const page of dynamicPrerenderPages) { try { @@ -65,8 +100,9 @@ export async function collectPrerenderPaths(options: { } } } catch (err) { - console.warn( + throw new Error( `[void] Failed to resolve prerender paths for ${page.pattern}: ${err instanceof Error ? err.message : String(err)}`, + { cause: err }, ); } } @@ -79,12 +115,7 @@ export async function collectPrerenderPaths(options: { // Custom SSR: getPrerenderPaths() in src/main.ssr.ts const ssrPrerender = detectSsrPrerender(root); if (ssrPrerender?.hasGetPrerenderPaths) { - const viteServer = await createServer({ - root, - configFile: false, - server: { middlewareMode: true }, - logLevel: "silent", - }); + const viteServer = await createSsrLoadServer(root); try { const entryPath = getSsrServerEntryPath(root)!; const mod = await viteServer.ssrLoadModule(entryPath); @@ -97,8 +128,9 @@ export async function collectPrerenderPaths(options: { } } } catch (err) { - console.warn( + throw new Error( `[void] Failed to resolve SSR prerender paths: ${err instanceof Error ? err.message : String(err)}`, + { cause: err }, ); } finally { await viteServer.close(); diff --git a/packages/void/test/integration/static-pages.test.ts b/packages/void/test/integration/static-pages.test.ts index c37e2e35d..b83ba5f59 100644 --- a/packages/void/test/integration/static-pages.test.ts +++ b/packages/void/test/integration/static-pages.test.ts @@ -73,6 +73,18 @@ describe("playground/static-pages build (output: static)", () => { expect(html).toContain("Post hello"); }); + it("prerenders dynamic pages with getPrerenderPaths using virtual modules", () => { + // tags/[tag] exports getPrerenderPaths that imports from virtual:tags plugin + const vuePath = join(DIST, "client/tags/vue.html"); + const vitePath = join(DIST, "client/tags/vite.html"); + expect(existsSync(vuePath)).toBe(true); + expect(existsSync(vitePath)).toBe(true); + + const html = readFileSync(vuePath, "utf-8"); + expect(html).toContain('data-testid="tag-title"'); + expect(html).toContain("Tag vue"); + }); + it("does not prerender dynamic pages without getPrerenderPaths", () => { // items/[id] has no getPrerenderPaths, so it should NOT be prerendered expect(existsSync(join(DIST, "client/items"))).toBe(false); diff --git a/playground/static-pages/pages/tags/[tag].server.ts b/playground/static-pages/pages/tags/[tag].server.ts new file mode 100644 index 000000000..dc61013be --- /dev/null +++ b/playground/static-pages/pages/tags/[tag].server.ts @@ -0,0 +1,16 @@ +import { defineHandler } from "void"; +import { tags } from "virtual:tags"; + +export interface Props { + tag: string; +} + +export const prerender = true; + +export function getPrerenderPaths() { + return tags.map((tag: string) => ({ tag })); +} + +export const loader = defineHandler((c) => { + return { tag: c.req.param("tag") }; +}); diff --git a/playground/static-pages/pages/tags/[tag].vue b/playground/static-pages/pages/tags/[tag].vue new file mode 100644 index 000000000..d96eee207 --- /dev/null +++ b/playground/static-pages/pages/tags/[tag].vue @@ -0,0 +1,9 @@ + + + diff --git a/playground/static-pages/vite.config.ts b/playground/static-pages/vite.config.ts index 44c74f311..02abc9b1c 100644 --- a/playground/static-pages/vite.config.ts +++ b/playground/static-pages/vite.config.ts @@ -1,7 +1,23 @@ -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; import { voidPlugin } from "void"; import { voidVue } from "@void-x/vue"; +function virtualTagsPlugin(): Plugin { + const VIRTUAL_ID = "virtual:tags"; + const RESOLVED_ID = "\0virtual:tags"; + return { + name: "virtual-tags", + resolveId(id) { + if (id === VIRTUAL_ID) return RESOLVED_ID; + }, + load(id) { + if (id === RESOLVED_ID) { + return `export const tags = ["vue", "vite"];`; + } + }, + }; +} + export default defineConfig({ - plugins: [voidPlugin(), voidVue()], + plugins: [voidPlugin(), voidVue(), virtualTagsPlugin()], });