diff --git a/crates/rspack_plugin_rsc/src/client_plugin.rs b/crates/rspack_plugin_rsc/src/client_plugin.rs index 9082b5f40fc1..a02088924abb 100644 --- a/crates/rspack_plugin_rsc/src/client_plugin.rs +++ b/crates/rspack_plugin_rsc/src/client_plugin.rs @@ -96,7 +96,7 @@ fn record_module( return; } - if is_css_mod(module.as_ref()) { + if is_css_mod(module.as_ref(), resource.as_ref()) { return; } @@ -188,9 +188,15 @@ fn collect_server_entry_css_files( continue; }; - // Server-entry CSS blocks use the server entry resource as their request. - // Client component blocks use the client module request, so this lookup - // also filters out non-CSS blocks without walking dependencies again. + // Only server CSS blocks should populate `entryCssFiles` for loadCss(). + // Client component blocks use the client module request here; their CSS is + // recorded on `clientManifest[*].cssFiles` when recording the client module. + // + // It is expected for a grouped client owner to have no `server_entries` + // record when that server entry only owns client components and imports no + // server CSS directly. Seeding `server_entries` for that case would make + // client component CSS look like server-entry CSS, which would duplicate + // the client manifest data and change the meaning of `entryCssFiles`. if entry_state .server_entries .get(server_entry) @@ -658,7 +664,9 @@ async fn make(&self, compilation: &mut Compilation) -> Result<()> { if !client_modules.is_empty() || entry_state.has_css_imports_by_server_entry() { let dependency = Box::new(RscEntryDependency::new( entry_name.clone(), - client_modules.clone(), + entry_state.ungrouped_client_entries.clone(), + entry_state.root_client_entries.clone(), + entry_state.client_entries_by_server_entry.clone(), entry_state.css_imports_by_server_entry(), false, )); diff --git a/crates/rspack_plugin_rsc/src/component_info.rs b/crates/rspack_plugin_rsc/src/component_info.rs index 41fad64f0212..a255d872edee 100644 --- a/crates/rspack_plugin_rsc/src/component_info.rs +++ b/crates/rspack_plugin_rsc/src/component_info.rs @@ -1,8 +1,8 @@ use derive_more::Debug; use rspack_collections::IdentifierSet; use rspack_core::{ - Compilation, DependencyId, Module, ModuleGraph, RscMeta, RscModuleType, RuntimeSpec, - module_declared_side_effect_free, + Compilation, DependencyId, Module, ModuleGraph, ModuleIdentifier, RscMeta, RscModuleType, + RuntimeSpec, module_declared_side_effect_free, }; use rspack_plugin_javascript::dependency::{ CommonJsExportRequireDependency, ESMExportImportedSpecifierDependency, @@ -20,19 +20,33 @@ use crate::{ // { [request to inject into client compilation]: [exported names] } pub type ClientComponentImports = FxIndexMap>; +pub type ClientComponentImportsByServerEntry = FxIndexMap; // { [server entry path]: [`import.meta.rspackRsc` importer paths] } // Used only to let `loadCss()` importers inherit their nearest server entry CSS files. pub type ImportMetaRscImporters = FxHashMap>; -// Tracks server component traversal per current `use server-entry` owner. -// This lets a shared server component be visited once for each server entry -// that needs to collect CSS from it, while still preventing recursive loops. -type VisitedServerComponents = FxHashSet<(rspack_core::ModuleIdentifier, Option)>; +// Tracks server component traversal per current `use server-entry` owner and +// dynamic import context. This lets a shared server component be visited once +// for each server entry and sync/dynamic path that needs to collect client +// imports, while still preventing recursive loops. +type VisitedServerComponents = FxHashSet<(ModuleIdentifier, Option, bool)>; #[derive(Debug, Default)] pub struct ComponentInfo { pub should_inject_ssr_modules: bool, + /// All client component imports reached from this RSC entry. pub client_component_imports: ClientComponentImports, + /// Client component imports reached through a server-side dynamic import. + /// + /// These imports mark modules that must be emitted as independent async + /// blocks in the client compiler, so `import("./Client")` from a server + /// component does not get merged into the synchronous root/server-entry + /// client owner group. + pub async_client_component_imports: ClientComponentImports, + /// Client component imports reached without a `use server-entry` owner. + pub root_client_component_imports: ClientComponentImports, + /// Client component imports reached under each `use server-entry` owner. + pub client_component_imports_by_server_entry: ClientComponentImportsByServerEntry, pub css_imports_by_server_entry: CssImportsByServerEntry, pub root_css_imports: RootCssImports, pub import_meta_rsc_importers: ImportMetaRscImporters, @@ -62,6 +76,7 @@ pub fn collect_component_info_from_entry_dependency( resolved_module.as_ref(), &[], None, + false, &mut visited_client_modules, &mut visited_server_components, &mut component_info, @@ -77,6 +92,7 @@ fn traverse_module( module: &dyn Module, imported_identifiers: &[Atom], current_server_entry: Option<&str>, + is_under_server_dynamic_import: bool, visited_client_modules: &mut IdentifierSet, visited_server_components: &mut VisitedServerComponents, component_info: &mut ComponentInfo, @@ -101,7 +117,7 @@ fn traverse_module( .insert(resource.to_string()); } - if is_css_mod(module) { + if is_css_mod(module, resource.as_ref()) { record_css_import( compilation, module, @@ -119,6 +135,8 @@ fn traverse_module( module, resource.as_ref(), imported_identifiers, + current_server_entry, + is_under_server_dynamic_import, is_first_visit_client_module, component_info, ); @@ -128,6 +146,7 @@ fn traverse_module( let is_first_visit_server_component = visited_server_components.insert(( module.identifier(), current_server_entry.map(ToOwned::to_owned), + is_under_server_dynamic_import, )); if !is_first_visit_server_component { return; @@ -143,6 +162,11 @@ fn traverse_module( continue; }; let imported_ids = get_imported_ids(module_graph, &connection.dependency_id); + // A dependency with a parent block is inside a server-side dynamic import. + // Propagate the flag so client components reached that way keep a matching + // independent async chunk in the client compiler. + let is_under_server_dynamic_import = + is_under_server_dynamic_import || module_graph.get_parent_block(dependency_id).is_some(); let Some(resolved_module) = module_graph.module_by_identifier(&connection.resolved_module) else { @@ -154,6 +178,7 @@ fn traverse_module( resolved_module.as_ref(), &imported_ids, current_server_entry, + is_under_server_dynamic_import, visited_client_modules, visited_server_components, component_info, @@ -203,31 +228,38 @@ fn record_client_component_import( module: &dyn Module, resource: &str, imported_identifiers: &[Atom], + current_server_entry: Option<&str>, + is_under_server_dynamic_import: bool, is_first_visit_client_module: bool, component_info: &mut ComponentInfo, ) { - if is_first_visit_client_module { - component_info + if is_first_visit_client_module + || component_info .client_component_imports - .entry(resource.to_string()) - .or_default(); - add_client_import( - module, - resource, - imported_identifiers, - true, - &mut component_info.client_component_imports, - ); - } else if component_info - .client_component_imports - .contains_key(resource) + .contains_key(resource) { - add_client_import( + if is_under_server_dynamic_import { + add_client_import_to_scope( + module, + resource, + imported_identifiers, + &mut component_info.client_component_imports, + ); + add_client_import_to_scope( + module, + resource, + imported_identifiers, + &mut component_info.async_client_component_imports, + ); + return; + } + + add_client_import_for_server_entry( module, resource, imported_identifiers, - false, - &mut component_info.client_component_imports, + current_server_entry, + component_info, ); } } @@ -277,6 +309,61 @@ fn get_imported_ids(module_graph: &ModuleGraph, dependency_id: &DependencyId) -> } } +fn add_client_import_for_server_entry( + module: &dyn Module, + resource: &str, + imported_identifiers: &[Atom], + current_server_entry: Option<&str>, + component_info: &mut ComponentInfo, +) { + add_client_import_to_scope( + module, + resource, + imported_identifiers, + &mut component_info.client_component_imports, + ); + + let Some(server_entry) = current_server_entry else { + add_client_import_to_scope( + module, + resource, + imported_identifiers, + &mut component_info.root_client_component_imports, + ); + return; + }; + + let client_component_imports = component_info + .client_component_imports_by_server_entry + .entry(server_entry.to_string()) + .or_default(); + add_client_import_to_scope( + module, + resource, + imported_identifiers, + client_component_imports, + ); +} + +fn add_client_import_to_scope( + module: &dyn Module, + mod_request: &str, + imported_identifiers: &[Atom], + client_component_imports: &mut ClientComponentImports, +) { + let is_first_visit_module = !client_component_imports.contains_key(mod_request); + if is_first_visit_module { + client_component_imports.insert(mod_request.to_string(), Default::default()); + } + add_client_import( + module, + mod_request, + imported_identifiers, + is_first_visit_module, + client_component_imports, + ); +} + fn add_client_import( module: &dyn Module, mod_request: &str, diff --git a/crates/rspack_plugin_rsc/src/plugin_state.rs b/crates/rspack_plugin_rsc/src/plugin_state.rs index baeca4cf482f..619dd8637358 100644 --- a/crates/rspack_plugin_rsc/src/plugin_state.rs +++ b/crates/rspack_plugin_rsc/src/plugin_state.rs @@ -16,6 +16,7 @@ use crate::reference_manifest::{ pub type ActionIdNamePair = (Atom, Atom); pub type CssImportsByServerEntry = FxHashMap>; +pub type ClientModulesByServerEntry = FxHashMap>; pub type RootCssImports = FxIndexSet; /// Structured info about a client module to inject into the client compiler. @@ -41,7 +42,14 @@ pub struct ServerEntryState { #[derive(Debug, Default)] pub struct EntryState { pub server_entries: FxHashMap, + /// All client modules discovered from the RSC graph for this entry. pub injected_client_entries: Vec, + /// Client modules that cannot be assigned to exactly one owner and stay as standalone async chunks. + pub ungrouped_client_entries: Vec, + /// Client modules used only by the root RSC tree. + pub root_client_entries: Vec, + /// Client modules used only by one `use server-entry` subtree. + pub client_entries_by_server_entry: ClientModulesByServerEntry, pub client_modules: FxHashMap, /// Root CSS import paths reached through a parent chain without `use server-entry`. /// These are attached directly to the matching client compiler entry. diff --git a/crates/rspack_plugin_rsc/src/rsc_entry_dependency.rs b/crates/rspack_plugin_rsc/src/rsc_entry_dependency.rs index c78a3611f782..17b1cc7337fa 100644 --- a/crates/rspack_plugin_rsc/src/rsc_entry_dependency.rs +++ b/crates/rspack_plugin_rsc/src/rsc_entry_dependency.rs @@ -9,14 +9,21 @@ use rspack_core::{ DependencyType, FactorizeInfo, ModuleDependency, ResourceIdentifier, }; -use crate::plugin_state::{ClientModuleImport, CssImportsByServerEntry}; +use crate::plugin_state::{ + ClientModuleImport, ClientModulesByServerEntry, CssImportsByServerEntry, +}; #[cacheable] #[derive(Debug, Clone)] pub struct RscEntryDependency { id: DependencyId, pub name: Arc, + /// Client modules that should keep the existing one-module-per-async-block behavior. pub client_modules: Vec, + /// Client modules owned by the root RSC entry. + pub root_client_modules: Vec, + #[cacheable(with=AsMap)] + pub client_modules_by_server_entry: ClientModulesByServerEntry, #[cacheable(with=AsMap)] pub css_imports_by_server_entry: CssImportsByServerEntry, /// When true, client modules are loaded eagerly (not as code-split points). @@ -30,6 +37,8 @@ impl RscEntryDependency { pub fn new( name: Arc, client_modules: Vec, + root_client_modules: Vec, + client_modules_by_server_entry: ClientModulesByServerEntry, css_imports_by_server_entry: CssImportsByServerEntry, is_server_side_rendering: bool, ) -> Self { @@ -38,6 +47,8 @@ impl RscEntryDependency { id: DependencyId::new(), name, client_modules, + root_client_modules, + client_modules_by_server_entry, css_imports_by_server_entry, is_server_side_rendering, resource_identifier, diff --git a/crates/rspack_plugin_rsc/src/rsc_entry_module.rs b/crates/rspack_plugin_rsc/src/rsc_entry_module.rs index 8fc568c4b735..6abcba5febaa 100644 --- a/crates/rspack_plugin_rsc/src/rsc_entry_module.rs +++ b/crates/rspack_plugin_rsc/src/rsc_entry_module.rs @@ -11,22 +11,22 @@ use rspack_core::{ AsyncDependenciesBlock, AsyncDependenciesBlockIdentifier, BoxDependency, BoxModule, BuildContext, BuildInfo, BuildMeta, BuildMetaExportsType, BuildResult, CodeGenerationResult, Compilation, Context, DependenciesBlock, Dependency, DependencyId, DependencyRange, FactoryMeta, ImportPhase, - LibIdentOptions, Module, ModuleCodeGenerationContext, ModuleDependency, ModuleGraph, - ModuleIdentifier, ModuleLayer, ModuleType, ReferencedSpecifier, RuntimeSpec, SourceType, - contextify, impl_module_meta_info, impl_source_map_config, module_update_hash, + LibIdentOptions, Module, ModuleCodeGenerationContext, ModuleGraph, ModuleIdentifier, ModuleLayer, + ModuleType, ReferencedSpecifier, RuntimeSpec, SourceType, contextify, impl_module_meta_info, + impl_source_map_config, module_update_hash, rspack_sources::{BoxSource, RawStringSource, SourceExt}, }; use rspack_error::{Result, impl_empty_diagnosable_trait}; use rspack_hash::{RspackHash, RspackHashDigest}; use rspack_plugin_javascript::dependency::ImportEagerDependency; use rspack_util::{fx_hash::FxIndexSet, source_map::SourceMapKind}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; use swc_core::ecma::atoms::Atom; use crate::{ client_reference_dependency::ClientReferenceDependency, constants::LAYERS_NAMES, - plugin_state::{ClientModuleImport, CssImportsByServerEntry}, + plugin_state::{ClientModuleImport, ClientModulesByServerEntry, CssImportsByServerEntry}, }; #[impl_source_map_config] @@ -38,6 +38,9 @@ pub struct RscEntryModule { identifier: ModuleIdentifier, lib_ident: String, client_modules: Vec, + root_client_modules: Vec, + #[cacheable(with=AsMap)] + client_modules_by_server_entry: ClientModulesByServerEntry, #[cacheable(with=AsMap)] css_imports_by_server_entry: CssImportsByServerEntry, name: Arc, @@ -53,6 +56,8 @@ impl RscEntryModule { pub fn new( name: Arc, client_modules: Vec, + root_client_modules: Vec, + client_modules_by_server_entry: ClientModulesByServerEntry, css_imports_by_server_entry: CssImportsByServerEntry, is_server_side_rendering: bool, ) -> Self { @@ -60,6 +65,8 @@ impl RscEntryModule { let identifier = create_identifier( name.as_ref(), &client_modules, + &root_client_modules, + &client_modules_by_server_entry, &css_imports_by_server_entry, is_server_side_rendering, ); @@ -75,6 +82,8 @@ impl RscEntryModule { identifier, lib_ident, client_modules, + root_client_modules, + client_modules_by_server_entry, css_imports_by_server_entry, name, is_server_side_rendering, @@ -94,69 +103,89 @@ impl RscEntryModule { } fn render_debug_comments(&self, compilation: &Compilation) -> String { - let module_graph = compilation.get_module_graph(); - let referenced_exports_by_request = create_referenced_exports_by_request(&self.client_modules); let mut source = String::new(); + let root_chunking = self.debug_chunking("single async block"); + let ungrouped_chunking = self.debug_chunking("one async block per module"); + let server_entry_chunking = self.debug_chunking("one async block per server-entry"); + + if self.is_server_side_rendering + && self.root_client_modules.is_empty() + && self.client_modules_by_server_entry.is_empty() + && self.css_imports_by_server_entry.is_empty() + { + append_client_modules_debug_section( + &mut source, + compilation, + "ssr-eager", + "eager import for SSR", + None, + &self.client_modules, + ); + return source; + } - if self.is_server_side_rendering { - for dep_id in self.get_dependencies() { - let dependency = module_graph.dependency_by_id(dep_id); - let dep = dependency - .downcast_ref::() - .unwrap_or_else(|| { - panic!( - "Expected dependency of eager RscEntryModule to be ImportEagerDependency, got {:?}", - dependency.dependency_type() - ) - }); - append_debug_comment_for_request( - &mut source, - referenced_exports_by_request - .get(dep.request()) - .map(String::as_str), - compilation, - dep.request(), - ); - } + append_client_modules_debug_section( + &mut source, + compilation, + "root", + root_chunking, + None, + &self.root_client_modules, + ); + append_client_modules_debug_section( + &mut source, + compilation, + "ungrouped", + ungrouped_chunking, + None, + &self.client_modules, + ); - return source; + for server_entry in self.debug_server_entries() { + append_server_entry_debug_section( + &mut source, + compilation, + server_entry.as_str(), + server_entry_chunking, + self.css_imports_by_server_entry.get(&server_entry), + self.client_modules_by_server_entry.get(&server_entry), + ); } - for block_id in self.get_blocks() { - let block = module_graph - .block_by_id(block_id) - .expect("should have block"); - - for dependency_id in block.get_dependencies() { - let dependency = module_graph.dependency_by_id(dependency_id); - let dep = dependency - .downcast_ref::() - .unwrap_or_else(|| { - panic!( - "Expected dependency of RscEntryModule to be ClientReferenceDependency, got {:?}", - dependency.dependency_type() - ) - }); - - append_debug_comment_for_request( - &mut source, - referenced_exports_by_request - .get(dep.user_request()) - .map(String::as_str) - .or_else(|| { - self - .css_imports_by_server_entry - .values() - .any(|imports| imports.contains(dep.user_request())) - .then_some("side-effect") - }), - compilation, - dep.user_request(), - ); + source + } + + fn debug_chunking(&self, async_chunking: &'static str) -> &'static str { + if self.is_server_side_rendering { + "eager import for SSR" + } else { + async_chunking + } + } + + fn debug_server_entries(&self) -> Vec { + let mut server_entries = self + .css_imports_by_server_entry + .keys() + .cloned() + .collect::>(); + for server_entry in self.client_modules_by_server_entry.keys() { + if !server_entries.contains(server_entry) { + server_entries.push(server_entry.clone()); } } + server_entries.sort_unstable(); + server_entries + } - source + fn all_client_modules(&self) -> Vec<&ClientModuleImport> { + let mut client_modules = Vec::new(); + client_modules.extend(self.client_modules.iter()); + client_modules.extend(self.root_client_modules.iter()); + for modules in self.client_modules_by_server_entry.values() { + client_modules.extend(modules.iter()); + } + client_modules } } @@ -228,8 +257,9 @@ impl Module for RscEntryModule { ) -> Result { if self.is_server_side_rendering { // Eager: no code-split points; use ImportEagerDependency (CSS filtering done at call site). - let mut dependencies: Vec = Vec::with_capacity(self.client_modules.len()); - for client_module in &self.client_modules { + let all_client_modules = self.all_client_modules(); + let mut dependencies: Vec = Vec::with_capacity(all_client_modules.len()); + for client_module in all_client_modules { let referenced_specifiers = create_referenced_specifiers(&client_module.ids); let dep = ImportEagerDependency::new( Atom::from(client_module.request.as_str()), @@ -248,32 +278,83 @@ impl Module for RscEntryModule { }) } else { // Non-eager: code-split points; use AsyncDependenciesBlock + ClientReferenceDependency. - let mut blocks = - Vec::with_capacity(self.client_modules.len() + self.css_imports_by_server_entry.len()); + let mut blocks = Vec::with_capacity( + self.client_modules.len() + + self.css_imports_by_server_entry.len() + + self.client_modules_by_server_entry.len() + + usize::from(!self.root_client_modules.is_empty()), + ); let dependencies: Vec = vec![]; - for (server_entry, css_imports) in &self.css_imports_by_server_entry { - if css_imports.is_empty() { - continue; + let mut server_entries = self + .css_imports_by_server_entry + .keys() + .cloned() + .collect::>(); + for server_entry in self.client_modules_by_server_entry.keys() { + if !server_entries.contains(server_entry) { + server_entries.push(server_entry.clone()); } + } + server_entries.sort_unstable(); - let dependencies = css_imports - .iter() - .map(|request| { + for server_entry in server_entries { + let mut block_dependencies: Vec = Vec::new(); + + if let Some(css_imports) = self.css_imports_by_server_entry.get(&server_entry) { + block_dependencies.extend(css_imports.iter().map(|request| { Box::new(ClientReferenceDependency::new( request.clone(), Default::default(), self.is_server_side_rendering, )) as Box + })); + } + + if let Some(client_modules) = self.client_modules_by_server_entry.get(&server_entry) { + block_dependencies.extend(client_modules.iter().map(|client_module| { + Box::new(ClientReferenceDependency::new( + client_module.request.clone(), + client_module.ids.clone(), + self.is_server_side_rendering, + )) as Box + })); + } + + if block_dependencies.is_empty() { + continue; + } + + let block_modifier = format!("server-entry={server_entry}"); + let block = AsyncDependenciesBlock::new( + self.identifier, + None, + Some(&block_modifier), + block_dependencies, + Some(server_entry.clone()), + ); + blocks.push(Box::new(block)); + } + + if !self.root_client_modules.is_empty() { + let dependencies = self + .root_client_modules + .iter() + .map(|client_module| { + Box::new(ClientReferenceDependency::new( + client_module.request.clone(), + client_module.ids.clone(), + self.is_server_side_rendering, + )) as Box }) .collect::>(); let block = AsyncDependenciesBlock::new( self.identifier, None, - Some(server_entry.as_str()), + None, dependencies, - Some(server_entry.clone()), + Some(format!("{}#root-client", self.name)), ); blocks.push(Box::new(block)); } @@ -333,6 +414,8 @@ impl_empty_diagnosable_trait!(RscEntryModule); fn create_identifier( name: &str, client_modules: &[ClientModuleImport], + root_client_modules: &[ClientModuleImport], + client_modules_by_server_entry: &ClientModulesByServerEntry, css_imports_by_server_entry: &CssImportsByServerEntry, is_server_side_rendering: bool, ) -> ModuleIdentifier { @@ -342,18 +425,21 @@ fn create_identifier( identifier.push(if is_server_side_rendering { '1' } else { '0' }); identifier.push('|'); - let mut client_modules = client_modules.iter().collect::>(); - client_modules.sort_unstable_by(|a, b| a.request.cmp(&b.request)); - for client_module in client_modules { - push_value(&mut identifier, &client_module.request); + identifier.push_str("ungrouped["); + push_client_modules(&mut identifier, client_modules); + identifier.push_str("]|root["); + push_client_modules(&mut identifier, root_client_modules); + identifier.push_str("]|server["); + let mut client_modules_by_server_entry = + client_modules_by_server_entry.iter().collect::>(); + client_modules_by_server_entry.sort_unstable_by_key(|(server_entry, _)| *server_entry); + for (server_entry, client_modules) in client_modules_by_server_entry { + push_value(&mut identifier, server_entry); identifier.push('['); - - let ids = sorted_strs(client_module.ids.iter().map(|id| id.as_str())); - for id in ids { - push_value(&mut identifier, id); - } + push_client_modules(&mut identifier, client_modules); identifier.push(']'); } + identifier.push(']'); identifier.push('|'); let mut css_imports_by_server_entry = css_imports_by_server_entry.iter().collect::>(); @@ -372,6 +458,21 @@ fn create_identifier( ModuleIdentifier::from(identifier) } +fn push_client_modules(identifier: &mut String, client_modules: &[ClientModuleImport]) { + let mut client_modules = client_modules.iter().collect::>(); + client_modules.sort_unstable_by(|a, b| a.request.cmp(&b.request)); + for client_module in client_modules { + push_value(identifier, &client_module.request); + identifier.push('['); + + let ids = sorted_strs(client_module.ids.iter().map(|id| id.as_str())); + for id in ids { + push_value(identifier, id); + } + identifier.push(']'); + } +} + fn sorted_strs<'a>(values: impl Iterator) -> Vec<&'a str> { let mut values = values.collect::>(); values.sort_unstable(); @@ -397,48 +498,127 @@ fn create_referenced_specifiers(ids: &FxIndexSet) -> Option, client_modules: &[ClientModuleImport], -) -> FxHashMap<&str, String> { - client_modules - .iter() - .map(|client_module| { - let exports = format_referenced_exports(client_module); - (client_module.request.as_str(), exports) - }) - .collect() +) { + if client_modules.is_empty() { + return; + } + + let mut client_modules = client_modules.iter().collect::>(); + client_modules.sort_unstable_by(|a, b| a.request.cmp(&b.request)); + append_debug_section_header( + source, + chunk_group, + chunking, + server_entry + .map(|server_entry| contextify(compilation.options.context.as_path(), server_entry)), + ); + for client_module in client_modules { + let request = contextify( + compilation.options.context.as_path(), + client_module.request.as_str(), + ); + append_debug_module_line(source, &request, &format_referenced_exports(client_module)); + } + append_debug_section_end(source); } -fn append_debug_comment_for_request( +fn append_server_entry_debug_section( source: &mut String, - exports: Option<&str>, compilation: &Compilation, - request: &str, + server_entry: &str, + chunking: &str, + css_imports: Option<&FxIndexSet>, + client_modules: Option<&Vec>, ) { - let request = contextify(compilation.options.context.as_path(), request); - append_debug_comment(source, &request, exports.unwrap_or("unknown")); -} + if css_imports.is_none_or(|css_imports| css_imports.is_empty()) + && client_modules.is_none_or(|client_modules| client_modules.is_empty()) + { + return; + } -fn sanitize_comment_part(value: &str) -> Cow<'_, str> { - if value.contains("*/") { - value.cow_replace("*/", "* /") - } else { - Cow::Borrowed(value) + append_debug_section_header( + source, + "server-entry", + chunking, + Some(contextify( + compilation.options.context.as_path(), + server_entry, + )), + ); + + if let Some(css_imports) = css_imports { + for css_import in sorted_strs(css_imports.iter().map(String::as_str)) { + let request = contextify(compilation.options.context.as_path(), css_import); + append_debug_module_line(source, &request, "side-effect"); + } } + + if let Some(client_modules) = client_modules { + let mut client_modules = client_modules.iter().collect::>(); + client_modules.sort_unstable_by(|a, b| a.request.cmp(&b.request)); + for client_module in client_modules { + let request = contextify( + compilation.options.context.as_path(), + client_module.request.as_str(), + ); + append_debug_module_line(source, &request, &format_referenced_exports(client_module)); + } + } + + append_debug_section_end(source); } -fn append_debug_comment(source: &mut String, request: &str, exports: &str) { +fn append_debug_section_header( + source: &mut String, + chunk_group: &str, + chunking: &str, + server_entry: Option, +) { if !source.is_empty() { source.push('\n'); } - let request = sanitize_comment_part(request); - let exports = sanitize_comment_part(exports); + let chunk_group = sanitize_comment_part(chunk_group); + let chunking = sanitize_comment_part(chunking); write!( source, - "/*!\n * module: {request}\n * exports: {exports}\n */" + "/*!\n * chunk group: {chunk_group}\n * chunking: {chunking}\n" ) .expect("writing debug comments to String should not fail"); + + if let Some(server_entry) = server_entry { + let server_entry = sanitize_comment_part(&server_entry); + writeln!(source, " * server-entry: {server_entry}") + .expect("writing debug comments to String should not fail"); + } + + source.push_str(" * modules:\n"); +} + +fn append_debug_module_line(source: &mut String, request: &str, exports: &str) { + let request = sanitize_comment_part(request); + let exports = sanitize_comment_part(exports); + writeln!(source, " * - {request} (exports: {exports})") + .expect("writing debug comments to String should not fail"); +} + +fn append_debug_section_end(source: &mut String) { + source.push_str(" */"); +} + +fn sanitize_comment_part(value: &str) -> Cow<'_, str> { + if value.contains("*/") { + value.cow_replace("*/", "* /") + } else { + Cow::Borrowed(value) + } } fn format_referenced_exports(client_module: &ClientModuleImport) -> String { diff --git a/crates/rspack_plugin_rsc/src/rsc_entry_module_factory.rs b/crates/rspack_plugin_rsc/src/rsc_entry_module_factory.rs index 6f39fa665df7..5385886f1929 100644 --- a/crates/rspack_plugin_rsc/src/rsc_entry_module_factory.rs +++ b/crates/rspack_plugin_rsc/src/rsc_entry_module_factory.rs @@ -17,6 +17,8 @@ impl ModuleFactory for RscEntryModuleFactory { RscEntryModule::new( dependency.name.clone(), dependency.client_modules.clone(), + dependency.root_client_modules.clone(), + dependency.client_modules_by_server_entry.clone(), dependency.css_imports_by_server_entry.clone(), dependency.is_server_side_rendering, ) diff --git a/crates/rspack_plugin_rsc/src/server_plugin.rs b/crates/rspack_plugin_rsc/src/server_plugin.rs index a9686fb4de66..88fbb58dd564 100644 --- a/crates/rspack_plugin_rsc/src/server_plugin.rs +++ b/crates/rspack_plugin_rsc/src/server_plugin.rs @@ -19,7 +19,8 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ component_info::{ - ClientComponentImports, ImportMetaRscImporters, collect_component_info_from_entry_dependency, + ClientComponentImports, ClientComponentImportsByServerEntry, ImportMetaRscImporters, + collect_component_info_from_entry_dependency, }, constants::{CSS_REGEX, LAYERS_NAMES}, coordinator::Coordinator, @@ -27,8 +28,8 @@ use crate::{ loaders::action_entry_loader::ACTION_ENTRY_LOADER_IDENTIFIER, manifest_runtime_module::RscManifestRuntimeModule, plugin_state::{ - ActionIdNamePair, ClientModuleImport, CssImportsByServerEntry, PLUGIN_STATES, PluginState, - RootCssImports, + ActionIdNamePair, ClientModuleImport, ClientModulesByServerEntry, CssImportsByServerEntry, + PLUGIN_STATES, PluginState, RootCssImports, }, reference_manifest::{ RscCssLinkProps, RscEntryManifest, RscManifest, build_server_consumer_module_map, @@ -43,6 +44,9 @@ struct ClientEntry { entry_name: Arc, runtime: RuntimeSpec, client_imports: ClientComponentImports, + async_client_component_imports: ClientComponentImports, + root_client_imports: ClientComponentImports, + client_imports_by_server_entry: ClientComponentImportsByServerEntry, css_imports_by_server_entry: CssImportsByServerEntry, root_css_imports: RootCssImports, import_meta_rsc_importers: ImportMetaRscImporters, @@ -79,6 +83,87 @@ pub struct RscServerPluginOptions { pub on_manifest: Option, } +fn client_imports_to_modules(client_imports: &ClientComponentImports) -> Vec { + client_imports + .iter() + .map(|(request, ids)| ClientModuleImport { + request: request.clone(), + ids: ids.iter().cloned().collect(), + }) + .collect() +} + +fn group_client_entries_by_owner( + client_imports: &ClientComponentImports, + async_client_component_imports: &ClientComponentImports, + root_client_imports: &ClientComponentImports, + client_imports_by_server_entry: &ClientComponentImportsByServerEntry, +) -> ( + Vec, + Vec, + ClientModulesByServerEntry, +) { + let mut ungrouped_client_entries = Vec::new(); + let mut root_client_entries = Vec::new(); + let mut client_entries_by_server_entry: ClientModulesByServerEntry = Default::default(); + + for (request, ids) in client_imports { + let client_module = ClientModuleImport { + request: request.clone(), + ids: ids.iter().cloned().collect(), + }; + + // Preserve server-side dynamic import boundaries in the client compiler: + // `RscEntryModule` emits ungrouped modules as one async block per module, + // so a server `import("./Client")` keeps a matching client chunk instead + // of being merged into the synchronous root/server-entry owner group. + // The `client_module` above uses ids from `client_imports`, which keeps + // the merged export set when the same module is reached by multiple paths. + if async_client_component_imports.contains_key(request.as_str()) { + ungrouped_client_entries.push(client_module); + continue; + } + + let mut owner_count = 0usize; + let is_root_owned = root_client_imports.contains_key(request.as_str()); + let mut owned_server_entry = None; + + if is_root_owned { + owner_count += 1; + } + + for (server_entry, client_imports) in client_imports_by_server_entry { + if client_imports.contains_key(request.as_str()) { + owner_count += 1; + if owned_server_entry.is_none() { + owned_server_entry = Some(server_entry); + } + } + } + + if owner_count == 1 { + if is_root_owned { + root_client_entries.push(client_module); + } else if let Some(server_entry) = owned_server_entry { + client_entries_by_server_entry + .entry(server_entry.clone()) + .or_default() + .push(client_module); + } else { + ungrouped_client_entries.push(client_module); + } + } else { + ungrouped_client_entries.push(client_module); + } + } + + ( + ungrouped_client_entries, + root_client_entries, + client_entries_by_server_entry, + ) +} + #[plugin] #[derive(Debug)] pub struct RscServerPlugin { @@ -276,6 +361,9 @@ impl RscServerPlugin { entry_name: entry_name.clone(), runtime: runtime.clone(), client_imports: component_info.client_component_imports, + async_client_component_imports: component_info.async_client_component_imports, + root_client_imports: component_info.root_client_component_imports, + client_imports_by_server_entry: component_info.client_component_imports_by_server_entry, css_imports_by_server_entry: component_info.css_imports_by_server_entry, root_css_imports: component_info.root_css_imports, import_meta_rsc_importers: component_info.import_meta_rsc_importers, @@ -472,13 +560,24 @@ impl RscServerPlugin { entry_name, runtime, client_imports, + async_client_component_imports, + root_client_imports, + client_imports_by_server_entry, css_imports_by_server_entry, root_css_imports, import_meta_rsc_importers, } = client_entry; - let client_entries = { - let mut modules = Vec::new(); + let client_entries = client_imports_to_modules(&client_imports); + let (ungrouped_client_entries, root_client_entries, client_entries_by_server_entry) = + group_client_entries_by_owner( + &client_imports, + &async_client_component_imports, + &root_client_imports, + &client_imports_by_server_entry, + ); + + { let entry_state = plugin_state.entries.entry(entry_name.clone()).or_default(); for (server_entry, css_imports) in css_imports_by_server_entry { entry_state @@ -498,23 +597,24 @@ impl RscServerPlugin { } entry_state.root_css_imports.extend(root_css_imports); - for (request, ids) in &client_imports { - modules.push(ClientModuleImport { - request: request.clone(), - ids: ids.iter().cloned().collect(), - }); - } - modules - }; + // These grouped client entries are only used to shape client compiler + // async blocks. Do not create a `server_entries[server_entry]` record + // from `client_entries_by_server_entry` alone: a server entry that only + // owns client components has no server CSS block to record here. + // + // Client component CSS is collected from the client module chunks and + // stored on `clientManifest[*].cssFiles` when that client module is + // recorded. `server_entries` is intentionally reserved for server CSS + // imports and `import.meta.rspackRsc.loadCss()` data, which are the only + // sources that should populate `entryCssFiles`. + entry_state.injected_client_entries = client_entries.clone(); + entry_state.ungrouped_client_entries = ungrouped_client_entries; + entry_state.root_client_entries = root_client_entries; + entry_state.client_entries_by_server_entry = client_entries_by_server_entry; + } // Add for the client compilation // Inject the entry to the client compiler. - plugin_state - .entries - .entry(entry_name.clone()) - .or_default() - .injected_client_entries = client_entries.clone(); - if !should_inject_ssr_modules { return None; } @@ -532,6 +632,8 @@ impl RscServerPlugin { entry_name.clone(), client_entries_for_ssr, Default::default(), + Default::default(), + Default::default(), true, ); let dependency_id = *(ssr_entry_dependency.id()); diff --git a/crates/rspack_plugin_rsc/src/utils.rs b/crates/rspack_plugin_rsc/src/utils.rs index 87e4167ff0c1..e792056365fa 100644 --- a/crates/rspack_plugin_rsc/src/utils.rs +++ b/crates/rspack_plugin_rsc/src/utils.rs @@ -29,15 +29,14 @@ pub fn get_module_resource<'a>(module: &'a dyn Module) -> Cow<'a, str> { } } -pub fn is_css_mod(module: &dyn Module) -> bool { +pub fn is_css_mod(module: &dyn Module, resource: &str) -> bool { if matches!( module.module_type(), ModuleType::Css | ModuleType::CssModule | ModuleType::CssAuto ) { return true; } - let resource = get_module_resource(module); - CSS_REGEX.is_match(resource.as_ref()) + CSS_REGEX.is_match(resource) } pub struct ChunkModules<'a> { diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/rspack.config.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/rspack.config.js new file mode 100644 index 000000000000..3fb3565eb93c --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/rspack.config.js @@ -0,0 +1,269 @@ +const path = require('node:path'); +const { experiments } = require('@rspack/core'); + +const { createPlugins, Layers } = experiments.rsc; +const { ServerPlugin, ClientPlugin } = createPlugins(); + +const ssrEntry = path.join(__dirname, 'src/framework/entry.ssr.js'); +const rscEntry = path.join(__dirname, 'src/framework/entry.rsc.js'); + +const pageOnePath = path.join(__dirname, 'src/pages/PageOne.js'); +const pageTwoPath = path.join(__dirname, 'src/pages/PageTwo.js'); +const clientPaths = { + pageOneA: path.join(__dirname, 'src/clients/PageOneClientA.js'), + pageOneB: path.join(__dirname, 'src/clients/PageOneClientB.js'), + pageOneDynamic: path.join(__dirname, 'src/clients/PageOneDynamicClient.js'), + pageTwo: path.join(__dirname, 'src/clients/PageTwoClient.js'), + rootA: path.join(__dirname, 'src/clients/RootOnlyA.js'), + rootB: path.join(__dirname, 'src/clients/RootOnlyB.js'), + sharedAcrossPages: path.join(__dirname, 'src/clients/SharedAcrossPages.js'), + sharedRootAndPage: path.join(__dirname, 'src/clients/SharedRootAndPage.js'), + sharedServerChild: path.join( + __dirname, + 'src/clients/SharedServerChildClient.js', + ), +}; + +const swcLoaderRule = { + test: /\.jsx?$/, + use: [ + { + loader: 'builtin:swc-loader', + options: { + detectSyntax: 'auto', + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + rspackExperiments: { + reactServerComponents: true, + }, + }, + }, + ], +}; + +const cssRule = { + test: /\.css$/, + type: 'css/auto', +}; + +function readAsset(compilation, file) { + return compilation.getAsset(file).source.source().toString(); +} + +function findCssAsset(compilation, marker) { + const cssAsset = compilation + .getAssets() + .filter(({ name }) => name.endsWith('.css')) + .find(({ source }) => source.source().toString().includes(marker)); + expect(cssAsset).toBeDefined(); + return cssAsset.name; +} + +function expectSameChunks(a, b) { + expect(a.chunks).toEqual(b.chunks); +} + +function expectDifferentChunks(a, b) { + expect(a.chunks).not.toEqual(b.chunks); +} + +module.exports = [ + { + mode: 'development', + target: 'node', + entry: { + main: { + import: ssrEntry, + }, + }, + resolve: { + extensions: ['...', '.ts', '.tsx', '.jsx'], + }, + module: { + rules: [ + cssRule, + swcLoaderRule, + { + resource: ssrEntry, + layer: Layers.ssr, + }, + { + resource: rscEntry, + layer: Layers.rsc, + resolve: { + conditionNames: ['react-server', '...'], + }, + }, + { + issuerLayer: Layers.rsc, + resolve: { + conditionNames: ['react-server', '...'], + }, + }, + ], + }, + plugins: [ + new ServerPlugin({ + onManifest(manifest) { + const mainEntry = manifest.main; + expect(mainEntry).toBeDefined(); + + const getClient = (request) => { + const client = mainEntry.clientManifest[request]; + expect(client).toBeDefined(); + return client; + }; + + const pageOneA = getClient(clientPaths.pageOneA); + const pageOneB = getClient(clientPaths.pageOneB); + const pageOneDynamic = getClient(clientPaths.pageOneDynamic); + const pageTwo = getClient(clientPaths.pageTwo); + const rootA = getClient(clientPaths.rootA); + const rootB = getClient(clientPaths.rootB); + const sharedAcrossPages = getClient(clientPaths.sharedAcrossPages); + const sharedRootAndPage = getClient(clientPaths.sharedRootAndPage); + const sharedServerChild = getClient(clientPaths.sharedServerChild); + + expectSameChunks(pageOneA, pageOneB); + expectDifferentChunks(pageOneA, pageOneDynamic); + expectDifferentChunks(pageOneA, sharedServerChild); + expectDifferentChunks(pageOneA, pageTwo); + + expectSameChunks(rootA, rootB); + expectDifferentChunks(rootA, pageOneA); + + expectDifferentChunks(sharedAcrossPages, pageOneA); + expectDifferentChunks(sharedAcrossPages, pageTwo); + expectDifferentChunks(sharedRootAndPage, rootA); + expectDifferentChunks(sharedRootAndPage, pageOneA); + + expect( + Object.keys(mainEntry.clientManifest).filter( + (request) => request === clientPaths.sharedAcrossPages, + ), + ).toHaveLength(1); + expect( + Object.keys(mainEntry.clientManifest).filter( + (request) => request === clientPaths.sharedRootAndPage, + ), + ).toHaveLength(1); + + expect(pageOneA.cssFiles).toBeDefined(); + expect(pageOneB.cssFiles).toBeDefined(); + expect(pageOneDynamic.cssFiles).toBeDefined(); + expect(sharedServerChild.cssFiles).toBeDefined(); + expect(pageTwo.cssFiles).toBeDefined(); + expect(mainEntry.entryCssFiles[pageOnePath]).toBeDefined(); + expect(mainEntry.entryCssFiles[pageTwoPath]).toBeDefined(); + + expect(mainEntry.entryCssFiles[pageOnePath]).toEqual( + pageOneA.cssFiles, + ); + expect(mainEntry.entryCssFiles[pageOnePath]).toEqual( + pageOneB.cssFiles, + ); + expect(mainEntry.entryCssFiles[pageOnePath]).not.toEqual( + pageOneDynamic.cssFiles, + ); + expect(mainEntry.entryCssFiles[pageOnePath]).not.toEqual( + sharedServerChild.cssFiles, + ); + expect(mainEntry.entryCssFiles[pageTwoPath]).toEqual( + pageTwo.cssFiles, + ); + }, + }), + ], + optimization: { + moduleIds: 'named', + chunkIds: 'named', + }, + }, + { + mode: 'development', + target: 'web', + entry: { + main: { + import: './src/framework/entry.client.js', + }, + }, + resolve: { + extensions: ['...', '.ts', '.tsx', '.jsx'], + }, + module: { + rules: [cssRule, swcLoaderRule], + }, + plugins: [ + new ClientPlugin(), + (compiler) => { + compiler.hooks.done.tap('AssertRscClientChunkGrouping', (stats) => { + const { compilation } = stats; + + const pageOneCssFile = findCssAsset( + compilation, + 'page-one-server-css', + ); + const pageOneCss = readAsset(compilation, pageOneCssFile); + expect(pageOneCss).toContain('page-one-client-a-css'); + expect(pageOneCss).toContain('page-one-client-b-css'); + expect(pageOneCss).not.toContain('page-one-dynamic-client-css'); + expect(pageOneCss).not.toContain('shared-server-child-client-css'); + expect(pageOneCss).not.toContain('page-two-client-css'); + expect(pageOneCss).not.toContain('shared-across-pages-client-css'); + + const pageOneDynamicCssFile = findCssAsset( + compilation, + 'page-one-dynamic-client-css', + ); + expect(pageOneDynamicCssFile).not.toBe(pageOneCssFile); + + const sharedServerChildCssFile = findCssAsset( + compilation, + 'shared-server-child-client-css', + ); + expect(sharedServerChildCssFile).not.toBe(pageOneCssFile); + + const pageTwoCssFile = findCssAsset( + compilation, + 'page-two-server-css', + ); + const pageTwoCss = readAsset(compilation, pageTwoCssFile); + expect(pageTwoCssFile).not.toBe(pageOneCssFile); + expect(pageTwoCss).toContain('page-two-client-css'); + expect(pageTwoCss).not.toContain('page-one-client-a-css'); + expect(pageTwoCss).not.toContain('shared-across-pages-client-css'); + + const rootCssFile = findCssAsset(compilation, 'root-client-a-css'); + const rootCss = readAsset(compilation, rootCssFile); + expect(rootCssFile).not.toBe(pageOneCssFile); + expect(rootCssFile).not.toBe(pageTwoCssFile); + expect(rootCss).toContain('root-client-b-css'); + expect(rootCss).not.toContain('page-one-server-css'); + + const sharedAcrossPagesCssFile = findCssAsset( + compilation, + 'shared-across-pages-client-css', + ); + expect(sharedAcrossPagesCssFile).not.toBe(pageOneCssFile); + expect(sharedAcrossPagesCssFile).not.toBe(pageTwoCssFile); + + const sharedRootAndPageCssFile = findCssAsset( + compilation, + 'shared-root-page-client-css', + ); + expect(sharedRootAndPageCssFile).not.toBe(rootCssFile); + expect(sharedRootAndPageCssFile).not.toBe(pageOneCssFile); + }); + }, + ], + optimization: { + moduleIds: 'named', + chunkIds: 'named', + }, + }, +]; diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/Root.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/Root.js new file mode 100644 index 000000000000..a586ec58a871 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/Root.js @@ -0,0 +1,17 @@ +import { RootOnlyA } from './clients/RootOnlyA'; +import { RootOnlyB } from './clients/RootOnlyB'; +import { SharedRootAndPage } from './clients/SharedRootAndPage'; +import { PageOne } from './pages/PageOne'; +import { PageTwo } from './pages/PageTwo'; + +export const Root = async () => { + return ( +
+ + + + + +
+ ); +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientA.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientA.css new file mode 100644 index 000000000000..6def066c702e --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientA.css @@ -0,0 +1,3 @@ +.page-one-client-a-css { + color: dodgerblue; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientA.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientA.js new file mode 100644 index 000000000000..150b9d68b328 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientA.js @@ -0,0 +1,7 @@ +"use client"; + +import './PageOneClientA.css'; + +export function PageOneClientA() { + return ; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientB.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientB.css new file mode 100644 index 000000000000..95311ff62349 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientB.css @@ -0,0 +1,3 @@ +.page-one-client-b-css { + color: royalblue; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientB.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientB.js new file mode 100644 index 000000000000..51676eb2df0b --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneClientB.js @@ -0,0 +1,7 @@ +"use client"; + +import './PageOneClientB.css'; + +export function PageOneClientB() { + return ; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneDynamicClient.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneDynamicClient.css new file mode 100644 index 000000000000..974963c2b005 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneDynamicClient.css @@ -0,0 +1,3 @@ +.page-one-dynamic-client-css { + color: rgb(247, 190, 87); +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneDynamicClient.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneDynamicClient.js new file mode 100644 index 000000000000..682f80460641 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageOneDynamicClient.js @@ -0,0 +1,9 @@ +"use client"; + +import './PageOneDynamicClient.css'; + +export function PageOneDynamicClient() { + return ( + + ); +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageTwoClient.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageTwoClient.css new file mode 100644 index 000000000000..85135df12596 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageTwoClient.css @@ -0,0 +1,3 @@ +.page-two-client-css { + color: darkorange; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageTwoClient.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageTwoClient.js new file mode 100644 index 000000000000..3d813cc87642 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/PageTwoClient.js @@ -0,0 +1,7 @@ +"use client"; + +import './PageTwoClient.css'; + +export function PageTwoClient() { + return ; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyA.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyA.css new file mode 100644 index 000000000000..d381c2cb7494 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyA.css @@ -0,0 +1,3 @@ +.root-client-a-css { + color: seagreen; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyA.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyA.js new file mode 100644 index 000000000000..c74e9f5d75ad --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyA.js @@ -0,0 +1,7 @@ +"use client"; + +import './RootOnlyA.css'; + +export function RootOnlyA() { + return ; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyB.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyB.css new file mode 100644 index 000000000000..22a8280f23c4 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyB.css @@ -0,0 +1,3 @@ +.root-client-b-css { + color: mediumseagreen; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyB.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyB.js new file mode 100644 index 000000000000..7acecc149960 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/RootOnlyB.js @@ -0,0 +1,7 @@ +"use client"; + +import './RootOnlyB.css'; + +export function RootOnlyB() { + return ; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedAcrossPages.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedAcrossPages.css new file mode 100644 index 000000000000..35e7bf4618c7 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedAcrossPages.css @@ -0,0 +1,3 @@ +.shared-across-pages-client-css { + color: purple; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedAcrossPages.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedAcrossPages.js new file mode 100644 index 000000000000..2399cf0a3089 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedAcrossPages.js @@ -0,0 +1,9 @@ +"use client"; + +import './SharedAcrossPages.css'; + +export function SharedAcrossPages() { + return ( + + ); +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedRootAndPage.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedRootAndPage.css new file mode 100644 index 000000000000..42dea008aade --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedRootAndPage.css @@ -0,0 +1,3 @@ +.shared-root-page-client-css { + color: teal; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedRootAndPage.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedRootAndPage.js new file mode 100644 index 000000000000..84ff2e71b502 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedRootAndPage.js @@ -0,0 +1,9 @@ +"use client"; + +import './SharedRootAndPage.css'; + +export function SharedRootAndPage() { + return ( + + ); +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedServerChildClient.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedServerChildClient.css new file mode 100644 index 000000000000..0851bb79323b --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedServerChildClient.css @@ -0,0 +1,3 @@ +.shared-server-child-client-css { + color: rgb(108, 196, 161); +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedServerChildClient.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedServerChildClient.js new file mode 100644 index 000000000000..fbd1d5406b73 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/clients/SharedServerChildClient.js @@ -0,0 +1,11 @@ +"use client"; + +import './SharedServerChildClient.css'; + +export function SharedServerChildClient() { + return ( + + ); +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.client.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.client.js new file mode 100644 index 000000000000..896a2a69715c --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.client.js @@ -0,0 +1 @@ +// This entry mirrors the client compiler half of an RSC app. diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.rsc.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.rsc.js new file mode 100644 index 000000000000..fe0ad44127c8 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.rsc.js @@ -0,0 +1,10 @@ +import { renderToReadableStream } from 'react-server-dom-rspack/server'; +import { Root } from '../Root'; + +export const renderRscStream = () => { + return renderToReadableStream(); +}; + +it('should build the RSC client chunk grouping fixture', () => { + expect(typeof renderRscStream).toBe('function'); +}); diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.ssr.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.ssr.js new file mode 100644 index 000000000000..1b979fb4b288 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/framework/entry.ssr.js @@ -0,0 +1,7 @@ +import { createFromReadableStream } from 'react-server-dom-rspack/client'; +import { renderRscStream } from './entry.rsc'; + +export const renderHTML = async () => { + const rscStream = await renderRscStream(); + return createFromReadableStream(rscStream); +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageOne.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageOne.css new file mode 100644 index 000000000000..654d2e8987cd --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageOne.css @@ -0,0 +1,3 @@ +.page-one-server-css { + color: steelblue; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageOne.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageOne.js new file mode 100644 index 000000000000..0897d3d5be4f --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageOne.js @@ -0,0 +1,27 @@ +"use server-entry"; + +import { PageOneClientA } from '../clients/PageOneClientA'; +import { PageOneClientB } from '../clients/PageOneClientB'; +import { SharedAcrossPages } from '../clients/SharedAcrossPages'; +import { SharedRootAndPage } from '../clients/SharedRootAndPage'; +import { SharedServerChild } from '../server/SharedServerChild'; +import './PageOne.css'; + +export const PageOne = async () => { + const { PageOneDynamicClient } = await import('../clients/PageOneDynamicClient'); + const { SharedServerChild: DynamicSharedServerChild } = await import( + '../server/SharedServerChild' + ); + + return ( +
+ + + + + + + +
+ ); +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageTwo.css b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageTwo.css new file mode 100644 index 000000000000..c9ffa720c355 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageTwo.css @@ -0,0 +1,3 @@ +.page-two-server-css { + color: crimson; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageTwo.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageTwo.js new file mode 100644 index 000000000000..b53f56bf7ce0 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/pages/PageTwo.js @@ -0,0 +1,14 @@ +"use server-entry"; + +import { PageTwoClient } from '../clients/PageTwoClient'; +import { SharedAcrossPages } from '../clients/SharedAcrossPages'; +import './PageTwo.css'; + +export const PageTwo = async () => { + return ( +
+ + +
+ ); +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/server/SharedServerChild.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/server/SharedServerChild.js new file mode 100644 index 000000000000..fd01adb896a0 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/src/server/SharedServerChild.js @@ -0,0 +1,5 @@ +import { SharedServerChildClient } from '../clients/SharedServerChildClient'; + +export function SharedServerChild() { + return ; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/test.config.js b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/test.config.js new file mode 100644 index 000000000000..d4923883e5ac --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/client-chunk-grouping/test.config.js @@ -0,0 +1,6 @@ +/** @type {import("../../../..").TConfigCaseConfig} */ +module.exports = { + findBundle: function () { + return ['bundle0.js']; + }, +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/__snapshot__/rsc-entry-debug-comments.txt b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/__snapshot__/rsc-entry-debug-comments.txt index 61c34bb65432..1d2e8d279338 100644 --- a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/__snapshot__/rsc-entry-debug-comments.txt +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/__snapshot__/rsc-entry-debug-comments.txt @@ -1,17 +1,44 @@ server "use strict"; /*! - * module: ./src/Client.js - * exports: Client + * chunk group: ssr-eager + * chunking: eager import for SSR + * modules: + * - ./src/clients/PageOneClient.js (exports: PageOneClient) + * - ./src/clients/PageTwoClient.js (exports: PageTwoClient) + * - ./src/clients/RootClient.js (exports: RootClient) + * - ./src/clients/SharedAcrossPages.js (exports: *) + * - ./src/clients/SharedRootAndPage.js (exports: *) */ client "use strict"; /*! - * module: ./src/App.css - * exports: side-effect + * chunk group: root + * chunking: single async block + * modules: + * - ./src/clients/RootClient.js (exports: RootClient) */ /*! - * module: ./src/Client.js - * exports: Client + * chunk group: ungrouped + * chunking: one async block per module + * modules: + * - ./src/clients/SharedAcrossPages.js (exports: *) + * - ./src/clients/SharedRootAndPage.js (exports: *) + */ +/*! + * chunk group: server-entry + * chunking: one async block per server-entry + * server-entry: ./src/pages/PageOne.js + * modules: + * - ./src/pages/PageOne.css (exports: side-effect) + * - ./src/clients/PageOneClient.js (exports: PageOneClient) + */ +/*! + * chunk group: server-entry + * chunking: one async block per server-entry + * server-entry: ./src/pages/PageTwo.js + * modules: + * - ./src/pages/PageTwo.css (exports: side-effect) + * - ./src/clients/PageTwoClient.js (exports: PageTwoClient) */ \ No newline at end of file diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/App.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/App.js index 139b31952910..4d61be3fb8e8 100644 --- a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/App.js +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/App.js @@ -1,8 +1,15 @@ -'use server-entry'; - -import './App.css'; -import { Client } from './Client'; +import { RootClient } from './clients/RootClient'; +import { SharedRootAndPage } from './clients/SharedRootAndPage'; +import { PageOne } from './pages/PageOne'; +import { PageTwo } from './pages/PageTwo'; export const App = () => { - return ; + return ( + <> + + + + + + ); }; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/Client.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/Client.js deleted file mode 100644 index 66940c1e1f3d..000000000000 --- a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/Client.js +++ /dev/null @@ -1,5 +0,0 @@ -'use client'; - -export const Client = () => { - return ; -}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/PageOneClient.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/PageOneClient.js new file mode 100644 index 000000000000..3016668e939f --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/PageOneClient.js @@ -0,0 +1,5 @@ +'use client'; + +export const PageOneClient = () => { + return ; +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/PageTwoClient.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/PageTwoClient.js new file mode 100644 index 000000000000..30183ab0582c --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/PageTwoClient.js @@ -0,0 +1,5 @@ +'use client'; + +export const PageTwoClient = () => { + return ; +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/RootClient.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/RootClient.js new file mode 100644 index 000000000000..44d3570ae138 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/RootClient.js @@ -0,0 +1,5 @@ +'use client'; + +export const RootClient = () => { + return ; +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/SharedAcrossPages.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/SharedAcrossPages.js new file mode 100644 index 000000000000..5c7970e353d0 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/SharedAcrossPages.js @@ -0,0 +1,5 @@ +'use client'; + +export const SharedAcrossPages = () => { + return ; +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/SharedRootAndPage.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/SharedRootAndPage.js new file mode 100644 index 000000000000..7fc7cd1e1655 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/clients/SharedRootAndPage.js @@ -0,0 +1,5 @@ +'use client'; + +export const SharedRootAndPage = () => { + return ; +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/App.css b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageOne.css similarity index 63% rename from tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/App.css rename to tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageOne.css index 629e44411cdc..271f55a5d2bd 100644 --- a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/App.css +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageOne.css @@ -1,3 +1,3 @@ -.app { +.page-one { color: seagreen; } diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageOne.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageOne.js new file mode 100644 index 000000000000..4efdee9cab4b --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageOne.js @@ -0,0 +1,16 @@ +'use server-entry'; + +import { PageOneClient } from '../clients/PageOneClient'; +import { SharedAcrossPages } from '../clients/SharedAcrossPages'; +import { SharedRootAndPage } from '../clients/SharedRootAndPage'; +import './PageOne.css'; + +export const PageOne = () => { + return ( +
+ + + +
+ ); +}; diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageTwo.css b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageTwo.css new file mode 100644 index 000000000000..9eaaf9219617 --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageTwo.css @@ -0,0 +1,3 @@ +.page-two { + color: rebeccapurple; +} diff --git a/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageTwo.js b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageTwo.js new file mode 100644 index 000000000000..73cb6301d58f --- /dev/null +++ b/tests/rspack-test/configCases/rsc-plugin/rsc-entry-debug-comments/src/pages/PageTwo.js @@ -0,0 +1,14 @@ +'use server-entry'; + +import { PageTwoClient } from '../clients/PageTwoClient'; +import { SharedAcrossPages } from '../clients/SharedAcrossPages'; +import './PageTwo.css'; + +export const PageTwo = () => { + return ( +
+ + +
+ ); +};