Skip to content

Commit 3afaaf4

Browse files
authored
fix: CSS modules not working in _app/_layout/_error and across routes in non-island components (#3781)
1 parent 072d092 commit 3afaaf4

30 files changed

Lines changed: 465 additions & 40 deletions

packages/fresh/src/commands.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { setAdditionalStyles } from "./context.ts";
21
import { HttpError } from "./error.ts";
32
import { isHandlerByMethod, type PageResponse } from "./handlers.ts";
43
import {
@@ -74,31 +73,36 @@ export function newErrorCmd<State>(
7473
export interface AppCommand<State> {
7574
type: CommandType.App;
7675
component: RouteComponent<State>;
76+
css?: string[];
7777
}
7878
export function newAppCmd<State>(
7979
component: RouteComponent<State>,
80+
css?: string[],
8081
): AppCommand<State> {
81-
return { type: CommandType.App, component };
82+
return { type: CommandType.App, component, css };
8283
}
8384

8485
export interface LayoutCommand<State> {
8586
type: CommandType.Layout;
8687
pattern: string;
8788
component: RouteComponent<State>;
8889
config?: LayoutConfig;
90+
css?: string[];
8991
includeLastSegment: boolean;
9092
}
9193
export function newLayoutCmd<State>(
9294
pattern: string,
9395
component: RouteComponent<State>,
9496
config: LayoutConfig | undefined,
9597
includeLastSegment: boolean,
98+
css?: string[],
9699
): LayoutCommand<State> {
97100
return {
98101
type: CommandType.Layout,
99102
pattern,
100103
component,
101104
config,
105+
css,
102106
includeLastSegment,
103107
};
104108
}
@@ -253,7 +257,7 @@ function applyCommandsInner<State>(
253257
break;
254258
}
255259
case CommandType.App: {
256-
root.app = cmd.component;
260+
root.app = { component: cmd.component, css: cmd.css ?? null };
257261
break;
258262
}
259263
case CommandType.Layout: {
@@ -265,6 +269,7 @@ function applyCommandsInner<State>(
265269
segment.layout = {
266270
component: cmd.component,
267271
config: cmd.config ?? null,
272+
css: cmd.css ?? null,
268273
};
269274
break;
270275
}
@@ -290,10 +295,6 @@ function applyCommandsInner<State>(
290295
def = await route();
291296
}
292297

293-
if (def.css !== undefined) {
294-
setAdditionalStyles(ctx, def.css);
295-
}
296-
297298
return renderRoute(ctx, def);
298299
});
299300

packages/fresh/src/context.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,11 @@ export type ServerIslandRegistry = Map<ComponentType, Island>;
120120
export const internals: unique symbol = Symbol("fresh_internal");
121121

122122
export interface UiTree<Data, State> {
123-
app: AnyComponent<PageProps<Data, State>> | null;
124-
layouts: ComponentDef<Data, State>[];
123+
app: {
124+
component: AnyComponent<PageProps<Data, State>>;
125+
css: string[] | null;
126+
} | null;
127+
layouts: (ComponentDef<Data, State> & { css: string[] | null })[];
125128
}
126129

127130
/**
@@ -131,7 +134,10 @@ export type FreshContext<State = unknown> = Context<State>;
131134

132135
export let getBuildCache: <T>(ctx: Context<T>) => BuildCache<T>;
133136
export let getInternals: <T>(ctx: Context<T>) => UiTree<unknown, T>;
134-
export let setAdditionalStyles: <T>(ctx: Context<T>, css: string[]) => void;
137+
export let setAdditionalStyles: <T>(
138+
ctx: Context<T>,
139+
css: string[] | null | undefined,
140+
) => void;
135141

136142
/**
137143
* The context passed to every middleware. It is unique for every request.
@@ -204,8 +210,23 @@ export class Context<State> {
204210
// deno-lint-ignore no-explicit-any
205211
getInternals = <T>(ctx: Context<T>) => ctx.#internal as any;
206212
getBuildCache = <T>(ctx: Context<T>) => ctx.#buildCache;
207-
setAdditionalStyles = <T>(ctx: Context<T>, css: string[]) =>
208-
ctx.#additionalStyles = css;
213+
setAdditionalStyles = <T>(
214+
ctx: Context<T>,
215+
css: string[] | null | undefined,
216+
) => {
217+
if (css == null) return;
218+
219+
if (ctx.#additionalStyles === null) {
220+
ctx.#additionalStyles = css.slice();
221+
return;
222+
}
223+
224+
for (const href of css) {
225+
if (!ctx.#additionalStyles.includes(href)) {
226+
ctx.#additionalStyles.push(href);
227+
}
228+
}
229+
};
209230
}
210231

211232
constructor(
@@ -305,6 +326,7 @@ export class Context<State> {
305326
props.Component = () => child;
306327

307328
const def = defs[i];
329+
setAdditionalStyles(this, def.css);
308330

309331
const result = await renderRouteComponent(this, def, () => child);
310332
if (result instanceof Response) {
@@ -320,16 +342,20 @@ export class Context<State> {
320342

321343
let hasApp = true;
322344

323-
if (isAsyncAnyComponent(appDef)) {
345+
if (appDef !== null) {
346+
setAdditionalStyles(this, appDef.css);
347+
}
348+
349+
if (appDef !== null && isAsyncAnyComponent(appDef.component)) {
324350
props.Component = () => appChild;
325-
const result = await renderAsyncAnyComponent(appDef, props);
351+
const result = await renderAsyncAnyComponent(appDef.component, props);
326352
if (result instanceof Response) {
327353
return result;
328354
}
329355

330356
appVNode = result;
331357
} else if (appDef !== null) {
332-
appVNode = h(appDef, {
358+
appVNode = h(appDef.component, {
333359
Component: () => appChild,
334360
config: this.config,
335361
data: null,

packages/fresh/src/dev/dev_build_cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const WINDOWS_SEPARATOR = pathWin32.SEPARATOR;
2020
/** Normalize a path to use forward slashes so that generated files
2121
* are portable across operating systems (e.g. build on Windows,
2222
* deploy on Linux). */
23-
function toPosix(p: string): string {
23+
export function toPosix(p: string): string {
2424
return p.replaceAll(WINDOWS_SEPARATOR, "/");
2525
}
2626

packages/fresh/src/dev/fs_crawl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type FsAdapter, fsAdapter } from "../fs.ts";
22
import type { WalkEntry } from "@std/fs/walk";
3-
import type { FsRouteFileNoMod } from "./dev_build_cache.ts";
3+
import { type FsRouteFileNoMod, toPosix } from "./dev_build_cache.ts";
44
import * as path from "@std/path";
55
import { pathToPattern } from "../router.ts";
66
import { CommandType } from "../commands.ts";
@@ -86,7 +86,7 @@ export async function crawlRouteDir<State>(
8686

8787
files.push({
8888
id,
89-
filePath: entry.path,
89+
filePath: toPosix(entry.path),
9090
type,
9191
pattern,
9292
routePattern,

packages/fresh/src/dev/fs_crawl_test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from "@std/expect/expect";
22
import { createFakeFs } from "../test_utils.ts";
3-
import { walkDir } from "./fs_crawl.ts";
3+
import { crawlRouteDir, walkDir } from "./fs_crawl.ts";
44

55
Deno.test("walkDir - ", async () => {
66
const fs = createFakeFs({
@@ -43,3 +43,25 @@ Deno.test("walkDir - respects skip patterns", async () => {
4343
"routes/api/users.ts",
4444
]);
4545
});
46+
47+
Deno.test({
48+
name: "crawlRouteDir.filePath - normalized Windows paths",
49+
ignore: Deno.build.os !== "windows",
50+
fn: async () => {
51+
const fs = createFakeFs({
52+
"foo\\bar\\baz.txt": "foo",
53+
"D:\\foo\\bar.tsx": "foo",
54+
});
55+
56+
const rawFiles = await crawlRouteDir(fs, "foo", [], () => {});
57+
58+
expect(rawFiles).toEqual(expect.arrayContaining([
59+
expect.objectContaining({
60+
filePath: "foo/bar/baz.txt",
61+
}),
62+
expect.objectContaining({
63+
filePath: "D:/foo/bar.tsx",
64+
}),
65+
]));
66+
},
67+
});

packages/fresh/src/fs_routes.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ export function fsItemsToCommands<State>(
106106
}
107107
if (!mod.default) continue;
108108

109-
commands.push(newLayoutCmd(pattern, mod.default, mod.config, true));
109+
commands.push(
110+
newLayoutCmd(pattern, mod.default, mod.config, true, mod.css),
111+
);
110112
continue;
111113
}
112114
case CommandType.Error: {
@@ -116,6 +118,7 @@ export function fsItemsToCommands<State>(
116118
{
117119
component: mod.default ?? undefined,
118120
config: mod.config ?? undefined,
121+
css: mod.css,
119122
// deno-lint-ignore no-explicit-any
120123
handler: (handlers as any) ?? undefined,
121124
},
@@ -128,6 +131,7 @@ export function fsItemsToCommands<State>(
128131
commands.push(newNotFoundCmd({
129132
config: mod.config,
130133
component: mod.default,
134+
css: mod.css,
131135
// deno-lint-ignore no-explicit-any
132136
handler: handlers as any ?? undefined,
133137
}));
@@ -137,7 +141,7 @@ export function fsItemsToCommands<State>(
137141
const { mod } = validateFsMod<State>(filePath, rawMod, type);
138142
if (mod.default === undefined) continue;
139143

140-
commands.push(newAppCmd(mod.default));
144+
commands.push(newAppCmd(mod.default, mod.css));
141145
continue;
142146
}
143147
case CommandType.Route: {

packages/fresh/src/internals_dev.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {
77
type IslandModChunk,
88
type PendingStaticFile,
99
prepareStaticFile,
10+
toPosix,
1011
writeCompiledEntry,
1112
} from "./dev/dev_build_cache.ts";
1213
export { specToName } from "./dev/builder.ts";

packages/fresh/src/segments.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AnyComponent } from "preact";
22
import type { MaybeLazyMiddleware, Middleware } from "./middlewares/mod.ts";
33
import { type Method, patternToSegments } from "./router.ts";
44
import type { LayoutConfig, Route } from "./types.ts";
5-
import { type Context, getInternals } from "./context.ts";
5+
import { type Context, getInternals, setAdditionalStyles } from "./context.ts";
66
import { recordSpanError, tracer } from "./otel.ts";
77
import { type HandlerFn, isHandlerByMethod } from "./handlers.ts";
88
import {
@@ -22,10 +22,14 @@ export interface Segment<State> {
2222
layout: {
2323
component: RouteComponent<State>;
2424
config: LayoutConfig | null;
25+
css: string[] | null;
2526
} | null;
2627
errorRoute: Route<State> | null;
2728
notFound: Middleware<State> | null;
28-
app: RouteComponent<State> | null;
29+
app: {
30+
component: RouteComponent<State>;
31+
css: string[] | null;
32+
} | null;
2933
children: Map<string, Segment<State>>;
3034
parent: Segment<State> | null;
3135
}
@@ -105,7 +109,11 @@ export function segmentToMiddlewares<State>(
105109
internals.app = null;
106110
}
107111

108-
const def = { props: null, component: layout.component };
112+
const def = {
113+
props: null,
114+
component: layout.component,
115+
css: layout.css,
116+
};
109117
if (layout.config?.skipInheritedLayouts) {
110118
internals.layouts = [def];
111119
} else {
@@ -145,6 +153,8 @@ export async function renderRoute<State>(
145153
route: Route<State>,
146154
status = 200,
147155
): Promise<Response> {
156+
setAdditionalStyles(ctx, route.css);
157+
148158
const internals = getInternals(ctx);
149159
if (route.config?.skipAppWrapper) {
150160
internals.app = null;

packages/fresh/src/test_utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DEFAULT_CONN_INFO } from "./app.ts";
77
import type { Command } from "./commands.ts";
88
import { fsItemsToCommands, type FsRouteFile } from "./fs_routes.ts";
99
import * as path from "@std/path";
10+
import { toPosix } from "./dev/dev_build_cache.ts";
1011

1112
const STUB = {} as unknown as Deno.ServeHandlerInfo;
1213

@@ -123,7 +124,10 @@ export function createFakeFs(files: Record<string, unknown>): FsAdapter {
123124
},
124125
// deno-lint-ignore require-await
125126
async isDirectory(dir) {
126-
return Object.keys(files).some((file) => file.startsWith(dir + "/"));
127+
return Object.keys(files).some((file) =>
128+
// normalize path to posix before comparing
129+
toPosix(file).startsWith(dir + "/")
130+
);
127131
},
128132
async mkdirp(_dir: string) {
129133
},

0 commit comments

Comments
 (0)