Skip to content

Recover transitive CSS-only split chunk styles#184

Merged
justin808 merged 1 commit into
mainfrom
codex/180-transitive-css-recovery
Jul 7, 2026
Merged

Recover transitive CSS-only split chunk styles#184
justin808 merged 1 commit into
mainfrom
codex/180-transitive-css-recovery

Conversation

@justin808

@justin808 justin808 commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Recovers stylesheet hints when a client reference imports a child module whose CSS is moved into a CSS-only SplitChunks chunk.
  • Applies the bounded child-module recovery to both RSCWebpackPlugin and RSCRspackPlugin while keeping the existing group/chunk guard that prevents shared dependency CSS broadcast.
  • Adds focused webpack and rspack fixtures/tests for Button.js -> Panel.js -> Panel.css under a styles CSS-only cache group, plus a webpack server-build regression that proves shared dependency CSS-only chunks still stay excluded.

Fixes #180.

Validation

  • Red before fix: yarn jest tests/webpack-plugin/plugin-integration.test.ts --runInBand -t "transitive CSS" failed because Button.js had css: [] while styles.chunk.css was emitted.
  • Red before fix: yarn jest tests/rspack-plugin/plugin.test.ts --runInBand -t "child-module CSS-only" failed because Button.js had css: [] while styles.chunk.css was emitted.
  • yarn build passed.
  • Focused after-fix webpack run passed: yarn jest tests/webpack-plugin/plugin-integration.test.ts --runInBand -t "shared dependency|transitive CSS|server build".
  • Focused after-fix rspack run passed: yarn jest tests/rspack-plugin/plugin.test.ts --runInBand -t "child-module CSS-only|child-module CSS when the child is split|CSS-only split chunk styles|scopes manifest CSS|does not attach an importing".
  • .agents/bin/validate passed: yarn build plus yarn test (7 RSC suites / 15 tests; 25 non-RSC suites / 259 tests).
  • git diff --check origin/main...HEAD and git diff --check passed.

