diff --git a/Cargo.lock b/Cargo.lock index bccecc3e5e2..8bcf9abf7cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3617,6 +3617,7 @@ dependencies = [ "async-trait", "bitflags 2.5.0", "dashmap", + "futures", "indexmap 2.2.6", "itertools", "linked_hash_set", diff --git a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs index 9864f1910fa..e1b1f0ff706 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs @@ -73,10 +73,16 @@ use rspack_plugin_web_worker_template::web_worker_template_plugin; use rspack_plugin_worker::WorkerPlugin; pub use self::{ - raw_banner::RawBannerPluginOptions, raw_copy::RawCopyRspackPluginOptions, - raw_html::RawHtmlRspackPluginOptions, raw_ignore::RawIgnorePluginOptions, - raw_limit_chunk_count::RawLimitChunkCountPluginOptions, raw_mf::RawContainerPluginOptions, - raw_progress::RawProgressPluginOptions, raw_rsc::RawRSCClientEntryRspackPluginOptions, + raw_banner::RawBannerPluginOptions, + raw_copy::RawCopyRspackPluginOptions, + raw_html::RawHtmlRspackPluginOptions, + raw_ignore::RawIgnorePluginOptions, + raw_limit_chunk_count::RawLimitChunkCountPluginOptions, + raw_mf::RawContainerPluginOptions, + raw_progress::RawProgressPluginOptions, + raw_rsc::{ + RawRSCClientEntryRspackPluginOptions, RawRSCClientReferenceManifestRspackPluginOptions, + }, raw_swc_js_minimizer::RawSwcJsMinimizerRspackPluginOptions, }; use self::{ @@ -497,7 +503,9 @@ impl BuiltinPlugin { plugins.push(RSCClientEntryRspackPlugin::new(plugin_options.into()).boxed()) } BuiltinPluginName::RSCClientReferenceManifestRspackPlugin => { - plugins.push(RSCClientReferenceManifestRspackPlugin::default().boxed()) + let plugin_options: RawRSCClientReferenceManifestRspackPluginOptions = + downcast_into::(self.options)?; + plugins.push(RSCClientReferenceManifestRspackPlugin::new(plugin_options.into()).boxed()) } } Ok(()) diff --git a/crates/rspack_binding_options/src/options/raw_builtins/raw_rsc.rs b/crates/rspack_binding_options/src/options/raw_builtins/raw_rsc.rs index ea6bc48a045..b1a54cc70a7 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/raw_rsc.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/raw_rsc.rs @@ -1,7 +1,7 @@ use napi_derive::napi; -use rspack_plugin_rsc::rsc_client_entry_rspack_plugin::{ - RSCClientEntryRspackPluginOptions, ReactRoute, -}; +use rspack_plugin_rsc::rsc_client_entry_rspack_plugin::RSCClientEntryRspackPluginOptions; +use rspack_plugin_rsc::rsc_client_reference_manifest_rspack_plugin::RSCClientReferenceManifestRspackPluginOptions; +use rspack_plugin_rsc::ReactRoute; use serde::Deserialize; use serde::Serialize; @@ -30,6 +30,22 @@ pub struct RawRSCClientReferenceManifestRspackPluginOptions { pub routes: Option>, } +impl From + for RSCClientReferenceManifestRspackPluginOptions +{ + fn from(value: RawRSCClientReferenceManifestRspackPluginOptions) -> Self { + let raw_routes = value.routes.unwrap_or_default(); + let routes: Vec = raw_routes + .into_iter() + .map(|route| ReactRoute { + name: route.name, + import: route.import, + }) + .collect(); + RSCClientReferenceManifestRspackPluginOptions { routes } + } +} + impl From for RSCClientEntryRspackPluginOptions { fn from(value: RawRSCClientEntryRspackPluginOptions) -> Self { let raw_routes = value.routes.unwrap_or_default(); diff --git a/crates/rspack_plugin_rsc/Cargo.toml b/crates/rspack_plugin_rsc/Cargo.toml index 32aea508f90..673cbff02d5 100644 --- a/crates/rspack_plugin_rsc/Cargo.toml +++ b/crates/rspack_plugin_rsc/Cargo.toml @@ -9,6 +9,7 @@ version = "0.1.0" async-trait = { workspace = true } bitflags = { workspace = true } dashmap = { workspace = true } +futures = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } linked_hash_set = { workspace = true } diff --git a/crates/rspack_plugin_rsc/src/lib.rs b/crates/rspack_plugin_rsc/src/lib.rs index ab926d7f883..9dc8054d1b5 100644 --- a/crates/rspack_plugin_rsc/src/lib.rs +++ b/crates/rspack_plugin_rsc/src/lib.rs @@ -8,7 +8,7 @@ mod plugin; pub use crate::loader::*; pub use crate::plugin::*; pub use crate::utils::{ - decl::RSCAdditionalData, export_visitor, has_client_directive, rsc_visitor, + decl::RSCAdditionalData, decl::ReactRoute, export_visitor, has_client_directive, rsc_visitor, }; mod loader; mod utils; diff --git a/crates/rspack_plugin_rsc/src/loader/rsc_client_entry_loader.rs b/crates/rspack_plugin_rsc/src/loader/rsc_client_entry_loader.rs index 5c7ed0afa99..2ce98f82f86 100644 --- a/crates/rspack_plugin_rsc/src/loader/rsc_client_entry_loader.rs +++ b/crates/rspack_plugin_rsc/src/loader/rsc_client_entry_loader.rs @@ -5,19 +5,20 @@ use std::{ use indexmap::set::IndexSet; use itertools::Itertools; +use once_cell::sync::Lazy; +use regex::Regex; use rspack_core::{Mode, RunnerContext}; use rspack_error::Result; use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; use serde::{Deserialize, Serialize}; +use url::form_urlencoded; -use crate::{utils::shared_data::SHARED_CLIENT_IMPORTS, ReactRoute}; +use crate::utils::shared_data::SHARED_CLIENT_IMPORTS; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RSCClientEntryLoaderOptions { - entry: HashMap, root: String, - routes: Option>, } #[derive(Debug)] @@ -25,6 +26,15 @@ pub struct RSCClientEntryLoader { identifier: Identifier, options: RSCClientEntryLoaderOptions, } +#[derive(Debug, Clone, Default)] +struct QueryParsedRequest { + pub is_client_entry: bool, + pub is_route_entry: bool, + pub chunk_name: String, +} + +static RSC_CLIENT_ENTRY_RE: Lazy = + Lazy::new(|| Regex::new(r"rsc-client-entry-loader").expect("regexp init failed")); impl RSCClientEntryLoader { pub fn new(options: RSCClientEntryLoaderOptions) -> Self { @@ -34,74 +44,44 @@ impl RSCClientEntryLoader { } } - pub fn get_routes_code(&self) -> String { - if let Some(routes) = self.options.routes.as_ref() { - let code = routes - .iter() - .map(|f| { - format!( - r#"import(/* webpackChunkName: "{}" */ "{}");"#, - f.name, f.import - ) - }) - .join("\n"); - code - } else { - String::from("") - } + pub fn get_client_imports_by_name(&self, chunk_name: &str) -> Option> { + let all_client_imports = &SHARED_CLIENT_IMPORTS.lock().unwrap(); + let client_imports = all_client_imports.get(&String::from(chunk_name)).cloned(); + client_imports } - pub fn get_entry_chunk_name(&self, resource_path: &str) -> Option { - let result = self - .options - .entry - .clone() - .into_iter() - .find(|(_, path)| path == resource_path); - let chunk_name = if let Some(result) = result { - let resolved_name = if result.0 == "client-entry" { - String::from("server-entry") - } else { - result.0 - }; - Some(resolved_name) - } else { - None - }; - chunk_name + pub fn format_client_imports(&self, chunk_name: &str) -> Option { + let file_name = format!("[{}]_client_imports.json", chunk_name); + Some(Path::new(&self.options.root).join(file_name)) } - pub fn get_route_chunk_name(&self, resource_path: &str) -> Option { - if let Some(routes) = self.options.routes.as_ref() { - let route = routes.into_iter().find(|f| f.import == resource_path); - let chunk_name = if let Some(route) = route { - Some(route.name.clone()) - } else { - None - }; - chunk_name + fn parse_query(&self, query: Option<&str>) -> QueryParsedRequest { + if let Some(query) = query { + let hash_query: HashMap<_, _> = + form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) + .into_owned() + .collect(); + QueryParsedRequest { + chunk_name: String::from(hash_query.get("name").unwrap_or(&String::from(""))), + is_client_entry: hash_query + .get("from") + .unwrap_or(&String::from("")) + .eq("client-entry"), + is_route_entry: hash_query + .get("from") + .unwrap_or(&String::from("")) + .eq("route-entry"), + } } else { - None + QueryParsedRequest::default() } } - pub fn get_client_imports_by_name(&self, chunk_name: &str) -> Option> { - let all_client_imports = &SHARED_CLIENT_IMPORTS.lock().unwrap(); - let client_imports = all_client_imports.get(&String::from(chunk_name)).cloned(); - client_imports - } - - pub fn format_client_imports( - &self, - entry_chunk_name: Option<&String>, - route_chunk_name: Option<&String>, - ) -> Option { - let chunk_name = entry_chunk_name.or(route_chunk_name); - if let Some(chunk_name) = chunk_name { - let file_name = format!("[{}]_client_imports.json", chunk_name); - Some(Path::new(&self.options.root).join(file_name)) + pub fn is_match(&self, resource_path: Option<&str>) -> bool { + if let Some(resource_path) = resource_path { + RSC_CLIENT_ENTRY_RE.is_match(resource_path) } else { - None + false } } @@ -122,15 +102,19 @@ impl Loader for RSCClientEntryLoader { let content = std::mem::take(&mut loader_context.content).expect("Content should be available"); let resource_path = loader_context.resource_path().to_str(); let mut source = content.try_into_string()?; - - if let Some(resource_path) = resource_path { - let chunk_name = self.get_entry_chunk_name(resource_path); - let route_chunk_name = self.get_route_chunk_name(resource_path); - let client_imports_path = - self.format_client_imports(chunk_name.as_ref(), route_chunk_name.as_ref()); + let query = loader_context.resource_query(); + + if self.is_match(resource_path) { + let parsed = self.parse_query(query); + let chunk_name = parsed.chunk_name; + let is_client_entry = parsed.is_client_entry; + let is_route_entry = parsed.is_route_entry; + // let route_chunk_name = self.get_route_chunk_name(resource_path); let mut hmr = String::from(""); let development = Some(Mode::is_development(&loader_context.context.options.mode)).unwrap_or(false); + let client_imports_path = self.format_client_imports(&chunk_name); + if development { if let Some(client_imports_path) = client_imports_path { // HMR @@ -144,10 +128,9 @@ impl Loader for RSCClientEntryLoader { } } } - // Entrypoint - if let Some(chunk_name) = &chunk_name { - let client_imports = self.get_client_imports_by_name(chunk_name); + if is_client_entry { + let client_imports = self.get_client_imports_by_name(&chunk_name); if let Some(client_imports) = client_imports { let code = client_imports @@ -156,12 +139,12 @@ impl Loader for RSCClientEntryLoader { .join("\n"); source = format!("{}{}", code, source); } - let routes = self.get_routes_code(); - source = format!("{}{}{}", hmr, routes, source); + source = format!("{}{}", hmr, source); } + // Route - if let Some(chunk_name) = &route_chunk_name { - let client_imports = self.get_client_imports_by_name(chunk_name); + if is_route_entry { + let client_imports = self.get_client_imports_by_name(&chunk_name); if let Some(client_imports) = client_imports { let code = client_imports diff --git a/crates/rspack_plugin_rsc/src/plugin/rsc_client_entry_rspack_plugin.rs b/crates/rspack_plugin_rsc/src/plugin/rsc_client_entry_rspack_plugin.rs index c8fddd3f316..c8198e14aa4 100644 --- a/crates/rspack_plugin_rsc/src/plugin/rsc_client_entry_rspack_plugin.rs +++ b/crates/rspack_plugin_rsc/src/plugin/rsc_client_entry_rspack_plugin.rs @@ -10,20 +10,13 @@ use rspack_core::{ }; use rspack_error::Result; use rspack_hook::{plugin, plugin_hook}; -use serde::{Deserialize, Serialize}; use serde_json::to_string; -use crate::utils::decl::ClientImports; +use crate::utils::decl::{ClientImports, ReactRoute}; use crate::utils::has_client_directive; use crate::utils::sever_reference::RSCServerReferenceManifest; use crate::utils::shared_data::SHARED_CLIENT_IMPORTS; -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ReactRoute { - pub name: String, - pub import: String, -} - #[derive(Debug, Default, Clone)] pub struct RSCClientEntryRspackPluginOptions { pub routes: Vec, @@ -56,6 +49,7 @@ impl RSCClientEntryRspackPlugin { fn get_route_entry(&self, resource: &str) -> Option<&ReactRoute> { self.options.routes.iter().find(|&f| f.import == resource) } + fn filter_client_components( &self, compilation: &Compilation, diff --git a/crates/rspack_plugin_rsc/src/plugin/rsc_client_reference_manifest_rspack_plugin.rs b/crates/rspack_plugin_rsc/src/plugin/rsc_client_reference_manifest_rspack_plugin.rs index 7585f37d484..448c91e030d 100644 --- a/crates/rspack_plugin_rsc/src/plugin/rsc_client_reference_manifest_rspack_plugin.rs +++ b/crates/rspack_plugin_rsc/src/plugin/rsc_client_reference_manifest_rspack_plugin.rs @@ -3,9 +3,10 @@ use std::path::PathBuf; use std::time::Instant; use rspack_core::rspack_sources::{RawSource, SourceExt}; +use rspack_core::EntryOptions; use rspack_core::{ - AssetInfo, Compilation, CompilationAsset, CompilationProcessAssets, ExportInfoProvided, Plugin, - PluginContext, + AssetInfo, Compilation, CompilationAsset, CompilationProcessAssets, CompilerFinishMake, + EntryDependency, ExportInfoProvided, Plugin, PluginContext, }; use rspack_error::Result; use rspack_hook::{plugin, plugin_hook}; @@ -13,13 +14,66 @@ use rspack_util::path::relative; use serde_json::to_string; use sugar_path::SugarPath; -use crate::utils::decl::{ClientRef, ClientReferenceManifest, ServerRef, ServerReferenceManifest}; +use crate::utils::decl::{ + ClientRef, ClientReferenceManifest, ReactRoute, ServerRef, ServerReferenceManifest, +}; use crate::utils::has_client_directive; use crate::utils::shared_data::{SHARED_CLIENT_IMPORTS, SHARED_DATA}; +#[derive(Debug, Default, Clone)] +pub struct RSCClientReferenceManifestRspackPluginOptions { + pub routes: Vec, +} + #[plugin] #[derive(Debug, Default, Clone)] -pub struct RSCClientReferenceManifestRspackPlugin; +pub struct RSCClientReferenceManifestRspackPlugin { + pub options: RSCClientReferenceManifestRspackPluginOptions, +} + +impl RSCClientReferenceManifestRspackPlugin { + pub fn new(options: RSCClientReferenceManifestRspackPluginOptions) -> Self { + Self::new_inner(options) + } + async fn add_entry(&self, compilation: &mut Compilation) -> Result<()> { + // TODO: server-entry is Server compiler entry chunk name + // we should read it from SHARED_CLIENT_IMPORTS, in this way we do not need options.routes config + // however, access SHARED_CLIENT_IMPORTS will throw thread error + let context = compilation.options.context.clone(); + let request = format!( + "rsc-client-entry-loader.js?from={}&name={}", + "client-entry", "server-entry" + ); + let entry = Box::new(EntryDependency::new(request, context.clone(), false)); + compilation + .add_include( + entry, + EntryOptions { + name: Some(String::from("client-entry")), + ..Default::default() + }, + ) + .await?; + for ReactRoute { name, .. } in self.options.routes.clone() { + let request = format!( + "rsc-client-entry-loader.js?from={}&name={}", + "route-entry", name + ); + let entry = Box::new(EntryDependency::new(request, context.clone(), false)); + compilation + .add_include( + entry, + EntryOptions { + name: Some(String::from("client-entry")), + ..Default::default() + }, + ) + .await?; + } + Ok(()) + } +} + #[derive(Debug, Default, Clone)] pub struct RSCClientReferenceManifest; @@ -29,6 +83,12 @@ async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { plugin.process_assets_stage_optimize_hash(compilation) } +#[plugin_hook(CompilerFinishMake for RSCClientReferenceManifestRspackPlugin)] +async fn finish_make(&self, compilation: &mut Compilation) -> Result<()> { + self.add_entry(compilation).await?; + Ok(()) +} + impl RSCClientReferenceManifest { fn normalize_module_id(&self, module_path: &PathBuf) -> String { let path_str = module_path.to_str().expect("TODO:"); @@ -235,6 +295,11 @@ impl Plugin for RSCClientReferenceManifestRspackPlugin { ctx: PluginContext<&mut rspack_core::ApplyContext>, _options: &mut rspack_core::CompilerOptions, ) -> Result<()> { + ctx + .context + .compiler_hooks + .finish_make + .tap(finish_make::new(self)); ctx .context .compilation_hooks diff --git a/crates/rspack_plugin_rsc/src/utils/decl.rs b/crates/rspack_plugin_rsc/src/utils/decl.rs index 8c640bcae03..c6c613fa092 100644 --- a/crates/rspack_plugin_rsc/src/utils/decl.rs +++ b/crates/rspack_plugin_rsc/src/utils/decl.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use indexmap::set::IndexSet; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use crate::export_visitor::ExportSpecifier; @@ -41,3 +41,9 @@ pub struct RSCAdditionalData { pub directives: Vec, pub exports: Vec, } + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ReactRoute { + pub name: String, + pub import: String, +} diff --git a/crates/rspack_plugin_rsc/src/utils/shared_data.rs b/crates/rspack_plugin_rsc/src/utils/shared_data.rs index a20e4e96dc5..4bbeed50549 100644 --- a/crates/rspack_plugin_rsc/src/utils/shared_data.rs +++ b/crates/rspack_plugin_rsc/src/utils/shared_data.rs @@ -5,4 +5,5 @@ use once_cell::sync::Lazy; use crate::utils::decl::{ClientImports, ServerReferenceManifest}; pub static SHARED_DATA: Lazy> = Lazy::new(|| Mutex::default()); +// Collected client imports, group by entry name or route chunk name pub static SHARED_CLIENT_IMPORTS: Lazy> = Lazy::new(|| Mutex::default()); diff --git a/packages/rspack/src/builtin-plugin/RSCClientReferenceManifestRspackPlugin.ts b/packages/rspack/src/builtin-plugin/RSCClientReferenceManifestRspackPlugin.ts index 74c26b965c4..deb926678d5 100644 --- a/packages/rspack/src/builtin-plugin/RSCClientReferenceManifestRspackPlugin.ts +++ b/packages/rspack/src/builtin-plugin/RSCClientReferenceManifestRspackPlugin.ts @@ -3,35 +3,29 @@ import type { RawRscClientReferenceManifestRspackPluginOptions } from "@rspack/b import { BuiltinPluginName } from "@rspack/binding"; import type { Compiler } from "../Compiler"; -import type { RuleSetCondition } from "../config/zod"; import type { RspackBuiltinPlugin } from "./base"; import { create } from "./base"; const RawRSCClientReferenceManifestRspackPlugin = create( BuiltinPluginName.RSCClientReferenceManifestRspackPlugin, - () => {}, + options => options, "compilation" ); interface ResolvedOptions { - routes: NonNullable< - RawRscClientReferenceManifestRspackPluginOptions["routes"] - >; - entry: Record; root: string; } -interface Options - extends Pick { - exclude?: RuleSetCondition; -} +interface Options extends RawRscClientReferenceManifestRspackPluginOptions {} export class RSCClientReferenceManifestRspackPlugin { plugin: RspackBuiltinPlugin; options: Options; resolvedOptions: ResolvedOptions; - constructor(options: Options = {}) { - this.plugin = new RawRSCClientReferenceManifestRspackPlugin(); + constructor(options: Options) { + this.plugin = new RawRSCClientReferenceManifestRspackPlugin({ + routes: options.routes + }); this.options = options; this.resolvedOptions = {} as any; } @@ -42,11 +36,7 @@ export class RSCClientReferenceManifestRspackPlugin { compiler.options.module.rules = []; } compiler.options.module.rules.push({ - test: [/\.(j|t|mj|cj)sx?$/i], - exclude: this.options.exclude ?? { - // Exclude libraries in node_modules ... - and: [/node_modules/] - }, + test: /rsc-client-entry-loader\.(j|t|mj|cj)sx?/, use: [ { loader: "builtin:rsc-client-entry-loader", @@ -56,25 +46,11 @@ export class RSCClientReferenceManifestRspackPlugin { }); } resolveOptions(compiler: Compiler): ResolvedOptions { - const entry = Object.assign({}, compiler.options.entry); - const resolvedEntry: Record = {}; const root = compiler.options.context ?? process.cwd(); - // TODO: support dynamic entry - if (typeof entry === "object") { - for (let item of Object.keys(entry)) { - const imports = entry[item].import; - if (imports) { - resolvedEntry[item] = imports[0]; - } - } - } - const resolvedRoutes = this.options.routes ?? []; // TODO: config output const output = path.resolve(root, "./dist/server"); return { - entry: resolvedEntry, - root: output, - routes: resolvedRoutes + root: output }; } }