fix: CSS modules not working in _app/_layout/_error and across routes in non-island components#3781
fix: CSS modules not working in _app/_layout/_error and across routes in non-island components#3781Hajime-san wants to merge 22 commits intodenoland:mainfrom
Conversation
fa5fa74 to
f1934f0
Compare
bartlomieju
left a comment
There was a problem hiding this comment.
Nice fix — the approach of threading css through the existing command → segment → context pipeline is clean and consistent with the rest of the architecture. A few things caught my eye below (inline), but overall this looks solid.
The setAdditionalStyles refactor from a simple setter to a dedup-accumulator is exactly right, getSsrModule() properly handles both real files and virtual module IDs, and the BFS graph walk in collectRouteCss is thorough.
Let me know if you'd like a hand getting this over the finish line — happy to help with any of the items below or with testing.
| `["__FRESH_CSS_PLACEHOLDER__"]`, | ||
| info.css | ||
| ? JSON.stringify(info.css.map((css) => `/${css}`)) | ||
| : "null", |
There was a problem hiding this comment.
This uses content.replace() which only replaces the first occurrence of the placeholder. If two utility-file CSS placeholders (e.g. from both _app and _layout) get hoisted into the same shared chunk like server-entry, only the first one would be replaced.
Should this be replaceAll (or a loop) to be safe?
Alternatively, if each route virtual module gets its own unique placeholder, this is fine — but it might be worth a comment explaining why a single replace is sufficient.
There was a problem hiding this comment.
Nice catch. I notice that it causes requesting to http://localhost:8000/__FRESH_CSS_PLACEHOLDER__ in build mode. It should be replaceAll. Fixed and added a test.
|
|
||
| route.css = server === undefined | ||
| ? route.css | ||
| : await collectRouteCss(server, route.filePath); |
There was a problem hiding this comment.
This re-walks the full SSR import graph and overwrites route.css on every load call (including HMR invalidations). That seems intentional for picking up changes during dev, but for deep dependency trees the fetchModule calls inside collectRouteCss could add up.
Might be worth a short comment here noting this is intentional for HMR correctness, so a future reader doesn't try to "optimize" it with caching.
There was a problem hiding this comment.
Sorry for confusing, it seems to be my AI slop code. This code is unnecessary for HMR.
I'm not sure if the HMR is working with the changes in this PR. Since it definitely works after a page reload, I'm thinking of putting it out of scope for now.
| @@ -298,16 +321,18 @@ export class Context<State> { | |||
|
|
|||
There was a problem hiding this comment.
Nit: appDef?.css evaluates to undefined when appDef is null, which the new setAdditionalStyles handles via the css == null guard. Works correctly, but it's a subtle null-propagation chain. An explicit guard might be clearer:
if (appDef !== null) {
setAdditionalStyles(this, appDef.css);
}Especially since the appDef !== null checks already exist a few lines below.
|
|
||
| if (ctx.#additionalStyles === null) { | ||
| ctx.#additionalStyles = css.slice(); | ||
| return; |
There was a problem hiding this comment.
Minor: includes() is O(n) per insertion. For the small CSS lists in practice this is fine, but a Set would be more idiomatic if you ever want to tighten this up. Low priority.
There was a problem hiding this comment.
Yep, I added a FIXME comment below. I think it could be use Set instead of css: Array<string>.It seems that at least all the tests pass in the following branch.
Hajime-san/fresh@fix-ssr-css-modules...Hajime-san:fresh:perf-css
I'm planning it as a follow up PR.
| }); | ||
| }, | ||
| ); | ||
| }, |
There was a problem hiding this comment.
The tests cover _layout.tsx which is great. The PR title also mentions _app, _error, and _404 — are those paths already exercised by existing tests, or would it be worth adding a case or two here to lock in coverage for those as well?
|
@bartlomieju UPDATE:
It seems an another problem of |
This reverts commit f1934f0.
b069071 to
0395403
Compare
0395403 to
ba75ad3
Compare
1d16070 to
9c1e62f
Compare
| `["__FRESH_CSS_PLACEHOLDER__"]`, | ||
| info.css | ||
| ? JSON.stringify(info.css.map((css) => `/${css}`)) | ||
| : "null", |
There was a problem hiding this comment.
Nice catch. I notice that it causes requesting to http://localhost:8000/__FRESH_CSS_PLACEHOLDER__ in build mode. It should be replaceAll. Fixed and added a test.
| files.push({ | ||
| id, | ||
| filePath: entry.path, | ||
| filePath: toPosix(entry.path), |
There was a problem hiding this comment.
Suddenly, several tests failed on Windows. It's likely that a potential bug was occured by this PR.
vite dev - css modules => ./packages/fresh/src/test_utils.ts:198:8
vite dev - css modules in _app/_layout/_error non-island component are injected => ./packages/fresh/src/test_utils.ts:198:8
vite dev - route css import => ./packages/fresh/src/test_utils.ts:198:8debug console on Windows
------- post-test output -------
👺 > fresh:route-css > name _index
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="green">
<h1 class="_root_1mdiz_1">
green text
</h1>
</div>
<div class="red">
<h1 class="_root_xlv1v_1">
red text
</h1>
</div>
<h1>
ok
</h1>
</body>
</html>
[
"Port 5173 is in use, trying another one...",
"",
" VITE v7.3.1 ready in 1276 ms",
"",
" ➜ Local: http://127.0.0.1:5174/"
]
[
"Port 5173 is in use, trying another one...",
"",
" VITE v7.3.1 ready in 1276 ms",
"",
" ➜ Local: http://127.0.0.1:5174/",
"\x1b[2m \x1b[32m➜\x1b[39m \x1b[1mNetwork\x1b[22m\x1b[2m: use \x1b[22m\x1b[1m--host\x1b[22m\x1b[2m to expose\x1b[22m",
"👺 > fresh:route-css > route {",
' id: "/_app",',
' filePath: "D:\\\\a\\\\fresh\\\\fresh\\\\packages\\\\plugin-vite\\\\tests\\\\fixtures\\\\non_island_css_modules\\\\routes\\\\_app.tsx",',
' type: "app",',
' pattern: "*",',
' routePattern: "*",',
" lazy: false,",
" css: [",
' "/@fs/D:/a/fresh/fresh/packages/plugin-vite/demo/components/CssModulesNonIsland.module.css"',
" ],",
" overrideConfig: undefined",
"}",
"👺 > fresh:route-css > name __app",
"👺 > fresh:route-css > route {",
' id: "/_layout",',
' filePath: "D:\\\\a\\\\fresh\\\\fresh\\\\packages\\\\plugin-vite\\\\tests\\\\fixtures\\\\non_island_css_modules\\\\routes\\\\_layout.tsx",',
' type: "layout",',
' pattern: "/",',
' routePattern: "/",',
" lazy: false,",
" css: [",
' "/@fs/D:/a/fresh/fresh/packages/plugin-vite/demo/components/CssModulesNonIsland3.module.css"',
" ],",
" overrideConfig: undefined",
"}",
"👺 > fresh:route-css > name __layout",
"👺 > fresh:route-css > route {",
' id: "/_error",',
' filePath: "D:\\\\a\\\\fresh\\\\fresh\\\\packages\\\\plugin-vite\\\\tests\\\\fixtures\\\\non_island_css_modules\\\\routes\\\\_error.tsx",',
' type: "error",',
' pattern: "/",',
' routePattern: "/",',
" lazy: false,",
" css: [",
' "/@fs/D:/a/fresh/fresh/packages/plugin-vite/demo/components/CssModulesNonIsland2.module.css"',
" ],",
" overrideConfig: undefined",
"}",
"👺 > fresh:route-css > name __error",
"👺 > fresh:route-css > route {",
' id: "/index",',
' filePath: "D:\\\\a\\\\fresh\\\\fresh\\\\packages\\\\plugin-vite\\\\tests\\\\fixtures\\\\non_island_css_modules\\\\routes\\\\index.tsx",',
' type: "route",',
' pattern: "/",',
' routePattern: "/",',
" lazy: true,",
" css: [],",
' overrideConfig: { methods: "ALL" }',
"}",
"👺 > fresh:route-css > name _index",
"👺 > renderRoute > setAdditionalStyles []",
"👺 > render Compose VNode > setAdditionalStyles []",
"👺 > render compose App > setAdditionalStyles []"
]
./packages/plugin-vite/tests/dev_server_test.ts => vite dev - css modules in _app/_layout/_error non-island component are injected ...----- post-test output end -----
./packages/plugin-vite/tests/dev_server_test.ts => vite dev - css modules in _app/_layout/_error non-island component are injected ... FAILED (5s)
./packages/plugin-vite/tests/build_test.ts => vite build - tailwind _app ... ok (6s)
./packages/plugin-vite/tests/build_test.ts => vite build - partial island ... ok (1s)
------- post-test output -------|
|
||
| if (ctx.#additionalStyles === null) { | ||
| ctx.#additionalStyles = css.slice(); | ||
| return; |
There was a problem hiding this comment.
Yep, I added a FIXME comment below. I think it could be use Set instead of css: Array<string>.It seems that at least all the tests pass in the following branch.
Hajime-san/fresh@fix-ssr-css-modules...Hajime-san:fresh:perf-css
I'm planning it as a follow up PR.
This comment was marked as outdated.
This comment was marked as outdated.
ef270d9 to
814e3b5
Compare
| routeFileToName.set(route.filePath, name); | ||
| routeFileToName.set(toPosix(route.filePath), name); |
There was a problem hiding this comment.
seems to be same as https://github.com/denoland/fresh/pull/3781/changes#r3143377420
| const normalized = path.normalize(item.file); | ||
| const normalized = toPosix(path.normalize(item.file)); |
Originally reported:
Repro:
https://github.com/honmanyau/fresh-2-layout-css-module-issue/blob/24bf9c597a057c60acdb2c2934a65f22b6276ecd/routes/page-b/_layout.tsx
The PR seems to resolve the island components problem, but not non-island.
This PR introduces CSS Modules handling for non-island components rendered through utility files such as
_app,_layout,_error, and_404. The core change is that utility-filemod.cssis now preserved through Fresh’s command and render pipeline, so CSS referenced from app/layout/route/error rendering can be accumulated and emitted in the final response instead of being dropped.On the Vite side, development now also collects CSS exposed throughfresh-route-css::*virtual modules, which is the path used by server-rendered non-island components in utility files. For production builds, CSS placeholder replacement is no longer limited to route chunks, because this CSS can be hoisted into shared chunks such asserver-entry.I added new tests and confirmed to work fine about our private project via
linksin deno config locally.