From 2a1c9c773f7215f4b2c49f6152ec516655c02f62 Mon Sep 17 00:00:00 2001 From: jserfeng <1114550440@qq.com> Date: Tue, 5 Mar 2024 17:44:04 +0800 Subject: [PATCH] feat: lazy compilation --- Cargo.lock | 18 + crates/node_binding/binding.d.ts | 24 +- crates/node_binding/scripts/banner.d.ts | 2 + crates/rspack_binding_options/Cargo.toml | 1 + .../src/options/raw_builtins/mod.rs | 18 +- .../raw_builtins/raw_lazy_compilation.rs | 135 +++ .../src/dependency/dependency_type.rs | 2 + crates/rspack_core/src/module_factory.rs | 2 +- crates/rspack_core/src/utils/queue.rs | 8 + crates/rspack_database/src/database.rs | 8 + crates/rspack_macros/src/plugin.rs | 40 +- crates/rspack_plugin_javascript/Cargo.toml | 1 + .../rspack_plugin_lazy_compilation/Cargo.toml | 19 + .../rspack_plugin_lazy_compilation/expand.rs | 957 ++++++++++++++++++ .../src/backend.rs | 14 + .../src/dependency.rs | 54 + .../src/factory.rs | 50 + .../rspack_plugin_lazy_compilation/src/lib.rs | 6 + .../src/module.rs | 323 ++++++ .../src/plugin.rs | 199 ++++ crates/rspack_plugin_runtime/src/lib.rs | 2 - packages/rspack-dev-server/src/server.ts | 32 - packages/rspack/etc/api.md | 87 +- packages/rspack/src/Watching.ts | 4 + packages/rspack/src/builtin-plugin/base.ts | 2 +- packages/rspack/src/builtin-plugin/index.ts | 1 + .../lazy-compilation/backend.ts | 229 +++++ .../lazy-compilation/lazyCompilation.ts | 18 + .../builtin-plugin/lazy-compilation/plugin.ts | 68 ++ packages/rspack/src/config/normalization.ts | 11 +- packages/rspack/src/config/zod.ts | 16 +- packages/rspack/src/rspackOptionsApply.ts | 31 +- .../lazy-compilation/context/test.filter.js | 3 - .../context/webpack.config.js | 1 + .../lazy-compilation/https/test.filter.js | 3 +- .../lazy-compilation/https/webpack.config.js | 1 + .../module-test/test.filter.js | 3 - .../module-test/webpack.config.js | 3 +- .../only-entries/test.filter.js | 3 - .../lazy-compilation/simple/test.filter.js | 3 +- .../lazy-compilation/simple/webpack.config.js | 3 +- .../lazy-compilation/unrelated/test.filter.js | 3 - website/docs/en/config/experiments.mdx | 26 + website/docs/zh/config/experiments.mdx | 26 + 44 files changed, 2371 insertions(+), 89 deletions(-) create mode 100644 crates/rspack_binding_options/src/options/raw_builtins/raw_lazy_compilation.rs create mode 100644 crates/rspack_plugin_lazy_compilation/Cargo.toml create mode 100644 crates/rspack_plugin_lazy_compilation/expand.rs create mode 100644 crates/rspack_plugin_lazy_compilation/src/backend.rs create mode 100644 crates/rspack_plugin_lazy_compilation/src/dependency.rs create mode 100644 crates/rspack_plugin_lazy_compilation/src/factory.rs create mode 100644 crates/rspack_plugin_lazy_compilation/src/lib.rs create mode 100644 crates/rspack_plugin_lazy_compilation/src/module.rs create mode 100644 crates/rspack_plugin_lazy_compilation/src/plugin.rs create mode 100644 packages/rspack/src/builtin-plugin/lazy-compilation/backend.ts create mode 100644 packages/rspack/src/builtin-plugin/lazy-compilation/lazyCompilation.ts create mode 100644 packages/rspack/src/builtin-plugin/lazy-compilation/plugin.ts delete mode 100644 webpack-test/hotCases/lazy-compilation/context/test.filter.js delete mode 100644 webpack-test/hotCases/lazy-compilation/module-test/test.filter.js delete mode 100644 webpack-test/hotCases/lazy-compilation/only-entries/test.filter.js delete mode 100644 webpack-test/hotCases/lazy-compilation/unrelated/test.filter.js diff --git a/Cargo.lock b/Cargo.lock index d1c2850a9404..202b57293f20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2831,6 +2831,7 @@ dependencies = [ "rspack_plugin_ignore", "rspack_plugin_javascript", "rspack_plugin_json", + "rspack_plugin_lazy_compilation", "rspack_plugin_library", "rspack_plugin_limit_chunk_count", "rspack_plugin_merge_duplicate_chunks", @@ -3404,6 +3405,23 @@ dependencies = [ "rspack_error", ] +[[package]] +name = "rspack_plugin_lazy_compilation" +version = "0.1.0" +dependencies = [ + "async-trait", + "once_cell", + "rspack_core", + "rspack_error", + "rspack_hook", + "rspack_identifier", + "rspack_plugin_javascript", + "rspack_regex", + "rspack_util", + "rustc-hash", + "tokio", +] + [[package]] name = "rspack_plugin_library" version = "0.1.0" diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 1701083285a6..929c9b19fc77 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -7,6 +7,8 @@ export type JsFilename = | ((pathData: JsPathData, assetInfo?: JsAssetInfo) => string); export type LocalJsFilename = JsFilename; + +export type RawLazyCompilationTest = RegExp | ((m: JsModule) => boolean); /* -- banner.d.ts end -- */ /* -- napi-rs generated below -- */ @@ -171,7 +173,8 @@ export enum BuiltinPluginName { SwcCssMinimizerRspackPlugin = 'SwcCssMinimizerRspackPlugin', BundlerInfoRspackPlugin = 'BundlerInfoRspackPlugin', CssExtractRspackPlugin = 'CssExtractRspackPlugin', - JsLoaderRspackPlugin = 'JsLoaderRspackPlugin' + JsLoaderRspackPlugin = 'JsLoaderRspackPlugin', + LazyCompilationPlugin = 'LazyCompilationPlugin' } export function cleanupGlobalTrace(): void @@ -966,6 +969,14 @@ export interface RawJavascriptParserOptions { wrappedContextCritical: boolean } +export interface RawLazyCompilationOption { + module: (err: Error | null, arg: RawModuleArg) => any + test?: RawLazyCompilationTest + entries: boolean + imports: boolean + cacheable: boolean +} + export interface RawLibraryAuxiliaryComment { root?: string commonjs?: string @@ -1001,6 +1012,11 @@ export interface RawLimitChunkCountPluginOptions { maxChunks: number } +export interface RawModuleArg { + module: string + path: string +} + export interface RawModuleFilenameTemplateFnCtx { identifier: string shortIdentifier: string @@ -1015,6 +1031,12 @@ export interface RawModuleFilenameTemplateFnCtx { namespace: string } +export interface RawModuleInfo { + active: boolean + client: string + data: string +} + export interface RawModuleOptions { rules: Array parser?: Record diff --git a/crates/node_binding/scripts/banner.d.ts b/crates/node_binding/scripts/banner.d.ts index 052dc4f4f10e..b77c3ec55cb9 100644 --- a/crates/node_binding/scripts/banner.d.ts +++ b/crates/node_binding/scripts/banner.d.ts @@ -7,6 +7,8 @@ export type JsFilename = | ((pathData: JsPathData, assetInfo?: JsAssetInfo) => string); export type LocalJsFilename = JsFilename; + +export type RawLazyCompilationTest = RegExp | ((m: JsModule) => boolean); /* -- banner.d.ts end -- */ /* -- napi-rs generated below -- */ diff --git a/crates/rspack_binding_options/Cargo.toml b/crates/rspack_binding_options/Cargo.toml index c4f2ec697cb2..fec0d2226e14 100644 --- a/crates/rspack_binding_options/Cargo.toml +++ b/crates/rspack_binding_options/Cargo.toml @@ -40,6 +40,7 @@ rspack_plugin_html = { path = "../rspack_plugin_html" } rspack_plugin_ignore = { path = "../rspack_plugin_ignore" } rspack_plugin_javascript = { path = "../rspack_plugin_javascript" } rspack_plugin_json = { path = "../rspack_plugin_json" } +rspack_plugin_lazy_compilation = { path = "../rspack_plugin_lazy_compilation" } rspack_plugin_library = { path = "../rspack_plugin_library" } rspack_plugin_limit_chunk_count = { path = "../rspack_plugin_limit_chunk_count" } rspack_plugin_merge_duplicate_chunks = { path = "../rspack_plugin_merge_duplicate_chunks" } diff --git a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs index 68284bc97f81..4e02139ed3e2 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs @@ -4,6 +4,7 @@ mod raw_copy; mod raw_css_extract; mod raw_html; mod raw_ignore; +mod raw_lazy_compilation; mod raw_limit_chunk_count; mod raw_mf; mod raw_progress; @@ -14,7 +15,7 @@ mod raw_to_be_deprecated; use napi::{bindgen_prelude::FromNapiValue, Env, JsUnknown}; use napi_derive::napi; -use rspack_core::{BoxPlugin, Define, DefinePlugin, PluginExt, Provide, ProvidePlugin}; +use rspack_core::{BoxPlugin, Define, DefinePlugin, Plugin, PluginExt, Provide, ProvidePlugin}; use rspack_error::Result; use rspack_ids::{ DeterministicChunkIdsPlugin, DeterministicModuleIdsPlugin, NamedChunkIdsPlugin, @@ -79,6 +80,7 @@ pub use self::{ use self::{ raw_bundle_info::{RawBundlerInfoModeWrapper, RawBundlerInfoPluginOptions}, raw_css_extract::RawCssExtractPluginOption, + raw_lazy_compilation::{JsBackend, RawLazyCompilationOption}, raw_mf::{RawConsumeSharedPluginOptions, RawContainerReferencePluginOptions, RawProvideOptions}, raw_runtime_chunk::RawRuntimeChunkOptions, raw_size_limits::RawSizeLimitsPluginOptions, @@ -165,6 +167,7 @@ pub enum BuiltinPluginName { // rspack js adapter plugins // naming format follow XxxRspackPlugin JsLoaderRspackPlugin, + LazyCompilationPlugin, } #[napi(object)] @@ -468,6 +471,19 @@ impl BuiltinPlugin { JsLoaderResolverPlugin::new(downcast_into::(self.options)?).boxed(), ); } + BuiltinPluginName::LazyCompilationPlugin => { + let options = downcast_into::(self.options)?; + let js_backend = JsBackend::from(&options); + plugins.push(Box::new( + rspack_plugin_lazy_compilation::plugin::LazyCompilationPlugin::new( + options.cacheable, + js_backend, + options.test.map(|test| test.into()), + options.entries, + options.imports, + ), + ) as Box) + } } Ok(()) } diff --git a/crates/rspack_binding_options/src/options/raw_builtins/raw_lazy_compilation.rs b/crates/rspack_binding_options/src/options/raw_builtins/raw_lazy_compilation.rs new file mode 100644 index 000000000000..5a74ea151917 --- /dev/null +++ b/crates/rspack_binding_options/src/options/raw_builtins/raw_lazy_compilation.rs @@ -0,0 +1,135 @@ +use napi::{ + bindgen_prelude::{FromNapiValue, ToNapiValue, ValidateNapiValue}, + Either, +}; +use napi_derive::napi; +use rspack_binding_values::{JsModule, ToJsModule}; +use rspack_core::ModuleIdentifier; +use rspack_napi::threadsafe_function::ThreadsafeFunction; +use rspack_plugin_lazy_compilation::{ + backend::{Backend, ModuleInfo}, + plugin::{LazyCompilationTest, LazyCompilationTestCheck}, +}; +use rspack_regex::RspackRegex; + +use crate::RawRegexMatcher; + +#[derive(Debug)] +pub struct RawLazyCompilationTest>>( + pub Either, +); + +impl FromNapiValue for RawLazyCompilationTest { + unsafe fn from_napi_value( + env: napi::sys::napi_env, + napi_val: napi::sys::napi_value, + ) -> napi::Result { + Ok(Self(Either::from_napi_value(env, napi_val)?)) + } +} + +impl ToNapiValue for RawLazyCompilationTest { + unsafe fn to_napi_value( + env: napi::sys::napi_env, + val: Self, + ) -> napi::Result { + Either::to_napi_value(env, val.0) + } +} + +#[derive(Debug)] +pub struct LazyCompilationTestFn { + tsfn: ThreadsafeFunction>, +} + +impl LazyCompilationTestCheck for LazyCompilationTestFn { + fn test(&self, m: &dyn rspack_core::Module) -> bool { + let res = self + .tsfn + .blocking_call_with_sync( + m.to_js_module() + .expect("failed to convert module to js module"), + ) + .expect("failed to invoke lazyCompilation.test"); + + res.unwrap_or(false) + } +} + +impl From for LazyCompilationTest { + fn from(value: RawLazyCompilationTest) -> Self { + match value.0 { + Either::A(regex) => Self::Regex( + RspackRegex::with_flags(®ex.source, ®ex.flags).unwrap_or_else(|_| { + let msg = format!("[lazyCompilation]incorrect regex {:?}", regex); + panic!("{msg}"); + }), + ), + Either::B(tsfn) => Self::Fn(LazyCompilationTestFn { tsfn }), + } + } +} + +#[napi(object)] +pub struct RawModuleInfo { + pub active: bool, + pub client: String, + pub data: String, +} + +#[napi(object, object_to_js = false)] +pub struct RawLazyCompilationOption { + pub module: ThreadsafeFunction, + pub test: Option, + pub entries: bool, + pub imports: bool, + pub cacheable: bool, +} + +#[napi(object)] +pub struct RawModuleArg { + pub module: String, + pub path: String, +} + +pub(crate) struct JsBackend { + module: ThreadsafeFunction, +} + +impl std::fmt::Debug for JsBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JsBackend").finish() + } +} + +impl From<&RawLazyCompilationOption> for JsBackend { + fn from(value: &RawLazyCompilationOption) -> Self { + Self { + module: value.module.clone(), + } + } +} + +#[async_trait::async_trait] +impl Backend for JsBackend { + async fn module( + &mut self, + identifier: ModuleIdentifier, + path: String, + ) -> rspack_error::Result { + let module_info = self + .module + .call(RawModuleArg { + module: identifier.to_string(), + path, + }) + .await + .expect("channel should have result"); + + Ok(ModuleInfo { + active: module_info.active, + client: module_info.client, + data: module_info.data, + }) + } +} diff --git a/crates/rspack_core/src/dependency/dependency_type.rs b/crates/rspack_core/src/dependency/dependency_type.rs index 757dd6920fd5..79838cc5f92f 100644 --- a/crates/rspack_core/src/dependency/dependency_type.rs +++ b/crates/rspack_core/src/dependency/dependency_type.rs @@ -93,6 +93,7 @@ pub enum DependencyType { /// Webpack is included WebpackIsIncluded, LoaderImport, + LazyImport, Custom(Box), // TODO it will increase large layout size } @@ -149,6 +150,7 @@ impl DependencyType { DependencyType::ProvideModuleForShared => Cow::Borrowed("provide module for shared"), DependencyType::ConsumeSharedFallback => Cow::Borrowed("consume shared fallback"), DependencyType::WebpackIsIncluded => Cow::Borrowed("__webpack_is_included__"), + DependencyType::LazyImport => Cow::Borrowed("lazy import()"), } } } diff --git a/crates/rspack_core/src/module_factory.rs b/crates/rspack_core/src/module_factory.rs index b592598eeb01..38dd7319f867 100644 --- a/crates/rspack_core/src/module_factory.rs +++ b/crates/rspack_core/src/module_factory.rs @@ -6,7 +6,7 @@ use sugar_path::SugarPath; use crate::{BoxDependency, BoxModule, Context, ModuleIdentifier, Resolve}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ModuleFactoryCreateData { pub resolve_options: Option>, pub context: Context, diff --git a/crates/rspack_core/src/utils/queue.rs b/crates/rspack_core/src/utils/queue.rs index f337e5789e22..70d1c1354793 100644 --- a/crates/rspack_core/src/utils/queue.rs +++ b/crates/rspack_core/src/utils/queue.rs @@ -32,6 +32,14 @@ impl WorkerQueue { } } + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + pub fn add_task(&mut self, task: T) -> usize { self.inner.push_back(task); self.inner.len() diff --git a/crates/rspack_database/src/database.rs b/crates/rspack_database/src/database.rs index afd6283f65b7..9bf7f6d4d01e 100644 --- a/crates/rspack_database/src/database.rs +++ b/crates/rspack_database/src/database.rs @@ -31,6 +31,14 @@ impl Database { } } + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + pub fn contains(&self, id: &Ukey) -> bool { self.inner.contains_key(id) } diff --git a/crates/rspack_macros/src/plugin.rs b/crates/rspack_macros/src/plugin.rs index 39c685512422..38d4e86490ce 100644 --- a/crates/rspack_macros/src/plugin.rs +++ b/crates/rspack_macros/src/plugin.rs @@ -7,7 +7,7 @@ use syn::{ pub fn expand_struct(mut input: syn::ItemStruct) -> proc_macro::TokenStream { let ident = &input.ident; let inner_ident = plugin_inner_ident(ident); - + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let inner_fields = input.fields.clone(); let is_named_struct = matches!(&inner_fields, syn::Fields::Named(_)); let is_unit_struct = matches!(&inner_fields, syn::Fields::Unit); @@ -19,7 +19,7 @@ pub fn expand_struct(mut input: syn::ItemStruct) -> proc_macro::TokenStream { input.fields = syn::Fields::Named( syn::FieldsNamed::parse - .parse2(quote! { { inner: ::std::sync::Arc<#inner_ident> } }) + .parse2(quote! { { inner: ::std::sync::Arc<#inner_ident #ty_generics> } }) .expect("Failed to parse"), ); @@ -51,33 +51,33 @@ pub fn expand_struct(mut input: syn::ItemStruct) -> proc_macro::TokenStream { let inner_struct = if is_named_struct { quote! { - pub struct #inner_ident #inner_fields + pub struct #inner_ident #impl_generics #where_clause #inner_fields } } else { quote! { - pub struct #inner_ident; + pub struct #inner_ident #impl_generics #where_clause; } }; let expanded = quote! { #input - impl #ident { + impl #impl_generics #ident #ty_generics #where_clause { #new_inner_fn - fn from_inner(inner: &::std::sync::Arc<#inner_ident>) -> Self { + fn from_inner(inner: &::std::sync::Arc<#inner_ident #ty_generics>) -> Self { Self { inner: ::std::sync::Arc::clone(inner), } } - fn inner(&self) -> &::std::sync::Arc<#inner_ident> { + fn inner(&self) -> &::std::sync::Arc<#inner_ident #ty_generics> { &self.inner } } - impl ::std::ops::Deref for #ident { - type Target = #inner_ident; + impl #impl_generics ::std::ops::Deref for #ident #ty_generics #where_clause { + type Target = #inner_ident #ty_generics; fn deref(&self) -> &Self::Target { &self.inner } @@ -99,6 +99,7 @@ pub struct HookArgs { trait_: syn::Path, name: syn::Ident, stage: Option, + generics: syn::Generics, } impl Parse for HookArgs { @@ -118,10 +119,13 @@ impl Parse for HookArgs { _ => return Err(input.error("expected \"stage\" or end of attribute")), } } + + let generics = input.parse::()?; Ok(Self { trait_, name, stage, + generics, }) } } @@ -131,7 +135,9 @@ pub fn expand_fn(args: HookArgs, input: syn::ItemFn) -> proc_macro::TokenStream name, trait_, stage, + generics, } = args; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let syn::ItemFn { mut sig, block, .. } = input; let real_sig = sig.clone(); let mut rest_args = Vec::new(); @@ -172,32 +178,32 @@ pub fn expand_fn(args: HookArgs, input: syn::ItemFn) -> proc_macro::TokenStream let expanded = quote! { #[allow(non_camel_case_types)] - struct #fn_ident { - inner: ::std::sync::Arc<#inner_ident>, + struct #fn_ident #impl_generics #where_clause { + inner: ::std::sync::Arc<#inner_ident #ty_generics>, } - impl #fn_ident { - pub(crate) fn new(plugin: &#name) -> Self { + impl #impl_generics #fn_ident #ty_generics #where_clause { + pub(crate) fn new(plugin: &#name #ty_generics) -> Self { #fn_ident { inner: ::std::sync::Arc::clone(plugin.inner()), } } } - impl #name { + impl #impl_generics #name #ty_generics #where_clause { #[allow(clippy::ptr_arg)] #real_sig #block } - impl ::std::ops::Deref for #fn_ident { - type Target = #inner_ident; + impl #impl_generics ::std::ops::Deref for #fn_ident #ty_generics #where_clause { + type Target = #inner_ident #ty_generics; fn deref(&self) -> &Self::Target { &self.inner } } #attr - impl #trait_ for #fn_ident { + impl #impl_generics #trait_ for #fn_ident #ty_generics #where_clause { #sig { #call_real_fn } diff --git a/crates/rspack_plugin_javascript/Cargo.toml b/crates/rspack_plugin_javascript/Cargo.toml index 32410fa9c7ea..1e5c639895e9 100644 --- a/crates/rspack_plugin_javascript/Cargo.toml +++ b/crates/rspack_plugin_javascript/Cargo.toml @@ -44,6 +44,7 @@ swc_core = { workspace = true, features = [ "ecma_transforms_typescript", "base", "ecma_quote", + "base", ] } swc_node_comments = { workspace = true } url = { workspace = true } diff --git a/crates/rspack_plugin_lazy_compilation/Cargo.toml b/crates/rspack_plugin_lazy_compilation/Cargo.toml new file mode 100644 index 000000000000..9a61fd5cd60e --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2021" +license = "MIT" +name = "rspack_plugin_lazy_compilation" +version = "0.1.0" + +[dependencies] +async-trait = { workspace = true } +once_cell = { workspace = true } +rustc-hash = { workspace = true } +tokio = { workspace = true } + +rspack_core = { path = "../rspack_core" } +rspack_error = { path = "../rspack_error" } +rspack_hook = { path = "../rspack_hook" } +rspack_identifier = { path = "../rspack_identifier" } +rspack_plugin_javascript = { path = "../rspack_plugin_javascript" } +rspack_regex = { path = "../rspack_regex" } +rspack_util = { path = "../rspack_util" } diff --git a/crates/rspack_plugin_lazy_compilation/expand.rs b/crates/rspack_plugin_lazy_compilation/expand.rs new file mode 100644 index 000000000000..03b6edfbaae6 --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/expand.rs @@ -0,0 +1,957 @@ +#![feature(prelude_import)] +#![feature(let_chains)] +#[prelude_import] +use std::prelude::rust_2021::*; +#[macro_use] +extern crate std; +pub mod backend { + use rspack_core::ModuleIdentifier; + use rspack_error::Result; + pub struct ModuleInfo { + pub active: bool, + pub data: String, + pub client: String, + } + pub trait Backend: std::fmt::Debug + Send + Sync { + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn module<'life0, 'async_trait>( + &'life0 mut self, + original_module: ModuleIdentifier, + path: String, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future> + + ::core::marker::Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait; + } +} +mod dependency { + use std::path::PathBuf; + + use rspack_core::{ + AsContextDependency, AsDependencyTemplate, Context, Dependency, DependencyCategory, + DependencyId, DependencyType, ModuleDependency, NormalModuleCreateData, Resolve, ResourceData, + }; + use rspack_error::Diagnostic; + use rspack_identifier::Identifier; + use rustc_hash::FxHashSet as HashSet; + pub(crate) struct ProxyCreateData { + pub resolve_options: Option>, + pub resource_resolve_data: ResourceData, + pub context: Context, + pub issuer: Option>, + pub issuer_identifier: Option, + pub file_dependencies: HashSet, + pub context_dependencies: HashSet, + pub missing_dependencies: HashSet, + pub diagnostics: Vec, + } + #[automatically_derived] + impl ::core::fmt::Debug for ProxyCreateData { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + let names: &'static _ = &[ + "resolve_options", + "resource_resolve_data", + "context", + "issuer", + "issuer_identifier", + "file_dependencies", + "context_dependencies", + "missing_dependencies", + "diagnostics", + ]; + let values: &[&dyn ::core::fmt::Debug] = &[ + &self.resolve_options, + &self.resource_resolve_data, + &self.context, + &self.issuer, + &self.issuer_identifier, + &self.file_dependencies, + &self.context_dependencies, + &self.missing_dependencies, + &&self.diagnostics, + ]; + ::core::fmt::Formatter::debug_struct_fields_finish(f, "ProxyCreateData", names, values) + } + } + #[automatically_derived] + impl ::core::clone::Clone for ProxyCreateData { + #[inline] + fn clone(&self) -> ProxyCreateData { + ProxyCreateData { + resolve_options: ::core::clone::Clone::clone(&self.resolve_options), + resource_resolve_data: ::core::clone::Clone::clone(&self.resource_resolve_data), + context: ::core::clone::Clone::clone(&self.context), + issuer: ::core::clone::Clone::clone(&self.issuer), + issuer_identifier: ::core::clone::Clone::clone(&self.issuer_identifier), + file_dependencies: ::core::clone::Clone::clone(&self.file_dependencies), + context_dependencies: ::core::clone::Clone::clone(&self.context_dependencies), + missing_dependencies: ::core::clone::Clone::clone(&self.missing_dependencies), + diagnostics: ::core::clone::Clone::clone(&self.diagnostics), + } + } + } + impl ProxyCreateData { + pub(crate) fn new(module_create_data: &NormalModuleCreateData) -> Self { + Self { + resolve_options: module_create_data.create_data.resolve_options.clone(), + resource_resolve_data: module_create_data.resource_resolve_data.clone(), + context: module_create_data.context.clone(), + issuer: module_create_data.create_data.issuer.clone(), + issuer_identifier: module_create_data.create_data.issuer_identifier, + file_dependencies: module_create_data.create_data.file_dependencies.clone(), + context_dependencies: module_create_data.create_data.context_dependencies.clone(), + missing_dependencies: module_create_data.create_data.missing_dependencies.clone(), + diagnostics: module_create_data.diagnostics.clone(), + } + } + } + pub(crate) struct LazyCompilationDependency { + id: DependencyId, + pub original_module_create_data: ProxyCreateData, + request: String, + } + #[automatically_derived] + impl ::core::fmt::Debug for LazyCompilationDependency { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field3_finish( + f, + "LazyCompilationDependency", + "id", + &self.id, + "original_module_create_data", + &self.original_module_create_data, + "request", + &&self.request, + ) + } + } + #[automatically_derived] + impl ::core::clone::Clone for LazyCompilationDependency { + #[inline] + fn clone(&self) -> LazyCompilationDependency { + LazyCompilationDependency { + id: ::core::clone::Clone::clone(&self.id), + original_module_create_data: ::core::clone::Clone::clone(&self.original_module_create_data), + request: ::core::clone::Clone::clone(&self.request), + } + } + } + impl LazyCompilationDependency { + pub fn new(original_module_create_data: ProxyCreateData) -> Self { + let request = { + let res = ::alloc::fmt::format(format_args!( + "{0}?lazy-compilation-proxy-dep", + &original_module_create_data.resource_resolve_data.resource + )); + res + }; + Self { + id: DependencyId::new(), + original_module_create_data, + request, + } + } + } + impl ModuleDependency for LazyCompilationDependency { + fn request(&self) -> &str { + &self.request + } + } + impl AsDependencyTemplate for LazyCompilationDependency {} + impl AsContextDependency for LazyCompilationDependency {} + impl Dependency for LazyCompilationDependency { + fn dependency_debug_name(&self) -> &'static str { + "lazy compilation dependency" + } + fn id(&self) -> &rspack_core::DependencyId { + &self.id + } + fn category(&self) -> &DependencyCategory { + &DependencyCategory::Esm + } + fn dependency_type(&self) -> &DependencyType { + &DependencyType::LazyImport + } + } +} +mod factory { + use std::sync::Arc; + + use rspack_core::{ + ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult, NormalModuleFactory, + }; + use rspack_error::Result; + + use crate::dependency::LazyCompilationDependency; + pub(crate) struct LazyCompilationDependencyFactory { + normal_module_factory: Arc, + } + #[automatically_derived] + impl ::core::fmt::Debug for LazyCompilationDependencyFactory { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "LazyCompilationDependencyFactory", + "normal_module_factory", + &&self.normal_module_factory, + ) + } + } + impl LazyCompilationDependencyFactory { + pub fn new(normal_module_factory: Arc) -> Self { + Self { + normal_module_factory, + } + } + } + impl ModuleFactory for LazyCompilationDependencyFactory { + #[allow( + clippy::async_yields_async, + clippy::diverging_sub_expression, + clippy::let_unit_value, + clippy::no_effect_underscore_binding, + clippy::shadow_same, + clippy::type_complexity, + clippy::type_repetition_in_bounds, + clippy::used_underscore_binding + )] + fn create<'life0, 'life1, 'async_trait>( + &'life0 self, + data: &'life1 mut ModuleFactoryCreateData, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future> + + ::core::marker::Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + 'life1: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = + ::core::option::Option::None::> + { + return __ret; + } + let __self = self; + let __ret: Result = { + let dep: &LazyCompilationDependency = data + .dependency + .as_any() + .downcast_ref() + .expect("should be lazy compile dependency"); + let proxy_data = &dep.original_module_create_data; + let dep = dep.clone(); + let mut create_data = ModuleFactoryCreateData { + resolve_options: proxy_data.resolve_options.clone(), + context: proxy_data.context.clone(), + dependency: Box::new(dep), + issuer: proxy_data.issuer.clone(), + issuer_identifier: proxy_data.issuer_identifier, + file_dependencies: proxy_data.file_dependencies.clone(), + context_dependencies: proxy_data.context_dependencies.clone(), + missing_dependencies: proxy_data.missing_dependencies.clone(), + diagnostics: proxy_data.diagnostics.clone(), + }; + __self.normal_module_factory.create(&mut create_data).await + }; + #[allow(unreachable_code)] + __ret + }) + } + } +} +mod module { + use std::{hash::Hash, path::PathBuf, sync::Arc}; + + use rspack_core::{ + impl_build_info_meta, module_namespace_promise, + rspack_sources::{RawSource, Source}, + AsyncDependenciesBlock, AsyncDependenciesBlockIdentifier, BoxDependency, BuildContext, + BuildInfo, BuildMeta, BuildResult, CodeGenerationResult, Compilation, ConcatenationScope, + Context, DependenciesBlock, DependencyId, Module, ModuleIdentifier, ModuleType, RuntimeGlobals, + RuntimeSpec, SourceType, TemplateContext, + }; + use rspack_error::{Diagnosable, Diagnostic, Result}; + use rspack_identifier::Identifiable; + use rspack_plugin_javascript::dependency::CommonJsRequireDependency; + use rspack_util::source_map::{ModuleSourceMapConfig, SourceMapKind}; + use rustc_hash::FxHashSet; + + use crate::dependency::{LazyCompilationDependency, ProxyCreateData}; + static MODULE_TYPE: ModuleType = ModuleType::Js; + static SOURCE_TYPE: [SourceType; 1] = [SourceType::JavaScript]; + pub(crate) struct LazyCompilationProxyModule { + build_info: Option, + build_meta: Option, + original_module: ModuleIdentifier, + cacheable: bool, + readable_identifier: String, + identifier: ModuleIdentifier, + blocks: Vec, + dependencies: Vec, + source_map_kind: SourceMapKind, + create_data: ProxyCreateData, + pub request: String, + pub active: bool, + pub data: String, + pub client: String, + } + #[automatically_derived] + impl ::core::fmt::Debug for LazyCompilationProxyModule { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + let names: &'static _ = &[ + "build_info", + "build_meta", + "original_module", + "cacheable", + "readable_identifier", + "identifier", + "blocks", + "dependencies", + "source_map_kind", + "create_data", + "request", + "active", + "data", + "client", + ]; + let values: &[&dyn ::core::fmt::Debug] = &[ + &self.build_info, + &self.build_meta, + &self.original_module, + &self.cacheable, + &self.readable_identifier, + &self.identifier, + &self.blocks, + &self.dependencies, + &self.source_map_kind, + &self.create_data, + &self.request, + &self.active, + &self.data, + &&self.client, + ]; + ::core::fmt::Formatter::debug_struct_fields_finish( + f, + "LazyCompilationProxyModule", + names, + values, + ) + } + } + impl Hash for LazyCompilationProxyModule { + fn hash(&self, state: &mut H) { + self.build_meta.hash(state); + self.original_module.hash(state); + self.readable_identifier.hash(state); + self.identifier.hash(state); + self.blocks.hash(state); + self.dependencies.hash(state); + } + } + impl PartialEq for LazyCompilationProxyModule { + fn eq(&self, other: &Self) -> bool { + self.original_module == other.original_module + && self.readable_identifier == other.readable_identifier + && self.identifier == other.identifier + } + } + impl Eq for LazyCompilationProxyModule {} + impl ModuleSourceMapConfig for LazyCompilationProxyModule { + fn get_source_map_kind(&self) -> &SourceMapKind { + &self.source_map_kind + } + fn set_source_map_kind(&mut self, source_map: SourceMapKind) { + self.source_map_kind = source_map; + } + } + impl LazyCompilationProxyModule { + pub(crate) fn new( + original_module: ModuleIdentifier, + create_data: ProxyCreateData, + request: String, + cacheable: bool, + active: bool, + data: String, + client: String, + ) -> Self { + let readable_identifier = { + let res = ::alloc::fmt::format(format_args!( + "lazy-compilation-proxy|{0}", + create_data.context.shorten(&original_module) + )); + res + }; + let identifier = { + let res = ::alloc::fmt::format(format_args!("lazy-compilation-proxy|{0}", original_module)); + res + } + .into(); + Self { + build_info: None, + build_meta: None, + cacheable, + original_module, + create_data, + readable_identifier, + identifier, + source_map_kind: SourceMapKind::None, + blocks: ::alloc::vec::Vec::new(), + dependencies: ::alloc::vec::Vec::new(), + active, + request, + client, + data, + } + } + } + impl Diagnosable for LazyCompilationProxyModule { + fn add_diagnostic(&self, _diagnostic: Diagnostic) { + ::core::panicking::panic("not implemented") + } + fn add_diagnostics(&self, _diagnostics: Vec) { + ::core::panicking::panic("not implemented") + } + } + impl Module for LazyCompilationProxyModule { + fn build_info(&self) -> Option<&::rspack_core::BuildInfo> { + self.build_info.as_ref() + } + fn build_meta(&self) -> Option<&::rspack_core::BuildMeta> { + self.build_meta.as_ref() + } + fn set_module_build_info_and_meta( + &mut self, + build_info: ::rspack_core::BuildInfo, + build_meta: ::rspack_core::BuildMeta, + ) { + self.build_info = Some(build_info); + self.build_meta = Some(build_meta); + } + fn source_types(&self) -> &[SourceType] { + &SOURCE_TYPE + } + fn module_type(&self) -> &ModuleType { + &MODULE_TYPE + } + fn size(&self, _source_type: &SourceType) -> f64 { + 200f64 + } + fn original_source(&self) -> Option<&dyn Source> { + None + } + fn readable_identifier(&self, _context: &Context) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(&self.readable_identifier) + } + fn get_diagnostics(&self) -> Vec { + ::alloc::vec::Vec::new() + } + #[allow( + clippy::async_yields_async, + clippy::diverging_sub_expression, + clippy::let_unit_value, + clippy::no_effect_underscore_binding, + clippy::shadow_same, + clippy::type_complexity, + clippy::type_repetition_in_bounds, + clippy::used_underscore_binding + )] + fn build<'life0, 'life1, 'life2, 'async_trait>( + &'life0 mut self, + _build_context: BuildContext<'life1>, + _compilation: Option<&'life2 Compilation>, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future> + + ::core::marker::Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = + ::core::option::Option::None::> + { + return __ret; + } + let mut __self = self; + let _build_context = _build_context; + let _compilation = _compilation; + let __ret: Result = { + let client_dep = CommonJsRequireDependency::new(__self.client.clone(), None, 0, 0, false); + let mut dependencies = ::alloc::vec::Vec::new(); + let mut blocks = ::alloc::vec::Vec::new(); + dependencies.push(Box::new(client_dep) as BoxDependency); + if __self.active { + let dep = LazyCompilationDependency::new(__self.create_data.clone()); + blocks.push(AsyncDependenciesBlock::new( + __self.identifier, + None, + None, + <[_]>::into_vec( + #[rustc_box] + ::alloc::boxed::Box::new([Box::new(dep)]), + ), + )); + } + let mut files = FxHashSet::default(); + files.extend(__self.create_data.file_dependencies.clone()); + files.insert(PathBuf::from( + &__self.create_data.resource_resolve_data.resource, + )); + Ok(BuildResult { + build_info: BuildInfo { + cacheable: __self.cacheable, + file_dependencies: files, + ..Default::default() + }, + build_meta: BuildMeta::default(), + analyze_result: Default::default(), + dependencies, + blocks, + optimization_bailouts: ::alloc::vec::Vec::new(), + }) + }; + #[allow(unreachable_code)] + __ret + }) + } + fn code_generation( + &self, + compilation: &Compilation, + _runtime: Option<&RuntimeSpec>, + mut concatenation_scope: Option, + ) -> Result { + let mut runtime_requirements = RuntimeGlobals::empty(); + runtime_requirements.insert(RuntimeGlobals::MODULE); + runtime_requirements.insert(RuntimeGlobals::REQUIRE); + let client_dep_id = self.dependencies[0]; + let module_graph = &compilation.get_module_graph(); + let chunk_graph = &compilation.chunk_graph; + let client_module = module_graph + .module_identifier_by_dependency_id(&client_dep_id) + .expect("should have module"); + let block = self.blocks.first(); + let client = { + let res = ::alloc::fmt::format(format_args!( + "var client = __webpack_require__(\"{0}\");\nvar data = \"{1}\"", + chunk_graph + .get_module_id(*client_module) + .as_ref() + .expect("should have module id"), + self.data + )); + res + }; + let keep_active = { + let res = ::alloc::fmt::format( + format_args!( + "var dispose = client.keepAlive({{ data: data, active: {0}, module: module, onError: onError }})", + block.is_some() + ), + ); + res + }; + let source = if let Some(block_id) = block { + let block = module_graph + .block_by_id(block_id) + .expect("should have block"); + let dep_id = block.get_dependencies()[0]; + let module = module_graph + .module_identifier_by_dependency_id(&dep_id) + .expect("should have module"); + let mut template_ctx = TemplateContext { + compilation, + module: module_graph + .module_by_identifier(module) + .expect("should have module") + .as_ref(), + runtime_requirements: &mut runtime_requirements, + init_fragments: &mut ::alloc::vec::Vec::new(), + runtime: None, + concatenation_scope: concatenation_scope.as_mut(), + }; + RawSource::from({ + let res = ::alloc::fmt::format( + format_args!( + "{3}\n module.exports = {0};\n if (module.hot) {{\n module.hot.accept();\n module.hot.accept(\"{1}\", function() {{ module.hot.invalidate(); }});\n module.hot.dispose(function(data) {{ delete data.resolveSelf; dispose(data); }});\n if (module.hot.data && module.hot.data.resolveSelf)\n module.hot.data.resolveSelf(module.exports);\n }}\n function onError() {{ /* ignore */ }}\n {2}\n ", + module_namespace_promise(& mut template_ctx, & dep_id, + Some(block_id), & self.request, "import()", false), + chunk_graph.get_module_id(* module).as_ref() + .expect("should have module id"), keep_active, client + ), + ); + res + }) + } else { + RawSource::from({ + let res = ::alloc::fmt::format( + format_args!( + "{0}\n var resolveSelf, onError;\n module.exports = new Promise(function(resolve, reject) {{ resolveSelf = resolve; onError = reject; }});\n if (module.hot) {{\n module.hot.accept();\n if (module.hot.data && module.hot.data.resolveSelf) module.hot.data.resolveSelf(module.exports);\n module.hot.dispose(function(data) {{ data.resolveSelf = resolveSelf; dispose(data); }});\n }}\n {1}\n ", + client, keep_active + ), + ); + res + }) + }; + let mut codegen_result = CodeGenerationResult::default().with_javascript(Arc::new(source)); + codegen_result.runtime_requirements = runtime_requirements; + codegen_result.set_hash( + &compilation.options.output.hash_function, + &compilation.options.output.hash_digest, + &compilation.options.output.hash_salt, + ); + Ok(codegen_result) + } + } + impl Identifiable for LazyCompilationProxyModule { + fn identifier(&self) -> rspack_identifier::Identifier { + self.identifier + } + } + impl DependenciesBlock for LazyCompilationProxyModule { + fn add_block_id(&mut self, block: rspack_core::AsyncDependenciesBlockIdentifier) { + self.blocks.push(block); + } + fn get_blocks(&self) -> &[rspack_core::AsyncDependenciesBlockIdentifier] { + &self.blocks + } + fn add_dependency_id(&mut self, dependency: rspack_core::DependencyId) { + self.dependencies.push(dependency); + } + fn get_dependencies(&self) -> &[rspack_core::DependencyId] { + &self.dependencies + } + } +} +pub mod plugin { + use std::sync::Arc; + + use once_cell::sync::Lazy; + use rspack_core::{ + BoxModule, Compilation, CompilationParams, DependencyType, ModuleFactory, + NormalModuleCreateData, Plugin, PluginContext, PluginNormalModuleFactoryModuleHookOutput, + }; + use rspack_hook::{plugin, plugin_hook, AsyncSeries2}; + use rspack_regex::RspackRegex; + use tokio::sync::Mutex; + + use crate::{ + backend::Backend, dependency::ProxyCreateData, factory::LazyCompilationDependencyFactory, + module::LazyCompilationProxyModule, + }; + static WEBPACK_DEV_SERVER_CLIENT_RE: Lazy = Lazy::new(|| { + RspackRegex::new( + r#"(webpack|rspack)[/\\]hot[/\\]|(webpack|rspack)-dev-server[/\\]client|(webpack|rspack)-hot-middleware[/\\]client"#, + ) + .expect("should compile regex") + }); + pub struct LazyCompilationPlugin { + inner: ::std::sync::Arc>, + } + #[automatically_derived] + impl ::core::fmt::Debug for LazyCompilationPlugin { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "LazyCompilationPlugin", + "inner", + &&self.inner, + ) + } + } + impl LazyCompilationPlugin { + #[allow(clippy::too_many_arguments)] + fn new_inner( + backend: Mutex, + entries: bool, + imports: bool, + test: Option, + cacheable: bool, + ) -> Self { + Self { + inner: ::std::sync::Arc::new(LazyCompilationPluginInner { + backend, + entries, + imports, + test, + cacheable, + }), + } + } + fn from_inner(inner: &::std::sync::Arc>) -> Self { + Self { + inner: ::std::sync::Arc::clone(inner), + } + } + fn inner(&self) -> &::std::sync::Arc> { + &self.inner + } + } + impl ::std::ops::Deref for LazyCompilationPlugin { + type Target = LazyCompilationPluginInner; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + #[doc(hidden)] + pub struct LazyCompilationPluginInner { + backend: Mutex, + entries: bool, + imports: bool, + test: Option, + cacheable: bool, + } + #[automatically_derived] + impl ::core::fmt::Debug for LazyCompilationPluginInner { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field5_finish( + f, + "LazyCompilationPluginInner", + "backend", + &self.backend, + "entries", + &self.entries, + "imports", + &self.imports, + "test", + &self.test, + "cacheable", + &&self.cacheable, + ) + } + } + impl LazyCompilationPlugin { + pub fn new( + cacheable: bool, + backend: T, + test: Option, + entries: bool, + imports: bool, + ) -> Self { + Self::new_inner(Mutex::new(backend), entries, imports, test, cacheable) + } + fn check_test(&self, module: &BoxModule) -> bool { + if let Some(test) = &self.test { + test.test(&module.name_for_condition().unwrap_or("".into())) + } else { + true + } + } + } + #[allow(non_camel_case_types)] + struct compilation { + inner: ::std::sync::Arc>, + } + impl compilation { + pub(crate) fn new(plugin: &LazyCompilationPlugin) -> Box { + Box::new(compilation { + inner: ::std::sync::Arc::clone(plugin.inner()), + }) + } + } + impl LazyCompilationPlugin { + #[allow(clippy::ptr_arg)] + async fn compilation( + &self, + compilation: &mut Compilation, + params: &mut CompilationParams, + ) -> Result<()> { + Ok(()) + } + } + impl ::std::ops::Deref for compilation { + type Target = LazyCompilationPluginInner; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + impl AsyncSeries2 for compilation { + #[allow( + clippy::async_yields_async, + clippy::diverging_sub_expression, + clippy::let_unit_value, + clippy::no_effect_underscore_binding, + clippy::shadow_same, + clippy::type_complexity, + clippy::type_repetition_in_bounds, + clippy::used_underscore_binding + )] + fn run<'life0, 'life1, 'life2, 'async_trait>( + &'life0 self, + compilation: &'life1 mut Compilation, + params: &'life2 mut CompilationParams, + ) -> ::core::pin::Pin< + Box> + ::core::marker::Send + 'async_trait>, + > + where + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::> { + return __ret; + } + let __self = self; + let __ret: Result<()> = { + LazyCompilationPlugin::compilation( + &LazyCompilationPlugin::from_inner(&__self.inner), + compilation, + params, + ) + .await + }; + #[allow(unreachable_code)] + __ret + }) + } + } + impl Plugin for LazyCompilationPlugin { + #[allow( + clippy::async_yields_async, + clippy::diverging_sub_expression, + clippy::let_unit_value, + clippy::no_effect_underscore_binding, + clippy::shadow_same, + clippy::type_complexity, + clippy::type_repetition_in_bounds, + clippy::used_underscore_binding + )] + fn normal_module_factory_module<'life0, 'life1, 'life2, 'async_trait>( + &'life0 self, + _ctx: PluginContext, + module: BoxModule, + args: &'life1 mut NormalModuleCreateData<'life2>, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future + + ::core::marker::Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = + ::core::option::Option::None:: + { + return __ret; + } + let __self = self; + let _ctx = _ctx; + let module = module; + let __ret: PluginNormalModuleFactoryModuleHookOutput = { + if let Some(query) = &args.resource_resolve_data.resource_query + && query.contains("lazy-compilation-proxy-dep") + { + let remaining_query = query.clone().replace("lazy-compilation-proxy-dep", ""); + args.resource_resolve_data.resource_query = + if remaining_query.is_empty() || remaining_query == "?" { + None + } else { + Some(remaining_query) + }; + return Ok(module); + } + let create_data = args.create_data; + let dep_type = create_data.dependency.dependency_type(); + let is_imports = match dep_type { + DependencyType::DynamicImport + | DependencyType::DynamicImportEager + | DependencyType::ContextElement => true, + _ => false, + }; + let is_entries = match dep_type { + DependencyType::Entry => true, + _ => false, + }; + #[allow(clippy::if_same_then_else)] + if match dep_type { + DependencyType::ModuleHotAccept + | DependencyType::ModuleHotDecline + | DependencyType::ImportMetaHotAccept + | DependencyType::ImportMetaHotDecline => true, + _ => false, + } { + return Ok(module); + } else if !is_entries && !is_imports { + return Ok(module); + } + if !__self.entries && is_entries { + return Ok(module); + } + if !__self.imports && is_imports { + return Ok(module); + } + if WEBPACK_DEV_SERVER_CLIENT_RE.test(args.resolve_data_request) + || !__self.check_test(&module) + { + return Ok(module); + } + let mut backend = __self.backend.lock().await; + let module_identifier = module.identifier(); + let info = backend + .module( + module_identifier, + args.resource_resolve_data.resource.clone(), + ) + .await?; + match module_identifier { + tmp => { + { + ::std::io::_eprint(format_args!( + "[{0}:{1}:{2}] {3} = {4:#?}\n", + "crates/rspack_plugin_lazy_compilation/src/plugin.rs", + 151u32, + 5u32, + "module_identifier", + &tmp + )); + }; + tmp + } + }; + Ok(Box::new(LazyCompilationProxyModule::new( + module_identifier, + ProxyCreateData::new(args), + args.resolve_data_request.to_string(), + __self.cacheable, + info.active, + info.data, + info.client, + )) as BoxModule) + }; + #[allow(unreachable_code)] + __ret + }) + } + } +} diff --git a/crates/rspack_plugin_lazy_compilation/src/backend.rs b/crates/rspack_plugin_lazy_compilation/src/backend.rs new file mode 100644 index 000000000000..87f2e8d07f60 --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/src/backend.rs @@ -0,0 +1,14 @@ +use rspack_core::ModuleIdentifier; +use rspack_error::Result; + +pub struct ModuleInfo { + pub active: bool, + pub data: String, + pub client: String, +} + +#[async_trait::async_trait] +pub trait Backend: std::fmt::Debug + Send + Sync { + async fn module(&mut self, original_module: ModuleIdentifier, path: String) + -> Result; +} diff --git a/crates/rspack_plugin_lazy_compilation/src/dependency.rs b/crates/rspack_plugin_lazy_compilation/src/dependency.rs new file mode 100644 index 000000000000..b7562843bd49 --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/src/dependency.rs @@ -0,0 +1,54 @@ +use rspack_core::{ + AsContextDependency, AsDependencyTemplate, Dependency, DependencyCategory, DependencyId, + DependencyType, ModuleDependency, ModuleFactoryCreateData, +}; + +#[derive(Debug, Clone)] +pub(crate) struct LazyCompilationDependency { + id: DependencyId, + pub original_module_create_data: ModuleFactoryCreateData, + request: String, +} + +impl LazyCompilationDependency { + pub fn new(original_module_create_data: ModuleFactoryCreateData) -> Self { + let dep = original_module_create_data + .dependency + .as_module_dependency() + .expect("LazyCompilation: should convert to module dependency"); + let request = format!("{}?lazy-compilation-proxy-dep", dep.request()); + + Self { + id: DependencyId::new(), + original_module_create_data, + request, + } + } +} + +impl ModuleDependency for LazyCompilationDependency { + fn request(&self) -> &str { + &self.request + } +} + +impl AsDependencyTemplate for LazyCompilationDependency {} +impl AsContextDependency for LazyCompilationDependency {} + +impl Dependency for LazyCompilationDependency { + fn dependency_debug_name(&self) -> &'static str { + "lazy compilation dependency" + } + + fn id(&self) -> &rspack_core::DependencyId { + &self.id + } + + fn category(&self) -> &DependencyCategory { + &DependencyCategory::Esm + } + + fn dependency_type(&self) -> &DependencyType { + &DependencyType::LazyImport + } +} diff --git a/crates/rspack_plugin_lazy_compilation/src/factory.rs b/crates/rspack_plugin_lazy_compilation/src/factory.rs new file mode 100644 index 000000000000..c5dde4621f17 --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/src/factory.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use rspack_core::{ + ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult, NormalModuleFactory, +}; +use rspack_error::Result; + +use crate::dependency::LazyCompilationDependency; + +#[derive(Debug)] +pub(crate) struct LazyCompilationDependencyFactory { + normal_module_factory: Arc, +} + +impl LazyCompilationDependencyFactory { + pub fn new(normal_module_factory: Arc) -> Self { + Self { + normal_module_factory, + } + } +} + +#[async_trait::async_trait] +impl ModuleFactory for LazyCompilationDependencyFactory { + async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result { + let dep: &LazyCompilationDependency = data + .dependency + .as_any() + .downcast_ref() + .expect("should be lazy compile dependency"); + + let proxy_data = &dep.original_module_create_data; + + let dep = dep.clone(); + + let mut create_data = ModuleFactoryCreateData { + resolve_options: proxy_data.resolve_options.clone(), + context: proxy_data.context.clone(), + dependency: Box::new(dep), + issuer: proxy_data.issuer.clone(), + issuer_identifier: proxy_data.issuer_identifier, + file_dependencies: proxy_data.file_dependencies.clone(), + context_dependencies: proxy_data.context_dependencies.clone(), + missing_dependencies: proxy_data.missing_dependencies.clone(), + diagnostics: proxy_data.diagnostics.clone(), + }; + + self.normal_module_factory.create(&mut create_data).await + } +} diff --git a/crates/rspack_plugin_lazy_compilation/src/lib.rs b/crates/rspack_plugin_lazy_compilation/src/lib.rs new file mode 100644 index 000000000000..c30623e5320b --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/src/lib.rs @@ -0,0 +1,6 @@ +#![feature(let_chains)] +pub mod backend; +mod dependency; +mod factory; +mod module; +pub mod plugin; diff --git a/crates/rspack_plugin_lazy_compilation/src/module.rs b/crates/rspack_plugin_lazy_compilation/src/module.rs new file mode 100644 index 000000000000..aa4c6157e890 --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/src/module.rs @@ -0,0 +1,323 @@ +use std::{hash::Hash, path::PathBuf, sync::Arc}; + +use rspack_core::{ + impl_module_meta_info, module_namespace_promise, + rspack_sources::{RawSource, Source}, + AsyncDependenciesBlock, AsyncDependenciesBlockIdentifier, BoxDependency, BuildContext, BuildInfo, + BuildMeta, BuildResult, CodeGenerationResult, Compilation, ConcatenationScope, Context, + DependenciesBlock, DependencyId, FactoryMeta, Module, ModuleFactoryCreateData, ModuleIdentifier, + ModuleType, RuntimeGlobals, RuntimeSpec, SourceType, TemplateContext, +}; +use rspack_error::{Diagnosable, Diagnostic, Result}; +use rspack_identifier::Identifiable; +use rspack_plugin_javascript::dependency::CommonJsRequireDependency; +use rspack_util::source_map::{ModuleSourceMapConfig, SourceMapKind}; +use rustc_hash::FxHashSet; + +use crate::dependency::LazyCompilationDependency; + +static MODULE_TYPE: ModuleType = ModuleType::Js; +static SOURCE_TYPE: [SourceType; 1] = [SourceType::JavaScript]; + +#[derive(Debug)] +pub(crate) struct LazyCompilationProxyModule { + build_info: Option, + build_meta: Option, + factory_meta: Option, + original_module: ModuleIdentifier, + cacheable: bool, + + readable_identifier: String, + identifier: ModuleIdentifier, + + blocks: Vec, + dependencies: Vec, + + source_map_kind: SourceMapKind, + create_data: ModuleFactoryCreateData, + pub resource: String, + + pub active: bool, + pub data: String, + pub client: String, +} + +impl Hash for LazyCompilationProxyModule { + fn hash(&self, state: &mut H) { + self.build_meta.hash(state); + self.original_module.hash(state); + self.readable_identifier.hash(state); + self.identifier.hash(state); + self.blocks.hash(state); + self.dependencies.hash(state); + } +} + +impl PartialEq for LazyCompilationProxyModule { + fn eq(&self, other: &Self) -> bool { + self.original_module == other.original_module + && self.readable_identifier == other.readable_identifier + && self.identifier == other.identifier + } +} + +impl Eq for LazyCompilationProxyModule {} + +impl ModuleSourceMapConfig for LazyCompilationProxyModule { + fn get_source_map_kind(&self) -> &SourceMapKind { + &self.source_map_kind + } + + fn set_source_map_kind(&mut self, source_map: SourceMapKind) { + self.source_map_kind = source_map; + } +} + +impl LazyCompilationProxyModule { + pub(crate) fn new( + original_module: ModuleIdentifier, + create_data: ModuleFactoryCreateData, + resource: String, + cacheable: bool, + active: bool, + data: String, + client: String, + ) -> Self { + let readable_identifier = format!( + "lazy-compilation-proxy|{}", + create_data.context.shorten(&original_module) + ); + let identifier = format!("lazy-compilation-proxy|{original_module}").into(); + + Self { + build_info: None, + build_meta: None, + cacheable, + original_module, + create_data, + readable_identifier, + resource, + identifier, + source_map_kind: SourceMapKind::empty(), + factory_meta: None, + blocks: vec![], + dependencies: vec![], + active, + client, + data, + } + } +} + +impl Diagnosable for LazyCompilationProxyModule { + fn add_diagnostic(&self, _diagnostic: Diagnostic) { + unimplemented!() + } + fn add_diagnostics(&self, _diagnostics: Vec) { + unimplemented!() + } +} + +#[async_trait::async_trait] +impl Module for LazyCompilationProxyModule { + impl_module_meta_info!(); + + fn source_types(&self) -> &[SourceType] { + &SOURCE_TYPE + } + + fn module_type(&self) -> &ModuleType { + &MODULE_TYPE + } + + fn size(&self, _source_type: &SourceType) -> f64 { + 200f64 + } + + fn original_source(&self) -> Option<&dyn Source> { + None + } + + fn readable_identifier(&self, _context: &Context) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(&self.readable_identifier) + } + + fn get_diagnostics(&self) -> Vec { + vec![] + } + + async fn build( + &mut self, + _build_context: BuildContext<'_>, + _compilation: Option<&Compilation>, + ) -> Result { + let client_dep = CommonJsRequireDependency::new(self.client.clone(), None, 0, 0, false); + let mut dependencies = vec![]; + let mut blocks = vec![]; + + dependencies.push(Box::new(client_dep) as BoxDependency); + + if self.active { + let dep = LazyCompilationDependency::new(self.create_data.clone()); + + blocks.push(AsyncDependenciesBlock::new( + self.identifier, + None, + None, + vec![Box::new(dep)], + )); + } + + let mut files = FxHashSet::default(); + files.extend(self.create_data.file_dependencies.clone()); + files.insert(PathBuf::from(&self.resource)); + + Ok(BuildResult { + build_info: BuildInfo { + cacheable: self.cacheable, + file_dependencies: files, + ..Default::default() + }, + build_meta: BuildMeta::default(), + analyze_result: Default::default(), + dependencies, + blocks, + optimization_bailouts: vec![], + }) + } + + fn code_generation( + &self, + compilation: &Compilation, + _runtime: Option<&RuntimeSpec>, + mut concatenation_scope: Option, + ) -> Result { + let mut runtime_requirements = RuntimeGlobals::empty(); + runtime_requirements.insert(RuntimeGlobals::MODULE); + runtime_requirements.insert(RuntimeGlobals::REQUIRE); + + let client_dep_id = self.dependencies[0]; + let module_graph = &compilation.get_module_graph(); + let chunk_graph = &compilation.chunk_graph; + + let client_module = module_graph + .module_identifier_by_dependency_id(&client_dep_id) + .expect("should have module"); + + let block = self.blocks.first(); + + let client = format!( + "var client = __webpack_require__(\"{}\");\nvar data = \"{}\"", + chunk_graph + .get_module_id(*client_module) + .as_ref() + .expect("should have module id"), + self.data + ); + + let keep_active = format!( + "var dispose = client.keepAlive({{ data: data, active: {}, module: module, onError: onError }})", + block.is_some() + ); + + let source = if let Some(block_id) = block { + let block = module_graph + .block_by_id(block_id) + .expect("should have block"); + + let dep_id = block.get_dependencies()[0]; + let module = module_graph + .module_identifier_by_dependency_id(&dep_id) + .expect("should have module"); + + let mut template_ctx = TemplateContext { + compilation, + module: module_graph + .module_by_identifier(module) + .expect("should have module") + .as_ref(), + runtime_requirements: &mut runtime_requirements, + init_fragments: &mut vec![], + runtime: None, + concatenation_scope: concatenation_scope.as_mut(), + }; + + RawSource::from(format!( + "{client} + module.exports = {}; + if (module.hot) {{ + module.hot.accept(); + module.hot.accept(\"{}\", function() {{ module.hot.invalidate(); }}); + module.hot.dispose(function(data) {{ delete data.resolveSelf; dispose(data); }}); + if (module.hot.data && module.hot.data.resolveSelf) + module.hot.data.resolveSelf(module.exports); + }} + function onError() {{ /* ignore */ }} + {} + ", + module_namespace_promise( + &mut template_ctx, + &dep_id, + Some(block_id), + &self.resource, + "import()", + false + ), + chunk_graph + .get_module_id(*module) + .as_ref() + .expect("should have module id"), + keep_active, + )) + } else { + RawSource::from(format!( + "{} + var resolveSelf, onError; + module.exports = new Promise(function(resolve, reject) {{ resolveSelf = resolve; onError = reject; }}); + if (module.hot) {{ + module.hot.accept(); + if (module.hot.data && module.hot.data.resolveSelf) module.hot.data.resolveSelf(module.exports); + module.hot.dispose(function(data) {{ data.resolveSelf = resolveSelf; dispose(data); }}); + }} + {} + ", + client, + keep_active + )) + }; + + let mut codegen_result = CodeGenerationResult::default().with_javascript(Arc::new(source)); + codegen_result.runtime_requirements = runtime_requirements; + codegen_result.set_hash( + &compilation.options.output.hash_function, + &compilation.options.output.hash_digest, + &compilation.options.output.hash_salt, + ); + + Ok(codegen_result) + } +} + +impl Identifiable for LazyCompilationProxyModule { + fn identifier(&self) -> rspack_identifier::Identifier { + self.identifier + } +} + +impl DependenciesBlock for LazyCompilationProxyModule { + fn add_block_id(&mut self, block: rspack_core::AsyncDependenciesBlockIdentifier) { + self.blocks.push(block); + } + + fn get_blocks(&self) -> &[rspack_core::AsyncDependenciesBlockIdentifier] { + &self.blocks + } + + fn add_dependency_id(&mut self, dependency: rspack_core::DependencyId) { + self.dependencies.push(dependency); + } + + fn get_dependencies(&self) -> &[rspack_core::DependencyId] { + &self.dependencies + } +} diff --git a/crates/rspack_plugin_lazy_compilation/src/plugin.rs b/crates/rspack_plugin_lazy_compilation/src/plugin.rs new file mode 100644 index 000000000000..9c6e36adb4cb --- /dev/null +++ b/crates/rspack_plugin_lazy_compilation/src/plugin.rs @@ -0,0 +1,199 @@ +use std::{fmt::Debug, sync::Arc}; + +use once_cell::sync::Lazy; +use rspack_core::{ + ApplyContext, BoxModule, Compilation, CompilationParams, CompilerCompilation, CompilerOptions, + DependencyType, Module, ModuleFactory, ModuleFactoryCreateData, NormalModuleCreateData, + NormalModuleFactoryModule, Plugin, PluginContext, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; +use rspack_regex::RspackRegex; +use tokio::sync::Mutex; + +use crate::{ + backend::Backend, factory::LazyCompilationDependencyFactory, module::LazyCompilationProxyModule, +}; + +static WEBPACK_DEV_SERVER_CLIENT_RE: Lazy = Lazy::new(|| { + RspackRegex::new( + r#"(webpack|rspack)[/\\]hot[/\\]|(webpack|rspack)-dev-server[/\\]client|(webpack|rspack)-hot-middleware[/\\]client"#, + ) + .expect("should compile regex") +}); + +#[derive(Debug, Hash, Clone)] +pub enum LazyCompilationTest { + Regex(RspackRegex), + Fn(F), +} + +pub trait LazyCompilationTestCheck: Send + Sync + Debug { + fn test(&self, module: &dyn Module) -> bool; +} + +impl LazyCompilationTest { + fn test(&self, module: &dyn Module) -> bool { + match self { + LazyCompilationTest::Regex(regex) => { + regex.test(&module.name_for_condition().unwrap_or("".into())) + } + LazyCompilationTest::Fn(f) => f.test(module), + } + } +} + +#[derive(Debug)] +#[plugin] +pub struct LazyCompilationPlugin { + backend: Mutex, + entries: bool, // enable for entries + imports: bool, // enable for imports + test: Option>, + cacheable: bool, +} + +impl LazyCompilationPlugin { + pub fn new( + cacheable: bool, + backend: T, + test: Option>, + entries: bool, + imports: bool, + ) -> Self { + Self::new_inner(Mutex::new(backend), entries, imports, test, cacheable) + } + + fn check_test(&self, module: &BoxModule) -> bool { + if let Some(test) = &self.inner.test { + test.test(module.as_ref()) + } else { + true + } + } +} + +#[plugin_hook(CompilerCompilation for LazyCompilationPlugin)] +async fn compilation( + &self, + compilation: &mut Compilation, + params: &mut CompilationParams, +) -> Result<()> { + compilation.set_dependency_factory( + DependencyType::LazyImport, + Arc::new(LazyCompilationDependencyFactory::new( + params.normal_module_factory.clone(), + )) as Arc, + ); + + Ok(()) +} + +#[plugin_hook(NormalModuleFactoryModule for LazyCompilationPlugin)] +async fn normal_module_factory_module( + &self, + module_factory_create_data: &mut ModuleFactoryCreateData, + create_data: &mut NormalModuleCreateData, + module: &mut BoxModule, +) -> Result<()> { + if let Some(query) = &create_data.resource_resolve_data.resource_query + && query.contains("lazy-compilation-proxy-dep") + { + let remaining_query = query.clone().replace("lazy-compilation-proxy-dep", ""); + + create_data.resource_resolve_data.resource_query = + if remaining_query.is_empty() || remaining_query == "?" { + None + } else { + Some(remaining_query) + }; + + return Ok(()); + } + + let dep_type = module_factory_create_data.dependency.dependency_type(); + + let is_imports = matches!( + dep_type, + DependencyType::DynamicImport + | DependencyType::DynamicImportEager + | DependencyType::ContextElement + ); + let is_entries = matches!(dep_type, DependencyType::Entry); + + #[allow(clippy::if_same_then_else)] + if matches!( + dep_type, + DependencyType::ModuleHotAccept + | DependencyType::ModuleHotDecline + | DependencyType::ImportMetaHotAccept + | DependencyType::ImportMetaHotDecline + ) { + // TODO: we cannot access module graph at this stage + // if hmr point to a module that is already been dyn imported + // eg: import('./foo'); module.hot.accept('./foo') + // however we cannot access module graph at this time, so we cannot + // detect this case easily + return Ok(()); + } else if !is_entries && !is_imports { + return Ok(()); + } + + if !self.entries && is_entries { + return Ok(()); + } + if !self.imports && is_imports { + return Ok(()); + } + + if WEBPACK_DEV_SERVER_CLIENT_RE.test(&create_data.resource_resolve_data.resource) + || !self.check_test(module) + { + return Ok(()); + } + + let mut backend = self.backend.lock().await; + let module_identifier = module.identifier(); + let info = backend + .module( + module_identifier, + create_data.resource_resolve_data.resource.clone(), + ) + .await?; + + *module = Box::new(LazyCompilationProxyModule::new( + module_identifier, + module_factory_create_data.clone(), + create_data.resource_resolve_data.resource.clone(), + self.cacheable, + info.active, + info.data, + info.client, + )); + + Ok(()) +} + +#[async_trait::async_trait] +impl Plugin + for LazyCompilationPlugin +{ + fn apply( + &self, + ctx: PluginContext<&mut ApplyContext>, + _options: &mut CompilerOptions, + ) -> Result<()> { + ctx + .context + .compiler_hooks + .compilation + .tap(compilation::new(self)); + + ctx + .context + .normal_module_factory_hooks + .module + .tap(normal_module_factory_module::new(self)); + Ok(()) + } +} diff --git a/crates/rspack_plugin_runtime/src/lib.rs b/crates/rspack_plugin_runtime/src/lib.rs index 59f113b32021..195b39e01326 100644 --- a/crates/rspack_plugin_runtime/src/lib.rs +++ b/crates/rspack_plugin_runtime/src/lib.rs @@ -1,8 +1,6 @@ #![feature(get_mut_unchecked)] mod helpers; pub use helpers::*; -mod lazy_compilation; -pub use lazy_compilation::LazyCompilationPlugin; mod common_js_chunk_format; pub use common_js_chunk_format::CommonJsChunkFormatPlugin; mod runtime_plugin; diff --git a/packages/rspack-dev-server/src/server.ts b/packages/rspack-dev-server/src/server.ts index e222fa9e3578..276661bf5c03 100644 --- a/packages/rspack-dev-server/src/server.ts +++ b/packages/rspack-dev-server/src/server.ts @@ -219,38 +219,6 @@ export class RspackDevServer extends WebpackDevServer { private override setupMiddlewares() { const middlewares: WebpackDevServer.Middleware[] = []; - const compilers = - this.compiler instanceof MultiCompiler - ? this.compiler.compilers - : [this.compiler]; - - compilers.forEach(compiler => { - if (compiler.options.experiments.lazyCompilation) { - middlewares.push({ - // @ts-expect-error - middleware: (req, res) => { - if (req.url.indexOf("/lazy-compilation-web/") > -1) { - const path = req.url.replace("/lazy-compilation-web/", ""); - if (fs.existsSync(path)) { - compiler.__internal__rebuild( - new Set([path]), - new Set(), - error => { - if (error) { - throw error; - } - res.write(""); - res.end(); - console.log("lazy compiler success"); - } - ); - } - } - } - }); - } - }); - middlewares.forEach(middleware => { if (typeof middleware === "function") { // @ts-expect-error diff --git a/packages/rspack/etc/api.md b/packages/rspack/etc/api.md index 256352df669b..cf07c9c22333 100644 --- a/packages/rspack/etc/api.md +++ b/packages/rspack/etc/api.md @@ -2778,7 +2778,19 @@ export type Experiments = z.infer; // @public (undocumented) const experiments: z.ZodObject<{ - lazyCompilation: z.ZodOptional; + lazyCompilation: z.ZodUnion<[z.ZodOptional, z.ZodObject<{ + imports: z.ZodOptional; + entries: z.ZodOptional; + test: z.ZodOptional, z.ZodFunction], z.ZodUnknown>, z.ZodBoolean>]>>; + }, "strip", z.ZodTypeAny, { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + }, { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + }>]>; asyncWebAssembly: z.ZodOptional; outputModule: z.ZodOptional; topLevelAwait: z.ZodOptional; @@ -2810,7 +2822,11 @@ const experiments: z.ZodObject<{ } | undefined; }>>; }, "strict", z.ZodTypeAny, { - lazyCompilation?: boolean | undefined; + lazyCompilation?: boolean | { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + } | undefined; asyncWebAssembly?: boolean | undefined; outputModule?: boolean | undefined; topLevelAwait?: boolean | undefined; @@ -2824,7 +2840,11 @@ const experiments: z.ZodObject<{ } | undefined; } | undefined; }, { - lazyCompilation?: boolean | undefined; + lazyCompilation?: boolean | { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + } | undefined; asyncWebAssembly?: boolean | undefined; outputModule?: boolean | undefined; topLevelAwait?: boolean | undefined; @@ -2848,7 +2868,7 @@ export interface ExperimentsNormalized { // (undocumented) futureDefaults?: boolean; // (undocumented) - lazyCompilation?: boolean; + lazyCompilation?: false | LazyCompilationOptions; // (undocumented) outputModule?: boolean; // (undocumented) @@ -4218,6 +4238,24 @@ type KnownStatsProfile = { building: number; }; +// @public (undocumented) +export type LazyCompilationOptions = z.infer; + +// @public (undocumented) +const lazyCompilationOptions: z.ZodObject<{ + imports: z.ZodOptional; + entries: z.ZodOptional; + test: z.ZodOptional, z.ZodFunction], z.ZodUnknown>, z.ZodBoolean>]>>; +}, "strip", z.ZodTypeAny, { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; +}, { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; +}>; + // @public (undocumented) export type Library = z.infer; @@ -7883,6 +7921,7 @@ declare namespace rspackExports { OptimizationSplitChunksOptions, Optimization, RspackFutureOptions, + LazyCompilationOptions, Experiments, Watch, WatchOptions, @@ -8502,7 +8541,19 @@ export const rspackOptions: z.ZodObject<{ target: z.ZodOptional, z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodUnion<[z.ZodEnum<["web", "webworker", "es3", "es5", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020", "es2021", "es2022", "browserslist"]>, z.ZodLiteral<"node">]>, z.ZodLiteral<"async-node">]>, z.ZodType<`node${number}`, z.ZodTypeDef, `node${number}`>]>, z.ZodType<`async-node${number}`, z.ZodTypeDef, `async-node${number}`>]>, z.ZodType<`node${number}.${number}`, z.ZodTypeDef, `node${number}.${number}`>]>, z.ZodType<`async-node${number}.${number}`, z.ZodTypeDef, `async-node${number}.${number}`>]>, z.ZodLiteral<"electron-main">]>, z.ZodType<`electron${number}-main`, z.ZodTypeDef, `electron${number}-main`>]>, z.ZodType<`electron${number}.${number}-main`, z.ZodTypeDef, `electron${number}.${number}-main`>]>, z.ZodLiteral<"electron-renderer">]>, z.ZodType<`electron${number}-renderer`, z.ZodTypeDef, `electron${number}-renderer`>]>, z.ZodType<`electron${number}.${number}-renderer`, z.ZodTypeDef, `electron${number}.${number}-renderer`>]>, z.ZodLiteral<"electron-preload">]>, z.ZodType<`electron${number}-preload`, z.ZodTypeDef, `electron${number}-preload`>]>, z.ZodType<`electron${number}.${number}-preload`, z.ZodTypeDef, `electron${number}.${number}-preload`>]>]>, z.ZodArray, z.ZodLiteral<"node">]>, z.ZodLiteral<"async-node">]>, z.ZodType<`node${number}`, z.ZodTypeDef, `node${number}`>]>, z.ZodType<`async-node${number}`, z.ZodTypeDef, `async-node${number}`>]>, z.ZodType<`node${number}.${number}`, z.ZodTypeDef, `node${number}.${number}`>]>, z.ZodType<`async-node${number}.${number}`, z.ZodTypeDef, `async-node${number}.${number}`>]>, z.ZodLiteral<"electron-main">]>, z.ZodType<`electron${number}-main`, z.ZodTypeDef, `electron${number}-main`>]>, z.ZodType<`electron${number}.${number}-main`, z.ZodTypeDef, `electron${number}.${number}-main`>]>, z.ZodLiteral<"electron-renderer">]>, z.ZodType<`electron${number}-renderer`, z.ZodTypeDef, `electron${number}-renderer`>]>, z.ZodType<`electron${number}.${number}-renderer`, z.ZodTypeDef, `electron${number}.${number}-renderer`>]>, z.ZodLiteral<"electron-preload">]>, z.ZodType<`electron${number}-preload`, z.ZodTypeDef, `electron${number}-preload`>]>, z.ZodType<`electron${number}.${number}-preload`, z.ZodTypeDef, `electron${number}.${number}-preload`>]>, "many">]>>; mode: z.ZodOptional>; experiments: z.ZodOptional; + lazyCompilation: z.ZodUnion<[z.ZodOptional, z.ZodObject<{ + imports: z.ZodOptional; + entries: z.ZodOptional; + test: z.ZodOptional, z.ZodFunction], z.ZodUnknown>, z.ZodBoolean>]>>; + }, "strip", z.ZodTypeAny, { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + }, { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + }>]>; asyncWebAssembly: z.ZodOptional; outputModule: z.ZodOptional; topLevelAwait: z.ZodOptional; @@ -8534,7 +8585,11 @@ export const rspackOptions: z.ZodObject<{ } | undefined; }>>; }, "strict", z.ZodTypeAny, { - lazyCompilation?: boolean | undefined; + lazyCompilation?: boolean | { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + } | undefined; asyncWebAssembly?: boolean | undefined; outputModule?: boolean | undefined; topLevelAwait?: boolean | undefined; @@ -8548,7 +8603,11 @@ export const rspackOptions: z.ZodObject<{ } | undefined; } | undefined; }, { - lazyCompilation?: boolean | undefined; + lazyCompilation?: boolean | { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + } | undefined; asyncWebAssembly?: boolean | undefined; outputModule?: boolean | undefined; topLevelAwait?: boolean | undefined; @@ -9980,7 +10039,11 @@ export const rspackOptions: z.ZodObject<{ target?: false | "es3" | "es5" | "es2015" | "es2016" | "es2017" | "es2018" | "es2019" | "es2020" | "es2021" | "es2022" | "node" | "async-node" | "web" | "webworker" | "browserslist" | `node${number}` | `async-node${number}` | `node${number}.${number}` | `async-node${number}.${number}` | "electron-main" | `electron${number}-main` | `electron${number}.${number}-main` | "electron-renderer" | `electron${number}-renderer` | `electron${number}.${number}-renderer` | "electron-preload" | `electron${number}-preload` | `electron${number}.${number}-preload` | ("es3" | "es5" | "es2015" | "es2016" | "es2017" | "es2018" | "es2019" | "es2020" | "es2021" | "es2022" | "node" | "async-node" | "web" | "webworker" | "browserslist" | `node${number}` | `async-node${number}` | `node${number}.${number}` | `async-node${number}.${number}` | "electron-main" | `electron${number}-main` | `electron${number}.${number}-main` | "electron-renderer" | `electron${number}-renderer` | `electron${number}.${number}-renderer` | "electron-preload" | `electron${number}-preload` | `electron${number}.${number}-preload`)[] | undefined; mode?: "none" | "development" | "production" | undefined; experiments?: { - lazyCompilation?: boolean | undefined; + lazyCompilation?: boolean | { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + } | undefined; asyncWebAssembly?: boolean | undefined; outputModule?: boolean | undefined; topLevelAwait?: boolean | undefined; @@ -10404,7 +10467,11 @@ export const rspackOptions: z.ZodObject<{ target?: false | "es3" | "es5" | "es2015" | "es2016" | "es2017" | "es2018" | "es2019" | "es2020" | "es2021" | "es2022" | "node" | "async-node" | "web" | "webworker" | "browserslist" | `node${number}` | `async-node${number}` | `node${number}.${number}` | `async-node${number}.${number}` | "electron-main" | `electron${number}-main` | `electron${number}.${number}-main` | "electron-renderer" | `electron${number}-renderer` | `electron${number}.${number}-renderer` | "electron-preload" | `electron${number}-preload` | `electron${number}.${number}-preload` | ("es3" | "es5" | "es2015" | "es2016" | "es2017" | "es2018" | "es2019" | "es2020" | "es2021" | "es2022" | "node" | "async-node" | "web" | "webworker" | "browserslist" | `node${number}` | `async-node${number}` | `node${number}.${number}` | `async-node${number}.${number}` | "electron-main" | `electron${number}-main` | `electron${number}.${number}-main` | "electron-renderer" | `electron${number}-renderer` | `electron${number}.${number}-renderer` | "electron-preload" | `electron${number}-preload` | `electron${number}.${number}-preload`)[] | undefined; mode?: "none" | "development" | "production" | undefined; experiments?: { - lazyCompilation?: boolean | undefined; + lazyCompilation?: boolean | { + imports?: boolean | undefined; + entries?: boolean | undefined; + test?: RegExp | ((args_0: Module, ...args_1: unknown[]) => boolean) | undefined; + } | undefined; asyncWebAssembly?: boolean | undefined; outputModule?: boolean | undefined; topLevelAwait?: boolean | undefined; @@ -12049,6 +12116,8 @@ export class Watching { // (undocumented) lastWatcherStartTime: number; // (undocumented) + lazyCompilationInvalidate(files: Set): void; + // (undocumented) onChange?: () => void; // (undocumented) onInvalid?: () => void; diff --git a/packages/rspack/src/Watching.ts b/packages/rspack/src/Watching.ts index 0dc67476d3b9..3575883e8e61 100644 --- a/packages/rspack/src/Watching.ts +++ b/packages/rspack/src/Watching.ts @@ -197,6 +197,10 @@ export class Watching { this.#invalidate(); } + lazyCompilationInvalidate(files: Set) { + this.#invalidate(new Map(), new Map(), files, new Set()); + } + #invalidate( fileTimeInfoEntries?: Map, contextTimeInfoEntries?: Map, diff --git a/packages/rspack/src/builtin-plugin/base.ts b/packages/rspack/src/builtin-plugin/base.ts index e756cf7da6d9..2a02ddadc436 100644 --- a/packages/rspack/src/builtin-plugin/base.ts +++ b/packages/rspack/src/builtin-plugin/base.ts @@ -50,7 +50,7 @@ export function createBuiltinPlugin( export function create( name: binding.BuiltinPluginName, resolve: (...args: T) => R, - // `affectedHooks` is used to inform `createChildCompile` about which builtin plugin can be reversed. + // `affectedHooks` is used to inform `createChildCompile` about which builtin plugin can be reserved. // However, this has a drawback as it doesn't represent the actual condition but merely serves as an indicator. affectedHooks?: AffectedHooks ) { diff --git a/packages/rspack/src/builtin-plugin/index.ts b/packages/rspack/src/builtin-plugin/index.ts index 4e8c7222b31a..d821c7a7abea 100644 --- a/packages/rspack/src/builtin-plugin/index.ts +++ b/packages/rspack/src/builtin-plugin/index.ts @@ -58,6 +58,7 @@ export * from "./SwcJsMinimizerPlugin"; export * from "./WarnCaseSensitiveModulesPlugin"; export * from "./WebWorkerTemplatePlugin"; export * from "./WorkerPlugin"; +export * from "./lazy-compilation/plugin"; ///// DEPRECATED ///// import { RawBuiltins } from "@rspack/binding"; diff --git a/packages/rspack/src/builtin-plugin/lazy-compilation/backend.ts b/packages/rspack/src/builtin-plugin/lazy-compilation/backend.ts new file mode 100644 index 000000000000..aea6f1c145c2 --- /dev/null +++ b/packages/rspack/src/builtin-plugin/lazy-compilation/backend.ts @@ -0,0 +1,229 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +import type { + IncomingMessage, + ServerResponse, + ServerOptions as ServerOptionsImport +} from "http"; +import type { AddressInfo, ListenOptions, Server, Socket } from "net"; +import type { Compiler } from "../.."; +import type { SecureContextOptions, TlsOptions } from "tls"; + +export interface LazyCompilationDefaultBackendOptions { + /** + * A custom client. + */ + client?: string; + + /** + * Specifies where to listen to from the server. + */ + listen?: number | ListenOptions | ((server: Server) => void); + + /** + * Specifies the protocol the client should use to connect to the server. + */ + protocol?: "http" | "https"; + + /** + * Specifies how to create the server handling the EventSource requests. + */ + server?: + | ServerOptionsImport + | ServerOptionsHttps + | (() => Server); +} + +export type ServerOptionsHttps< + Request extends typeof IncomingMessage = typeof IncomingMessage, + Response extends typeof ServerResponse = typeof ServerResponse +> = SecureContextOptions & TlsOptions & ServerOptionsImport; + +const getBackend = + ( + options: Omit & { + client: NonNullable; + } + ) => + ( + compiler: Compiler, + callback: ( + err: any, + obj?: { + dispose: (callback: (err: any) => void) => void; + module: (args: { module: string; path: string }) => { + data: string; + client: string; + active: boolean; + }; + } + ) => void + ) => { + const logger = compiler.getInfrastructureLogger("LazyCompilationBackend"); + const activeModules = new Map(); + const filesByKey: Map = new Map(); + const prefix = "/lazy-compilation-using-"; + const isHttps = + options.protocol === "https" || + (typeof options.server === "object" && + ("key" in options.server || "pfx" in options.server)); + + const createServer = + typeof options.server === "function" + ? options.server + : (() => { + const http = isHttps ? require("https") : require("http"); + return http.createServer.bind(http, options.server); + })(); + const listen = + typeof options.listen === "function" + ? options.listen + : (server: Server) => { + let listen = options.listen; + if (typeof listen === "object" && !("port" in listen)) + listen = { ...listen, port: undefined }; + server.listen(listen); + }; + + const protocol = options.protocol || (isHttps ? "https" : "http"); + + const requestListener = (req: any, res: ServerResponse) => { + const keys = req.url.slice(prefix.length).split("@"); + req.socket.on("close", () => { + setTimeout(() => { + for (const key of keys) { + const oldValue = activeModules.get(key) || 0; + activeModules.set(key, oldValue - 1); + if (oldValue === 1) { + logger.log( + `${key} is no longer in use. Next compilation will skip this module.` + ); + } + } + }, 120000); + }); + req.socket.setNoDelay(true); + res.writeHead(200, { + "content-type": "text/event-stream", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Headers": "*" + }); + res.write("\n"); + const moduleActivated = []; + for (const key of keys) { + const oldValue = activeModules.get(key) || 0; + activeModules.set(key, oldValue + 1); + if (oldValue === 0) { + logger.log(`${key} is now in use and will be compiled.`); + moduleActivated.push(key); + } + } + + if (moduleActivated.length && compiler.watching) { + compiler.watching.lazyCompilationInvalidate( + new Set(moduleActivated.map(key => filesByKey.get(key)!)) + ); + } + }; + + const server = createServer() as Server; + server.on("request", requestListener); + + let isClosing = false; + const sockets: Set = new Set(); + server.on("connection", socket => { + sockets.add(socket); + socket.on("close", () => { + sockets.delete(socket); + }); + if (isClosing) socket.destroy(); + }); + server.on("clientError", e => { + if (e.message !== "Server is disposing") logger.warn(e); + }); + server.on("listening", (err: any) => { + if (err) return callback(err); + const addr = server.address() as AddressInfo; + if (typeof addr === "string") + throw new Error("addr must not be a string"); + const urlBase = + addr.address === "::" || addr.address === "0.0.0.0" + ? `${protocol}://localhost:${addr.port}` + : addr.family === "IPv6" + ? `${protocol}://[${addr.address}]:${addr.port}` + : `${protocol}://${addr.address}:${addr.port}`; + logger.log( + `Server-Sent-Events server for lazy compilation open at ${urlBase}.` + ); + + const result = { + dispose(callback: any) { + isClosing = true; + // Removing the listener is a workaround for a memory leak in node.js + server.off("request", requestListener); + server.close(err => { + callback(err); + }); + for (const socket of sockets) { + socket.destroy(new Error("Server is disposing")); + } + }, + module({ + module: originalModule, + path + }: { + module: string; + path: string; + }) { + const key = `${encodeURIComponent( + originalModule.replace(/\\/g, "/").replace(/@/g, "_") + ).replace(/%(2F|3A|24|26|2B|2C|3B|3D|3A)/g, decodeURIComponent)}`; + filesByKey.set(key, path); + const active = activeModules.get(key) > 0; + return { + client: `${options.client}?${encodeURIComponent(urlBase + prefix)}`, + data: key, + active + }; + } + }; + state.module = result.module; + state.dispose = result.dispose; + callback(null, result); + }); + listen(server); + }; + +export default getBackend; + +function unimplemented() { + throw new Error("access before initialization"); +} + +const state: { + module: typeof moduleImpl; + dispose: typeof dispose; +} = { + module: unimplemented as any, + dispose: unimplemented +}; + +export function dispose(callback: any) { + state.dispose(callback); + state.dispose = unimplemented; + state.module = unimplemented as any; +} + +export function moduleImpl(args: { module: string; path: string }): { + active: boolean; + data: string; + client: string; +} { + return state.module(args); +} diff --git a/packages/rspack/src/builtin-plugin/lazy-compilation/lazyCompilation.ts b/packages/rspack/src/builtin-plugin/lazy-compilation/lazyCompilation.ts new file mode 100644 index 000000000000..692492e2cc0f --- /dev/null +++ b/packages/rspack/src/builtin-plugin/lazy-compilation/lazyCompilation.ts @@ -0,0 +1,18 @@ +import { BuiltinPluginName, JsModule, RawRegexMatcher } from "@rspack/binding"; +import { create } from "../base"; + +export const BuiltinLazyCompilationPlugin = create( + BuiltinPluginName.LazyCompilationPlugin, + ( + module: (args: { module: string; path: string }) => { + active: boolean; + data: string; + client: string; + }, + cacheable: boolean, + entries: boolean, + imports: boolean, + test?: RawRegexMatcher | ((m: JsModule) => boolean) + ) => ({ module, cacheable, imports, entries, test }), + "thisCompilation" +); diff --git a/packages/rspack/src/builtin-plugin/lazy-compilation/plugin.ts b/packages/rspack/src/builtin-plugin/lazy-compilation/plugin.ts new file mode 100644 index 000000000000..f9b7890771bc --- /dev/null +++ b/packages/rspack/src/builtin-plugin/lazy-compilation/plugin.ts @@ -0,0 +1,68 @@ +import type { Compiler } from "../.."; + +import { BuiltinLazyCompilationPlugin } from "./lazyCompilation"; +import getBackend, { + moduleImpl, + dispose, + LazyCompilationDefaultBackendOptions +} from "./backend"; +import { JsModule, RawRegexMatcher } from "@rspack/binding"; + +export default class LazyCompilationPlugin { + cacheable: boolean; + entries: boolean; + imports: boolean; + test?: RawRegexMatcher | ((m: JsModule) => boolean); + backend?: LazyCompilationDefaultBackendOptions; + + constructor( + cacheable: boolean, + entries: boolean, + imports: boolean, + test?: RawRegexMatcher | ((m: JsModule) => boolean), + backend?: LazyCompilationDefaultBackendOptions + ) { + this.cacheable = cacheable; + this.entries = entries; + this.imports = imports; + this.test = test; + this.backend = backend; + } + + apply(compiler: Compiler) { + const backend = getBackend({ + ...this.backend, + client: require.resolve( + `../../../hot/lazy-compilation-${ + compiler.options.externalsPresets.node ? "node" : "web" + }.js` + ) + }); + + new BuiltinLazyCompilationPlugin( + moduleImpl, + this.cacheable, + this.entries, + this.imports, + this.test + ).apply(compiler); + + let initialized = false; + compiler.hooks.beforeCompile.tapAsync( + "LazyCompilationPlugin", + (_params, callback) => { + if (initialized) return callback(); + backend(compiler, (err, result) => { + if (err) return callback(err); + initialized = true; + callback(); + }); + } + ); + compiler.hooks.shutdown.tapAsync("LazyCompilationPlugin", callback => { + dispose(callback); + }); + } +} + +export { LazyCompilationPlugin }; diff --git a/packages/rspack/src/config/normalization.ts b/packages/rspack/src/config/normalization.ts index 1fbb86c8f905..9e39a363ce0f 100644 --- a/packages/rspack/src/config/normalization.ts +++ b/packages/rspack/src/config/normalization.ts @@ -81,7 +81,8 @@ import type { Watch, WatchOptions, WebassemblyModuleFilename, - WorkerPublicPath + WorkerPublicPath, + LazyCompilationOptions } from "./zod"; export const getNormalizedRspackOptions = ( @@ -298,7 +299,11 @@ export const getNormalizedRspackOptions = ( performance: config.performance, plugins: nestedArray(config.plugins, p => [...p]), experiments: nestedConfig(config.experiments, experiments => ({ - ...experiments + ...experiments, + lazyCompilation: optionalNestedConfig( + experiments.lazyCompilation, + options => (options === true ? {} : options) + ) })), watch: config.watch, watchOptions: cloneObject(config.watchOptions), @@ -499,7 +504,7 @@ export interface ModuleOptionsNormalized { } export interface ExperimentsNormalized { - lazyCompilation?: boolean; + lazyCompilation?: false | LazyCompilationOptions; asyncWebAssembly?: boolean; outputModule?: boolean; topLevelAwait?: boolean; diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index faed9b4e2d15..83441959bf10 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -1,4 +1,4 @@ -import { JsAssetInfo, RawFuncUseCtx } from "@rspack/binding"; +import { JsAssetInfo, RawFuncUseCtx, JsModule } from "@rspack/binding"; import type * as webpackDevServer from "webpack-dev-server"; import { z } from "zod"; @@ -7,7 +7,6 @@ import type { Builtins as BuiltinsType } from "../builtin-plugin"; import { Chunk } from "../Chunk"; import { PathData } from "../Compilation"; import { Module } from "../Module"; -import { deprecatedWarn } from "../util"; //#region Name const name = z.string(); @@ -1193,8 +1192,19 @@ const rspackFutureOptions = z.strictObject({ }); export type RspackFutureOptions = z.infer; +const lazyCompilationOptions = z.object({ + imports: z.boolean().optional(), + entries: z.boolean().optional(), + test: z + .instanceof(RegExp) + .or(z.function().args(z.custom()).returns(z.boolean())) + .optional() +}); + +export type LazyCompilationOptions = z.infer; + const experiments = z.strictObject({ - lazyCompilation: z.boolean().optional(), + lazyCompilation: z.boolean().optional().or(lazyCompilationOptions), asyncWebAssembly: z.boolean().optional(), outputModule: z.boolean().optional(), topLevelAwait: z.boolean().optional(), diff --git a/packages/rspack/src/rspackOptionsApply.ts b/packages/rspack/src/rspackOptionsApply.ts index 0ed88eef1804..d03feaad3ee1 100644 --- a/packages/rspack/src/rspackOptionsApply.ts +++ b/packages/rspack/src/rspackOptionsApply.ts @@ -8,6 +8,7 @@ * https://github.com/webpack/webpack/blob/main/LICENSE */ import assert from "assert"; +import { Module } from "./Module"; import fs from "graceful-fs"; import { @@ -61,7 +62,8 @@ import { SourceMapDevToolPlugin, SplitChunksPlugin, WarnCaseSensitiveModulesPlugin, - WorkerPlugin + WorkerPlugin, + LazyCompilationPlugin } from "./builtin-plugin"; import EntryOptionPlugin from "./lib/EntryOptionPlugin"; import IgnoreWarningsPlugin from "./lib/ignoreWarningsPlugin"; @@ -256,6 +258,33 @@ export class RspackOptionsApply { } } + if (options.experiments.lazyCompilation) { + const lazyOptions = options.experiments.lazyCompilation; + + new LazyCompilationPlugin( + // this is only for test + // @ts-expect-error cacheable is hide + lazyOptions.cacheable ?? true, + lazyOptions.entries ?? true, + lazyOptions.imports ?? true, + typeof lazyOptions.test === "function" + ? function (jsModule) { + return (lazyOptions.test as (jsModule: Module) => boolean)!.call( + lazyOptions, + new Module(jsModule) + ); + } + : lazyOptions.test + ? { + source: lazyOptions.test.source, + flags: lazyOptions.test.flags + } + : undefined, + // @ts-expect-error backend is hide + lazyOptions.backend + ).apply(compiler); + } + if ( options.output.enabledLibraryTypes && options.output.enabledLibraryTypes.length > 0 diff --git a/webpack-test/hotCases/lazy-compilation/context/test.filter.js b/webpack-test/hotCases/lazy-compilation/context/test.filter.js deleted file mode 100644 index 5c32e24f1f81..000000000000 --- a/webpack-test/hotCases/lazy-compilation/context/test.filter.js +++ /dev/null @@ -1,3 +0,0 @@ - -module.exports = () => {return false} - \ No newline at end of file diff --git a/webpack-test/hotCases/lazy-compilation/context/webpack.config.js b/webpack-test/hotCases/lazy-compilation/context/webpack.config.js index 95c9eda71874..bca70843c7f2 100644 --- a/webpack-test/hotCases/lazy-compilation/context/webpack.config.js +++ b/webpack-test/hotCases/lazy-compilation/context/webpack.config.js @@ -4,6 +4,7 @@ module.exports = { experiments: { lazyCompilation: { + cacheable: false, entries: false, imports: true, backend: { diff --git a/webpack-test/hotCases/lazy-compilation/https/test.filter.js b/webpack-test/hotCases/lazy-compilation/https/test.filter.js index 5c32e24f1f81..90e5eab00bcd 100644 --- a/webpack-test/hotCases/lazy-compilation/https/test.filter.js +++ b/webpack-test/hotCases/lazy-compilation/https/test.filter.js @@ -1,3 +1,2 @@ -module.exports = () => {return false} - \ No newline at end of file +module.exports = () => { return false } diff --git a/webpack-test/hotCases/lazy-compilation/https/webpack.config.js b/webpack-test/hotCases/lazy-compilation/https/webpack.config.js index 11b07858d4d3..ca3f7afdf0fb 100644 --- a/webpack-test/hotCases/lazy-compilation/https/webpack.config.js +++ b/webpack-test/hotCases/lazy-compilation/https/webpack.config.js @@ -7,6 +7,7 @@ const path = require("path"); module.exports = { experiments: { lazyCompilation: { + cacheable: false, entries: false, backend: { server: { diff --git a/webpack-test/hotCases/lazy-compilation/module-test/test.filter.js b/webpack-test/hotCases/lazy-compilation/module-test/test.filter.js deleted file mode 100644 index 5c32e24f1f81..000000000000 --- a/webpack-test/hotCases/lazy-compilation/module-test/test.filter.js +++ /dev/null @@ -1,3 +0,0 @@ - -module.exports = () => {return false} - \ No newline at end of file diff --git a/webpack-test/hotCases/lazy-compilation/module-test/webpack.config.js b/webpack-test/hotCases/lazy-compilation/module-test/webpack.config.js index ede992e2343c..559f2fcbd22a 100644 --- a/webpack-test/hotCases/lazy-compilation/module-test/webpack.config.js +++ b/webpack-test/hotCases/lazy-compilation/module-test/webpack.config.js @@ -5,7 +5,8 @@ module.exports = { experiments: { lazyCompilation: { entries: false, - test: module => !/moduleB/.test(module.nameForCondition()) + cacheable: false, + test: /moduleA/ } } }; diff --git a/webpack-test/hotCases/lazy-compilation/only-entries/test.filter.js b/webpack-test/hotCases/lazy-compilation/only-entries/test.filter.js deleted file mode 100644 index 5c32e24f1f81..000000000000 --- a/webpack-test/hotCases/lazy-compilation/only-entries/test.filter.js +++ /dev/null @@ -1,3 +0,0 @@ - -module.exports = () => {return false} - \ No newline at end of file diff --git a/webpack-test/hotCases/lazy-compilation/simple/test.filter.js b/webpack-test/hotCases/lazy-compilation/simple/test.filter.js index 5c32e24f1f81..90e5eab00bcd 100644 --- a/webpack-test/hotCases/lazy-compilation/simple/test.filter.js +++ b/webpack-test/hotCases/lazy-compilation/simple/test.filter.js @@ -1,3 +1,2 @@ -module.exports = () => {return false} - \ No newline at end of file +module.exports = () => { return false } diff --git a/webpack-test/hotCases/lazy-compilation/simple/webpack.config.js b/webpack-test/hotCases/lazy-compilation/simple/webpack.config.js index 148a5e400106..16ad9ede08e4 100644 --- a/webpack-test/hotCases/lazy-compilation/simple/webpack.config.js +++ b/webpack-test/hotCases/lazy-compilation/simple/webpack.config.js @@ -4,7 +4,8 @@ module.exports = { experiments: { lazyCompilation: { - entries: false + entries: false, + cacheable: false } } }; diff --git a/webpack-test/hotCases/lazy-compilation/unrelated/test.filter.js b/webpack-test/hotCases/lazy-compilation/unrelated/test.filter.js deleted file mode 100644 index 5c32e24f1f81..000000000000 --- a/webpack-test/hotCases/lazy-compilation/unrelated/test.filter.js +++ /dev/null @@ -1,3 +0,0 @@ - -module.exports = () => {return false} - \ No newline at end of file diff --git a/website/docs/en/config/experiments.mdx b/website/docs/en/config/experiments.mdx index a0aeba64a50c..638dddcac95b 100644 --- a/website/docs/en/config/experiments.mdx +++ b/website/docs/en/config/experiments.mdx @@ -62,6 +62,32 @@ Enable support for [top level await](https://github.com/tc39/proposal-top-level- Enabled by default and can be turned off with this configuration. +## experiments.lazyCompilation + + + +- **Type:** + +```ts +type LazyCompilationOptions = + | boolean + | { + entries?: boolean; + + imports?: boolean; + + test?: RegExp | ((m: Module) => boolean); + }; +``` + +- **Default:** `false` + +Enable lazy compilation, which can greatly improve the build performance of multi-entry projects or large projects. For example, if you have twenty entry points, only the accessed entry points will be built. Or if there are many `import()` statements in the project, each module pointed to by `import()` will only be built when it is actually accessed. + +If set to true, lazy compilation will be applied by default to both entry modules and modules pointed to by `import()`. You can decide whether it applies only to entries or only to `import()` through a configuration object. The `entries` option determines whether it applies to entries, while the `import()` option determines whether it applies to `import()`. + +In addition, you can also configure a `test` parameter for more fine-grained control over which modules are lazily compiled. The `test` parameter can be a regular expression that matches only those modules that should be lazily compiled. It can also be a function where the input is of type 'Module' and returns a boolean value indicating whether it meets the criteria for lazy compilation logic. + ## experiments.rspackFuture diff --git a/website/docs/zh/config/experiments.mdx b/website/docs/zh/config/experiments.mdx index 4b99175660b3..18fbebdceaba 100644 --- a/website/docs/zh/config/experiments.mdx +++ b/website/docs/zh/config/experiments.mdx @@ -59,6 +59,32 @@ module.exports = { 默认开启,可通过该配置关闭。 +## experiments.lazyCompilation + + + +- **类型:** + +```ts +type LazyCompilationOptions = + | boolean + | { + entries?: boolean; + + imports?: boolean; + + test?: RegExp | ((m: Module) => boolean); + }; +``` + +- **默认值:** `false` + +开启懒编译,这对提高多入口项目或大型项目的构建性能会非常有帮助。例如你有二十个入口,只有访问到的入口才会进行构建,或者如果项目中存在非常多的 `import()`,每一个 `import()` 所指向的模块都只有在被真正访问到时,才进行构建。 + +如果设置为 true,则默认会对入口模块以及 `import()` 指向的模块进行懒编译。你可以通过配置对象形式,来决定是否只对入口或只对 `import()` 生效。`entries` 决定是否对入口生效,`import()` 决定是否对 `import()` 生效。 + +除此以外你还可以配置 `test` 来更细粒度控制对哪些模块进行懒编译。`test` 可以是一个正则表达式,只对该正则匹配到的模块进行懒编译,`test` 也可以是一个函数,函数的输入是 `Module` 类型,返回 `boolean` 类型,表示是否命中懒编译逻辑。 + ## experiments.rspackFuture