diff --git a/integration/helpers/rsc-vite-framework/package.json b/integration/helpers/rsc-vite-framework/package.json index 52d123e207..03488c7e3b 100644 --- a/integration/helpers/rsc-vite-framework/package.json +++ b/integration/helpers/rsc-vite-framework/package.json @@ -2,6 +2,7 @@ "name": "integration-rsc-vite-framework", "version": "0.0.0", "private": true, + "sideEffects": false, "type": "module", "scripts": { "dev": "vite", @@ -17,6 +18,8 @@ "@types/node": "^22.13.1", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@vanilla-extract/css": "^1.17.4", + "@vanilla-extract/vite-plugin": "^5.1.1", "@vitejs/plugin-react": "^4.5.2", "@vitejs/plugin-rsc": "0.4.11", "cross-env": "^7.0.3", diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index 1a5a962006..f6468c3b02 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -77,8 +77,10 @@ type ViteConfigBuildArgs = { type ViteConfigBaseArgs = { templateName?: TemplateName; + base?: string; envDir?: string; mdx?: boolean; + vanillaExtract?: boolean; }; type ViteConfigArgs = ( @@ -140,15 +142,18 @@ export const viteConfig = { ].join("\n") } ${args.mdx ? 'import mdx from "@mdx-js/rollup";' : ""} + ${args.vanillaExtract ? 'import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";' : ""} import { envOnlyMacros } from "vite-env-only"; import tsconfigPaths from "vite-tsconfig-paths"; export default async () => ({ ${args.port ? await viteConfig.server(args) : ""} ${viteConfig.build(args)} + ${args.base ? `base: "${args.base}",` : ""} envDir: ${args.envDir ? `"${args.envDir}"` : "undefined"}, plugins: [ ${args.mdx ? "mdx()," : ""} + ${args.vanillaExtract ? "vanillaExtractPlugin({ emitCssInSsr: true })," : ""} reactRouter(), envOnlyMacros(), tsconfigPaths() @@ -331,6 +336,25 @@ export const reactRouterServe = async ({ return () => serveProc.kill(); }; +export const runStartScript = async ({ + cwd, + port, + basename, +}: { + cwd: string; + port: number; + basename?: string; +}) => { + let nodeBin = process.argv[0]; + let proc = spawn(nodeBin, ["start.js"], { + cwd, + stdio: "pipe", + env: { NODE_ENV: "production", PORT: port.toFixed(0) }, + }); + await waitForServer(proc, { port, basename }); + return () => proc.kill(); +}; + export const wranglerPagesDev = async ({ cwd, port, diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index 13c7e3a03a..dbc1191533 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -1,19 +1,20 @@ import type { Page } from "@playwright/test"; import { test, expect } from "@playwright/test"; import getPort from "get-port"; -import dedent from "dedent"; import { createProject, createEditor, dev, build, + runStartScript, reactRouterServe, customDev, EXPRESS_SERVER, reactRouterConfig, viteConfig, viteMajorTemplates, + type TemplateName, } from "./helpers/vite.js"; const js = String.raw; @@ -22,7 +23,26 @@ const css = String.raw; const PADDING = "20px"; const NEW_PADDING = "30px"; -const files = { +const fixtures = [ + ...viteMajorTemplates, + { + templateName: "rsc-vite-framework", + templateDisplayName: "RSC Vite Framework", + }, +] as const satisfies ReadonlyArray<{ + templateName: TemplateName; + templateDisplayName: string; +}>; + +type RouteBasePath = "css" | "rsc-server-first-css"; +const getRouteBasePaths = (templateName: TemplateName) => { + if (templateName.includes("rsc")) { + return ["css", "rsc-server-first-css"] as const satisfies RouteBasePath[]; + } + return ["css"] as const satisfies RouteBasePath[]; +}; + +const files = ({ templateName }: { templateName: TemplateName }) => ({ "postcss.config.js": js` export default ({ plugins: [ @@ -44,22 +64,33 @@ const files = { ], }); `, - "app/entry.client.tsx": js` - import "./entry.client.css"; - - import { HydratedRouter } from "react-router/dom"; - import { startTransition, StrictMode } from "react"; - import { hydrateRoot } from "react-dom/client"; - - startTransition(() => { - hydrateRoot( - document, - - - - ); - }); - `, + // RSC Framework mode doesn't support custom entries yet + ...(!templateName.includes("rsc") + ? { + "app/entry.client.tsx": js` + import "./entry.client.css"; + + import { HydratedRouter } from "react-router/dom"; + import { startTransition, StrictMode } from "react"; + import { hydrateRoot } from "react-dom/client"; + + startTransition(() => { + hydrateRoot( + document, + + + + ); + }); + `, + "app/entry.client.css": css` + .entry-client { + background: pink; + padding: ${PADDING}; + } + `, + } + : {}), "app/root.tsx": js` import { Links, Meta, Outlet, Scripts } from "react-router"; @@ -78,107 +109,93 @@ const files = { ); } `, - "app/entry.client.css": css` - .entry-client { - background: pink; - padding: ${PADDING}; - } - `, - "app/styles-bundled.css": css` - .index_bundled { - background: papayawhip; - padding: ${PADDING}; - } - `, - "app/styles-postcss-linked.css": css` - .index_postcss_linked { - background: salmon; - padding: PADDING_INJECTED_VIA_POSTCSS; - } - `, - "app/styles.module.css": css` - .index { - background: peachpuff; - padding: ${PADDING}; - } - `, - "app/styles-vanilla-global.css.ts": js` - import { createVar, globalStyle } from "@vanilla-extract/css"; - - globalStyle(".index_vanilla_global", { - background: "lightgreen", - padding: "${PADDING}", - }); - `, - "app/styles-vanilla-local.css.ts": js` - import { style } from "@vanilla-extract/css"; - - export const index = style({ - background: "lightblue", - padding: "${PADDING}", - }); - `, - "app/routes/_index.tsx": js` - import "../styles-bundled.css"; - import postcssLinkedStyles from "../styles-postcss-linked.css?url"; - import cssModulesStyles from "../styles.module.css"; - import "../styles-vanilla-global.css"; - import * as stylesVanillaLocal from "../styles-vanilla-local.css"; - - export function links() { - return [{ rel: "stylesheet", href: postcssLinkedStyles }]; - } + ...Object.assign( + {}, + ...getRouteBasePaths(templateName).map((routeBasePath) => { + const isServerFirstRoute = routeBasePath === "rsc-server-first-css"; + const exportName = isServerFirstRoute ? "ServerComponent" : "default"; + + return { + [`app/routes/${routeBasePath}/styles-bundled.css`]: css` + .${routeBasePath}-bundled { + background: papayawhip; + padding: ${PADDING}; + } + `, + [`app/routes/${routeBasePath}/styles-postcss-linked.css`]: css` + .${routeBasePath}-postcss-linked { + background: salmon; + padding: PADDING_INJECTED_VIA_POSTCSS; + } + `, + [`app/routes/${routeBasePath}/styles.module.css`]: css` + .index { + background: peachpuff; + padding: ${PADDING}; + } + `, + [`app/routes/${routeBasePath}/styles-vanilla-global.css.ts`]: js` + import { createVar, globalStyle } from "@vanilla-extract/css"; + + globalStyle(".${routeBasePath}-vanilla-global", { + background: "lightgreen", + padding: "${PADDING}", + }); + `, + [`app/routes/${routeBasePath}/styles-vanilla-local.css.ts`]: js` + import { style } from "@vanilla-extract/css"; - export default function IndexRoute() { - return ( - <> - -
-
-
-
-
-
-

CSS test

+ export const index = style({ + background: "lightblue", + padding: "${PADDING}", + }); + `, + [`app/routes/${routeBasePath}/route.tsx`]: js` + import "./styles-bundled.css"; + import postcssLinkedStyles from "./styles-postcss-linked.css?url"; + import cssModulesStyles from "./styles.module.css"; + import "./styles-vanilla-global.css"; + import * as stylesVanillaLocal from "./styles-vanilla-local.css"; + + // Workaround for "Generated an empty chunk" warnings in RSC Framework Mode + export function loader() { + return null; + } + + export function links() { + return [{ rel: "stylesheet", href: postcssLinkedStyles }]; + } + + function TestRoute() { + return ( + <> + +
+
+
+
+
+
+

CSS test

+
+
+
-
-
-
- - ); - } - `, -}; - -const VITE_CONFIG = async ({ - port, - base, - cssCodeSplit, -}: { - port: number; - base?: string; - cssCodeSplit?: boolean; -}) => dedent` - import { reactRouter } from "@react-router/dev/vite"; - import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; - - export default async () => ({ - ${await viteConfig.server({ port })} - ${viteConfig.build({ cssCodeSplit })} - ${base ? `base: "${base}",` : ""} - plugins: [ - reactRouter(), - vanillaExtractPlugin({ - emitCssInSsr: true, - }), - ], - }); -`; + + ); + } + + export ${exportName === "default" ? "default" : `const ${exportName} =`} TestRoute; + `, + }; + }), + ), +}); test.describe("Vite CSS", () => { - viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => { + fixtures.forEach(({ templateName, templateDisplayName }) => { test.describe(templateDisplayName, () => { test.describe("vite dev", async () => { let port: number; @@ -190,10 +207,14 @@ test.describe("Vite CSS", () => { cwd = await createProject( { "react-router.config.ts": reactRouterConfig({ - viteEnvironmentApi: templateName === "vite-6-template", + viteEnvironmentApi: templateName !== "vite-5-template", }), - "vite.config.ts": await VITE_CONFIG({ port }), - ...files, + "vite.config.ts": await viteConfig.basic({ + port, + templateName, + vanillaExtract: true, + }), + ...files({ templateName }), }, templateName, ); @@ -204,20 +225,24 @@ test.describe("Vite CSS", () => { test.describe(() => { test.use({ javaScriptEnabled: false }); test("without JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); + await pageLoadWorkflow({ page, port, templateName }); }); }); test.describe(() => { test.use({ javaScriptEnabled: true }); test("with JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); - await hmrWorkflow({ page, port, cwd }); + await pageLoadWorkflow({ page, port, templateName }); + await hmrWorkflow({ page, port, cwd, templateName }); }); }); }); test.describe("vite dev with custom base", async () => { + test.fixme( + templateName.includes("rsc"), + "RSC Framework mode doesn't support basename yet", + ); let port: number; let cwd: string; let stop: () => void; @@ -231,8 +256,13 @@ test.describe("Vite CSS", () => { viteEnvironmentApi: templateName === "vite-6-template", basename: base, }), - "vite.config.ts": await VITE_CONFIG({ port, base }), - ...files, + "vite.config.ts": await viteConfig.basic({ + port, + base, + templateName, + vanillaExtract: true, + }), + ...files({ templateName }), }, templateName, ); @@ -243,20 +273,25 @@ test.describe("Vite CSS", () => { test.describe(() => { test.use({ javaScriptEnabled: false }); test("without JS", async ({ page }) => { - await pageLoadWorkflow({ page, port, base }); + await pageLoadWorkflow({ page, port, base, templateName }); }); }); test.describe(() => { test.use({ javaScriptEnabled: true }); test("with JS", async ({ page }) => { - await pageLoadWorkflow({ page, port, base }); - await hmrWorkflow({ page, port, cwd, base }); + await pageLoadWorkflow({ page, port, base, templateName }); + await hmrWorkflow({ page, port, cwd, base, templateName }); }); }); }); test.describe("express", async () => { + test.fixme( + templateName.includes("rsc"), + "RSC Framework mode doesn't support Vite middleware mode yet", + ); + let port: number; let cwd: string; let stop: () => void; @@ -265,9 +300,16 @@ test.describe("Vite CSS", () => { port = await getPort(); cwd = await createProject( { - "vite.config.ts": await VITE_CONFIG({ port }), + "react-router.config.ts": reactRouterConfig({ + viteEnvironmentApi: templateName !== "vite-5-template", + }), + "vite.config.ts": await viteConfig.basic({ + port, + templateName, + vanillaExtract: true, + }), "server.mjs": EXPRESS_SERVER({ port }), - ...files, + ...files({ templateName }), }, templateName, ); @@ -278,15 +320,15 @@ test.describe("Vite CSS", () => { test.describe(() => { test.use({ javaScriptEnabled: false }); test("without JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); + await pageLoadWorkflow({ page, port, templateName }); }); }); test.describe(() => { test.use({ javaScriptEnabled: true }); test("with JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); - await hmrWorkflow({ page, port, cwd }); + await pageLoadWorkflow({ page, port, templateName }); + await hmrWorkflow({ page, port, cwd, templateName }); }); }); }); @@ -300,8 +342,15 @@ test.describe("Vite CSS", () => { port = await getPort(); cwd = await createProject( { - "vite.config.ts": await VITE_CONFIG({ port }), - ...files, + "react-router.config.ts": reactRouterConfig({ + viteEnvironmentApi: templateName !== "vite-5-template", + }), + "vite.config.ts": await viteConfig.basic({ + port, + templateName, + vanillaExtract: true, + }), + ...files({ templateName }), }, templateName, ); @@ -321,28 +370,43 @@ test.describe("Vite CSS", () => { VITE_CJS_IGNORE_WARNING: "true", }, }); - expect(stderr.toString()).toBeFalsy(); + let stderrString = stderr.toString(); + if (templateName.includes("rsc")) { + // In RSC builds, the same assets can be generated multiple times + stderrString = stderrString.replace( + /The emitted file ".*?" overwrites a previously emitted file of the same name\.\n?/g, + "", + ); + } + expect(stderrString).toBeFalsy(); expect(status).toBe(0); - stop = await reactRouterServe({ cwd, port }); + stop = templateName.includes("rsc") + ? await runStartScript({ cwd, port }) + : await reactRouterServe({ cwd, port }); }); test.afterAll(() => stop()); test.describe(() => { test.use({ javaScriptEnabled: false }); test("without JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); + await pageLoadWorkflow({ page, port, templateName }); }); }); test.describe(() => { test.use({ javaScriptEnabled: true }); test("with JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); + await pageLoadWorkflow({ page, port, templateName }); }); }); }); test.describe("vite build with CSS code splitting disabled", async () => { + test.fixme( + templateName.includes("rsc"), + "RSC Framework mode doesn't support disabling CSS code splitting yet (likely due to @vitejs/plugin-rsc)", + ); + let port: number; let cwd: string; let stop: () => void; @@ -351,11 +415,16 @@ test.describe("Vite CSS", () => { port = await getPort(); cwd = await createProject( { - "vite.config.ts": await VITE_CONFIG({ + "react-router.config.ts": reactRouterConfig({ + viteEnvironmentApi: templateName !== "vite-5-template", + }), + "vite.config.ts": await viteConfig.basic({ port, + templateName, cssCodeSplit: false, + vanillaExtract: true, }), - ...files, + ...files({ templateName }), }, templateName, ); @@ -377,21 +446,23 @@ test.describe("Vite CSS", () => { }); expect(stderr.toString()).toBeFalsy(); expect(status).toBe(0); - stop = await reactRouterServe({ cwd, port }); + stop = templateName.includes("rsc") + ? await runStartScript({ cwd, port }) + : await reactRouterServe({ cwd, port }); }); test.afterAll(() => stop()); test.describe(() => { test.use({ javaScriptEnabled: false }); test("without JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); + await pageLoadWorkflow({ page, port, templateName }); }); }); test.describe(() => { test.use({ javaScriptEnabled: true }); test("with JS", async ({ page }) => { - await pageLoadWorkflow({ page, port }); + await pageLoadWorkflow({ page, port, templateName }); }); }); }); @@ -403,30 +474,34 @@ async function pageLoadWorkflow({ page, port, base, + templateName, }: { page: Page; port: number; base?: string; + templateName: TemplateName; }) { - let pageErrors: Error[] = []; - page.on("pageerror", (error) => pageErrors.push(error)); + for (const routeBase of getRouteBasePaths(templateName)) { + let pageErrors: Error[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - await page.goto(`http://localhost:${port}${base ?? "/"}`, { - waitUntil: "networkidle", - }); + await page.goto(`http://localhost:${port}${base ?? "/"}${routeBase}`, { + waitUntil: "networkidle", + }); - await Promise.all( - [ - "#css-bundled", - "#css-postcss-linked", - "#css-modules", - "#css-vanilla-global", - "#css-vanilla-local", - ].map( - async (selector) => - await expect(page.locator(selector)).toHaveCSS("padding", PADDING), - ), - ); + await Promise.all( + [ + "#css-bundled", + "#css-postcss-linked", + "#css-modules", + "#css-vanilla-global", + "#css-vanilla-local", + ].map( + async (selector) => + await expect(page.locator(selector)).toHaveCSS("padding", PADDING), + ), + ); + } } async function hmrWorkflow({ @@ -434,60 +509,77 @@ async function hmrWorkflow({ cwd, port, base, + templateName, }: { page: Page; cwd: string; port: number; base?: string; + templateName: TemplateName; }) { - let pageErrors: Error[] = []; - page.on("pageerror", (error) => pageErrors.push(error)); + if (templateName.includes("rsc")) { + // TODO: Fix CSS HMR support in RSC Framework mode + return; + } - await page.goto(`http://localhost:${port}${base ?? "/"}`, { - waitUntil: "networkidle", - }); + for (const routeBase of getRouteBasePaths(templateName)) { + let pageErrors: Error[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - let input = page.locator("input"); - await expect(input).toBeVisible(); - await input.type("stateful"); - await expect(input).toHaveValue("stateful"); - - let edit = createEditor(cwd); - let modifyCss = (contents: string) => - contents - .replace(PADDING, NEW_PADDING) - .replace( - "PADDING_INJECTED_VIA_POSTCSS", - "NEW_PADDING_INJECTED_VIA_POSTCSS", + await page.goto(`http://localhost:${port}${base ?? "/"}${routeBase}`, { + waitUntil: "networkidle", + }); + + let input = page.locator("input"); + await expect(input).toBeVisible(); + await input.type("stateful"); + await expect(input).toHaveValue("stateful"); + + let edit = createEditor(cwd); + let modifyCss = (contents: string) => + contents + .replace(PADDING, NEW_PADDING) + .replace( + "PADDING_INJECTED_VIA_POSTCSS", + "NEW_PADDING_INJECTED_VIA_POSTCSS", + ); + + await Promise.all([ + edit(`app/routes/${routeBase}/styles-bundled.css`, modifyCss), + edit(`app/routes/${routeBase}/styles.module.css`, modifyCss), + edit(`app/routes/${routeBase}/styles-vanilla-global.css.ts`, modifyCss), + edit(`app/routes/${routeBase}/styles-vanilla-local.css.ts`, modifyCss), + edit(`app/routes/${routeBase}/styles-postcss-linked.css`, modifyCss), + ]); + + await Promise.all( + [ + "#css-bundled", + "#css-postcss-linked", + "#css-modules", + "#css-vanilla-global", + "#css-vanilla-local", + ].map( + async (selector) => + await expect(page.locator(selector)).toHaveCSS( + "padding", + NEW_PADDING, + ), + ), + ); + + // Ensure CSS updates were handled by HMR + await expect(input).toHaveValue("stateful"); + + if (routeBase === "css") { + // The following change triggers a full page reload, so we check it after all the checks for HMR state preservation + await edit("app/entry.client.css", modifyCss); + await expect(page.locator("#entry-client")).toHaveCSS( + "padding", + NEW_PADDING, ); + } - await Promise.all([ - edit("app/styles-bundled.css", modifyCss), - edit("app/styles.module.css", modifyCss), - edit("app/styles-vanilla-global.css.ts", modifyCss), - edit("app/styles-vanilla-local.css.ts", modifyCss), - edit("app/styles-postcss-linked.css", modifyCss), - ]); - - await Promise.all( - [ - "#css-bundled", - "#css-postcss-linked", - "#css-modules", - "#css-vanilla-global", - "#css-vanilla-local", - ].map( - async (selector) => - await expect(page.locator(selector)).toHaveCSS("padding", NEW_PADDING), - ), - ); - - // Ensure CSS updates were handled by HMR - await expect(input).toHaveValue("stateful"); - - // The following change triggers a full page reload, so we check it after all the checks for HMR state preservation - await edit("app/entry.client.css", modifyCss); - await expect(page.locator("#entry-client")).toHaveCSS("padding", NEW_PADDING); - - expect(pageErrors).toEqual([]); + expect(pageErrors).toEqual([]); + } } diff --git a/packages/react-router-dev/vite/rsc/virtual-route-modules.ts b/packages/react-router-dev/vite/rsc/virtual-route-modules.ts index e0093b7cc3..8c54a90676 100644 --- a/packages/react-router-dev/vite/rsc/virtual-route-modules.ts +++ b/packages/react-router-dev/vite/rsc/virtual-route-modules.ts @@ -3,12 +3,14 @@ import * as babel from "../babel"; import { parse as esModuleLexer } from "es-module-lexer"; import { removeExports } from "../remove-exports"; +const SERVER_ONLY_COMPONENT_EXPORTS = ["ServerComponent"] as const; + const SERVER_ONLY_ROUTE_EXPORTS = [ + ...SERVER_ONLY_COMPONENT_EXPORTS, "loader", "action", "unstable_middleware", "headers", - "ServerComponent", ] as const; type ServerOnlyRouteExport = (typeof SERVER_ONLY_ROUTE_EXPORTS)[number]; const SERVER_ONLY_ROUTE_EXPORTS_SET = new Set(SERVER_ONLY_ROUTE_EXPORTS); @@ -16,13 +18,34 @@ function isServerOnlyRouteExport(name: string): name is ServerOnlyRouteExport { return SERVER_ONLY_ROUTE_EXPORTS_SET.has(name as ServerOnlyRouteExport); } -const COMPONENT_EXPORTS = [ - "default", +const COMMON_COMPONENT_EXPORTS = [ "ErrorBoundary", "HydrateFallback", "Layout", ] as const; +const SERVER_FIRST_COMPONENT_EXPORTS = [ + ...COMMON_COMPONENT_EXPORTS, + ...SERVER_ONLY_COMPONENT_EXPORTS, +] as const; +type ServerFirstComponentExport = + (typeof SERVER_FIRST_COMPONENT_EXPORTS)[number]; +const SERVER_FIRST_COMPONENT_EXPORTS_SET = new Set( + SERVER_FIRST_COMPONENT_EXPORTS, +); +function isServerFirstComponentExport( + name: string, +): name is ServerFirstComponentExport { + return SERVER_FIRST_COMPONENT_EXPORTS_SET.has( + name as ServerFirstComponentExport, + ); +} + +const CLIENT_COMPONENT_EXPORTS = [ + ...COMMON_COMPONENT_EXPORTS, + "default", +] as const; + export const CLIENT_NON_COMPONENT_EXPORTS = [ "clientAction", "clientLoader", @@ -42,7 +65,7 @@ function isClientNonComponentExport( const CLIENT_ROUTE_EXPORTS = [ ...CLIENT_NON_COMPONENT_EXPORTS, - ...COMPONENT_EXPORTS, + ...CLIENT_COMPONENT_EXPORTS, ] as const; type ClientRouteExport = (typeof CLIENT_ROUTE_EXPORTS)[number]; const CLIENT_ROUTE_EXPORTS_SET = new Set(CLIENT_ROUTE_EXPORTS); @@ -120,11 +143,27 @@ async function createVirtualRouteModuleCode({ let code = ""; if (isServerFirstRoute) { + if (staticExports.some(isServerFirstComponentExport)) { + code += `import React from "react";\n`; + } for (const staticExport of staticExports) { if (isClientNonComponentExport(staticExport)) { code += `export { ${staticExport} } from "${clientModuleId}";\n`; + } else if ( + isReactServer && + isServerFirstComponentExport(staticExport) && + // Layout wraps all other component exports so doesn't need CSS injected + staticExport !== "Layout" + ) { + code += `import { ${staticExport} as ${staticExport}WithoutCss } from "${serverModuleId}";\n`; + code += `export ${staticExport === "ServerComponent" ? "default " : " "}function ${staticExport}(props) {\n`; + code += ` return React.createElement(React.Fragment, null,\n`; + code += ` import.meta.viteRsc.loadCss(),\n`; + code += ` React.createElement(${staticExport}WithoutCss, props),\n`; + code += ` );\n`; + code += `}\n`; } else if (isReactServer && isRouteExport(staticExport)) { - code += `export { ${staticExport}${staticExport === "ServerComponent" ? " as default" : ""} } from "${serverModuleId}";\n`; + code += `export { ${staticExport} } from "${serverModuleId}";\n`; } else if (isCustomRouteExport(staticExport)) { code += `export { ${staticExport} } from "${isReactServer ? serverModuleId : clientModuleId}";\n`; } @@ -206,7 +245,7 @@ function createVirtualClientRouteModuleCode({ const { staticExports, isServerFirstRoute, hasClientExports } = parseRouteExports(routeSource); const exportsToRemove = isServerFirstRoute - ? [...SERVER_ONLY_ROUTE_EXPORTS, ...COMPONENT_EXPORTS] + ? [...SERVER_ONLY_ROUTE_EXPORTS, ...CLIENT_COMPONENT_EXPORTS] : SERVER_ONLY_ROUTE_EXPORTS; const clientRouteModuleAst = babel.parse(routeSource, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0b8091100..3815f2f0b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,6 +563,12 @@ importers: '@types/react-dom': specifier: ^18.2.7 version: 18.2.7 + '@vanilla-extract/css': + specifier: ^1.17.4 + version: 1.17.4(babel-plugin-macros@3.1.0) + '@vanilla-extract/vite-plugin': + specifier: ^5.1.1 + version: 5.1.1(@types/node@22.14.0)(babel-plugin-macros@3.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))(yaml@2.8.0) '@vitejs/plugin-react': specifier: ^4.5.2 version: 4.5.2(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))