Skip to content

fix(pages): link CSS that island and Markdown pages reach through components - #481

Open
Brooooooklyn wants to merge 1 commit into
mainfrom
fix/island-transitive-css
Open

Brooooooklyn wants to merge 1 commit into
mainfrom
fix/island-transitive-css

Conversation

@Brooooooklyn

Copy link
Copy Markdown
Member

Fixes #452

Problem

pages/layout.tsx ──renders──► src/Shell.tsx ──imports──► src/global.css

vp dev applied global.css. A production build did not:

  • The islands client entry was only built when a page or layout file had a direct CSS import, <style> or island import (a text scan). With CSS only reachable through a component, no client build ran, so pages got no <link rel="stylesheet">.
  • Even when the entry was built, it only imported the direct CSS imports of pages and layouts.

Fix

server build                                  client build
────────────                                  ────────────
buildEnd: walk static importedIds             islands client entry
  layouts first, then pages     ──recorded──►   import "<css id>"; ...
  (real resolution: aliases,                    ▼
   packages, Vue/Svelte styles)                 islands-client-*.css → <link> on island/md pages
  • When the entry is built: for every app with island or Markdown pages (hasIslandPages). It replaces the needsIslandClientBundle text scan, which only the adapters used.
  • What CSS goes in:
    • The server build has already resolved every import of the island pages and layouts, so the server build's module graph lists the stylesheets they reach.
    • Static imports only, in import order, like the dev CSS collector. ?inline / ?url / ?raw are skipped.
    • The walk also enters island components, as dev does, so a layout that renders one without hydrating keeps its CSS.
    • Without a server build (client-only builds), the old direct-import scan is still used.
  • Framework details:
    • CSS modules (*.module.css, Vue <style module>) are kept. Vite marks them side-effect free, so the bare import would drop them.
    • Vue scoped styles: cssScopeTo is cleared for the recorded ids, because the component itself is not in the client bundle.
    • Svelte: the entry imports vite-plugin-svelte's own ?svelte&type=style id, compiled through the existing island-style loader, so the same rule is not emitted twice in mixed apps.
  • No extra JS:
    • Pages that need no JS still get only the <link>, no <script>.
    • With no islands and no page needing JS, the entry holds only the CSS imports, so no hydration runtime is bundled.
    • React and Solid now add the entry to the client build only, as Vue and Svelte already did, so the worker no longer bundles it.

Behaviour changes worth knowing

  • Static-output island apps without any direct CSS import are now prerendered. Before, the missing client build meant no HTML.
  • A Vue island page or layout with <style scoped> used to fail the build (PARSE_ERROR on ...&scoped=true). It now builds and links the CSS.
  • playground/markdown-blog: same CSS rules, but @void/md/theme.css (imported in the script) now comes before the layout's <style>, which is Vue's normal order.

Known limit (unchanged, not in this PR)

In apps with both island and regular pages, collectRouteCss (routing-assets.ts) puts a chunk's own CSS before the CSS of chunks it imports, so the islands stylesheet can come before shared layout CSS. That order already applied to direct page CSS on main. Changing it would change the link order on every page.

Tests

New production-build tests for each adapter (packages/{react,vue,svelte,solid}/test/integration/island-css-build.test.ts, shared test/island-css-build.ts). Each one builds a static app where a layout renders a component that imports CSS, with an island page and a Markdown page, and checks:

  • the page links a stylesheet, and each rule (plain CSS, CSS module, Vue/Svelte <style>, Vue scoped with matching data-v-*, Svelte matching svelte-* class) appears exactly once;
  • pages that need no JS have no <script>;
  • React: an app with no CSS has no link, script or hydrateRoot, and dist/ssr has no islands client.

Plus unit tests for collectIslandStyles and islandStyleRoots (order, no repeats, \0 ids, ?inline/?url).

Check Result
New build tests on main sources 6 fail (no client build)
New build tests with the fix pass
Undo each part (walk, entry decision, each adapter hook, CSS-module keep, Vue scope, Svelte id) a test fails for each
Adapter + md package tests, browser suites with local Chrome pass
vp fmt --check, vp lint, typecheck void/react/vue/svelte/solid clean

🤖 Generated with Claude Code

…ponents

In production, island and Markdown pages only got the CSS their page and
layout files import directly. A stylesheet imported by a component that a
layout renders was missing, and when no file imported CSS directly the
islands client entry was not built at all, so no page got a stylesheet.

- Build the islands client entry for every app with island or Markdown
  pages, replacing the text scan in needsIslandClientBundle.
- Record the stylesheets that island pages and their layouts reach through
  static imports from the server build's module graph, and import them in
  the islands client entry. This covers aliases, packages, CSS modules and
  Vue/Svelte style blocks, and keeps the CSS order of dev.
- Keep CSS modules that the entry imports (Vite marks them side-effect
  free), keep Vue scoped styles whose component is not in the client
  bundle, and load Svelte styles through vite-plugin-svelte's own id so a
  rule is not emitted twice.
- Without islands, the entry only holds the CSS imports, so no hydration
  runtime is bundled. The React and Solid entry is now added only to the
  client build.

Fixes #452

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T17:35:32.108652Z 331e23b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://fix-island-transitive-css-void-cloud.voidzero-docs.workers.dev (commit 331e23b)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://d63f4864-void-cloud.voidzero-docs.workers.dev 331e23b 2026-10-18T17:32:51.915Z Visit the dashboard ↗

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 331e23bff7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +150 to +152
const imported = getModuleInfo(id)?.importedIds ?? [];
for (let index = imported.length - 1; index >= 0; index--) {
pending.push(imported[index]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Collect styles using the client-side dependency graph

For Node/Bun/Deno builds, a layout can import a package whose conditional server entry renders the component while its browser entry imports the stylesheet. The SSR build externalizes ordinary installed packages, so getModuleInfo() returns no traversable imports here; consequently the stylesheet is absent from styles, and because the islands entry imports only that recorded list, production island/Markdown pages lose CSS that the dev client graph applies. The collection needs to account for client-resolved package dependencies rather than relying exclusively on the server graph.

AGENTS.md reference: packages/void/AGENTS.md:L40-L40

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this in this PR. Reasons:

  • Dev doesn't do it either. The dev CSS collector (packages/void/src/pages/dev-css-assets.ts, visit) stops at pre-bundled packages (if (isOptimizedDepUrl(node.url)) return;). So CSS that a package's own JS imports is not linked at first paint in dev either. This PR matches dev; it does not fall behind it.
  • It never worked before. On main only the direct CSS imports of pages and layouts were bundled, so a stylesheet imported inside a package's JS was never linked on island pages, on any target.
  • What does work: a page, layout or app component that imports a package stylesheet directly (import 'ui-lib/style.css'). CSS imports go through Vite's CSS pipeline in the server build and are not externalized, so they are in the recorded list on the Node target too. On Cloudflare, packages are bundled, so their CSS imports are followed as well.

Following client-resolved package graphs for island pages would be a new feature for both dev and build: it means loading package entries in the client build only to inspect them. That's a separate change from #452, which is about the app's own components.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Island/Markdown pages lose CSS in production when it is imported through a component

1 participant