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 (