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
14 changes: 8 additions & 6 deletions packages/void/src/pages/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,20 @@ export async function handlePageGet(
getAssetTags: (
req: Request,
env: any,
componentId: string,
) => Promise<{ css: string; preloads: string; body: string }>,
configHead?: HeadConfig,
island?: boolean,
): Promise<Response> {
const shared = c.get("shared") || {};

// Run loader and check for redirect/non-data responses before extracting props
// Run loader and check for redirect/error responses before extracting props
let props: Record<string, unknown> = {};
if (handler) {
const result = await handler(c);
if (result instanceof Response) {
const status = result.status;
if (status >= 300 && status < 400) return result;
if (status >= 300) return result;
}
props = await extractProps(result);
}
Expand Down Expand Up @@ -211,7 +212,7 @@ export async function handlePageGet(
);
}

const assetTags = await getAssetTags(c.req.raw, c.env);
const assetTags = await getAssetTags(c.req.raw, c.env, componentId);
const html = await renderFn(pageObj, assetTags);

if (!hasDeferred) {
Expand Down Expand Up @@ -250,6 +251,7 @@ export async function handlePageMutation(
getAssetTags: (
req: Request,
env: any,
componentId: string,
) => Promise<{ css: string; preloads: string; body: string }>,
configHead?: HeadConfig,
island?: boolean,
Expand Down Expand Up @@ -285,7 +287,7 @@ export async function handlePageMutation(
const props = stripDeferred(rawProps);
const pageObj = buildMutationPageObj(props, e.errors);
if (isPagesRequest(c)) return pageJsonResponse(pageObj);
const assetTags = await getAssetTags(c.req.raw, c.env);
const assetTags = await getAssetTags(c.req.raw, c.env, componentId);
return new Response(await renderFn(pageObj, assetTags), {
headers: { "content-type": "text/html; charset=utf-8", ...POWERED_BY },
});
Expand All @@ -309,7 +311,7 @@ export async function handlePageMutation(
const props = stripDeferred(rawProps);
const pageObj = buildMutationPageObj(props, errors);
if (isPagesRequest(c)) return pageJsonResponse(pageObj);
const assetTags = await getAssetTags(c.req.raw, c.env);
const assetTags = await getAssetTags(c.req.raw, c.env, componentId);
return new Response(await renderFn(pageObj, assetTags), {
headers: { "content-type": "text/html; charset=utf-8", ...POWERED_BY },
});
Expand All @@ -326,7 +328,7 @@ export async function handlePageMutation(
const props = stripDeferred(rawProps);
const pageObj = buildMutationPageObj(props, null);
if (isPagesRequest(c)) return pageJsonResponse(pageObj);
const assetTags = await getAssetTags(c.req.raw, c.env);
const assetTags = await getAssetTags(c.req.raw, c.env, componentId);
return new Response(await renderFn(pageObj, assetTags), {
headers: { "content-type": "text/html; charset=utf-8", ...POWERED_BY },
});
Expand Down
3 changes: 2 additions & 1 deletion packages/void/src/plugin-routing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { readConfig, getDatabaseDialect } from "./config";
import { dirname, join } from "node:path";
import { dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { loadEnv, type Plugin } from "vite";
import { scanRoutes, type ScanResult } from "./router/scan";
Expand Down Expand Up @@ -205,6 +205,7 @@ export function routingPlugin(root: string, options: RoutingPluginOptions): Plug
: undefined;
return {
pagesDir,
pagesDirRelative: relative(root, pagesDir).replaceAll("\\", "/"),
protocolPath: resolveFile("void/pages-protocol"),
headPath: resolveFile("void/pages-head"),
clientEntryId: "virtual:void-pages-client",
Expand Down
93 changes: 83 additions & 10 deletions packages/void/src/router/compile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { HeaderRule, RedirectRule } from "../cli/types";
import type { VoidAuthProvider } from "../auth/providers";
import type { ScanResult } from "./scan";
import { resolveLayoutIds } from "../pages/layout-resolution";

/**
* Script tag injected into dev HTML to load the binding-hint virtual module.
Expand Down Expand Up @@ -59,6 +60,8 @@ export interface SsrCompileOptions {
export interface PagesCompileOptions {
/** Absolute path to pages/ directory */
pagesDir: string;
/** Pages directory relative to Vite root, e.g. "pages" — used for manifest key lookup */
pagesDirRelative: string;
/** Absolute path to void/pages-protocol module */
protocolPath: string;
/** Absolute path to void/pages-head module */
Expand Down Expand Up @@ -715,6 +718,71 @@ function compilePageRoutes(lines: string[], scan: ScanResult, pages: PagesCompil
lines.push(` return Array.from(seen);`);
lines.push(`}`);
lines.push("");
// Build manifest-key mapping for each page and its layout chain
// so CSS collection can be scoped to the active route only
const relPagesDir = pages.pagesDirRelative;
const pageCssKeys: Record<string, string[]> = {};
for (const page of scan.pages.pages) {
const keys = [`${relPagesDir}/${page.componentPath}`];
Comment thread
Brooooooklyn marked this conversation as resolved.
const layoutIds = resolveLayoutIds(page, scan.pages.layouts, scan.pages.namedLayouts);
for (const layoutId of layoutIds) {
// Map layout IDs back to file paths
const layout = scan.pages.layouts.find(
(l) => (l.directory ? `${l.directory}/layout` : "layout") === layoutId,
);
if (layout) {
keys.push(`${relPagesDir}/${layout.filePath}`);
continue;
}
const named = scan.pages.namedLayouts.find((nl) => {
const id = nl.directory ? `${nl.directory}/_layouts/${nl.name}` : `_layouts/${nl.name}`;
return id === layoutId;
});
if (named) {
keys.push(`${relPagesDir}/${named.filePath}`);
}
}
pageCssKeys[page.componentId] = keys;
}
lines.push(`const PAGE_CSS_KEYS = ${JSON.stringify(pageCssKeys)};`);
lines.push("");

lines.push(`function collectRouteCss(manifest, entryId, pageKeys) {`);
lines.push(` const cssFiles = new Set();`);
lines.push(` const visited = new Set();`);
lines.push(` const toVisit = [entryId];`);
lines.push(` while (toVisit.length > 0) {`);
lines.push(` const id = toVisit.pop();`);
lines.push(` if (visited.has(id)) continue;`);
lines.push(` visited.add(id);`);
lines.push(` const chunk = manifest[id];`);
lines.push(` if (!chunk) continue;`);
lines.push(` if (Array.isArray(chunk.css)) {`);
lines.push(` for (const f of chunk.css) cssFiles.add(f);`);
lines.push(` }`);
lines.push(` if (Array.isArray(chunk.imports)) {`);
lines.push(` for (const i of chunk.imports) toVisit.push(i);`);
lines.push(` }`);
lines.push(` }`);
lines.push(` for (const key of pageKeys) {`);
lines.push(` const stack = [key];`);
lines.push(` while (stack.length > 0) {`);
lines.push(` const id = stack.pop();`);
lines.push(` if (visited.has(id)) continue;`);
lines.push(` visited.add(id);`);
lines.push(` const chunk = manifest[id];`);
lines.push(` if (!chunk) continue;`);
lines.push(` if (Array.isArray(chunk.css)) {`);
lines.push(` for (const f of chunk.css) cssFiles.add(f);`);
lines.push(` }`);
lines.push(` if (Array.isArray(chunk.imports)) {`);
lines.push(` for (const i of chunk.imports) stack.push(i);`);
lines.push(` }`);
lines.push(` }`);
Comment thread
cursor[bot] marked this conversation as resolved.
lines.push(` }`);
lines.push(` return cssFiles;`);
lines.push(`}`);
lines.push("");
lines.push(`async function loadPagesManifest(request, env) {`);
lines.push(` if (cachedPagesManifest) return cachedPagesManifest;`);
lines.push(
Expand All @@ -732,7 +800,7 @@ function compilePageRoutes(lines: string[], scan: ScanResult, pages: PagesCompil
lines.push(` return cachedPagesManifest;`);
lines.push(`}`);
lines.push("");
lines.push(`async function getPagesAssetTags(request, env) {`);
lines.push(`async function getPagesAssetTags(request, env, componentId) {`);
lines.push(` const manifest = await loadPagesManifest(request, env);`);
lines.push(` if (!manifest || typeof manifest !== "object") {`);
lines.push(` if (!import.meta.env.DEV) return { css: "", preloads: "", body: "" };`);
Expand All @@ -743,14 +811,12 @@ function compilePageRoutes(lines: string[], scan: ScanResult, pages: PagesCompil
lines.push(` return { css: "", preloads: "", body: "" };`);
lines.push(` }`);
lines.push(` const ids = collectPagesManifestFiles(manifest, PAGES_CLIENT_ENTRY_ID);`);
lines.push(` const cssFiles = new Set();`);
lines.push(` const pageKeys = componentId ? (PAGE_CSS_KEYS[componentId] || []) : [];`);
lines.push(` const cssFiles = collectRouteCss(manifest, PAGES_CLIENT_ENTRY_ID, pageKeys);`);
Comment thread
cursor[bot] marked this conversation as resolved.
lines.push(` const preloadFiles = new Set();`);
lines.push(` for (const id of ids) {`);
lines.push(` const chunk = manifest[id];`);
lines.push(` if (!chunk || typeof chunk !== "object") continue;`);
lines.push(` if (Array.isArray(chunk.css)) {`);
lines.push(` for (const cssFile of chunk.css) cssFiles.add(cssFile);`);
lines.push(` }`);
lines.push(` if (id !== PAGES_CLIENT_ENTRY_ID && typeof chunk.file === "string") {`);
lines.push(` preloadFiles.add(chunk.file);`);
lines.push(` }`);
Expand Down Expand Up @@ -803,7 +869,7 @@ function compilePageRoutes(lines: string[], scan: ScanResult, pages: PagesCompil
lines.push(` return { css: "", preloads, body: bodyTag };`);
lines.push(`}`);
lines.push("");
lines.push(`async function getIslandsAssetTags(request, env) {`);
lines.push(`async function getIslandsAssetTags(request, env, componentId) {`);
lines.push(` const manifest = await loadPagesManifest(request, env);`);
lines.push(` if (!manifest || typeof manifest !== "object") {`);
lines.push(` if (!import.meta.env.DEV) return { css: "", preloads: "", body: "" };`);
Expand All @@ -822,14 +888,21 @@ function compilePageRoutes(lines: string[], scan: ScanResult, pages: PagesCompil
lines.push(` for (const mdId of mdIds) ids.push(mdId);`);
lines.push(` }`);
}
lines.push(` const cssFiles = new Set();`);
// Collect route-scoped CSS (page + layout chunks) plus static-import CSS
lines.push(` const pageKeys = componentId ? (PAGE_CSS_KEYS[componentId] || []) : [];`);
lines.push(` const cssFiles = collectRouteCss(manifest, ISLANDS_CLIENT_ENTRY_ID, pageKeys);`);
if (pages.mdClientEntryId) {
// Also collect CSS from the md client entry's static imports
lines.push(` if (mdEntry && typeof mdEntry.file === "string") {`);
lines.push(` const mdKeys = [];`);
lines.push(` const mdCss = collectRouteCss(manifest, MD_CLIENT_ENTRY_ID, mdKeys);`);
lines.push(` for (const f of mdCss) cssFiles.add(f);`);
lines.push(` }`);
}
lines.push(` const preloadFiles = new Set();`);
lines.push(` for (const id of ids) {`);
lines.push(` const chunk = manifest[id];`);
lines.push(` if (!chunk || typeof chunk !== "object") continue;`);
lines.push(` if (Array.isArray(chunk.css)) {`);
lines.push(` for (const cssFile of chunk.css) cssFiles.add(cssFile);`);
lines.push(` }`);
// Exclude entry points from preloads (they get their own <script> tags)
const excludeFromPreload = pages.mdClientEntryId
? `id !== ISLANDS_CLIENT_ENTRY_ID && id !== MD_CLIENT_ENTRY_ID`
Expand Down
3 changes: 3 additions & 0 deletions packages/void/test/unit/pages-compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const testOptions = {

const pagesOptions: PagesCompileOptions = {
pagesDir: "/project/pages",
pagesDirRelative: "pages",
protocolPath: "/resolved/void/pages/protocol.mjs",
headPath: "/resolved/void/pages/head.mjs",
clientEntryId: "virtual:void-pages-client",
Expand Down Expand Up @@ -389,5 +390,7 @@ describe("compileRouteTable with pages", () => {
expect(code).toContain("getPagesAssetTags");
expect(code).toContain("loadPagesManifest");
expect(code).toContain("getPagesDevClientTags");
expect(code).toContain("collectRouteCss");
expect(code).toContain("PAGE_CSS_KEYS");
});
});
Loading