Review And Churn Notes

  • Manual self-review completed against origin/main...HEAD.
  • Read-only pre-push review subagent found no blocking findings. It noted the new fixtures must be committed, which they are in this PR, and that the changelog needs this PR number once assigned.
  • Post-push churn: 4 review-fix amends. The amends added this PR number to the changelog, addressed the split-child finding, tightened chunk membership to the current chunk group, hoisted chunk-use counting per compilation, added the server shared-CSS non-broadcast regression, and documented the private-chunk heuristic. Current head is 6d05731.
  • Deeper nested child traversal remains intentionally outside CSS recovery misses transitively-imported CSS under a css/mini-extract merge group (residual of #148 fix) #180's bounded one-hop recovery scope.

QA Evidence

qa-evidence v1
scope: #180 transitive CSS-only SplitChunks recovery
base: origin/main @ 0ba73172b02d6d939819ae2187638a5127802465
head: 6d05731
fixtures_created:
  - tests/webpack-plugin/fixtures/transitive-css-only-chunk/{Button.js,Panel.js,Panel.css,entryOnly.js,index.js}
  - tests/rspack-plugin/fixtures/transitive-css-only-chunk/{Button.js,Panel.js,Panel.css,entryOnly.js,index.js}
regression_coverage_added:
  - webpack server build keeps shared dependency CSS-only split chunks out of client-reference css arrays
red_before_fix:
  - webpack focused transitive CSS test: failed with empty Button.css despite styles.chunk.css
  - rspack focused child-module CSS-only test: failed with empty Button.css despite styles.chunk.css
local_validation:
  - yarn build: pass
  - focused webpack shared/transitive/server CSS tests: pass
  - focused rspack child/direct/scope CSS tests: pass
  - .agents/bin/validate: pass (yarn build; yarn test with 7 RSC suites / 15 tests and 25 non-RSC suites / 259 tests)
  - git diff --check origin/main...HEAD: pass
  - git diff --check: pass
review_gate:
  - read-only pre-push review: no blocking findings
  - hosted review triage: fixed direct child split into its own JS chunk, current-group chunk membership, and per-compilation chunk-use counting; added server shared-CSS non-broadcast regression; deeper nested child traversal remains intentionally out of #180 scope
coordination:
  - live open PR collision sweep before branch: none
  - agent-coord target #180 claim is released/expired; target read degraded for batch state, so coordination remains partially UNKNOWN/degraded

Summary by CodeRabbit

  • Bug Fixes

    • Preserved stylesheet references for client-rendered components when related styles are moved into separate chunks.
    • Improved handling of shared and transitive CSS so page/component manifests no longer pick up unrelated styles.
    • Kept server-side manifest CSS output accurate for split and shared chunk scenarios.
  • Tests

    • Added coverage for transitive CSS, CSS-only split chunks, and shared stylesheet cases to prevent regressions.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Both the rspack and webpack RSC plugins now compute a chunk-group usage count and per-module chunk sets to recover CSS transitively imported by non-client-reference child modules when SplitChunks cache groups move that CSS into CSS-only split chunks. New fixtures and integration tests validate the fix; a changelog entry documents it.

Changes

Transitive CSS recovery fix

Layer / File(s) Summary
Changelog entry
CHANGELOG.md
Adds a Fixed entry and PR link reference documenting the CSS recovery fix.
rspack plugin CSS recovery logic
src/react-server-dom-rspack/plugin.ts
Precomputes chunkGroupUseCount and moduleChunks, replaces the prior CSS traversal with addDirectStyleImports and belongsToReferenceChunkGroup to selectively recover CSS from one-hop non-style child modules.
rspack fixtures and tests
tests/rspack-plugin/fixtures/transitive-css-only-chunk/*, tests/rspack-plugin/plugin.test.ts
Adds a Button/Panel fixture with CSS and two tests validating server-manifest CSS scoping, including with a custom splitChunks config.
webpack plugin CSS recovery logic
src/webpack/RSCWebpackPlugin.ts
Adds the same chunkGroupUseCount/moduleChunks computation and refactors CSS sibling recovery into addDirectStyleImports and belongsToReferenceChunkGroup, updating the dependency-walk gating logic.
webpack fixtures and tests
tests/webpack-plugin/fixtures/transitive-css-only-chunk/*, tests/webpack-plugin/plugin-integration.test.ts
Adds matching fixtures and integration tests verifying preserved CSS association and that shared split stylesheets are not broadcast to unrelated manifest entries.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Compiler as Webpack/Rspack Compilation
  participant Plugin as RSC Plugin (buildManifest)
  participant ChunkGraph as Chunk/Module Graph
  participant Manifest as Client/Server Manifest

  Compiler->>Plugin: emit compilation with chunk groups
  Plugin->>ChunkGraph: compute chunkGroupUseCount per chunk
  Plugin->>ChunkGraph: compute moduleChunks for reference module
  Plugin->>ChunkGraph: traverse outgoing connections (child modules)
  ChunkGraph-->>Plugin: candidate non-style child module
  Plugin->>Plugin: belongsToReferenceChunkGroup(child, moduleChunks, chunkGroupUseCount)
  alt shares chunk or uniquely used
    Plugin->>ChunkGraph: addDirectStyleImports(child)
    ChunkGraph-->>Plugin: recovered CSS from css/mini-extract module
    Plugin->>Manifest: record CSS on reference entry
  else shared elsewhere
    Plugin->>Plugin: skip CSS to avoid contamination
  end
Loading

Poem:
A bunny hopped through chunks anew,
Chasing CSS that split and flew,
Counted groups, one hop allowed,
Kept the styles that wandered proud,
No shared purple leaked askew. 🐇🎨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The plugin updates and new webpack/rspack tests match #180's requirement to recover child-module CSS in CSS-only split chunks.
Out of Scope Changes check ✅ Passed The changelog entry and fixture/test additions stay aligned with the CSS-recovery fix and related coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: recovering CSS for transitive client-reference cases in CSS-only split chunks.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/180-transitive-css-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@justin808 justin808 force-pushed the codex/180-transitive-css-recovery branch from c9105d2 to 032af63 Compare July 7, 2026 09:31
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR recovers RSC stylesheet hints for CSS-only split chunks. The main changes are:

  • Added child-module CSS recovery in the webpack plugin.
  • Added the same recovery path in the rspack plugin.
  • Added focused webpack and rspack fixtures for direct child CSS extraction.
  • Documented the fix in the changelog.

Confidence Score: 4/5

The direct child CSS recovery works for the tested shape, but nested child-module CSS can still be omitted.

  • The new traversal scans direct style imports on the reference and on one qualifying child.
  • Deeper local child modules are not visited, so their extracted CSS-only chunks can be missing from the manifest.
  • The issue is mirrored in the webpack and rspack implementations.

src/webpack/RSCWebpackPlugin.ts and src/react-server-dom-rspack/plugin.ts

Important Files Changed

Filename Overview
src/webpack/RSCWebpackPlugin.ts Adds webpack recovery for direct child-module CSS moved into CSS-only split chunks, but deeper child modules can still be missed.
src/react-server-dom-rspack/plugin.ts Mirrors the webpack recovery in the rspack plugin, with the same one-hop limit for nested child-module CSS.
tests/webpack-plugin/plugin-integration.test.ts Adds webpack coverage for the direct Button -> Panel -> Panel.css CSS-only split chunk case.
tests/rspack-plugin/plugin.test.ts Adds rspack coverage for the direct child-module CSS-only split chunk case.
CHANGELOG.md Records the stylesheet hint recovery in the fixed section.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Client reference] --> B[Direct CSS import]
  A --> C[Local child module]
  C --> D[Child direct CSS import]
  C -. not visited .-> E[Nested child module]
  E --> F[Nested CSS-only split chunk]
  B --> G[RSC manifest CSS]
  D --> G
  F -. missing hint .-> G
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Client reference] --> B[Direct CSS import]
  A --> C[Local child module]
  C --> D[Child direct CSS import]
  C -. not visited .-> E[Nested child module]
  E --> F[Nested CSS-only split chunk]
  B --> G[RSC manifest CSS]
  D --> G
  F -. missing hint .-> G
Loading

Reviews (1): Last reviewed commit: "Recover transitive CSS split chunks" | Re-trigger Greptile

Comment thread src/webpack/RSCWebpackPlugin.ts
Comment thread src/react-server-dom-rspack/plugin.ts

@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: c9105d266a

ℹ️ 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 thread src/webpack/RSCWebpackPlugin.ts Outdated
@justin808

Copy link
Copy Markdown
Member Author

Review triage note:

qa-evidence v1

@justin808 justin808 force-pushed the codex/180-transitive-css-recovery branch from 032af63 to 03b7585 Compare July 7, 2026 09:41
@justin808

Copy link
Copy Markdown
Member Author

Review triage update:

  • The direct-child split finding is fixed in head 03b7585 with tests where Panel.js is split into panel.chunk.js and Panel.css is merged into styles.chunk.css for both webpack and rspack.
  • The deeper nested Button -> Panel -> Icon -> Icon.css case remains intentionally outside CSS recovery misses transitively-imported CSS under a css/mini-extract merge group (residual of #148 fix) #180. This PR implements the issue's accepted one-hop child-module recovery while preserving the shared dependency CSS broadcast guard; recursive traversal would be a broader follow-up decision.

qa-evidence v1

Comment thread src/webpack/RSCWebpackPlugin.ts
Comment thread src/react-server-dom-rspack/plugin.ts
Comment thread src/webpack/RSCWebpackPlugin.ts Outdated
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review summary

This PR extends the #112 sibling-chunk CSS recovery to follow one non-style child hop, so a client reference's local child component (e.g. Button.js -> Panel.js -> Panel.css) keeps its stylesheet hint after a CSS-only SplitChunks cache group moves Panel.css into a separate chunk. The webpack and rspack implementations are near-identical, and the new fixtures/tests reproduce the red-before-fix repro from #180.

Findings posted inline:

  1. Possible CSS leak in the opposite direction (PLAUSIBLE, most severe)src/webpack/RSCWebpackPlugin.ts:832-853 and src/react-server-dom-rspack/plugin.ts:757-771. The new one-hop recovery gates only on "is this a style import" and "does the child share a chunk with the reference," but never checks whether the non-style child is itself an independently-registered client reference. If a parent client reference statically imports a nested 'use client' child that ends up bundled into the parent's own chunk (plausible for the FS-walk-discovered static-islands fixture's ParentStyledIsland -> StyledIsland shape), the child's own CSS could get attached to the parent's manifest entry too — contradicting the PR's own comment that "Shared dependency chunks still do not qualify, keeping the RSCWebpackPlugin re-broadcasts shared initial-chunk CSS (vendor/common) as render-blocking <link precedence="rsc-css"> per client reference (post-#52) #108 broadcast fixed." The existing tests/rspack-plugin/plugin.test.ts:289 test only asserts the child excludes the parent's CSS, never the reverse, so this direction isn't covered by CI. Worth confirming with a build/test run and, if reproducible, adding an explicit "skip if depModule is itself a resolved client reference" guard plus a symmetric test assertion.

  2. Chunk-membership gate looser than documented (PLAUSIBLE, lower severity)src/webpack/RSCWebpackPlugin.ts:805 / src/react-server-dom-rspack/plugin.ts:731. moduleChunks (used by sharesReferenceChunk) is built from every chunk containing the module compilation-wide rather than intersected with the current chunk group's groupChunks, so the gate can pass due to an unrelated chunk group's duplicate copy of the module. Impact is limited since emitted CSS is still filtered through groupChunks in addCssFromModuleChunks, but the gate doesn't match its documented "bundled into one of the reference's own JS chunks" intent.

Minor, non-blocking: directCssDepFiles now walks getOutgoingConnections(module) twice per invocation (once inside addDirectStyleImports(module), once in the main loop) — a straightforward partition-once-and-reuse would avoid the double walk, though the cost is bounded to one client reference's direct edges per chunk.

No issues found with null-guards, the one-hop bound (applied consistently in both files), CSS dedup, or the directCssDepFiles gating to client-reference modules only.

@justin808 justin808 force-pushed the codex/180-transitive-css-recovery branch from 03b7585 to fe9ad50 Compare July 7, 2026 09:48
@justin808

Copy link
Copy Markdown
Member Author

Review triage update for head fe9ad50:

qa-evidence v1

Comment thread src/webpack/RSCWebpackPlugin.ts Outdated
Comment thread src/react-server-dom-rspack/plugin.ts Outdated
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review summary

This PR extends the RSC webpack/rspack plugins' CSS sibling-chunk recovery to follow one additional non-style hop: when a client reference's own JS chunk lost its CSS to a css/mini-extract SplitChunks cache group, the new belongsToReferenceChunkGroup/addDirectStyleImports logic also recovers CSS imported by a direct child module of the reference (e.g. Button.js -> Panel.js -> Panel.css), while still excluding CSS from chunks shared across unrelated chunk groups (keeping #108 fixed). New fixtures/tests cover both bundlers.

Verified: the one-hop child-CSS recovery is correctly scoped per client-reference module (belongsToReferenceChunkGroup only evaluates direct outgoing connections of the specific reference being processed), so it does not reopen the #108 shared-chunk CSS broadcast — a sibling-reference CSS "leak" scenario I initially suspected turned out to require each reference to directly import the shared child anyway, which is correct/intended attribution, not a bug.

One real issue survived verification:

  • src/webpack/RSCWebpackPlugin.ts:778-786 and src/react-server-dom-rspack/plugin.ts:716-725chunkGroupUseCount is rebuilt from scratch (scanning all compilation.chunkGroups and their chunks) on every invocation of recordChunkGroup / every iteration of the outer chunk-group loop, even though its value doesn't depend on the chunk group currently being processed. recordChunkGroup is called once per chunk group (multiple loops in the webpack file, one in rspack), so this turns an O(chunkGroups × chunksPerGroup) pass into an O(chunkGroups² × chunksPerGroup) one. Apps with many 'use client' components — where each tends to become its own async chunk group under splitChunks — could see this meaningfully slow manifest generation. Hoisting the map computation out to run once per compilation (before the per-group loop(s)) would fix this in both files. Left inline comments on both spots.

Minor/non-blocking: the two plugin files now duplicate ~40 nearly-identical lines of new traversal logic (chunkGroupUseCount, belongsToReferenceChunkGroup, addDirectStyleImports) with no shared helper — consistent with the existing webpack/rspack duplication pattern in this codebase, so not a new problem this PR needs to solve, but worth keeping in mind if this logic needs another fix later.

@justin808 justin808 force-pushed the codex/180-transitive-css-recovery branch from fe9ad50 to 88f7bf0 Compare July 7, 2026 09:50
Comment thread src/webpack/RSCWebpackPlugin.ts
Comment thread src/webpack/RSCWebpackPlugin.ts
@justin808

Copy link
Copy Markdown
Member Author

Final review triage update for head 88f7bf0:

  • Hoisted chunkGroupUseCount to one computation per compilation in both webpack and rspack helpers.
  • Current-group chunk membership remains tightened from the previous amend.
  • Focused CSS tests and .agents/bin/validate passed after this change.
  • The “child is itself a client reference” suggestion remains triaged non-blocking for this repo because existing webpack behavior intentionally includes a rendered child client reference's CSS in the importing parent chunk when bundled there.
  • Deeper nested child traversal remains outside CSS recovery misses transitively-imported CSS under a css/mini-extract merge group (residual of #148 fix) #180's one-hop acceptance scope.

qa-evidence v1

Comment thread src/webpack/RSCWebpackPlugin.ts
@justin808

Copy link
Copy Markdown
Member Author

Final thread triage for head 88f7bf0:

qa-evidence v1

@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review summary

This PR extends the existing sibling-chunk CSS recovery (directCssDepFiles, in both RSCWebpackPlugin and RSCRspackPlugin) to also follow one hop into a client reference's non-style child module, so a child component's stylesheet is recovered even after a CSS-only SplitChunks cache group moves it into a separate chunk. New fixtures/tests cover the webpack and rspack cases, and the changelog is updated correctly.

Correctness: I traced the new sharesReferenceChunk/addDirectStyleImports guard closely, including a scenario where a suspected reintroduction of the old #108 "CSS broadcast" bug seemed plausible (a non-style child shared by two unrelated client references, with all CSS merged into one physical css/mini-extract chunk). After a full trace of the algorithm (sharesReferenceChunk only follows the reference's own real module-graph edges intersected with its own chunk-group's chunks — it never scans another reference's graph), I could not confirm a broadcast: the mechanism strictly requires an actual dependency edge from the reference to the CSS-owning module, so cross-reference leakage isn't possible through this code path. Existing regression tests (per-chunk CSS scoping, CSS-only shared dependency chunks stay excluded) continue to hold conceptually. No correctness bugs found that I'd block on.

Lower-severity notes (posted inline):

Nothing here should block the fix from landing; the notes are about durability/maintainability of the approach.

Comment thread src/webpack/RSCWebpackPlugin.ts Outdated
Comment on lines +799 to +805
// into a CSS-only chunk). Follow the module's DIRECT `.css`
// imports to the chunk(s) that carry them, intersected with this
// chunk group, and merge that CSS. Only direct imports are
// followed, so CSS reached through a shared dependency (imported by
// another module, not the reference) is NOT picked up — the #108
// broadcast stays fixed.
// chunk group, and merge that CSS. Also follow one non-style child
// hop when that child is bundled into one of the reference's own
// JS chunks, so a local child component's extracted stylesheet is
// recovered after a CSS-only SplitChunks merge. Shared dependency
// chunks still do not qualify, keeping the #108 broadcast fixed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This comment only documents the "child bundled into one of the reference's own JS chunks" case (moduleChunks.has(depChunk) in belongsToReferenceChunkGroup below). It doesn't mention the second, less obvious branch:

if (groupChunks.has(depChunk) && (chunkGroupUseCount.get(depChunk) ?? 0) === 1) {
  return true;
}

That branch is what actually makes the new "keeps child-module CSS when the child is split into its own JS chunk" test pass (Panel.js ends up in its own panel.chunk.js, not the same chunk as Button.js). It's also the subtler half of this fix from a correctness standpoint: it relies on the invariant that a chunk used by exactly one chunk group in the whole compilation is "private" to that reference, which only holds because each client reference gets its own async chunk group. Worth spelling that out here (and the corresponding rationale in src/react-server-dom-rspack/plugin.ts) so a future reader doesn't have to reverse-engineer why the chunkGroupUseCount === 1 check is safe against the #108 broadcast regression.

Comment thread src/react-server-dom-rspack/plugin.ts
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review summary

Reviewed the transitive/child-module CSS-only SplitChunks recovery for both RSCWebpackPlugin and RSCRspackPlugin, plus the new fixtures/tests.

Correctness: The core mechanism (directCssDepFilesaddDirectStyleImportsbelongsToReferenceChunkGroup) looks sound. It's demand-driven (only follows a reference module's own graph edges, one non-style hop deep) rather than broadcasting group-wide, so it shouldn't reopen #108. The chunkGroupUseCount === 1 heuristic for "this split chunk is private to the current reference" is a reasonable signal given that each client reference gets its own async chunk group in this plugin's architecture — I couldn't construct a case where it reintroduces the shared-dependency broadcast, since attachment is always gated on an actual module-graph edge from the specific reference. I left two inline comments asking to document that heuristic more explicitly where it's defined, since it's the subtlest part of the fix and the existing doc comments only describe the "same chunk" case, not the "used by exactly one chunk group" case that the new "child split into its own JS chunk" tests actually exercise.

Duplication (quality, non-blocking): The new logic (chunkGroupUseCount build-up, addDirectStyleImports, belongsToReferenceChunkGroup, moduleChunks) is ~90 lines duplicated nearly verbatim between src/webpack/RSCWebpackPlugin.ts and src/react-server-dom-rspack/plugin.ts. The PR description itself notes 3 review-fix amends were needed to keep both plugins in sync, which is exactly the kind of drift this duplication invites over time. Not a blocker for this PR, but might be worth a follow-up to extract the shared traversal into a common helper parameterized over the small Set/iterable API differences between webpack and rspack.

Minor perf nit: In both files, getOutgoingConnections(module) (or its rspack equivalent) is iterated twice for the top-level module — once inside addDirectStyleImports(module), once again in the loop that walks non-style children right after. Could reuse a single materialized array to avoid the double walk, though this is negligible relative to the rest of the per-module work.

Tests/fixtures: The new webpack and rspack fixtures and tests mirror each other well and cover both the "child CSS ends up in a CSS-only chunk" and "child is split into its own JS chunk" cases, plus a regression check that the existing shared-dependency exclusion (#108) and the no-warning invariant still hold.

No security or blocking correctness issues found.

Comment thread src/webpack/RSCWebpackPlugin.ts
Comment thread src/react-server-dom-rspack/plugin.ts
Comment thread src/react-server-dom-rspack/plugin.ts
@justin808 justin808 force-pushed the codex/180-transitive-css-recovery branch from 88f7bf0 to 6d05731 Compare July 7, 2026 10:08
Comment thread src/webpack/RSCWebpackPlugin.ts
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review summary

This PR adds a bounded "one non-style child hop" to the existing sibling-CSS-only-chunk recovery in both RSCWebpackPlugin and RSCRspackPlugin, so a client reference's local child component (e.g. Button.js -> Panel.js -> Panel.css) keeps its stylesheet hint after SplitChunks/mini-css-extract moves the CSS into a CSS-only chunk. It gates the extra hop behind a new belongsToReferenceChunkGroup heuristic meant to keep the existing #108 "don't broadcast a shared dependency's CSS to every reference" fix intact, and adds matching webpack/rspack fixtures and tests. Left two inline comments; the more important one below.

Most significant finding — possible reintroduction of #108 under merged chunk-group topologies (src/webpack/RSCWebpackPlugin.ts:846, src/react-server-dom-rspack/plugin.ts:773): belongsToReferenceChunkGroup treats "this chunk is a member of exactly one chunk group in the whole compilation" as a proxy for "not a shared dependency." That equivalence only holds when every distinct client reference gets its own chunk group, which is true for the client build topology the new regression test uses (chunkName: 'client-[request]'). But this repo's own isServer: true tests (plugin-integration.test.ts:767-802) show multiple distinct client references can collapse into the same chunk group on the server — in that topology the count is trivially 1 for every chunk, so the gate stops discriminating "private to this reference" from "shared by several references," and a genuinely shared dependency's CSS (e.g. reusing the split-shared-css fixture with isServer: true instead of per-reference chunk naming) could get attached to every reference that imports it. There's no test for the isServer/merged-group case; worth adding one before merging.

Secondary finding — efficiency (src/react-server-dom-rspack/plugin.ts:716): the new chunkGroupUseCount map is rebuilt by scanning every chunk group's chunks on every iteration of the outer for (const chunkGroup of compilation.chunkGroups) loop, even though its contents don't depend on the loop variable — O(G²·C) instead of O(G·C) for G chunk groups. Worth hoisting once above the loop (the webpack file already does this correctly, so this is rspack-specific).

Note: I initially (mistakenly) posted the same efficiency concern against the webpack file too, based on a stale local checkout of that file; I've posted a retraction inline once I re-verified against the actual PR diff — the webpack file already hoists this correctly.

@justin808

Copy link
Copy Markdown
Member Author

Final current-head review triage for 6d05731:

  • Server/shared CSS broadcast finding: covered by the new webpack server-build regression in this head. The test asserts shared-styles.chunk.css is emitted and absent from both Button.js and SettingsPage.js manifest CSS arrays; focused webpack coverage and .agents/bin/validate passed.
  • rspack chunkGroupUseCount efficiency finding: stale on this head. src/react-server-dom-rspack/plugin.ts builds the map once before the outer for (const chunkGroup of compilation.chunkGroups) loop, mirroring the webpack hoist.
  • One-hop/deeper traversal note remains non-blocking by CSS recovery misses transitively-imported CSS under a css/mini-extract merge group (residual of #148 fix) #180 scope; recursive traversal is intentionally not folded into this PR.
  • Current-head review threads are resolved as of this comment.

qa-evidence v1

Comment thread src/webpack/RSCWebpackPlugin.ts
Comment thread src/react-server-dom-rspack/plugin.ts
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review summary

Overall this is a well-scoped, well-tested fix. I read both plugin diffs in full, cross-checked the added fixtures/tests, and reasoned through the chunk-graph logic by hand (couldn't run yarn build/yarn jest in this review sandbox — no node_modules and shell execution for yarn/test commands was gated).

What looks solid:

  • The webpack/rspack implementations are kept structurally parallel (same helper names, same one-hop recursion bound, same chunkGroupUseCount heuristic), consistent with this repo's dual-plugin architecture.
  • chunkGroupUseCount is computed once per compilation (not per chunk group), so the added work is O(total chunk-group memberships) — no meaningful perf regression.
  • The bounded "one non-style hop" scope is clearly documented as intentional (deeper nested traversal is out of scope for CSS recovery misses transitively-imported CSS under a css/mini-extract merge group (residual of #148 fix) #180), and the new fixtures (Button.js -> Panel.js -> Panel.css) directly exercise the two real-world shapes: CSS-only split chunk, and child split into its own JS chunk.
  • The pre-existing #108 shared-dependency exclusion test (CSS-only shared dependency chunks stay excluded..., client build) and the new server-side regression (does not broadcast a shared dependency's split stylesheet...) both exercise the new one-hop code path and pass per the PR's validation notes, so the original broadcast bug isn't reintroduced for the shared-sibling-module case.
  • No security implications — this is build-time chunk-graph analysis over the developer's own compilation graph, not user-controllable input.

One thing worth double-checking before merge (left as inline comments on both src/webpack/RSCWebpackPlugin.ts and src/react-server-dom-rspack/plugin.ts, at belongsToReferenceChunkGroup):

The moduleChunks.has(depChunk) branch (same physical chunk as the reference) is a strong signal for a plain helper child like the new Panel.js fixture, but this repo already has a fixture (static-islands: ParentStyledIsland.js synchronously importing StyledIsland.js, where StyledIsland.js is also independently tracked as its own client reference) that plausibly hits the same branch. If the bundler duplicates StyledIsland's module into an inline copy inside ParentStyledIsland's own chunk (the standard outcome for a module that's both sync-required and separately entry-pointed), this new one-hop recovery would pull StyledIsland.css onto ParentStyledIsland's manifest entry too — on top of StyledIsland's own separate entry already carrying it.

The existing tests for that fixture (lines 289, 307, 417, 437 in tests/rspack-plugin/plugin.test.ts) only assert the reverse direction (child excludes parent's CSS); none assert the parent excludes the child's CSS, so this direction isn't locked in. Since #108/#112 exist specifically to prevent cross-reference CSS broadcast, and this PR is what makes that leak newly reachable, it'd be good to confirm whether this is intended and pin it down with an assertion either way. I flagged this as a reasoned-through risk (not an executed/confirmed failure) since I couldn't run the actual build here.

@justin808 justin808 merged commit 0533d53 into main Jul 7, 2026
32 checks passed
@justin808 justin808 deleted the codex/180-transitive-css-recovery branch July 7, 2026 10:26
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.

CSS recovery misses transitively-imported CSS under a css/mini-extract merge group (residual of #148 fix)

1 participant