diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 8d04232580..0dcc18eb04 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -36,7 +36,7 @@ import type { Cache } from "./cache"; import { generate, parse } from "./babel"; import type { NodeRequestHandler } from "./node-adapter"; import { fromNodeRequest, toNodeRequest } from "./node-adapter"; -import { getStylesForUrl, isCssModulesFile } from "./styles"; +import { getStylesForPathname, isCssModulesFile } from "./styles"; import * as VirtualModule from "./virtual-module"; import { resolveFileUrl } from "./resolve-file-url"; import { combineURLs } from "./combine-urls"; @@ -678,7 +678,22 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { }`; }) .join(",\n ")} - };`; + }; + ${ + ctx.reactRouterConfig.future.unstable_viteEnvironmentApi && + viteCommand === "serve" + ? ` + export const getCriticalCss = ({ pathname }) => { + return { + rel: "stylesheet", + href: "${ + viteUserConfig.base ?? "/" + }@react-router/critical.css?pathname=" + pathname, + }; + } + ` + : "" + }`; }; let loadViteManifest = async (directory: string) => { @@ -1341,15 +1356,14 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { setDevServerHooks({ // Give the request handler access to the critical CSS in dev to avoid a // flash of unstyled content since Vite injects CSS file contents via JS - getCriticalCss: async (build, url) => { - return getStylesForUrl({ + getCriticalCss: async (pathname) => { + return getStylesForPathname({ rootDirectory: ctx.rootDirectory, entryClientFilePath: ctx.entryClientFilePath, reactRouterConfig: ctx.reactRouterConfig, viteDevServer, loadCssContents, - build, - url, + pathname, }); }, // If an error is caught within the request handler, let Vite fix the @@ -1397,6 +1411,30 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { } ); + if (ctx.reactRouterConfig.future.unstable_viteEnvironmentApi) { + viteDevServer.middlewares.use(async (req, res, next) => { + let [reqPathname, reqSearch] = (req.url ?? "").split("?"); + if (reqPathname === "/@react-router/critical.css") { + let pathname = new URLSearchParams(reqSearch).get("pathname"); + if (!pathname) { + return next("No pathname provided"); + } + let css = await getStylesForPathname({ + rootDirectory: ctx.rootDirectory, + entryClientFilePath: ctx.entryClientFilePath, + reactRouterConfig: ctx.reactRouterConfig, + viteDevServer, + loadCssContents, + pathname, + }); + res.setHeader("Content-Type", "text/css"); + res.end(css); + } else { + next(); + } + }); + } + return () => { // Let user servers handle SSR requests in middleware mode, // otherwise the Vite plugin will handle the request diff --git a/packages/react-router-dev/vite/styles.ts b/packages/react-router-dev/vite/styles.ts index feea06b5a4..6ca3fcb415 100644 --- a/packages/react-router-dev/vite/styles.ts +++ b/packages/react-router-dev/vite/styles.ts @@ -1,15 +1,12 @@ import * as path from "node:path"; -import type { ServerBuild } from "react-router"; import { matchRoutes } from "react-router"; import type { ModuleNode, ViteDevServer } from "vite"; import type { ResolvedReactRouterConfig } from "../config/config"; +import type { RouteManifest, RouteManifestEntry } from "../config/routes"; import type { LoadCssContents } from "./plugin"; import { resolveFileUrl } from "./resolve-file-url"; -type ServerRouteManifest = ServerBuild["routes"]; -type ServerRoute = ServerRouteManifest[string]; - // Style collection logic adapted from solid-start: https://github.com/solidjs/solid-start // Vite doesn't expose these so we just copy the list for now @@ -163,8 +160,8 @@ const findDeps = async ( await Promise.all(branches); }; -const groupRoutesByParentId = (manifest: ServerRouteManifest) => { - let routes: Record[]> = {}; +const groupRoutesByParentId = (manifest: RouteManifest) => { + let routes: Record> = {}; Object.values(manifest).forEach((route) => { if (route) { @@ -179,45 +176,62 @@ const groupRoutesByParentId = (manifest: ServerRouteManifest) => { return routes; }; -// Create a map of routes by parentId to use recursively instead of -// repeatedly filtering the manifest. -const createRoutes = ( - manifest: ServerRouteManifest, +type RouteManifestEntryWithChildren = Omit & + ( + | { index?: false | undefined; children: RouteManifestEntryWithChildren[] } + | { index: true; children?: never } + ); + +const createRoutesWithChildren = ( + manifest: RouteManifest, parentId: string = "", routesByParentId = groupRoutesByParentId(manifest) -): NonNullable[] => { +): RouteManifestEntryWithChildren[] => { return (routesByParentId[parentId] || []).map((route) => ({ ...route, - children: createRoutes(manifest, route.id, routesByParentId), + ...(route.index + ? { + index: true, + } + : { + index: false, + children: createRoutesWithChildren( + manifest, + route.id, + routesByParentId + ), + }), })); }; -export const getStylesForUrl = async ({ +export const getStylesForPathname = async ({ viteDevServer, rootDirectory, reactRouterConfig, entryClientFilePath, loadCssContents, - build, - url, + pathname, }: { viteDevServer: ViteDevServer; rootDirectory: string; - reactRouterConfig: Pick; + reactRouterConfig: Pick< + ResolvedReactRouterConfig, + "appDirectory" | "routes" | "basename" + >; entryClientFilePath: string; loadCssContents: LoadCssContents; - build: ServerBuild; - url: string | undefined; + pathname: string | undefined; }): Promise => { - if (url === undefined || url.includes("?_data=")) { + if (pathname === undefined || pathname.includes("?_data=")) { return undefined; } - let routes = createRoutes(build.routes); + let routesWithChildren = createRoutesWithChildren(reactRouterConfig.routes); let appPath = path.relative(process.cwd(), reactRouterConfig.appDirectory); let documentRouteFiles = - matchRoutes(routes, url, build.basename)?.map((match) => - path.resolve(appPath, reactRouterConfig.routes[match.route.id].file) + matchRoutes(routesWithChildren, pathname, reactRouterConfig.basename)?.map( + (match) => + path.resolve(appPath, reactRouterConfig.routes[match.route.id].file) ) ?? []; let styles = await getStylesForFiles({ diff --git a/packages/react-router/lib/dom/global.ts b/packages/react-router/lib/dom/global.ts index 58ec4ef4cf..c6e3f5b58b 100644 --- a/packages/react-router/lib/dom/global.ts +++ b/packages/react-router/lib/dom/global.ts @@ -1,11 +1,11 @@ import type { HydrationState, Router as DataRouter } from "../router/router"; -import type { AssetsManifest, FutureConfig } from "./ssr/entry"; +import type { AssetsManifest, CriticalCss, FutureConfig } from "./ssr/entry"; import type { RouteModules } from "./ssr/routeModules"; export type WindowReactRouterContext = { basename?: string; state: HydrationState; - criticalCss?: string; + criticalCss?: CriticalCss; future: FutureConfig; ssr: boolean; isSpaMode: boolean; diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index adbdd112a2..bb2cad8aef 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -241,9 +241,12 @@ export function Links() { return ( <> - {criticalCss ? ( + {typeof criticalCss === "string" ? (