diff --git a/Cargo.lock b/Cargo.lock index dca898dc2d4..2ecd97644ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2971,6 +2971,7 @@ dependencies = [ "rspack_paths", "rspack_plugin_asset", "rspack_plugin_banner", + "rspack_plugin_context_replacement", "rspack_plugin_copy", "rspack_plugin_css", "rspack_plugin_devtool", @@ -3399,6 +3400,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "rspack_plugin_context_replacement" +version = "0.1.0" +dependencies = [ + "derivative", + "rspack_core", + "rspack_error", + "rspack_hook", + "rspack_paths", + "rspack_regex", + "rustc-hash 1.1.0", + "tracing", +] + [[package]] name = "rspack_plugin_copy" version = "0.1.0" diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 0b0248cfea4..b1bf342c1a0 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -118,6 +118,30 @@ export class JsCompilation { get entries(): JsEntries } +export class JsContextModuleFactoryAfterResolveData { + get resource(): string + set resource(resource: string) + get context(): string + set context(context: string) + get request(): string + set request(request: string) + get regExp(): RawRegex | undefined + set regExp(rawRegExp: RawRegex | undefined) + get recursive(): boolean + set recursive(recursive: boolean) +} + +export class JsContextModuleFactoryBeforeResolveData { + get context(): string + set context(context: string) + get request(): string + set request(request: string) + get regExp(): RawRegex | undefined + set regExp(rawRegExp: RawRegex | undefined) + get recursive(): boolean + set recursive(recursive: boolean) +} + export class JsEntries { clear(): void get size(): number @@ -264,6 +288,7 @@ export enum BuiltinPluginName { RuntimeChunkPlugin = 'RuntimeChunkPlugin', SizeLimitsPlugin = 'SizeLimitsPlugin', NoEmitOnErrorsPlugin = 'NoEmitOnErrorsPlugin', + ContextReplacementPlugin = 'ContextReplacementPlugin', HttpExternalsRspackPlugin = 'HttpExternalsRspackPlugin', CopyRspackPlugin = 'CopyRspackPlugin', HtmlRspackPlugin = 'HtmlRspackPlugin', @@ -461,18 +486,6 @@ export interface JsCompatSource { map?: string } -export interface JsContextModuleFactoryAfterResolveData { - resource: string - context: string - request: string - regExp?: RawRegex -} - -export interface JsContextModuleFactoryBeforeResolveData { - context: string - request?: string -} - export interface JsCreateData { request: string userRequest: string @@ -1122,6 +1135,14 @@ export interface RawContainerReferencePluginOptions { enhanced: boolean } +export interface RawContextReplacementPluginOptions { + resourceRegExp: RawRegex + newContentResource?: string + newContentRecursive?: boolean + newContentRegExp?: RawRegex + newContentCreateContextMap?: Record +} + export interface RawCopyGlobOptions { caseSensitiveMatch?: boolean dot?: boolean diff --git a/crates/node_binding/src/plugins/interceptor.rs b/crates/node_binding/src/plugins/interceptor.rs index 3ec1840f407..a5b8dc81a32 100644 --- a/crates/node_binding/src/plugins/interceptor.rs +++ b/crates/node_binding/src/plugins/interceptor.rs @@ -15,8 +15,8 @@ use rspack_binding_values::{ JsAfterEmitData, JsAfterResolveData, JsAfterResolveOutput, JsAfterTemplateExecutionData, JsAlterAssetTagGroupsData, JsAlterAssetTagsData, JsAssetEmittedArgs, JsBeforeAssetTagGenerationData, JsBeforeEmitData, JsBeforeResolveArgs, JsBeforeResolveOutput, - JsChunk, JsChunkAssetArgs, JsCompilationWrapper, JsContextModuleFactoryAfterResolveData, - JsContextModuleFactoryAfterResolveResult, JsContextModuleFactoryBeforeResolveData, + JsChunk, JsChunkAssetArgs, JsCompilationWrapper, JsContextModuleFactoryAfterResolveDataWrapper, + JsContextModuleFactoryAfterResolveResult, JsContextModuleFactoryBeforeResolveDataWrapper, JsContextModuleFactoryBeforeResolveResult, JsCreateData, JsExecuteModuleArg, JsFactorizeArgs, JsFactorizeOutput, JsModule, JsNormalModuleFactoryCreateModuleArgs, JsResolveArgs, JsResolveForSchemeArgs, JsResolveForSchemeOutput, JsResolveOutput, JsRuntimeGlobals, @@ -24,13 +24,13 @@ use rspack_binding_values::{ }; use rspack_collections::IdentifierSet; use rspack_core::{ - parse_resource, AfterResolveData, AfterResolveResult, AssetEmittedInfo, BeforeResolveData, - BeforeResolveResult, BoxModule, Chunk, ChunkUkey, CodeGenerationResults, Compilation, - CompilationAdditionalTreeRuntimeRequirements, CompilationAdditionalTreeRuntimeRequirementsHook, - CompilationAfterOptimizeModules, CompilationAfterOptimizeModulesHook, - CompilationAfterProcessAssets, CompilationAfterProcessAssetsHook, CompilationAfterSeal, - CompilationAfterSealHook, CompilationBuildModule, CompilationBuildModuleHook, - CompilationChunkAsset, CompilationChunkAssetHook, CompilationChunkHash, CompilationChunkHashHook, + parse_resource, AfterResolveResult, AssetEmittedInfo, BeforeResolveResult, BoxModule, Chunk, + ChunkUkey, CodeGenerationResults, Compilation, CompilationAdditionalTreeRuntimeRequirements, + CompilationAdditionalTreeRuntimeRequirementsHook, CompilationAfterOptimizeModules, + CompilationAfterOptimizeModulesHook, CompilationAfterProcessAssets, + CompilationAfterProcessAssetsHook, CompilationAfterSeal, CompilationAfterSealHook, + CompilationBuildModule, CompilationBuildModuleHook, CompilationChunkAsset, + CompilationChunkAssetHook, CompilationChunkHash, CompilationChunkHashHook, CompilationExecuteModule, CompilationExecuteModuleHook, CompilationFinishModules, CompilationFinishModulesHook, CompilationOptimizeChunkModules, CompilationOptimizeChunkModulesHook, CompilationOptimizeModules, CompilationOptimizeModulesHook, @@ -1507,23 +1507,14 @@ impl ContextModuleFactoryBeforeResolve for ContextModuleFactoryBeforeResolveTap async fn run(&self, result: BeforeResolveResult) -> rspack_error::Result { let js_result = match result { BeforeResolveResult::Ignored => JsContextModuleFactoryBeforeResolveResult::A(false), - BeforeResolveResult::Data(d) => { - JsContextModuleFactoryBeforeResolveResult::B(JsContextModuleFactoryBeforeResolveData { - context: d.context, - request: d.request, - }) - } + BeforeResolveResult::Data(data) => JsContextModuleFactoryBeforeResolveResult::B( + JsContextModuleFactoryBeforeResolveDataWrapper::new(data), + ), }; match self.function.call_with_promise(js_result).await { Ok(js_result) => match js_result { napi::bindgen_prelude::Either::A(_) => Ok(BeforeResolveResult::Ignored), - napi::bindgen_prelude::Either::B(d) => { - let data = BeforeResolveData { - context: d.context, - request: d.request, - }; - Ok(BeforeResolveResult::Data(Box::new(data))) - } + napi::bindgen_prelude::Either::B(js_data) => Ok(BeforeResolveResult::Data(js_data.take())), }, Err(err) => Err(err), } @@ -1539,29 +1530,13 @@ impl ContextModuleFactoryAfterResolve for ContextModuleFactoryAfterResolveTap { async fn run(&self, result: AfterResolveResult) -> rspack_error::Result { let js_result = match result { AfterResolveResult::Ignored => JsContextModuleFactoryAfterResolveResult::A(false), - AfterResolveResult::Data(d) => { - JsContextModuleFactoryAfterResolveResult::B(JsContextModuleFactoryAfterResolveData { - resource: d.resource.as_str().to_owned(), - context: d.context.to_owned(), - request: d.request.to_owned(), - reg_exp: d.reg_exp.clone().map(|r| r.into()), - }) - } + AfterResolveResult::Data(data) => JsContextModuleFactoryAfterResolveResult::B( + JsContextModuleFactoryAfterResolveDataWrapper::new(data), + ), }; match self.function.call_with_promise(js_result).await? { napi::Either::A(_) => Ok(AfterResolveResult::Ignored), - napi::Either::B(d) => { - let data = AfterResolveData { - resource: d.resource.into(), - context: d.context, - request: d.request, - reg_exp: match d.reg_exp { - Some(r) => Some(r.try_into()?), - None => None, - }, - }; - Ok(AfterResolveResult::Data(Box::new(data))) - } + napi::Either::B(js_data) => Ok(AfterResolveResult::Data(js_data.take())), } } diff --git a/crates/rspack_binding_options/Cargo.toml b/crates/rspack_binding_options/Cargo.toml index 46fbe4e23e8..262d7f76624 100644 --- a/crates/rspack_binding_options/Cargo.toml +++ b/crates/rspack_binding_options/Cargo.toml @@ -36,6 +36,7 @@ rspack_napi_macros = { version = "0.1.0", path = "../rspack_n rspack_paths = { version = "0.1.0", path = "../rspack_paths" } rspack_plugin_asset = { version = "0.1.0", path = "../rspack_plugin_asset" } rspack_plugin_banner = { version = "0.1.0", path = "../rspack_plugin_banner" } +rspack_plugin_context_replacement = { version = "0.1.0", path = "../rspack_plugin_context_replacement" } rspack_plugin_copy = { version = "0.1.0", path = "../rspack_plugin_copy" } rspack_plugin_css = { version = "0.1.0", path = "../rspack_plugin_css" } rspack_plugin_devtool = { version = "0.1.0", path = "../rspack_plugin_devtool" } 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 db120ec9367..eff25843495 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs @@ -26,6 +26,7 @@ use rspack_ids::{ use rspack_napi::NapiResultExt; use rspack_plugin_asset::AssetPlugin; use rspack_plugin_banner::BannerPlugin; +use rspack_plugin_context_replacement::ContextReplacementPlugin; use rspack_plugin_copy::{CopyRspackPlugin, CopyRspackPluginOptions}; use rspack_plugin_css::CssPlugin; use rspack_plugin_devtool::{ @@ -91,9 +92,10 @@ use self::{ raw_size_limits::RawSizeLimitsPluginOptions, }; use crate::{ - plugins::JsLoaderRspackPlugin, JsLoaderRunner, RawDynamicEntryPluginOptions, - RawEvalDevToolModulePluginOptions, RawExternalItemWrapper, RawExternalsPluginOptions, - RawHttpExternalsRspackPluginOptions, RawSourceMapDevToolPluginOptions, RawSplitChunksOptions, + plugins::JsLoaderRspackPlugin, JsLoaderRunner, RawContextReplacementPluginOptions, + RawDynamicEntryPluginOptions, RawEvalDevToolModulePluginOptions, RawExternalItemWrapper, + RawExternalsPluginOptions, RawHttpExternalsRspackPluginOptions, RawSourceMapDevToolPluginOptions, + RawSplitChunksOptions, }; #[napi(string_enum)] @@ -161,6 +163,7 @@ pub enum BuiltinPluginName { RuntimeChunkPlugin, SizeLimitsPlugin, NoEmitOnErrorsPlugin, + ContextReplacementPlugin, // rspack specific plugins // naming format follow XxxRspackPlugin @@ -504,6 +507,11 @@ impl BuiltinPlugin { BuiltinPluginName::NoEmitOnErrorsPlugin => { plugins.push(NoEmitOnErrorsPlugin::default().boxed()); } + BuiltinPluginName::ContextReplacementPlugin => { + let raw_options = downcast_into::(self.options)?; + let options = raw_options.try_into()?; + plugins.push(ContextReplacementPlugin::new(options).boxed()); + } } Ok(()) } diff --git a/crates/rspack_binding_options/src/plugins/context_replacement.rs b/crates/rspack_binding_options/src/plugins/context_replacement.rs new file mode 100644 index 00000000000..1f2f9c7b727 --- /dev/null +++ b/crates/rspack_binding_options/src/plugins/context_replacement.rs @@ -0,0 +1,57 @@ +use napi::bindgen_prelude::Object; +use napi_derive::napi; +use rspack_binding_values::RawRegex; +use rspack_error::{miette::IntoDiagnostic, Error}; +use rspack_plugin_context_replacement::ContextReplacementPluginOptions; +use rspack_regex::RspackRegex; +use rustc_hash::FxHashMap as HashMap; + +#[napi(object, object_to_js = false)] +pub struct RawContextReplacementPluginOptions { + pub resource_reg_exp: RawRegex, + pub new_content_resource: Option, + pub new_content_recursive: Option, + pub new_content_reg_exp: Option, + #[napi(ts_type = "Record")] + pub new_content_create_context_map: Option, + // new_content_callback +} + +impl TryFrom for ContextReplacementPluginOptions { + type Error = Error; + + fn try_from(val: RawContextReplacementPluginOptions) -> Result { + let new_content_reg_exp = match val.new_content_reg_exp { + Some(js_regex) => { + let regex = RspackRegex::with_flags(&js_regex.source, &js_regex.flags)?; + Some(regex) + } + None => None, + }; + + let new_content_create_context_map = if let Some(raw) = val.new_content_create_context_map { + let mut map = HashMap::default(); + let keys = Object::keys(&raw).into_diagnostic()?; + for key in keys { + let value = raw.get::<&str, String>(&key).into_diagnostic()?; + if let Some(value) = value { + map.insert(key, value); + } + } + Some(map) + } else { + None + }; + + Ok(Self { + resource_reg_exp: RspackRegex::with_flags( + &val.resource_reg_exp.source, + &val.resource_reg_exp.flags, + )?, + new_content_resource: val.new_content_resource, + new_content_recursive: val.new_content_recursive, + new_content_reg_exp, + new_content_create_context_map, + }) + } +} diff --git a/crates/rspack_binding_options/src/plugins/mod.rs b/crates/rspack_binding_options/src/plugins/mod.rs index 34831055ade..c3af088cf4d 100644 --- a/crates/rspack_binding_options/src/plugins/mod.rs +++ b/crates/rspack_binding_options/src/plugins/mod.rs @@ -1,2 +1,5 @@ +mod context_replacement; mod js_loader; + +pub use context_replacement::*; pub(super) use js_loader::{JsLoaderRspackPlugin, JsLoaderRunner}; diff --git a/crates/rspack_binding_values/src/context_module_factory.rs b/crates/rspack_binding_values/src/context_module_factory.rs index 53efd945cc3..a8e52868970 100644 --- a/crates/rspack_binding_values/src/context_module_factory.rs +++ b/crates/rspack_binding_values/src/context_module_factory.rs @@ -1,24 +1,225 @@ -use napi::bindgen_prelude::Either; +use napi::bindgen_prelude::{ + ClassInstance, Either, FromNapiValue, ToNapiValue, TypeName, ValidateNapiValue, +}; use napi_derive::napi; +use rspack_core::{AfterResolveData, BeforeResolveData}; use crate::RawRegex; -#[napi(object)] -pub struct JsContextModuleFactoryBeforeResolveData { - pub context: String, - pub request: Option, +#[napi] +pub struct JsContextModuleFactoryBeforeResolveData(Box); + +#[napi] +impl JsContextModuleFactoryBeforeResolveData { + #[napi(getter)] + pub fn context(&self) -> &str { + &self.0.context + } + + #[napi(setter)] + pub fn set_context(&mut self, context: String) { + self.0.context = context; + } + + #[napi(getter)] + pub fn request(&self) -> &str { + &self.0.request + } + + #[napi(setter)] + pub fn set_request(&mut self, request: String) { + self.0.request = request; + } + + #[napi(getter)] + pub fn reg_exp(&self) -> Either { + match &self.0.reg_exp { + Some(r) => Either::A(r.clone().into()), + None => Either::B(()), + } + } + + #[napi(setter)] + pub fn set_reg_exp(&mut self, raw_reg_exp: Either) { + self.0.reg_exp = match raw_reg_exp { + Either::A(raw_reg_exp) => match raw_reg_exp.try_into() { + Ok(reg_exp) => Some(reg_exp), + Err(_) => None, + }, + Either::B(_) => None, + }; + } + + #[napi(getter)] + pub fn recursive(&self) -> bool { + self.0.recursive + } + + #[napi(setter)] + pub fn set_recursive(&mut self, recursive: bool) { + self.0.recursive = recursive; + } +} + +pub struct JsContextModuleFactoryBeforeResolveDataWrapper(Box); + +impl JsContextModuleFactoryBeforeResolveDataWrapper { + pub fn new(data: Box) -> Self { + Self(data) + } + + pub fn take(self) -> Box { + self.0 + } +} + +impl FromNapiValue for JsContextModuleFactoryBeforeResolveDataWrapper { + unsafe fn from_napi_value( + env: napi::sys::napi_env, + napi_val: napi::sys::napi_value, + ) -> napi::Result { + let instance = + as FromNapiValue>::from_napi_value( + env, napi_val, + )?; + Ok(Self(instance.0.clone())) + } +} + +impl ToNapiValue for JsContextModuleFactoryBeforeResolveDataWrapper { + unsafe fn to_napi_value( + env: napi::sys::napi_env, + val: Self, + ) -> napi::Result { + let js_val = JsContextModuleFactoryBeforeResolveData(val.0); + ToNapiValue::to_napi_value(env, js_val) + } +} + +impl TypeName for JsContextModuleFactoryBeforeResolveDataWrapper { + fn type_name() -> &'static str { + "JsContextModuleFactoryBeforeResolveData" + } + fn value_type() -> napi::ValueType { + napi::ValueType::Object + } } +impl ValidateNapiValue for JsContextModuleFactoryBeforeResolveDataWrapper {} + pub type JsContextModuleFactoryBeforeResolveResult = - Either; + Either; + +#[napi] +pub struct JsContextModuleFactoryAfterResolveData(Box); -#[napi(object)] -pub struct JsContextModuleFactoryAfterResolveData { - pub resource: String, - pub context: String, - pub request: String, - pub reg_exp: Option, +#[napi] +impl JsContextModuleFactoryAfterResolveData { + #[napi(getter)] + pub fn resource(&self) -> &str { + self.0.resource.as_str() + } + + #[napi(setter)] + pub fn set_resource(&mut self, resource: String) { + self.0.resource = resource.into(); + } + + #[napi(getter)] + pub fn context(&self) -> &str { + &self.0.context + } + + #[napi(setter)] + pub fn set_context(&mut self, context: String) { + self.0.context = context; + } + + #[napi(getter)] + pub fn request(&self) -> &str { + &self.0.request + } + + #[napi(setter)] + pub fn set_request(&mut self, request: String) { + self.0.request = request; + } + + #[napi(getter)] + pub fn reg_exp(&self) -> Either { + match &self.0.reg_exp { + Some(r) => Either::A(r.clone().into()), + None => Either::B(()), + } + } + + #[napi(setter)] + pub fn set_reg_exp(&mut self, raw_reg_exp: Either) { + self.0.reg_exp = match raw_reg_exp { + Either::A(raw_reg_exp) => match raw_reg_exp.try_into() { + Ok(reg_exp) => Some(reg_exp), + Err(_) => None, + }, + Either::B(_) => None, + }; + } + + #[napi(getter)] + pub fn recursive(&self) -> bool { + self.0.recursive + } + + #[napi(setter)] + pub fn set_recursive(&mut self, recursive: bool) { + self.0.recursive = recursive; + } +} + +pub struct JsContextModuleFactoryAfterResolveDataWrapper(Box); + +impl JsContextModuleFactoryAfterResolveDataWrapper { + pub fn new(data: Box) -> Self { + JsContextModuleFactoryAfterResolveDataWrapper(data) + } + + pub fn take(self) -> Box { + self.0 + } +} + +impl FromNapiValue for JsContextModuleFactoryAfterResolveDataWrapper { + unsafe fn from_napi_value( + env: napi::sys::napi_env, + napi_val: napi::sys::napi_value, + ) -> napi::Result { + let instance = + as FromNapiValue>::from_napi_value( + env, napi_val, + )?; + Ok(Self(instance.0.clone())) + } } +impl ToNapiValue for JsContextModuleFactoryAfterResolveDataWrapper { + unsafe fn to_napi_value( + env: napi::sys::napi_env, + val: Self, + ) -> napi::Result { + let js_val = JsContextModuleFactoryAfterResolveData(val.0); + ToNapiValue::to_napi_value(env, js_val) + } +} + +impl TypeName for JsContextModuleFactoryAfterResolveDataWrapper { + fn type_name() -> &'static str { + "JsContextModuleFactoryAfterResolveData" + } + fn value_type() -> napi::ValueType { + napi::ValueType::Object + } +} + +impl ValidateNapiValue for JsContextModuleFactoryAfterResolveDataWrapper {} + pub type JsContextModuleFactoryAfterResolveResult = - Either; + Either; diff --git a/crates/rspack_core/src/context_module.rs b/crates/rspack_core/src/context_module.rs index 5a3de3df9fc..02fb03a5bb6 100644 --- a/crates/rspack_core/src/context_module.rs +++ b/crates/rspack_core/src/context_module.rs @@ -1,15 +1,15 @@ use std::path::PathBuf; -use std::sync::LazyLock; -use std::{borrow::Cow, fs, hash::Hash, sync::Arc}; +use std::sync::{Arc, LazyLock}; +use std::{borrow::Cow, hash::Hash}; -use cow_utils::CowUtils; +use derivative::Derivative; use indoc::formatdoc; use itertools::Itertools; use regex::{Captures, Regex}; use rspack_collections::{Identifiable, Identifier}; -use rspack_error::{impl_empty_diagnosable_trait, miette::IntoDiagnostic, Diagnostic, Result}; +use rspack_error::{impl_empty_diagnosable_trait, Diagnostic, Result}; use rspack_macros::impl_source_map_config; -use rspack_paths::{AssertUtf8, Utf8Path, Utf8PathBuf}; +use rspack_paths::Utf8PathBuf; use rspack_regex::RspackRegex; use rspack_sources::{BoxSource, ConcatSource, RawSource, SourceExt}; use rspack_util::itoa; @@ -25,23 +25,17 @@ use crate::{ BuildMetaDefaultObject, BuildMetaExportsType, BuildResult, ChunkGraph, ChunkGroupOptions, CodeGenerationResult, Compilation, ConcatenationScope, ContextElementDependency, DependenciesBlock, Dependency, DependencyCategory, DependencyId, DependencyLocation, - DependencyType, DynamicImportMode, ExportsType, FactoryMeta, FakeNamespaceObjectMode, - GroupOptions, ImportAttributes, LibIdentOptions, Module, ModuleLayer, ModuleType, - RealDependencyLocation, Resolve, ResolveInnerOptions, ResolveOptionsWithDependencyType, - ResolverFactory, RuntimeGlobals, RuntimeSpec, SourceType, + DynamicImportMode, ExportsType, FactoryMeta, FakeNamespaceObjectMode, GroupOptions, + ImportAttributes, LibIdentOptions, Module, ModuleLayer, ModuleType, RealDependencyLocation, + Resolve, RuntimeGlobals, RuntimeSpec, SourceType, }; -#[derive(Debug, Clone)] -pub struct AlternativeRequest { - pub context: String, - pub request: String, -} - -impl AlternativeRequest { - pub fn new(context: String, request: String) -> Self { - Self { context, request } - } -} +static WEBPACK_CHUNK_NAME_PLACEHOLDER: LazyLock = + LazyLock::new(|| Regex::new(r"\[index|request\]").expect("regexp init failed")); +static WEBPACK_CHUNK_NAME_INDEX_PLACEHOLDER: LazyLock = + LazyLock::new(|| Regex::new(r"\[index\]").expect("regexp init failed")); +static WEBPACK_CHUNK_NAME_REQUEST_PLACEHOLDER: LazyLock = + LazyLock::new(|| Regex::new(r"\[request\]").expect("regexp init failed")); #[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum ContextMode { @@ -157,31 +151,39 @@ pub enum FakeMapValue { Map(HashMap), } +pub type ResolveContextModuleDependencies = + Arc Result> + Send + Sync>; + #[impl_source_map_config] -#[derive(Debug)] +#[derive(Derivative)] +#[derivative(Debug)] pub struct ContextModule { dependencies: Vec, blocks: Vec, identifier: Identifier, options: ContextModuleOptions, - resolve_factory: Arc, factory_meta: Option, build_info: Option, build_meta: Option, + #[derivative(Debug = "ignore")] + resolve_dependencies: ResolveContextModuleDependencies, } impl ContextModule { - pub fn new(options: ContextModuleOptions, resolve_factory: Arc) -> Self { + pub fn new( + resolve_dependencies: ResolveContextModuleDependencies, + options: ContextModuleOptions, + ) -> Self { Self { dependencies: Vec::new(), blocks: Vec::new(), identifier: create_identifier(&options), options, - resolve_factory, factory_meta: None, build_info: None, build_meta: None, source_map_kind: SourceMapKind::empty(), + resolve_dependencies, } } @@ -873,7 +875,82 @@ impl Module for ContextModule { _build_context: BuildContext<'_>, _: Option<&Compilation>, ) -> Result { - let (dependencies, blocks) = self.resolve_dependencies()?; + let resolve_dependencies = &self.resolve_dependencies; + let context_element_dependencies = resolve_dependencies(self.options.clone())?; + + let mut dependencies: Vec = vec![]; + let mut blocks = vec![]; + if matches!(self.options.context_options.mode, ContextMode::LazyOnce) + && !context_element_dependencies.is_empty() + { + let loc = RealDependencyLocation::new( + self.options.context_options.start, + self.options.context_options.end, + ); + let mut block = AsyncDependenciesBlock::new( + (*self.identifier).into(), + Some(DependencyLocation::Real(loc)), + None, + context_element_dependencies + .into_iter() + .map(|dep| Box::new(dep) as Box) + .collect(), + None, + ); + if let Some(group_options) = &self.options.context_options.group_options { + block.set_group_options(group_options.clone()); + } + blocks.push(Box::new(block)); + } else if matches!(self.options.context_options.mode, ContextMode::Lazy) { + let mut index = 0; + // TODO(shulaoda): add loc for ContextElementDependency and AsyncDependenciesBlock + for context_element_dependency in context_element_dependencies { + let group_options = self + .options + .context_options + .group_options + .as_ref() + .and_then(|g| g.normal_options()); + let name = group_options + .and_then(|group_options| group_options.name.as_ref()) + .map(|name| { + let name = if !WEBPACK_CHUNK_NAME_PLACEHOLDER.is_match(name) { + Cow::Owned(format!("{name}[index]")) + } else { + Cow::Borrowed(name) + }; + let name = WEBPACK_CHUNK_NAME_INDEX_PLACEHOLDER + .replace_all(&name, |_: &Captures| index.to_string()); + index += 1; + let name = WEBPACK_CHUNK_NAME_REQUEST_PLACEHOLDER.replace_all(&name, |_: &Captures| { + to_path(&context_element_dependency.user_request) + }); + name.into_owned() + }); + let preload_order = group_options.and_then(|o| o.preload_order); + let prefetch_order = group_options.and_then(|o| o.prefetch_order); + let fetch_priority = group_options.and_then(|o| o.fetch_priority); + let mut block = AsyncDependenciesBlock::new( + (*self.identifier).into(), + None, + Some(&context_element_dependency.user_request.clone()), + vec![Box::new(context_element_dependency)], + Some(self.options.context_options.request.clone()), + ); + block.set_group_options(GroupOptions::ChunkGroup(ChunkGroupOptions::new( + name, + preload_order, + prefetch_order, + fetch_priority, + ))); + blocks.push(Box::new(block)); + } + } else { + dependencies = context_element_dependencies + .into_iter() + .map(|d| Box::new(d) as BoxDependency) + .collect(); + } let mut context_dependencies: HashSet = Default::default(); context_dependencies.insert(self.options.resource.clone().into_std_path_buf()); @@ -979,210 +1056,6 @@ impl Identifiable for ContextModule { } } -static WEBPACK_CHUNK_NAME_PLACEHOLDER: LazyLock = - LazyLock::new(|| Regex::new(r"\[index|request\]").expect("regexp init failed")); -static WEBPACK_CHUNK_NAME_INDEX_PLACEHOLDER: LazyLock = - LazyLock::new(|| Regex::new(r"\[index\]").expect("regexp init failed")); -static WEBPACK_CHUNK_NAME_REQUEST_PLACEHOLDER: LazyLock = - LazyLock::new(|| Regex::new(r"\[request\]").expect("regexp init failed")); - -impl ContextModule { - fn visit_dirs( - ctx: &str, - dir: &Utf8Path, - dependencies: &mut Vec, - options: &ContextModuleOptions, - resolve_options: &ResolveInnerOptions, - ) -> Result<()> { - if !dir.is_dir() { - return Ok(()); - } - let include = &options.context_options.include; - let exclude = &options.context_options.exclude; - for entry in fs::read_dir(dir).into_diagnostic()? { - let path = entry.into_diagnostic()?.path().assert_utf8(); - let path_str = path.as_str(); - - if let Some(exclude) = exclude - && exclude.test(path_str) - { - // ignore excluded files - continue; - } - - if path.is_dir() { - if options.context_options.recursive { - Self::visit_dirs(ctx, &path, dependencies, options, resolve_options)?; - } - } else if path.file_name().map_or(false, |name| name.starts_with('.')) { - // ignore hidden files - continue; - } else { - if let Some(include) = include - && !include.test(path_str) - { - // ignore not included files - continue; - } - - // FIXME: nodejs resolver return path of context, sometimes is '/a/b', sometimes is '/a/b/' - let relative_path = { - let path_str = path_str.to_owned().drain(ctx.len()..).collect::(); - let p = path_str.cow_replace('\\', "/"); - if p.as_ref().starts_with('/') { - format!(".{p}") - } else { - format!("./{p}") - } - }; - - let requests = alternative_requests( - resolve_options, - vec![AlternativeRequest::new(ctx.to_string(), relative_path)], - ); - - let Some(reg_exp) = &options.context_options.reg_exp else { - return Ok(()); - }; - - requests.iter().for_each(|r| { - if !reg_exp.test(&r.request) { - return; - } - dependencies.push(ContextElementDependency { - id: DependencyId::new(), - request: format!( - "{}{}{}{}", - options.addon, - r.request, - options.resource_query.clone(), - options.resource_fragment.clone(), - ), - user_request: r.request.to_string(), - category: options.context_options.category, - context: options.resource.clone().into(), - layer: options.layer.clone(), - options: options.context_options.clone(), - resource_identifier: ContextElementDependency::create_resource_identifier( - options.resource.as_str(), - &path, - options.context_options.attributes.as_ref(), - ), - attributes: options.context_options.attributes.clone(), - referenced_exports: options.context_options.referenced_exports.clone(), - dependency_type: DependencyType::ContextElement(options.type_prefix), - }); - }) - } - } - Ok(()) - } - - // Vec> makes sense if T is a large type (see #3530, 1st comment). - // #3530: https://github.com/rust-lang/rust-clippy/issues/3530 - #[allow(clippy::vec_box)] - fn resolve_dependencies(&self) -> Result<(Vec, Vec>)> { - tracing::trace!("resolving context module path {}", self.options.resource); - - let resolver = &self.resolve_factory.get(ResolveOptionsWithDependencyType { - resolve_options: self.options.resolve_options.clone(), - resolve_to_context: false, - dependency_category: self.options.context_options.category, - }); - - let mut context_element_dependencies = vec![]; - Self::visit_dirs( - self.options.resource.as_str(), - &self.options.resource, - &mut context_element_dependencies, - &self.options, - &resolver.options(), - )?; - context_element_dependencies.sort_by_cached_key(|d| d.user_request.to_string()); - - tracing::trace!( - "resolving dependencies for {:?}", - context_element_dependencies - ); - - let mut dependencies: Vec = vec![]; - let mut blocks = vec![]; - if matches!(self.options.context_options.mode, ContextMode::LazyOnce) - && !context_element_dependencies.is_empty() - { - let loc = RealDependencyLocation::new( - self.options.context_options.start, - self.options.context_options.end, - ); - let mut block = AsyncDependenciesBlock::new( - self.identifier, - Some(DependencyLocation::Real(loc)), - None, - context_element_dependencies - .into_iter() - .map(|dep| Box::new(dep) as Box) - .collect(), - None, - ); - if let Some(group_options) = &self.options.context_options.group_options { - block.set_group_options(group_options.clone()); - } - blocks.push(Box::new(block)); - } else if matches!(self.options.context_options.mode, ContextMode::Lazy) { - let mut index = 0; - // TODO(shulaoda): add loc for ContextElementDependency and AsyncDependenciesBlock - for context_element_dependency in context_element_dependencies { - let group_options = self - .options - .context_options - .group_options - .as_ref() - .and_then(|g| g.normal_options()); - let name = group_options - .and_then(|group_options| group_options.name.as_ref()) - .map(|name| { - let name = if !WEBPACK_CHUNK_NAME_PLACEHOLDER.is_match(name) { - Cow::Owned(format!("{name}[index]")) - } else { - Cow::Borrowed(name) - }; - let name = WEBPACK_CHUNK_NAME_INDEX_PLACEHOLDER - .replace_all(&name, |_: &Captures| index.to_string()); - index += 1; - let name = WEBPACK_CHUNK_NAME_REQUEST_PLACEHOLDER.replace_all(&name, |_: &Captures| { - to_path(&context_element_dependency.user_request) - }); - name.into_owned() - }); - let preload_order = group_options.and_then(|o| o.preload_order); - let prefetch_order = group_options.and_then(|o| o.prefetch_order); - let fetch_priority = group_options.and_then(|o| o.fetch_priority); - let mut block = AsyncDependenciesBlock::new( - self.identifier, - None, - Some(&context_element_dependency.user_request.clone()), - vec![Box::new(context_element_dependency)], - Some(self.options.context_options.request.clone()), - ); - block.set_group_options(GroupOptions::ChunkGroup(ChunkGroupOptions::new( - name, - preload_order, - prefetch_order, - fetch_priority, - ))); - blocks.push(Box::new(block)); - } - } else { - dependencies = context_element_dependencies - .into_iter() - .map(|d| Box::new(d) as BoxDependency) - .collect(); - } - - Ok((dependencies, blocks)) - } -} - fn create_identifier(options: &ContextModuleOptions) -> Identifier { let mut id = options.resource.as_str().to_owned(); if !options.resource_query.is_empty() { @@ -1249,70 +1122,3 @@ fn create_identifier(options: &ContextModuleOptions) -> Identifier { } id.into() } - -pub fn normalize_context(str: &str) -> String { - if str == "./" || str == "." { - return String::new(); - } - if str.ends_with('/') { - return str.to_string(); - } - str.to_string() + "/" -} - -fn alternative_requests( - resolve_options: &ResolveInnerOptions, - mut items: Vec, -) -> Vec { - // TODO: should respect fullySpecified resolve options - for item in std::mem::take(&mut items) { - if !resolve_options.is_enforce_extension_enabled() { - items.push(item.clone()); - } - for ext in resolve_options.extensions() { - if item.request.ends_with(ext) { - items.push(AlternativeRequest::new( - item.context.clone(), - item.request[..(item.request.len() - ext.len())].to_string(), - )); - } - } - } - - for item in std::mem::take(&mut items) { - items.push(item.clone()); - for main_file in resolve_options.main_files() { - if item.request.ends_with(&format!("/{main_file}")) { - items.push(AlternativeRequest::new( - item.context.clone(), - item.request[..(item.request.len() - main_file.len())].to_string(), - )); - items.push(AlternativeRequest::new( - item.context.clone(), - item.request[..(item.request.len() - main_file.len() - 1)].to_string(), - )); - } - } - } - - for item in std::mem::take(&mut items) { - items.push(item.clone()); - // TODO resolveOptions.modules can be array - for module in resolve_options.modules() { - let dir = module.cow_replace('\\', "/"); - let full_path: String = format!( - "{}{}", - item.context.cow_replace('\\', "/"), - &item.request[1..] - ); - if full_path.starts_with(dir.as_ref()) { - items.push(AlternativeRequest::new( - item.context.clone(), - full_path[(dir.len() + 1)..].to_string(), - )); - } - } - } - - items -} diff --git a/crates/rspack_core/src/context_module_factory.rs b/crates/rspack_core/src/context_module_factory.rs index e987effcdb5..a17ffe9f18b 100644 --- a/crates/rspack_core/src/context_module_factory.rs +++ b/crates/rspack_core/src/context_module_factory.rs @@ -1,39 +1,44 @@ -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, fs, sync::Arc}; use cow_utils::CowUtils; -use rspack_error::{error, Result}; +use derivative::Derivative; +use rspack_error::{error, miette::IntoDiagnostic, Result}; use rspack_hook::define_hook; -use rspack_paths::Utf8PathBuf; +use rspack_paths::{AssertUtf8, Utf8Path, Utf8PathBuf}; use rspack_regex::RspackRegex; +use swc_core::common::util::take::Take; use tracing::instrument; use crate::{ - resolve, ContextModule, ContextModuleOptions, DependencyCategory, ErrorSpan, ModuleExt, - ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult, ModuleIdentifier, RawModule, - ResolveArgs, ResolveOptionsWithDependencyType, ResolveResult, Resolver, ResolverFactory, - SharedPluginDriver, + resolve, BoxDependency, ContextElementDependency, ContextModule, ContextModuleOptions, + DependencyCategory, DependencyId, DependencyType, ErrorSpan, ModuleExt, ModuleFactory, + ModuleFactoryCreateData, ModuleFactoryResult, ModuleIdentifier, RawModule, ResolveArgs, + ResolveContextModuleDependencies, ResolveInnerOptions, ResolveOptionsWithDependencyType, + ResolveResult, Resolver, ResolverFactory, SharedPluginDriver, }; -#[derive(Clone)] +#[derive(Debug)] pub enum BeforeResolveResult { Ignored, Data(Box), } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct BeforeResolveData { // context_info // resolve_options pub context: String, - pub request: Option, + pub request: String, // assertions - // dependencies + pub dependencies: Vec, // dependency_type // file_dependencies // missing_dependencies // context_dependencies // create_data // cacheable + pub recursive: bool, + pub reg_exp: Option, } #[derive(Clone)] @@ -42,11 +47,12 @@ pub enum AfterResolveResult { Data(Box), } -#[derive(Clone)] +#[derive(Derivative)] +#[derivative(Debug, Clone)] pub struct AfterResolveData { pub resource: Utf8PathBuf, pub context: String, - // dependencies + pub dependencies: Vec, // layer // resolve_options // file_dependencies: HashSet, @@ -54,7 +60,7 @@ pub struct AfterResolveData { // context_dependencies: HashSet, pub request: String, // mode - // recursive: bool, + pub recursive: bool, pub reg_exp: Option, // namespace_object // addon: String, @@ -65,6 +71,8 @@ pub struct AfterResolveData { // type_prefix: String, // category: String, // referenced_exports + #[derivative(Debug = "ignore")] + pub resolve_dependencies: ResolveContextModuleDependencies, } define_hook!(ContextModuleFactoryBeforeResolve: AsyncSeriesWaterfall(data: BeforeResolveResult) -> BeforeResolveResult); @@ -76,30 +84,33 @@ pub struct ContextModuleFactoryHooks { pub after_resolve: ContextModuleFactoryAfterResolveHook, } -#[derive(Debug)] +#[derive(Derivative)] +#[derivative(Debug)] pub struct ContextModuleFactory { - resolver_factory: Arc, loader_resolver_factory: Arc, plugin_driver: SharedPluginDriver, + #[derivative(Debug = "ignore")] + resolve_dependencies: ResolveContextModuleDependencies, } #[async_trait::async_trait] impl ModuleFactory for ContextModuleFactory { #[instrument(name = "context_module_factory:create", skip_all)] async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result { - if let Some(before_resolve_result) = self.before_resolve(data).await? { - return Ok(before_resolve_result); - } - - let (factorize_result, mut context_module_options) = self.resolve(data).await?; + match self.before_resolve(data).await? { + BeforeResolveResult::Ignored => return Ok(ModuleFactoryResult::default()), + BeforeResolveResult::Data(before_resolve_result) => { + let (factorize_result, context_module_options) = + self.resolve(data, before_resolve_result).await?; + if let Some(context_module_options) = context_module_options { + if let Some(factorize_result) = self.after_resolve(data, context_module_options).await? { + return Ok(factorize_result); + } + } - if let Some(context_module_options) = context_module_options.as_mut() { - if let Some(factorize_result) = self.after_resolve(context_module_options).await? { - return Ok(factorize_result); + Ok(factorize_result) } } - - Ok(factorize_result) } } @@ -109,20 +120,55 @@ impl ContextModuleFactory { loader_resolver_factory: Arc, plugin_driver: SharedPluginDriver, ) -> Self { + let resolve_dependencies: ResolveContextModuleDependencies = Arc::new(move |options| { + tracing::trace!("resolving context module path {}", options.resource); + + let resolver = &resolver_factory.get(ResolveOptionsWithDependencyType { + resolve_options: options.resolve_options.clone(), + resolve_to_context: false, + dependency_category: options.context_options.category, + }); + + let mut context_element_dependencies = vec![]; + visit_dirs( + options.resource.as_str(), + &options.resource, + &mut context_element_dependencies, + &options, + &resolver.options(), + )?; + context_element_dependencies.sort_by_cached_key(|d| d.user_request.to_string()); + + tracing::trace!( + "resolving dependencies for {:?}", + context_element_dependencies + ); + + Ok(context_element_dependencies) + }); + Self { - resolver_factory, loader_resolver_factory, plugin_driver, + resolve_dependencies, } } async fn before_resolve( &self, data: &mut ModuleFactoryCreateData, - ) -> Result> { + ) -> Result { + let dependency = data.dependencies[0] + .as_context_dependency_mut() + .expect("should be context dependency"); + let dependency_options = dependency.options(); + let before_resolve_data = BeforeResolveData { context: data.context.to_string(), - request: data.request().map(|r| r.to_string()), + request: dependency.request().to_string(), + recursive: dependency_options.recursive, + reg_exp: dependency_options.reg_exp.clone(), + dependencies: data.dependencies.clone(), }; match self @@ -132,10 +178,11 @@ impl ContextModuleFactory { .call(BeforeResolveResult::Data(Box::new(before_resolve_data))) .await? { - BeforeResolveResult::Ignored => Ok(Some(ModuleFactoryResult::default())), - BeforeResolveResult::Data(d) => { - data.context = d.context.into(); - Ok(None) + BeforeResolveResult::Ignored => Ok(BeforeResolveResult::Ignored), + BeforeResolveResult::Data(mut result) => { + // The dependencies can be modified in the before resolve hook + data.dependencies = result.dependencies.take(); + Ok(BeforeResolveResult::Data(result)) } } } @@ -153,6 +200,7 @@ impl ContextModuleFactory { async fn resolve( &self, data: &mut ModuleFactoryCreateData, + before_resolve_data: Box, ) -> Result<(ModuleFactoryResult, Option)> { let plugin_driver = &self.plugin_driver; let dependency = data.dependencies[0] @@ -160,8 +208,8 @@ impl ContextModuleFactory { .expect("should be context dependency"); let mut file_dependencies = Default::default(); let mut missing_dependencies = Default::default(); - // let context_dependencies = Default::default(); - let request = dependency.request(); + + let request = before_resolve_data.request; let (loader_request, specifier) = match request.rfind('!') { Some(idx) => { let mut loaders_prefix = String::new(); @@ -181,7 +229,7 @@ impl ContextModuleFactory { } else { loaders_request.split('!').collect() }; - let resource = &request[idx + 1..]; + let resource = request[idx + 1..].to_string(); let mut loader_result = Vec::with_capacity(loaders.len()); let loader_resolver = self.get_loader_resolver(); @@ -217,10 +265,10 @@ impl ContextModuleFactory { }; let resolve_args = ResolveArgs { - context: data.context.clone(), + context: before_resolve_data.context.into(), importer: data.issuer_identifier.as_ref(), issuer: data.issuer.as_deref(), - specifier, + specifier: specifier.as_str(), dependency_type: dependency.dependency_type(), dependency_category: dependency.category(), span: dependency @@ -237,6 +285,10 @@ impl ContextModuleFactory { let (module, context_module_options) = match resource_data { Ok(ResolveResult::Resource(resource)) => { + let mut dependency_options = dependency.options().clone(); + dependency_options.recursive = before_resolve_data.recursive; + dependency_options.reg_exp = before_resolve_data.reg_exp.clone(); + let options = ContextModuleOptions { addon: loader_request.to_string(), resource: resource.path, @@ -248,8 +300,8 @@ impl ContextModuleFactory { type_prefix: dependency.type_prefix(), }; let module = Box::new(ContextModule::new( + self.resolve_dependencies.clone(), options.clone(), - plugin_driver.resolver_factory.clone(), )); (module, Some(options)) } @@ -282,14 +334,18 @@ impl ContextModuleFactory { async fn after_resolve( &self, - context_module_options: &mut ContextModuleOptions, + data: &mut ModuleFactoryCreateData, + mut context_module_options: ContextModuleOptions, ) -> Result> { let context_options = &context_module_options.context_options; let after_resolve_data = AfterResolveData { resource: context_module_options.resource.clone(), context: context_options.context.clone(), + dependencies: data.dependencies.clone(), request: context_options.request.clone(), reg_exp: context_options.reg_exp.clone(), + recursive: context_options.recursive, + resolve_dependencies: self.resolve_dependencies.clone(), }; match self @@ -300,16 +356,176 @@ impl ContextModuleFactory { .await? { AfterResolveResult::Ignored => Ok(Some(ModuleFactoryResult::default())), - AfterResolveResult::Data(d) => { - context_module_options.resource = d.resource; - context_module_options.context_options.reg_exp = d.reg_exp; + AfterResolveResult::Data(mut after_resolve_data) => { + // The dependencies can be modified in the after resolve hook + data.dependencies = after_resolve_data.dependencies.take(); + + context_module_options.resource = after_resolve_data.resource; + context_module_options.context_options.context = after_resolve_data.context; + context_module_options.context_options.reg_exp = after_resolve_data.reg_exp; + context_module_options.context_options.recursive = after_resolve_data.recursive; let module = ContextModule::new( + after_resolve_data.resolve_dependencies, context_module_options.clone(), - self.resolver_factory.clone(), ); + Ok(Some(ModuleFactoryResult::new_with_module(Box::new(module)))) } } } } + +fn visit_dirs( + ctx: &str, + dir: &Utf8Path, + dependencies: &mut Vec, + options: &ContextModuleOptions, + resolve_options: &ResolveInnerOptions, +) -> Result<()> { + if !dir.is_dir() { + return Ok(()); + } + let include = &options.context_options.include; + let exclude = &options.context_options.exclude; + for entry in fs::read_dir(dir).into_diagnostic()? { + let path = entry.into_diagnostic()?.path().assert_utf8(); + let path_str = path.as_str(); + + if let Some(exclude) = exclude + && exclude.test(path_str) + { + // ignore excluded files + continue; + } + + if path.is_dir() { + if options.context_options.recursive { + visit_dirs(ctx, &path, dependencies, options, resolve_options)?; + } + } else if path.file_name().map_or(false, |name| name.starts_with('.')) { + // ignore hidden files + continue; + } else { + if let Some(include) = include + && !include.test(path_str) + { + // ignore not included files + continue; + } + + // FIXME: nodejs resolver return path of context, sometimes is '/a/b', sometimes is '/a/b/' + let relative_path = { + let path_str = path_str.to_owned().drain(ctx.len()..).collect::(); + let p = path_str.cow_replace('\\', "/"); + if p.as_ref().starts_with('/') { + format!(".{p}") + } else { + format!("./{p}") + } + }; + + let requests = alternative_requests( + resolve_options, + vec![AlternativeRequest::new(ctx.to_string(), relative_path)], + ); + + let Some(reg_exp) = &options.context_options.reg_exp else { + return Ok(()); + }; + + requests.iter().for_each(|r| { + if !reg_exp.test(&r.request) { + return; + } + dependencies.push(ContextElementDependency { + id: DependencyId::new(), + request: format!( + "{}{}{}{}", + options.addon, + r.request, + options.resource_query.clone(), + options.resource_fragment.clone(), + ), + user_request: r.request.to_string(), + category: options.context_options.category, + context: options.resource.clone().into(), + layer: options.layer.clone(), + options: options.context_options.clone(), + resource_identifier: ContextElementDependency::create_resource_identifier( + options.resource.as_str(), + &path, + options.context_options.attributes.as_ref(), + ), + attributes: options.context_options.attributes.clone(), + referenced_exports: options.context_options.referenced_exports.clone(), + dependency_type: DependencyType::ContextElement(options.type_prefix), + }); + }) + } + } + Ok(()) +} + +#[derive(Debug, Clone)] +pub struct AlternativeRequest { + pub context: String, + pub request: String, +} + +impl AlternativeRequest { + pub fn new(context: String, request: String) -> Self { + Self { context, request } + } +} + +fn alternative_requests( + resolve_options: &ResolveInnerOptions, + mut items: Vec, +) -> Vec { + // TODO: should respect fullySpecified resolve options + for item in std::mem::take(&mut items) { + if !resolve_options.is_enforce_extension_enabled() { + items.push(item.clone()); + } + for ext in resolve_options.extensions() { + if item.request.ends_with(ext) { + items.push(AlternativeRequest::new( + item.context.clone(), + item.request[..(item.request.len() - ext.len())].to_string(), + )); + } + } + } + + for item in std::mem::take(&mut items) { + items.push(item.clone()); + for main_file in resolve_options.main_files() { + if item.request.ends_with(&format!("/{main_file}")) { + items.push(AlternativeRequest::new( + item.context.clone(), + item.request[..(item.request.len() - main_file.len())].to_string(), + )); + items.push(AlternativeRequest::new( + item.context.clone(), + item.request[..(item.request.len() - main_file.len() - 1)].to_string(), + )); + } + } + } + + for item in std::mem::take(&mut items) { + items.push(item.clone()); + for module in resolve_options.modules() { + let dir = module.cow_replace('\\', "/"); + if item.request.starts_with(&format!("./{}/", dir)) { + items.push(AlternativeRequest::new( + item.context.clone(), + item.request[dir.len() + 3..].to_string(), + )); + } + } + } + + items +} diff --git a/crates/rspack_core/src/dependency/context_dependency.rs b/crates/rspack_core/src/dependency/context_dependency.rs index 94421cf5774..f795eea52e8 100644 --- a/crates/rspack_core/src/dependency/context_dependency.rs +++ b/crates/rspack_core/src/dependency/context_dependency.rs @@ -1,3 +1,5 @@ +use rspack_error::Diagnostic; + use crate::{ContextOptions, ContextTypePrefix, Dependency}; pub trait ContextDependency: Dependency { @@ -12,6 +14,9 @@ pub trait ContextDependency: Dependency { } fn type_prefix(&self) -> ContextTypePrefix; + + fn critical(&self) -> &Option; + fn critical_mut(&mut self) -> &mut Option; } pub trait AsContextDependency { diff --git a/crates/rspack_macros/src/plugin.rs b/crates/rspack_macros/src/plugin.rs index 3b6981185d3..7aeeca3bb24 100644 --- a/crates/rspack_macros/src/plugin.rs +++ b/crates/rspack_macros/src/plugin.rs @@ -2,7 +2,7 @@ use proc_macro2::Span; use quote::quote; use syn::{ parse::{Parse, ParseStream, Parser}, - Result, Token, + Pat, PatIdent, Result, Token, }; pub fn expand_struct(mut input: syn::ItemStruct) -> proc_macro::TokenStream { @@ -155,9 +155,19 @@ pub fn expand_fn(args: HookArgs, input: syn::ItemFn) -> proc_macro::TokenStream vis, .. } = input; + let real_sig = sig.clone(); + + for arg in sig.inputs.iter_mut().skip(1) { + if let syn::FnArg::Typed(syn::PatType { pat, .. }) = arg { + if let Pat::Ident(PatIdent { mutability, .. }) = &mut **pat { + *mutability = None; + } + } + } + let mut rest_args = Vec::new(); - for arg in real_sig.inputs.iter().skip(1) { + for arg in sig.inputs.iter().skip(1) { if let syn::FnArg::Typed(syn::PatType { pat, .. }) = arg { rest_args.push(pat) } else { diff --git a/crates/rspack_plugin_context_replacement/Cargo.toml b/crates/rspack_plugin_context_replacement/Cargo.toml new file mode 100644 index 00000000000..6b110d63ede --- /dev/null +++ b/crates/rspack_plugin_context_replacement/Cargo.toml @@ -0,0 +1,20 @@ +[package] +description = "rspack context replacement plugin" +edition = "2021" +license = "MIT" +name = "rspack_plugin_context_replacement" +repository = "https://github.com/web-infra-dev/rspack" +version = "0.1.0" + +[dependencies] +derivative = { workspace = true } +rspack_core = { version = "0.1.0", path = "../rspack_core" } +rspack_error = { version = "0.1.0", path = "../rspack_error" } +rspack_hook = { version = "0.1.0", path = "../rspack_hook" } +rspack_paths = { version = "0.1.0", path = "../rspack_paths" } +rspack_regex = { version = "0.1.0", path = "../rspack_regex" } +rustc-hash = { workspace = true } +tracing = { workspace = true } + +[package.metadata.cargo-shear] +ignored = ["tracing"] diff --git a/crates/rspack_plugin_context_replacement/src/lib.rs b/crates/rspack_plugin_context_replacement/src/lib.rs new file mode 100644 index 00000000000..8e781765f2f --- /dev/null +++ b/crates/rspack_plugin_context_replacement/src/lib.rs @@ -0,0 +1,162 @@ +use std::sync::Arc; + +use derivative::Derivative; +use rspack_core::{ + AfterResolveResult, ApplyContext, BeforeResolveResult, CompilerOptions, ContextElementDependency, + ContextModuleFactoryAfterResolve, ContextModuleFactoryBeforeResolve, DependencyId, + DependencyType, Plugin, PluginContext, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; +use rspack_paths::Utf8PathBuf; +use rspack_regex::RspackRegex; +use rustc_hash::FxHashMap as HashMap; + +pub struct ContextReplacementPluginOptions { + pub resource_reg_exp: RspackRegex, + pub new_content_resource: Option, + pub new_content_recursive: Option, + pub new_content_reg_exp: Option, + pub new_content_create_context_map: Option>, +} + +#[plugin] +#[derive(Derivative)] +#[derivative(Debug)] +pub struct ContextReplacementPlugin { + resource_reg_exp: RspackRegex, + new_content_resource: Option, + new_content_recursive: Option, + new_content_reg_exp: Option, + new_content_create_context_map: Option>, +} + +impl ContextReplacementPlugin { + pub fn new(options: ContextReplacementPluginOptions) -> Self { + Self::new_inner( + options.resource_reg_exp, + options.new_content_resource, + options.new_content_recursive, + options.new_content_reg_exp, + options.new_content_create_context_map, + ) + } +} + +#[plugin_hook(ContextModuleFactoryBeforeResolve for ContextReplacementPlugin)] +async fn cmf_before_resolve(&self, mut result: BeforeResolveResult) -> Result { + if let BeforeResolveResult::Data(data) = &mut result { + if self.resource_reg_exp.test(&data.request) { + if let Some(new_content_resource) = &self.new_content_resource { + data.request = new_content_resource.clone(); + } + } + if let Some(new_content_recursive) = self.new_content_recursive { + data.recursive = new_content_recursive; + } + if let Some(new_content_reg_exp) = &self.new_content_reg_exp { + data.reg_exp = Some(new_content_reg_exp.clone()); + } + // if let Some(new_content_callback) = &self.new_content_after_resolve_callback { + // new_content_callback(&mut result).await?; + // } else { + for d in &mut data.dependencies { + if let Some(d) = d.as_context_dependency_mut() { + *d.critical_mut() = None; + } + } + // } + } + + Ok(result) +} + +#[plugin_hook(ContextModuleFactoryAfterResolve for ContextReplacementPlugin)] +async fn cmf_after_resolve(&self, mut result: AfterResolveResult) -> Result { + if let AfterResolveResult::Data(data) = &mut result { + if self.resource_reg_exp.test(data.resource.as_str()) { + if let Some(new_content_resource) = &self.new_content_resource { + if new_content_resource.starts_with('/') || new_content_resource.chars().nth(1) == Some(':') + { + data.resource = new_content_resource.clone().into(); + } else { + data.resource = data.resource.join(Utf8PathBuf::from(new_content_resource)); + } + } + if let Some(new_content_recursive) = self.new_content_recursive { + data.recursive = new_content_recursive; + } + if let Some(new_content_reg_exp) = &self.new_content_reg_exp { + data.reg_exp = Some(new_content_reg_exp.clone()); + } + if let Some(new_content_create_context_map) = &self.new_content_create_context_map { + let new_content_create_context_map = new_content_create_context_map.clone(); + data.resolve_dependencies = Arc::new(move |options| { + let deps = new_content_create_context_map + .iter() + .map(|(key, value)| { + let resource_identifier = ContextElementDependency::create_resource_identifier( + options.resource.as_str(), + value.as_str().into(), + options.context_options.attributes.as_ref(), + ); + ContextElementDependency { + id: DependencyId::new(), + request: format!( + "{}{}{}", + value, + options.resource_query.clone(), + options.resource_fragment.clone(), + ), + user_request: key.to_string(), + category: options.context_options.category, + context: options.resource.clone().into(), + layer: options.layer.clone(), + options: options.context_options.clone(), + resource_identifier, + attributes: options.context_options.attributes.clone(), + referenced_exports: options.context_options.referenced_exports.clone(), + dependency_type: DependencyType::ContextElement(options.type_prefix), + } + }) + .collect::>(); + Ok(deps) + }); + } + // if let Some(new_content_callback) = &self.new_content_callback { + // new_content_callback(&mut result).await?; + // } else { + for d in &mut data.dependencies { + if let Some(d) = d.as_context_dependency_mut() { + *d.critical_mut() = None; + } + } + // } + } + } + Ok(result) +} + +impl Plugin for ContextReplacementPlugin { + fn name(&self) -> &'static str { + "rspack.ContextReplacementPlugin" + } + + fn apply( + &self, + ctx: PluginContext<&mut ApplyContext>, + _options: &mut CompilerOptions, + ) -> Result<()> { + ctx + .context + .context_module_factory_hooks + .before_resolve + .tap(cmf_before_resolve::new(self)); + ctx + .context + .context_module_factory_hooks + .after_resolve + .tap(cmf_after_resolve::new(self)); + Ok(()) + } +} diff --git a/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs b/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs index 40808177134..e421b1be3b7 100644 --- a/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs +++ b/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs @@ -38,7 +38,7 @@ pub enum ModuleFilenameTemplate { Fn(ModuleFilenameTemplateFn), } -type AppendFn = Box Fn(PathData) -> BoxFuture<'static, Result> + Sync + Send>; +type AppendFn = Box BoxFuture<'static, Result> + Sync + Send>; pub enum Append { String(String), diff --git a/crates/rspack_plugin_ignore/src/lib.rs b/crates/rspack_plugin_ignore/src/lib.rs index 4b2fe3461f4..3f31eec1997 100644 --- a/crates/rspack_plugin_ignore/src/lib.rs +++ b/crates/rspack_plugin_ignore/src/lib.rs @@ -79,7 +79,7 @@ async fn cmf_before_resolve(&self, data: BeforeResolveResult) -> Result Ok(BeforeResolveResult::Ignored), BeforeResolveResult::Data(d) => { - if let Some(false) = self.check_ignore(d.request.as_deref(), &d.context).await { + if let Some(false) = self.check_ignore(Some(&d.request), &d.context).await { Ok(BeforeResolveResult::Ignored) } else { Ok(BeforeResolveResult::Data(d)) diff --git a/crates/rspack_plugin_javascript/src/dependency/context/common_js_require_context_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/context/common_js_require_context_dependency.rs index 35fb428a146..61b87182bb7 100644 --- a/crates/rspack_plugin_javascript/src/dependency/context/common_js_require_context_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/context/common_js_require_context_dependency.rs @@ -1,9 +1,9 @@ use rspack_core::{ - AsModuleDependency, Compilation, ContextDependency, RealDependencyLocation, RuntimeSpec, + AsModuleDependency, Compilation, ContextDependency, ContextOptions, Dependency, + DependencyCategory, DependencyId, DependencyTemplate, DependencyType, ModuleGraph, + RealDependencyLocation, RuntimeSpec, TemplateContext, TemplateReplaceSource, }; -use rspack_core::{ContextOptions, Dependency, TemplateReplaceSource}; -use rspack_core::{DependencyCategory, DependencyId, DependencyTemplate}; -use rspack_core::{DependencyType, TemplateContext}; +use rspack_error::Diagnostic; use super::{ context_dependency_template_as_require_call, create_resource_identifier_for_context_dependency, @@ -17,6 +17,7 @@ pub struct CommonJsRequireContextDependency { resource_identifier: String, options: ContextOptions, optional: bool, + critical: Option, } impl CommonJsRequireContextDependency { @@ -34,6 +35,7 @@ impl CommonJsRequireContextDependency { resource_identifier, optional, id: DependencyId::new(), + critical: None, } } } @@ -58,6 +60,13 @@ impl Dependency for CommonJsRequireContextDependency { fn could_affect_referencing_module(&self) -> rspack_core::AffectType { rspack_core::AffectType::True } + + fn get_diagnostics(&self, _module_graph: &ModuleGraph) -> Option> { + if let Some(critical) = self.critical() { + return Some(vec![critical.clone()]); + } + None + } } impl ContextDependency for CommonJsRequireContextDependency { @@ -88,6 +97,14 @@ impl ContextDependency for CommonJsRequireContextDependency { fn type_prefix(&self) -> rspack_core::ContextTypePrefix { rspack_core::ContextTypePrefix::Normal } + + fn critical(&self) -> &Option { + &self.critical + } + + fn critical_mut(&mut self) -> &mut Option { + &mut self.critical + } } impl DependencyTemplate for CommonJsRequireContextDependency { diff --git a/crates/rspack_plugin_javascript/src/dependency/context/import_context_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/context/import_context_dependency.rs index dbd36ab9097..7342bfa10f1 100644 --- a/crates/rspack_plugin_javascript/src/dependency/context/import_context_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/context/import_context_dependency.rs @@ -1,9 +1,9 @@ use rspack_core::{ - AsModuleDependency, Compilation, ContextDependency, RealDependencyLocation, RuntimeSpec, + AsModuleDependency, Compilation, ContextDependency, ContextOptions, Dependency, + DependencyCategory, DependencyId, DependencyTemplate, DependencyType, ModuleGraph, + RealDependencyLocation, RuntimeSpec, TemplateContext, TemplateReplaceSource, }; -use rspack_core::{ContextOptions, Dependency, TemplateReplaceSource}; -use rspack_core::{DependencyCategory, DependencyId, DependencyTemplate}; -use rspack_core::{DependencyType, TemplateContext}; +use rspack_error::Diagnostic; use super::{ context_dependency_template_as_require_call, create_resource_identifier_for_context_dependency, @@ -17,6 +17,7 @@ pub struct ImportContextDependency { range_callee: (u32, u32), resource_identifier: String, optional: bool, + critical: Option, } impl ImportContextDependency { @@ -34,6 +35,7 @@ impl ImportContextDependency { id: DependencyId::new(), resource_identifier, optional, + critical: None, } } } @@ -58,6 +60,13 @@ impl Dependency for ImportContextDependency { fn could_affect_referencing_module(&self) -> rspack_core::AffectType { rspack_core::AffectType::True } + + fn get_diagnostics(&self, _module_graph: &ModuleGraph) -> Option> { + if let Some(critical) = self.critical() { + return Some(vec![critical.clone()]); + } + None + } } impl ContextDependency for ImportContextDependency { @@ -88,6 +97,14 @@ impl ContextDependency for ImportContextDependency { fn type_prefix(&self) -> rspack_core::ContextTypePrefix { rspack_core::ContextTypePrefix::Import } + + fn critical(&self) -> &Option { + &self.critical + } + + fn critical_mut(&mut self) -> &mut Option { + &mut self.critical + } } impl DependencyTemplate for ImportContextDependency { diff --git a/crates/rspack_plugin_javascript/src/dependency/context/import_meta_context_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/context/import_meta_context_dependency.rs index 77c87089959..a7c5a9bad85 100644 --- a/crates/rspack_plugin_javascript/src/dependency/context/import_meta_context_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/context/import_meta_context_dependency.rs @@ -1,10 +1,9 @@ use rspack_core::{ - module_raw, AsModuleDependency, Compilation, ContextDependency, RealDependencyLocation, - RuntimeSpec, + module_raw, AsModuleDependency, Compilation, ContextDependency, ContextOptions, Dependency, + DependencyCategory, DependencyId, DependencyTemplate, DependencyType, ModuleGraph, + RealDependencyLocation, RuntimeSpec, TemplateContext, TemplateReplaceSource, }; -use rspack_core::{ContextOptions, Dependency, DependencyCategory, DependencyId}; -use rspack_core::{DependencyTemplate, DependencyType}; -use rspack_core::{TemplateContext, TemplateReplaceSource}; +use rspack_error::Diagnostic; use super::create_resource_identifier_for_context_dependency; @@ -15,6 +14,7 @@ pub struct ImportMetaContextDependency { range: RealDependencyLocation, resource_identifier: String, optional: bool, + critical: Option, } impl ImportMetaContextDependency { @@ -26,6 +26,7 @@ impl ImportMetaContextDependency { resource_identifier, optional, id: DependencyId::new(), + critical: None, } } } @@ -50,6 +51,13 @@ impl Dependency for ImportMetaContextDependency { fn could_affect_referencing_module(&self) -> rspack_core::AffectType { rspack_core::AffectType::True } + + fn get_diagnostics(&self, _module_graph: &ModuleGraph) -> Option> { + if let Some(critical) = self.critical() { + return Some(vec![critical.clone()]); + } + None + } } impl ContextDependency for ImportMetaContextDependency { @@ -80,6 +88,14 @@ impl ContextDependency for ImportMetaContextDependency { fn type_prefix(&self) -> rspack_core::ContextTypePrefix { rspack_core::ContextTypePrefix::Normal } + + fn critical(&self) -> &Option { + &self.critical + } + + fn critical_mut(&mut self) -> &mut Option { + &mut self.critical + } } impl DependencyTemplate for ImportMetaContextDependency { diff --git a/crates/rspack_plugin_javascript/src/dependency/context/require_context_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/context/require_context_dependency.rs index bbfa8d557a9..5d0041e1237 100644 --- a/crates/rspack_plugin_javascript/src/dependency/context/require_context_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/context/require_context_dependency.rs @@ -1,10 +1,9 @@ use rspack_core::{ - module_raw, AsModuleDependency, Compilation, ContextDependency, RealDependencyLocation, - RuntimeSpec, + module_raw, AsModuleDependency, Compilation, ContextDependency, ContextOptions, Dependency, + DependencyCategory, DependencyId, DependencyTemplate, DependencyType, ModuleGraph, + RealDependencyLocation, RuntimeSpec, TemplateContext, TemplateReplaceSource, }; -use rspack_core::{ContextOptions, Dependency, DependencyCategory, DependencyId}; -use rspack_core::{DependencyTemplate, DependencyType}; -use rspack_core::{TemplateContext, TemplateReplaceSource}; +use rspack_error::Diagnostic; use super::create_resource_identifier_for_context_dependency; @@ -15,6 +14,7 @@ pub struct RequireContextDependency { range: RealDependencyLocation, resource_identifier: String, optional: bool, + critical: Option, } impl RequireContextDependency { @@ -26,6 +26,7 @@ impl RequireContextDependency { id: DependencyId::new(), resource_identifier, optional, + critical: None, } } } @@ -50,6 +51,13 @@ impl Dependency for RequireContextDependency { fn could_affect_referencing_module(&self) -> rspack_core::AffectType { rspack_core::AffectType::True } + + fn get_diagnostics(&self, _module_graph: &ModuleGraph) -> Option> { + if let Some(critical) = self.critical() { + return Some(vec![critical.clone()]); + } + None + } } impl ContextDependency for RequireContextDependency { @@ -80,6 +88,14 @@ impl ContextDependency for RequireContextDependency { fn type_prefix(&self) -> rspack_core::ContextTypePrefix { rspack_core::ContextTypePrefix::Normal } + + fn critical(&self) -> &Option { + &self.critical + } + + fn critical_mut(&mut self) -> &mut Option { + &mut self.critical + } } impl DependencyTemplate for RequireContextDependency { diff --git a/crates/rspack_plugin_javascript/src/dependency/context/require_resolve_context_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/context/require_resolve_context_dependency.rs index 1dbbbfb1b3c..067bbf8429e 100644 --- a/crates/rspack_plugin_javascript/src/dependency/context/require_resolve_context_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/context/require_resolve_context_dependency.rs @@ -3,6 +3,7 @@ use rspack_core::{ ContextTypePrefix, Dependency, DependencyCategory, DependencyId, DependencyTemplate, DependencyType, RealDependencyLocation, RuntimeSpec, TemplateContext, TemplateReplaceSource, }; +use rspack_error::Diagnostic; use super::{context_dependency_template_as_id, create_resource_identifier_for_context_dependency}; @@ -13,6 +14,7 @@ pub struct RequireResolveContextDependency { range: RealDependencyLocation, resource_identifier: String, optional: bool, + critical: Option, } impl RequireResolveContextDependency { @@ -24,6 +26,7 @@ impl RequireResolveContextDependency { range, resource_identifier, optional, + critical: None, } } } @@ -78,6 +81,14 @@ impl ContextDependency for RequireResolveContextDependency { fn type_prefix(&self) -> ContextTypePrefix { ContextTypePrefix::Normal } + + fn critical(&self) -> &Option { + &self.critical + } + + fn critical_mut(&mut self) -> &mut Option { + &mut self.critical + } } impl DependencyTemplate for RequireResolveContextDependency { diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/common_js_imports_parse_plugin.rs b/crates/rspack_plugin_javascript/src/parser_plugin/common_js_imports_parse_plugin.rs index 308a41e9d73..82f2d87df60 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/common_js_imports_parse_plugin.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/common_js_imports_parse_plugin.rs @@ -1,8 +1,9 @@ use rspack_core::{ - ConstDependency, ContextMode, DependencyCategory, RealDependencyLocation, SpanExt, + ConstDependency, ContextDependency, ContextMode, DependencyCategory, RealDependencyLocation, + SpanExt, }; use rspack_core::{ContextNameSpaceObject, ContextOptions}; -use rspack_error::Severity; +use rspack_error::{DiagnosticExt, Severity}; use swc_core::common::{Span, Spanned}; use swc_core::ecma::ast::{CallExpr, Expr, Ident, MemberExpr, UnaryExpr}; @@ -46,7 +47,10 @@ fn create_commonjs_require_context_dependency( referenced_exports: None, attributes: None, }; - CommonJsRequireContextDependency::new(options, span.into(), (start, end), parser.in_try) + let mut dep = + CommonJsRequireContextDependency::new(options, span.into(), (start, end), parser.in_try); + *dep.critical_mut() = result.critical; + dep } fn create_require_resolve_context_dependency( @@ -288,7 +292,7 @@ impl CommonJsImportsParserPlugin { ) -> Option { let start = ident.span().real_lo(); let end = ident.span().real_hi(); - let dep = CommonJsRequireContextDependency::new( + let mut dep = CommonJsRequireContextDependency::new( ContextOptions { mode: ContextMode::Sync, recursive: true, @@ -310,7 +314,7 @@ impl CommonJsImportsParserPlugin { (start, end), parser.in_try, ); - parser.warning_diagnostics.push(Box::new( + *dep.critical_mut() = Some( create_traceable_error( "Critical dependency".into(), "require function is used in a way in which dependencies cannot be statically extracted" @@ -318,8 +322,10 @@ impl CommonJsImportsParserPlugin { parser.source_file, ident.span().into(), ) - .with_severity(Severity::Warn), - )); + .with_severity(Severity::Warn) + .boxed() + .into(), + ); parser.dependencies.push(Box::new(dep)); Some(true) } diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs b/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs index f1e640d8c42..459c5e54ac5 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use rspack_core::{ - AsyncDependenciesBlock, DependencyLocation, DynamicImportMode, GroupOptions, ImportAttributes, - RealDependencyLocation, + AsyncDependenciesBlock, ContextDependency, DependencyLocation, DynamicImportMode, GroupOptions, + ImportAttributes, RealDependencyLocation, }; use rspack_core::{ChunkGroupOptions, DynamicImportFetchPriority}; use rspack_core::{ContextNameSpaceObject, ContextOptions, DependencyCategory, SpanExt}; @@ -147,41 +147,42 @@ impl JavascriptParserPlugin for ImportParserPlugin { query, fragment, replaces, + critical, } = create_context_dependency(¶m, &dyn_imported.expr, parser); let reg_exp = context_reg_exp(®, "", Some(dyn_imported.span().into()), parser); - parser - .dependencies - .push(Box::new(ImportContextDependency::new( - ContextOptions { - mode: mode.into(), - recursive: true, - reg_exp, - include, - exclude, - category: DependencyCategory::Esm, - request: format!("{}{}{}", context.clone(), query, fragment), - context, - namespace_object: if parser.build_meta.strict_harmony_module { - ContextNameSpaceObject::Strict - } else { - ContextNameSpaceObject::Bool(true) - }, - group_options: Some(GroupOptions::ChunkGroup(ChunkGroupOptions::new( - chunk_name, - chunk_preload, - chunk_prefetch, - fetch_priority, - ))), - replaces, - start: node.span().real_lo(), - end: node.span().real_hi(), - referenced_exports: exports, - attributes, + let mut dep = ImportContextDependency::new( + ContextOptions { + mode: mode.into(), + recursive: true, + reg_exp, + include, + exclude, + category: DependencyCategory::Esm, + request: format!("{}{}{}", context.clone(), query, fragment), + context, + namespace_object: if parser.build_meta.strict_harmony_module { + ContextNameSpaceObject::Strict + } else { + ContextNameSpaceObject::Bool(true) }, - node.span().into(), - (import_call.span.real_lo(), import_call.span.real_hi()), - parser.in_try, - ))); + group_options: Some(GroupOptions::ChunkGroup(ChunkGroupOptions::new( + chunk_name, + chunk_preload, + chunk_prefetch, + fetch_priority, + ))), + replaces, + start: node.span().real_lo(), + end: node.span().real_hi(), + referenced_exports: exports, + attributes, + }, + node.span().into(), + (import_call.span.real_lo(), import_call.span.real_hi()), + parser.in_try, + ); + *dep.critical_mut() = critical; + parser.dependencies.push(Box::new(dep)); Some(true) } } diff --git a/crates/rspack_plugin_javascript/src/visitors/dependency/context_dependency_helper.rs b/crates/rspack_plugin_javascript/src/visitors/dependency/context_dependency_helper.rs index 4c3966643c4..a9f14114dcc 100644 --- a/crates/rspack_plugin_javascript/src/visitors/dependency/context_dependency_helper.rs +++ b/crates/rspack_plugin_javascript/src/visitors/dependency/context_dependency_helper.rs @@ -4,7 +4,7 @@ use std::sync::LazyLock; use itertools::Itertools; use regex::Regex; use rspack_core::parse_resource; -use rspack_error::Severity; +use rspack_error::{Diagnostic, DiagnosticExt, Severity}; use rspack_util::json_stringify; use swc_core::ecma::ast::Expr; @@ -19,6 +19,8 @@ pub fn create_context_dependency( expr: &Expr, parser: &mut crate::visitors::JavascriptParser, ) -> ContextModuleScanResult { + let mut critical = None; + if param.is_template_string() { let quasis = param.quasis(); let Some(prefix) = quasis.first() else { @@ -84,15 +86,17 @@ pub fn create_context_dependency( if parser.javascript_options.wrapped_context_critical { let range = param.range(); - parser.warning_diagnostics.push(Box::new( - create_traceable_error( - "Critical dependency".into(), - "a part of the request of a dependency is an expression".to_string(), - parser.source_file, - rspack_core::ErrorSpan::new(range.0, range.1), - ) - .with_severity(Severity::Warn), - )); + let warn: Diagnostic = create_traceable_error( + "Critical dependency".into(), + "a part of the request of a dependency is an expression".to_string(), + parser.source_file, + rspack_core::ErrorSpan::new(range.0, range.1), + ) + .with_severity(Severity::Warn) + .boxed() + .into(); + let warn = warn.with_module_identifier(Some(*parser.module_identifier)); + critical = Some(warn); } // Webpack will walk only the expression parts of the template string @@ -107,6 +111,7 @@ pub fn create_context_dependency( query, fragment, replaces, + critical, } } else if param.is_wrapped() && let prefix_is_string = param @@ -158,15 +163,17 @@ pub fn create_context_dependency( if parser.javascript_options.wrapped_context_critical { let range = param.range(); - parser.warning_diagnostics.push(Box::new( - create_traceable_error( - "Critical dependency".into(), - "a part of the request of a dependency is an expression".to_string(), - parser.source_file, - rspack_core::ErrorSpan::new(range.0, range.1), - ) - .with_severity(Severity::Warn), - )); + let warn: Diagnostic = create_traceable_error( + "Critical dependency".into(), + "a part of the request of a dependency is an expression".to_string(), + parser.source_file, + rspack_core::ErrorSpan::new(range.0, range.1), + ) + .with_severity(Severity::Warn) + .boxed() + .into(); + let warn = warn.with_module_identifier(Some(*parser.module_identifier)); + critical = Some(warn); } // Webpack will walk only the dynamic parts of evaluated expression @@ -181,19 +188,22 @@ pub fn create_context_dependency( query, fragment, replaces, + critical, } } else { if parser.javascript_options.expr_context_critical { let range = param.range(); - parser.warning_diagnostics.push(Box::new( - create_traceable_error( - "Critical dependency".into(), - "the request of a dependency is an expression".to_string(), - parser.source_file, - rspack_core::ErrorSpan::new(range.0, range.1), - ) - .with_severity(Severity::Warn), - )); + let warn: Diagnostic = create_traceable_error( + "Critical dependency".into(), + "the request of a dependency is an expression".to_string(), + parser.source_file, + rspack_core::ErrorSpan::new(range.0, range.1), + ) + .with_severity(Severity::Warn) + .boxed() + .into(); + let warn = warn.with_module_identifier(Some(*parser.module_identifier)); + critical = Some(warn); } parser.walk_expression(expr); @@ -204,6 +214,7 @@ pub fn create_context_dependency( query: String::new(), fragment: String::new(), replaces: Vec::new(), + critical, } } } @@ -214,6 +225,7 @@ pub struct ContextModuleScanResult { pub query: String, pub fragment: String, pub replaces: Vec<(String, u32, u32)>, + pub critical: Option, } pub(super) fn split_context_from_prefix(prefix: String) -> (String, String) { diff --git a/packages/rspack/etc/api.md b/packages/rspack/etc/api.md index 2f561ffacfc..6126a6f8738 100644 --- a/packages/rspack/etc/api.md +++ b/packages/rspack/etc/api.md @@ -38,6 +38,7 @@ import { JsChunkGroup } from '@rspack/binding'; import { JsChunkGroupOrigin } from '@rspack/binding'; import type { JsCodegenerationResult } from '@rspack/binding'; import { JsCompilation } from '@rspack/binding'; +import type { JsContextModuleFactoryAfterResolveData } from '@rspack/binding'; import type { JsCreateData } from '@rspack/binding'; import type { JsFactoryMeta } from '@rspack/binding'; import { JsHtmlPluginTag } from '@rspack/binding'; @@ -1560,13 +1561,33 @@ class ContextModuleFactory { } // @public (undocumented) -type ContextModuleFactoryAfterResolveResult = false | { - resource: string; - context: string; - request: string; - regExp?: RegExp; - dependencies: Array; -}; +class ContextModuleFactoryAfterResolveData { + constructor(data: JsContextModuleFactoryAfterResolveData); + // (undocumented) + static __from_binding(binding: JsContextModuleFactoryAfterResolveData): ContextModuleFactoryAfterResolveData; + // (undocumented) + static __to_binding(data: ContextModuleFactoryAfterResolveData): JsContextModuleFactoryAfterResolveData; + // (undocumented) + get context(): string; + set context(val: string); + // (undocumented) + get dependencies(): Dependency[]; + // (undocumented) + get recursive(): boolean; + set recursive(val: boolean); + // (undocumented) + get regExp(): RegExp | undefined; + set regExp(val: RegExp | undefined); + // (undocumented) + get request(): string; + set request(val: string); + // (undocumented) + get resource(): string; + set resource(val: string); +} + +// @public (undocumented) +type ContextModuleFactoryAfterResolveResult = false | ContextModuleFactoryAfterResolveData; // @public (undocumented) type ContextModuleFactoryBeforeResolveResult = false | { @@ -1574,6 +1595,17 @@ type ContextModuleFactoryBeforeResolveResult = false | { request?: string; }; +// @public (undocumented) +export const ContextReplacementPlugin: { + new (resourceRegExp: RegExp, newContentResource?: any, newContentRecursive?: any, newContentRegExp?: any): { + name: BuiltinPluginName; + _args: [resourceRegExp: RegExp, newContentResource?: any, newContentRecursive?: any, newContentRegExp?: any]; + affectedHooks: "done" | "make" | "compile" | "emit" | "afterEmit" | "invalid" | "thisCompilation" | "afterDone" | "compilation" | "normalModuleFactory" | "contextModuleFactory" | "initialize" | "shouldEmit" | "infrastructureLog" | "beforeRun" | "run" | "assetEmitted" | "failed" | "shutdown" | "watchRun" | "watchClose" | "environment" | "afterEnvironment" | "afterPlugins" | "afterResolvers" | "beforeCompile" | "afterCompile" | "finishMake" | "entryOption" | undefined; + raw(compiler: Compiler_2): BuiltinPlugin; + apply(compiler: Compiler_2): void; + }; +}; + // @public (undocumented) export const CopyRspackPlugin: { new (copy: CopyRspackPluginOptions): { @@ -10070,6 +10102,7 @@ declare namespace rspackExports { EvalSourceMapDevToolPlugin, EvalDevToolModulePlugin, CssExtractRspackPlugin, + ContextReplacementPlugin, SwcLoaderEnvConfig, SwcLoaderEsParserConfig, SwcLoaderJscConfig, @@ -11572,6 +11605,7 @@ export const rspackOptions: z.ZodObject<{ errorsSpace: z.ZodOptional; warningsSpace: z.ZodOptional; }, "strict", z.ZodTypeAny, { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -11598,7 +11632,6 @@ export const rspackOptions: z.ZodObject<{ builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; @@ -11649,6 +11682,7 @@ export const rspackOptions: z.ZodObject<{ errorsSpace?: number | undefined; warningsSpace?: number | undefined; }, { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -11675,7 +11709,6 @@ export const rspackOptions: z.ZodObject<{ builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; @@ -13395,6 +13428,7 @@ export const rspackOptions: z.ZodObject<{ } | undefined; watch?: boolean | undefined; stats?: boolean | "verbose" | "normal" | "none" | "errors-only" | "errors-warnings" | "minimal" | "detailed" | "summary" | { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -13421,7 +13455,6 @@ export const rspackOptions: z.ZodObject<{ builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; @@ -13969,6 +14002,7 @@ export const rspackOptions: z.ZodObject<{ } | undefined; watch?: boolean | undefined; stats?: boolean | "verbose" | "normal" | "none" | "errors-only" | "errors-warnings" | "minimal" | "detailed" | "summary" | { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -13995,7 +14029,6 @@ export const rspackOptions: z.ZodObject<{ builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; @@ -14783,6 +14816,7 @@ const statsOptions: z.ZodObject<{ errorsSpace: z.ZodOptional; warningsSpace: z.ZodOptional; }, "strict", z.ZodTypeAny, { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -14809,7 +14843,6 @@ const statsOptions: z.ZodObject<{ builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; @@ -14860,6 +14893,7 @@ const statsOptions: z.ZodObject<{ errorsSpace?: number | undefined; warningsSpace?: number | undefined; }, { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -14886,7 +14920,6 @@ const statsOptions: z.ZodObject<{ builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; @@ -15050,6 +15083,7 @@ const statsValue: z.ZodUnion<[z.ZodUnion<[z.ZodBoolean, z.ZodEnum<["normal", "no errorsSpace: z.ZodOptional; warningsSpace: z.ZodOptional; }, "strict", z.ZodTypeAny, { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -15076,7 +15110,6 @@ const statsValue: z.ZodUnion<[z.ZodUnion<[z.ZodBoolean, z.ZodEnum<["normal", "no builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; @@ -15127,6 +15160,7 @@ const statsValue: z.ZodUnion<[z.ZodUnion<[z.ZodBoolean, z.ZodEnum<["normal", "no errorsSpace?: number | undefined; warningsSpace?: number | undefined; }, { + source?: boolean | undefined; publicPath?: boolean | undefined; hash?: boolean | undefined; all?: boolean | undefined; @@ -15153,7 +15187,6 @@ const statsValue: z.ZodUnion<[z.ZodUnion<[z.ZodBoolean, z.ZodEnum<["normal", "no builtAt?: boolean | undefined; moduleAssets?: boolean | undefined; nestedModules?: boolean | undefined; - source?: boolean | undefined; logging?: boolean | "log" | "info" | "verbose" | "none" | "error" | "warn" | undefined; loggingDebug?: string | boolean | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean) | (string | RegExp | ((args_0: string, ...args_1: unknown[]) => boolean))[] | undefined; loggingTrace?: boolean | undefined; diff --git a/packages/rspack/src/Compiler.ts b/packages/rspack/src/Compiler.ts index adf5e8899ed..ae3fb7f4f47 100644 --- a/packages/rspack/src/Compiler.ts +++ b/packages/rspack/src/Compiler.ts @@ -30,7 +30,11 @@ import { Chunk } from "./Chunk"; import { Compilation } from "./Compilation"; import { ContextModuleFactory } from "./ContextModuleFactory"; import { ThreadsafeWritableNodeFS } from "./FileSystem"; -import { CodeGenerationResult, Module } from "./Module"; +import { + CodeGenerationResult, + ContextModuleFactoryAfterResolveData, + Module +} from "./Module"; import { NormalModuleFactory } from "./NormalModuleFactory"; import { ResolverFactory } from "./ResolverFactory"; import { RuleSetCompiler } from "./RuleSetCompiler"; @@ -53,10 +57,7 @@ import type Watchpack from "watchpack"; import type { Source } from "webpack-sources"; import type { CompilationParams } from "./Compilation"; import type { FileSystemInfoEntry } from "./FileSystemInfo"; -import type { - ContextModuleFactoryAfterResolveResult, - ResolveData -} from "./Module"; +import type { ResolveData } from "./Module"; import type { NormalModuleCreateData } from "./NormalModuleFactory"; import type { EntryNormalized, @@ -1158,33 +1159,13 @@ class Compiler { | binding.JsContextModuleFactoryAfterResolveData ) => { const data = bindingData - ? ({ - resource: bindingData.resource, - regExp: bindingData.regExp - ? new RegExp( - bindingData.regExp.source, - bindingData.regExp.flags - ) - : undefined, - request: bindingData.request, - context: bindingData.context, - // TODO: Dependencies are not fully supported yet; this is a placeholder to prevent errors in moment-locales-webpack-plugin. - dependencies: [] - } satisfies ContextModuleFactoryAfterResolveResult) + ? ContextModuleFactoryAfterResolveData.__from_binding( + bindingData + ) : false; const ret = await queried.promise(data); const result = ret - ? ({ - resource: ret.resource, - context: ret.context, - request: ret.request, - regExp: ret.regExp - ? { - source: ret.regExp.source, - flags: ret.regExp.flags - } - : undefined - } satisfies binding.JsContextModuleFactoryAfterResolveData) + ? ContextModuleFactoryAfterResolveData.__to_binding(ret) : false; return result; } diff --git a/packages/rspack/src/Module.ts b/packages/rspack/src/Module.ts index 5c474a7c9f4..a83e3dbf2ec 100644 --- a/packages/rspack/src/Module.ts +++ b/packages/rspack/src/Module.ts @@ -1,5 +1,6 @@ import type { JsCodegenerationResult, + JsContextModuleFactoryAfterResolveData, JsCreateData, JsFactoryMeta, JsModule, @@ -9,6 +10,7 @@ import type { Source } from "webpack-sources"; import type { Compilation } from "./Compilation"; import { DependenciesBlock } from "./DependenciesBlock"; +import type { Dependency } from "./Dependency"; import { JsSource } from "./util/source"; export type ResourceData = { @@ -41,15 +43,81 @@ export type ContextModuleFactoryBeforeResolveResult = request?: string; }; +export class ContextModuleFactoryAfterResolveData { + #inner: JsContextModuleFactoryAfterResolveData; + + static __from_binding(binding: JsContextModuleFactoryAfterResolveData) { + return new ContextModuleFactoryAfterResolveData(binding); + } + + static __to_binding(data: ContextModuleFactoryAfterResolveData) { + return data.#inner; + } + + constructor(data: JsContextModuleFactoryAfterResolveData) { + this.#inner = data; + } + + get resource(): string { + return this.#inner.resource; + } + + set resource(val: string) { + this.#inner.resource = val; + } + + get context(): string { + return this.#inner.context; + } + + set context(val: string) { + this.#inner.context = val; + } + + get request(): string { + return this.#inner.request; + } + + set request(val: string) { + this.#inner.request = val; + } + + get regExp(): RegExp | undefined { + if (!this.#inner.regExp) { + return undefined; + } + const { source, flags } = this.#inner.regExp; + return new RegExp(source, flags); + } + + set regExp(val: RegExp | undefined) { + if (!val) { + this.#inner.regExp = undefined; + return; + } + this.#inner.regExp = { + source: val.source, + flags: val.flags + }; + } + + get recursive(): boolean { + return this.#inner.recursive; + } + + set recursive(val: boolean) { + this.#inner.recursive = val; + } + + get dependencies(): Dependency[] { + // TODO: Dependencies are not fully supported yet; this is a placeholder to prevent errors in moment-locales-webpack-plugin. + return []; + } +} + export type ContextModuleFactoryAfterResolveResult = | false - | { - resource: string; - context: string; - request: string; - regExp?: RegExp; - dependencies: Array; - }; + | ContextModuleFactoryAfterResolveData; export class Module { #inner: JsModule | ModuleDTO; diff --git a/packages/rspack/src/builtin-plugin/ContextReplacementPlugin.ts b/packages/rspack/src/builtin-plugin/ContextReplacementPlugin.ts new file mode 100644 index 00000000000..110a624ed32 --- /dev/null +++ b/packages/rspack/src/builtin-plugin/ContextReplacementPlugin.ts @@ -0,0 +1,54 @@ +import { + BuiltinPluginName, + type RawContextReplacementPluginOptions +} from "@rspack/binding"; + +import { create } from "./base"; + +export const ContextReplacementPlugin = create( + BuiltinPluginName.ContextReplacementPlugin, + ( + resourceRegExp: RegExp, + newContentResource?: any, + newContentRecursive?: any, + newContentRegExp?: any + ) => { + const rawOptions: RawContextReplacementPluginOptions = { + resourceRegExp + }; + if (typeof newContentResource === "function") { + // rawOptions.newContentCallback = newContentResource; + } else if ( + typeof newContentResource === "string" && + typeof newContentRecursive === "object" + ) { + rawOptions.newContentResource = newContentResource; + rawOptions.newContentCreateContextMap = newContentRecursive; + } else if ( + typeof newContentResource === "string" && + typeof newContentRecursive === "function" + ) { + rawOptions.newContentResource = newContentResource; + // rawOptions.newContentCreateContextMap = newContentRecursive; + } else { + if (typeof newContentResource !== "string") { + // biome-ignore lint/style/noParameterAssign: based on webpack's logic + newContentRegExp = newContentRecursive; + // biome-ignore lint/style/noParameterAssign: based on webpack's logic + newContentRecursive = newContentResource; + // biome-ignore lint/style/noParameterAssign: based on webpack's logic + newContentResource = undefined; + } + if (typeof newContentRecursive !== "boolean") { + // biome-ignore lint/style/noParameterAssign: based on webpack's logic + newContentRegExp = newContentRecursive; + // biome-ignore lint/style/noParameterAssign: based on webpack's logic + newContentRecursive = undefined; + } + rawOptions.newContentResource = newContentResource; + rawOptions.newContentRecursive = newContentRecursive; + rawOptions.newContentRegExp = newContentRegExp; + } + return rawOptions; + } +); diff --git a/packages/rspack/src/builtin-plugin/index.ts b/packages/rspack/src/builtin-plugin/index.ts index 480767a87f8..25182ec9b0f 100644 --- a/packages/rspack/src/builtin-plugin/index.ts +++ b/packages/rspack/src/builtin-plugin/index.ts @@ -64,3 +64,4 @@ export * from "./WebWorkerTemplatePlugin"; export * from "./WorkerPlugin"; export * from "./FetchCompileAsyncWasmPlugin"; export * from "./NoEmitOnErrorsPlugin"; +export * from "./ContextReplacementPlugin"; diff --git a/packages/rspack/src/exports.ts b/packages/rspack/src/exports.ts index 6c58b9246d6..3b4076ab395 100644 --- a/packages/rspack/src/exports.ts +++ b/packages/rspack/src/exports.ts @@ -258,6 +258,7 @@ export { SourceMapDevToolPlugin } from "./builtin-plugin"; export { EvalSourceMapDevToolPlugin } from "./builtin-plugin"; export { EvalDevToolModulePlugin } from "./builtin-plugin"; export { CssExtractRspackPlugin } from "./builtin-plugin"; +export { ContextReplacementPlugin } from "./builtin-plugin"; ///// Rspack Postfixed Internal Loaders ///// export type { diff --git a/tests/webpack-test/configCases/context-replacement/System.import/test.filter.js b/tests/webpack-test/configCases/context-replacement/System.import/test.filter.js deleted file mode 100644 index 3be456dcd23..00000000000 --- a/tests/webpack-test/configCases/context-replacement/System.import/test.filter.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = () => {return false} \ No newline at end of file diff --git a/tests/webpack-test/configCases/context-replacement/a/new-context/node_modules/error.js b/tests/webpack-test/configCases/context-replacement/a/new-context/node_modules/error.js new file mode 100644 index 00000000000..a7450cb49bc --- /dev/null +++ b/tests/webpack-test/configCases/context-replacement/a/new-context/node_modules/error.js @@ -0,0 +1,7 @@ +This +should +result +in +an +error +}]) \ No newline at end of file diff --git a/tests/webpack-test/configCases/context-replacement/a/new-context/node_modules/replaced.js b/tests/webpack-test/configCases/context-replacement/a/new-context/node_modules/replaced.js new file mode 100644 index 00000000000..f74a2f2b564 --- /dev/null +++ b/tests/webpack-test/configCases/context-replacement/a/new-context/node_modules/replaced.js @@ -0,0 +1 @@ +module.exports = "ok"; diff --git a/tests/webpack-test/configCases/context-replacement/a/test.filter.js b/tests/webpack-test/configCases/context-replacement/a/test.filter.js index 3be456dcd23..7910c8152ef 100644 --- a/tests/webpack-test/configCases/context-replacement/a/test.filter.js +++ b/tests/webpack-test/configCases/context-replacement/a/test.filter.js @@ -1 +1 @@ -module.exports = () => {return false} \ No newline at end of file +module.exports = () => { return false } \ No newline at end of file diff --git a/tests/webpack-test/configCases/context-replacement/b/test.filter.js b/tests/webpack-test/configCases/context-replacement/b/test.filter.js index 3be456dcd23..7910c8152ef 100644 --- a/tests/webpack-test/configCases/context-replacement/b/test.filter.js +++ b/tests/webpack-test/configCases/context-replacement/b/test.filter.js @@ -1 +1 @@ -module.exports = () => {return false} \ No newline at end of file +module.exports = () => { return false } \ No newline at end of file diff --git a/website/docs/en/plugins/webpack/_meta.json b/website/docs/en/plugins/webpack/_meta.json index 732f13c0939..bcdf3eca101 100644 --- a/website/docs/en/plugins/webpack/_meta.json +++ b/website/docs/en/plugins/webpack/_meta.json @@ -22,5 +22,6 @@ "normal-module-replacement-plugin", "javascript-modules-plugin", "internal-plugins", - "no-emit-on-errors-plugin" + "no-emit-on-errors-plugin", + "context-replacement-plugin" ] diff --git a/website/docs/en/plugins/webpack/context-replacement-plugin.mdx b/website/docs/en/plugins/webpack/context-replacement-plugin.mdx new file mode 100644 index 00000000000..18ea32305ca --- /dev/null +++ b/website/docs/en/plugins/webpack/context-replacement-plugin.mdx @@ -0,0 +1,58 @@ +import WebpackLicense from '@components/WebpackLicense'; + + + +# ContextReplacementPlugin + +`Context` refers to a `require` or dynamic `import()` with an expression such as `require('./locale/' + name + '.json')`. +When encountering such an expression, Rspack infers the directory (`'./locale/'`) and a regular expression (`/^.*\.json$/`). +Since the name is not known at compile time, Rspack includes every file as module in the bundle. + +The `ContextReplacementPlugin` allows you to override the inferred information. There are various ways to configure the plugin: + +## Options + +- **Type:** + +```ts +new rspack.ContextReplacementPlugin( + resourceRegExp: RegExp, + newContentResource?: string, + newContentRecursive?: boolean, + newContentRegExp?: RegExp +) +``` + +If the resource (directory) matches `resourceRegExp`, the plugin replaces the default resource, recursive flag or generated regular expression with `newContentResource`, `newContentRecursive` or `newContextRegExp` respectively. +If `newContentResource` is relative, it is resolved relative to the previous resource. + +## Examples + +### Basic Use Case + +```js +new rspack.ContextReplacementPlugin(/moment[/\\]locale$/, /de|fr|hu/); +``` + +The `moment/locale` context is restricted to files matching `/de|fr|hu/`. Thus only those locales are included (see [this issue](https://github.com/moment/moment/issues/2373) for more information). + +### Other Options + +The `newContentResource` and `newContentCreateContextMap` parameters are also available: + +```ts +new rspack.ContextReplacementPlugin( + resourceRegExp: RegExp, + newContentResource: string, + newContentCreateContextMap: object // mapping runtime-request (userRequest) to compile-time-request (request) +); +``` + +These two parameters can be used together to redirect requests in a more targeted way. The `newContentCreateContextMap` allows you to map runtime requests to compile requests in the form of an object: + +```js +new rspack.ContextReplacementPlugin(/selector/, './folder', { + './request': './request', + './other-request': './new-request', +}); +``` diff --git a/website/docs/zh/plugins/webpack/_meta.json b/website/docs/zh/plugins/webpack/_meta.json index 732f13c0939..bcdf3eca101 100644 --- a/website/docs/zh/plugins/webpack/_meta.json +++ b/website/docs/zh/plugins/webpack/_meta.json @@ -22,5 +22,6 @@ "normal-module-replacement-plugin", "javascript-modules-plugin", "internal-plugins", - "no-emit-on-errors-plugin" + "no-emit-on-errors-plugin", + "context-replacement-plugin" ] diff --git a/website/docs/zh/plugins/webpack/context-replacement-plugin.mdx b/website/docs/zh/plugins/webpack/context-replacement-plugin.mdx new file mode 100644 index 00000000000..77e683f11a6 --- /dev/null +++ b/website/docs/zh/plugins/webpack/context-replacement-plugin.mdx @@ -0,0 +1,58 @@ +import WebpackLicense from '@components/WebpackLicense'; + + + +# ContextReplacementPlugin + +`context` 是指带有表达式的 `require` 或动态 `import()`,例如 `require('./locale/' + name + '.json')` +遇到这种表达式时,Rspack 会推断出目录(`'./locale/'`)和一个正则表达式(`/^.*.json$/`)。 +由于在编译时无法确定 `name`,Rspack 会将每个文件都作为模块打包。 + +`ContextReplacementPlugin` 允许你覆盖这些推断的信息。该插件可以通过多种方式进行配置: + +## 选项 + +- **类型:** + +```ts +new rspack.ContextReplacementPlugin( + resourceRegExp: RegExp, + newContentResource?: string, + newContentRecursive?: boolean, + newContentRegExp?: RegExp +) +``` + +如果目录匹配 `resourceRegExp`,插件会用 `newContentResource`、`newContentRecursive` 或 `newContextRegExp` 分别替换默认资源、递归标志或生成的正则表达式。 +如果 `newContentResource` 是相对路径,则相对于前一个资源进行解析。 + +## 示例 + +### 基本使用 + +```js +new rspack.ContextReplacementPlugin(/moment[/\\]locale$/, /de|fr|hu/); +``` + +这个示例限制了 `moment/locale` 目录下的文件,仅打包匹配 `/de|fr|hu/` 的文件。因此,仅包含这些语言模块(在[这里](https://github.com/moment/moment/issues/2373)了解更多信息)。 + +### 其他选项 + +关于 `newContentResource` 和 `newContentCreateContextMap` 参数: + +```ts +new rspack.ContextReplacementPlugin( + resourceRegExp: RegExp, + newContentResource: string, + newContentCreateContextMap: object // 映射运行时请求(userRequest)到编译时请求(request) +); +``` + +这两个参数可以一起使用,以更有针对性地重定向请求。`newContentCreateContextMap` 允许你以对象的形式映射运行时请求到编译时请求: + +```js +new rspack.ContextReplacementPlugin(/selector/, './folder', { + './request': './request', + './other-request': './new-request', +}); +```