Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions packages/fresh/src/runtime/server/preact_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export class RenderState {
islandProps: any[] = [];
islands = new Set<Island>();
islandAssets = new Set<string>();
/** CSS assets already injected in `<head>` via `RemainingHead`. */
injectedCss = new Set<string>();
// deno-lint-ignore no-explicit-any
encounteredPartials = new Set<any>();
owners = new Map<VNode, VNode>();
Expand Down Expand Up @@ -116,6 +118,7 @@ export class RenderState {
this.islands.clear();
this.encounteredPartials.clear();
this.owners.clear();
this.injectedCss.clear();
this.slots = [];
this.islandProps = [];
this.ownerStack = [];
Expand Down Expand Up @@ -511,13 +514,19 @@ function RemainingHead() {
if (island.css.length > 0) {
for (let i = 0; i < island.css.length; i++) {
const css = island.css[i];
items.push(h("link", { rel: "stylesheet", href: css }));
if (!RENDER_STATE!.injectedCss.has(css)) {
RENDER_STATE!.injectedCss.add(css);
items.push(h("link", { rel: "stylesheet", href: css }));
}
}
}
});

RENDER_STATE.islandAssets.forEach((css) => {
items.push(h("link", { rel: "stylesheet", href: css }));
if (!RENDER_STATE!.injectedCss.has(css)) {
RENDER_STATE!.injectedCss.add(css);
items.push(h("link", { rel: "stylesheet", href: css }));
}
});

if (items.length > 0) {
Expand Down Expand Up @@ -629,10 +638,32 @@ export function FreshScripts() {

// Remaining slots must be rendered before creating the Fresh runtime
// script, so that we have the full list of islands rendered

// Collect CSS for islands discovered after <head> was rendered
// (e.g. islands used in _app.tsx, _layout.tsx, or _error.tsx).
// deno-lint-ignore no-explicit-any
const lateCssLinks: VNode<any>[] = [];
RENDER_STATE.islands.forEach((island) => {
for (let i = 0; i < island.css.length; i++) {
const css = island.css[i];
if (!RENDER_STATE!.injectedCss.has(css)) {
RENDER_STATE!.injectedCss.add(css);
lateCssLinks.push(h("link", { rel: "stylesheet", href: css }));
}
}
});
RENDER_STATE.islandAssets.forEach((css) => {
if (!RENDER_STATE!.injectedCss.has(css)) {
RENDER_STATE!.injectedCss.add(css);
lateCssLinks.push(h("link", { rel: "stylesheet", href: css }));
}
});

return (
h(
Fragment,
null,
...lateCssLinks,
slots.map((slot) => {
if (slot === null) return null;
return (
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-vite/demo/islands/AppNav.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.nav {
background-color: rgb(30, 30, 30);
}
10 changes: 10 additions & 0 deletions packages/plugin-vite/demo/islands/AppNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @ts-ignore upstream issue https://github.com/denoland/deno/issues/30560
import styles from "./AppNav.module.css";

export function AppNav() {
return (
<nav class={`app-nav ${styles.nav}`}>
<span>Fresh</span>
</nav>
);
}
2 changes: 2 additions & 0 deletions packages/plugin-vite/demo/routes/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PageProps } from "fresh";
import { AppNav } from "../islands/AppNav.tsx";

export default function App({ Component }: PageProps) {
return (
Expand All @@ -9,6 +10,7 @@ export default function App({ Component }: PageProps) {
<meta name="custom" content="foo" />
</head>
<body>
<AppNav />
<Component />
</body>
</html>
Expand Down
10 changes: 10 additions & 0 deletions packages/plugin-vite/demo/routes/tests/css_modules_page2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CssModules } from "../../islands/CssModules.tsx";

export default function Page2() {
return (
<div>
<CssModules />
<p class="page2-marker">page2</p>
</div>
);
}
14 changes: 13 additions & 1 deletion packages/plugin-vite/src/plugins/dev_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,23 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] {
url.pathname !== "/__inspect" &&
res.headers.get("Content-Type")?.includes("text/html")
) {
const clientEnv = server.environments.client;
const collected = await collectCss(
"fresh:client-entry",
server.environments.client,
clientEnv,
);

// Also collect CSS from island modules. In dev mode,
// island css is [] so RemainingHead can't inject them.
// Walk all loaded modules that look like island entries
// to pick up their CSS module imports.
for (const mod of clientEnv.moduleGraph.idToModuleMap.values()) {
if (mod.id?.includes("fresh-island::")) {
const islandCss = await collectCss(mod.id, clientEnv);
collected.push(...islandCss);
}
}

let html = await res.text();

const styles = collected.join("\n");
Expand Down
52 changes: 52 additions & 0 deletions packages/plugin-vite/tests/build_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,58 @@ integrationTest("vite build - css modules", async () => {
);
});

// Issue: https://github.com/denoland/fresh/issues/3633
integrationTest(
"vite build - css modules in _app.tsx island are injected",
async () => {
await launchProd(
{ cwd: viteResult.tmp },
async (address) => {
await withBrowser(async (page) => {
await page.goto(`${address}/tests/css_modules`, {
waitUntil: "networkidle2",
});

// The AppNav island is in _app.tsx and uses a CSS module.
// Its styles should be injected even though the island
// is discovered after <head> renders.
const bgColor = await page
.locator(".app-nav")
.evaluate((el) =>
window.getComputedStyle(el as Element).backgroundColor
);
expect(bgColor).toEqual("rgb(30, 30, 30)");
});
},
);
},
);

// Issue: https://github.com/denoland/fresh/issues/3633
integrationTest(
"vite build - css modules work on second page with shared island",
async () => {
await launchProd(
{ cwd: viteResult.tmp },
async (address) => {
await withBrowser(async (page) => {
// Access the second page that shares the CssModules island
await page.goto(`${address}/tests/css_modules_page2`, {
waitUntil: "networkidle2",
});

// The shared CssModules island's CSS should work here too
const color = await page
.locator(".red > h1")
// deno-lint-ignore no-explicit-any
.evaluate((el) => window.getComputedStyle(el as any).color);
expect(color).toEqual("rgb(255, 0, 0)");
});
},
);
},
);

integrationTest("vite build - route css import", async () => {
await launchProd(
{ cwd: viteResult.tmp },
Expand Down
47 changes: 47 additions & 0 deletions packages/plugin-vite/tests/dev_server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,53 @@ integrationTest("vite dev - css modules", async () => {
});
});

// Issue: https://github.com/denoland/fresh/issues/3633
integrationTest(
"vite dev - css modules in _app.tsx island are injected",
async () => {
await withBrowser(async (page) => {
await page.goto(`${demoServer.address()}/tests/css_modules`, {
waitUntil: "networkidle2",
});

await waitFor(async () => {
const bgColor = await page
.locator(".app-nav")
// deno-lint-ignore no-explicit-any
.evaluate((el) => window.getComputedStyle(el as any).backgroundColor);
expect(bgColor).toEqual("rgb(30, 30, 30)");
return true;
});
});
},
);

// Issue: https://github.com/denoland/fresh/issues/3633
integrationTest(
"vite dev - css modules work on second page with shared island",
async () => {
await withBrowser(async (page) => {
// First visit a different page, then visit page2 that shares
// the CssModules island. In dev mode, the CSS must still work.
await page.goto(`${demoServer.address()}/`, {
waitUntil: "networkidle2",
});
await page.goto(`${demoServer.address()}/tests/css_modules_page2`, {
waitUntil: "networkidle2",
});

await waitFor(async () => {
const color = await page
.locator(".red > h1")
// deno-lint-ignore no-explicit-any
.evaluate((el) => window.getComputedStyle(el as any).color);
expect(color).toEqual("rgb(255, 0, 0)");
return true;
});
});
},
);

integrationTest("vite dev - route css import", async () => {
await withBrowser(async (page) => {
await page.goto(`${demoServer.address()}/tests/css`, {
Expand Down