cloudflare / cloudflare/vinext

App Router: next/dynamic global CSS loads after CSS modules, breaking cascade order

Open
#1,931 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 11h
Merged PRs (30d)
122

Description

## Problem

Global (non-module) stylesheets imported by `next/dynamic` client components load **after** route CSS and CSS modules, so global rules incorrectly win the cascade. The Next.js compat fixture `test/e2e/app-dir/next-dynamic-css/next-dynamic-css.test.ts` (v16.2.6) fails on vinext: a global `.global-class` background overrides the CSS-module background on elements that use both.

Root cause: App Router route/global CSS is emitted by the RSC build and linked in the document head, while dynamic client imports are built in the client graph and their CSS is inserted at runtime by Vite's preload helper — appended at the end of ``, after module CSS. Same stylesheet can also be emitted twice (RSC copy + client copy) with different hashed filenames, so nothing dedupes.

## History

PR #1908 originally carried a fix and it was removed during review — twice:

1. **App-wide dedupe set** (build-time): recorded every global CSS import seen in the RSC build and emptied matching client imports. Rejected — no route scoping; a stylesheet imported by route A's RSC graph would be dropped from a dynamic chunk that is route B's only source of that CSS.
2. **Replacement mechanism**: deterministic `global-css-` chunks across RSC/SSR/client builds (so the same source file emits the same CSS filename everywhere), plus a `Node.prototype.appendChild`/`insertBefore` interceptor in the browser entry that deduped/reordered stylesheet links at insertion time, plus post-hoc rewriting of `__vite_rsc_assets_manifest.js` (sort global CSS first, drop CSS-only chunks' phantom JS entries that 404 on preload). Worked (the ported fixture passed, including a route-A/route-B over-dedupe regression), but was withdrawn as architecturally unacceptable: a platform monkey-patch intercepting every DOM insertion in user apps, string-surgery on another plugin's build output, `setTimeout`-faked `load` events, and a `global-css-` magic string hand-matched in four files.

The removed implementation is recoverable from the PR history (`c7c51600` and earlier) for reference.

## Proposed design

Own the stylesheet insertion seam instead of compensating downstream:

1. **Keep deterministic `global-css-*` chunk naming** across RSC/SSR/client builds (the sound part of the removed fix) so a stylesheet's identity is stable across environments.
2. **React stylesheet precedence**: emit dynamic-chunk global CSS via React hoistable stylesheets (``) from the existing `DynamicPreloadChunks` shim (which already has the module→CSS manifest), with global CSS in a lower precedence tier than CSS modules. React then owns ordering and dedupes by href, server- and client-side.
3. **Vite runtime insert becomes a no-op**: Vite's preload helper skips inserting a stylesheet link whose href already exists in the document, so the SSR-emitted link suppresses the late duplicate without any interception.
4. **Fix the manifest at emission, not post-hoc**: drop CSS-only chunks' placeholder JS entries in `generateBundle` (they are never written to disk; preloading them 404s) instead of rewriting the serialized manifest after `buildApp`.

### Risks to validate first

- Exact-href matching in Vite's preload dedupe (basePath / absolute URL normalization).
- Inline-CSS mode replaces links with `` — link-based dedupe misses; needs the data-href equivalent.
- All vinext-emitted route CSS must participate in the precedence system, otherwise React's precedence tiers and raw `<link>` tags interleave unpredictably. This is the biggest interop question.

## Acceptance

- Port of `next-dynamic-css.test.ts` passes (global vs module cascade order after prerender probing).
- Route-scoping regression: route A imports a stylesheet through RSC only; route B's sole source is a `next/dynamic` client import; `/b` still receives the CSS.
- No 404s for phantom `global-css-*` JS preloads.
- No `Node.prototype` patching; no post-hoc manifest rewriting.

Contributor guide

Open the contributing guide

Research direction

Start with test/e2e/app-dir/next-dynamic-css/next-dynamic-css.test.ts and inspect the existing DynamicPreloadChunks shim, Vite preload behavior, and generateBundle handling. Use the removed implementation at PR history c7c51600 for context, then validate cascade order, route scoping, inline-CSS behavior, and the absence of phantom JS preload 404s without Node.prototype patching or post-hoc manifest rewriting.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, react, typescript, vite
Domain
build-system, frontend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.