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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this package will be documented in this file.
- Skipped rspack diagnostics CSS collection when client-reference diagnostics are disabled, avoiding dead per-chunk-group CSS scans on the default build path. ([#152])

### Fixed
- Recovered RSC stylesheet hints for CSS imported by a client reference's child module when a CSS-merging SplitChunks cache group moves the stylesheet into a CSS-only split chunk. ([#184])
- Fixed Webpack client-reference string paths to resolve relative to the compiler context and warned when `output.publicPath: "auto"` cannot be serialized into the RSC manifest. ([#171])
- Fixed `RSCRspackPlugin` to skip manifest and diagnostics emission when the Flight client runtime is missing, matching the Webpack plugin's misconfiguration behavior instead of writing unusable assets. ([#176])
- Fixed Rspack client-reference manifests to emit deterministic chunk pair ordering under Rspack 2 while preserving the full sibling chunk set. ([#140])
Expand Down Expand Up @@ -81,6 +82,7 @@ All notable changes to this package will be documented in this file.
[#172]: https://github.com/shakacode/react_on_rails_rsc/pull/172
[#176]: https://github.com/shakacode/react_on_rails_rsc/pull/176
[#183]: https://github.com/shakacode/react_on_rails_rsc/pull/183
[#184]: https://github.com/shakacode/react_on_rails_rsc/pull/184
[#165]: https://github.com/shakacode/react_on_rails_rsc/pull/165
[#164]: https://github.com/shakacode/react_on_rails_rsc/pull/164
[#151]: https://github.com/shakacode/react_on_rails_rsc/pull/151
Expand Down
56 changes: 49 additions & 7 deletions src/react-server-dom-rspack/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,17 @@ export class RSCRspackPlugin {
// plugin, lines 241-294). Each module gets the full list of sibling
// chunks in its group — this ensures splitChunks dependencies are
// included.
const chunkGroupUseCount = new Map<AnyChunk, number>();
for (const candidateGroup of compilation.chunkGroups) {
for (const candidateChunkUnknown of candidateGroup.chunks) {
const candidateChunk = candidateChunkUnknown as AnyChunk;
chunkGroupUseCount.set(
candidateChunk,
Comment thread
justin808 marked this conversation as resolved.
(chunkGroupUseCount.get(candidateChunk) ?? 0) + 1,
);
}
}

for (const chunkGroup of compilation.chunkGroups) {
const groupChunkList = [...chunkGroup.chunks].map((chunk) => chunk as AnyChunk);
const normalizedChunkGroup = { name: chunkGroup.name, chunks: groupChunkList };
Expand All @@ -728,6 +739,11 @@ export class RSCRspackPlugin {
const directCssDepFiles = (module: AnyModule): string[] => {
if (!getOutgoingConnections || cssPrefix === null) return [];
const files = new Set<string>();
const moduleChunks = new Set(
[...compilation.chunkGraph.getModuleChunks(module)]
.map((chunk) => chunk as AnyChunk)
.filter((chunk) => groupChunkSet.has(chunk)),
);
const addCssFromModuleChunks = (cssModule: AnyModule): void => {
for (const cssChunkUnknown of compilation.chunkGraph.getModuleChunks(cssModule)) {
const cssChunk = cssChunkUnknown as AnyChunk;
Expand All @@ -737,18 +753,44 @@ export class RSCRspackPlugin {
}
}
};
const addDirectStyleImports = (sourceModule: AnyModule): void => {
for (const connection of getOutgoingConnections(sourceModule)) {
const depModule = connection.module ?? connection.resolvedModule;
if (!depModule?.resource) continue;
const depResource = depModule.resource.replace(/[?#].*$/, '');
if (!STYLE_SOURCE_RE.test(depResource)) continue;
addCssFromModuleChunks(depModule);
for (const cssConnection of getOutgoingConnections(depModule)) {
const extractedCssModule = cssConnection.module ?? cssConnection.resolvedModule;
if (!extractedCssModule || extractedCssModule.type !== 'css/mini-extract') continue;
addCssFromModuleChunks(extractedCssModule);
}
}
};
// Match the webpack plugin: a one-hop non-style child qualifies only
// when it shares the reference module's chunk or rspack split it to a
// chunk used by this async chunk group only. The use-count check keeps
// local child-component CSS without re-broadcasting shared dependency
// chunks across reference groups (#108).
const belongsToReferenceChunkGroup = (depModule: AnyModule): boolean => {
for (const depChunkUnknown of compilation.chunkGraph.getModuleChunks(depModule)) {
const depChunk = depChunkUnknown as AnyChunk;
if (moduleChunks.has(depChunk)) return true;
Comment thread
justin808 marked this conversation as resolved.
if (groupChunkSet.has(depChunk) && (chunkGroupUseCount.get(depChunk) ?? 0) === 1) {
Comment thread
justin808 marked this conversation as resolved.
return true;
}
}
return false;
};
Comment thread
justin808 marked this conversation as resolved.

addDirectStyleImports(module);
Comment thread
justin808 marked this conversation as resolved.
for (const connection of getOutgoingConnections(module)) {
const depModule = connection.module ?? connection.resolvedModule;
if (!depModule?.resource) continue;
const depResource = depModule.resource.replace(/[?#].*$/, '');
Comment thread
justin808 marked this conversation as resolved.
if (!STYLE_SOURCE_RE.test(depResource)) continue;
addCssFromModuleChunks(depModule);
for (const cssConnection of getOutgoingConnections(depModule)) {
const extractedCssModule = cssConnection.module ?? cssConnection.resolvedModule;
if (!extractedCssModule || extractedCssModule.type !== 'css/mini-extract') continue;
addCssFromModuleChunks(extractedCssModule);
}
if (STYLE_SOURCE_RE.test(depResource)) continue;
if (!belongsToReferenceChunkGroup(depModule)) continue;
addDirectStyleImports(depModule);
}

return [...files];
Expand Down
60 changes: 47 additions & 13 deletions src/webpack/RSCWebpackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,15 @@ export class RSCWebpackPlugin {

let missingClientReferenceBlocksWarningEmitted = false;
const clientReferenceChunkGroupsByResource = new Map<string, Set<FlightChunkGroup>>();
const chunkGroupUseCount = new Map<FlightChunk, number>();
Comment thread
justin808 marked this conversation as resolved.
for (const candidateGroup of compilation.chunkGroups) {
for (const candidateChunk of candidateGroup.chunks) {
chunkGroupUseCount.set(
candidateChunk,
(chunkGroupUseCount.get(candidateChunk) ?? 0) + 1,
);
}
}

// Records every module of `chunkGroup` whose resource is in
// `chunkResolvedClientFiles`, listing the chunk group's own
Expand Down Expand Up @@ -789,10 +798,12 @@ export class RSCWebpackPlugin {
// css/mini-extract cache group that moves the extracted CssModule
// 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 belongs to the reference's async chunk group:
// either it shares one of the reference module's chunks, or webpack
// split it to a chunk used by this group only. That second check
// keeps a split local child component's stylesheet while still
// excluding chunks shared by multiple reference groups (#108).
// Guarded on `moduleGraph`/`getModuleChunksIterable`, which the
// unit-test mocks omit (they exercise the per-chunk pass only).
const moduleGraph = compilation.moduleGraph;
Expand All @@ -801,6 +812,9 @@ export class RSCWebpackPlugin {
const directCssDepFiles = (module: FlightModule): string[] => {
if (!moduleGraph || !getModuleChunksIterable || cssPrefix === null) return [];
const files = new Set<string>();
const moduleChunks = new Set(
[...getModuleChunksIterable(module)].filter((chunk) => groupChunks.has(chunk)),
);
Comment thread
justin808 marked this conversation as resolved.
const addCssFromModuleChunks = (cssModule: FlightModule): void => {
for (const cssChunk of getModuleChunksIterable(cssModule)) {
if (!groupChunks.has(cssChunk)) continue;
Expand All @@ -811,6 +825,32 @@ export class RSCWebpackPlugin {
}
}
};
const addDirectStyleImports = (sourceModule: FlightModule): void => {
for (const connection of moduleGraph.getOutgoingConnections(sourceModule)) {
const depModule = connection.module ?? connection.resolvedModule;
if (!depModule || !depModule.resource) continue;
const depResource = depModule.resource.replace(/[?#].*$/, '');
if (!STYLE_SOURCE_RE.test(depResource)) continue;
addCssFromModuleChunks(depModule);
for (const cssConnection of moduleGraph.getOutgoingConnections(depModule)) {
const extractedCssModule = cssConnection.module ?? cssConnection.resolvedModule;
if (!extractedCssModule || extractedCssModule.type !== 'css/mini-extract') {
continue;
}
Comment thread
justin808 marked this conversation as resolved.
addCssFromModuleChunks(extractedCssModule);
}
}
};
const belongsToReferenceChunkGroup = (depModule: FlightModule): boolean => {
for (const depChunk of getModuleChunksIterable(depModule)) {
if (moduleChunks.has(depChunk)) return true;
if (groupChunks.has(depChunk) && (chunkGroupUseCount.get(depChunk) ?? 0) === 1) {
Comment thread
justin808 marked this conversation as resolved.
return true;
}
}
return false;
};
Comment thread
justin808 marked this conversation as resolved.
addDirectStyleImports(module);
for (const connection of moduleGraph.getOutgoingConnections(module)) {
Comment thread
justin808 marked this conversation as resolved.
// `module` is the resolved destination for most connections;
// some dependency types leave it null with the target on
Comment thread
justin808 marked this conversation as resolved.
Expand All @@ -823,15 +863,9 @@ export class RSCWebpackPlugin {
// emitted chunk file is always `.css`. Strip any webpack
// resource query/fragment (`./Button.css?inline`) first.
const depResource = depModule.resource.replace(/[?#].*$/, '');
if (!STYLE_SOURCE_RE.test(depResource)) continue;
addCssFromModuleChunks(depModule);
for (const cssConnection of moduleGraph.getOutgoingConnections(depModule)) {
const extractedCssModule = cssConnection.module ?? cssConnection.resolvedModule;
if (!extractedCssModule || extractedCssModule.type !== 'css/mini-extract') {
continue;
}
addCssFromModuleChunks(extractedCssModule);
}
if (STYLE_SOURCE_RE.test(depResource)) continue;
if (!belongsToReferenceChunkGroup(depModule)) continue;
addDirectStyleImports(depModule);
}
Comment thread
justin808 marked this conversation as resolved.
return [...files];
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client';

import { Panel } from './Panel';

export default function Button() {
return 'button:' + Panel();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.panel {
color: rebeccapurple;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './Panel.css';

export function Panel() {
return 'panel';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const entryOnly = 'entry-only-module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { entryOnly } from './entryOnly';

export const app = [entryOnly];
54 changes: 54 additions & 0 deletions tests/rspack-plugin/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,60 @@ describe('RSCRspackPlugin', () => {
expect(readManifestCss(result, '/StyledIsland.js')).toContain('.styled-island');
});

it('keeps child-module CSS-only split chunk styles in the server manifest', () => {
const result = run('transitive-css-only-chunk', {
isServer: true,
clientReferences: staticIslandClientReferences(/^\.\/Button\.js$/),
publicPath: '/assets',
withCss: true,
configExtra: splitStaticIslandCssOnlyChunks,
});

expect(result.assets).toContain('styles.chunk.css');
expect(manifestMetadataFor(result, '/Button.js').css).toEqual([
'/assets/styles.chunk.css',
]);
expect(readManifestCss(result, '/Button.js')).toContain('.panel');
});

it('keeps child-module CSS when the child is split into its own JS chunk', () => {
const result = run('transitive-css-only-chunk', {
isServer: true,
clientReferences: staticIslandClientReferences(/^\.\/Button\.js$/),
publicPath: '/assets',
withCss: true,
configExtra: {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
default: false,
defaultVendors: false,
panel: {
test: /Panel\.js$/,
name: 'panel',
chunks: 'all',
enforce: true,
},
styles: {
name: 'styles',
type: 'css/mini-extract',
chunks: 'all',
enforce: true,
},
},
},
},
},
});

expect(result.assets).toEqual(expect.arrayContaining(['panel.chunk.js', 'styles.chunk.css']));
const button = manifestMetadataFor(result, '/Button.js');
expect(manifestChunkFiles(button.chunks)).toContain('panel.chunk.js');
expect(button.css).toContain('/assets/styles.chunk.css');
});

it('keeps mixed chunk-local and CSS-only split styles in the server manifest', () => {
const result = run('static-islands', {
isServer: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client';

import { Panel } from './Panel';

export default function Button() {
return 'button:' + Panel();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.panel {
color: rebeccapurple;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './Panel.css';

export function Panel() {
return 'panel';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const entryOnly = 'entry-only-module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { entryOnly } from './entryOnly';

export const app = [entryOnly];
Loading