From 0dc5b1740f4d043361c01a8db357c831f31f51a6 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:24:40 +0900 Subject: [PATCH 01/26] add test --- .../tests/non_island_css_modules/_layout.tsx | 11 ++++++++++ .../tests/non_island_css_modules/index.tsx | 3 +++ packages/plugin-vite/tests/build_test.ts | 22 +++++++++++++++++++ packages/plugin-vite/tests/dev_server_test.ts | 20 +++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx create mode 100644 packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx diff --git a/packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx b/packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx new file mode 100644 index 00000000000..fbd3e704d0f --- /dev/null +++ b/packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx @@ -0,0 +1,11 @@ +import { CssModulesNonIsland } from "../../../components/CssModuleNonIsland.tsx"; +import { define } from "../../../utils.ts"; + +export default define.layout(({ Component }) => { + return ( + <> + + + + ); +}); diff --git a/packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx b/packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx new file mode 100644 index 00000000000..8f21eae576c --- /dev/null +++ b/packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

non-island CSS Modules

; +} diff --git a/packages/plugin-vite/tests/build_test.ts b/packages/plugin-vite/tests/build_test.ts index 8edafe63d2f..4180aeab797 100644 --- a/packages/plugin-vite/tests/build_test.ts +++ b/packages/plugin-vite/tests/build_test.ts @@ -388,6 +388,28 @@ integrationTest( }, ); +integrationTest( + "vite build - css modules in _layout.tsx non-island component are injected", + async () => { + await launchProd( + { cwd: viteResult.tmp }, + async (address) => { + await withBrowser(async (page) => { + await page.goto(`${address}/tests/non_island_css_modules`, { + waitUntil: "networkidle2", + }); + + const color = await page + .locator(".green > h1") + // deno-lint-ignore no-explicit-any + .evaluate((el) => window.getComputedStyle(el as any).color); + expect(color).toEqual("rgb(0, 128, 0)"); + }); + }, + ); + }, +); + integrationTest("vite build - route css import", async () => { await launchProd( { cwd: viteResult.tmp }, diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index ffc30f92ee2..c380f615b30 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -282,6 +282,26 @@ integrationTest( }, ); +integrationTest( + "vite dev - css modules in _layout.tsx non-island component are injected", + async () => { + await withBrowser(async (page) => { + await page.goto(`${demoServer.address()}/tests/non_island_css_modules`, { + waitUntil: "networkidle2", + }); + + await waitFor(async () => { + const color = await page + .locator(".green > h1") + // deno-lint-ignore no-explicit-any + .evaluate((el) => window.getComputedStyle(el as any).color); + expect(color).toEqual("rgb(0, 128, 0)"); + return true; + }); + }); + }, +); + integrationTest("vite dev - route css import", async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/css`, { From c03d47683bed1ec3361780084a9487d15b3dfd0a Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:50:51 +0900 Subject: [PATCH 02/26] works --- packages/fresh/src/commands.ts | 15 ++-- packages/fresh/src/context.ts | 45 ++++++++++-- packages/fresh/src/fs_routes.ts | 8 +- packages/fresh/src/segments.ts | 18 ++++- .../plugin-vite/src/plugins/dev_server.ts | 18 ++++- .../src/plugins/server_snapshot.ts | 73 +++++++++++++++---- 6 files changed, 143 insertions(+), 34 deletions(-) diff --git a/packages/fresh/src/commands.ts b/packages/fresh/src/commands.ts index f207d5bc8e5..c841c9bc402 100644 --- a/packages/fresh/src/commands.ts +++ b/packages/fresh/src/commands.ts @@ -1,4 +1,3 @@ -import { setAdditionalStyles } from "./context.ts"; import { HttpError } from "./error.ts"; import { isHandlerByMethod, type PageResponse } from "./handlers.ts"; import { @@ -74,11 +73,13 @@ export function newErrorCmd( export interface AppCommand { type: CommandType.App; component: RouteComponent; + css?: string[]; } export function newAppCmd( component: RouteComponent, + css?: string[], ): AppCommand { - return { type: CommandType.App, component }; + return { type: CommandType.App, component, css }; } export interface LayoutCommand { @@ -86,6 +87,7 @@ export interface LayoutCommand { pattern: string; component: RouteComponent; config?: LayoutConfig; + css?: string[]; includeLastSegment: boolean; } export function newLayoutCmd( @@ -93,12 +95,14 @@ export function newLayoutCmd( component: RouteComponent, config: LayoutConfig | undefined, includeLastSegment: boolean, + css?: string[], ): LayoutCommand { return { type: CommandType.Layout, pattern, component, config, + css, includeLastSegment, }; } @@ -253,7 +257,7 @@ function applyCommandsInner( break; } case CommandType.App: { - root.app = cmd.component; + root.app = { component: cmd.component, css: cmd.css ?? null }; break; } case CommandType.Layout: { @@ -265,6 +269,7 @@ function applyCommandsInner( segment.layout = { component: cmd.component, config: cmd.config ?? null, + css: cmd.css ?? null, }; break; } @@ -290,10 +295,6 @@ function applyCommandsInner( def = await route(); } - if (def.css !== undefined) { - setAdditionalStyles(ctx, def.css); - } - return renderRoute(ctx, def); }); diff --git a/packages/fresh/src/context.ts b/packages/fresh/src/context.ts index 3c2d9c07815..4570d3cd21e 100644 --- a/packages/fresh/src/context.ts +++ b/packages/fresh/src/context.ts @@ -98,8 +98,11 @@ export type ServerIslandRegistry = Map; export const internals: unique symbol = Symbol("fresh_internal"); export interface UiTree { - app: AnyComponent> | null; - layouts: ComponentDef[]; + app: { + component: AnyComponent>; + css: string[] | null; + } | null; + layouts: (ComponentDef & { css: string[] | null })[]; } /** @@ -109,7 +112,10 @@ export type FreshContext = Context; export let getBuildCache: (ctx: Context) => BuildCache; export let getInternals: (ctx: Context) => UiTree; -export let setAdditionalStyles: (ctx: Context, css: string[]) => void; +export let setAdditionalStyles: ( + ctx: Context, + css: string[] | null | undefined, +) => void; /** * The context passed to every middleware. It is unique for every request. @@ -182,8 +188,24 @@ export class Context { // deno-lint-ignore no-explicit-any getInternals = (ctx: Context) => ctx.#internal as any; getBuildCache = (ctx: Context) => ctx.#buildCache; - setAdditionalStyles = (ctx: Context, css: string[]) => - ctx.#additionalStyles = css; + setAdditionalStyles = ( + ctx: Context, + css: string[] | null | undefined, + ) => { + if (css === null || css === undefined || css.length === 0) return; + + if (ctx.#additionalStyles === null) { + ctx.#additionalStyles = css.slice(); + return; + } + + for (let i = 0; i < css.length; i++) { + const href = css[i]; + if (!ctx.#additionalStyles.includes(href)) { + ctx.#additionalStyles.push(href); + } + } + }; } constructor( @@ -283,6 +305,9 @@ export class Context { props.Component = () => child; const def = defs[i]; + if (def.css !== null) { + setAdditionalStyles(this, def.css); + } const result = await renderRouteComponent(this, def, () => child); if (result instanceof Response) { @@ -298,16 +323,20 @@ export class Context { let hasApp = true; - if (isAsyncAnyComponent(appDef)) { + if (appDef !== null && appDef.css !== null) { + setAdditionalStyles(this, appDef.css); + } + + if (appDef !== null && isAsyncAnyComponent(appDef.component)) { props.Component = () => appChild; - const result = await renderAsyncAnyComponent(appDef, props); + const result = await renderAsyncAnyComponent(appDef.component, props); if (result instanceof Response) { return result; } appVNode = result; } else if (appDef !== null) { - appVNode = h(appDef, { + appVNode = h(appDef.component, { Component: () => appChild, config: this.config, data: null, diff --git a/packages/fresh/src/fs_routes.ts b/packages/fresh/src/fs_routes.ts index a50826d99e6..52afe50e58f 100644 --- a/packages/fresh/src/fs_routes.ts +++ b/packages/fresh/src/fs_routes.ts @@ -106,7 +106,9 @@ export function fsItemsToCommands( } if (!mod.default) continue; - commands.push(newLayoutCmd(pattern, mod.default, mod.config, true)); + commands.push( + newLayoutCmd(pattern, mod.default, mod.config, true, mod.css), + ); continue; } case CommandType.Error: { @@ -116,6 +118,7 @@ export function fsItemsToCommands( { component: mod.default ?? undefined, config: mod.config ?? undefined, + css: mod.css, // deno-lint-ignore no-explicit-any handler: (handlers as any) ?? undefined, }, @@ -128,6 +131,7 @@ export function fsItemsToCommands( commands.push(newNotFoundCmd({ config: mod.config, component: mod.default, + css: mod.css, // deno-lint-ignore no-explicit-any handler: handlers as any ?? undefined, })); @@ -137,7 +141,7 @@ export function fsItemsToCommands( const { mod } = validateFsMod(filePath, rawMod, type); if (mod.default === undefined) continue; - commands.push(newAppCmd(mod.default)); + commands.push(newAppCmd(mod.default, mod.css)); continue; } case CommandType.Route: { diff --git a/packages/fresh/src/segments.ts b/packages/fresh/src/segments.ts index 07ae745aa15..950129b0e2a 100644 --- a/packages/fresh/src/segments.ts +++ b/packages/fresh/src/segments.ts @@ -2,7 +2,7 @@ import type { AnyComponent } from "preact"; import type { MaybeLazyMiddleware, Middleware } from "./middlewares/mod.ts"; import { type Method, patternToSegments } from "./router.ts"; import type { LayoutConfig, Route } from "./types.ts"; -import { type Context, getInternals } from "./context.ts"; +import { type Context, getInternals, setAdditionalStyles } from "./context.ts"; import { recordSpanError, tracer } from "./otel.ts"; import { type HandlerFn, isHandlerByMethod } from "./handlers.ts"; import { @@ -22,10 +22,14 @@ export interface Segment { layout: { component: RouteComponent; config: LayoutConfig | null; + css: string[] | null; } | null; errorRoute: Route | null; notFound: Middleware | null; - app: RouteComponent | null; + app: { + component: RouteComponent; + css: string[] | null; + } | null; children: Map>; parent: Segment | null; } @@ -105,7 +109,11 @@ export function segmentToMiddlewares( internals.app = null; } - const def = { props: null, component: layout.component }; + const def = { + props: null, + component: layout.component, + css: layout.css, + }; if (layout.config?.skipInheritedLayouts) { internals.layouts = [def]; } else { @@ -145,6 +153,10 @@ export async function renderRoute( route: Route, status = 200, ): Promise { + if (route.css !== undefined) { + setAdditionalStyles(ctx, route.css); + } + const internals = getInternals(ctx); if (route.config?.skipAppWrapper) { internals.app = null; diff --git a/packages/plugin-vite/src/plugins/dev_server.ts b/packages/plugin-vite/src/plugins/dev_server.ts index c1ca76d6c1f..0fb0f3d6895 100644 --- a/packages/plugin-vite/src/plugins/dev_server.ts +++ b/packages/plugin-vite/src/plugins/dev_server.ts @@ -129,6 +129,7 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { res.headers.get("Content-Type")?.includes("text/html") ) { const clientEnv = server.environments.client; + const ssrEnv = server.environments.ssr; const collected = await collectCss( "fresh:client-entry", clientEnv, @@ -145,9 +146,24 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { } } + // Route/app/layout/error CSS lives behind fresh-route-css virtual + // modules that are first discovered in the SSR graph. + for (const mod of ssrEnv.moduleGraph.idToModuleMap.values()) { + if (mod.id?.includes("fresh-route-css::")) { + let id = mod.id; + if (id.startsWith("\0fresh-route-css::")) { + id = `/@id/fresh-route-css::${ + id.slice("\0fresh-route-css::".length) + }.module.css`; + } + const routeCss = await collectCss(id, clientEnv); + collected.push(...routeCss); + } + } + let html = await res.text(); - const styles = collected.join("\n"); + const styles = Array.from(new Set(collected)).join("\n"); html = html.replace("", styles + ""); const newRes = new Response(html, { diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index cf37c0452dd..8301b3240bc 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -23,6 +23,8 @@ import { import * as path from "@std/path"; import { getBuildId } from "./build_id.ts"; +const CSS_LANG_REG = /\.(css|less|sass|scss)(\?.*)?$/; + export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { const modName = "fresh:server-snapshot"; @@ -491,7 +493,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { filter: { id: /^\0fresh-route-css::/, }, - handler(id) { + async handler(id) { const name = id.slice("\0fresh-route-css::".length); const route = routes.get(name); @@ -501,6 +503,10 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { return `export default ["__FRESH_CSS_PLACEHOLDER__"];`; } + route.css = server === undefined + ? route.css + : await collectRouteCss(server, route.filePath); + const imports = route.css.map((css) => `import "${css}";`).join("\n"); return `${imports} export default ${JSON.stringify(route.css)} @@ -520,19 +526,20 @@ export default ${JSON.stringify(route.css)} const manifest = JSON.parse(asset.source as string) as Manifest; for (const info of Object.values(manifest)) { - if (info.name?.startsWith("_fresh-route___")) { - const filePath = path.join(serverOutDir, info.file); - const content = await Deno.readTextFile(filePath); - - const replaced = content.replace( - `["__FRESH_CSS_PLACEHOLDER__"]`, - info.css - ? JSON.stringify(info.css.map((css) => `/${css}`)) - : "null", - ); + if (!/\.(?:c|m)?js$/.test(info.file)) continue; - await Deno.writeTextFile(filePath, replaced); - } + const filePath = path.join(serverOutDir, info.file); + const content = await Deno.readTextFile(filePath); + if (!content.includes(`["__FRESH_CSS_PLACEHOLDER__"]`)) continue; + + const replaced = content.replace( + `["__FRESH_CSS_PLACEHOLDER__"]`, + info.css + ? JSON.stringify(info.css.map((css) => `/${css}`)) + : "null", + ); + + await Deno.writeTextFile(filePath, replaced); } } }, @@ -587,6 +594,46 @@ export default mod.default; ]; } +async function collectRouteCss( + server: ViteDevServer, + id: string, +): Promise { + const env = server.environments.ssr; + const out = new Set(); + const seen = new Set(); + const queue = [id]; + + let current: string | undefined; + while ((current = queue.pop()) !== undefined) { + if (seen.has(current)) continue; + seen.add(current); + + let mod = env.moduleGraph.getModuleById(current) ?? + env.moduleGraph.getModuleById(`\0${current}`); + + if (mod === undefined || mod.transformResult === null) { + await env.fetchModule(current); + mod = env.moduleGraph.getModuleById(current) ?? + env.moduleGraph.getModuleById(`\0${current}`); + } + + if (mod === undefined) continue; + + if (mod.id !== null && CSS_LANG_REG.test(mod.id)) { + out.add(mod.url); + continue; + } + + mod.importedModules.forEach((imported) => { + if (imported.id !== null) { + queue.push(imported.id); + } + }); + } + + return Array.from(out); +} + function walkUp( mod: EnvironmentModuleNode, fn: (mod: EnvironmentModuleNode) => boolean, From cbc4256254956a22c11e0439f9de1f6a7eba7d0d Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:10:41 +0900 Subject: [PATCH 03/26] fix --- packages/plugin-vite/src/plugins/server_snapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 8301b3240bc..a2381ac02cd 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -403,7 +403,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { }, transform: { filter: { - id: /\.(css|less|sass|scss)(\?.*)?$/, + id: CSS_LANG_REG, }, handler(_code, id) { if (server) { From 69afe2ef542ffa7792e7848d24c41a8e7075e976 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:01:11 +0900 Subject: [PATCH 04/26] simpler --- packages/fresh/src/context.ts | 10 +++------- packages/fresh/src/segments.ts | 4 +--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/fresh/src/context.ts b/packages/fresh/src/context.ts index 4570d3cd21e..ff82f1f72c5 100644 --- a/packages/fresh/src/context.ts +++ b/packages/fresh/src/context.ts @@ -192,7 +192,7 @@ export class Context { ctx: Context, css: string[] | null | undefined, ) => { - if (css === null || css === undefined || css.length === 0) return; + if (css == null) return; if (ctx.#additionalStyles === null) { ctx.#additionalStyles = css.slice(); @@ -305,9 +305,7 @@ export class Context { props.Component = () => child; const def = defs[i]; - if (def.css !== null) { - setAdditionalStyles(this, def.css); - } + setAdditionalStyles(this, def.css); const result = await renderRouteComponent(this, def, () => child); if (result instanceof Response) { @@ -323,9 +321,7 @@ export class Context { let hasApp = true; - if (appDef !== null && appDef.css !== null) { - setAdditionalStyles(this, appDef.css); - } + setAdditionalStyles(this, appDef?.css); if (appDef !== null && isAsyncAnyComponent(appDef.component)) { props.Component = () => appChild; diff --git a/packages/fresh/src/segments.ts b/packages/fresh/src/segments.ts index 950129b0e2a..b2f65d7da66 100644 --- a/packages/fresh/src/segments.ts +++ b/packages/fresh/src/segments.ts @@ -153,9 +153,7 @@ export async function renderRoute( route: Route, status = 200, ): Promise { - if (route.css !== undefined) { - setAdditionalStyles(ctx, route.css); - } + setAdditionalStyles(ctx, route.css); const internals = getInternals(ctx); if (route.config?.skipAppWrapper) { From db939386f0214fd9fd25120bfac59519dbce8488 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:23:02 +0900 Subject: [PATCH 05/26] revert --- packages/plugin-vite/src/plugins/dev_server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-vite/src/plugins/dev_server.ts b/packages/plugin-vite/src/plugins/dev_server.ts index 0fb0f3d6895..7bffde11591 100644 --- a/packages/plugin-vite/src/plugins/dev_server.ts +++ b/packages/plugin-vite/src/plugins/dev_server.ts @@ -163,7 +163,7 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { let html = await res.text(); - const styles = Array.from(new Set(collected)).join("\n"); + const styles = collected.join("\n"); html = html.replace("", styles + ""); const newRes = new Response(html, { From 1f42f059a8bdb9b56c25feea1d81ffeb9a5923c5 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:25:45 +0900 Subject: [PATCH 06/26] comment --- packages/plugin-vite/src/plugins/server_snapshot.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index a2381ac02cd..71988b4b804 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -526,6 +526,9 @@ export default ${JSON.stringify(route.css)} const manifest = JSON.parse(asset.source as string) as Manifest; for (const info of Object.values(manifest)) { + // Utility-file(_app/_layout/_error)'s CSS Modules can be hoisted into + // shared chunks like "server-entry", not just route chunks. + // Replace placeholders in any emitted JS chunk that contains one. if (!/\.(?:c|m)?js$/.test(info.file)) continue; const filePath = path.join(serverOutDir, info.file); From df3fcdbde88bc9f8703ef6048b1176e4692175bc Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:41:04 +0900 Subject: [PATCH 07/26] comment --- packages/plugin-vite/src/plugins/server_snapshot.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 71988b4b804..6e74e8a65c7 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -526,7 +526,7 @@ export default ${JSON.stringify(route.css)} const manifest = JSON.parse(asset.source as string) as Manifest; for (const info of Object.values(manifest)) { - // Utility-file(_app/_layout/_error)'s CSS Modules can be hoisted into + // Utility-file(_app/_layout/_error)'s CSS can be hoisted into // shared chunks like "server-entry", not just route chunks. // Replace placeholders in any emitted JS chunk that contains one. if (!/\.(?:c|m)?js$/.test(info.file)) continue; @@ -611,15 +611,22 @@ async function collectRouteCss( if (seen.has(current)) continue; seen.add(current); + // Modules may be registered under either their public id or Vite's + // internal "\0" id, depending on when they entered the graph. let mod = env.moduleGraph.getModuleById(current) ?? env.moduleGraph.getModuleById(`\0${current}`); - if (mod === undefined || mod.transformResult === null) { + if (mod?.transformResult == null) { + // Dev transforms are lazy. Force Vite to load the module before we + // inspect its imports for CSS dependencies. await env.fetchModule(current); mod = env.moduleGraph.getModuleById(current) ?? env.moduleGraph.getModuleById(`\0${current}`); } + // Some ids still won't resolve into the SSR graph (for example, if Vite + // does not materialize the module after fetch). Skip those and keep + // collecting CSS from the remaining graph. if (mod === undefined) continue; if (mod.id !== null && CSS_LANG_REG.test(mod.id)) { @@ -627,6 +634,8 @@ async function collectRouteCss( continue; } + // Layout/app/error utility files can reach CSS through nested component + // imports, so walk the full SSR import graph for this route module. mod.importedModules.forEach((imported) => { if (imported.id !== null) { queue.push(imported.id); From f1934f0735acf58d6cf86d828c0186d72cd655c6 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 25 Apr 2026 19:15:01 +0900 Subject: [PATCH 08/26] try fix windows --- .../src/plugins/server_snapshot.ts | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 6e74e8a65c7..01d038267cf 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -408,7 +408,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { handler(_code, id) { if (server) { const ssrGraph = server.environments.ssr.moduleGraph; - const mod = ssrGraph.getModuleById(id); + const mod = getSsrModule(server.environments.ssr, id); if (mod === undefined) return; const snapshot = ssrGraph.getModuleById("\0fresh:server-snapshot"); @@ -424,10 +424,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { if (name !== undefined) { const route = routes.get(name); if (route !== undefined) { - const mod = ssrGraph.getModuleById(id); - if (mod !== undefined) { - route.css.push(mod.url); - } + route.css.push(mod.url); const routeMod = ssrGraph.getModuleById( `\0fresh-route-css::${name}`, @@ -611,17 +608,14 @@ async function collectRouteCss( if (seen.has(current)) continue; seen.add(current); - // Modules may be registered under either their public id or Vite's - // internal "\0" id, depending on when they entered the graph. - let mod = env.moduleGraph.getModuleById(current) ?? - env.moduleGraph.getModuleById(`\0${current}`); - + let mod = getSsrModule(env, current); if (mod?.transformResult == null) { // Dev transforms are lazy. Force Vite to load the module before we // inspect its imports for CSS dependencies. - await env.fetchModule(current); - mod = env.moduleGraph.getModuleById(current) ?? - env.moduleGraph.getModuleById(`\0${current}`); + await env.fetchModule( + path.isAbsolute(current) ? path.toFileUrl(current).href : current, + ); + mod = getSsrModule(env, current); } // Some ids still won't resolve into the SSR graph (for example, if Vite @@ -646,6 +640,28 @@ async function collectRouteCss( return Array.from(out); } +function getSsrModule( + env: ViteDevServer["environments"]["ssr"], + id: string, +): EnvironmentModuleNode | undefined { + // Real files are keyed by file path in Vite's module graph, while Fresh's + // virtual route modules are addressed by their module ids. + if (path.isAbsolute(id)) { + const mods = env.moduleGraph.getModulesByFile(path.normalize(id)); + if (mods === undefined) return undefined; + + for (const mod of mods) { + if (mod.transformResult !== null) return mod; + } + + return mods.values().next().value; + } + + const mod = env.moduleGraph.getModuleById(id) ?? + env.moduleGraph.getModuleById(`\0${id}`); + return mod; +} + function walkUp( mod: EnvironmentModuleNode, fn: (mod: EnvironmentModuleNode) => boolean, From 5be11d3a035b6614e48986b5caa5e13906663bd2 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 00:42:04 +0900 Subject: [PATCH 09/26] add test fixture --- .../demo/components/CssModuleNonIsland2.tsx | 9 +++ .../demo/components/CssModuleNonIsland3.tsx | 9 +++ .../CssModulesNonIsland2.module.css | 3 + .../CssModulesNonIsland3.module.css | 3 + packages/plugin-vite/tests/build_test.ts | 52 ++++++++++++++---- packages/plugin-vite/tests/dev_server_test.ts | 55 ++++++++++++++----- .../fixtures/non_island_css_modules/main.ts | 5 ++ .../non_island_css_modules/routes/_app.tsx | 17 ++++++ .../non_island_css_modules/routes/_error.tsx | 6 ++ .../non_island_css_modules/routes/_layout.tsx | 11 ++++ .../non_island_css_modules/routes/index.tsx | 3 + .../fixtures/non_island_css_modules/utils.ts | 4 ++ .../non_island_css_modules/vite.config.ts | 6 ++ 13 files changed, 160 insertions(+), 23 deletions(-) create mode 100644 packages/plugin-vite/demo/components/CssModuleNonIsland2.tsx create mode 100644 packages/plugin-vite/demo/components/CssModuleNonIsland3.tsx create mode 100644 packages/plugin-vite/demo/components/CssModulesNonIsland2.module.css create mode 100644 packages/plugin-vite/demo/components/CssModulesNonIsland3.module.css create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/main.ts create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/index.tsx create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/utils.ts create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/vite.config.ts diff --git a/packages/plugin-vite/demo/components/CssModuleNonIsland2.tsx b/packages/plugin-vite/demo/components/CssModuleNonIsland2.tsx new file mode 100644 index 00000000000..ff43d15c98c --- /dev/null +++ b/packages/plugin-vite/demo/components/CssModuleNonIsland2.tsx @@ -0,0 +1,9 @@ +import styles from "./CssModulesNonIsland2.module.css"; + +export function CssModulesNonIsland2() { + return ( +
+

blue text

+
+ ); +} diff --git a/packages/plugin-vite/demo/components/CssModuleNonIsland3.tsx b/packages/plugin-vite/demo/components/CssModuleNonIsland3.tsx new file mode 100644 index 00000000000..1d4bc9af0e9 --- /dev/null +++ b/packages/plugin-vite/demo/components/CssModuleNonIsland3.tsx @@ -0,0 +1,9 @@ +import styles from "./CssModulesNonIsland3.module.css"; + +export function CssModulesNonIsland3() { + return ( +
+

red text

+
+ ); +} diff --git a/packages/plugin-vite/demo/components/CssModulesNonIsland2.module.css b/packages/plugin-vite/demo/components/CssModulesNonIsland2.module.css new file mode 100644 index 00000000000..98cbbee4c6d --- /dev/null +++ b/packages/plugin-vite/demo/components/CssModulesNonIsland2.module.css @@ -0,0 +1,3 @@ +.root { + color: blue; +} diff --git a/packages/plugin-vite/demo/components/CssModulesNonIsland3.module.css b/packages/plugin-vite/demo/components/CssModulesNonIsland3.module.css new file mode 100644 index 00000000000..d810e45683b --- /dev/null +++ b/packages/plugin-vite/demo/components/CssModulesNonIsland3.module.css @@ -0,0 +1,3 @@ +.root { + color: red; +} diff --git a/packages/plugin-vite/tests/build_test.ts b/packages/plugin-vite/tests/build_test.ts index 4180aeab797..cd2a684e622 100644 --- a/packages/plugin-vite/tests/build_test.ts +++ b/packages/plugin-vite/tests/build_test.ts @@ -389,21 +389,53 @@ integrationTest( ); integrationTest( - "vite build - css modules in _layout.tsx non-island component are injected", + "vite build - css modules in _app/_layout/_error non-island component are injected", async () => { + const fixture = path.join(FIXTURE_DIR, "non_island_css_modules"); + await using res = await buildVite(fixture); + await launchProd( - { cwd: viteResult.tmp }, + { cwd: res.tmp }, async (address) => { await withBrowser(async (page) => { - await page.goto(`${address}/tests/non_island_css_modules`, { - waitUntil: "networkidle2", - }); + { + // check _app/_layout + await page.goto(`${address}`, { + waitUntil: "networkidle2", + }); + + const _app = await page + .locator(".green > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_app).toEqual("rgb(0, 128, 0)"); + + const _layout = await page + .locator(".red > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_layout).toEqual("rgb(255, 0, 0)"); + } - const color = await page - .locator(".green > h1") - // deno-lint-ignore no-explicit-any - .evaluate((el) => window.getComputedStyle(el as any).color); - expect(color).toEqual("rgb(0, 128, 0)"); + { + // check _app/_layout/_error + await page.goto(`${address}/non_existent`, { + waitUntil: "networkidle2", + }); + + const _app = await page + .locator(".green > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_app).toEqual("rgb(0, 128, 0)"); + + const _layout = await page + .locator(".red > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_layout).toEqual("rgb(255, 0, 0)"); + + const _error = await page + .locator(".blue > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_error).toEqual("rgb(0, 0, 255)"); + } }); }, ); diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index c380f615b30..3e2ed844919 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -283,20 +283,49 @@ integrationTest( ); integrationTest( - "vite dev - css modules in _layout.tsx non-island component are injected", + "vite dev - css modules in _app/_layout/_error non-island component are injected", async () => { - await withBrowser(async (page) => { - await page.goto(`${demoServer.address()}/tests/non_island_css_modules`, { - waitUntil: "networkidle2", - }); - - await waitFor(async () => { - const color = await page - .locator(".green > h1") - // deno-lint-ignore no-explicit-any - .evaluate((el) => window.getComputedStyle(el as any).color); - expect(color).toEqual("rgb(0, 128, 0)"); - return true; + const fixture = path.join(FIXTURE_DIR, "non_island_css_modules"); + await launchDevServer(fixture, async (address) => { + await withBrowser(async (page) => { + { + // check _app/_layout + await page.goto(`${address}`, { + waitUntil: "networkidle2", + }); + + const _app = await page + .locator(".green > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_app).toEqual("rgb(0, 128, 0)"); + + const _layout = await page + .locator(".red > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_layout).toEqual("rgb(255, 0, 0)"); + } + + { + // check _app/_layout/_error + await page.goto(`${address}/non_existent`, { + waitUntil: "networkidle2", + }); + + const _app = await page + .locator(".green > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_app).toEqual("rgb(0, 128, 0)"); + + const _layout = await page + .locator(".red > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_layout).toEqual("rgb(255, 0, 0)"); + + const _error = await page + .locator(".blue > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_error).toEqual("rgb(0, 0, 255)"); + } }); }); }, diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/main.ts b/packages/plugin-vite/tests/fixtures/non_island_css_modules/main.ts new file mode 100644 index 00000000000..e5cf428a2cd --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/main.ts @@ -0,0 +1,5 @@ +import { App, staticFiles } from "@fresh/core"; + +export const app = new App() + .use(staticFiles()) + .fsRoutes(); diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx new file mode 100644 index 00000000000..0ab52fa3624 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx @@ -0,0 +1,17 @@ +import type { PageProps } from "fresh"; +import { CssModulesNonIsland } from "../../../../demo/components/CssModuleNonIsland.tsx"; + +export default function App({ Component }: PageProps) { + return ( + + + + + + + + + + + ); +} diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx new file mode 100644 index 00000000000..409876b12d4 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx @@ -0,0 +1,6 @@ +import { define } from "../utils.ts"; +import { CssModulesNonIsland2 } from "../../../../demo/components/CssModuleNonIsland2.tsx"; + +export default define.page((props) => { + return ; +}); diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx new file mode 100644 index 00000000000..9efa65f50ec --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx @@ -0,0 +1,11 @@ +import { CssModulesNonIsland3 } from "../../../../demo/components/CssModuleNonIsland3.tsx"; +import { define } from "../utils.ts"; + +export default define.layout(({ Component }) => { + return ( + <> + + + + ); +}); diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/index.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/index.tsx new file mode 100644 index 00000000000..00fdc5813f9 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/index.tsx @@ -0,0 +1,3 @@ +export default function Hello() { + return

ok

; +} diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/utils.ts b/packages/plugin-vite/tests/fixtures/non_island_css_modules/utils.ts new file mode 100644 index 00000000000..8c8da636c20 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/utils.ts @@ -0,0 +1,4 @@ +import { createDefine } from "@fresh/core"; + +// deno-lint-ignore no-explicit-any +export const define = createDefine(); diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/vite.config.ts b/packages/plugin-vite/tests/fixtures/non_island_css_modules/vite.config.ts new file mode 100644 index 00000000000..727d49a2772 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import { fresh } from "@fresh/plugin-vite"; + +export default defineConfig({ + plugins: [fresh()], +}); From bf4193a466a1e040db7719e980b2183572fffca7 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:15:11 +0900 Subject: [PATCH 10/26] replaceAll --- packages/plugin-vite/src/plugins/server_snapshot.ts | 3 ++- packages/plugin-vite/tests/build_test.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 01d038267cf..69bd1e1d96c 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -532,7 +532,8 @@ export default ${JSON.stringify(route.css)} const content = await Deno.readTextFile(filePath); if (!content.includes(`["__FRESH_CSS_PLACEHOLDER__"]`)) continue; - const replaced = content.replace( + // Replace all placeholders in the file with the CSS + const replaced = content.replaceAll( `["__FRESH_CSS_PLACEHOLDER__"]`, info.css ? JSON.stringify(info.css.map((css) => `/${css}`)) diff --git a/packages/plugin-vite/tests/build_test.ts b/packages/plugin-vite/tests/build_test.ts index cd2a684e622..0d4b04c44cc 100644 --- a/packages/plugin-vite/tests/build_test.ts +++ b/packages/plugin-vite/tests/build_test.ts @@ -464,6 +464,19 @@ integrationTest("vite build - route css import", async () => { ); }); +integrationTest( + "vite build - __FRESH_CSS_PLACEHOLDER__ has been replaced", + async () => { + await using res = await buildVite(DEMO_DIR, { base: "/my-app/" }); + + const serverEntryJs = await Deno.readTextFile( + path.join(res.tmp, "_fresh", "server", "server-entry.mjs"), + ); + + expect(serverEntryJs).not.toContain("__FRESH_CSS_PLACEHOLDER__"); + }, +); + integrationTest("vite build - remote island", async () => { const fixture = path.join(FIXTURE_DIR, "remote_island"); await using res = await buildVite(fixture); From b6fe5ed5ee8de3830d6905f32a606978a10f32c4 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 11:22:44 +0900 Subject: [PATCH 11/26] Revert "try fix windows" This reverts commit f1934f0735acf58d6cf86d828c0186d72cd655c6. --- .../src/plugins/server_snapshot.ts | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 69bd1e1d96c..4e43bfd8fa7 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -408,7 +408,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { handler(_code, id) { if (server) { const ssrGraph = server.environments.ssr.moduleGraph; - const mod = getSsrModule(server.environments.ssr, id); + const mod = ssrGraph.getModuleById(id); if (mod === undefined) return; const snapshot = ssrGraph.getModuleById("\0fresh:server-snapshot"); @@ -424,7 +424,10 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { if (name !== undefined) { const route = routes.get(name); if (route !== undefined) { - route.css.push(mod.url); + const mod = ssrGraph.getModuleById(id); + if (mod !== undefined) { + route.css.push(mod.url); + } const routeMod = ssrGraph.getModuleById( `\0fresh-route-css::${name}`, @@ -609,14 +612,17 @@ async function collectRouteCss( if (seen.has(current)) continue; seen.add(current); - let mod = getSsrModule(env, current); + // Modules may be registered under either their public id or Vite's + // internal "\0" id, depending on when they entered the graph. + let mod = env.moduleGraph.getModuleById(current) ?? + env.moduleGraph.getModuleById(`\0${current}`); + if (mod?.transformResult == null) { // Dev transforms are lazy. Force Vite to load the module before we // inspect its imports for CSS dependencies. - await env.fetchModule( - path.isAbsolute(current) ? path.toFileUrl(current).href : current, - ); - mod = getSsrModule(env, current); + await env.fetchModule(current); + mod = env.moduleGraph.getModuleById(current) ?? + env.moduleGraph.getModuleById(`\0${current}`); } // Some ids still won't resolve into the SSR graph (for example, if Vite @@ -641,28 +647,6 @@ async function collectRouteCss( return Array.from(out); } -function getSsrModule( - env: ViteDevServer["environments"]["ssr"], - id: string, -): EnvironmentModuleNode | undefined { - // Real files are keyed by file path in Vite's module graph, while Fresh's - // virtual route modules are addressed by their module ids. - if (path.isAbsolute(id)) { - const mods = env.moduleGraph.getModulesByFile(path.normalize(id)); - if (mods === undefined) return undefined; - - for (const mod of mods) { - if (mod.transformResult !== null) return mod; - } - - return mods.values().next().value; - } - - const mod = env.moduleGraph.getModuleById(id) ?? - env.moduleGraph.getModuleById(`\0${id}`); - return mod; -} - function walkUp( mod: EnvironmentModuleNode, fn: (mod: EnvironmentModuleNode) => boolean, From c54f479f45604b7a336856e636fff73cbbbfbe3f Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 11:26:54 +0900 Subject: [PATCH 12/26] lint --- .../tests/fixtures/non_island_css_modules/routes/_error.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx index 409876b12d4..7328641a1c0 100644 --- a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx @@ -1,6 +1,6 @@ import { define } from "../utils.ts"; import { CssModulesNonIsland2 } from "../../../../demo/components/CssModuleNonIsland2.tsx"; -export default define.page((props) => { +export default define.page(() => { return ; }); From c5fd7e545039745e1c20712e7f1872c66c7c2f6c Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:15:50 +0900 Subject: [PATCH 13/26] rm --- .../routes/tests/non_island_css_modules/_layout.tsx | 11 ----------- .../routes/tests/non_island_css_modules/index.tsx | 3 --- 2 files changed, 14 deletions(-) delete mode 100644 packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx delete mode 100644 packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx diff --git a/packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx b/packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx deleted file mode 100644 index fbd3e704d0f..00000000000 --- a/packages/plugin-vite/demo/routes/tests/non_island_css_modules/_layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { CssModulesNonIsland } from "../../../components/CssModuleNonIsland.tsx"; -import { define } from "../../../utils.ts"; - -export default define.layout(({ Component }) => { - return ( - <> - - - - ); -}); diff --git a/packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx b/packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx deleted file mode 100644 index 8f21eae576c..00000000000 --- a/packages/plugin-vite/demo/routes/tests/non_island_css_modules/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

non-island CSS Modules

; -} From a5d90ca12fefd472f5282eb1ca86599ba0d3bc23 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:30:59 +0900 Subject: [PATCH 14/26] rm slop impl --- packages/plugin-vite/src/plugins/dev_server.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/plugin-vite/src/plugins/dev_server.ts b/packages/plugin-vite/src/plugins/dev_server.ts index 7bffde11591..c1ca76d6c1f 100644 --- a/packages/plugin-vite/src/plugins/dev_server.ts +++ b/packages/plugin-vite/src/plugins/dev_server.ts @@ -129,7 +129,6 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { res.headers.get("Content-Type")?.includes("text/html") ) { const clientEnv = server.environments.client; - const ssrEnv = server.environments.ssr; const collected = await collectCss( "fresh:client-entry", clientEnv, @@ -146,21 +145,6 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { } } - // Route/app/layout/error CSS lives behind fresh-route-css virtual - // modules that are first discovered in the SSR graph. - for (const mod of ssrEnv.moduleGraph.idToModuleMap.values()) { - if (mod.id?.includes("fresh-route-css::")) { - let id = mod.id; - if (id.startsWith("\0fresh-route-css::")) { - id = `/@id/fresh-route-css::${ - id.slice("\0fresh-route-css::".length) - }.module.css`; - } - const routeCss = await collectCss(id, clientEnv); - collected.push(...routeCss); - } - } - let html = await res.text(); const styles = collected.join("\n"); From ba75ad3708be9023e36f1248fb2af5ed3b931456 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:14:21 +0900 Subject: [PATCH 15/26] normalize windows path --- packages/fresh/src/dev/dev_build_cache.ts | 2 +- packages/fresh/src/dev/fs_crawl.ts | 4 ++-- packages/fresh/src/dev/fs_crawl_test.ts | 24 ++++++++++++++++++++++- packages/fresh/src/test_utils.ts | 6 +++++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/fresh/src/dev/dev_build_cache.ts b/packages/fresh/src/dev/dev_build_cache.ts index dd985551113..90a01462744 100644 --- a/packages/fresh/src/dev/dev_build_cache.ts +++ b/packages/fresh/src/dev/dev_build_cache.ts @@ -20,7 +20,7 @@ const WINDOWS_SEPARATOR = pathWin32.SEPARATOR; /** Normalize a path to use forward slashes so that generated files * are portable across operating systems (e.g. build on Windows, * deploy on Linux). */ -function toPosix(p: string): string { +export function toPosix(p: string): string { return p.replaceAll(WINDOWS_SEPARATOR, "/"); } diff --git a/packages/fresh/src/dev/fs_crawl.ts b/packages/fresh/src/dev/fs_crawl.ts index cc61f6669f2..5612a84ad2b 100644 --- a/packages/fresh/src/dev/fs_crawl.ts +++ b/packages/fresh/src/dev/fs_crawl.ts @@ -1,6 +1,6 @@ import { type FsAdapter, fsAdapter } from "../fs.ts"; import type { WalkEntry } from "@std/fs/walk"; -import type { FsRouteFileNoMod } from "./dev_build_cache.ts"; +import { type FsRouteFileNoMod, toPosix } from "./dev_build_cache.ts"; import * as path from "@std/path"; import { pathToPattern } from "../router.ts"; import { CommandType } from "../commands.ts"; @@ -86,7 +86,7 @@ export async function crawlRouteDir( files.push({ id, - filePath: entry.path, + filePath: toPosix(entry.path), type, pattern, routePattern, diff --git a/packages/fresh/src/dev/fs_crawl_test.ts b/packages/fresh/src/dev/fs_crawl_test.ts index fe55f04981d..af75f8a2348 100644 --- a/packages/fresh/src/dev/fs_crawl_test.ts +++ b/packages/fresh/src/dev/fs_crawl_test.ts @@ -1,6 +1,6 @@ import { expect } from "@std/expect/expect"; import { createFakeFs } from "../test_utils.ts"; -import { walkDir } from "./fs_crawl.ts"; +import { crawlRouteDir, walkDir } from "./fs_crawl.ts"; Deno.test("walkDir - ", async () => { const fs = createFakeFs({ @@ -43,3 +43,25 @@ Deno.test("walkDir - respects skip patterns", async () => { "routes/api/users.ts", ]); }); + +Deno.test({ + name: "crawlRouteDir.filePath - normalized Windows paths", + ignore: Deno.build.os !== "windows", + fn: async () => { + const fs = createFakeFs({ + "foo\\bar\\baz.txt": "foo", + "D:\\foo\\bar.tsx": "foo", + }); + + const rawFiles = await crawlRouteDir(fs, "foo", [], () => {}); + + expect(rawFiles).toEqual(expect.arrayContaining([ + expect.objectContaining({ + filePath: "foo/bar/baz.txt", + }), + expect.objectContaining({ + filePath: "D:/foo/bar.tsx", + }), + ])); + }, +}); diff --git a/packages/fresh/src/test_utils.ts b/packages/fresh/src/test_utils.ts index ed6b6776238..f76dff31015 100644 --- a/packages/fresh/src/test_utils.ts +++ b/packages/fresh/src/test_utils.ts @@ -7,6 +7,7 @@ import { DEFAULT_CONN_INFO } from "./app.ts"; import type { Command } from "./commands.ts"; import { fsItemsToCommands, type FsRouteFile } from "./fs_routes.ts"; import * as path from "@std/path"; +import { toPosix } from "./dev/dev_build_cache.ts"; const STUB = {} as unknown as Deno.ServeHandlerInfo; @@ -123,7 +124,10 @@ export function createFakeFs(files: Record): FsAdapter { }, // deno-lint-ignore require-await async isDirectory(dir) { - return Object.keys(files).some((file) => file.startsWith(dir + "/")); + return Object.keys(files).some((file) => + // normalize path to posix before comparing + toPosix(file).startsWith(dir + "/") + ); }, async mkdirp(_dir: string) { }, From 881cd2abe81da689241261aa1a9c1c8578da832d Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:54:19 +0900 Subject: [PATCH 16/26] review --- packages/fresh/src/context.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/fresh/src/context.ts b/packages/fresh/src/context.ts index ff82f1f72c5..2c9c96cd67f 100644 --- a/packages/fresh/src/context.ts +++ b/packages/fresh/src/context.ts @@ -321,7 +321,9 @@ export class Context { let hasApp = true; - setAdditionalStyles(this, appDef?.css); + if (appDef !== null) { + setAdditionalStyles(this, appDef.css); + } if (appDef !== null && isAsyncAnyComponent(appDef.component)) { props.Component = () => appChild; From 9c1e62fc1064b6333f1bee707b6c023148b1893c Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:07:31 +0900 Subject: [PATCH 17/26] review --- packages/fresh/src/context.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fresh/src/context.ts b/packages/fresh/src/context.ts index 2c9c96cd67f..e631648d111 100644 --- a/packages/fresh/src/context.ts +++ b/packages/fresh/src/context.ts @@ -201,6 +201,7 @@ export class Context { for (let i = 0; i < css.length; i++) { const href = css[i]; + // FIXME: consider to use `Set` instead of `css: string[]` for entire codebase if (!ctx.#additionalStyles.includes(href)) { ctx.#additionalStyles.push(href); } From 814e3b5a03aa146ac3593f0e83249d13947bbad6 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:56:57 +0900 Subject: [PATCH 18/26] review --- packages/plugin-vite/src/plugins/server_snapshot.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 4e43bfd8fa7..4cf94855e92 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -503,6 +503,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { return `export default ["__FRESH_CSS_PLACEHOLDER__"];`; } + // Re-collected on every load for HMR correctness. route.css = server === undefined ? route.css : await collectRouteCss(server, route.filePath); From 948dbb94397ccbc61f97c4ff8172fbc11f8d085b Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:04:52 +0900 Subject: [PATCH 19/26] rm wrong HMR --- .../src/plugins/server_snapshot.ts | 56 +------------------ 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 4cf94855e92..b95b80a8550 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -493,7 +493,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { filter: { id: /^\0fresh-route-css::/, }, - async handler(id) { + handler(id) { const name = id.slice("\0fresh-route-css::".length); const route = routes.get(name); @@ -503,11 +503,6 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { return `export default ["__FRESH_CSS_PLACEHOLDER__"];`; } - // Re-collected on every load for HMR correctness. - route.css = server === undefined - ? route.css - : await collectRouteCss(server, route.filePath); - const imports = route.css.map((css) => `import "${css}";`).join("\n"); return `${imports} export default ${JSON.stringify(route.css)} @@ -599,55 +594,6 @@ export default mod.default; ]; } -async function collectRouteCss( - server: ViteDevServer, - id: string, -): Promise { - const env = server.environments.ssr; - const out = new Set(); - const seen = new Set(); - const queue = [id]; - - let current: string | undefined; - while ((current = queue.pop()) !== undefined) { - if (seen.has(current)) continue; - seen.add(current); - - // Modules may be registered under either their public id or Vite's - // internal "\0" id, depending on when they entered the graph. - let mod = env.moduleGraph.getModuleById(current) ?? - env.moduleGraph.getModuleById(`\0${current}`); - - if (mod?.transformResult == null) { - // Dev transforms are lazy. Force Vite to load the module before we - // inspect its imports for CSS dependencies. - await env.fetchModule(current); - mod = env.moduleGraph.getModuleById(current) ?? - env.moduleGraph.getModuleById(`\0${current}`); - } - - // Some ids still won't resolve into the SSR graph (for example, if Vite - // does not materialize the module after fetch). Skip those and keep - // collecting CSS from the remaining graph. - if (mod === undefined) continue; - - if (mod.id !== null && CSS_LANG_REG.test(mod.id)) { - out.add(mod.url); - continue; - } - - // Layout/app/error utility files can reach CSS through nested component - // imports, so walk the full SSR import graph for this route module. - mod.importedModules.forEach((imported) => { - if (imported.id !== null) { - queue.push(imported.id); - } - }); - } - - return Array.from(out); -} - function walkUp( mod: EnvironmentModuleNode, fn: (mod: EnvironmentModuleNode) => boolean, From 3770e6df0ba390e617c648489e04fe2e6fb82529 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:08:46 +0900 Subject: [PATCH 20/26] try windows --- packages/fresh/src/internals_dev.ts | 1 + packages/plugin-vite/src/plugins/server_snapshot.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/fresh/src/internals_dev.ts b/packages/fresh/src/internals_dev.ts index e2d78b3d68b..bda505f237c 100644 --- a/packages/fresh/src/internals_dev.ts +++ b/packages/fresh/src/internals_dev.ts @@ -7,6 +7,7 @@ export { type IslandModChunk, type PendingStaticFile, prepareStaticFile, + toPosix, writeCompiledEntry, } from "./dev/dev_build_cache.ts"; export { specToName } from "./dev/builder.ts"; diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index b95b80a8550..3b79eb46b8a 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -13,6 +13,7 @@ import { pathToSpec, type PendingStaticFile, specToName, + toPosix, UniqueNamer, } from "fresh/internal-dev"; import { @@ -390,7 +391,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { if (def) { return `fresh-island::${def.name}`; } - const routeDef = routeFileToName.get(file); + const routeDef = routeFileToName.get(toPosix(file)); if (routeDef !== undefined) { return `fresh-route::${routeDef}`; } From 871e871e0d27f908177013d470fd373b08d60eae Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:07:49 +0900 Subject: [PATCH 21/26] try windows --- packages/plugin-vite/src/plugins/server_snapshot.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 3b79eb46b8a..54749c79c74 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -161,7 +161,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { const route = result.routes[i]; const name = routeNamer.getUniqueName(route.id); - routeFileToName.set(route.filePath, name); + routeFileToName.set(toPosix(route.filePath), name); routes.set(name, route); } @@ -391,7 +391,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { if (def) { return `fresh-island::${def.name}`; } - const routeDef = routeFileToName.get(toPosix(file)); + const routeDef = routeFileToName.get(file); if (routeDef !== undefined) { return `fresh-route::${routeDef}`; } From 493f81a46b9fd6d49426b1b714dd60eae82c8a2b Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:37:52 +0900 Subject: [PATCH 22/26] try windows --- packages/plugin-vite/src/plugins/server_snapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 54749c79c74..4c906818d58 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -419,7 +419,7 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { let item: EnvironmentModuleNode | undefined; while ((item = queue.pop()) !== undefined) { if (item.file !== null) { - const normalized = path.normalize(item.file); + const normalized = toPosix(path.normalize(item.file)); const name = routeFileToName.get(normalized); if (name !== undefined) { From 7a597d9312346079a83fa75d614da39149dff934 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 23 May 2026 15:34:31 +0900 Subject: [PATCH 23/26] enhance test --- .../demo/components/CssModuleNonIsland4.tsx | 9 +++ .../CssModulesNonIsland4.module.css | 3 + .../src/plugins/server_snapshot.ts | 20 ++++-- .../src/plugins/server_snapshot_test.ts | 40 ++++++++++++ packages/plugin-vite/tests/build_test.ts | 65 +++++++++++++++++-- packages/plugin-vite/tests/dev_server_test.ts | 24 ++++++- .../non_island_css_modules/routes/_404.tsx | 6 ++ .../non_island_css_modules/routes/boom.tsx | 5 ++ 8 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 packages/plugin-vite/demo/components/CssModuleNonIsland4.tsx create mode 100644 packages/plugin-vite/demo/components/CssModulesNonIsland4.module.css create mode 100644 packages/plugin-vite/src/plugins/server_snapshot_test.ts create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/boom.tsx diff --git a/packages/plugin-vite/demo/components/CssModuleNonIsland4.tsx b/packages/plugin-vite/demo/components/CssModuleNonIsland4.tsx new file mode 100644 index 00000000000..7ae45e828f0 --- /dev/null +++ b/packages/plugin-vite/demo/components/CssModuleNonIsland4.tsx @@ -0,0 +1,9 @@ +import styles from "./CssModulesNonIsland4.module.css"; + +export function CssModulesNonIsland4() { + return ( +
+

orange text

+
+ ); +} diff --git a/packages/plugin-vite/demo/components/CssModulesNonIsland4.module.css b/packages/plugin-vite/demo/components/CssModulesNonIsland4.module.css new file mode 100644 index 00000000000..d70e5bc2823 --- /dev/null +++ b/packages/plugin-vite/demo/components/CssModulesNonIsland4.module.css @@ -0,0 +1,3 @@ +.root { + color: orange; +} diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index 4c906818d58..192d2936c68 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -25,6 +25,17 @@ import * as path from "@std/path"; import { getBuildId } from "./build_id.ts"; const CSS_LANG_REG = /\.(css|less|sass|scss)(\?.*)?$/; +export const FRESH_CSS_PLACEHOLDER = `["__FRESH_CSS_PLACEHOLDER__"]`; + +export function replaceFreshCssPlaceholders( + content: string, + css: string[] | undefined, +): string { + return content.replaceAll( + FRESH_CSS_PLACEHOLDER, + css ? JSON.stringify(css.map((href) => `/${href}`)) : "null", + ); +} export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { const modName = "fresh:server-snapshot"; @@ -530,15 +541,10 @@ export default ${JSON.stringify(route.css)} const filePath = path.join(serverOutDir, info.file); const content = await Deno.readTextFile(filePath); - if (!content.includes(`["__FRESH_CSS_PLACEHOLDER__"]`)) continue; + if (!content.includes(FRESH_CSS_PLACEHOLDER)) continue; // Replace all placeholders in the file with the CSS - const replaced = content.replaceAll( - `["__FRESH_CSS_PLACEHOLDER__"]`, - info.css - ? JSON.stringify(info.css.map((css) => `/${css}`)) - : "null", - ); + const replaced = replaceFreshCssPlaceholders(content, info.css); await Deno.writeTextFile(filePath, replaced); } diff --git a/packages/plugin-vite/src/plugins/server_snapshot_test.ts b/packages/plugin-vite/src/plugins/server_snapshot_test.ts new file mode 100644 index 00000000000..9202cf8fc3e --- /dev/null +++ b/packages/plugin-vite/src/plugins/server_snapshot_test.ts @@ -0,0 +1,40 @@ +import { expect } from "@std/expect/expect"; +import { + FRESH_CSS_PLACEHOLDER, + replaceFreshCssPlaceholders, +} from "./server_snapshot.ts"; + +Deno.test("server snapshot - replaceFreshCssPlaceholders with no css", () => { + const output = replaceFreshCssPlaceholders( + `export default ${FRESH_CSS_PLACEHOLDER};`, + undefined, + ); + + expect(output).toEqual("export default null;"); +}); + +Deno.test("server snapshot - replaceFreshCssPlaceholders once", () => { + const output = replaceFreshCssPlaceholders( + `const css = ${FRESH_CSS_PLACEHOLDER};`, + ["assets/server-entry.css"], + ); + + expect(output).toEqual(`const css = ["/assets/server-entry.css"];`); +}); + +Deno.test("server snapshot - replaceFreshCssPlaceholders multiple times", () => { + const output = replaceFreshCssPlaceholders( + [ + `const appCss = ${FRESH_CSS_PLACEHOLDER};`, + `const layoutCss = ${FRESH_CSS_PLACEHOLDER};`, + `const errorCss = ${FRESH_CSS_PLACEHOLDER};`, + ].join("\n"), + ["assets/server-entry.css"], + ); + + expect(output).toEqual([ + `const appCss = ["/assets/server-entry.css"];`, + `const layoutCss = ["/assets/server-entry.css"];`, + `const errorCss = ["/assets/server-entry.css"];`, + ].join("\n")); +}); diff --git a/packages/plugin-vite/tests/build_test.ts b/packages/plugin-vite/tests/build_test.ts index 0d4b04c44cc..e4d8354e059 100644 --- a/packages/plugin-vite/tests/build_test.ts +++ b/packages/plugin-vite/tests/build_test.ts @@ -1,4 +1,5 @@ import { expect } from "@std/expect"; +import { walk } from "@std/fs/walk"; import { waitFor, waitForText, @@ -13,8 +14,13 @@ import { usingEnv, } from "./test_utils.ts"; import * as path from "@std/path"; +import { FRESH_CSS_PLACEHOLDER } from "../src/plugins/server_snapshot.ts"; const viteResult = await buildVite(DEMO_DIR); +const NON_ISLAND_CSS_MODULES_FIXTURE = path.join( + FIXTURE_DIR, + "non_island_css_modules", +); integrationTest("vite build - launches", async () => { await launchProd( @@ -391,8 +397,7 @@ integrationTest( integrationTest( "vite build - css modules in _app/_layout/_error non-island component are injected", async () => { - const fixture = path.join(FIXTURE_DIR, "non_island_css_modules"); - await using res = await buildVite(fixture); + await using res = await buildVite(NON_ISLAND_CSS_MODULES_FIXTURE); await launchProd( { cwd: res.tmp }, @@ -417,7 +422,7 @@ integrationTest( { // check _app/_layout/_error - await page.goto(`${address}/non_existent`, { + await page.goto(`${address}/boom`, { waitUntil: "networkidle2", }); @@ -436,6 +441,28 @@ integrationTest( .evaluate((el) => window.getComputedStyle(el).color); expect(_error).toEqual("rgb(0, 0, 255)"); } + + { + // check _app/_layout/_404 + await page.goto(`${address}/non_existent`, { + waitUntil: "networkidle2", + }); + + const _app = await page + .locator(".green > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_app).toEqual("rgb(0, 128, 0)"); + + const _layout = await page + .locator(".red > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_layout).toEqual("rgb(255, 0, 0)"); + + const _404 = await page + .locator(".orange > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_404).toEqual("rgb(255, 165, 0)"); + } }); }, ); @@ -465,15 +492,41 @@ integrationTest("vite build - route css import", async () => { }); integrationTest( - "vite build - __FRESH_CSS_PLACEHOLDER__ has been replaced", + "vite build - __FRESH_CSS_PLACEHOLDER__ has been replaced in all server chunks", + async () => { + await using res = await buildVite(NON_ISLAND_CSS_MODULES_FIXTURE, { + base: "/my-app/", + }); + + const serverDir = path.join(res.tmp, "_fresh", "server"); + const inspected: string[] = []; + for await ( + const entry of walk(serverDir, { + exts: [".mjs"], + includeDirs: false, + }) + ) { + const content = await Deno.readTextFile(entry.path); + inspected.push(path.relative(serverDir, entry.path)); + expect(content).not.toContain(FRESH_CSS_PLACEHOLDER); + } + + expect(inspected.length).toBeGreaterThan(0); + }, +); + +integrationTest( + "vite build - shared server chunk keeps utility css references after replacement", async () => { - await using res = await buildVite(DEMO_DIR, { base: "/my-app/" }); + await using res = await buildVite(NON_ISLAND_CSS_MODULES_FIXTURE); const serverEntryJs = await Deno.readTextFile( path.join(res.tmp, "_fresh", "server", "server-entry.mjs"), ); - expect(serverEntryJs).not.toContain("__FRESH_CSS_PLACEHOLDER__"); + const cssRefs = serverEntryJs.match(/"\/assets\/server-entry-.*?\.css"/g) ?? + []; + expect(cssRefs.length).toBeGreaterThanOrEqual(4); }, ); diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index 3e2ed844919..2349f22dd50 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -307,7 +307,7 @@ integrationTest( { // check _app/_layout/_error - await page.goto(`${address}/non_existent`, { + await page.goto(`${address}/boom`, { waitUntil: "networkidle2", }); @@ -326,6 +326,28 @@ integrationTest( .evaluate((el) => window.getComputedStyle(el).color); expect(_error).toEqual("rgb(0, 0, 255)"); } + + { + // check _app/_layout/_404 + await page.goto(`${address}/non_existent`, { + waitUntil: "networkidle2", + }); + + const _app = await page + .locator(".green > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_app).toEqual("rgb(0, 128, 0)"); + + const _layout = await page + .locator(".red > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_layout).toEqual("rgb(255, 0, 0)"); + + const _404 = await page + .locator(".orange > h1") + .evaluate((el) => window.getComputedStyle(el).color); + expect(_404).toEqual("rgb(255, 165, 0)"); + } }); }); }, diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx new file mode 100644 index 00000000000..a7ea53d8fd7 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx @@ -0,0 +1,6 @@ +import { CssModulesNonIsland4 } from "../../../../demo/components/CssModuleNonIsland4.tsx"; +import { define } from "../utils.ts"; + +export default define.page(() => { + return ; +}); diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/boom.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/boom.tsx new file mode 100644 index 00000000000..cd79ff05703 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/boom.tsx @@ -0,0 +1,5 @@ +import { define } from "../utils.ts"; + +export default define.page(() => { + throw new Error("boom"); +}); From e75d5ddf64b77957727457febfaf4c826b3a37d1 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Wed, 27 May 2026 14:44:10 +0900 Subject: [PATCH 24/26] review for const loop --- packages/fresh/src/context.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/fresh/src/context.ts b/packages/fresh/src/context.ts index e631648d111..f487627d2d7 100644 --- a/packages/fresh/src/context.ts +++ b/packages/fresh/src/context.ts @@ -199,9 +199,7 @@ export class Context { return; } - for (let i = 0; i < css.length; i++) { - const href = css[i]; - // FIXME: consider to use `Set` instead of `css: string[]` for entire codebase + for (const href of css) { if (!ctx.#additionalStyles.includes(href)) { ctx.#additionalStyles.push(href); } From 9ce01e2627edc77c7f17055231ef377d0c4bfbd6 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Wed, 27 May 2026 14:46:49 +0900 Subject: [PATCH 25/26] mv fixture --- .../non_island_css_modules}/components/CssModuleNonIsland2.tsx | 0 .../non_island_css_modules}/components/CssModuleNonIsland3.tsx | 0 .../non_island_css_modules}/components/CssModuleNonIsland4.tsx | 0 .../components/CssModulesNonIsland2.module.css | 0 .../components/CssModulesNonIsland3.module.css | 0 .../components/CssModulesNonIsland4.module.css | 0 .../tests/fixtures/non_island_css_modules/routes/_404.tsx | 2 +- .../tests/fixtures/non_island_css_modules/routes/_error.tsx | 2 +- .../tests/fixtures/non_island_css_modules/routes/_layout.tsx | 2 +- 9 files changed, 3 insertions(+), 3 deletions(-) rename packages/plugin-vite/{demo => tests/fixtures/non_island_css_modules}/components/CssModuleNonIsland2.tsx (100%) rename packages/plugin-vite/{demo => tests/fixtures/non_island_css_modules}/components/CssModuleNonIsland3.tsx (100%) rename packages/plugin-vite/{demo => tests/fixtures/non_island_css_modules}/components/CssModuleNonIsland4.tsx (100%) rename packages/plugin-vite/{demo => tests/fixtures/non_island_css_modules}/components/CssModulesNonIsland2.module.css (100%) rename packages/plugin-vite/{demo => tests/fixtures/non_island_css_modules}/components/CssModulesNonIsland3.module.css (100%) rename packages/plugin-vite/{demo => tests/fixtures/non_island_css_modules}/components/CssModulesNonIsland4.module.css (100%) diff --git a/packages/plugin-vite/demo/components/CssModuleNonIsland2.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland2.tsx similarity index 100% rename from packages/plugin-vite/demo/components/CssModuleNonIsland2.tsx rename to packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland2.tsx diff --git a/packages/plugin-vite/demo/components/CssModuleNonIsland3.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland3.tsx similarity index 100% rename from packages/plugin-vite/demo/components/CssModuleNonIsland3.tsx rename to packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland3.tsx diff --git a/packages/plugin-vite/demo/components/CssModuleNonIsland4.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland4.tsx similarity index 100% rename from packages/plugin-vite/demo/components/CssModuleNonIsland4.tsx rename to packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland4.tsx diff --git a/packages/plugin-vite/demo/components/CssModulesNonIsland2.module.css b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland2.module.css similarity index 100% rename from packages/plugin-vite/demo/components/CssModulesNonIsland2.module.css rename to packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland2.module.css diff --git a/packages/plugin-vite/demo/components/CssModulesNonIsland3.module.css b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland3.module.css similarity index 100% rename from packages/plugin-vite/demo/components/CssModulesNonIsland3.module.css rename to packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland3.module.css diff --git a/packages/plugin-vite/demo/components/CssModulesNonIsland4.module.css b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland4.module.css similarity index 100% rename from packages/plugin-vite/demo/components/CssModulesNonIsland4.module.css rename to packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland4.module.css diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx index a7ea53d8fd7..2e8e201adc2 100644 --- a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_404.tsx @@ -1,4 +1,4 @@ -import { CssModulesNonIsland4 } from "../../../../demo/components/CssModuleNonIsland4.tsx"; +import { CssModulesNonIsland4 } from "../components/CssModuleNonIsland4.tsx"; import { define } from "../utils.ts"; export default define.page(() => { diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx index 7328641a1c0..9a263934760 100644 --- a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_error.tsx @@ -1,5 +1,5 @@ import { define } from "../utils.ts"; -import { CssModulesNonIsland2 } from "../../../../demo/components/CssModuleNonIsland2.tsx"; +import { CssModulesNonIsland2 } from "../components/CssModuleNonIsland2.tsx"; export default define.page(() => { return ; diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx index 9efa65f50ec..b94e8413fb9 100644 --- a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_layout.tsx @@ -1,4 +1,4 @@ -import { CssModulesNonIsland3 } from "../../../../demo/components/CssModuleNonIsland3.tsx"; +import { CssModulesNonIsland3 } from "../components/CssModuleNonIsland3.tsx"; import { define } from "../utils.ts"; export default define.layout(({ Component }) => { From f7f49ab87d587efd3ebe40c49ffaf67703795962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Wed, 27 May 2026 07:58:34 +0200 Subject: [PATCH 26/26] fix: colocate CssModuleNonIsland fixture files under tests/fixtures Move CssModuleNonIsland.tsx and CssModulesNonIsland.module.css from demo/components into the non_island_css_modules fixture directory, and update the _app.tsx import to match. The fixture is now self-contained. --- .../components/CssModuleNonIsland.tsx | 10 ++++++++++ .../components/CssModulesNonIsland.module.css | 3 +++ .../fixtures/non_island_css_modules/routes/_app.tsx | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland.tsx create mode 100644 packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland.module.css diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland.tsx new file mode 100644 index 00000000000..c36712eb206 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModuleNonIsland.tsx @@ -0,0 +1,10 @@ +// @ts-ignore upstream issue https://github.com/denoland/deno/issues/30560 +import styles from "./CssModulesNonIsland.module.css"; + +export function CssModulesNonIsland() { + return ( +
+

green text

+
+ ); +} diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland.module.css b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland.module.css new file mode 100644 index 00000000000..211cf429c98 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/components/CssModulesNonIsland.module.css @@ -0,0 +1,3 @@ +.root { + color: green; +} diff --git a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx index 0ab52fa3624..332ae5830b7 100644 --- a/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx +++ b/packages/plugin-vite/tests/fixtures/non_island_css_modules/routes/_app.tsx @@ -1,5 +1,5 @@ import type { PageProps } from "fresh"; -import { CssModulesNonIsland } from "../../../../demo/components/CssModuleNonIsland.tsx"; +import { CssModulesNonIsland } from "../components/CssModuleNonIsland.tsx"; export default function App({ Component }: PageProps) { return (