Skip to content

Commit aab2e0e

Browse files
authored
fix(rsc): handle server component css by server entry scope (#13844)
* fix(rsc): handle server component css by server entry scope * samply RscEntryModule identifier * chore: add css test case * chore: refactor plugin state * feat: fix shared css in root and page * fix: RscEntryModule create_identifier
1 parent 5e5666f commit aab2e0e

28 files changed

Lines changed: 633 additions & 118 deletions

crates/rspack_plugin_rsc/src/client_plugin.rs

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use rspack_collections::IdentifierSet;
66
use rspack_core::{
77
ChunkGraph, ChunkGroup, ChunkGroupUkey, ChunkUkey, Compilation, CompilationAfterProcessAssets,
88
CompilationParams, CompilerCompilation, CompilerFailed, CompilerId, CompilerMake,
9-
CrossOriginLoading, DependenciesBlock, Dependency, DependencyId, DependencyType, Logger,
10-
ModuleGraph, ModuleId, ModuleIdentifier, Plugin,
9+
CrossOriginLoading, DependenciesBlock, Dependency, DependencyId, DependencyType, EntryDependency,
10+
Logger, ModuleGraph, ModuleId, ModuleIdentifier, Plugin,
1111
};
1212
use rspack_error::{Diagnostic, Result};
1313
use rspack_hook::{plugin, plugin_hook};
@@ -98,8 +98,8 @@ fn record_module(
9898

9999
if is_css_mod(module.as_ref()) {
100100
let mut matched_server_entries = Vec::new();
101-
for (server_entry, imports) in &entry_state.css_imports_per_server_entry {
102-
if imports.contains(resource.as_ref()) {
101+
for (server_entry, server_entry_state) in &entry_state.server_entries {
102+
if server_entry_state.css_imports.contains(resource.as_ref()) {
103103
matched_server_entries.push(server_entry.clone());
104104
}
105105
}
@@ -128,9 +128,10 @@ fn record_module(
128128

129129
for server_entry in matched_server_entries {
130130
entry_state
131-
.entry_css_files
132-
.entry(server_entry.clone())
131+
.server_entries
132+
.entry(server_entry)
133133
.or_default()
134+
.css_files
134135
.extend(css_files.clone());
135136
}
136137

@@ -261,7 +262,10 @@ fn record_chunk_group(
261262
}
262263
}
263264

264-
fn collect_entry_js_files(compilation: &Compilation, plugin_state: &mut PluginState) -> Result<()> {
265+
fn collect_bootstrap_scripts(
266+
compilation: &Compilation,
267+
plugin_state: &mut PluginState,
268+
) -> Result<()> {
265269
for (entry_name, chunk_group_ukey) in &compilation.build_chunk_graph_artifact.entrypoints {
266270
let Some(entry_state) = plugin_state.entries.get_mut(entry_name.as_str()) else {
267271
continue;
@@ -281,7 +285,7 @@ fn collect_entry_js_files(compilation: &Compilation, plugin_state: &mut PluginSt
281285
.expect("module_loading should be initialized in traverse_modules before recording modules")
282286
.prefix;
283287

284-
let entry_js_files = chunk_group
288+
let bootstrap_scripts = chunk_group
285289
.get_files(&compilation.build_chunk_graph_artifact.chunk_by_ukey)
286290
.into_iter()
287291
.filter(|chunk_file| chunk_file.ends_with(".js"))
@@ -297,7 +301,7 @@ fn collect_entry_js_files(compilation: &Compilation, plugin_state: &mut PluginSt
297301
.map(|file| prefixed_asset_path(prefix, &file))
298302
.collect::<FxIndexSet<String>>();
299303

300-
entry_state.entry_js_files = entry_js_files;
304+
entry_state.bootstrap_scripts = bootstrap_scripts;
301305
}
302306
Ok(())
303307
}
@@ -520,6 +524,7 @@ async fn compilation(
520524
params: &mut CompilationParams,
521525
) -> Result<()> {
522526
compilation.set_dependency_factory(DependencyType::RscEntry, Arc::new(RscEntryModuleFactory));
527+
compilation.set_dependency_factory(DependencyType::Entry, params.normal_module_factory.clone());
523528
compilation.set_dependency_factory(
524529
DependencyType::RscClientReference,
525530
params.normal_module_factory.clone(),
@@ -543,29 +548,30 @@ async fn make(&self, compilation: &mut Compilation) -> Result<()> {
543548
)
544549
})?;
545550

546-
let mut include_dependencies = vec![];
547551
for (entry_name, entry_state) in &plugin_state.entries {
548552
let client_modules = &entry_state.injected_client_entries;
549-
{
550-
if compilation.entries.get(entry_name.as_ref()).is_none() {
551-
compilation.push_diagnostic(Diagnostic::error(
552-
"RSC Client Entry Mismatch".to_string(),
553-
format!(
554-
"Entry '{}' not found in the client compiler. Failed to inject the following client modules: {}",
555-
entry_name,
556-
client_modules
557-
.iter()
558-
.map(|m| m.request.as_str())
559-
.collect::<Vec<_>>()
560-
.join(", ")
561-
),
562-
));
563-
continue;
564-
}
553+
if compilation.entries.get(entry_name.as_ref()).is_none() {
554+
compilation.push_diagnostic(Diagnostic::error(
555+
"RSC Client Entry Mismatch".to_string(),
556+
format!(
557+
"Entry '{}' not found in the client compiler. Failed to inject the following client modules: {}",
558+
entry_name,
559+
client_modules
560+
.iter()
561+
.map(|m| m.request.as_str())
562+
.collect::<Vec<_>>()
563+
.join(", ")
564+
),
565+
));
566+
continue;
567+
}
565568

569+
let mut include_dependencies = Vec::new();
570+
if !client_modules.is_empty() || entry_state.has_css_imports_by_server_entry() {
566571
let dependency = Box::new(RscEntryDependency::new(
567572
entry_name.clone(),
568573
client_modules.clone(),
574+
entry_state.css_imports_by_server_entry(),
569575
false,
570576
));
571577
self
@@ -580,8 +586,23 @@ async fn make(&self, compilation: &mut Compilation) -> Result<()> {
580586
.add_dependency(dependency);
581587
}
582588

589+
let mut entry_dependencies = Vec::new();
590+
for request in &entry_state.root_css_imports {
591+
let dependency = Box::new(EntryDependency::new(
592+
request.clone(),
593+
compilation.options.context.clone(),
594+
None,
595+
false,
596+
));
597+
entry_dependencies.push(*dependency.id());
598+
compilation
599+
.get_module_graph_mut()
600+
.add_dependency(dependency);
601+
}
602+
583603
#[allow(clippy::unwrap_used)]
584604
let entry_data = compilation.entries.get_mut(entry_name.as_ref()).unwrap();
605+
entry_data.dependencies.append(&mut entry_dependencies);
585606
entry_data
586607
.include_dependencies
587608
.append(&mut include_dependencies);
@@ -614,8 +635,8 @@ async fn after_process_assets(
614635
self.traverse_modules(compilation, &mut plugin_state)?;
615636
logger.time_end(start);
616637

617-
let start = logger.time("record entry js files");
618-
collect_entry_js_files(compilation, &mut plugin_state)?;
638+
let start = logger.time("record bootstrap scripts");
639+
collect_bootstrap_scripts(compilation, &mut plugin_state)?;
619640
logger.time_end(start);
620641

621642
for (entry_name, client_entries) in self.client_entries_per_entry.borrow().iter() {

crates/rspack_plugin_rsc/src/component_info.rs

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,23 @@ use rspack_plugin_javascript::dependency::{
99
ESMImportSpecifierDependency,
1010
};
1111
use rspack_util::fx_hash::{FxIndexMap, FxIndexSet};
12-
use rustc_hash::FxHashMap;
1312
use swc_core::atoms::{Atom, Wtf8Atom};
1413

1514
use crate::{
1615
constants::{IMAGE_REGEX, LAYERS_NAMES},
17-
plugin_state::ActionIdNamePair,
16+
plugin_state::{ActionIdNamePair, CssImportsByServerEntry, RootCssImports},
1817
utils::{get_module_resource, is_css_mod},
1918
};
2019

2120
// { [request to inject into client compilation]: [exported names] }
22-
// CSS requests are represented by an empty export set.
2321
pub type ClientComponentImports = FxIndexMap<String, FxIndexSet<Atom>>;
24-
// { [server entry path]: [css imports] }
25-
// Used only to map emitted CSS files back to server entries in the manifest.
26-
pub type CssImportsPerServerEntry = FxHashMap<String, FxIndexSet<String>>;
2722

2823
#[derive(Debug, Default)]
2924
pub struct ComponentInfo {
3025
pub should_inject_ssr_modules: bool,
3126
pub client_component_imports: ClientComponentImports,
32-
pub css_imports_per_server_entry: CssImportsPerServerEntry,
27+
pub css_imports_by_server_entry: CssImportsByServerEntry,
28+
pub root_css_imports: RootCssImports,
3329
pub action_imports: Vec<(String, Vec<ActionIdNamePair>)>,
3430
}
3531

@@ -114,6 +110,46 @@ fn filter_client_components(
114110
return;
115111
}
116112

113+
// CSS ownership depends on the current parent chain, so record it before the
114+
// `visited` short-circuit. A stylesheet may be seen first through a
115+
// `use server-entry` component and later through the root RSC tree; the later
116+
// root visit still needs to update `root_css_imports` and remove any earlier
117+
// server-entry ownership.
118+
if is_css_mod(module) {
119+
let side_effect_free = module_declared_side_effect_free(module).unwrap_or(false);
120+
121+
if side_effect_free {
122+
let exports_info = compilation
123+
.exports_info_artifact
124+
.get_exports_info_data(&module.identifier());
125+
let unused = !exports_info.is_module_used(Some(runtime));
126+
if unused {
127+
return;
128+
}
129+
}
130+
131+
if server_entries.is_empty() {
132+
// CSS with no `use server-entry` in its parent chain should load with
133+
// the client entry rather than through RSC server-entry CSS metadata.
134+
component_info.root_css_imports.insert(resource.to_string());
135+
component_info
136+
.css_imports_by_server_entry
137+
.retain(|_, css_imports| {
138+
css_imports.shift_remove(resource.as_ref());
139+
!css_imports.is_empty()
140+
});
141+
} else if !component_info.root_css_imports.contains(resource.as_ref()) {
142+
for server_entry in server_entries.iter() {
143+
component_info
144+
.css_imports_by_server_entry
145+
.entry(server_entry.clone())
146+
.or_default()
147+
.insert(resource.to_string());
148+
}
149+
}
150+
return;
151+
}
152+
117153
if visited.contains(&module.identifier()) {
118154
if component_info
119155
.client_component_imports
@@ -151,32 +187,7 @@ fn filter_client_components(
151187
}
152188

153189
let module_graph = compilation.get_module_graph();
154-
if is_css_mod(module) {
155-
let side_effect_free = module_declared_side_effect_free(module).unwrap_or(false);
156-
157-
if side_effect_free {
158-
let exports_info = compilation
159-
.exports_info_artifact
160-
.get_exports_info_data(&module.identifier());
161-
let unused = !exports_info.is_module_used(Some(runtime));
162-
if unused {
163-
return;
164-
}
165-
}
166-
167-
component_info
168-
.client_component_imports
169-
.entry(resource.to_string())
170-
.or_default();
171-
172-
for server_entry in server_entries.iter() {
173-
component_info
174-
.css_imports_per_server_entry
175-
.entry(server_entry.clone())
176-
.or_default()
177-
.insert(resource.to_string());
178-
}
179-
} else if is_client_component_entry_module(module) {
190+
if is_client_component_entry_module(module) {
180191
if !component_info
181192
.client_component_imports
182193
.contains_key(resource.as_ref())

crates/rspack_plugin_rsc/src/manifest_runtime_module.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ impl RuntimeModule for RscManifestRuntimeModule {
8080
client_manifest,
8181
server_consumer_module_map,
8282
module_loading,
83-
entry_css_files: &entry_state.entry_css_files,
84-
entry_js_files: &entry_state.entry_js_files,
83+
server_entries: &entry_state.server_entries,
84+
bootstrap_scripts: &entry_state.bootstrap_scripts,
8585
};
8686

8787
Ok(formatdoc! {

crates/rspack_plugin_rsc/src/plugin_state.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use crate::reference_manifest::{
1515
};
1616

1717
pub type ActionIdNamePair = (Atom, Atom);
18+
pub type CssImportsByServerEntry = FxHashMap<String, FxIndexSet<String>>;
19+
pub type RootCssImports = FxIndexSet<String>;
1820

1921
/// Structured info about a client module to inject into the client compiler.
2022
#[rspack_cacheable::cacheable]
@@ -25,24 +27,50 @@ pub struct ClientModuleImport {
2527
pub ids: FxIndexSet<Atom>,
2628
}
2729

30+
#[derive(Debug, Default)]
31+
pub struct ServerEntryState {
32+
/// CSS import paths referenced by this server entry.
33+
pub css_imports: FxIndexSet<String>,
34+
/// CSS chunk file paths emitted for this server entry.
35+
pub css_files: FxIndexSet<String>,
36+
}
37+
2838
/// State for one compilation entry.
2939
#[derive(Debug, Default)]
3040
pub struct EntryState {
41+
pub server_entries: FxHashMap<String, ServerEntryState>,
3142
pub injected_client_entries: Vec<ClientModuleImport>,
3243
pub client_modules: FxHashMap<String, ManifestExport>,
33-
/// Server entry resource -> CSS import paths.
34-
pub css_imports_per_server_entry: FxHashMap<String, FxIndexSet<String>>,
44+
/// Root CSS import paths reached through a parent chain without `use server-entry`.
45+
/// These are attached directly to the matching client compiler entry.
46+
pub root_css_imports: RootCssImports,
3547
/// Dependency path -> action id/name pairs.
3648
pub client_actions: FxHashMap<String, Vec<ActionIdNamePair>>,
3749
pub server_actions: ServerReferenceManifest,
38-
/// Server entry resource -> CSS chunk file paths.
39-
pub entry_css_files: FxHashMap<String, FxIndexSet<String>>,
40-
pub entry_js_files: FxIndexSet<String>,
50+
pub bootstrap_scripts: FxIndexSet<String>,
4151
pub changed_server_components: IdentifierSet,
4252
/// Precomputed in chunk_ids hook.
4353
pub server_consumer_module_map: Option<FxHashMap<String, ManifestNode>>,
4454
}
4555

56+
impl EntryState {
57+
pub fn has_css_imports_by_server_entry(&self) -> bool {
58+
self
59+
.server_entries
60+
.values()
61+
.any(|server_entry| !server_entry.css_imports.is_empty())
62+
}
63+
64+
pub fn css_imports_by_server_entry(&self) -> CssImportsByServerEntry {
65+
self
66+
.server_entries
67+
.iter()
68+
.filter(|(_, server_entry)| !server_entry.css_imports.is_empty())
69+
.map(|(name, server_entry)| (name.clone(), server_entry.css_imports.clone()))
70+
.collect()
71+
}
72+
}
73+
4674
#[derive(Debug, Default)]
4775
pub struct PluginState {
4876
pub module_loading: Option<ModuleLoading>,

crates/rspack_plugin_rsc/src/reference_manifest.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize, Serializer, ser::SerializeMap};
1010
use crate::{
1111
constants::LAYERS_NAMES,
1212
loaders::action_entry_loader::{ACTION_ENTRY_LOADER_IDENTIFIER, parse_action_entries},
13-
plugin_state::PluginState,
13+
plugin_state::{PluginState, ServerEntryState},
1414
utils::{ChunkModules, get_module_resource},
1515
};
1616

@@ -65,6 +65,28 @@ where
6565
}
6666
}
6767

68+
fn serialize_server_entries_css_files<S>(
69+
server_entries: &FxHashMap<String, ServerEntryState>,
70+
serializer: S,
71+
) -> Result<S::Ok, S::Error>
72+
where
73+
S: Serializer,
74+
{
75+
let mut map = serializer.serialize_map(Some(
76+
server_entries
77+
.values()
78+
.filter(|server_entry| !server_entry.css_files.is_empty())
79+
.count(),
80+
))?;
81+
for (server_entry, state) in server_entries {
82+
if state.css_files.is_empty() {
83+
continue;
84+
}
85+
map.serialize_entry(server_entry, &state.css_files)?;
86+
}
87+
map.end()
88+
}
89+
6890
#[derive(Debug, Serialize)]
6991
#[serde(rename_all = "camelCase")]
7092
pub struct RscEntryManifest<'a> {
@@ -73,8 +95,13 @@ pub struct RscEntryManifest<'a> {
7395
#[serde(serialize_with = "serialize_none_as_empty_object")]
7496
pub server_consumer_module_map: Option<&'a FxHashMap<String, ManifestNode>>,
7597
pub module_loading: &'a ModuleLoading,
76-
pub entry_css_files: &'a FxHashMap<String, FxIndexSet<String>>,
77-
pub entry_js_files: &'a FxIndexSet<String>,
98+
#[serde(
99+
rename = "entryCssFiles",
100+
serialize_with = "serialize_server_entries_css_files"
101+
)]
102+
pub server_entries: &'a FxHashMap<String, ServerEntryState>,
103+
#[serde(rename = "entryJsFiles")]
104+
pub bootstrap_scripts: &'a FxIndexSet<String>,
78105
}
79106

80107
/// Full manifest (all entries) for the onManifest callback. Map from entry name to per-entry manifest.

0 commit comments

Comments
 (0)