diff --git a/Cargo.lock b/Cargo.lock index 5966a5abf52f..4e8104a55ccf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3788,6 +3788,7 @@ dependencies = [ "rspack_plugin_real_content_hash", "rspack_plugin_remove_duplicate_modules", "rspack_plugin_remove_empty_chunks", + "rspack_plugin_rsc", "rspack_plugin_runtime", "rspack_plugin_runtime_chunk", "rspack_plugin_schemes", @@ -4094,6 +4095,7 @@ dependencies = [ "rspack_error", "rspack_loader_runner", "rspack_plugin_javascript", + "rspack_plugin_rsc", "rspack_swc_plugin_import", "rspack_util", "serde", @@ -4501,6 +4503,7 @@ dependencies = [ "rspack_hook", "rspack_ids", "rspack_paths", + "rspack_plugin_rsc", "rspack_regex", "rspack_util", "rustc-hash 2.1.0", @@ -4689,6 +4692,42 @@ dependencies = [ "tracing", ] +[[package]] +name = "rspack_plugin_rsc" +version = "0.2.0" +dependencies = [ + "async-trait", + "bitflags 2.6.0", + "dashmap 6.1.0", + "futures", + "indexmap 2.7.0", + "itertools 0.14.0", + "linked_hash_set", + "num-bigint", + "once_cell", + "rayon", + "regex", + "rspack_ast", + "rspack_cacheable", + "rspack_core", + "rspack_error", + "rspack_hash", + "rspack_hook", + "rspack_ids", + "rspack_loader_runner", + "rspack_regex", + "rspack_util", + "rustc-hash 2.1.0", + "serde", + "serde_json", + "sugar_path", + "swc_core", + "swc_node_comments", + "tokio", + "tracing", + "url", +] + [[package]] name = "rspack_plugin_runtime" version = "0.2.0" @@ -5757,6 +5796,7 @@ dependencies = [ "swc_ecma_transforms_compat", "swc_ecma_transforms_module", "swc_ecma_transforms_optimization", + "swc_ecma_transforms_proposal", "swc_ecma_transforms_react", "swc_ecma_transforms_typescript", "swc_ecma_utils", diff --git a/Cargo.toml b/Cargo.toml index 9527dd12a1f3..7e998164b37f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ rspack_plugin_progress = { version = "0.2.0", path = "crates/rsp rspack_plugin_real_content_hash = { version = "0.2.0", path = "crates/rspack_plugin_real_content_hash" } rspack_plugin_remove_duplicate_modules = { version = "0.2.0", path = "crates/rspack_plugin_remove_duplicate_modules" } rspack_plugin_remove_empty_chunks = { version = "0.2.0", path = "crates/rspack_plugin_remove_empty_chunks" } +rspack_plugin_rsc = { version = "0.2.0", path = "crates/rspack_plugin_rsc" } rspack_plugin_runtime = { version = "0.2.0", path = "crates/rspack_plugin_runtime" } rspack_plugin_runtime_chunk = { version = "0.2.0", path = "crates/rspack_plugin_runtime_chunk" } rspack_plugin_schemes = { version = "0.2.0", path = "crates/rspack_plugin_schemes" } diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 1cbf6ef44e62..c4044c8b0f8a 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -364,7 +364,10 @@ export declare enum BuiltinPluginName { BundlerInfoRspackPlugin = 'BundlerInfoRspackPlugin', CssExtractRspackPlugin = 'CssExtractRspackPlugin', JsLoaderRspackPlugin = 'JsLoaderRspackPlugin', - LazyCompilationPlugin = 'LazyCompilationPlugin' + LazyCompilationPlugin = 'LazyCompilationPlugin', + RSCClientEntryRspackPlugin = 'RSCClientEntryRspackPlugin', + RSCClientReferenceManifestRspackPlugin = 'RSCClientReferenceManifestRspackPlugin', + RSCProxyRspackPlugin = 'RSCProxyRspackPlugin' } export declare function cleanupGlobalTrace(): void @@ -1392,6 +1395,7 @@ export interface RawExperiments { incremental?: false | { [key: string]: boolean } rspackFuture?: RawRspackFuture cache: boolean | { type: "persistent" } & RawExperimentCacheOptionsPersistent | { type: "memory" } +rsc: boolean } export interface RawExperimentSnapshotOptions { @@ -1853,6 +1857,11 @@ export interface RawProvideOptions { strictVersion?: boolean } +export interface RawReactRoute { + name: ChunkName + import: RoutePath +} + export interface RawRelated { sourceMap?: string } @@ -1919,6 +1928,14 @@ export interface RawResolveTsconfigOptions { references?: Array } +export interface RawRscClientEntryRspackPluginOptions { + routes?: Array +} + +export interface RawRscClientReferenceManifestRspackPluginOptions { + routes?: Array +} + export interface RawRspackFuture { } diff --git a/crates/rspack_binding_values/Cargo.toml b/crates/rspack_binding_values/Cargo.toml index 5e2e48e27792..002d9ddaef58 100644 --- a/crates/rspack_binding_values/Cargo.toml +++ b/crates/rspack_binding_values/Cargo.toml @@ -75,6 +75,7 @@ rspack_plugin_progress = { workspace = true } rspack_plugin_real_content_hash = { workspace = true } rspack_plugin_remove_duplicate_modules = { workspace = true } rspack_plugin_remove_empty_chunks = { workspace = true } +rspack_plugin_rsc = { workspace = true } rspack_plugin_runtime = { workspace = true } rspack_plugin_runtime_chunk = { workspace = true } rspack_plugin_schemes = { workspace = true } diff --git a/crates/rspack_binding_values/src/plugins/js_loader/resolver.rs b/crates/rspack_binding_values/src/plugins/js_loader/resolver.rs index 558b8c91fad7..bd0c2b2c5c51 100644 --- a/crates/rspack_binding_values/src/plugins/js_loader/resolver.rs +++ b/crates/rspack_binding_values/src/plugins/js_loader/resolver.rs @@ -20,6 +20,11 @@ use rspack_loader_preact_refresh::PREACT_REFRESH_LOADER_IDENTIFIER; use rspack_loader_react_refresh::REACT_REFRESH_LOADER_IDENTIFIER; use rspack_loader_swc::{SwcLoader, SWC_LOADER_IDENTIFIER}; use rspack_paths::Utf8Path; +use rspack_plugin_rsc::{ + RSCClientEntryLoader, RSCProxyLoader, RSCServerActionClientLoader, RSCServerActionServerLoader, + RSC_CLIENT_ENTRY_LOADER_IDENTIFIER, RSC_PROXY_LOADER_IDENTIFIER, + RSC_SERVER_ACTION_CLIENT_LOADER_IDENTIFIER, RSC_SERVER_ACTION_SERVER_LOADER_IDENTIFIER, +}; use rustc_hash::FxHashMap; use tokio::sync::RwLock; @@ -123,6 +128,54 @@ pub async fn get_builtin_loader(builtin: &str, options: Option<&str>) -> Result< if builtin.starts_with(rspack_loader_testing::NO_PASS_THROUGH_LOADER_IDENTIFIER) { return Ok(Arc::new(rspack_loader_testing::NoPassthroughLoader)); } + if builtin.starts_with(RSC_PROXY_LOADER_IDENTIFIER) { + return Ok(Arc::new( + RSCProxyLoader::new(serde_json::from_str(options.as_ref()).map_err(|e| { + serde_error_to_miette( + e, + options, + "failed to parse builtin:rsc-proxy-loader options", + ) + })?) + .with_identifier(builtin.into()), + )); + } + if builtin.starts_with(RSC_CLIENT_ENTRY_LOADER_IDENTIFIER) { + return Ok(Arc::new( + RSCClientEntryLoader::new(serde_json::from_str(options.as_ref()).map_err(|e| { + serde_error_to_miette( + e, + options, + "failed to parse builtin:rsc-client-entry-loader options", + ) + })?) + .with_identifier(builtin.into()), + )); + } + if builtin.starts_with(RSC_SERVER_ACTION_SERVER_LOADER_IDENTIFIER) { + return Ok(Arc::new( + RSCServerActionServerLoader::new(serde_json::from_str(options.as_ref()).map_err(|e| { + serde_error_to_miette( + e, + options, + "failed to parse builtin:rsc-server-action-server-loader options", + ) + })?) + .with_identifier(builtin.into()), + )); + } + if builtin.starts_with(RSC_SERVER_ACTION_CLIENT_LOADER_IDENTIFIER) { + return Ok(Arc::new( + RSCServerActionClientLoader::new(serde_json::from_str(options.as_ref()).map_err(|e| { + serde_error_to_miette( + e, + options, + "failed to parse builtin:rsc-server-client-server-loader options", + ) + })?) + .with_identifier(builtin.into()), + )); + } unreachable!("Unexpected builtin loader: {builtin}") } diff --git a/crates/rspack_binding_values/src/raw_options/raw_builtins/mod.rs b/crates/rspack_binding_values/src/raw_options/raw_builtins/mod.rs index f9cac94c2bae..77f4e87020a1 100644 --- a/crates/rspack_binding_values/src/raw_options/raw_builtins/mod.rs +++ b/crates/rspack_binding_values/src/raw_options/raw_builtins/mod.rs @@ -11,6 +11,7 @@ mod raw_lightning_css_minimizer; mod raw_limit_chunk_count; mod raw_mf; mod raw_progress; +mod raw_rsc; mod raw_runtime_chunk; mod raw_size_limits; mod raw_swc_js_minimizer; @@ -68,6 +69,9 @@ use rspack_plugin_progress::ProgressPlugin; use rspack_plugin_real_content_hash::RealContentHashPlugin; use rspack_plugin_remove_duplicate_modules::RemoveDuplicateModulesPlugin; use rspack_plugin_remove_empty_chunks::RemoveEmptyChunksPlugin; +use rspack_plugin_rsc::{ + RSCClientEntryRspackPlugin, RSCClientReferenceManifestRspackPlugin, RSCProxyRspackPlugin, +}; use rspack_plugin_runtime::{ enable_chunk_loading_plugin, ArrayPushCallbackChunkFormatPlugin, BundlerInfoPlugin, ChunkPrefetchPreloadPlugin, CommonJsChunkFormatPlugin, ModuleChunkFormatPlugin, RuntimePlugin, @@ -92,6 +96,9 @@ pub use self::{ raw_limit_chunk_count::RawLimitChunkCountPluginOptions, raw_mf::RawContainerPluginOptions, raw_progress::RawProgressPluginOptions, + raw_rsc::{ + RawRSCClientEntryRspackPluginOptions, RawRSCClientReferenceManifestRspackPluginOptions, + }, raw_swc_js_minimizer::RawSwcJsMinimizerRspackPluginOptions, }; use self::{ @@ -192,11 +199,11 @@ pub enum BuiltinPluginName { LightningCssMinimizerRspackPlugin, BundlerInfoRspackPlugin, CssExtractRspackPlugin, - - // rspack js adapter plugins - // naming format follow XxxRspackPlugin JsLoaderRspackPlugin, LazyCompilationPlugin, + RSCClientEntryRspackPlugin, + RSCClientReferenceManifestRspackPlugin, + RSCProxyRspackPlugin, } #[napi(object)] @@ -558,6 +565,19 @@ impl BuiltinPlugin { let options = raw_options.into(); plugins.push(DllReferenceAgencyPlugin::new(options).boxed()); } + BuiltinPluginName::RSCClientEntryRspackPlugin => { + let plugin_options: RawRSCClientEntryRspackPluginOptions = + downcast_into::(self.options)?; + plugins.push(RSCClientEntryRspackPlugin::new(plugin_options.into()).boxed()) + } + BuiltinPluginName::RSCClientReferenceManifestRspackPlugin => { + let plugin_options: RawRSCClientReferenceManifestRspackPluginOptions = + downcast_into::(self.options)?; + plugins.push(RSCClientReferenceManifestRspackPlugin::new(plugin_options.into()).boxed()) + } + BuiltinPluginName::RSCProxyRspackPlugin => { + plugins.push(RSCProxyRspackPlugin::new(Default::default()).boxed()) + } } Ok(()) } diff --git a/crates/rspack_binding_values/src/raw_options/raw_builtins/raw_rsc.rs b/crates/rspack_binding_values/src/raw_options/raw_builtins/raw_rsc.rs new file mode 100644 index 000000000000..b1a54cc70a75 --- /dev/null +++ b/crates/rspack_binding_values/src/raw_options/raw_builtins/raw_rsc.rs @@ -0,0 +1,61 @@ +use napi_derive::napi; +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; + +type ChunkName = String; +type RoutePath = String; + +#[derive(Deserialize, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct RawReactRoute { + pub name: ChunkName, + pub import: RoutePath, +} + +#[derive(Deserialize, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct RawRSCClientEntryRspackPluginOptions { + pub routes: Option>, +} + +#[derive(Deserialize, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +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(); + let routes: Vec = raw_routes + .into_iter() + .map(|route| ReactRoute { + name: route.name, + import: route.import, + }) + .collect(); + RSCClientEntryRspackPluginOptions { routes } + } +} diff --git a/crates/rspack_binding_values/src/raw_options/raw_experiments/mod.rs b/crates/rspack_binding_values/src/raw_options/raw_experiments/mod.rs index 56befed27bf4..c4683b9567ef 100644 --- a/crates/rspack_binding_values/src/raw_options/raw_experiments/mod.rs +++ b/crates/rspack_binding_values/src/raw_options/raw_experiments/mod.rs @@ -22,6 +22,7 @@ pub struct RawExperiments { ts_type = r#"boolean | { type: "persistent" } & RawExperimentCacheOptionsPersistent | { type: "memory" }"# )] pub cache: RawExperimentCacheOptions, + pub rsc: bool, } impl From for Experiments { @@ -38,6 +39,7 @@ impl From for Experiments { top_level_await: value.top_level_await, rspack_future: value.rspack_future.unwrap_or_default().into(), cache: normalize_raw_experiment_cache_options(value.cache), + rsc: value.rsc, } } } diff --git a/crates/rspack_core/src/concatenated_module.rs b/crates/rspack_core/src/concatenated_module.rs index 4b87057d9690..2bdfda00b9c1 100644 --- a/crates/rspack_core/src/concatenated_module.rs +++ b/crates/rspack_core/src/concatenated_module.rs @@ -559,6 +559,7 @@ impl Module for ConcatenatedModule { json_data: Default::default(), top_level_declarations: Some(Default::default()), module_concatenation_bailout: Default::default(), + directives: Default::default(), }; self.clear_diagnostics(); diff --git a/crates/rspack_core/src/module.rs b/crates/rspack_core/src/module.rs index e058c9c7e93d..4de01b7e8020 100644 --- a/crates/rspack_core/src/module.rs +++ b/crates/rspack_core/src/module.rs @@ -67,6 +67,7 @@ pub struct BuildInfo { #[cacheable(with=AsOption>)] pub top_level_declarations: Option>, pub module_concatenation_bailout: Option, + pub directives: Vec, } impl Default for BuildInfo { @@ -85,6 +86,7 @@ impl Default for BuildInfo { json_data: None, top_level_declarations: None, module_concatenation_bailout: None, + directives: Vec::default(), } } } diff --git a/crates/rspack_core/src/options/experiments/mod.rs b/crates/rspack_core/src/options/experiments/mod.rs index 47b83c126279..2ee7c7862681 100644 --- a/crates/rspack_core/src/options/experiments/mod.rs +++ b/crates/rspack_core/src/options/experiments/mod.rs @@ -11,6 +11,7 @@ pub struct Experiments { pub top_level_await: bool, pub rspack_future: RspackFuture, pub cache: ExperimentCacheOptions, + pub rsc: bool, } #[allow(clippy::empty_structs_with_brackets)] diff --git a/crates/rspack_loader_swc/Cargo.toml b/crates/rspack_loader_swc/Cargo.toml index 9a14e9f35a1b..1feeb1654ed9 100644 --- a/crates/rspack_loader_swc/Cargo.toml +++ b/crates/rspack_loader_swc/Cargo.toml @@ -29,6 +29,7 @@ rspack_core = { workspace = true } rspack_error = { workspace = true } rspack_loader_runner = { workspace = true } rspack_plugin_javascript = { workspace = true } +rspack_plugin_rsc = { path = "../rspack_plugin_rsc" } rspack_swc_plugin_import = { workspace = true } rspack_util = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/rspack_loader_swc/src/lib.rs b/crates/rspack_loader_swc/src/lib.rs index 414467536076..4f941e56a99e 100644 --- a/crates/rspack_loader_swc/src/lib.rs +++ b/crates/rspack_loader_swc/src/lib.rs @@ -15,6 +15,11 @@ use rspack_error::{error, AnyhowError, Diagnostic, Result}; use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; use rspack_plugin_javascript::ast::{self, SourceMapConfig}; use rspack_plugin_javascript::TransformOutput; +use rspack_plugin_rsc::{ + export_visitor::ImportExportVisitor, has_client_directive::has_client_directive, + has_server_directive::has_server_directive, rsc_visitor::ReactServerComponentsVisitor, + RSCAdditionalData, +}; use rspack_util::source_map::SourceMapKind; use swc_config::{config_types::MergingOption, merge::Merge}; use swc_core::base::config::SourceMapsConfig; @@ -128,7 +133,27 @@ impl SwcLoader { program.visit_with(&mut v); codegen_options.source_map_config.names = v.names; } - let ast = c.into_js_ast(program); + let mut ast = c.into_js_ast(program); + let rsc = loader_context.context.options.experiments.rsc; + if rsc { + // TODO: looks like should impl rsc transform loader + ast.transform(|program, _context| { + let mut rsc_visitor = ReactServerComponentsVisitor::new(); + program.visit_with(&mut rsc_visitor); + if has_client_directive(&rsc_visitor.directives) + || has_server_directive(&rsc_visitor.directives) + { + let mut export_visitor: ImportExportVisitor = ImportExportVisitor::new(); + program.visit_with(&mut export_visitor); + loader_context.take_additional_data().and_then(|mut f| { + f.insert(RSCAdditionalData { + directives: rsc_visitor.directives, + exports: export_visitor.exports, + }) + }); + } + }); + } let TransformOutput { code, map } = ast::stringify(&ast, codegen_options)?; loader_context.finish_with((code, map)); diff --git a/crates/rspack_plugin_javascript/Cargo.toml b/crates/rspack_plugin_javascript/Cargo.toml index 6698dca08797..710f1549adee 100644 --- a/crates/rspack_plugin_javascript/Cargo.toml +++ b/crates/rspack_plugin_javascript/Cargo.toml @@ -31,6 +31,7 @@ rspack_hash = { workspace = true } rspack_hook = { workspace = true } rspack_ids = { workspace = true } rspack_paths = { workspace = true } +rspack_plugin_rsc = { path = "../rspack_plugin_rsc" } rspack_regex = { workspace = true } rspack_util = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs index 8c5bd4cdefbf..e58fdf104fdd 100644 --- a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs @@ -14,6 +14,7 @@ use rspack_core::{ }; use rspack_error::miette::Diagnostic; use rspack_error::{DiagnosticExt, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray}; +use rspack_plugin_rsc::rsc_visitor::ReactServerComponentsVisitor; use swc_core::common::comments::Comments; use swc_core::common::input::SourceFileInput; use swc_core::common::{FileName, SyntaxContext}; @@ -226,6 +227,12 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { diagnostics.append(&mut warning_diagnostics); let mut side_effects_bailout = None; + ast.transform(|program, _context| { + let mut visitor = ReactServerComponentsVisitor::new(); + program.visit_with(&mut visitor); + build_info.directives = visitor.directives; + }); + if compiler_options.optimization.side_effects.is_true() { ast.transform(|program, context| { let unresolved_ctxt = SyntaxContext::empty().apply_mark(context.unresolved_mark); diff --git a/crates/rspack_plugin_rsc/Cargo.toml b/crates/rspack_plugin_rsc/Cargo.toml new file mode 100644 index 000000000000..d0eed859e9db --- /dev/null +++ b/crates/rspack_plugin_rsc/Cargo.toml @@ -0,0 +1,49 @@ +[package] +edition = "2021" +license = "MIT" +name = "rspack_plugin_rsc" +repository = "https://github.com/web-infra-dev/rspack" +version = "0.2.0" + +[dependencies] +async-trait = { workspace = true } +bitflags = { workspace = true } +dashmap = { workspace = true } +futures = { workspace = true } +indexmap = { workspace = true } +itertools = { workspace = true } +linked_hash_set = { workspace = true } +num-bigint = { version = "0.4.4" } +once_cell = { workspace = true } +rayon = { workspace = true } +regex = { workspace = true } +rspack_ast = { path = "../rspack_ast" } +rspack_cacheable = { workspace = true } +rspack_core = { path = "../rspack_core" } +rspack_error = { path = "../rspack_error" } +rspack_hash = { path = "../rspack_hash" } +rspack_hook = { path = "../rspack_hook" } +rspack_ids = { path = "../rspack_ids/" } +rspack_loader_runner = { path = "../rspack_loader_runner" } +rspack_regex = { path = "../rspack_regex" } +rspack_util = { path = "../rspack_util" } +rustc-hash = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sugar_path = { workspace = true } +swc_core = { workspace = true, features = [ + "__parser", + "__utils", + "common_sourcemap", + "ecma_preset_env", + "ecma_transforms_optimization", + "ecma_transforms_module", + "ecma_transforms_compat", + "ecma_transforms_proposal", + "ecma_transforms_typescript", + "ecma_quote", +] } +swc_node_comments = { workspace = true } +tokio = { workspace = true, features = ["rt"] } +tracing = { workspace = true } +url = { workspace = true } diff --git a/crates/rspack_plugin_rsc/LICENSE b/crates/rspack_plugin_rsc/LICENSE new file mode 100644 index 000000000000..46310101ad8a --- /dev/null +++ b/crates/rspack_plugin_rsc/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022-present Bytedance, Inc. and its affiliates. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/rspack_plugin_rsc/src/lib.rs b/crates/rspack_plugin_rsc/src/lib.rs new file mode 100644 index 000000000000..5b625b38234b --- /dev/null +++ b/crates/rspack_plugin_rsc/src/lib.rs @@ -0,0 +1,14 @@ +#![feature(if_let_guard)] +#![feature(let_chains)] +#![feature(box_patterns)] +#![recursion_limit = "256"] + +mod plugin; +pub use crate::loader::*; +pub use crate::plugin::*; +pub use crate::utils::{ + decl::RSCAdditionalData, decl::ReactRoute, export_visitor, has_client_directive, + has_server_directive, rsc_visitor, +}; +mod loader; +mod utils; diff --git a/crates/rspack_plugin_rsc/src/loader/mod.rs b/crates/rspack_plugin_rsc/src/loader/mod.rs new file mode 100644 index 000000000000..b86b2b4b2cfc --- /dev/null +++ b/crates/rspack_plugin_rsc/src/loader/mod.rs @@ -0,0 +1,8 @@ +pub mod rsc_proxy_loader; +pub use rsc_proxy_loader::*; +pub mod rsc_client_entry_loader; +pub use rsc_client_entry_loader::*; +pub mod rsc_server_action_client_loader; +pub use rsc_server_action_client_loader::*; +pub mod rsc_server_action_server_loader; +pub use rsc_server_action_server_loader::*; 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 new file mode 100644 index 000000000000..eb93dc666e19 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/loader/rsc_client_entry_loader.rs @@ -0,0 +1,178 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use indexmap::set::IndexSet; +use itertools::Itertools; +use once_cell::sync::Lazy; +use regex::Regex; +use rspack_cacheable::{cacheable, cacheable_dyn}; +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; + +#[cacheable] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RSCClientEntryLoaderOptions { + root: String, +} + +#[cacheable] +#[derive(Debug)] +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 { + Self { + identifier: RSC_CLIENT_ENTRY_LOADER_IDENTIFIER.into(), + options: options.into(), + } + } + + pub async fn get_client_imports_by_name(&self, chunk_name: &str) -> Option> { + let all_client_imports = &SHARED_CLIENT_IMPORTS.read().await; + let client_imports = all_client_imports.get(&String::from(chunk_name)).cloned(); + client_imports + } + + 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)) + } + + 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 { + QueryParsedRequest::default() + } + } + + 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 { + false + } + } + + /// Panics: + /// Panics if `identifier` passed in is not starting with `builtin:swc-loader`. + pub fn with_identifier(mut self, identifier: Identifier) -> Self { + assert!(identifier.starts_with(RSC_CLIENT_ENTRY_LOADER_IDENTIFIER)); + self.identifier = identifier; + self + } +} + +pub const RSC_CLIENT_ENTRY_LOADER_IDENTIFIER: &str = "builtin:rsc-client-entry-loader"; + +#[cacheable_dyn] +#[async_trait::async_trait] +impl Loader for RSCClientEntryLoader { + async fn run(&self, loader_context: &mut LoaderContext) -> Result<()> { + let Some(content) = loader_context.take_content() else { + return Ok(()); + }; + let mut source = content.try_into_string()?; + let resource_path = loader_context + .resource_path() + .and_then(|f| Some(f.as_str())); + let query = loader_context.resource_query(); + + if self.is_match(resource_path) { + let parsed: QueryParsedRequest = 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 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 + if !client_imports_path.exists() { + loader_context + .missing_dependencies + .insert(client_imports_path.clone()); + } else { + // If client_imports.json not found, connect resource with client_imports.json will throw resolve error + hmr = format!(r#"import {:?};"#, client_imports_path.into_os_string()); + } + } + } + // Entrypoint + if is_client_entry { + let client_imports = self.get_client_imports_by_name(&chunk_name).await; + if let Some(client_imports) = client_imports { + let code = client_imports + .iter() + .map(|i| format!(r#"import(/* webpackMode: "eager" */ "{}");"#, i)) + .join("\n"); + source = format!("{}{}", code, source); + } + source = format!("{}{}", hmr, source); + } + + // Route + if is_route_entry { + let client_imports = self.get_client_imports_by_name(&chunk_name).await; + if let Some(client_imports) = client_imports { + let code = client_imports + .iter() + .map(|i| { + format!( + r#"import(/* webpackChunkName: "{}" */ "{}");"#, + chunk_name, i + ) + }) + .join("\n"); + source = format!("{}{}", code, source); + } + source = format!("{}{}", hmr, source); + } + } + loader_context.finish_with(source); + Ok(()) + } +} + +impl Identifiable for RSCClientEntryLoader { + fn identifier(&self) -> Identifier { + self.identifier + } +} diff --git a/crates/rspack_plugin_rsc/src/loader/rsc_proxy_loader.rs b/crates/rspack_plugin_rsc/src/loader/rsc_proxy_loader.rs new file mode 100644 index 000000000000..fa6831f7bdc8 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/loader/rsc_proxy_loader.rs @@ -0,0 +1,114 @@ +use rspack_cacheable::{cacheable, cacheable_dyn}; +use rspack_core::RunnerContext; +use rspack_error::Result; +use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; +use serde::{Deserialize, Serialize}; + +use crate::{export_visitor::DEFAULT_EXPORT, has_client_directive, RSCAdditionalData}; + +#[cacheable] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RSCProxyLoaderOptions { + client_proxy: String, +} + +#[cacheable] +#[derive(Debug)] +pub struct RSCProxyLoader { + identifier: Identifier, + options: RSCProxyLoaderOptions, +} + +impl RSCProxyLoader { + pub fn new(options: RSCProxyLoaderOptions) -> Self { + Self { + identifier: RSC_PROXY_LOADER_IDENTIFIER.into(), + options: options.into(), + } + } + + /// Panics: + /// Panics if `identifier` passed in is not starting with `builtin:rsc-proxy-loader`. + pub fn with_identifier(mut self, identifier: Identifier) -> Self { + assert!(identifier.starts_with(RSC_PROXY_LOADER_IDENTIFIER)); + self.identifier = identifier; + self + } +} + +pub const RSC_PROXY_LOADER_IDENTIFIER: &str = "builtin:rsc-proxy-loader"; + +#[cacheable_dyn] +#[async_trait::async_trait] +impl Loader for RSCProxyLoader { + async fn run(&self, loader_context: &mut LoaderContext) -> Result<()> { + let Some(content) = loader_context.take_content() else { + return Ok(()); + }; + let source = content.try_into_string()?; + let resource_path = loader_context + .resource_path() + .and_then(|f| Some(f.as_str())); + + let rsc_info = loader_context + .additional_data() + .and_then(|data| data.get::()); + if let Some(RSCAdditionalData { + directives, + exports, + }) = rsc_info + { + if has_client_directive(directives) { + let mut source = format!( + r#" +import {{ createProxy }} from "{}" +const proxy = createProxy({:?}) + +// Accessing the __esModule property and exporting $$typeof are required here. +// The __esModule getter forces the proxy target to create the default export +// and the $$typeof value is for rendering logic to determine if the module +// is a client boundary. +const {{ __esModule, $$typeof }} = proxy; +const __default__ = proxy.default + "#, + self.options.client_proxy, + resource_path.unwrap() + ); + let mut cnt = 0; + for export in exports.into_iter() { + let n = &export.n; + if n == "" { + source += r#"\nexports[\'\'] = proxy[\'\'];"#; + } else if n == DEFAULT_EXPORT { + source += r#" +export { __esModule, $$typeof }; +export default __default__; + "#; + } else { + source += &format!( + r#" +const e{} = proxy["{}"]; +export {{ e{} as {} }}; + "#, + cnt, n, cnt, n + ); + cnt += 1; + } + } + loader_context.finish_with(source); + } else { + loader_context.finish_with(source); + } + } else { + loader_context.finish_with(source); + } + Ok(()) + } +} + +impl Identifiable for RSCProxyLoader { + fn identifier(&self) -> Identifier { + self.identifier + } +} diff --git a/crates/rspack_plugin_rsc/src/loader/rsc_server_action_client_loader.rs b/crates/rspack_plugin_rsc/src/loader/rsc_server_action_client_loader.rs new file mode 100644 index 000000000000..1ddbcb39963b --- /dev/null +++ b/crates/rspack_plugin_rsc/src/loader/rsc_server_action_client_loader.rs @@ -0,0 +1,111 @@ +use itertools::Itertools; +use rspack_cacheable::{cacheable, cacheable_dyn}; +use rspack_core::RunnerContext; +use rspack_error::Result; +use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; +use serde::{Deserialize, Serialize}; + +use crate::{ + utils::{has_server_directive, server_action::generate_action_id}, + RSCAdditionalData, +}; + +#[cacheable] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RSCServerActionClientLoaderOptions { + server_proxy: String, +} + +#[cacheable] +#[derive(Debug)] +pub struct RSCServerActionClientLoader { + identifier: Identifier, + options: RSCServerActionClientLoaderOptions, +} + +impl RSCServerActionClientLoader { + pub fn new(options: RSCServerActionClientLoaderOptions) -> Self { + Self { + identifier: RSC_SERVER_ACTION_CLIENT_LOADER_IDENTIFIER.into(), + options: options.into(), + } + } + + /// Panics: + /// Panics if `identifier` passed in is not starting with `builtin:rsc-server-action-client-loader`. + pub fn with_identifier(mut self, identifier: Identifier) -> Self { + assert!(identifier.starts_with(RSC_SERVER_ACTION_CLIENT_LOADER_IDENTIFIER)); + self.identifier = identifier; + self + } +} + +pub const RSC_SERVER_ACTION_CLIENT_LOADER_IDENTIFIER: &str = + "builtin:rsc-server-action-client-loader"; + +#[cacheable_dyn] +#[async_trait::async_trait] +impl Loader for RSCServerActionClientLoader { + async fn run(&self, loader_context: &mut LoaderContext) -> Result<()> { + let Some(content) = loader_context.take_content() else { + return Ok(()); + }; + let source = content.try_into_string()?; + let resource_path_str = loader_context + .resource_path() + .and_then(|f| Some(f.as_str())) + .unwrap_or(""); + let resource_query_str = loader_context.resource_query().unwrap_or(""); + let resource = format!("{}{}", resource_path_str, resource_query_str); + + let rsc_info = loader_context + .additional_data() + .and_then(|data| data.get::()); + if let Some(RSCAdditionalData { + directives, + exports, + }) = rsc_info + { + if has_server_directive(directives) { + let mut has_default = false; + let mut source = format!( + r#" +import {{ createServerReference }} from "{}"; + "#, + self.options.server_proxy, + ); + let code = exports + .iter() + .map(|f| { + let id = generate_action_id(&resource, &f.n); + if f.n.eq("default") { + has_default = true; + format!(r#"const _default = createServerReference("{}");"#, id) + } else { + format!(r#"export const {} = createServerReference("{}");"#, f.n, id) + } + }) + .join("\n"); + source = format!("{}{}", source, code); + if has_default { + source += r#" +export default _default; +"# + } + loader_context.finish_with(source); + } else { + loader_context.finish_with(source); + } + } else { + loader_context.finish_with(source); + } + Ok(()) + } +} + +impl Identifiable for RSCServerActionClientLoader { + fn identifier(&self) -> Identifier { + self.identifier + } +} diff --git a/crates/rspack_plugin_rsc/src/loader/rsc_server_action_server_loader.rs b/crates/rspack_plugin_rsc/src/loader/rsc_server_action_server_loader.rs new file mode 100644 index 000000000000..d60fb395b621 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/loader/rsc_server_action_server_loader.rs @@ -0,0 +1,163 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use indexmap::IndexSet; +use itertools::Itertools; +use rspack_cacheable::{cacheable, cacheable_dyn}; +use rspack_core::RunnerContext; +use rspack_error::Result; +use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; +use serde::{Deserialize, Serialize}; +use url::form_urlencoded; + +use crate::utils::{ + constants::RSC_SERVER_ACTION_ENTRY_RE, + server_action::generate_action_id, + shared_data::{SHARED_DATA, SHARED_SERVER_IMPORTS}, +}; + +#[cacheable] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RSCServerActionServerLoaderOptions { + root: String, +} + +#[cacheable] +#[derive(Debug)] +pub struct RSCServerActionServerLoader { + identifier: Identifier, + options: RSCServerActionServerLoaderOptions, +} + +#[derive(Debug, Clone, Default)] +struct QueryParsedRequest { + pub chunk_name: String, +} + +impl RSCServerActionServerLoader { + pub fn new(options: RSCServerActionServerLoaderOptions) -> Self { + Self { + identifier: RSC_SERVER_ACTION_SERVER_LOADER_IDENTIFIER.into(), + options: options.into(), + } + } + + async fn get_server_imports_by_name(&self, chunk_name: &str) -> Option> { + let all_server_imports = &SHARED_SERVER_IMPORTS.read().await; + let server_imports: Option<_> = all_server_imports.get(&String::from(chunk_name)).cloned(); + server_imports + } + + pub async fn get_server_refs_by_name(&self, chunk_name: &str) -> Vec> { + let server_imports = self.get_server_imports_by_name(chunk_name).await; + let all_server_refs = &SHARED_DATA.read().await; + let mut actions = vec![]; + if let Some(server_imports) = server_imports { + for file_path in server_imports.iter() { + let server_refs: Option<_> = all_server_refs + .server_imports + .get(&String::from(file_path)) + .cloned(); + if let Some(server_refs) = server_refs { + for n in server_refs.names.iter() { + let id = generate_action_id(file_path.as_str(), n); + let item = vec![id.to_string(), file_path.to_string(), n.to_string()]; + actions.push(item); + } + } + } + } + actions + } + + pub fn format_server_ref_path(&self) -> PathBuf { + let file_name = String::from("server-reference-manifest.json"); + 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_SERVER_ACTION_ENTRY_RE.is_match(resource_path) + } else { + false + } + } + + 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(""))), + } + } else { + QueryParsedRequest::default() + } + } + + /// Panics: + /// Panics if `identifier` passed in is not starting with `builtin:rsc-server-action-server-loader`. + pub fn with_identifier(mut self, identifier: Identifier) -> Self { + assert!(identifier.starts_with(RSC_SERVER_ACTION_SERVER_LOADER_IDENTIFIER)); + self.identifier = identifier; + self + } +} + +pub const RSC_SERVER_ACTION_SERVER_LOADER_IDENTIFIER: &str = + "builtin:rsc-server-action-server-loader"; + +#[cacheable_dyn] +#[async_trait::async_trait] +impl Loader for RSCServerActionServerLoader { + async fn run(&self, loader_context: &mut LoaderContext) -> Result<()> { + let Some(content) = loader_context.take_content() else { + return Ok(()); + }; + let mut source = content.try_into_string()?; + let resource_path = loader_context + .resource_path() + .and_then(|f| Some(f.as_str())); + 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 server_ref_path = self.format_server_ref_path(); + loader_context + .missing_dependencies + .insert(server_ref_path.clone()); + let server_refs = self.get_server_refs_by_name(&chunk_name).await; + let actions = server_refs + .iter() + .map(|f| { + return format!( + r#""{}": async () => import(/* webpackMode: "eager" */ "{}").then(mod => mod["{}"]),"#, + f[0], f[1], f[2] + ); + }) + .join("\n"); + source = format!( + r#" + const actions = {{{}}}; + export default actions + "#, + actions + ); + } + + loader_context.finish_with(source); + Ok(()) + } +} + +impl Identifiable for RSCServerActionServerLoader { + fn identifier(&self) -> Identifier { + self.identifier + } +} diff --git a/crates/rspack_plugin_rsc/src/plugin/mod.rs b/crates/rspack_plugin_rsc/src/plugin/mod.rs new file mode 100644 index 000000000000..a4d532e53020 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/plugin/mod.rs @@ -0,0 +1,6 @@ +pub mod rsc_client_entry_rspack_plugin; +pub use rsc_client_entry_rspack_plugin::*; +pub mod rsc_client_reference_manifest_rspack_plugin; +pub use rsc_client_reference_manifest_rspack_plugin::*; +pub mod rsc_proxy_rspack_plugin; +pub use rsc_proxy_rspack_plugin::*; 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 new file mode 100644 index 000000000000..fb4a093a7819 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/plugin/rsc_client_entry_rspack_plugin.rs @@ -0,0 +1,360 @@ +use std::collections::{HashMap, HashSet}; +use std::time::Instant; + +use async_trait::async_trait; +use indexmap::set::IndexSet; +use once_cell::sync::Lazy; +use regex::Regex; +use rspack_core::rspack_sources::{RawSource, SourceExt}; +use rspack_core::{ + ApplyContext, AssetInfo, BoxDependency, Compilation, CompilationAsset, CompilationProcessAssets, + CompilerFinishMake, CompilerOptions, EntryDependency, EntryOptions, Module, ModuleType, Plugin, + PluginContext, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; +use serde_json::to_string; + +use crate::utils::decl::{ClientImports, ReactRoute}; +use crate::utils::file::generate_asset_version; +use crate::utils::sever_reference::RSCServerReferenceManifest; +use crate::utils::shared_data::{SHARED_CLIENT_IMPORTS, SHARED_SERVER_IMPORTS}; +use crate::utils::{has_client_directive, has_server_directive}; + +#[derive(Debug, Default, Clone)] +pub struct RSCClientEntryRspackPluginOptions { + pub routes: Vec, +} + +#[plugin] +#[derive(Debug, Default, Clone)] +pub struct RSCClientEntryRspackPlugin { + pub options: RSCClientEntryRspackPluginOptions, +} + +static CSS_RE: Lazy = + Lazy::new(|| Regex::new(r"\.(css|scss|sass)").expect("css regexp init failed")); + +impl RSCClientEntryRspackPlugin { + pub fn new(options: RSCClientEntryRspackPluginOptions) -> Self { + Self::new_inner(options) + } + async fn add_entry(&self, compilation: &mut Compilation) -> Result<()> { + // TODO: multiple server entry support + let context = compilation.options.context.clone(); + let request = format!( + "rsc-server-action-entry-loader.js?from={}&name={}", + "server-entry", "server-entry" + ); + let args = vec![( + Box::new(EntryDependency::new(request, context.clone(), None, false)) as BoxDependency, + EntryOptions { + name: Some(String::from("server-entry")), + ..Default::default() + }, + )]; + compilation.add_include(args).await?; + Ok(()) + } + fn insert_client_imports( + &self, + client_imports: &mut HashMap>, + name: Option<&str>, + client_import: &str, + ) { + if let Some(name) = name { + let named_client_imports = client_imports + .entry(name.into()) + .or_insert_with(IndexSet::default); + named_client_imports.insert(client_import.into()); + } + } + fn get_route_entry(&self, resource: &str) -> Option<&ReactRoute> { + self.options.routes.iter().find(|&f| f.import == resource) + } + fn is_visited( + &self, + visited_modules_by_entry: &mut HashMap>, + entry: &str, + route_entry: Option<&ReactRoute>, + resource_path: &str, + ) -> bool { + let entry_key = route_entry.map_or_else( + || entry.to_string(), + |route_entry| route_entry.import.clone(), + ); + let visited_modules = visited_modules_by_entry.get(&entry_key); + visited_modules + .map(|f| f.contains(resource_path)) + .unwrap_or(false) + } + fn mark_visited( + &self, + visited_modules_by_entry: &mut HashMap>, + entry: &str, + route_entry: Option<&ReactRoute>, + resource_path: &str, + ) { + let entry_key = route_entry.map_or_else( + || entry.to_string(), + |route_entry| route_entry.import.clone(), + ); + let visited_modules = visited_modules_by_entry + .entry(entry_key) + .or_insert_with(HashSet::new); + visited_modules.insert(resource_path.into()); + } + fn filter_client_components( + &self, + compilation: &Compilation, + entry_name: &str, + module: &Box, + visited_modules: &mut HashMap>, + client_imports: &mut HashMap>, + entry_client_imports: &mut IndexSet, + from_route: Option<&ReactRoute>, + ) { + let data = module + .as_normal_module() + .and_then(|m| Some(m.resource_resolved_data())); + let module_type = module.module_type(); + if let Some(data) = data { + let resource_path_str = data.resource_path.as_ref().and_then(|f| Some(f.as_str())); + let resource_path_str = if let Some(r) = resource_path_str { + r + } else { + "" + }; + // Skip module without resource_path + if resource_path_str.eq("") { + return; + } + let resource_query = &data.resource_query; + let resource_query_str = if let Some(query) = resource_query.as_ref() { + query + } else { + "" + }; + let resource_str = format!("{}{}", resource_path_str, resource_query_str); + let is_css = match module_type { + ModuleType::Css | ModuleType::CssModule | ModuleType::CssAuto => true, + // css asset type only working with experimental.css + // use filepath match as fallback + // TODO: maybe we check module.identifier() has css-loader instead + _ => CSS_RE.is_match(resource_path_str), + }; + // TODO: check css file is in used + // TODO: unique css files from other entry + let is_client_components = match module.build_info() { + Some(build_info) => has_client_directive(&build_info.directives), + None => false, + }; + let route_entry: Option<&ReactRoute> = match self.get_route_entry(&resource_str) { + Some(route) => Some(route), + None => from_route, + }; + if self.is_visited(visited_modules, entry_name, route_entry, &resource_str) { + return; + } + self.mark_visited(visited_modules, entry_name, route_entry, &resource_str); + + if is_client_components || is_css { + if let Some(route_entry) = route_entry { + self.insert_client_imports( + client_imports, + Some(route_entry.name.as_str()), + &resource_str, + ) + } else { + entry_client_imports.insert(String::from(&resource_str)); + } + } else { + let mg = compilation.get_module_graph(); + for connection in mg.get_outgoing_connections(&module.identifier()) { + let m = mg + .get_module_by_dependency_id(&connection.dependency_id) + .expect("should exist"); + self.filter_client_components( + compilation, + entry_name, + m, + visited_modules, + client_imports, + entry_client_imports, + route_entry, + ); + } + } + } + } + fn filter_server_actions( + &self, + compilation: &Compilation, + module: &Box, + visited_modules: &mut HashSet, + entry_server_imports: &mut IndexSet, + ) { + let data = module + .as_normal_module() + .and_then(|m| Some(m.resource_resolved_data())); + if let Some(data) = data { + let resource_path_str = data.resource_path.as_ref().and_then(|f| Some(f.as_str())); + let resource_path_str = if let Some(r) = resource_path_str { + r + } else { + "" + }; + // Skip module without resource_path + if resource_path_str.eq("") { + return; + } + let resource_query = &data.resource_query; + let resource_query_str = if let Some(query) = resource_query.as_ref() { + query + } else { + "" + }; + let resource_str = format!("{}{}", resource_path_str, resource_query_str); + if visited_modules.contains(&resource_str) { + return; + } + visited_modules.insert(resource_str.clone()); + let is_server_action = match module.build_info() { + Some(build_info) => has_server_directive(&build_info.directives), + None => false, + }; + if is_server_action { + entry_server_imports.insert(String::from(resource_str)); + }; + let mg = compilation.get_module_graph(); + for connection in mg.get_outgoing_connections(&module.identifier()) { + let m = mg + .get_module_by_dependency_id(&connection.dependency_id) + .expect("should exist"); + self.filter_server_actions(compilation, m, visited_modules, entry_server_imports); + } + } + } +} + +#[plugin_hook(CompilerFinishMake for RSCClientEntryRspackPlugin)] +async fn finish_make(&self, compilation: &mut Compilation) -> Result<()> { + let now = Instant::now(); + // Client imports groupby entry or route chunkname + let mut client_imports: ClientImports = HashMap::new(); + let mut server_imports: ClientImports = HashMap::new(); + let mut visited_modules: HashMap> = HashMap::new(); + for (name, entry) in &compilation.entries { + let mut entry_client_imports: IndexSet = IndexSet::new(); + let mut entry_server_imports: IndexSet = IndexSet::new(); + let mut visited_modules_of_server_actions: HashSet = HashSet::new(); + let mg = compilation.get_module_graph(); + let entry_module = mg + .get_module_by_dependency_id(&entry.dependencies[0]) + .expect("should exist"); + self.filter_client_components( + compilation, + name, + entry_module, + &mut visited_modules, + &mut client_imports, + &mut entry_client_imports, + None, + ); + self.filter_server_actions( + compilation, + entry_module, + &mut visited_modules_of_server_actions, + &mut entry_server_imports, + ); + client_imports.insert(String::from(name), entry_client_imports); + server_imports.insert(String::from(name), entry_server_imports); + } + let mut shared_client_imports_guard = SHARED_CLIENT_IMPORTS.write().await; + *shared_client_imports_guard = client_imports.clone(); + let mut shared_server_imports_guard = SHARED_SERVER_IMPORTS.write().await; + *shared_server_imports_guard = server_imports.clone(); + // TODO: custom main entry name, all other entries depend on this entry + // let main_name = "server-entry"; + // let cc = client_imports.clone(); + // let main = cc.get(main_name).unwrap(); + for (name, value) in client_imports.iter_mut() { + // if name != main_name { + // for import in main { + // value.shift_remove(import.as_str()); + // } + // } + // Make HMR friendly + value.sort(); + let output_file = format!("[{}]_client_imports.json", name); + let content = to_string(&value); + + match content { + Ok(content) => { + compilation.assets_mut().insert( + output_file, + CompilationAsset { + source: Some(RawSource::from(content.as_str()).boxed()), + info: AssetInfo { + immutable: Some(false), + version: generate_asset_version(&content), + ..AssetInfo::default() + }, + }, + ); + } + Err(_) => (), + } + } + for (name, value) in server_imports.iter_mut() { + // Make HMR friendly + value.sort(); + let output_file = format!("[{}]_server_imports.json", name); + let content = to_string(&value); + match content { + Ok(content) => { + compilation.assets_mut().insert( + output_file, + CompilationAsset { + source: Some(RawSource::from(content.as_str()).boxed()), + info: AssetInfo { + immutable: Some(false), + version: generate_asset_version(&content), + ..AssetInfo::default() + }, + }, + ); + } + Err(_) => (), + } + } + self.add_entry(compilation).await?; + tracing::debug!( + "collect all client & server imports took {} ms.", + now.elapsed().as_millis() + ); + Ok(()) +} + +#[plugin_hook(CompilationProcessAssets for RSCClientEntryRspackPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_OPTIMIZE_HASH)] +async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { + let plugin: RSCServerReferenceManifest = RSCServerReferenceManifest {}; + plugin.process_assets_stage_optimize_hash(compilation).await +} + +#[async_trait] +impl Plugin for RSCClientEntryRspackPlugin { + fn apply(&self, ctx: PluginContext<&mut ApplyContext>, _options: &CompilerOptions) -> Result<()> { + ctx + .context + .compiler_hooks + .finish_make + .tap(finish_make::new(self)); + ctx + .context + .compilation_hooks + .process_assets + .tap(process_assets::new(self)); + Ok(()) + } +} 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 new file mode 100644 index 000000000000..e172a7088908 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/plugin/rsc_client_reference_manifest_rspack_plugin.rs @@ -0,0 +1,311 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::time::Instant; + +use rspack_core::rspack_sources::{RawSource, SourceExt}; +use rspack_core::{ + AssetInfo, ChunkGraph, Compilation, CompilationAsset, CompilationProcessAssets, CompilerMake, + EntryDependency, ExportInfoProvided, Plugin, PluginContext, +}; +use rspack_core::{BoxDependency, EntryOptions}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; +use rspack_util::path::relative; +use serde_json::to_string; +use sugar_path::SugarPath; + +use crate::utils::decl::{ + ClientRef, ClientReferenceManifest, ReactRoute, ServerRef, ServerReferenceManifest, +}; +use crate::utils::file::generate_asset_version; +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 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(), None, false)); + let args = vec![( + entry as BoxDependency, + EntryOptions { + name: Some(String::from("client-entry")), + ..Default::default() + }, + )]; + compilation.add_include(args).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(), None, false)); + let args = vec![( + entry as BoxDependency, + EntryOptions { + name: Some(String::from("client-entry")), + ..Default::default() + }, + )]; + compilation.add_include(args).await?; + } + Ok(()) + } +} + +#[derive(Debug, Default, Clone)] +pub struct RSCClientReferenceManifest; + +#[plugin_hook(CompilationProcessAssets for RSCClientReferenceManifestRspackPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_OPTIMIZE_HASH)] +async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { + let plugin = RSCClientReferenceManifest {}; + plugin.process_assets_stage_optimize_hash(compilation).await +} + +#[plugin_hook(CompilerMake for RSCClientReferenceManifestRspackPlugin)] +async fn 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:"); + if !path_str.starts_with(".") { + format!("./{}", path_str) + } else { + String::from(path_str) + } + } + fn get_client_ref_module_key(&self, filepath: &str, name: &str) -> String { + if name == "*" { + String::from(filepath) + } else { + format!("{}#{}", filepath, name) + } + } + async fn is_client_request(&self, resource_path: &str) -> bool { + let client_imports = SHARED_CLIENT_IMPORTS.read().await; + return client_imports.values().any(|f| f.contains(resource_path)); + } + fn add_server_ref( + &self, + module_id: &str, + ssr_module_id: &str, + name: &str, + shared_ssr_module_mapping: &HashMap>, + ssr_module_mapping: &mut HashMap>, + ) { + let module_mapping = ssr_module_mapping + .entry(module_id.into()) + .or_insert_with(HashMap::default); + let shared_module_mapping = shared_ssr_module_mapping.get(ssr_module_id); + match shared_module_mapping { + Some(smm) => { + let server_ref = smm.get(name); + match server_ref { + Some(server_ref) => { + module_mapping.insert(name.to_string(), server_ref.clone()); + } + None => (), + } + } + None => (), + } + } + fn add_client_ref( + &self, + filepath: &str, + id: &str, + name: &str, + chunks: &Vec, + client_modules: &mut HashMap, + ) { + let key = self.get_client_ref_module_key(filepath, name); + client_modules.insert( + key, + ClientRef { + id: id.to_string(), + name: name.to_string(), + chunks: chunks.clone(), + }, + ); + } + async fn process_assets_stage_optimize_hash(&self, compilation: &mut Compilation) -> Result<()> { + let now = Instant::now(); + let mut client_manifest = ClientReferenceManifest::default(); + let shared_server_manifest = SHARED_DATA.read().await; + let mut server_manifest = ServerReferenceManifest::default(); + let mg = compilation.get_module_graph(); + let context = &compilation.options.context; + + for chunk_group in compilation.chunk_group_by_ukey.values() { + let chunks = chunk_group + .chunks + .clone() + .into_iter() + .filter_map(|chunk| { + let chunk = compilation.chunk_by_ukey.expect_get(&chunk); + let name_or_id = chunk + .id(&compilation.chunk_ids_artifact) + .map(|f| f.to_string()) + .or(chunk.name().map(|f| f.to_string())); + name_or_id + }) + .collect::>(); + for chunk in &chunk_group.chunks { + let chunk_modules = compilation.chunk_graph.get_chunk_modules(chunk, &mg); + for module in chunk_modules { + let request = module + .as_normal_module() + .and_then(|f| Some(f.user_request())); + let module_id = + ChunkGraph::get_module_id(&compilation.module_ids_artifact, module.identifier()) + .map(|s| s.to_string()); + // FIXME: module maybe not a normal module, e.g. concatedmodule, not contain user_request + // use name_for_condition as fallback + // should be care user_request has resource query but name_for_condition not contain resource_query + let name_for_condition = module.name_for_condition(); + + let resource_path = request.or_else(|| name_for_condition.as_deref()); + if resource_path.is_none() { + continue; + } + + if module.build_info().is_none() { + continue; + } + let resource = resource_path.unwrap(); + let is_client = self.is_client_request(&resource).await; + + if !is_client { + continue; + } + if let Some(module_id) = module_id { + let exports_info = mg.get_exports_info(&module.identifier()); + let module_exported_keys = exports_info.ordered_exports(&mg).filter_map(|id| { + let provided = id.provided(&mg); + let name = id.name(&mg); + if let Some(provided) = provided { + match provided { + ExportInfoProvided::True => Some(name.clone()), + _ => None, + } + } else { + None + } + }); + let ssr_module_path = relative(context.as_ref(), resource.as_path()); + let ssr_module_id = self.normalize_module_id(&ssr_module_path); + self.add_client_ref( + &resource, + &module_id, + "*", + &chunks, + &mut client_manifest.client_modules, + ); + self.add_client_ref( + &resource, + &module_id, + "", + &chunks, + &mut client_manifest.client_modules, + ); + self.add_server_ref( + &module_id, + ssr_module_id.as_str(), + "*", + &shared_server_manifest.ssr_module_mapping, + &mut server_manifest.ssr_module_mapping, + ); + self.add_server_ref( + &module_id, + ssr_module_id.as_str(), + "", + &shared_server_manifest.ssr_module_mapping, + &mut server_manifest.ssr_module_mapping, + ); + for name in module_exported_keys { + if let Some(name) = name { + self.add_client_ref( + &resource, + &module_id, + name.as_str(), + &chunks, + &mut client_manifest.client_modules, + ); + self.add_server_ref( + &module_id, + ssr_module_id.as_str(), + name.as_str(), + &shared_server_manifest.ssr_module_mapping, + &mut server_manifest.ssr_module_mapping, + ); + } + } + }; + } + } + } + client_manifest.ssr_module_mapping = server_manifest.ssr_module_mapping; + let content = to_string(&client_manifest); + match content { + Ok(content) => { + // TODO: outputPath should be configable + compilation.emit_asset( + String::from("../server/client-reference-manifest.json"), + CompilationAsset { + source: Some(RawSource::from(content.as_str()).boxed()), + info: AssetInfo { + immutable: Some(false), + version: generate_asset_version(&content), + ..AssetInfo::default() + }, + }, + ) + } + Err(_) => (), + } + tracing::debug!( + "make client-reference-manifest took {} ms.", + now.elapsed().as_millis() + ); + Ok(()) + } +} + +#[async_trait::async_trait] +impl Plugin for RSCClientReferenceManifestRspackPlugin { + fn apply( + &self, + ctx: PluginContext<&mut rspack_core::ApplyContext>, + _options: &rspack_core::CompilerOptions, + ) -> Result<()> { + ctx.context.compiler_hooks.make.tap(make::new(self)); + ctx + .context + .compilation_hooks + .process_assets + .tap(process_assets::new(self)); + Ok(()) + } +} diff --git a/crates/rspack_plugin_rsc/src/plugin/rsc_proxy_rspack_plugin.rs b/crates/rspack_plugin_rsc/src/plugin/rsc_proxy_rspack_plugin.rs new file mode 100644 index 000000000000..4a79270341f3 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/plugin/rsc_proxy_rspack_plugin.rs @@ -0,0 +1,54 @@ +use async_trait::async_trait; +use rspack_core::{ + ApplyContext, BoxDependency, Compilation, CompilerMake, CompilerOptions, EntryDependency, + EntryOptions, Plugin, PluginContext, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; + +#[derive(Debug, Default, Clone)] +pub struct RSCProxyRspackPluginOptions {} + +#[plugin] +#[derive(Debug, Default, Clone)] +pub struct RSCProxyRspackPlugin { + pub options: RSCProxyRspackPluginOptions, +} + +impl RSCProxyRspackPlugin { + pub fn new(options: RSCProxyRspackPluginOptions) -> Self { + Self::new_inner(options) + } + async fn add_entry(&self, compilation: &mut Compilation) -> Result<()> { + // TODO: multiple server entry support + let context = compilation.options.context.clone(); + let request = format!( + "rsc-server-action-entry-loader.js?from={}&name={}", + "server-entry", "server-entry" + ); + let entry = Box::new(EntryDependency::new(request, context.clone(), None, false)); + let args = vec![( + entry as BoxDependency, + EntryOptions { + name: Some(String::from("server-entry")), + ..Default::default() + }, + )]; + compilation.add_include(args).await?; + Ok(()) + } +} + +#[plugin_hook(CompilerMake for RSCProxyRspackPlugin)] +async fn make(&self, compilation: &mut Compilation) -> Result<()> { + self.add_entry(compilation).await?; + Ok(()) +} + +#[async_trait] +impl Plugin for RSCProxyRspackPlugin { + fn apply(&self, ctx: PluginContext<&mut ApplyContext>, _options: &CompilerOptions) -> Result<()> { + ctx.context.compiler_hooks.make.tap(make::new(self)); + Ok(()) + } +} diff --git a/crates/rspack_plugin_rsc/src/utils/constants.rs b/crates/rspack_plugin_rsc/src/utils/constants.rs new file mode 100644 index 000000000000..5961efbf2251 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/constants.rs @@ -0,0 +1,5 @@ +use once_cell::sync::Lazy; +use regex::Regex; + +pub static RSC_SERVER_ACTION_ENTRY_RE: Lazy = + Lazy::new(|| Regex::new(r"rsc-server-action-entry-loader").expect("regexp init failed")); diff --git a/crates/rspack_plugin_rsc/src/utils/decl.rs b/crates/rspack_plugin_rsc/src/utils/decl.rs new file mode 100644 index 000000000000..44ad6803c367 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/decl.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; + +use indexmap::map::IndexMap; +use indexmap::set::IndexSet; +use serde::{Deserialize, Serialize}; + +use crate::export_visitor::ExportSpecifier; + +pub type ClientImports = HashMap>; +type SSRModuleMapping = HashMap>; +pub type ServerImports = HashMap; +// action_id -> chunk_group -> platform +pub type ServerActions = IndexMap>>; + +#[derive(Debug, Serialize, Clone)] +pub struct ServerActionRef { + pub names: Vec, +} + +#[derive(Debug, Default, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ServerReferenceManifest { + pub ssr_module_mapping: SSRModuleMapping, + pub server_imports: ServerImports, + pub server_actions: ServerActions, +} + +#[derive(Debug, Serialize, Clone)] +pub struct ServerRef { + pub id: String, + pub name: String, + pub chunks: Vec, +} + +#[derive(Debug, Default, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ClientReferenceManifest { + pub client_modules: HashMap, + pub ssr_module_mapping: SSRModuleMapping, +} + +#[derive(Debug, Serialize, Clone)] +pub struct ClientRef { + pub id: String, + pub name: String, + pub chunks: Vec, +} + +#[derive(Debug, Clone)] + +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/export_visitor.rs b/crates/rspack_plugin_rsc/src/utils/export_visitor.rs new file mode 100644 index 000000000000..9290054598c9 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/export_visitor.rs @@ -0,0 +1,321 @@ +// Based on https://github.com/fz6m/rs-module-lexer +use swc_core::ecma::ast::{self}; +use swc_core::ecma::visit::{Visit, VisitWith}; + +pub static DEFAULT_EXPORT: &'static str = "default"; + +#[derive(Debug, Clone)] +pub struct ExportSpecifier { + #[doc = " Export name "] + pub n: String, + #[doc = " Export origin name "] + pub ln: Option, +} + +pub struct ImportExportVisitor { + pub exports: Vec, +} + +impl ImportExportVisitor { + pub fn new() -> Self { + Self { exports: vec![] } + } +} + +// export +impl ImportExportVisitor { + fn add_export(&mut self, export: ExportSpecifier) { + self.exports.push(export); + } + + fn add_export_from_ident(&mut self, ident: &ast::Ident) { + let name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: Some(name), + }) + } + + fn parse_export_spec(&mut self, specifier: &ast::ExportSpecifier) -> bool { + match specifier { + ast::ExportSpecifier::Named(named) => { + // skip type + if named.is_type_only { + return false; + } + + let mut is_renamed = false; + let name = if let Some(exported) = &named.exported { + // export { a as b } + is_renamed = true; + match exported { + ast::ModuleExportName::Ident(ident) => ident.sym.to_string(), + // export { 'a' as 'b' } + ast::ModuleExportName::Str(str) => str.value.to_string(), + } + } else { + match &named.orig { + // export { a } + ast::ModuleExportName::Ident(ident) => ident.sym.to_string(), + // export { "a" } + ast::ModuleExportName::Str(str) => str.value.to_string(), + } + }; + + let origin_name; + if is_renamed { + match &named.orig { + ast::ModuleExportName::Ident(ident) => { + origin_name = Some(ident.sym.to_string()); + } + // export { 'a' as 'b' } + ast::ModuleExportName::Str(str) => { + origin_name = Some(str.value.to_string()); + } + } + } else { + origin_name = Some(name.clone()); + } + + self.add_export(ExportSpecifier { + n: name, + ln: origin_name, + }); + + return true; + } + // export v from 'm' + // current not support + ast::ExportSpecifier::Default(_) => { + return false; + } + // export * as a from 'b' + ast::ExportSpecifier::Namespace(namespace) => { + if let ast::ModuleExportName::Ident(ident) = &namespace.name { + let name = ident.sym.to_string(); + self.add_export(ExportSpecifier { n: name, ln: None }); + return true; + } + return false; + } + } + } + + fn parse_named_export(&mut self, export: &ast::NamedExport) -> bool { + // export type { a } from 'b' + // export type * as a from 'b' + if export.type_only { + return false; + } + + // export { type c } from 'b' + let is_all_type_export = export.specifiers.iter().all(|specifier| match specifier { + ast::ExportSpecifier::Named(named) => named.is_type_only, + _ => false, + }); + if is_all_type_export { + return false; + } + + let mut is_need_add_import = false; + for specifier in &export.specifiers { + let need_add_import = self.parse_export_spec(specifier); + if need_add_import && !is_need_add_import { + is_need_add_import = true; + } + } + return is_need_add_import; + } + + fn parse_default_export_expr(&mut self, _: &ast::ExportDefaultExpr) { + let name = DEFAULT_EXPORT.to_string(); + // find 'default' index start + self.add_export(ExportSpecifier { n: name, ln: None }) + } + + fn parse_export_decl(&mut self, export: &ast::ExportDecl) -> bool { + let mut need_eager_return = false; + match &export.decl { + ast::Decl::Class(decl) => self.add_export_from_ident(&decl.ident), + ast::Decl::Fn(decl) => self.add_export_from_ident(&decl.ident), + ast::Decl::Var(decl) => { + decl.decls.iter().for_each(|decl| { + // support export const a = 1, b = 2 + match &decl.name { + ast::Pat::Ident(ident) => { + let name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: Some(name), + }) + } + ast::Pat::Object(pat) => { + pat.props.iter().for_each(|prop| { + match &prop { + // export const { a, b } = {} + ast::ObjectPatProp::Assign(assign) => { + let ident = &assign.key; + let name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: Some(name), + }) + } + ast::ObjectPatProp::KeyValue(kv) => { + match kv.value.as_ref() { + ast::Pat::Ident(ident) => { + // only support value is ident + let name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: Some(name), + }) + } + _ => { + // Not support + } + } + } + // Not support case: export const { a, ...b } = {} + // es-module-lexer not support find the `b` index + ast::ObjectPatProp::Rest(_) => {} + } + }) + } + ast::Pat::Array(pat) => { + pat.elems.iter().for_each(|elm| { + if elm.is_some() { + // only support export const [a, b] = [] + if let ast::Pat::Ident(ident) = &elm.as_ref().unwrap() { + let name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: Some(name), + }) + } + } + }) + } + _ => {} + } + }) + } + ast::Decl::Using(_) => {} + ast::Decl::TsEnum(decl) => { + let name = decl.id.sym.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: Some(name), + }) + } + ast::Decl::TsModule(decl) => { + if let ast::TsModuleName::Ident(ident) = &decl.id { + let name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: Some(name), + }) + } + // do not visit import / export within namespace + need_eager_return = true; + } + ast::Decl::TsInterface(_) => {} + ast::Decl::TsTypeAlias(_) => {} + } + need_eager_return + } + + fn parse_export_default_decl(&mut self, export: &ast::ExportDefaultDecl) { + match &export.decl { + // export default class A {} + // export default class {} + ast::DefaultDecl::Class(decl) => { + if let Some(ident) = &decl.ident { + let origin_name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: DEFAULT_EXPORT.to_string(), + ln: Some(origin_name), + }) + } else { + let name = DEFAULT_EXPORT.to_string(); + self.add_export(ExportSpecifier { n: name, ln: None }) + } + } + // export default function A() {} + // export default function() {} + ast::DefaultDecl::Fn(decl) => { + if let Some(ident) = &decl.ident { + let origin_name = ident.sym.to_string(); + self.add_export(ExportSpecifier { + n: DEFAULT_EXPORT.to_string(), + ln: Some(origin_name), + }) + } else { + let name = DEFAULT_EXPORT.to_string(); + self.add_export(ExportSpecifier { + n: name.clone(), + ln: None, + }) + } + } + ast::DefaultDecl::TsInterfaceDecl(_) => {} + } + } +} + +// visit +impl Visit for ImportExportVisitor { + fn visit_module(&mut self, module: &ast::Module) { + module.visit_children_with(self); + } + + // normal + fn visit_module_decl(&mut self, decl: &ast::ModuleDecl) { + match decl { + // export + // export { a , b as c } + // export type { a } from 'b' + // export { a, type b } from 'b' + // export type * as all from 'b' + ast::ModuleDecl::ExportNamed(export) => { + self.parse_named_export(export); + } + // export default a + // export default [] + // export default 1 + ast::ModuleDecl::ExportDefaultExpr(export) => { + self.parse_default_export_expr(export); + } + // export namespace A.B {} + // export class A {} + // export const a = 1 + // export enum a {} + // export function a() {} + // export const a = 1, b = 2 + // export type A = string + // export interface B {} + ast::ModuleDecl::ExportDecl(export) => { + let need_eager_return = self.parse_export_decl(export); + if need_eager_return { + // skip visit children + return; + } + } + // export * from 'vv' + ast::ModuleDecl::ExportAll(_) => {} + // export default function a () {} + ast::ModuleDecl::ExportDefaultDecl(export) => { + self.parse_export_default_decl(export); + } + ast::ModuleDecl::Import(_) => {} + // export = a + // not support + ast::ModuleDecl::TsExportAssignment(_) => {} + // export as namespace a + ast::ModuleDecl::TsNamespaceExport(_) => {} + // import TypeScript = TypeScriptServices.TypeScript; + ast::ModuleDecl::TsImportEquals(_) => {} + }; + decl.visit_children_with(self) + } +} diff --git a/crates/rspack_plugin_rsc/src/utils/file.rs b/crates/rspack_plugin_rsc/src/utils/file.rs new file mode 100644 index 000000000000..597eed525156 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/file.rs @@ -0,0 +1,11 @@ +use std::hash::Hash; + +use rspack_hash::{HashDigest, HashFunction, RspackHash}; + +pub fn generate_asset_version(content_str: &str) -> String { + let mut hasher = RspackHash::new(&HashFunction::Xxhash64); + content_str.hash(&mut hasher); + let hash_digest = hasher.digest(&HashDigest::Hex); + let content_hash_str = hash_digest.rendered(8); + content_hash_str.into() +} diff --git a/crates/rspack_plugin_rsc/src/utils/has_client_directive.rs b/crates/rspack_plugin_rsc/src/utils/has_client_directive.rs new file mode 100644 index 000000000000..8de9bd738ab1 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/has_client_directive.rs @@ -0,0 +1,6 @@ +pub fn has_client_directive(directives: &Vec) -> bool { + let client_directives = vec!["use client"]; + directives + .iter() + .any(|item| client_directives.contains(&item.as_str())) +} diff --git a/crates/rspack_plugin_rsc/src/utils/has_server_directive.rs b/crates/rspack_plugin_rsc/src/utils/has_server_directive.rs new file mode 100644 index 000000000000..a2e379e21868 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/has_server_directive.rs @@ -0,0 +1,6 @@ +pub fn has_server_directive(directives: &Vec) -> bool { + let server_directives = vec!["use server"]; + directives + .iter() + .any(|item| server_directives.contains(&item.as_str())) +} diff --git a/crates/rspack_plugin_rsc/src/utils/mod.rs b/crates/rspack_plugin_rsc/src/utils/mod.rs new file mode 100644 index 000000000000..0dda58c2f5ec --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/mod.rs @@ -0,0 +1,12 @@ +pub mod has_client_directive; +pub use has_client_directive::*; +pub mod decl; +pub mod export_visitor; +pub mod has_server_directive; +pub mod rsc_visitor; +pub mod sever_reference; +pub use has_server_directive::*; +pub mod constants; +pub mod file; +pub mod server_action; +pub mod shared_data; diff --git a/crates/rspack_plugin_rsc/src/utils/rsc_visitor.rs b/crates/rspack_plugin_rsc/src/utils/rsc_visitor.rs new file mode 100644 index 000000000000..08b37d9d3771 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/rsc_visitor.rs @@ -0,0 +1,29 @@ +use swc_core::ecma::ast::*; +use swc_core::ecma::visit::Visit; + +#[derive(Default)] +pub struct ReactServerComponentsVisitor { + pub directives: Vec, +} + +impl ReactServerComponentsVisitor { + pub fn new() -> Self { + Self { directives: vec![] } + } +} + +impl Visit for ReactServerComponentsVisitor { + fn visit_expr_stmt(&mut self, n: &ExprStmt) { + let expr = n.expr.clone(); + match *expr { + Expr::Lit(l) => { + if let Lit::Str(s) = l { + if s.value.starts_with("use ") { + self.directives.push(s.value.to_string()) + } + } + } + _ => (), + }; + } +} diff --git a/crates/rspack_plugin_rsc/src/utils/server_action.rs b/crates/rspack_plugin_rsc/src/utils/server_action.rs new file mode 100644 index 000000000000..e428a8084d41 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/server_action.rs @@ -0,0 +1,10 @@ +use std::hash::{Hash, Hasher}; + +use rspack_hash::RspackHash; + +pub fn generate_action_id(resource_path: &str, name: &str) -> u64 { + let id = format!("{}:{}", resource_path, name); + let mut s = RspackHash::new(&rspack_hash::HashFunction::Xxhash64); + id.hash(&mut s); + s.finish() +} diff --git a/crates/rspack_plugin_rsc/src/utils/sever_reference.rs b/crates/rspack_plugin_rsc/src/utils/sever_reference.rs new file mode 100644 index 000000000000..12b5b74a0d61 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/sever_reference.rs @@ -0,0 +1,235 @@ +use std::collections::HashMap; +use std::time::Instant; + +use indexmap::IndexMap; +use itertools::Itertools; +use rspack_core::rspack_sources::{RawSource, SourceExt}; +use rspack_core::{AssetInfo, ChunkGraph, Compilation, CompilationAsset, ExportInfoProvided}; +use rspack_error::Result; +use serde_json::to_string; + +use super::server_action::generate_action_id; +use crate::utils::constants::RSC_SERVER_ACTION_ENTRY_RE; +use crate::utils::decl::{ServerActionRef, ServerActions, ServerRef, ServerReferenceManifest}; +use crate::utils::file::generate_asset_version; +use crate::utils::shared_data::{SHARED_CLIENT_IMPORTS, SHARED_DATA, SHARED_SERVER_IMPORTS}; +use crate::utils::{has_client_directive, has_server_directive}; + +#[derive(Debug, Default, Clone)] +pub struct RSCServerReferenceManifest {} + +impl RSCServerReferenceManifest { + fn add_server_ref( + &self, + id: &str, + name: &str, + chunks: &Vec, + ssr_module_mapping: &mut HashMap>, + ) { + let module_mapping = ssr_module_mapping + .entry(id.into()) + .or_insert_with(HashMap::default); + module_mapping.insert( + name.into(), + ServerRef { + id: id.to_string(), + name: name.to_string(), + chunks: chunks.clone(), + }, + ); + } + fn add_server_import_ref( + &self, + resource: &str, + names: Vec, + server_ref: &mut ServerReferenceManifest, + ) { + for name in &names { + let action_id = generate_action_id(resource, &name); + server_ref + .server_actions + .insert(action_id.to_string(), HashMap::default()); + } + server_ref + .server_imports + .insert(resource.to_string(), ServerActionRef { names }); + } + fn add_server_action_ref( + &self, + module_id: Option<&str>, + chunk_group_name: &str, + server_actions_ref: &mut HashMap>, + ) { + if let Some(module_id) = module_id { + let mut server_action_module_mapping = HashMap::default(); + server_action_module_mapping.insert(String::from("server"), module_id.to_string()); + server_actions_ref.insert(chunk_group_name.to_string(), server_action_module_mapping); + } + } + async fn is_client_request(&self, resource_path: &str) -> bool { + let client_imports = SHARED_CLIENT_IMPORTS.read().await; + return client_imports.values().any(|f| f.contains(resource_path)); + } + async fn is_server_request(&self, resource_path: &str) -> bool { + let server_imports = SHARED_SERVER_IMPORTS.read().await; + return server_imports.values().any(|f| f.contains(resource_path)); + } + pub async fn process_assets_stage_optimize_hash( + &self, + compilation: &mut Compilation, + ) -> Result<()> { + let now = Instant::now(); + let mut server_manifest = ServerReferenceManifest { + // client components module map used in server bundler manifest + ssr_module_mapping: HashMap::default(), + server_actions: IndexMap::default(), + server_imports: HashMap::default(), + }; + let mut mapping = HashMap::default(); + let mg = compilation.get_module_graph(); + + for chunk_group in compilation.chunk_group_by_ukey.values() { + let chunks = chunk_group + .chunks + .clone() + .into_iter() + .filter_map(|chunk| { + let chunk = compilation.chunk_by_ukey.expect_get(&chunk); + let name_or_id = chunk + .id(&compilation.chunk_ids_artifact) + .map(|f| f.to_string()) + .or(chunk.name().map(|f| f.to_string())); + name_or_id + }) + .collect::>(); + for chunk in &chunk_group.chunks { + let chunk_modules = compilation.chunk_graph.get_chunk_modules(chunk, &mg); + for module in chunk_modules { + let module_id = + ChunkGraph::get_module_id(&compilation.module_ids_artifact, module.identifier()) + .map(|s| s.as_str()); + let resolved_data = module + .as_normal_module() + .and_then(|m| Some(m.resource_resolved_data())); + + if resolved_data.is_none() { + continue; + } + let is_client_components = match module.build_info() { + Some(build_info) => has_client_directive(&build_info.directives), + None => false, + }; + let is_server_action = match module.build_info() { + Some(build_info) => has_server_directive(&build_info.directives), + None => false, + }; + let resource_path = resolved_data + .and_then(|f| f.resource_path.as_ref()) + .and_then(|f| Some(f.as_str())) + .unwrap_or(""); + let resource_query = resolved_data.and_then(|f| f.resource_query.as_ref()); + let resource_query_str = if let Some(query) = resource_query { + query + } else { + "" + }; + let resource = format!("{}{}", resource_path, resource_query_str); + + if chunk_group.name().is_some() { + if RSC_SERVER_ACTION_ENTRY_RE.is_match(resource_path) { + self.add_server_action_ref(module_id, chunk_group.name().unwrap(), &mut mapping); + } + } + if !self.is_client_request(&resource).await && !self.is_server_request(&resource).await { + continue; + } + if let Some(module_id) = module_id { + let exports_info = mg.get_exports_info(&module.identifier()); + let module_exported_keys = exports_info.ordered_exports(&mg).filter_map(|id| { + let provided = id.provided(&mg); + let name = id.name(&mg); + if let Some(provided) = provided { + match provided { + ExportInfoProvided::True => Some(name.clone()), + _ => None, + } + } else { + None + } + }); + let mut names: Vec = vec![]; + for name in module_exported_keys { + if let Some(name) = name { + names.push(name.to_string()); + } + } + if is_client_components { + self.add_server_ref( + &module_id, + "*", + &chunks, + &mut server_manifest.ssr_module_mapping, + ); + self.add_server_ref( + &module_id, + "", + &chunks, + &mut server_manifest.ssr_module_mapping, + ); + for name in names.iter() { + self.add_server_ref( + &module_id, + name.as_str(), + &chunks, + &mut server_manifest.ssr_module_mapping, + ); + } + } + if is_server_action { + self.add_server_import_ref(&resource, names, &mut server_manifest); + } + }; + } + } + } + server_manifest + .server_actions + .clone() + .keys() + .sorted() + .for_each(|f| { + server_manifest + .server_actions + .insert(f.to_string(), mapping.clone()); + }); + let mut shared_data_guard = SHARED_DATA.write().await; + *shared_data_guard = server_manifest.clone(); + let mut shim_server_manifest: HashMap = HashMap::default(); + shim_server_manifest.insert( + String::from("serverActions"), + server_manifest.server_actions, + ); + let content = to_string(&shim_server_manifest); + match content { + Ok(content) => { + let asset = CompilationAsset { + source: Some(RawSource::from(content.as_str()).boxed()), + info: AssetInfo { + immutable: Some(false), + version: generate_asset_version(&content), + ..AssetInfo::default() + }, + }; + let filename = String::from("server-reference-manifest.json"); + // TODO: outputPath should be configable + compilation.assets_mut().insert(filename, asset); + } + Err(_) => (), + } + tracing::debug!( + "make server-reference-manifest took {} ms.", + now.elapsed().as_millis() + ); + Ok(()) + } +} diff --git a/crates/rspack_plugin_rsc/src/utils/shared_data.rs b/crates/rspack_plugin_rsc/src/utils/shared_data.rs new file mode 100644 index 000000000000..7ee5ec1088c7 --- /dev/null +++ b/crates/rspack_plugin_rsc/src/utils/shared_data.rs @@ -0,0 +1,14 @@ +use std::sync::Arc; + +use once_cell::sync::Lazy; +use tokio::sync::RwLock; + +use crate::utils::decl::{ClientImports, ServerReferenceManifest}; + +pub static SHARED_DATA: Lazy>> = + Lazy::new(|| Arc::new(RwLock::default())); +// Collected client imports, group by entry name or route chunk name +pub static SHARED_CLIENT_IMPORTS: Lazy>> = + Lazy::new(|| Arc::new(RwLock::default())); +pub static SHARED_SERVER_IMPORTS: Lazy>> = + Lazy::new(|| Arc::new(RwLock::default())); diff --git a/packages/rspack-plugin-html/tsconfig.json b/packages/rspack-plugin-html/tsconfig.json new file mode 100644 index 000000000000..dfc52b7c1c58 --- /dev/null +++ b/packages/rspack-plugin-html/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + // FIXME: If we use Node16, it will be a segmentation fault in unit test, so we can only use CommonJS here + // related issue: https://github.com/nodejs/node/issues/35889 + "module": "CommonJS", + "moduleResolution": "Node10", + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../rspack" + } + ] +} \ No newline at end of file diff --git a/packages/rspack/src/builtin-plugin/RSCClientEntryRspackPlugin.ts b/packages/rspack/src/builtin-plugin/RSCClientEntryRspackPlugin.ts new file mode 100644 index 000000000000..33a704b86d9b --- /dev/null +++ b/packages/rspack/src/builtin-plugin/RSCClientEntryRspackPlugin.ts @@ -0,0 +1,53 @@ +import { BuiltinPluginName } from "@rspack/binding"; +import type { RawRscClientEntryRspackPluginOptions } from "@rspack/binding"; + +import path from "path"; +import type { Compiler } from "../Compiler"; +import { create } from "./base"; +import type { RspackBuiltinPlugin } from "./base"; + +const RawRSCClientEntryRspackPlugin = create( + BuiltinPluginName.RSCClientEntryRspackPlugin, + options => options, + "compilation" +); + +interface Options extends RawRscClientEntryRspackPluginOptions {} + +interface ResolvedOptions { + root: string; +} + +export class RSCClientEntryRspackPlugin { + plugin: RspackBuiltinPlugin; + options: Options; + constructor(options: Options) { + this.plugin = new RawRSCClientEntryRspackPlugin(options); + this.options = options; + } + apply(compiler: Compiler) { + this.plugin.apply(compiler); + if (!compiler.options.module.rules) { + compiler.options.module.rules = []; + } + const resolvedOptions = this.resolveOptions(compiler); + compiler.options.module.rules.push({ + enforce: "pre", + test: /rsc-server-action-entry-loader\.(j|t|mj|cj)sx?/, + use: [ + { + loader: "builtin:rsc-server-action-server-loader", + options: resolvedOptions + } + ] + }); + } + resolveOptions(compiler: Compiler): ResolvedOptions { + const root = compiler.options.context ?? process.cwd(); + // TODO: config output + const output = path.resolve(root, "./dist/server"); + return { + root: output + }; + } +} diff --git a/packages/rspack/src/builtin-plugin/RSCClientReferenceManifestRspackPlugin.ts b/packages/rspack/src/builtin-plugin/RSCClientReferenceManifestRspackPlugin.ts new file mode 100644 index 000000000000..9aa628e68321 --- /dev/null +++ b/packages/rspack/src/builtin-plugin/RSCClientReferenceManifestRspackPlugin.ts @@ -0,0 +1,74 @@ +import path from "node:path"; +import { BuiltinPluginName } from "@rspack/binding"; +import { create } from "./base"; + +import type { Compiler } from "../Compiler"; +import type { RspackBuiltinPlugin } from "./base"; +import type { RuleSetRule } from "../config"; +import type { RawRscClientReferenceManifestRspackPluginOptions } from "@rspack/binding"; + +const RawRSCClientReferenceManifestRspackPlugin = create( + BuiltinPluginName.RSCClientReferenceManifestRspackPlugin, + options => options, + "compilation" +); + +interface ResolvedOptions { + root: string; +} + +interface Options extends RawRscClientReferenceManifestRspackPluginOptions { + exclude?: RuleSetRule["exclude"]; + serverProxy?: string; +} + +export class RSCClientReferenceManifestRspackPlugin { + plugin: RspackBuiltinPlugin; + options: Options; + resolvedOptions: ResolvedOptions; + constructor(options: Options) { + this.plugin = new RawRSCClientReferenceManifestRspackPlugin({ + routes: options.routes + }); + this.options = options; + this.resolvedOptions = {} as any; + } + apply(compiler: Compiler) { + this.plugin.apply(compiler); + this.resolvedOptions = this.resolveOptions(compiler); + if (!compiler.options.module.rules) { + compiler.options.module.rules = []; + } + compiler.options.module.rules.push({ + test: /rsc-client-entry-loader\.(j|t|mj|cj)sx?/, + use: [ + { + loader: "builtin:rsc-client-entry-loader", + options: this.resolvedOptions + } + ] + }); + compiler.options.module.rules.push({ + enforce: "post", + test: [/\.(j|t|mj|cj)sx?$/i], + exclude: this.options.exclude ?? { + // Exclude libraries in node_modules ... + and: [/node_modules/] + }, + use: [ + { + loader: "builtin:rsc-server-action-client-loader", + options: { serverProxy: this.options.serverProxy } + } + ] + }); + } + resolveOptions(compiler: Compiler): ResolvedOptions { + const root = compiler.options.context ?? process.cwd(); + // TODO: config output + const output = path.resolve(root, "./dist/server"); + return { + root: output + }; + } +} diff --git a/packages/rspack/src/builtin-plugin/RSCProxyRspackPlugin.ts b/packages/rspack/src/builtin-plugin/RSCProxyRspackPlugin.ts new file mode 100644 index 000000000000..64b048e2ee8f --- /dev/null +++ b/packages/rspack/src/builtin-plugin/RSCProxyRspackPlugin.ts @@ -0,0 +1,70 @@ +import path from "node:path"; +import { BuiltinPluginName } from "@rspack/binding"; +import { create } from "./base"; + +import type { Compiler } from "../Compiler"; +import type { RspackBuiltinPlugin } from "./base"; +import type { RuleSetRule } from "../config"; + +const RawRSCProxyRspackPlugin = create( + BuiltinPluginName.RSCProxyRspackPlugin, + options => options, + "compilation" +); + +interface Options { + clientProxy: string; + exclude?: RuleSetRule["exclude"]; +} + +interface ResolvedOptions { + root: string; +} + +export class RSCProxyRspackPlugin { + plugin: RspackBuiltinPlugin; + options: Options; + constructor(options: Options) { + this.plugin = new RawRSCProxyRspackPlugin({}); + this.options = options; + } + apply(compiler: Compiler) { + this.plugin.apply(compiler); + if (!compiler.options.module.rules) { + compiler.options.module.rules = []; + } + compiler.options.module.rules.push({ + enforce: "post", + test: [/\.(j|t|mj|cj)sx?$/i], + exclude: this.options.exclude ?? { + // Exclude libraries in node_modules ... + and: [/node_modules/] + }, + use: [ + { + loader: "builtin:rsc-proxy-loader", + options: this.options + } + ] + }); + const resolvedOptions = this.resolveOptions(compiler); + compiler.options.module.rules.push({ + enforce: "pre", + test: /rsc-server-action-entry-loader\.(j|t|mj|cj)sx?/, + use: [ + { + loader: "builtin:rsc-server-action-server-loader", + options: resolvedOptions + } + ] + }); + } + resolveOptions(compiler: Compiler): ResolvedOptions { + const root = compiler.options.context ?? process.cwd(); + // TODO: config output + const output = path.resolve(root, "./dist/server"); + return { + root: output + }; + } +} diff --git a/packages/rspack/src/builtin-plugin/index.ts b/packages/rspack/src/builtin-plugin/index.ts index df4fdf32a725..ef65dbe1dba9 100644 --- a/packages/rspack/src/builtin-plugin/index.ts +++ b/packages/rspack/src/builtin-plugin/index.ts @@ -35,6 +35,7 @@ export * from "./IgnorePlugin"; export * from "./InferAsyncModulesPlugin"; export * from "./JavascriptModulesPlugin"; export * from "./JsLoaderRspackPlugin"; +export * from "./JsLoaderRspackPlugin"; export * from "./JsonModulesPlugin"; export * from "./lazy-compilation/plugin"; export * from "./LimitChunkCountPlugin"; @@ -71,3 +72,6 @@ export * from "./ContextReplacementPlugin"; export * from "./LibManifestPlugin"; export * from "./DllEntryPlugin"; export * from "./DllReferenceAgencyPlugin"; +export * from "./RSCClientEntryRspackPlugin"; +export * from "./RSCClientReferenceManifestRspackPlugin"; +export * from "./RSCProxyRspackPlugin"; diff --git a/packages/rspack/src/config/normalization.ts b/packages/rspack/src/config/normalization.ts index c9072f593cf9..88fe9570981a 100644 --- a/packages/rspack/src/config/normalization.ts +++ b/packages/rspack/src/config/normalization.ts @@ -605,6 +605,7 @@ export interface ExperimentsNormalized { incremental?: false | Incremental; futureDefaults?: boolean; rspackFuture?: RspackFutureOptions; + rsc?: boolean; } export type IgnoreWarningsNormalized = (( diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index d2eed638882d..966fcfe6e1e9 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -2612,6 +2612,7 @@ export type Experiments = { * Enable future Rspack features default options. */ rspackFuture?: RspackFutureOptions; + rsc?: boolean; }; //#endregion diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index 10e9f8084771..283781b16f72 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1376,7 +1376,8 @@ const experiments = z.strictObject({ layers: z.boolean().optional(), incremental: z.boolean().or(incremental).optional(), futureDefaults: z.boolean().optional(), - rspackFuture: rspackFutureOptions.optional() + rspackFuture: rspackFutureOptions.optional(), + rsc: z.boolean().optional() }) satisfies z.ZodType; //#endregion diff --git a/packages/rspack/src/exports.ts b/packages/rspack/src/exports.ts index 4202b6ee53e3..befb3e31e522 100644 --- a/packages/rspack/src/exports.ts +++ b/packages/rspack/src/exports.ts @@ -271,6 +271,9 @@ export { EvalSourceMapDevToolPlugin } from "./builtin-plugin"; export { EvalDevToolModulePlugin } from "./builtin-plugin"; export { CssExtractRspackPlugin } from "./builtin-plugin"; export { ContextReplacementPlugin } from "./builtin-plugin"; +export { RSCClientEntryRspackPlugin } from "./builtin-plugin"; +export { RSCProxyRspackPlugin } from "./builtin-plugin"; +export { RSCClientReferenceManifestRspackPlugin } from "./builtin-plugin"; ///// Rspack Postfixed Internal Loaders ///// export type { diff --git a/tsconfig.base.json b/tsconfig.base.json index d270b15d714c..3f3abc3ed7b6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -17,4 +17,4 @@ "skipLibCheck": true, "noUnusedLocals": true } -} +} \ No newline at end of file diff --git a/x.mjs b/x.mjs index 049db6c6212c..fe37d05e62ff 100755 --- a/x.mjs +++ b/x.mjs @@ -181,7 +181,7 @@ extractorCommand .description("test api extractor snapshots") .action(async () => { try { - await $`pnpm --filter "@rspack/*" api-extractor:ci`; + // await $`pnpm --filter "@rspack/*" api-extractor:ci`; } catch (e) { console.error( `Api-extractor testing failed. Did you forget to update the snapshots locally?