From 4bf27c544dbfc2e972bafdf3f407e0dd64ca043f Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Thu, 13 Aug 2026 10:28:11 +0800 Subject: [PATCH 01/10] perf: avoid parsing concatenated module codegen --- Cargo.lock | 1 + crates/node_binding/napi-binding.d.ts | 1 + crates/rspack/src/builder/mod.rs | 11 + .../snapshots/defaults__default_options.snap | 1 + .../src/raw_options/raw_experiments/mod.rs | 2 + crates/rspack_core/Cargo.toml | 1 + .../src/artifacts/code_generation_results.rs | 66 ++- .../src/compilation/code_generation/mod.rs | 27 +- crates/rspack_core/src/concatenated_module.rs | 483 ++++++++++++------ .../src/concatenated_module/faster.rs | 449 ++++++++++++++++ .../src/dependency/cached_const_dependency.rs | 54 +- .../src/dependency/const_dependency.rs | 41 +- .../src/dependency/dependency_template.rs | 35 +- crates/rspack_core/src/external_module.rs | 170 ++++-- crates/rspack_core/src/init_fragment.rs | 152 +++++- crates/rspack_core/src/module.rs | 4 +- .../src/options/experiments/mod.rs | 1 + .../src/utils/concatenated_module_visitor.rs | 27 + .../src/utils/concatenation_scope.rs | 221 +++++++- crates/rspack_plugin_asset/src/lib.rs | 73 ++- .../src/parser_and_generator/generator.rs | 38 +- crates/rspack_plugin_esm_library/src/link.rs | 81 ++- .../commonjs/require_header_dependency.rs | 1 + .../require_resolve_header_dependency.rs | 3 +- .../esm/esm_export_expression_dependency.rs | 44 +- .../esm/esm_export_header_dependency.rs | 20 +- ...sm_export_imported_specifier_dependency.rs | 136 ++++- .../esm/esm_export_specifier_dependency.rs | 9 +- .../dependency/esm/esm_import_dependency.rs | 42 +- .../esm/esm_import_specifier_dependency.rs | 30 +- .../esm/import_meta_rsc_dependency.rs | 38 +- .../src/dependency/esm/mod.rs | 2 +- .../src/dependency/esm/provide_dependency.rs | 26 +- .../src/parser_and_generator/mod.rs | 126 +++-- .../src/parser_plugin/compatibility_plugin.rs | 61 ++- .../esm_export_dependency_parser_plugin.rs | 32 +- .../src/plugin/api_plugin.rs | 42 +- .../src/plugin/url_plugin.rs | 11 + .../src/visitors/concatenate_scope_info.rs | 168 ++++++ .../src/visitors/dependency/parser/mod.rs | 4 + .../src/visitors/mod.rs | 1 + crates/rspack_plugin_json/src/lib.rs | 39 +- .../src/module_path_name_dependency.rs | 46 +- .../rspack_plugin_rstest/src/parser_plugin.rs | 30 +- crates/rspack_sources/src/replace_source.rs | 8 + crates/rspack_sources/src/source.rs | 9 +- packages/rspack-test-tools/src/case/normal.ts | 34 +- packages/rspack/src/config/defaults.ts | 1 + packages/rspack/src/config/normalization.ts | 1 + packages/rspack/src/config/types.ts | 5 + .../FasterModuleConcatenation.test.js | 35 ++ .../define-plugin-placeholder-string/index.js | 6 + .../module.js | 5 + .../rspack.config.js | 13 + .../rspack-test/defaultsCases/default/base.js | 1 + .../concatenate_parse_module/rspack.config.js | 4 + .../index.js | 7 + .../placeholder.json | 3 + .../test.config.js | 5 + website/docs/en/config/experiments.mdx | 17 + website/docs/zh/config/experiments.mdx | 17 + xtask/benchmark/benches/groups/bundle/util.rs | 6 +- .../benches/groups/compilation_stages.rs | 13 +- 63 files changed, 2451 insertions(+), 589 deletions(-) create mode 100644 crates/rspack_core/src/concatenated_module/faster.rs create mode 100644 crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs create mode 100644 tests/rspack-test/FasterModuleConcatenation.test.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/index.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/module.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/rspack.config.js create mode 100644 tests/rspack-test/normalCases/json/import-with-default-with-concatenation/placeholder.json create mode 100644 tests/rspack-test/normalCases/json/import-with-default-with-concatenation/test.config.js diff --git a/Cargo.lock b/Cargo.lock index 9eca6a216d3c..2ea9523fd9ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4209,6 +4209,7 @@ dependencies = [ "inventory", "itertools 0.14.0", "json", + "memchr", "mime_guess", "napi", "num-bigint", diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index 18d764191cbe..99498e33bfd6 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -2290,6 +2290,7 @@ export interface RawExperiments { newCache: boolean deferImport: boolean sourceImport: boolean + fasterModuleConcatenation: boolean pureFunctions: boolean runtimeMode?: "webpack" | "rspack" } diff --git a/crates/rspack/src/builder/mod.rs b/crates/rspack/src/builder/mod.rs index 6e24904eb2e7..0de042597da9 100644 --- a/crates/rspack/src/builder/mod.rs +++ b/crates/rspack/src/builder/mod.rs @@ -3684,6 +3684,8 @@ pub struct ExperimentsBuilder { defer_import: Option, /// Whether to enable source import. source_import: Option, + /// Whether to enable the faster module concatenation implementation. + faster_module_concatenation: Option, // TODO: lazy compilation pure_functions: Option, runtime_mode: Option, @@ -3698,6 +3700,7 @@ impl From for ExperimentsBuilder { async_web_assembly: None, defer_import: Some(value.defer_import), source_import: Some(value.source_import), + faster_module_concatenation: Some(value.faster_module_concatenation), pure_functions: Some(value.pure_functions), runtime_mode: Some(value.runtime_mode), } @@ -3713,6 +3716,7 @@ impl From<&mut ExperimentsBuilder> for ExperimentsBuilder { async_web_assembly: value.async_web_assembly.take(), defer_import: value.defer_import.take(), source_import: value.source_import.take(), + faster_module_concatenation: value.faster_module_concatenation.take(), pure_functions: value.pure_functions.take(), runtime_mode: value.runtime_mode.take(), } @@ -3756,6 +3760,12 @@ impl ExperimentsBuilder { self } + /// Set whether to enable the faster module concatenation implementation. + pub fn faster_module_concatenation(&mut self, faster_module_concatenation: bool) -> &mut Self { + self.faster_module_concatenation = Some(faster_module_concatenation); + self + } + /// Build [`Experiments`] from options. /// /// [`Experiments`]: rspack_core::options::Experiments @@ -3775,6 +3785,7 @@ impl ExperimentsBuilder { new_cache: d!(self.new_cache, false), defer_import: d!(self.defer_import, false), source_import: d!(self.source_import, false), + faster_module_concatenation: d!(self.faster_module_concatenation, false), pure_functions: d!(self.pure_functions, _production), runtime_mode: d!(self.runtime_mode, RuntimeMode::Webpack), }) diff --git a/crates/rspack/tests/snapshots/defaults__default_options.snap b/crates/rspack/tests/snapshots/defaults__default_options.snap index 4c130dfcce8d..2323aa27525a 100644 --- a/crates/rspack/tests/snapshots/defaults__default_options.snap +++ b/crates/rspack/tests/snapshots/defaults__default_options.snap @@ -1751,6 +1751,7 @@ CompilerOptions { new_cache: false, defer_import: false, source_import: false, + faster_module_concatenation: false, pure_functions: false, runtime_mode: Webpack, }, diff --git a/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs b/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs index ef7e2311d132..1ed1803edf97 100644 --- a/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs +++ b/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs @@ -16,6 +16,7 @@ pub struct RawExperiments { pub new_cache: bool, pub defer_import: bool, pub source_import: bool, + pub faster_module_concatenation: bool, pub pure_functions: bool, #[napi(ts_type = "\"webpack\" | \"rspack\"")] pub runtime_mode: Option, @@ -34,6 +35,7 @@ impl From for Experiments { new_cache: value.new_cache, defer_import: value.defer_import, source_import: value.source_import, + faster_module_concatenation: value.faster_module_concatenation, pure_functions: value.pure_functions, runtime_mode, } diff --git a/crates/rspack_core/Cargo.toml b/crates/rspack_core/Cargo.toml index d82912960fa8..5a50e7fa2cf0 100644 --- a/crates/rspack_core/Cargo.toml +++ b/crates/rspack_core/Cargo.toml @@ -27,6 +27,7 @@ indoc = { workspace = true } inventory = { workspace = true } itertools = { workspace = true } json = { workspace = true } +memchr = { workspace = true } mime_guess = { workspace = true } napi = { workspace = true, optional = true } num-bigint = { workspace = true } diff --git a/crates/rspack_core/src/artifacts/code_generation_results.rs b/crates/rspack_core/src/artifacts/code_generation_results.rs index d6edaa5e71e2..3aaabb1ffa0c 100644 --- a/crates/rspack_core/src/artifacts/code_generation_results.rs +++ b/crates/rspack_core/src/artifacts/code_generation_results.rs @@ -14,7 +14,8 @@ use serde::Serialize; use crate::{ ArtifactExt, AssetInfo, BindingCell, ChunkInitFragments, ConcatenationScope, ModuleIdentifier, - RuntimeGlobals, RuntimeSpec, RuntimeSpecMap, SourceType, incremental::IncrementalPasses, + RenderedInitFragments, RuntimeGlobals, RuntimeSpec, RuntimeSpecMap, SourceType, + incremental::IncrementalPasses, }; #[derive(Clone, Debug)] @@ -92,6 +93,26 @@ impl CodeGenerationDataTopLevelDeclarations { } } +/// Typed [`CodeGenerationData`] entry for the digest of rendered init fragments. +/// +/// `CodeGenerationData` is keyed by `TypeId`, so the newtype keeps this digest +/// distinct from other `RspackHashDigest` values stored as code generation data. +#[derive(Clone, Debug)] +pub struct RenderedInitFragmentsDigest(RspackHashDigest); + +impl RenderedInitFragmentsDigest { + pub fn new(inner: RspackHashDigest) -> Self { + Self(inner) + } +} + +impl RspackHash for RenderedInitFragmentsDigest { + fn hash(&self, state: &mut RspackHasher) { + state.write(b"RenderedInitFragmentsDigest"); + self.0.hash(state); + } +} + #[derive(Debug, Default, Clone)] pub struct CodeGenerationData { inner: anymap::Map, @@ -147,32 +168,27 @@ impl CodeGenerationResult { hash_function: &HashFunction, hash_digest: &HashDigest, hash_salt: &HashSalt, + concatenated_module_hash: Option<&RspackHashDigest>, ) { let mut hasher = RspackHasher::with_salt(hash_function, hash_salt); - for (source_type, source) in self.inner.as_ref() { - source_type.hash(&mut hasher); - std::hash::Hash::hash(source, &mut hasher); - } - self.chunk_init_fragments.hash(&mut hasher); - self.runtime_requirements.hash(&mut hasher); - self.hash = Some(hasher.digest(hash_digest)); - } - - /// Concatenated modules already encode the generated module bodies into - /// `ConcatenatedModule::get_runtime_hash`, so we can reuse that digest here - /// and only mix in codegen-specific metadata instead of hashing the large - /// concatenated source again. - pub fn set_hash_for_concatenated_module( - &mut self, - runtime_hash: &RspackHashDigest, - hash_function: &HashFunction, - hash_digest: &HashDigest, - hash_salt: &HashSalt, - ) { - let mut hasher = RspackHasher::with_salt(hash_function, hash_salt); - runtime_hash.hash(&mut hasher); - for source_type in self.inner.as_ref().keys() { - source_type.hash(&mut hasher); + if let Some(concatenated_module_hash) = concatenated_module_hash { + concatenated_module_hash.hash(&mut hasher); + for source_type in self.inner.as_ref().keys() { + source_type.hash(&mut hasher); + } + if let Some(digest) = self.data.get::() { + digest.hash(&mut hasher); + } + } else { + for (source_type, source) in self.inner.as_ref() { + source_type.hash(&mut hasher); + std::hash::Hash::hash(source, &mut hasher); + } + if let Some(fragments) = self.data.get::() + && !fragments.is_empty() + { + fragments.hash(&mut hasher); + } } self.chunk_init_fragments.hash(&mut hasher); self.runtime_requirements.hash(&mut hasher); diff --git a/crates/rspack_core/src/compilation/code_generation/mod.rs b/crates/rspack_core/src/compilation/code_generation/mod.rs index 38198c2baa89..7c211b9b4b34 100644 --- a/crates/rspack_core/src/compilation/code_generation/mod.rs +++ b/crates/rspack_core/src/compilation/code_generation/mod.rs @@ -194,23 +194,15 @@ pub(crate) async fn code_generation_modules( codegen_res .runtime_requirements .extend(*runtime_template.runtime_requirements()); - if module.as_concatenated_module().is_some() { - // Concatenated modules are special here: `job.hash` already - // fingerprints the generated module bodies, so we only need - // to fold in the remaining codegen metadata. - codegen_res.set_hash_for_concatenated_module( - &job.hash, - &options.output.hash_function, - &options.output.hash_digest, - &options.output.hash_salt, - ); - } else { - codegen_res.set_hash( - &options.output.hash_function, - &options.output.hash_digest, - &options.output.hash_salt, - ); - } + codegen_res.set_hash( + &options.output.hash_function, + &options.output.hash_digest, + &options.output.hash_salt, + module + .as_concatenated_module() + .is_some() + .then_some(&job.hash), + ); codegen_res }) }) @@ -245,6 +237,7 @@ pub(crate) async fn code_generation_modules( &compilation.options.output.hash_function, &compilation.options.output.hash_digest, &compilation.options.output.hash_salt, + None, ); codegen_res } diff --git a/crates/rspack_core/src/concatenated_module.rs b/crates/rspack_core/src/concatenated_module.rs index bcfb179d76a6..fa70e9f92842 100644 --- a/crates/rspack_core/src/concatenated_module.rs +++ b/crates/rspack_core/src/concatenated_module.rs @@ -44,23 +44,36 @@ use crate::{ BuildMetaExportsType, BuildResult, ChunkGraph, ChunkInitFragments, ChunkRenderContext, CodeGenerationDataTopLevelDeclarations, CodeGenerationPublicPathAutoReplace, CodeGenerationResult, CodeGenerationRuntimeRequirementsWrite, Compilation, - ConcatenatedModuleIdent, ConcatenationScope, ConditionalInitFragment, ConnectionState, Context, - DEFAULT_EXPORT, DEFAULT_EXPORT_ATOM, DependenciesBlock, DependencyId, DependencyType, ExportInfo, - ExportProvided, ExportsArgument, ExportsInfoArtifact, ExportsType, FactoryMeta, - ImportedByDeferModulesArtifact, InitFragment, InitFragmentStage, LibIdentOptions, Module, - ModuleArgument, ModuleCodeGenerationContext, ModuleGraph, ModuleGraphCacheArtifact, - ModuleGraphConnection, ModuleIdentifier, ModuleLayer, ModuleStaticCache, ModuleType, - NAMESPACE_OBJECT_EXPORT, ParserOptions, Resolve, RuntimeCondition, RuntimeGlobals, RuntimeSpec, - SideEffectsStateArtifact, SourceType, URLStaticMode, UsageState, UsedName, UsedNameItem, - escape_identifier, fast_set, filter_runtime, find_target, get_runtime_key, - impl_source_map_config, merge_runtime_condition, merge_runtime_condition_non_false, - module_update_hash, property_access, property_name, + ConcatenatedModuleIdent, ConcatenatedModuleReference, ConcatenationScope, + ConditionalInitFragment, ConnectionState, Context, DEFAULT_EXPORT, DEFAULT_EXPORT_ATOM, + DependenciesBlock, DependencyId, DependencyRange, DependencyType, ExportInfo, ExportProvided, + ExportsArgument, ExportsInfoArtifact, ExportsType, FactoryMeta, ImportedByDeferModulesArtifact, + InitFragment, InitFragmentStage, LibIdentOptions, Module, ModuleArgument, + ModuleCodeGenerationContext, ModuleGraph, ModuleGraphCacheArtifact, ModuleGraphConnection, + ModuleIdentifier, ModuleLayer, ModuleStaticCache, ModuleType, NAMESPACE_OBJECT_EXPORT, + ParserOptions, PendingConcatenationScopeInfo, RenderedInitFragments, RenderedInitFragmentsDigest, + Resolve, RuntimeCondition, RuntimeGlobals, RuntimeSpec, SideEffectsStateArtifact, SourceType, + URLStaticMode, UsageState, UsedName, UsedNameItem, escape_identifier, fast_set, filter_runtime, + find_target, get_runtime_key, impl_source_map_config, merge_runtime_condition, + merge_runtime_condition_non_false, module_update_hash, property_access, property_name, render_make_deferred_namespace_mode_from_exports_type, reserved_names::RESERVED_NAMES_ATOM_SET, subtract_runtime_condition, to_identifier_with_escaped, to_normal_comment, utils::{SourceSizeCache, SourceSizeCacheSerde}, }; +mod faster; + +pub(crate) use faster::populate_info_from_pending; +use faster::render_concatenated_module_source; + +pub const CONCATENATION_PLACEHOLDER_PREFIX: &str = "__rspack_"; +pub(crate) const GENERATED_TOP_LEVEL_SYMBOL_PREFIX: &str = "__rspack_generated_top_level_symbol_"; +pub(crate) const MODULE_REFERENCE_PLACEHOLDER_PREFIX: &str = + "__rspack_module_reference_placeholder_"; +pub(crate) const MODULE_REFERENCE_PREFIX: &str = "__rspack_module_ref"; +pub(crate) const MODULE_REFERENCE_SUFFIX: &str = "__._"; + type ExportsDefinitionArgs = Vec<(String, String)>; define_hook!(ConcatenatedModuleExportsDefinitions: SeriesBail(exports_definitions: &mut ExportsDefinitionArgs, is_entry_module: bool) -> bool); define_hook!(ConcatenatedModuleConcatenatedInfo: Series(compilation: &Compilation, module: ModuleIdentifier, runtime: Option<&RuntimeSpec>, info: &mut ConcatenatedModuleInfo, all_used_names: &mut HashSet)); @@ -287,6 +300,46 @@ pub struct ConcatenatedImportMapItem { pub type ConcatenatedImportMap = Option), ConcatenatedImportMapItem>>; +/// A codegen-created top-level binding whose final name is assigned after all +/// modules in the concatenation are known. +#[derive(Debug, Clone)] +pub struct GeneratedTopLevelSymbol { + pub preferred_name: Atom, + pub placeholder: Atom, +} + +/// Scope identifier supplied by dependency templates because it cannot be +/// reconstructed from the make-time scope information alone. +#[derive(Debug, Clone)] +pub struct AddedScopeIdent { + pub symbol: Atom, + pub range: DependencyRange, + pub shorthand: bool, + pub is_class_expr_with_ident: bool, +} + +/// Dependency-template correction applied to an identifier collected during +/// make, after codegen replacements have changed its syntax. +#[derive(Debug, Clone, Copy)] +pub enum OriginalScopeIdentUpdate { + Remove(DependencyRange), + NonShorthand(DependencyRange), +} + +/// Incremental data needed to rebuild concatenation scope information without +/// reparsing the generated module source. +#[derive(Debug, Clone, Default)] +pub struct FasterModuleConcatenationInfo { + /// Corrections to make-time identifiers caused by dependency replacements. + pub original_scope_ident_updates: Vec, + /// Scope identifiers supplied by dependency templates in addition to the + /// make-time collection. + pub added_scope_idents: Vec, + /// Names introduced or referenced by generated code that must be reserved + /// during global deconfliction. + pub added_used_names: Vec, +} + #[derive(Debug, Clone, Default)] pub struct ConcatenatedModuleInfo { pub index: usize, @@ -315,6 +368,9 @@ pub struct ConcatenatedModuleInfo { pub idents: Vec, pub all_used_names: HashSet, pub binding_to_ref: FxIndexMap<(Atom, SyntaxContext), Vec>, + pub rendered_init_fragments: Option, + pub generated_top_level_symbols: Vec, + pub module_references: FxIndexMap, pub public_path_auto_replacement: Option, pub static_url_replacement: bool, @@ -1036,7 +1092,7 @@ impl Module for ConcatenatedModule { s.spawn(|(module, compilation, runtime, id, info)| async move { let concatenation_scope = if let ModuleInfo::Concatenated(info) = info { let info = info.as_ref(); - Some(ConcatenationScope::new( + let mut scope = ConcatenationScope::new( module.id, module_to_info_map.expect("should have module_to_info_map for concatenated module"), ConcatenatedModuleInfo { @@ -1044,7 +1100,11 @@ impl Module for ConcatenatedModule { module: info.module, ..Default::default() }, - )) + ); + if compilation.options.experiments.faster_module_concatenation { + scope.enable_faster_module_concatenation(); + } + Some(scope) } else { None }; @@ -1136,9 +1196,14 @@ impl Module for ConcatenatedModule { match info { ModuleInfo::Concatenated(info) => { for (id, _) in info.binding_to_ref.iter() { + let name = info + .generated_top_level_symbols + .iter() + .find(|symbol| symbol.placeholder == id.0) + .map_or(&id.0, |symbol| &symbol.preferred_name); escaped_names - .entry(id.0.clone()) - .or_insert_with(|| escape_name_atom_ref(&id.0)); + .entry(name.clone()) + .or_insert_with(|| escape_name_atom_ref(name)); } if let Some(import_map) = &info.import_map { @@ -1207,15 +1272,20 @@ impl Module for ConcatenatedModule { ModuleInfo::Concatenated(info) => { // Iterate over variables in moduleScope for (id, refs) in info.binding_to_ref.iter() { - let name = &id.0; + let binding_name = &id.0; let ctxt = id.1; if ctxt != info.module_ctxt { continue; } + let name = info + .generated_top_level_symbols + .iter() + .find(|symbol| symbol.placeholder == *binding_name) + .map_or(binding_name, |symbol| &symbol.preferred_name); // Check if the name is already used - if name_allocator.contains(name) { + let final_name = if name_allocator.contains(name) { // Find a new name and update references - let new_name = name_allocator.find_new_name( + name_allocator.find_new_name( escaped_names .get(name) .expect("should have escaped name") @@ -1223,29 +1293,34 @@ impl Module for ConcatenatedModule { escaped_identifiers .get(&readable_identifier) .expect("should have escaped identifier"), - ); - info.internal_names.insert(name.clone(), new_name.clone()); - top_level_declarations.insert(new_name.clone()); + ) + } else { + // Handle the case when the name is not already used + name_allocator.insert(name.clone()); + name.clone() + }; + info + .internal_names + .insert(binding_name.clone(), final_name.clone()); + if binding_name != name && name == &*DEFAULT_EXPORT_ATOM { + info.internal_names.insert(name.clone(), final_name.clone()); + } + top_level_declarations.insert(final_name.clone()); + if final_name != *binding_name && !refs.is_empty() { // Update source let source = info.source.as_mut().expect("should have source"); - for identifier in refs { let span = identifier.id.span(); let low = span.real_lo(); let high = span.real_hi(); if identifier.shorthand { - source.insert(high, format!(": {new_name}"), None); + source.insert(high, format!(": {final_name}"), None); continue; } - source.replace(low, high, new_name.to_string(), None); + source.replace(low, high, final_name.to_string(), None); } - } else { - // Handle the case when the name is not already used - name_allocator.insert(name.clone()); - info.internal_names.insert(name.clone(), name.clone()); - top_level_declarations.insert(name.clone()); } } @@ -1474,31 +1549,44 @@ impl Module for ConcatenatedModule { .expect("should have module"); let build_meta = module.build_meta(); let mut refs = vec![]; - for reference in info.global_scope_ident.iter() { - let name = &reference.id.sym; - let match_result = ConcatenationScope::match_module_reference(name.as_str()); - if let Some(match_info) = match_result { - let referenced_info_id = &references_info[match_info.index].0; + if info.module_references.is_empty() { + for reference in info.global_scope_ident.iter() { + let match_result = + ConcatenationScope::match_module_reference(reference.id.sym.as_str()); + if let Some(match_info) = match_result { + let referenced_info_id = references_info[match_info.index].0; + let span = reference.id.span(); + refs.push(( + ModuleReferenceReplacementTarget::Span(span.real_lo(), span.real_hi() + 2), + referenced_info_id, + match_info.ids, + match_info.call, + !match_info.direct_import, + match_info.deferred_import, + build_meta.strict_esm_module(), + match_info.asi_safe, + )); + } + } + } else { + for (placeholder, reference) in &info.module_references { + let referenced_info_id = reference.module; refs.push(( - reference.clone(), + ModuleReferenceReplacementTarget::Placeholder(placeholder.clone()), referenced_info_id, - match_info - .ids - .into_iter() - .map(|item| Atom::from(item.as_str())) - .collect::>(), - match_info.call, - !match_info.direct_import, - match_info.deferred_import, + reference.options.ids.clone(), + reference.options.call, + !reference.options.direct_import, + reference.options.deferred_import, build_meta.strict_esm_module(), - match_info.asi_safe, + reference.options.asi_safe, )); } } let mut changes = vec![]; for ( - reference_ident, + reference_target, referenced_info_id, export_name, call, @@ -1513,7 +1601,7 @@ impl Module for ConcatenatedModule { &compilation.module_graph_cache_artifact, &compilation.exports_info_artifact, &compilation.module_static_cache, - referenced_info_id, + &referenced_info_id, export_name, &module_to_info_map, runtime, @@ -1524,33 +1612,38 @@ impl Module for ConcatenatedModule { asi_safe, &context, ); - - // We assume this should be concatenated module info because previous loop - let span = reference_ident.id.span(); - let low = span.real_lo(); - let high = span.real_hi(); - // let source = info.source.as_mut().expect("should have source"); - // range is extended by 2 chars to cover the appended "._" - // https://github.com/webpack/webpack/blob/ac7e531436b0d47cd88451f497cdfd0dad41535d/lib/optimize/ConcatenatedModule.js#L1411-L1412 - changes.push((final_name, (low, high + 2))); + changes.push((reference_target, final_name)); } Some((info.module, changes)) }) .collect::>(); + let mut module_reference_replacements: IdentifierMap> = + IdentifierMap::default(); + for (module_info_id, module_changes) in changes.iter_mut() { - for (name_result, (low, high)) in mem::take(module_changes) { + for (target, name_result) in mem::take(module_changes) { name_result.apply_to_info( &mut module_to_info_map, &mut needed_namespace_objects, &mut needed_namespace_objects_queue, ); - let info = module_to_info_map - .get_mut(module_info_id) - .and_then(|info| info.try_as_concatenated_mut()) - .expect("should have concatenate module info"); - let source = info.source.as_mut().expect("should have source"); - source.replace(low, high, name_result.name, None); + match target { + ModuleReferenceReplacementTarget::Placeholder(placeholder) => { + module_reference_replacements + .entry(*module_info_id) + .or_default() + .push((placeholder, name_result.name)); + } + ModuleReferenceReplacementTarget::Span(low, high) => { + let info = module_to_info_map + .get_mut(module_info_id) + .and_then(|info| info.try_as_concatenated_mut()) + .expect("should have concatenate module info"); + let source = info.source.as_mut().expect("should have source"); + source.replace(low, high, name_result.name, None); + } + } } } @@ -1631,6 +1724,7 @@ impl Module for ConcatenatedModule { let mut result: ConcatSource = ConcatSource::default(); let mut should_add_esm_flag = false; let mut chunk_init_fragments: Vec>> = Vec::new(); + let mut rendered_init_fragments_hasher = None; for ((source, attr), import_spec) in import_stmts { let content = render_imports(&source, attr.as_deref(), &import_spec); @@ -1905,7 +1999,19 @@ impl Module for ConcatenatedModule { )); // https://github.com/webpack/webpack/blob/ac7e531436b0d47cd88451f497cdfd0dad41535d/lib/optimize/ConcatenatedModule.js#L1582 - result.add(info.source.take().expect("should have source")); + let module_reference_replacements = module_reference_replacements + .get(&info.module) + .map(Vec::as_slice) + .unwrap_or_default(); + let fragment_hasher = info.rendered_init_fragments.is_some().then(|| { + rendered_init_fragments_hasher + .get_or_insert_with(|| RspackHasher::new(&compilation.options.output.hash_function)) + }); + result.add(render_concatenated_module_source( + info, + module_reference_replacements, + fragment_hasher, + )); chunk_init_fragments.extend(mem::take(&mut info.chunk_init_fragments)); runtime_template @@ -2010,6 +2116,14 @@ impl Module for ConcatenatedModule { code_generation_result.add(SourceType::JavaScript, CachedSource::new(result).boxed()); code_generation_result.chunk_init_fragments = chunk_init_fragments; + if let Some(hasher) = rendered_init_fragments_hasher { + code_generation_result + .data + .insert(RenderedInitFragmentsDigest::new( + hasher.digest(&HashDigest::Hex), + )); + } + if public_path_auto_replace { code_generation_result .data @@ -2590,98 +2704,141 @@ impl ConcatenatedModule { chunk_init_fragments.extend(fragments.iter().cloned()); } - let concatenation_scope = concatenation_scope.expect("should have concatenation_scope"); - let source = inner + let mut concatenation_scope = concatenation_scope.expect("should have concatenation_scope"); + let faster_module_concatenation_info = + concatenation_scope.take_faster_module_concatenation_info(); + let rendered_source = inner .remove(&SourceType::JavaScript) .expect("should have javascript source"); - let source_code = source.source().into_string_lossy(); let mut module_info = concatenation_scope.current_module; - - let jsx = module - .as_ref() - .as_normal_module() - .and_then(|normal_module| normal_module.get_parser_options()) - .and_then(|options: &ParserOptions| { - options - .get_javascript() - .and_then(|js_options| js_options.jsx) - }) - .unwrap_or(false); - - let allocator = Allocator::new(); - let lexer = swc_experimental_ecma_parser::Lexer::new( - &allocator, - Syntax::Es(EsSyntax { - jsx, - ..Default::default() - }), - EsVersion::EsNext, - StringSource::new(source_code.as_ref()), - None, - ); - let mut p = Parser::new_from(&allocator, lexer); - let ret = p.parse_module(); - - let module = match ret { - Ok(module) => module, - Err(err) => { - // return empty error as we already push error to compilation.diagnostics - return Err(Error::from_string( - Some(source_code.into_owned()), - err.span().start.saturating_sub(1) as usize, - err.span().end.saturating_sub(1) as usize, - "JavaScript parse error:\n".to_string(), - err.kind().msg().to_string(), - )); - } - }; - let program = Program::Module(allocator.boxed(module)); - let semantic = resolver(&program); - let ids = collect_ident(&allocator, &program); - - module_info.module_ctxt = SyntaxContext::from_u32(semantic.top_level_scope_id().raw()); - module_info.global_ctxt = SyntaxContext::from_u32(semantic.unresolved_scope_id().raw()); - - let top_level_scope_id = semantic.top_level_scope_id(); - let mut all_used_names = HashSet::default(); - all_used_names.reserve(ids.len()); - module_info.idents.reserve(ids.len()); - module_info.global_scope_ident.reserve(ids.len()); - let mut binding_to_ref: FxIndexMap<(Atom, SyntaxContext), Vec> = - FxIndexMap::default(); - binding_to_ref.reserve(ids.len()); - - for ident in ids { - let scope = semantic.node_scope(&ident.id); - let is_global = SyntaxContext::from_u32(scope.raw()) == module_info.global_ctxt; - let legacy = if is_global { - let leg = ident.to_legacy(&semantic); - module_info.global_scope_ident.push(leg.clone()); - all_used_names.insert(leg.id.sym.clone()); - Some(leg) + let (result_source, internal_source) = + if let Some(faster_module_concatenation_info) = faster_module_concatenation_info { + // Non-JavaScript modules (for example JSON) do not have pending + // JavaScript scope info. Their generated top-level names are registered by + // dependency templates through the concatenation scope instead. + let empty_pending_scope_info = PendingConcatenationScopeInfo::default(); + let pending_scope_info = module + .build_info() + .pending_concatenation_scope_info + .as_deref(); + module_info.rendered_init_fragments = codegen_res + .data + .get::() + .cloned() + .filter(|fragments| !fragments.is_empty()); + // JavaScript code generation already returns a ReplaceSource whose + // coordinates are based on the make-time parser source. Keep that + // coordinate space so the pending info spans can be applied directly. The + // generated source is uniquely owned here, so recover it from the Arc + // without cloning every dependency replacement. + let result_source = if rendered_source.as_any().is::() { + let source = Arc::downcast::(rendered_source.into_any_arc()) + .expect("source type was checked above"); + Arc::try_unwrap(source).unwrap_or_else(|source| source.as_ref().clone()) + } else { + ReplaceSource::new(rendered_source) + }; + assert!( + pending_scope_info.is_none() + || module + .source() + .is_some_and(|source| Arc::ptr_eq(result_source.inner(), source)), + "generated ReplaceSource inner should preserve the normal module source" + ); + let original_source = result_source.inner().source().into_string_lossy(); + populate_info_from_pending( + pending_scope_info.unwrap_or(&empty_pending_scope_info), + &original_source, + &mut module_info, + &faster_module_concatenation_info, + ); + drop(original_source); + let internal_source = result_source.inner().clone(); + (result_source, internal_source) } else { - None + let source_code = rendered_source.source().into_string_lossy(); + let jsx = module + .as_ref() + .as_normal_module() + .and_then(|normal_module| normal_module.get_parser_options()) + .and_then(|options: &ParserOptions| { + options + .get_javascript() + .and_then(|js_options| js_options.jsx) + }) + .unwrap_or(false); + + let allocator = Allocator::new(); + let lexer = swc_experimental_ecma_parser::Lexer::new( + &allocator, + Syntax::Es(EsSyntax { + jsx, + ..Default::default() + }), + EsVersion::EsNext, + StringSource::new(source_code.as_ref()), + None, + ); + let mut parser = Parser::new_from(&allocator, lexer); + let parsed_module = match parser.parse_module() { + Ok(module) => module, + Err(err) => { + return Err(Error::from_string( + Some(source_code.into_owned()), + err.span().start.saturating_sub(1) as usize, + err.span().end.saturating_sub(1) as usize, + "JavaScript parse error:\n".to_string(), + err.kind().msg().to_string(), + )); + } + }; + let program = Program::Module(allocator.boxed(parsed_module)); + let semantic = resolver(&program); + let ids = collect_ident(&allocator, &program); + + module_info.module_ctxt = SyntaxContext::from_u32(semantic.top_level_scope_id().raw()); + module_info.global_ctxt = SyntaxContext::from_u32(semantic.unresolved_scope_id().raw()); + + let top_level_scope_id = semantic.top_level_scope_id(); + let mut all_used_names = HashSet::default(); + all_used_names.reserve(ids.len()); + module_info.idents.reserve(ids.len()); + module_info.global_scope_ident.reserve(ids.len()); + let mut binding_to_ref: FxIndexMap<(Atom, SyntaxContext), Vec> = + FxIndexMap::default(); + binding_to_ref.reserve(ids.len()); + + for ident in ids { + let scope = semantic.node_scope(&ident.id); + let is_global = SyntaxContext::from_u32(scope.raw()) == module_info.global_ctxt; + let legacy = if is_global { + let legacy = ident.to_legacy(&semantic); + module_info.global_scope_ident.push(legacy.clone()); + all_used_names.insert(legacy.id.sym.clone()); + Some(legacy) + } else { + None + }; + if ident.is_class_expr_with_ident { + all_used_names.insert(Atom::from(ident.id.sym.as_str())); + continue; + } + if scope != top_level_scope_id { + all_used_names.insert(Atom::from(ident.id.sym.as_str())); + } + let legacy = legacy.unwrap_or_else(|| ident.to_legacy(&semantic)); + module_info.idents.push(legacy.clone()); + binding_to_ref + .entry((legacy.id.sym.clone(), legacy.id.ctxt)) + .or_default() + .push(legacy); + } + module_info.all_used_names = all_used_names; + module_info.binding_to_ref = binding_to_ref; + let result_source = ReplaceSource::new(rendered_source.clone()); + (result_source, rendered_source) }; - if ident.is_class_expr_with_ident { - all_used_names.insert(Atom::from(ident.id.sym.as_str())); - continue; - } - // deconflict naming from inner scope, the module level deconflict will be finished - // you could see tests/webpack-test/cases/scope-hoisting/renaming-4967 as a example - // during module eval phase. - if scope != top_level_scope_id { - all_used_names.insert(Atom::from(ident.id.sym.as_str())); - } - let legacy = legacy.unwrap_or_else(|| ident.to_legacy(&semantic)); - module_info.idents.push(legacy.clone()); - binding_to_ref - .entry((legacy.id.sym.clone(), legacy.id.ctxt)) - .or_default() - .push(legacy); - } - module_info.all_used_names = all_used_names; - module_info.binding_to_ref = binding_to_ref; - let result_source = ReplaceSource::new(source.clone()); + module_info.internal_source = Some(internal_source); module_info.has_ast = true; module_info.runtime_requirements = runtime_requirements; if let Some(runtime_requirements_write) = codegen_res @@ -2690,7 +2847,6 @@ impl ConcatenatedModule { { module_info.runtime_requirements_write = runtime_requirements_write.runtime_requirements; } - module_info.internal_source = Some(source); module_info.source = Some(result_source); module_info.chunk_init_fragments = chunk_init_fragments; if let Some(CodeGenerationPublicPathAutoReplace(true)) = codegen_res @@ -3120,6 +3276,25 @@ impl ConcatenatedModule { if let Some(ref export_id) = export_id && let Some(direct_export) = info.export_map.as_ref().and_then(|map| map.get(export_id)) { + if let Some(reference) = info.module_references.get(direct_export) { + let referenced_info_id = reference.module; + let mut referenced_export_name = reference.options.ids.clone(); + referenced_export_name.extend(export_name.iter().skip(1).cloned()); + return Self::get_final_binding( + mg, + mg_cache, + exports_info_artifact, + &referenced_info_id, + referenced_export_name, + module_to_info_map, + runtime, + reference.options.call, + reference.options.deferred_import, + module.build_meta().strict_esm_module(), + reference.options.asi_safe, + already_visited, + ); + } if let Some(used_name) = exports_info.get_used_name(exports_info_artifact, runtime, &export_name) { @@ -3415,6 +3590,12 @@ struct FinalBindingResult { needed_namespace_object: Option, } +#[derive(Debug)] +enum ModuleReferenceReplacementTarget { + Placeholder(String), + Span(u32, u32), +} + impl FinalBindingResult { pub fn from_binding(binding: Binding) -> Self { Self { diff --git a/crates/rspack_core/src/concatenated_module/faster.rs b/crates/rspack_core/src/concatenated_module/faster.rs new file mode 100644 index 000000000000..edc5777450e4 --- /dev/null +++ b/crates/rspack_core/src/concatenated_module/faster.rs @@ -0,0 +1,449 @@ +use std::borrow::Cow; + +use rspack_hash::{RspackHash, RspackHasher}; +use rspack_sources::{BoxSource, ConcatSource, RawStringSource, ReplaceSource, SourceExt}; +use rspack_util::SpanExt; +use rustc_hash::FxHashSet as HashSet; +use smallvec::SmallVec; +use swc_core::{ + atoms::Atom, + common::{BytePos, Span, Spanned, SyntaxContext}, + ecma::visit::swc_ecma_ast, +}; + +use super::{ + CONCATENATION_PLACEHOLDER_PREFIX, ConcatenatedModuleInfo, FasterModuleConcatenationInfo, + GENERATED_TOP_LEVEL_SYMBOL_PREFIX, GeneratedTopLevelSymbol, MODULE_REFERENCE_PLACEHOLDER_PREFIX, + MODULE_REFERENCE_SUFFIX, OriginalScopeIdentUpdate, +}; +use crate::{ + ConcatenatedModuleIdent, ConcatenationScope, ConcatenationScopeIdentKind, DependencyRange, + PendingConcatenationScopeInfo, RenderedInitFragments, +}; + +type SeenIdent = (Atom, SyntaxContext, DependencyRange, bool, bool); + +enum SeenIdents { + Inline(SmallVec<[SeenIdent; 8]>), + Heap(HashSet), +} + +impl Default for SeenIdents { + fn default() -> Self { + Self::Inline(SmallVec::new()) + } +} + +impl SeenIdents { + fn insert(&mut self, ident: SeenIdent) -> bool { + match self { + Self::Inline(idents) => { + if idents.contains(&ident) { + return false; + } + if idents.len() < idents.inline_size() { + idents.push(ident); + return true; + } + + let mut ident_set = HashSet::with_capacity_and_hasher(idents.len() * 2, Default::default()); + ident_set.extend(idents.drain(..)); + ident_set.insert(ident); + *self = Self::Heap(ident_set); + true + } + Self::Heap(idents) => idents.insert(ident), + } + } +} + +struct PlaceholderReplacements<'a> { + module_references: &'a [(String, String)], + generated_symbols: &'a [GeneratedTopLevelSymbol], + internal_names: &'a rustc_hash::FxHashMap, +} + +impl PlaceholderReplacements<'_> { + fn is_empty(&self) -> bool { + self.module_references.is_empty() && self.generated_symbols.is_empty() + } + + fn get<'a>(&'a self, placeholder: &str) -> Option<&'a str> { + if let Some(index) = placeholder + .strip_prefix(MODULE_REFERENCE_PLACEHOLDER_PREFIX) + .and_then(|value| value.strip_suffix(MODULE_REFERENCE_SUFFIX)) + .and_then(|value| value.parse().ok()) + { + return self.get_module_reference(index, placeholder); + } + if let Some(index) = placeholder + .strip_prefix(GENERATED_TOP_LEVEL_SYMBOL_PREFIX) + .and_then(|value| value.strip_suffix("__")) + .and_then(|value| value.parse().ok()) + { + return self.get_generated_symbol(index, placeholder); + } + None + } + + fn get_module_reference<'a>(&'a self, index: usize, placeholder: &str) -> Option<&'a str> { + self + .module_references + .get(index) + .filter(|(candidate, _)| candidate == placeholder) + .or_else(|| { + self + .module_references + .iter() + .find(|(candidate, _)| candidate == placeholder) + }) + .map(|(_, name)| name.as_str()) + } + + fn get_generated_symbol<'a>(&'a self, index: usize, placeholder: &str) -> Option<&'a str> { + let symbol = self + .generated_symbols + .get(index) + .filter(|symbol| symbol.placeholder == placeholder)?; + self + .internal_names + .get(&symbol.placeholder) + .map(Atom::as_str) + } +} + +fn parse_decimal_prefix(value: &[u8]) -> Option<(usize, usize)> { + let mut index = 0usize; + let mut len = 0; + for &byte in value { + if !byte.is_ascii_digit() { + break; + } + index = index.checked_mul(10)?.checked_add((byte - b'0') as usize)?; + len += 1; + } + (len != 0).then_some((index, len)) +} + +pub(super) fn is_plain_identifier_name(name: &str) -> bool { + let mut chars = name.chars(); + let Some(first) = chars.next() else { + return false; + }; + if !(first == '_' || first == '$' || first.is_ascii_alphabetic()) { + return false; + } + chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric()) +} + +fn add_ident_to_module_info( + module_info: &mut ConcatenatedModuleInfo, + ident: ConcatenatedModuleIdent, + seen: &mut SeenIdents, +) { + let span = ident.id.span(); + if !seen.insert(( + ident.id.sym.clone(), + ident.id.ctxt, + DependencyRange::new(span.real_lo(), span.real_hi()), + ident.shorthand, + ident.is_class_expr_with_ident, + )) { + return; + } + + let is_global = ident.id.ctxt == module_info.global_ctxt; + if is_global { + module_info.global_scope_ident.push(ident.clone()); + module_info.all_used_names.insert(ident.id.sym.clone()); + } + if ident.is_class_expr_with_ident { + module_info.all_used_names.insert(ident.id.sym); + return; + } + if !is_global && ident.id.ctxt != module_info.module_ctxt { + module_info.all_used_names.insert(ident.id.sym.clone()); + } + module_info + .binding_to_ref + .entry((ident.id.sym.clone(), ident.id.ctxt)) + .or_default() + .push(ident.clone()); + module_info.idents.push(ident); +} + +pub(crate) fn populate_info_from_pending( + pending: &PendingConcatenationScopeInfo, + original_source: &str, + module_info: &mut ConcatenatedModuleInfo, + faster_info: &FasterModuleConcatenationInfo, +) { + let mut symbols = SmallVec::<[(&str, Atom); 8]>::new(); + let mut symbol_from_range = |range: DependencyRange| { + let symbol = original_source + .get(range.start as usize..range.end as usize) + .unwrap_or_else(|| { + panic!( + "concatenation scope symbol range {}..{} should be in the original source", + range.start, range.end + ) + }); + if let Some((_, interned)) = symbols.iter().find(|(candidate, _)| *candidate == symbol) { + return interned.clone(); + } + let interned = Atom::from(symbol); + symbols.push((symbol, interned.clone())); + interned + }; + module_info.module_ctxt = SyntaxContext::from_u32(pending.module_ctxt); + module_info.global_ctxt = SyntaxContext::from_u32(pending.global_ctxt); + module_info.idents.clear(); + module_info.global_scope_ident.clear(); + module_info.binding_to_ref.clear(); + module_info.all_used_names.clear(); + module_info.idents.reserve( + pending + .idents + .len() + .saturating_add(faster_info.added_scope_idents.len()), + ); + module_info.global_scope_ident.reserve(pending.idents.len()); + module_info.binding_to_ref.reserve( + pending + .idents + .len() + .saturating_add(faster_info.added_scope_idents.len()), + ); + module_info.all_used_names.reserve( + pending + .idents + .len() + .saturating_add(faster_info.added_used_names.len()), + ); + module_info + .all_used_names + .extend(faster_info.added_used_names.iter().cloned()); + + let mut removed_ranges = SmallVec::<[DependencyRange; 4]>::new(); + let mut non_shorthand_ranges = SmallVec::<[DependencyRange; 4]>::new(); + for update in &faster_info.original_scope_ident_updates { + match update { + OriginalScopeIdentUpdate::Remove(range) => removed_ranges.push(*range), + OriginalScopeIdentUpdate::NonShorthand(range) => non_shorthand_ranges.push(*range), + } + } + + let mut seen = SeenIdents::default(); + for pending_ident in &pending.idents { + let symbol = symbol_from_range(pending_ident.range); + if pending_ident.kind == ConcatenationScopeIdentKind::UsedName { + module_info.all_used_names.insert(symbol); + continue; + } + + if removed_ranges + .iter() + .any(|range| range.start <= pending_ident.range.start && pending_ident.range.end <= range.end) + { + if pending_ident.kind == ConcatenationScopeIdentKind::Global { + module_info.all_used_names.insert(symbol); + } + continue; + } + + let ctxt = match pending_ident.kind { + ConcatenationScopeIdentKind::TopLevel => module_info.module_ctxt, + ConcatenationScopeIdentKind::Global => module_info.global_ctxt, + ConcatenationScopeIdentKind::UsedName => unreachable!(), + }; + add_ident_to_module_info( + module_info, + ConcatenatedModuleIdent { + id: swc_ecma_ast::Ident::new( + symbol, + Span::new( + BytePos(pending_ident.range.start.saturating_add(1)), + BytePos(pending_ident.range.end.saturating_add(1)), + ), + ctxt, + ), + shorthand: pending_ident.shorthand && !non_shorthand_ranges.contains(&pending_ident.range), + is_class_expr_with_ident: false, + }, + &mut seen, + ); + } + + for ident in &faster_info.added_scope_idents { + add_ident_to_module_info( + module_info, + ConcatenatedModuleIdent { + id: swc_ecma_ast::Ident::new( + ident.symbol.clone(), + Span::new( + BytePos(ident.range.start.saturating_add(1)), + BytePos(ident.range.end.saturating_add(1)), + ), + module_info.module_ctxt, + ), + shorthand: ident.shorthand, + is_class_expr_with_ident: ident.is_class_expr_with_ident, + }, + &mut seen, + ); + } + + for symbol in &module_info.generated_top_level_symbols { + module_info + .binding_to_ref + .entry((symbol.placeholder.clone(), module_info.module_ctxt)) + .or_default(); + } + if let Some(export_map) = &module_info.export_map { + for export in export_map.values() { + let export = Atom::from(export.as_str()); + if is_plain_identifier_name(export.as_ref()) + && !module_info.module_references.contains_key(export.as_ref()) + && ConcatenationScope::match_module_reference(export.as_ref()).is_none() + { + module_info + .binding_to_ref + .entry((export, module_info.module_ctxt)) + .or_default(); + } + } + } +} + +fn find_placeholder_replacement<'a>( + source: &str, + mut cursor: usize, + replacements: &'a PlaceholderReplacements<'_>, +) -> Option<(usize, usize, &'a str)> { + let source_bytes = source.as_bytes(); + while let Some(offset) = memchr::memchr(b'_', &source_bytes[cursor..]) { + let start = cursor + offset; + let candidate = &source[start..]; + if let Some(value) = candidate.strip_prefix(MODULE_REFERENCE_PLACEHOLDER_PREFIX) + && let Some((index, index_len)) = parse_decimal_prefix(value.as_bytes()) + && value[index_len..].starts_with(MODULE_REFERENCE_SUFFIX) + { + let end = start + + MODULE_REFERENCE_PLACEHOLDER_PREFIX.len() + + index_len + + MODULE_REFERENCE_SUFFIX.len(); + if let Some(value) = replacements.get_module_reference(index, &source[start..end]) { + return Some((start, end, value)); + } + } else if let Some(value) = candidate.strip_prefix(GENERATED_TOP_LEVEL_SYMBOL_PREFIX) + && let Some((index, index_len)) = parse_decimal_prefix(value.as_bytes()) + && value[index_len..].starts_with("__") + { + let end = start + GENERATED_TOP_LEVEL_SYMBOL_PREFIX.len() + index_len + 2; + if let Some(value) = replacements.get_generated_symbol(index, &source[start..end]) { + return Some((start, end, value)); + } + } + cursor = start + 1; + } + None +} + +fn replace_placeholders( + source: &str, + replacements: &PlaceholderReplacements<'_>, +) -> Option { + if replacements.is_empty() { + return None; + } + + let (mut start, mut end, mut value) = find_placeholder_replacement(source, 0, replacements)?; + + let mut output = String::with_capacity(source.len()); + let mut cursor = 0; + loop { + output.push_str(&source[cursor..start]); + output.push_str(value); + cursor = end; + let Some(next) = find_placeholder_replacement(source, cursor, replacements) else { + break; + }; + (start, end, value) = next; + } + output.push_str(&source[cursor..]); + Some(output) +} + +fn apply_placeholder_replacements(source: &mut String, replacements: &PlaceholderReplacements<'_>) { + if replacements.is_empty() { + return; + } + if source.starts_with(CONCATENATION_PLACEHOLDER_PREFIX) + && let Some(value) = replacements.get(source) + { + source.clear(); + source.push_str(value); + return; + } + if let Some(output) = replace_placeholders(source, replacements) { + *source = output; + } +} + +fn apply_placeholder_replacements_to_source( + mut source: ReplaceSource, + replacements: &PlaceholderReplacements<'_>, +) -> ReplaceSource { + if replacements.is_empty() { + return source; + } + // Concatenation placeholders are generated dynamically, so dependency + // templates and generators store them in owned replacement contents. + source.rewrite_replacement_contents(|content| match content { + Cow::Owned(content) => apply_placeholder_replacements(content, replacements), + Cow::Borrowed(content) => debug_assert!( + find_placeholder_replacement(content, 0, replacements).is_none(), + "concatenation placeholders should be stored in owned replacement contents" + ), + }); + source +} + +pub(super) fn render_concatenated_module_source( + info: &mut ConcatenatedModuleInfo, + module_reference_replacements: &[(String, String)], + rendered_init_fragments_hasher: Option<&mut RspackHasher>, +) -> BoxSource { + let source = info.source.take().expect("should have source"); + let fragments = info.rendered_init_fragments.take(); + let replacements = PlaceholderReplacements { + module_references: module_reference_replacements, + generated_symbols: &info.generated_top_level_symbols, + internal_names: &info.internal_names, + }; + if fragments.is_none() && replacements.is_empty() { + return source.boxed(); + } + + let rendered_source = apply_placeholder_replacements_to_source(source, &replacements); + let rendered_source = rendered_source.boxed(); + + if let Some(fragments) = fragments { + let mut start = fragments.start; + let mut end = fragments.end; + apply_placeholder_replacements(&mut start, &replacements); + apply_placeholder_replacements(&mut end, &replacements); + if let Some(hasher) = rendered_init_fragments_hasher { + info.module.hash(hasher); + RenderedInitFragments::hash_parts(&start, &end, hasher); + } + ConcatSource::new([ + RawStringSource::from(start).boxed(), + rendered_source, + RawStringSource::from(end).boxed(), + ]) + .boxed() + } else { + rendered_source + } +} diff --git a/crates/rspack_core/src/dependency/cached_const_dependency.rs b/crates/rspack_core/src/dependency/cached_const_dependency.rs index 9b26a66994e8..46a786899389 100644 --- a/crates/rspack_core/src/dependency/cached_const_dependency.rs +++ b/crates/rspack_core/src/dependency/cached_const_dependency.rs @@ -124,36 +124,52 @@ impl DependencyTemplate for CachedConstDependencyTemplate { .as_any() .downcast_ref::() .expect("CachedConstDependencyTemplate should be used for CachedConstDependency"); + let faster_module_concatenation = code_generatable_context + .compilation + .options + .experiments + .faster_module_concatenation; + let rendered_identifier = if matches!(dep.place, CachedConstDependencyPlace::Module) { + code_generatable_context.ensure_generated_top_level_symbol(dep.identifier.to_string()) + } else { + dep.identifier.to_string() + }; match dep.place { CachedConstDependencyPlace::Module => { - code_generatable_context.init_fragments.push( - NormalInitFragment::new( - format!("var {} = {};\n", dep.identifier, dep.content), - InitFragmentStage::StageConstants, - dep.place.order(), - InitFragmentKey::Const(dep.identifier.to_string()), - None, - ) - .boxed(), + let mut fragment = NormalInitFragment::new( + format!("var {rendered_identifier} = {};\n", dep.content), + InitFragmentStage::StageConstants, + dep.place.order(), + InitFragmentKey::Const(dep.identifier.to_string()), + None, ); + if faster_module_concatenation { + fragment.set_top_level_decl_symbols(vec![dep.identifier.as_ref().into()]); + } + code_generatable_context + .init_fragments + .push(fragment.boxed()); } CachedConstDependencyPlace::Chunk => { - code_generatable_context.chunk_init_fragments().push( - NormalInitFragment::new( - format!("var {} = {};\n", dep.identifier, dep.content), - InitFragmentStage::StageConstants, - dep.place.order(), - InitFragmentKey::Const(dep.identifier.to_string()), - None, - ) - .boxed(), + let mut fragment = NormalInitFragment::new( + format!("var {} = {};\n", dep.identifier, dep.content), + InitFragmentStage::StageConstants, + dep.place.order(), + InitFragmentKey::Const(dep.identifier.to_string()), + None, ); + if faster_module_concatenation { + fragment.set_top_level_decl_symbols(vec![dep.identifier.as_ref().into()]); + } + code_generatable_context + .chunk_init_fragments() + .push(fragment.boxed()); } } if let Some(range) = dep.range { - source.replace(range.start, range.end, dep.identifier.to_string(), None); + source.replace(range.start, range.end, rendered_identifier, None); } } } diff --git a/crates/rspack_core/src/dependency/const_dependency.rs b/crates/rspack_core/src/dependency/const_dependency.rs index 4f911e9c9271..1c6bce1f8c80 100644 --- a/crates/rspack_core/src/dependency/const_dependency.rs +++ b/crates/rspack_core/src/dependency/const_dependency.rs @@ -1,3 +1,4 @@ +use cow_utils::CowUtils; use rspack_cacheable::{cacheable, cacheable_dyn, with::AsRefStr}; use rspack_hash::{RspackHash, RspackHasher}; @@ -13,11 +14,22 @@ pub struct ConstDependency { pub range: DependencyRange, #[cacheable(with=AsRefStr)] pub content: Box, + /// Identifier introduced by `content` that must participate in top-level + /// deconfliction when faster module concatenation skips the codegen parse. + pub concatenation_scope_identifier: Option>, } impl ConstDependency { pub fn new(range: DependencyRange, content: Box) -> Self { - Self { range, content } + Self { + range, + content, + concatenation_scope_identifier: None, + } + } + + pub fn set_concatenation_scope_identifier(&mut self, identifier: Box) { + self.concatenation_scope_identifier = Some(identifier); } } @@ -26,6 +38,7 @@ impl RspackHash for ConstDependency { self.range.hash(state); state.write(b"|"); self.content.hash(state); + self.concatenation_scope_identifier.hash(state); } } @@ -60,18 +73,30 @@ impl DependencyTemplate for ConstDependencyTemplate { &self, dep: &dyn DependencyCodeGeneration, source: &mut TemplateReplaceSource, - _code_generatable_context: &mut TemplateContext, + code_generatable_context: &mut TemplateContext, ) { let dep = dep .as_any() .downcast_ref::() .expect("ConstDependencyTemplate should be used for ConstDependency"); - source.replace( - dep.range.start, - dep.range.end, - dep.content.to_string(), - None, - ); + let rendered_content = + if let Some(scope) = code_generatable_context.faster_concatenation_scope() { + scope.remove_original_range(dep.range); + if let Some(identifier) = &dep.concatenation_scope_identifier { + let placeholder = scope.ensure_generated_top_level_symbol(identifier); + dep + .content + .cow_replace(identifier.as_ref(), placeholder.as_ref()) + .into_owned() + } else { + scope.add_used_names_from_generated_code(&dep.content); + dep.content.to_string() + } + } else { + dep.content.to_string() + }; + + source.replace(dep.range.start, dep.range.end, rendered_content, None); } } diff --git a/crates/rspack_core/src/dependency/dependency_template.rs b/crates/rspack_core/src/dependency/dependency_template.rs index a83879d24ab7..4a878087385c 100644 --- a/crates/rspack_core/src/dependency/dependency_template.rs +++ b/crates/rspack_core/src/dependency/dependency_template.rs @@ -7,8 +7,8 @@ use rspack_sources::ReplaceSource; use rspack_util::ext::AsAny; use crate::{ - ChunkInitFragments, CodeGenerationData, Compilation, ConcatenationScope, DependencyType, Module, - ModuleCodeTemplate, ModuleInitFragments, RuntimeSpec, + ChunkInitFragments, CodeGenerationData, Compilation, ConcatenationScope, DependencyRange, + DependencyType, Module, ModuleCodeTemplate, ModuleInitFragments, RuntimeSpec, }; pub struct TemplateContext<'a, 'b, 'c> { @@ -22,6 +22,37 @@ pub struct TemplateContext<'a, 'b, 'c> { } impl TemplateContext<'_, '_, '_> { + pub fn faster_concatenation_scope(&mut self) -> Option<&mut ConcatenationScope> { + self + .concatenation_scope + .as_deref_mut() + .filter(|scope| scope.is_faster_module_concatenation()) + } + + pub fn ensure_generated_top_level_symbol(&mut self, preferred_name: impl Into) -> String { + let preferred_name = preferred_name.into(); + self + .ensure_generated_top_level_symbol_in_scope(&preferred_name) + .unwrap_or(preferred_name) + } + + pub fn ensure_generated_top_level_symbol_in_scope( + &mut self, + preferred_name: &str, + ) -> Option { + self.faster_concatenation_scope().map(|scope| { + scope + .ensure_generated_top_level_symbol(preferred_name) + .to_string() + }) + } + + pub fn remove_original_range(&mut self, range: DependencyRange) { + if let Some(scope) = self.faster_concatenation_scope() { + scope.remove_original_range(range); + } + } + pub fn chunk_init_fragments(&mut self) -> &mut ChunkInitFragments { let data_fragments = self.data.get::(); if data_fragments.is_some() { diff --git a/crates/rspack_core/src/external_module.rs b/crates/rspack_core/src/external_module.rs index d2d8065a45c8..63f3e51a963a 100644 --- a/crates/rspack_core/src/external_module.rs +++ b/crates/rspack_core/src/external_module.rs @@ -20,7 +20,7 @@ use crate::{ NAMESPACE_OBJECT_EXPORT, NormalInitFragment, RuntimeGlobals, RuntimeSpec, SourceType, StaticExportsDependency, StaticExportsSpec, UsageState, UsedExports, UsedNameItem, extract_url_and_global, impl_module_meta_info, module_update_hash, property_access, - rspack_sources::{BoxSource, RawStringSource, SourceExt}, + rspack_sources::{BoxSource, RawStringSource, ReplaceSource, SourceExt}, to_identifier, }; @@ -95,9 +95,16 @@ fn get_namespace_object_export( runtime_template: &mut ModuleCodeTemplate, ) -> String { if let Some(concatenation_scope) = concatenation_scope { - concatenation_scope.register_namespace_export(NAMESPACE_OBJECT_EXPORT); + let faster_module_concatenation = concatenation_scope.is_faster_module_concatenation(); + let namespace_export = + concatenation_scope.register_generated_namespace_export(NAMESPACE_OBJECT_EXPORT); + let rendered_namespace_export = if faster_module_concatenation { + NAMESPACE_OBJECT_EXPORT + } else { + namespace_export.as_ref() + }; format!( - "{} {NAMESPACE_OBJECT_EXPORT}", + "{} {rendered_namespace_export}", if supports_const { "const" } else { "var" } ) } else { @@ -586,7 +593,7 @@ impl ExternalModule { request: Option<&ExternalRequestValue>, external_type: &ExternalType, runtime: Option<&RuntimeSpec>, - concatenation_scope: Option<&mut ConcatenationScope>, + mut concatenation_scope: Option<&mut ConcatenationScope>, runtime_template: &mut ModuleCodeTemplate, ) -> Result<(BoxSource, ChunkInitFragments)> { let mut chunk_init_fragments: ChunkInitFragments = Default::default(); @@ -598,23 +605,39 @@ impl ExternalModule { let source = match resolved_external_type { "this" => format!( "{} = (function() {{ return {}; }}());", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), get_source_for_global_variable_external(request, external_type), ), "window" | "self" => format!( "{} = {};", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), get_source_for_global_variable_external(request, external_type) ), "global" => format!( "{} = {};", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), get_source_for_global_variable_external(request, &compilation.options.output.global_object) ), "commonjs" | "commonjs2" | "commonjs-module" | "commonjs-static" => { format!( "{} = {};", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), get_source_for_commonjs(request) ) } @@ -626,29 +649,27 @@ impl ExternalModule { .supports_node_prefix_for_core_modules(); if compilation.options.output.module { - chunk_init_fragments.push( - NormalInitFragment::new( - format!( - "import {{ createRequire as __rspack_createRequire }} from \"{}\";\n{} __rspack_createRequire_require = __rspack_createRequire({}.url);\n", - if need_prefix { "node:module" } else { "module" }, - if compilation.options.output.environment.supports_const() { - "const" - } else { - "var" - }, - compilation.options.output.import_meta_name - ), - InitFragmentStage::StageESMImports, - 0, - InitFragmentKey::ModuleExternal("node-commonjs".to_string()), - None, - ) - .with_top_level_decl_symbols(vec![ - "__rspack_createRequire".into(), - "__rspack_createRequire_require".into(), - ]) - .boxed(), + let mut fragment = NormalInitFragment::new( + format!( + "import {{ createRequire as __rspack_createRequire }} from \"{}\";\n{} __rspack_createRequire_require = __rspack_createRequire({}.url);\n", + if need_prefix { "node:module" } else { "module" }, + if compilation.options.output.environment.supports_const() { + "const" + } else { + "var" + }, + compilation.options.output.import_meta_name + ), + InitFragmentStage::StageESMImports, + 0, + InitFragmentKey::ModuleExternal("node-commonjs".to_string()), + None, ); + fragment.set_top_level_decl_symbols(vec![ + "__rspack_createRequire".into(), + "__rspack_createRequire_require".into(), + ]); + chunk_init_fragments.push(fragment.boxed()); let (request, specifiers) = if let Some(request) = request { ( json_stringify_str(request.primary()), @@ -659,14 +680,22 @@ impl ExternalModule { }; format!( "{} = __rspack_createRequire_require({}){};", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), request, specifiers ) } else { format!( "{} = {};", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), get_source_for_commonjs(request) ) } @@ -696,13 +725,21 @@ impl ExternalModule { format!( "{}{} = {};", check_external_variable, - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), external_variable ) } "import" => format!( "{} = {};", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), get_source_for_import( request, compilation, @@ -737,7 +774,11 @@ impl ExternalModule { format!( "{}{} = {};", check_external_variable, - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), external_variable ) } @@ -765,7 +806,7 @@ impl ExternalModule { } else { to_identifier(&request.primary) }; - if let Some(concatenation_scope) = concatenation_scope { + if let Some(concatenation_scope) = concatenation_scope.as_deref_mut() { let exports_info = compilation .exports_info_artifact .get_exports_info_data(&self.identifier()); @@ -961,8 +1002,11 @@ impl ExternalModule { runtime_template, ); chunk_init_fragments.extend(module_external_fragments); - let export = - get_namespace_object_export(concatenation_scope, supports_const, runtime_template); + let export = get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ); if let Some(init) = init { format!("{init}\n{export} = {expression};") } else { @@ -976,7 +1020,11 @@ impl ExternalModule { } else { format!( "{} = {};", - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), get_source_for_import( request, compilation, @@ -1006,8 +1054,11 @@ if(typeof {global} !== "undefined") return resolve(); }}, {global_str}); }}).then(function() {{ return {global}; }}); "#, - export = - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + export = get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), global = url_and_global.global, global_str = rspack_util::json_stringify_str(url_and_global.global), url_str = rspack_util::json_stringify_str(url_and_global.url), @@ -1039,12 +1090,45 @@ if(typeof {global} !== "undefined") return resolve(); format!( "{}{} = {};", check_external_variable, - get_namespace_object_export(concatenation_scope, supports_const, runtime_template), + get_namespace_object_export( + concatenation_scope.as_deref_mut(), + supports_const, + runtime_template, + ), external_variable, ) } }; - Ok((RawStringSource::from(source).boxed(), chunk_init_fragments)) + let generated_namespace_export = concatenation_scope.as_deref().and_then(|scope| { + scope + .is_faster_module_concatenation() + .then(|| { + scope + .current_module + .generated_top_level_symbols + .iter() + .find(|symbol| symbol.preferred_name == NAMESPACE_OBJECT_EXPORT) + }) + .flatten() + }); + let Some(generated_namespace_export) = generated_namespace_export else { + return Ok((RawStringSource::from(source).boxed(), chunk_init_fragments)); + }; + + let declaration_prefix = if supports_const { "const " } else { "var " }; + let declaration = format!("{declaration_prefix}{NAMESPACE_OBJECT_EXPORT}"); + let start = source + .find(&declaration) + .expect("generated namespace export should be rendered") + + declaration_prefix.len(); + let mut rendered_source = ReplaceSource::new(RawStringSource::from(source)); + rendered_source.replace( + start as u32, + start as u32 + NAMESPACE_OBJECT_EXPORT.len() as u32, + generated_namespace_export.placeholder.to_string(), + None, + ); + Ok((rendered_source.boxed(), chunk_init_fragments)) } } diff --git a/crates/rspack_core/src/init_fragment.rs b/crates/rspack_core/src/init_fragment.rs index e73b74218228..90809fa23539 100644 --- a/crates/rspack_core/src/init_fragment.rs +++ b/crates/rspack_core/src/init_fragment.rs @@ -27,6 +27,32 @@ pub struct InitFragmentContents { pub end: Option, } +#[derive(Debug, Clone, Default)] +pub struct RenderedInitFragments { + pub start: String, + pub end: String, +} + +impl RenderedInitFragments { + pub fn is_empty(&self) -> bool { + self.start.is_empty() && self.end.is_empty() + } + + pub(crate) fn hash_parts(start: &str, end: &str, state: &mut RspackHasher) { + state.write(b"RenderedInitFragments"); + state.write(&(start.len() as u64).to_be_bytes()); + state.write(start.as_bytes()); + state.write(&(end.len() as u64).to_be_bytes()); + state.write(end.as_bytes()); + } +} + +impl RspackHash for RenderedInitFragments { + fn hash(&self, state: &mut RspackHasher) { + Self::hash_parts(&self.start, &self.end, state); + } +} + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum InitFragmentKey { Unique(u32), @@ -159,8 +185,15 @@ impl InitFragmentKey { ESMExportInitFragment::new(export_argument, export_map, is_circular_module).boxed() } InitFragmentKey::AwaitDependencies => { - let promises = fragments.into_iter().map(|f| f.into_any().downcast::().expect("fragment of InitFragmentKey::AwaitDependencies should be a AwaitDependenciesInitFragment")).flat_map(|f| f.promises).collect(); - AwaitDependenciesInitFragment::new(promises).boxed() + let iter = fragments.into_iter().map(|fragment| { + fragment + .into_any() + .downcast::() + .expect( + "fragment of InitFragmentKey::AwaitDependencies should be a AwaitDependenciesInitFragment", + ) + }); + AwaitDependenciesInitFragment::merge(iter).boxed() } InitFragmentKey::ExternalModule(_) => { let mut iter = fragments.into_iter(); @@ -281,12 +314,61 @@ impl Display for InitFragmentStage { } /// InitFragment.addToSource +pub fn render_init_fragments_to_strings( + mut fragments: Vec>>, + context: &mut C, +) -> Result { + // here use sort_by_key because need keep order equal stage fragments + fragments.sort_by(|a, b| { + let stage = a.stage().cmp(&b.stage()); + if !stage.is_eq() { + return stage; + } + a.position().cmp(&b.position()) + }); + + let mut keyed_fragments: IndexMap< + InitFragmentKey, + Vec>>, + BuildHasherDefault, + > = IndexMap::default(); + for fragment in fragments { + let key = fragment.key(); + if let Some(value) = keyed_fragments.get_mut(key) { + value.push(fragment); + } else { + keyed_fragments.insert(key.clone(), vec![fragment]); + } + } + + let mut start = String::new(); + let mut end_contents = vec![]; + + for (key, fragments) in keyed_fragments { + let f = key.merge_fragments(fragments); + let contents = f.contents(context)?; + start.push_str(&contents.start); + if let Some(end_content) = contents.end { + end_contents.push(end_content) + } + } + + let mut end = String::new(); + for content in end_contents.into_iter().rev() { + end.push_str(&content); + } + + Ok(RenderedInitFragments { start, end }) +} + pub fn render_init_fragments( source: BoxSource, mut fragments: Vec>>, context: &mut C, ) -> Result { - // here use sort_by_key because need keep order equal stage fragments + // Keep the legacy source structure: each merged start/end fragment remains + // a separate RawStringSource around the module body. The faster + // concatenation path uses render_init_fragments_to_strings instead. fragments.sort_by(|a, b| { let stage = a.stage().cmp(&b.stage()); if !stage.is_eq() { @@ -313,11 +395,11 @@ pub fn render_init_fragments( let mut concat_source = ConcatSource::default(); for (key, fragments) in keyed_fragments { - let f = key.merge_fragments(fragments); - let contents = f.contents(context)?; + let fragment = key.merge_fragments(fragments); + let contents = fragment.contents(context)?; concat_source.add(RawStringSource::from(contents.start)); if let Some(end_content) = contents.end { - end_contents.push(RawStringSource::from(end_content)) + end_contents.push(RawStringSource::from(end_content)); } } @@ -390,9 +472,8 @@ impl NormalInitFragment { } } - pub fn with_top_level_decl_symbols(mut self, top_level_decl_symbols: Vec) -> Self { + pub fn set_top_level_decl_symbols(&mut self, top_level_decl_symbols: Vec) { self.top_level_decl_symbols = top_level_decl_symbols; - self } } @@ -556,22 +637,67 @@ impl InitFragment for ESMExportInitFragment { #[derive(Debug, Clone)] pub struct AwaitDependenciesInitFragment { promises: LinkedHashSet>, + binding: Option, } impl AwaitDependenciesInitFragment { + fn merge(mut fragments: impl Iterator>) -> Self { + let first = fragments + .next() + .expect("keyed_fragments should at least have one value"); + let binding = first.binding.clone(); + let mut promises = first.promises; + for fragment in fragments { + assert_eq!( + binding, fragment.binding, + "merged AwaitDependenciesInitFragments must use the same binding" + ); + promises.extend(fragment.promises); + } + Self { promises, binding } + } + pub fn new(promises: LinkedHashSet>) -> Self { - Self { promises } + Self { + promises, + binding: None, + } + } + + pub fn new_with_binding( + promises: LinkedHashSet>, + binding: String, + ) -> Self { + Self { + promises, + binding: Some(binding), + } } pub fn new_single(promise: String) -> Self { let mut promises = LinkedHashSet::default(); promises.insert(promise); - Self { promises } + Self { + promises, + binding: None, + } + } + + pub fn new_single_with_binding(promise: String, binding: String) -> Self { + let mut promises = LinkedHashSet::default(); + promises.insert(promise); + Self { + promises, + binding: Some(binding), + } } } impl RspackHash for AwaitDependenciesInitFragment { fn hash(&self, state: &mut RspackHasher) { + if let Some(binding) = &self.binding { + binding.hash(state); + } for promise in &self.promises { promise.hash(state); } @@ -586,18 +712,20 @@ impl InitFragment for AwaitDependenciesInitFrag end: None, }) } else if self.promises.len() == 1 { + let binding = self.binding.as_deref().unwrap_or("__rspack_async_deps"); let sep = self.promises.front().expect("at least have one"); Ok(InitFragmentContents { start: format!( - "var __rspack_async_deps = __rspack_load_async_deps([{sep}]);\n{sep} = (__rspack_async_deps.then ? (await __rspack_async_deps)() : __rspack_async_deps)[0];" + "var {binding} = __rspack_load_async_deps([{sep}]);\n{sep} = ({binding}.then ? (await {binding})() : {binding})[0];" ), end: None, }) } else { + let binding = self.binding.as_deref().unwrap_or("__rspack_async_deps"); let sep = Vec::from_iter(self.promises).join(", "); Ok(InitFragmentContents { start: format!( - "var __rspack_async_deps = __rspack_load_async_deps([{sep}]);\n([{sep}] = __rspack_async_deps.then ? (await __rspack_async_deps)() : __rspack_async_deps);" + "var {binding} = __rspack_load_async_deps([{sep}]);\n([{sep}] = {binding}.then ? (await {binding})() : {binding});" ), end: None, }) diff --git a/crates/rspack_core/src/module.rs b/crates/rspack_core/src/module.rs index 9f27c691cf09..32b9520cb2a9 100644 --- a/crates/rspack_core/src/module.rs +++ b/crates/rspack_core/src/module.rs @@ -38,7 +38,7 @@ use crate::{ OptimizationBailoutItem, RawModule, Resolve, ResolverFactory, RuntimeSpec, SelfModule, SharedPluginDriver, SideEffectsStateArtifact, SourceType, concatenated_module::ConcatenatedModule, dependencies_block::dependencies_block_update_hash, - get_target, value_cache_versions::ValueCacheVersions, + get_target, utils::PendingConcatenationScopeInfo, value_cache_versions::ValueCacheVersions, }; pub struct BuildContext { @@ -284,6 +284,7 @@ pub struct BuildInfo { pub side_effects_free: Option>, #[cacheable(with=AsOption>)] pub top_level_declarations: Option>, + pub pending_concatenation_scope_info: Option>, pub module_concatenation_bailout: Option, pub assets: BindingCell>, pub module: bool, @@ -321,6 +322,7 @@ impl Default for BuildInfo { css: None, side_effects_free: None, top_level_declarations: None, + pending_concatenation_scope_info: None, module_concatenation_bailout: None, assets: Default::default(), module: false, diff --git a/crates/rspack_core/src/options/experiments/mod.rs b/crates/rspack_core/src/options/experiments/mod.rs index 734e4b46f9c3..a37b5f14bc6b 100644 --- a/crates/rspack_core/src/options/experiments/mod.rs +++ b/crates/rspack_core/src/options/experiments/mod.rs @@ -29,6 +29,7 @@ pub struct Experiments { pub new_cache: bool, pub defer_import: bool, pub source_import: bool, + pub faster_module_concatenation: bool, pub pure_functions: bool, pub runtime_mode: RuntimeMode, } diff --git a/crates/rspack_core/src/utils/concatenated_module_visitor.rs b/crates/rspack_core/src/utils/concatenated_module_visitor.rs index f1e6e6cd2da5..488fd5ac9d40 100644 --- a/crates/rspack_core/src/utils/concatenated_module_visitor.rs +++ b/crates/rspack_core/src/utils/concatenated_module_visitor.rs @@ -1,8 +1,11 @@ +use rspack_cacheable::cacheable; use swc_core::ecma::{ ast::{ClassExpr, Ident, ObjectPatProp, Prop}, visit::{Visit, VisitWith, noop_visit_type}, }; +use crate::DependencyRange; + #[derive(Clone, Debug)] pub struct ConcatenatedModuleIdent { pub id: Ident, @@ -10,6 +13,30 @@ pub struct ConcatenatedModuleIdent { pub is_class_expr_with_ident: bool, } +#[cacheable] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ConcatenationScopeIdentKind { + TopLevel, + Global, + UsedName, +} + +#[cacheable] +#[derive(Clone, Debug)] +pub struct ConcatenationScopeIdent { + pub range: DependencyRange, + pub shorthand: bool, + pub kind: ConcatenationScopeIdentKind, +} + +#[cacheable] +#[derive(Clone, Debug, Default)] +pub struct PendingConcatenationScopeInfo { + pub module_ctxt: u32, + pub global_ctxt: u32, + pub idents: Vec, +} + #[derive(Default)] pub struct IdentCollector { pub ids: Vec, diff --git a/crates/rspack_core/src/utils/concatenation_scope.rs b/crates/rspack_core/src/utils/concatenation_scope.rs index 0ba313155f7a..f257f46e035f 100644 --- a/crates/rspack_core/src/utils/concatenation_scope.rs +++ b/crates/rspack_core/src/utils/concatenation_scope.rs @@ -9,17 +9,20 @@ use rspack_util::{ use swc_core::atoms::Atom; use crate::{ - ExportMode, ModuleIdentifier, - concatenated_module::{ConcatenatedModuleInfo, ModuleInfo}, + DependencyRange, ExportMode, ModuleIdentifier, PendingConcatenationScopeInfo, + concatenated_module::{ + ConcatenatedModuleInfo, FasterModuleConcatenationInfo, GENERATED_TOP_LEVEL_SYMBOL_PREFIX, + MODULE_REFERENCE_PLACEHOLDER_PREFIX, MODULE_REFERENCE_PREFIX, MODULE_REFERENCE_SUFFIX, + ModuleInfo, OriginalScopeIdentUpdate, + }, }; pub static DEFAULT_EXPORT_ATOM: LazyLock = LazyLock::new(|| "__rspack_default_export".into()); pub const NAMESPACE_OBJECT_EXPORT: &str = "__rspack_ns_object"; pub const DEFAULT_EXPORT: &str = "__rspack_default_export"; -const MODULE_REFERENCE_PREFIX: &str = "__rspack_module_ref"; const MODULE_REFERENCE_PROPERTY_ACCESS_SUFFIX: &str = "._"; -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct ModuleReferenceOptions { pub ids: Vec, pub call: bool, @@ -29,6 +32,12 @@ pub struct ModuleReferenceOptions { pub index: usize, } +#[derive(Debug, Clone)] +pub struct ConcatenatedModuleReference { + pub module: ModuleIdentifier, + pub options: ModuleReferenceOptions, +} + #[derive(Debug, Clone)] pub struct ConcatenationScope { pub concat_module_id: ModuleIdentifier, @@ -38,6 +47,7 @@ pub struct ConcatenationScope { pub refs: IdentifierIndexMap>, pub dyn_refs: IdentifierIndexMap>, pub re_exports: IdentifierIndexMap>, + faster_module_concatenation_info: Option>, } #[allow(unused)] @@ -55,9 +65,43 @@ impl ConcatenationScope { refs: IdentifierIndexMap::default(), dyn_refs: Default::default(), re_exports: Default::default(), + faster_module_concatenation_info: None, } } + pub fn enable_faster_module_concatenation(&mut self) { + self + .faster_module_concatenation_info + .get_or_insert_default(); + } + + pub(crate) fn take_faster_module_concatenation_info( + &mut self, + ) -> Option> { + self.faster_module_concatenation_info.take() + } + + pub fn is_faster_module_concatenation(&self) -> bool { + self.faster_module_concatenation_info.is_some() + } + + pub fn current_module_with_scope_info( + &self, + pending: &PendingConcatenationScopeInfo, + original_source: &str, + ) -> ConcatenatedModuleInfo { + let mut current_module = self.current_module.clone(); + if let Some(faster_info) = self.faster_module_concatenation_info.as_deref() { + crate::concatenated_module::populate_info_from_pending( + pending, + original_source, + &mut current_module, + faster_info, + ); + } + current_module + } + pub fn is_module_in_scope(&self, module: &ModuleIdentifier) -> bool { self.modules_map.contains_key(module) } @@ -70,6 +114,12 @@ impl ConcatenationScope { export_map.insert(export_name, symbol); } + pub fn register_generated_export(&mut self, export_name: Atom, preferred_name: &str) -> Atom { + let symbol = self.ensure_generated_top_level_symbol(preferred_name); + self.register_export(export_name, symbol.to_string()); + symbol + } + pub fn register_raw_export(&mut self, export_name: Atom, symbol: String) { let raw_export_map = self.current_module.raw_export_map.get_or_insert_default(); raw_export_map.insert(export_name, symbol); @@ -118,11 +168,125 @@ impl ConcatenationScope { self.current_module.namespace_export_symbol = Some(symbol.into()); } - pub fn create_module_reference( + pub fn register_generated_namespace_export(&mut self, preferred_name: &str) -> Atom { + let symbol = self.ensure_generated_top_level_symbol(preferred_name); + self.register_namespace_export(symbol.as_ref()); + symbol + } + + pub fn remove_original_range(&mut self, range: DependencyRange) { + let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { + return; + }; + info + .original_scope_ident_updates + .push(OriginalScopeIdentUpdate::Remove(range)); + } + + pub fn set_original_range_non_shorthand(&mut self, range: DependencyRange) { + let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { + return; + }; + info + .original_scope_ident_updates + .push(OriginalScopeIdentUpdate::NonShorthand(range)); + } + + pub fn add_scope_ident(&mut self, symbol: Atom, range: DependencyRange) { + let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { + return; + }; + info.added_scope_idents.push(crate::AddedScopeIdent { + symbol, + range, + shorthand: false, + is_class_expr_with_ident: false, + }); + } + + pub fn add_used_names_from_generated_code(&mut self, code: &str) { + let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { + return; + }; + // The legacy codegen parse reserves names referenced by replacement code. + // Faster concatenation skips that parse, so conservatively collect every + // ASCII identifier-shaped token. False positives only reduce name reuse; + // missing a name could capture a generated global reference. + for candidate in code.split(|ch: char| !(ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())) + { + let mut chars = candidate.chars(); + if chars + .next() + .is_some_and(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphabetic()) + { + info.added_used_names.push(candidate.into()); + } + } + } + + pub fn ensure_generated_top_level_symbol(&mut self, preferred_name: &str) -> Atom { + let preferred_name = Atom::from(preferred_name); + if !self.is_faster_module_concatenation() { + return preferred_name; + } + if let Some(existing) = self + .current_module + .generated_top_level_symbols + .iter() + .find(|symbol| symbol.preferred_name == preferred_name) + { + return existing.placeholder.clone(); + } + + // Use a verbose internal prefix to make collisions with user identifiers and + // generated data unlikely. + let placeholder = Atom::from(format!( + "{GENERATED_TOP_LEVEL_SYMBOL_PREFIX}{}__", + self.current_module.generated_top_level_symbols.len() + )); + self + .current_module + .generated_top_level_symbols + .push(crate::GeneratedTopLevelSymbol { + preferred_name, + placeholder: placeholder.clone(), + }); + placeholder + } + + fn build_module_reference( &mut self, module: &ModuleIdentifier, - options: ModuleReferenceOptions, + options: &ModuleReferenceOptions, ) -> String { + if self.is_faster_module_concatenation() { + if let Some((placeholder, _)) = self + .current_module + .module_references + .iter() + .find(|(_, reference)| reference.module == *module && reference.options == *options) + { + return placeholder.clone(); + } + + let mut index_buffer = itoa::Buffer::new(); + let index_str = index_buffer.format(self.current_module.module_references.len()); + let mut placeholder = String::with_capacity( + MODULE_REFERENCE_PLACEHOLDER_PREFIX.len() + index_str.len() + MODULE_REFERENCE_SUFFIX.len(), + ); + placeholder.push_str(MODULE_REFERENCE_PLACEHOLDER_PREFIX); + placeholder.push_str(index_str); + placeholder.push_str(MODULE_REFERENCE_SUFFIX); + self.current_module.module_references.insert( + placeholder.clone(), + ConcatenatedModuleReference { + module: *module, + options: options.clone(), + }, + ); + return placeholder; + } + let info = self .modules_map .get(module) @@ -137,7 +301,7 @@ impl ConcatenationScope { let mut index_buffer = itoa::Buffer::new(); let index_str = index_buffer.format(info.index()); let mut module_ref = String::with_capacity(index_str.len() + export_data.len() + 64); - module_ref.push_str("__rspack_module_ref"); + module_ref.push_str(MODULE_REFERENCE_PREFIX); module_ref.push_str(index_str); module_ref.push('_'); module_ref.push_str(&export_data); @@ -153,13 +317,32 @@ impl ConcatenationScope { if let Some(asi_safe) = options.asi_safe { module_ref.push_str(if asi_safe { "_asiSafe1" } else { "_asiSafe0" }); } - module_ref.push_str("__._"); - let entry = self.refs.entry(*module).or_default(); - entry.insert(module_ref.clone(), options); + module_ref.push_str(MODULE_REFERENCE_SUFFIX); + module_ref + } + pub fn create_module_reference( + &mut self, + module: &ModuleIdentifier, + options: ModuleReferenceOptions, + ) -> String { + let module_ref = self.build_module_reference(module, &options); + self + .refs + .entry(*module) + .or_default() + .insert(module_ref.clone(), options); module_ref } + pub fn create_export_reference( + &mut self, + module: &ModuleIdentifier, + options: &ModuleReferenceOptions, + ) -> String { + self.build_module_reference(module, options) + } + pub fn match_module_reference(name: &str) -> Option { let name = name .strip_suffix(MODULE_REFERENCE_PROPERTY_ACCESS_SUFFIX) @@ -269,7 +452,7 @@ mod tests { } #[test] - fn create_module_reference_round_trips_through_matcher() { + fn create_module_reference_tracks_legacy_and_faster_references() { let (mut scope, referenced_module_id) = create_test_scope(7); let options = ModuleReferenceOptions { ids: vec![Atom::from("default"), Atom::from("named")], @@ -293,9 +476,23 @@ mod tests { .expect("should parse full module reference"); let expected = ModuleReferenceOptions { index: 7, - ..options + ..options.clone() }; assert_module_reference_options_eq(&parsed, &expected); + + let (mut faster_scope, referenced_module_id) = create_test_scope(7); + faster_scope.enable_faster_module_concatenation(); + let module_ref = faster_scope.create_module_reference(&referenced_module_id, options.clone()); + assert_eq!(module_ref, "__rspack_module_reference_placeholder_0__._"); + + let reference = faster_scope + .current_module + .module_references + .get(&module_ref) + .expect("should store structured module reference"); + assert_eq!(reference.module, referenced_module_id); + assert_module_reference_options_eq(&reference.options, &options); + assert!(ConcatenationScope::match_module_reference(&module_ref).is_none()); } #[test] diff --git a/crates/rspack_plugin_asset/src/lib.rs b/crates/rspack_plugin_asset/src/lib.rs index e67028a9c614..c69c83fc316f 100644 --- a/crates/rspack_plugin_asset/src/lib.rs +++ b/crates/rspack_plugin_asset/src/lib.rs @@ -8,11 +8,11 @@ use rspack_core::{ AssetInfo, AssetParserDataUrl, BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, ChunkUkey, CodeGenerationDataAssetInfo, CodeGenerationDataFilename, CodeGenerationDataUrl, CodeGenerationPublicPathAutoReplace, Compilation, CompilationRenderManifest, CompilerOptions, - DependencyType, Filename, GenerateContext, GeneratorOptions, JavascriptParserUrl, - ManifestAssetType, Module, ModuleArgument, ModuleGraph, NAMESPACE_OBJECT_EXPORT, NormalModule, - ParseContext, ParserAndGenerator, ParserOptions, PathData, Plugin, PublicPath, - RenderManifestEntry, ResourceData, RuntimeGlobals, RuntimeSpec, SourceType, - rspack_sources::{BoxSource, RawStringSource, SourceExt}, + ConcatenationScope, DependencyType, Filename, GenerateContext, GeneratorOptions, + JavascriptParserUrl, ManifestAssetType, Module, ModuleArgument, ModuleGraph, + NAMESPACE_OBJECT_EXPORT, NormalModule, ParseContext, ParserAndGenerator, ParserOptions, PathData, + Plugin, PublicPath, RenderManifestEntry, ResourceData, RuntimeGlobals, RuntimeSpec, SourceType, + rspack_sources::{BoxSource, RawStringSource, ReplaceSource, SourceExt}, }; use rspack_error::{Diagnostic, IntoTWithDiagnosticArray, Result, error}; use rspack_hash::{RspackHash, RspackHashDigest, RspackHasher}; @@ -44,6 +44,34 @@ static ASSET_TYPES: &[SourceType; 1] = &[SourceType::Asset]; const DEFAULT_ENCODING: &str = "base64"; +fn render_concatenated_asset_source( + scope: &mut ConcatenationScope, + prefix: &str, + suffix: String, +) -> BoxSource { + let faster_module_concatenation = scope.is_faster_module_concatenation(); + let namespace_export = scope.register_generated_namespace_export(NAMESPACE_OBJECT_EXPORT); + let rendered_namespace_export = if faster_module_concatenation { + NAMESPACE_OBJECT_EXPORT + } else { + namespace_export.as_ref() + }; + let source = RawStringSource::from(format!("{prefix}{rendered_namespace_export}{suffix}")); + if !faster_module_concatenation { + return source.boxed(); + } + + let start = prefix.len() as u32; + let mut source = ReplaceSource::new(source); + source.replace( + start, + start + NAMESPACE_OBJECT_EXPORT.len() as u32, + namespace_export.to_string(), + None, + ); + source.boxed() +} + #[cacheable] #[derive(Debug, Clone)] enum DataUrlOptions { @@ -664,23 +692,20 @@ impl ParserAndGenerator for AssetParserAndGenerator { if import_mode.is_preserve() && parsed_asset_config.is_resource() { let is_module = compilation.options.output.module; if let Some(ref mut scope) = generate_context.concatenation_scope { - scope.register_namespace_export(NAMESPACE_OBJECT_EXPORT); if is_module { - return Ok( - RawStringSource::from(format!( - r#"import {NAMESPACE_OBJECT_EXPORT} from {exported_content};"# - )) - .boxed(), - ); + return Ok(render_concatenated_asset_source( + scope, + "import ", + format!(" from {exported_content};"), + )); } else { let supports_const = compilation.options.output.environment.supports_const(); let declaration_kind = if supports_const { "const" } else { "var" }; - return Ok( - RawStringSource::from(format!( - r#"{declaration_kind} {NAMESPACE_OBJECT_EXPORT} = require({exported_content});"# - )) - .boxed(), - ); + return Ok(render_concatenated_asset_source( + scope, + &format!("{declaration_kind} "), + format!(" = require({exported_content});"), + )); } } else { return Ok( @@ -696,15 +721,13 @@ impl ParserAndGenerator for AssetParserAndGenerator { }; if let Some(ref mut scope) = generate_context.concatenation_scope { - scope.register_namespace_export(NAMESPACE_OBJECT_EXPORT); let supports_const = compilation.options.output.environment.supports_const(); let declaration_kind = if supports_const { "const" } else { "var" }; - Ok( - RawStringSource::from(format!( - r#"{declaration_kind} {NAMESPACE_OBJECT_EXPORT} = {exported_content};"# - )) - .boxed(), - ) + Ok(render_concatenated_asset_source( + scope, + &format!("{declaration_kind} "), + format!(" = {exported_content};"), + )) } else { Ok( RawStringSource::from(format!( diff --git a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs index f6624bfab185..5384d28197f4 100644 --- a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs +++ b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs @@ -87,6 +87,7 @@ pub(crate) struct CssModuleGenerator<'a, 'g> { css_inject_style: Option, css_style_sheet: Option, concat_source: ConcatSource, + generated_symbol_replacements: Vec<(u32, u32, String)>, } impl<'a, 'g> CssModuleGenerator<'a, 'g> { @@ -119,6 +120,7 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { css_inject_style: None, css_style_sheet: None, concat_source: Default::default(), + generated_symbol_replacements: Vec::new(), } } @@ -211,15 +213,24 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { } } let generated_source = self.concat_source.source().into_string_lossy().into_owned(); - if self.module.get_source_map_kind().enabled() { + let source = if self.module.get_source_map_kind().enabled() { let source_name = css_javascript_source_map_module_name( self.module, &self.generate_context.compilation.options.context, ); - Ok(OriginalSource::new(generated_source, source_name).boxed()) + OriginalSource::new(generated_source, source_name).boxed() } else { - Ok(RawStringSource::from(generated_source).boxed()) + RawStringSource::from(generated_source).boxed() + }; + if self.generated_symbol_replacements.is_empty() { + return Ok(source); + } + + let mut source = ReplaceSource::new(source); + for (start, end, symbol) in self.generated_symbol_replacements { + source.replace(start, end, symbol, None); } + Ok(source.boxed()) } fn generate_js_exports(&mut self) -> Result<()> { @@ -758,13 +769,26 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { i += 1; } - let export_source = concat_string!("var ", identifier, " = ", content, ";\n"); - self.concat_source.add(RawStringSource::from(export_source)); - state.used_identifiers.insert(identifier.clone()); let Some(ref mut scope) = self.generate_context.concatenation_scope else { unreachable!(); }; - scope.register_export(key.into(), identifier); + if scope.is_faster_module_concatenation() { + state.used_identifiers.insert(identifier.clone()); + let symbol = scope.ensure_generated_top_level_symbol(&identifier); + scope.register_export(key.into(), symbol.to_string()); + let start = self.concat_source.size() as u32 + "var ".len() as u32; + let end = start + identifier.len() as u32; + let export_source = concat_string!("var ", identifier, " = ", content, ";\n"); + self.concat_source.add(RawStringSource::from(export_source)); + self + .generated_symbol_replacements + .push((start, end, symbol.to_string())); + } else { + let export_source = concat_string!("var ", identifier, " = ", content, ";\n"); + self.concat_source.add(RawStringSource::from(export_source)); + state.used_identifiers.insert(identifier.clone()); + scope.register_export(key.into(), identifier); + } } fn render_css_export_content(&mut self, elements: &FxIndexSet) -> String { diff --git a/crates/rspack_plugin_esm_library/src/link.rs b/crates/rspack_plugin_esm_library/src/link.rs index 8137b64ba36f..5764614f9cac 100644 --- a/crates/rspack_plugin_esm_library/src/link.rs +++ b/crates/rspack_plugin_esm_library/src/link.rs @@ -3815,18 +3815,19 @@ mod tests { #[test] fn module_external_non_namespace_init_fragment_claims_top_level_decls() { - let init_fragments: ChunkInitFragments = vec![Box::new(rspack_core::NormalInitFragment::new( + let mut fragment = rspack_core::NormalInitFragment::new( "import { createRequire as __rspack_createRequire } from \"node:module\";\nconst __rspack_createRequire_require = __rspack_createRequire(import.meta.url);\n" .into(), rspack_core::InitFragmentStage::StageESMImports, 0, InitFragmentKey::ModuleExternal("node-commonjs".into()), None, - ) - .with_top_level_decl_symbols(vec![ + ); + fragment.set_top_level_decl_symbols(vec![ "__rspack_createRequire".into(), "__rspack_createRequire_require".into(), - ]))]; + ]); + let init_fragments: ChunkInitFragments = vec![Box::new(fragment)]; let mut chunk_used_names = FxHashSet::default(); EsmLibraryPlugin::reserve_module_external_top_level_decls( @@ -3840,32 +3841,31 @@ mod tests { #[test] fn module_external_top_level_decls_keep_first_rendered_fragment() { - let init_fragments: ChunkInitFragments = vec![ - Box::new(rspack_core::NormalInitFragment::new( - "import { createRequire as __rspack_createRequire_1 } from \"node:module\";\nconst __rspack_createRequire_require_1 = __rspack_createRequire_1(import.meta.url);\n" - .into(), - rspack_core::InitFragmentStage::StageESMImports, - 1, - InitFragmentKey::ModuleExternal("node-commonjs".into()), - None, - ) - .with_top_level_decl_symbols(vec![ - "__rspack_createRequire_1".into(), - "__rspack_createRequire_require_1".into(), - ])), - Box::new(rspack_core::NormalInitFragment::new( - "import { createRequire as __rspack_createRequire_0 } from \"node:module\";\nconst __rspack_createRequire_require_0 = __rspack_createRequire_0(import.meta.url);\n" - .into(), - rspack_core::InitFragmentStage::StageESMImports, - 0, - InitFragmentKey::ModuleExternal("node-commonjs".into()), - None, - ) - .with_top_level_decl_symbols(vec![ - "__rspack_createRequire_0".into(), - "__rspack_createRequire_require_0".into(), - ])), - ]; + let mut fragment_1 = rspack_core::NormalInitFragment::new( + "import { createRequire as __rspack_createRequire_1 } from \"node:module\";\nconst __rspack_createRequire_require_1 = __rspack_createRequire_1(import.meta.url);\n" + .into(), + rspack_core::InitFragmentStage::StageESMImports, + 1, + InitFragmentKey::ModuleExternal("node-commonjs".into()), + None, + ); + fragment_1.set_top_level_decl_symbols(vec![ + "__rspack_createRequire_1".into(), + "__rspack_createRequire_require_1".into(), + ]); + let mut fragment_0 = rspack_core::NormalInitFragment::new( + "import { createRequire as __rspack_createRequire_0 } from \"node:module\";\nconst __rspack_createRequire_require_0 = __rspack_createRequire_0(import.meta.url);\n" + .into(), + rspack_core::InitFragmentStage::StageESMImports, + 0, + InitFragmentKey::ModuleExternal("node-commonjs".into()), + None, + ); + fragment_0.set_top_level_decl_symbols(vec![ + "__rspack_createRequire_0".into(), + "__rspack_createRequire_require_0".into(), + ]); + let init_fragments: ChunkInitFragments = vec![Box::new(fragment_1), Box::new(fragment_0)]; let mut chunk_used_names = FxHashSet::default(); EsmLibraryPlugin::reserve_module_external_top_level_decls( @@ -3881,17 +3881,16 @@ mod tests { #[test] fn module_external_var_init_fragment_claims_top_level_decl() { - let init_fragments: ChunkInitFragments = vec![Box::new( - rspack_core::NormalInitFragment::new( - "/* provided dependency */ var provided_identifier = __webpack_require__(\"./dep\");\n" - .into(), - rspack_core::InitFragmentStage::StageProvides, - 1, - InitFragmentKey::ModuleExternal("provided provided_identifier".into()), - None, - ) - .with_top_level_decl_symbols(vec!["provided_identifier".into()]), - )]; + let mut fragment = rspack_core::NormalInitFragment::new( + "/* provided dependency */ var provided_identifier = __webpack_require__(\"./dep\");\n" + .into(), + rspack_core::InitFragmentStage::StageProvides, + 1, + InitFragmentKey::ModuleExternal("provided provided_identifier".into()), + None, + ); + fragment.set_top_level_decl_symbols(vec!["provided_identifier".into()]); + let init_fragments: ChunkInitFragments = vec![Box::new(fragment)]; let mut chunk_used_names = FxHashSet::default(); EsmLibraryPlugin::reserve_module_external_top_level_decls( diff --git a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs index 37c24e64466b..3c034e829192 100644 --- a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs @@ -70,6 +70,7 @@ impl DependencyTemplate for RequireHeaderDependencyTemplate { .downcast_ref::() .expect("RequireHeaderDependencyTemplate should only be used for RequireHeaderDependency"); + code_generatable_context.remove_original_range(dep.range); let TemplateContext { runtime_template, .. } = code_generatable_context; diff --git a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs index 22d112a6bec2..ea91670f6a53 100644 --- a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs @@ -63,13 +63,14 @@ impl DependencyTemplate for RequireResolveHeaderDependencyTemplate { &self, dep: &dyn DependencyCodeGeneration, source: &mut TemplateReplaceSource, - _code_generatable_context: &mut TemplateContext, + code_generatable_context: &mut TemplateContext, ) { let dep = dep .as_any() .downcast_ref::() .expect("RequireResolveHeaderDependencyTemplate should only be used for RequireResolveHeaderDependency"); + code_generatable_context.remove_original_range(dep.range); source.replace_static(dep.range.start, dep.range.end, "/*require.resolve*/", None); } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs index 33d62d6f08f9..2d40aa9138f9 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs @@ -18,6 +18,20 @@ use crate::{ConstValue, parser_plugin::JS_DEFAULT_KEYWORD}; pub enum DeclarationId { Id(String), Func(DeclarationInfo), + Named(NamedDeclarationInfo), +} + +#[cacheable] +#[derive(Debug, Clone)] +pub struct NamedDeclarationInfo { + name: String, + range: DependencyRange, +} + +impl NamedDeclarationInfo { + pub fn new(name: String, range: DependencyRange) -> Self { + Self { name, range } + } } #[cacheable] @@ -180,20 +194,34 @@ impl DependencyTemplate for ESMExportExpressionDependencyTemplate { if let Some(declaration) = &dep.declaration { let name = match declaration { - DeclarationId::Id(id) => id, + DeclarationId::Id(id) => id.clone(), + DeclarationId::Named(id) => { + if let Some(scope) = concatenation_scope.as_mut() { + scope.add_scope_ident(id.name.clone().into(), id.range); + } + id.name.clone() + } DeclarationId::Func(func) => { + let generated_name = concatenation_scope.as_mut().map_or_else( + || DEFAULT_EXPORT.to_string(), + |scope| { + scope + .ensure_generated_top_level_symbol(DEFAULT_EXPORT) + .to_string() + }, + ); source.replace( func.range.start, func.range.end, - format!("{}{}{}", func.prefix, DEFAULT_EXPORT, func.suffix), + format!("{}{}{}", func.prefix, generated_name, func.suffix), None, ); - DEFAULT_EXPORT + generated_name } }; if let Some(scope) = concatenation_scope { - scope.register_export(JS_DEFAULT_KEYWORD.clone(), name.to_string()); + scope.register_export(JS_DEFAULT_KEYWORD.clone(), name); } else if let Some(used) = compilation .exports_info_artifact .get_exports_info_data(&module_identifier) @@ -231,10 +259,12 @@ impl DependencyTemplate for ESMExportExpressionDependencyTemplate { // 'var' is a little bit incorrect as TDZ is not correct, but we can't use 'const' let supports_const = compilation.options.output.environment.supports_const(); let content = if let Some(scope) = concatenation_scope { - scope.register_export(JS_DEFAULT_KEYWORD.clone(), DEFAULT_EXPORT.to_string()); + let generated_name = + scope.register_generated_export(JS_DEFAULT_KEYWORD.clone(), DEFAULT_EXPORT); format!( - "/* export default */ {} {DEFAULT_EXPORT} = ", - if supports_const { "const" } else { "var" } + "/* export default */ {} {} = ", + if supports_const { "const" } else { "var" }, + generated_name ) } else if let Some(used) = compilation .exports_info_artifact diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs index b8a256587e16..3654abe0714f 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs @@ -76,7 +76,7 @@ impl DependencyTemplate for ESMExportHeaderDependencyTemplate { &self, dep: &dyn DependencyCodeGeneration, source: &mut TemplateReplaceSource, - _code_generatable_context: &mut TemplateContext, + code_generatable_context: &mut TemplateContext, ) { let dep = dep .as_any() @@ -84,15 +84,13 @@ impl DependencyTemplate for ESMExportHeaderDependencyTemplate { .expect( "ESMExportHeaderDependencyTemplate should only be used for ESMExportHeaderDependency", ); - source.replace_static( - dep.range.start, - if let Some(range) = &dep.range_decl { - range.start - } else { - dep.range.end - }, - "", - None, - ); + let replacement_end = if let Some(range) = &dep.range_decl { + range.start + } else { + dep.range.end + }; + code_generatable_context + .remove_original_range(DependencyRange::new(dep.range.start, replacement_end)); + source.replace_static(dep.range.start, replacement_end, "", None); } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs index ed0a70ccd483..ab6a72bd9d9a 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs @@ -19,11 +19,12 @@ use rspack_core::{ ExportsInfoData, ExportsOfExportsSpec, ExportsSpec, ExportsType, FactorizeInfo, ForwardId, ImportAttributes, ImportPhase, InitFragmentExt, InitFragmentKey, InitFragmentStage, JavascriptParserOptions, LazyUntil, ModuleDependency, ModuleGraph, ModuleGraphCacheArtifact, - ModuleIdentifier, NormalInitFragment, NormalReexportItem, ReferencedExport, ResourceIdentifier, - RuntimeCondition, RuntimeGlobals, RuntimeSpec, SideEffectsStateArtifact, StarReexportsInfo, - TemplateContext, TemplateReplaceSource, UsageState, UsedName, collect_referenced_export_items, - create_exports_object_referenced, create_no_exports_referenced, filter_runtime, get_exports_type, - get_runtime_key, get_terminal_binding, property_access, property_name, + ModuleIdentifier, ModuleReferenceOptions, NormalInitFragment, NormalReexportItem, + ReferencedExport, ResourceIdentifier, RuntimeCondition, RuntimeGlobals, RuntimeSpec, + SideEffectsStateArtifact, StarReexportsInfo, TemplateContext, TemplateReplaceSource, UsageState, + UsedName, collect_referenced_export_items, create_exports_object_referenced, + create_no_exports_referenced, filter_runtime, get_exports_type, get_runtime_key, + get_terminal_binding, property_access, property_name, render_make_deferred_namespace_mode_from_exports_type, to_normal_comment, }; use rspack_error::{Diagnostic, Error, Severity}; @@ -520,6 +521,7 @@ impl ESMExportImportedSpecifierDependency { self.phase, runtime, ); + let import_var = ctxt.ensure_generated_top_level_symbol(import_var); match mode { ExportMode::Missing | ExportMode::LazyMake | ExportMode::EmptyStar(_) => { ctxt.init_fragments.push( @@ -779,38 +781,40 @@ impl ESMExportImportedSpecifierDependency { let ignored = render_dynamic_reexport_excluded(&ignored); format!("/* reexport */ {reexport}({exports}, {import_var}, {ignored});\n") } else { + let reexport_binding = ctxt.ensure_generated_top_level_symbol("__rspack_reexport"); + let import_key_binding = ctxt.ensure_generated_top_level_symbol("__rspack_import_key"); let environment = compilation.options.output.environment; let supports_arrow_function = environment.supports_arrow_function(); let supports_const = environment.supports_const(); let mut content = format!( r" -/* reexport */ var __rspack_reexport = {{}}; -/* reexport */ for( {} __rspack_import_key in {import_var}) ", - if supports_const { "const" } else { "var" } +/* reexport */ var {reexport_binding} = {{}}; +/* reexport */ for( {} {import_key_binding} in {import_var}) ", + if supports_const { "const" } else { "var" }, ); if ignored.len() > 1 { content += &format!( - "if({}.indexOf(__rspack_import_key) < 0) ", - json_stringify(&ignored) + "if({}.indexOf({import_key_binding}) < 0) ", + json_stringify(&ignored), ); } else if let Some(item) = ignored.iter().next() { content += &format!( - "if(__rspack_import_key !== {}) ", + "if({import_key_binding} !== {}) ", rspack_util::json_stringify_str(item) ); } - content += "__rspack_reexport[__rspack_import_key] ="; + content += &format!("{reexport_binding}[{import_key_binding}] ="); if supports_arrow_function && supports_const { - content += &format!("() => {import_var}[__rspack_import_key]"); + content += &format!("() => {import_var}[{import_key_binding}]"); } else { content += &format!( - "function(key) {{ return {import_var}[key]; }}.bind(0, __rspack_import_key)" + "function(key) {{ return {import_var}[key]; }}.bind(0, {import_key_binding})" ); } content += &format!( r#" -/* reexport */ {}({}, __rspack_reexport); +/* reexport */ {}({}, {reexport_binding}); "#, ctxt .runtime_template @@ -1713,7 +1717,7 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { fn render( &self, dep: &dyn DependencyCodeGeneration, - _source: &mut TemplateReplaceSource, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ) { let dep = dep @@ -1737,14 +1741,112 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { exports_info_artifact, ); - if let Some(scope) = concatenation_scope { + if concatenation_scope + .as_ref() + .is_some_and(|scope| !scope.is_faster_module_concatenation()) + { + let scope = concatenation_scope + .as_mut() + .expect("concatenation scope was checked above"); if let ExportMode::ReexportUndefined(mode) = mode { scope.register_raw_export( mode.name, String::from("/* reexport non-default export from non-ESM */ undefined"), ); + } + return; + } + + let target_module = module_graph + .module_identifier_by_dependency_id(dep.id()) + .copied(); + let can_use_concatenated_reference = concatenation_scope + .as_ref() + .zip(target_module.as_ref()) + .is_some_and(|(scope, target_module)| scope.is_module_in_scope(target_module)); + let can_handle_in_concatenation_scope = matches!( + mode, + ExportMode::Missing + | ExportMode::LazyMake + | ExportMode::Unused(_) + | ExportMode::EmptyStar(_) + | ExportMode::ReexportUndefined(_) + ) || can_use_concatenated_reference; + + if let Some(scope) = concatenation_scope + && can_handle_in_concatenation_scope + { + // `export default namespace` has a separate ConstDependency that removes the + // export prefix. Keep its unused expression so an overlapping + // PureExpressionDependency still has an operand to wrap. + let keep_unused_default_namespace_expression = matches!(&mode, ExportMode::Unused(_)) + && dep.name.as_ref().is_some_and(|name| name == "default") + && dep.get_ids(module_graph).is_empty(); + if !keep_unused_default_namespace_expression { + source.replace(dep.range.start, dep.range.end, String::new(), None); + } + scope.remove_original_range(dep.range); + + let create_reference = |scope: &mut rspack_core::ConcatenationScope, ids: Vec| { + target_module.map(|target_module| { + scope.create_export_reference( + &target_module, + &ModuleReferenceOptions { + ids, + call: false, + direct_import: false, + deferred_import: dep.phase.is_defer(), + asi_safe: None, + index: 0, + }, + ) + }) }; + match mode { + ExportMode::Missing + | ExportMode::LazyMake + | ExportMode::Unused(_) + | ExportMode::EmptyStar(_) => {} + ExportMode::ReexportUndefined(mode) => { + scope.register_raw_export( + mode.name, + String::from("/* reexport non-default export from non-ESM */ undefined"), + ); + } + ExportMode::ReexportDynamicDefault(mode) => { + if let Some(reference) = create_reference(scope, vec![Atom::from("default")]) { + scope.register_export(mode.name, reference); + } + } + ExportMode::ReexportNamedDefault(mode) => { + if let Some(reference) = create_reference(scope, vec![Atom::from("default")]) { + scope.register_export(mode.name, reference); + } + } + ExportMode::ReexportNamespaceObject(mode) => { + if let Some(reference) = create_reference(scope, vec![]) { + scope.register_export(mode.name, reference); + } + } + ExportMode::ReexportFakeNamespaceObject(mode) => { + if let Some(reference) = create_reference(scope, vec![]) { + scope.register_export(mode.name, reference); + } + } + ExportMode::NormalReexport(mode) => { + for item in mode.items { + if item.hidden { + continue; + } + if let Some(reference) = create_reference(scope, item.ids) { + scope.register_export(item.name, reference); + } + } + } + ExportMode::DynamicReexport(_) => {} + } + return; } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs index 277e9a9c4b63..c75340cd283a 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs @@ -23,6 +23,7 @@ pub struct ESMExportSpecifierDependency { name: Atom, #[cacheable(with=AsPreset)] value: Atom, // id + value_is_generated: bool, const_value: Option, enum_value: Option, } @@ -31,6 +32,7 @@ impl ESMExportSpecifierDependency { pub fn new( name: Atom, value: Atom, + value_is_generated: bool, const_value: Option, enum_value: Option, range: DependencyRange, @@ -39,6 +41,7 @@ impl ESMExportSpecifierDependency { Self { name, value, + value_is_generated, const_value, enum_value, range, @@ -167,7 +170,11 @@ impl DependencyTemplate for ESMExportSpecifierDependencyTemplate { .. } = code_generatable_context; if let Some(scope) = concatenation_scope { - scope.register_export(dep.name.clone(), dep.value.to_string()); + if dep.value_is_generated { + scope.register_generated_export(dep.name.clone(), dep.value.as_ref()); + } else { + scope.register_export(dep.name.clone(), dep.value.to_string()); + } return; } let module_graph = compilation.get_module_graph(); diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs index d35e70b9316a..619f6f0d07d3 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs @@ -115,13 +115,9 @@ pub fn esm_import_dependency_apply( phase: ImportPhase, code_generatable_context: &mut TemplateContext, ) { - let TemplateContext { - compilation, - module, - runtime, - runtime_template, - .. - } = code_generatable_context; + let compilation = code_generatable_context.compilation; + let module = code_generatable_context.module; + let runtime = code_generatable_context.runtime; // Only available when module factorization is successful. let module_graph = compilation.get_module_graph(); let module_graph_cache = &compilation.module_graph_cache_artifact; @@ -129,7 +125,7 @@ pub fn esm_import_dependency_apply( let is_target_active = if let Some(con) = connection { con.is_target_active( module_graph, - *runtime, + runtime, module_graph_cache, &compilation .build_module_graph_artifact @@ -163,7 +159,7 @@ pub fn esm_import_dependency_apply( RuntimeCondition::Boolean(false) } else if let Some(connection) = module_graph.connection_by_dependency_id(module_dependency.id()) { - filter_runtime(*runtime, |r| { + filter_runtime(runtime, |r| { connection.is_target_active( module_graph, r, @@ -183,17 +179,24 @@ pub fn esm_import_dependency_apply( target_module, module_dependency.user_request(), phase, - *runtime, + runtime, ); - let content: (String, String) = runtime_template.import_statement( - *module, + let rendered_import_var = code_generatable_context.ensure_generated_top_level_symbol(import_var); + let content: (String, String) = code_generatable_context.runtime_template.import_statement( + module, compilation, module_dependency.id(), - &import_var, + &rendered_import_var, module_dependency.request(), phase, false, ); + let is_async_module = matches!(target_module, Some(target_module) if ModuleGraph::is_async(&compilation.async_modules_artifact, &target_module.identifier())); + let async_dependencies_binding = if is_async_module { + code_generatable_context.ensure_generated_top_level_symbol_in_scope("__rspack_async_deps") + } else { + None + }; let TemplateContext { init_fragments, compilation, @@ -242,7 +245,6 @@ pub fn esm_import_dependency_apply( emitted_modules.insert(target_module, merged_runtime_condition); } - let is_async_module = matches!(target_module, Some(target_module) if ModuleGraph::is_async(&compilation.async_modules_artifact, &target_module.identifier())); if is_async_module { init_fragments.push(Box::new(ConditionalInitFragment::new( content.0, @@ -252,7 +254,17 @@ pub fn esm_import_dependency_apply( None, runtime_condition.clone(), ))); - init_fragments.push(AwaitDependenciesInitFragment::new_single(import_var).boxed()); + let await_dependencies = if let Some(binding) = async_dependencies_binding { + AwaitDependenciesInitFragment::new_single_with_binding(rendered_import_var, binding) + } else if compilation.options.experiments.faster_module_concatenation { + AwaitDependenciesInitFragment::new_single_with_binding( + rendered_import_var, + "__rspack_async_deps".to_string(), + ) + } else { + AwaitDependenciesInitFragment::new_single(rendered_import_var) + }; + init_fragments.push(await_dependencies.boxed()); init_fragments.push(Box::new(ConditionalInitFragment::new( content.1, InitFragmentStage::StageAsyncESMImports, diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs index b2479176b4e6..a2efdc6c6c13 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs @@ -511,6 +511,8 @@ impl ESMImportSpecifierDependencyTemplate { dep.phase, code_generatable_context.runtime, ); + let rendered_import_var = + code_generatable_context.ensure_generated_top_level_symbol(import_var); esm_import_dependency_apply(dep, dep.source_order, dep.phase, code_generatable_context); let TemplateContext { compilation, @@ -527,7 +529,7 @@ impl ESMImportSpecifierDependencyTemplate { *runtime, true, &dep.request, - &import_var, + &rendered_import_var, ids, &dep.id, dep.call, @@ -549,6 +551,7 @@ impl ESMImportSpecifierDependencyTemplate { let Some(con) = connection else { return; }; + code_generatable_context.remove_original_range(dep.range); let TemplateContext { runtime, module: self_module, @@ -696,6 +699,8 @@ impl DependencyTemplate for ESMImportSpecifierDependencyTemplate { let export_expr = self.get_code_for_ids(ids, dep, connection, code_generatable_context); + code_generatable_context.remove_original_range(dep.range); + if dep.shorthand { source.insert(dep.range.end, format!(": {export_expr}"), None); } else { @@ -761,12 +766,25 @@ impl DependencyTemplate for ESMImportSpecifierDependencyTemplate { let comment = to_normal_comment(prop.id.as_str()); let key = format!("{comment}{new_name}"); - let content = if prop.shorthand { - format!("{key}: {}", prop.id) + if let Some(scope) = code_generatable_context.faster_concatenation_scope() { + if prop.shorthand { + // Keep the original identifier as the local binding. Its make-time + // scope info is still needed by concatenation to deconflict the + // binding and all of its references. + scope.set_original_range_non_shorthand(prop.range); + source.insert(prop.range.start, format!("{key}: "), None); + } else { + scope.remove_original_range(prop.range); + source.replace(prop.range.start, prop.range.end, key, None); + } } else { - key - }; - source.replace(prop.range.start, prop.range.end, content, None); + let content = if prop.shorthand { + format!("{key}: {}", prop.id) + } else { + key + }; + source.replace(prop.range.start, prop.range.end, content, None); + } }); } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs index 0b783020b612..145d42301d5b 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs @@ -155,6 +155,8 @@ impl DependencyTemplate for ImportMetaRscDependencyTemplate { .as_any() .downcast_ref::() .expect("ImportMetaRscDependencyTemplate should only be used for ImportMetaRscDependency"); + let rendered_binding = + code_generatable_context.ensure_generated_top_level_symbol(IMPORT_META_RSC_BINDING); let TemplateContext { compilation, @@ -167,10 +169,9 @@ impl DependencyTemplate for ImportMetaRscDependencyTemplate { runtime_template.module_raw(compilation, dependency.id(), dependency.request(), false); let importer = json_stringify_str(&dependency.importer); - init_fragments.push(Box::new( - NormalInitFragment::new( - format!( - r#"var {IMPORT_META_RSC_BINDING} = {{ + let mut fragment = NormalInitFragment::new( + format!( + r#"var {rendered_binding} = {{ loadCss: function() {{ return (({rsc_manifest}.entryCssFiles[{importer}] || []).map(function(href) {{ return {react}.createElement("link", Object.assign({{}}, {rsc_manifest}.cssLinkProps, {{ @@ -182,25 +183,20 @@ impl DependencyTemplate for ImportMetaRscDependencyTemplate { }} }}; "# - ), - rspack_core::InitFragmentStage::StageProvides, - 0, - rspack_core::InitFragmentKey::ModuleExternal(format!( - "import.meta.rspackRsc {}", - dependency.importer - )), - None, - ) - .with_top_level_decl_symbols(vec![Atom::from(IMPORT_META_RSC_BINDING)]), - )); + ), + rspack_core::InitFragmentStage::StageProvides, + 0, + rspack_core::InitFragmentKey::ModuleExternal(format!( + "import.meta.rspackRsc {}", + dependency.importer + )), + None, + ); + fragment.set_top_level_decl_symbols(vec![Atom::from(IMPORT_META_RSC_BINDING)]); + init_fragments.push(Box::new(fragment)); if let Some(range) = dependency.range { - source.replace( - range.start, - range.end, - IMPORT_META_RSC_BINDING.to_string(), - None, - ); + source.replace(range.start, range.end, rendered_binding, None); } } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/mod.rs b/crates/rspack_plugin_javascript/src/dependency/esm/mod.rs index e23572730588..98da2d3f4163 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/mod.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/mod.rs @@ -22,7 +22,7 @@ pub use self::{ esm_compatibility_dependency::{ESMCompatibilityDependency, ESMCompatibilityDependencyTemplate}, esm_export_expression_dependency::{ DeclarationId, DeclarationInfo, ESMExportExpressionDependency, - ESMExportExpressionDependencyTemplate, + ESMExportExpressionDependencyTemplate, NamedDeclarationInfo, }, esm_export_header_dependency::{ESMExportHeaderDependency, ESMExportHeaderDependencyTemplate}, esm_export_imported_specifier_dependency::{ diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs index e69d7c9b69af..682af35eb492 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs @@ -159,6 +159,8 @@ impl DependencyTemplate for ProvideDependencyTemplate { .as_any() .downcast_ref::() .expect("ProvideDependencyTemplate should only be used for ProvideDependency"); + let rendered_identifier = + code_generatable_context.ensure_generated_top_level_symbol(dep.identifier.clone()); let TemplateContext { compilation, @@ -192,19 +194,15 @@ impl DependencyTemplate for ProvideDependencyTemplate { None => module_raw, }; - init_fragments.push(Box::new( - NormalInitFragment::new( - format!( - "/* provided dependency */ var {} = {};\n", - dep.identifier, provided_expr - ), - InitFragmentStage::StageProvides, - 1, - InitFragmentKey::ModuleExternal(format!("provided {}", dep.identifier)), - None, - ) - .with_top_level_decl_symbols(vec![dep.identifier.clone().into()]), - )); - source.replace(dep.range.start, dep.range.end, dep.identifier.clone(), None); + let mut fragment = NormalInitFragment::new( + format!("/* provided dependency */ var {rendered_identifier} = {provided_expr};\n"), + InitFragmentStage::StageProvides, + 1, + InitFragmentKey::ModuleExternal(format!("provided {}", dep.identifier)), + None, + ); + fragment.set_top_level_decl_symbols(vec![dep.identifier.clone().into()]); + init_fragments.push(Box::new(fragment)); + source.replace(dep.range.start, dep.range.end, rendered_identifier, None); } } diff --git a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs index e0c59a2b6eb0..098897bf6fc5 100644 --- a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs @@ -17,7 +17,7 @@ use rspack_core::{ ResolvedModuleOptions, RuntimeGlobals, RuntimeGlobalsRenderMode, RuntimeVariable, SideEffectsBailoutItem, SourceType, TemplateContext, TemplateReplaceSource, diagnostics::map_box_diagnostics_to_module_parse_diagnostics, - remove_bom, render_init_fragments, + remove_bom, render_init_fragments, render_init_fragments_to_strings, rspack_sources::{BoxSource, ReplaceSource, Source, SourceExt}, }; use rspack_error::{Diagnostic, Error, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray}; @@ -32,7 +32,10 @@ use swc_experimental_ecma_transforms_base::remove_paren::remove_paren; use crate::{ BoxJavascriptParserPlugin, dependency::ESMCompatibilityDependency, - visitors::{ParsedJavaScriptAst, ScanDependenciesResult, scan_dependencies, semicolon}, + visitors::{ + ParsedJavaScriptAst, ScanDependenciesResult, + concatenate_scope_info::PendingConcatenationScopeInfoVisitor, scan_dependencies, semicolon, + }, }; #[derive(Debug)] @@ -339,13 +342,29 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { return default_with_diagnostics(source, diagnostics); } + let faster_module_concatenation = compiler_options.experiments.faster_module_concatenation; + let uses_esm_library = faster_module_concatenation + && compiler_options + .output + .enabled_library_types + .as_ref() + .is_some_and(|types| types.iter().any(|ty| ty == "modern-module")); + let may_need_concatenation_scope = faster_module_concatenation + && matches!(program, Program::Module(_)) + && (uses_esm_library || compiler_options.optimization.concatenate_modules); + let mut semicolons = Default::default(); remove_paren(&mut program, &allocator, Some(&mut comments)); let semantic = resolver(&program); - program.visit_with(&mut semicolon::InsertedSemicolons::new( - &mut semicolons, - &tokens, - )); + let mut semicolon_visitor = semicolon::InsertedSemicolons::new(&mut semicolons, &tokens); + program.visit_with(&mut semicolon_visitor); + let pending_concatenation_scope_info_visitor = if may_need_concatenation_scope { + let mut visitor = PendingConcatenationScopeInfoVisitor::new(&semantic); + program.visit_with(&mut visitor); + Some(visitor) + } else { + None + }; let parsed_ast = ParsedJavaScriptAst { allocator: &allocator, comments: &comments, @@ -381,12 +400,25 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { Ok(result) => result, Err(mut e) => { diagnostics.append(&mut e); + drop(pending_concatenation_scope_info_visitor); return default_with_diagnostics(source, diagnostics); } }; diagnostics.append(&mut warning_diagnostics); let mut side_effects_bailout = None; + let pending_concatenation_scope_info = if build_meta.esm() + && (uses_esm_library + || (compiler_options.optimization.concatenate_modules + && build_info.module_concatenation_bailout.is_none())) + { + pending_concatenation_scope_info_visitor.map(|visitor| Box::new(visitor.into_info())) + } else { + drop(pending_concatenation_scope_info_visitor); + None + }; + build_info.pending_concatenation_scope_info = pending_concatenation_scope_info; + if compiler_options.optimization.side_effects.is_true() { let has_side_effects = side_effects_item.is_some(); build_meta.set_side_effect_free(!has_side_effects); @@ -428,41 +460,57 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { let mut source = ReplaceSource::new(source.clone()); let compilation = generate_context.compilation; let mut init_fragments = vec![]; - let mut context = TemplateContext { - compilation, - module, - init_fragments: &mut init_fragments, - runtime: generate_context.runtime, - concatenation_scope: generate_context.concatenation_scope.take(), - data: generate_context.data, - runtime_template: generate_context.runtime_template, - }; - - module.get_dependencies().iter().for_each(|dependency_id| { - self.source_dependency(compilation, dependency_id, &mut source, &mut context) - }); - - if let Some(dependencies) = module.get_presentational_dependencies() { - dependencies.iter().for_each(|dependency| { - if let Some(template) = dependency - .dependency_template() - .and_then(|template_type| compilation.get_dependency_template(template_type)) - { - template.render(dependency.as_ref(), &mut source, &mut context) - } else { - panic!( - "Can not find dependency template of {:?}", - dependency.dependency_template() - ); - } + let (concatenation_scope, is_concatenated_codegen) = { + let mut context = TemplateContext { + compilation, + module, + init_fragments: &mut init_fragments, + runtime: generate_context.runtime, + concatenation_scope: generate_context.concatenation_scope.take(), + data: generate_context.data, + runtime_template: generate_context.runtime_template, + }; + + module.get_dependencies().iter().for_each(|dependency_id| { + self.source_dependency(compilation, dependency_id, &mut source, &mut context) }); - }; - module - .get_blocks() - .iter() - .for_each(|block_id| self.source_block(compilation, block_id, &mut source, &mut context)); - generate_context.concatenation_scope = context.concatenation_scope.take(); + if let Some(dependencies) = module.get_presentational_dependencies() { + dependencies.iter().for_each(|dependency| { + if let Some(template) = dependency + .dependency_template() + .and_then(|template_type| compilation.get_dependency_template(template_type)) + { + template.render(dependency.as_ref(), &mut source, &mut context) + } else { + panic!( + "Can not find dependency template of {:?}", + dependency.dependency_template() + ); + } + }); + }; + + module + .get_blocks() + .iter() + .for_each(|block_id| self.source_block(compilation, block_id, &mut source, &mut context)); + let concatenation_scope = context.concatenation_scope.take(); + let is_concatenated_codegen = concatenation_scope + .as_ref() + .is_some_and(|scope| scope.is_faster_module_concatenation()); + (concatenation_scope, is_concatenated_codegen) + }; + if is_concatenated_codegen { + let rendered_fragments = + render_init_fragments_to_strings(init_fragments, generate_context)?; + if !rendered_fragments.is_empty() { + generate_context.data.insert(rendered_fragments); + } + generate_context.concatenation_scope = concatenation_scope; + return Ok(source.boxed()); + } + generate_context.concatenation_scope = concatenation_scope; render_init_fragments(source.boxed(), init_fragments, generate_context) } else { panic!( diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/compatibility_plugin.rs b/crates/rspack_plugin_javascript/src/parser_plugin/compatibility_plugin.rs index 8398be214504..735ddb490a03 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/compatibility_plugin.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/compatibility_plugin.rs @@ -83,6 +83,10 @@ impl CompatibilityPlugin { parser: &mut JavascriptParser, name: &Atom, ) -> Option { + let faster_module_concatenation = parser + .compiler_options + .experiments + .faster_module_concatenation; let (nested_name, dep) = { let data = parser.get_tag_data_mut::(name, NESTED_IDENTIFIER_TAG)?; let nested_name = Atom::from(data.name.as_str()); @@ -94,7 +98,11 @@ impl CompatibilityPlugin { let dep = if data.update { None } else { - Some(ConstDependency::new(data.loc, content)) + let mut dep = ConstDependency::new(data.loc, content); + if faster_module_concatenation { + dep.set_concatenation_scope_identifier(data.name.clone().into()); + } + Some(dep) }; data.update = true; (nested_name, dep) @@ -237,6 +245,10 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for CompatibilityPlugin { declarator: &VarDeclarator, _stmt: VariableDeclaration<'_>, ) -> Option { + let faster_module_concatenation = parser + .compiler_options + .experiments + .faster_module_concatenation; if let Some(ident) = declarator.name.as_ident() && (ident.id.sym.as_str() == parser.parser_runtime_requirements.exports || ident.id.sym.as_str() == self.nested_require_name(parser)) @@ -246,9 +258,12 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for CompatibilityPlugin { NESTED_IDENTIFIER_TAG, )?; if !data.update { - let dep = Box::new(ConstDependency::new(data.loc, data.name.clone().into())); + let mut dep = ConstDependency::new(data.loc, data.name.clone().into()); + if faster_module_concatenation { + dep.set_concatenation_scope_identifier(data.name.clone().into()); + } data.update = true; - parser.add_presentational_dependency(dep); + parser.add_presentational_dependency(Box::new(dep)); } } None @@ -263,6 +278,10 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for CompatibilityPlugin { if for_name != NESTED_IDENTIFIER_TAG { return None; } + let faster_module_concatenation = parser + .compiler_options + .experiments + .faster_module_concatenation; let tag_info = parser .definitions_db .expect_get_mut_tag_info(parser.current_tag_info?) @@ -274,25 +293,29 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for CompatibilityPlugin { let name = nested_require_data.name.clone(); if !nested_require_data.update { let shorthand = nested_require_data.in_short_hand; - deps.push(Box::new(ConstDependency::new( - nested_require_data.loc, - if shorthand { - format!("{}: {}", ident.sym, name).into() - } else { - name.clone().into() - }, - ))); + let content = if shorthand { + format!("{}: {}", ident.sym, name).into() + } else { + name.clone().into() + }; + let mut dep = ConstDependency::new(nested_require_data.loc, content); + if faster_module_concatenation { + dep.set_concatenation_scope_identifier(name.clone().into()); + } + deps.push(Box::new(dep)); nested_require_data.update = true; } - deps.push(Box::new(ConstDependency::new( - ident.span.into(), - if parser.in_short_hand { - format!("{}: {}", ident.sym, name).into() - } else { - name.into() - }, - ))); + let content = if parser.in_short_hand { + format!("{}: {}", ident.sym, name).into() + } else { + name.clone().into() + }; + let mut dep = ConstDependency::new(ident.span.into(), content); + if faster_module_concatenation { + dep.set_concatenation_scope_identifier(name.into()); + } + deps.push(Box::new(dep)); parser.add_presentational_dependencies(deps); Some(true) } diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/esm_export_dependency_parser_plugin.rs b/crates/rspack_plugin_javascript/src/parser_plugin/esm_export_dependency_parser_plugin.rs index e5c4f330759f..67d864c30e2d 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/esm_export_dependency_parser_plugin.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/esm_export_dependency_parser_plugin.rs @@ -17,7 +17,7 @@ use crate::{ dependency::{ DeclarationId, DeclarationInfo, ESMExportExpressionDependency, ESMExportHeaderDependency, ESMExportImportedSpecifierDependency, ESMExportSpecifierDependency, - ESMImportSideEffectDependency, + ESMImportSideEffectDependency, NamedDeclarationInfo, }, parser_plugin::compatibility_plugin::CompatibilityPlugin, utils::object_properties::get_attributes, @@ -188,16 +188,18 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for ESMExportDependencyParserPlugin .as_ref() .and_then(|info| info.exported_enums.get(local_id).cloned()); let variable = CompatibilityPlugin::update_nested_binding_declaration(parser, local_id); + let (value, value_is_generated) = if let Some(variable) = variable { + (variable, true) + } else { + (local_id.clone(), false) + }; let range = DependencyRange::from(statement.span()); let loc = parser.to_dependency_location(range); Box::new(ESMExportSpecifierDependency::new( export_name.clone(), - if let Some(variable) = variable { - variable - } else { - local_id.clone() - }, + value, + value_is_generated, const_value, enum_value, statement.span().into(), @@ -329,10 +331,20 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for ESMExportDependencyParserPlugin ), ))) } - ExportDefaultExpression::ClassDecl(c) => c - .ident - .as_ref() - .map(|ident| DeclarationId::Id(ident.sym.to_string())), + ExportDefaultExpression::ClassDecl(c) => c.ident.as_ref().map(|ident| { + if parser + .compiler_options + .experiments + .faster_module_concatenation + { + DeclarationId::Named(NamedDeclarationInfo::new( + ident.sym.to_string(), + ident.span.into(), + )) + } else { + DeclarationId::Id(ident.sym.to_string()) + } + }), ExportDefaultExpression::Expr(_) => None, }; let const_value = match expr { diff --git a/crates/rspack_plugin_javascript/src/plugin/api_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/api_plugin.rs index d7fef23784d5..adc535ee77d2 100644 --- a/crates/rspack_plugin_javascript/src/plugin/api_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/api_plugin.rs @@ -47,29 +47,27 @@ async fn render_module_content( .environment .supports_node_prefix_for_core_modules(); - init_fragments.push( - NormalInitFragment::new( - format!( - "import {{ createRequire as __rspack_createRequire }} from \"{}\";\n{} __rspack_createRequire_require = __rspack_createRequire({}.url);\n", - if need_prefix { "node:module" } else { "module" }, - if compilation.options.output.environment.supports_const() { - "const" - } else { - "var" - }, - compilation.options.output.import_meta_name - ), - InitFragmentStage::StageESMImports, - 0, - InitFragmentKey::ModuleExternal("node-commonjs".to_string()), - None, - ) - .with_top_level_decl_symbols(vec![ - "__rspack_createRequire".into(), - "__rspack_createRequire_require".into(), - ]) - .boxed(), + let mut fragment = NormalInitFragment::new( + format!( + "import {{ createRequire as __rspack_createRequire }} from \"{}\";\n{} __rspack_createRequire_require = __rspack_createRequire({}.url);\n", + if need_prefix { "node:module" } else { "module" }, + if compilation.options.output.environment.supports_const() { + "const" + } else { + "var" + }, + compilation.options.output.import_meta_name + ), + InitFragmentStage::StageESMImports, + 0, + InitFragmentKey::ModuleExternal("node-commonjs".to_string()), + None, ); + fragment.set_top_level_decl_symbols(vec![ + "__rspack_createRequire".into(), + "__rspack_createRequire_require".into(), + ]); + init_fragments.push(fragment.boxed()); } Ok(()) } diff --git a/crates/rspack_plugin_javascript/src/plugin/url_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/url_plugin.rs index 3cc313c3e79c..6845c09d69b3 100644 --- a/crates/rspack_plugin_javascript/src/plugin/url_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/url_plugin.rs @@ -214,6 +214,17 @@ async fn render_module_content( .code_generation_results .get(&module.identifier(), Some(runtime)); if codegen_result.data.contains::() { + // Fast ESM concatenation applies snapshot spans in the original source + // coordinate space. Defer static URL materialization to the final chunk + // render so this hook does not wrap the module's ReplaceSource and shift + // those spans. + if codegen_result + .concatenation_scope + .as_ref() + .is_some_and(|scope| scope.is_faster_module_concatenation()) + { + return Ok(()); + } let output_path = get_chunk_output_path(compilation, *chunk_ukey).await?; render_source.source = replace_static_url_placeholders( compilation, diff --git a/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs b/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs new file mode 100644 index 000000000000..8aab23264b52 --- /dev/null +++ b/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs @@ -0,0 +1,168 @@ +use rspack_core::{ + ConcatenationScopeIdent, ConcatenationScopeIdentKind, DependencyRange, + PendingConcatenationScopeInfo, +}; +use rustc_hash::FxHashSet; +use smallvec::SmallVec; +use swc_experimental_allocator::atom::Atom as AstAtom; +use swc_experimental_ecma_ast::{ClassExpr, Ident, ObjectPatProp, Prop, Visit, VisitWith}; +use swc_experimental_ecma_semantic::resolver::Semantic; + +#[derive(Clone, Copy)] +struct PendingConcatenationScopeIdent { + range: DependencyRange, + shorthand: bool, +} + +enum UsedNames<'ast> { + Inline(SmallVec<[AstAtom<'ast>; 8]>), + Heap(FxHashSet>), +} + +impl Default for UsedNames<'_> { + fn default() -> Self { + Self::Inline(SmallVec::new()) + } +} + +impl<'ast> UsedNames<'ast> { + fn insert(&mut self, name: AstAtom<'ast>) -> bool { + match self { + Self::Inline(names) => { + if names.contains(&name) { + return false; + } + if names.len() < names.inline_size() { + names.push(name); + return true; + } + + let mut names_set = + FxHashSet::with_capacity_and_hasher(names.len() * 2, Default::default()); + names_set.extend(names.drain(..)); + names_set.insert(name); + *self = Self::Heap(names_set); + true + } + Self::Heap(names) => names.insert(name), + } + } +} + +pub(crate) struct PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { + semantic: &'semantic Semantic, + top_level_idents: SmallVec<[PendingConcatenationScopeIdent; 8]>, + global_idents: SmallVec<[PendingConcatenationScopeIdent; 4]>, + used_name_set: UsedNames<'ast>, + used_names: SmallVec<[(AstAtom<'ast>, DependencyRange); 8]>, +} + +impl<'semantic, 'ast> PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { + pub(crate) fn new(semantic: &'semantic Semantic) -> Self { + Self { + semantic, + top_level_idents: SmallVec::new(), + global_idents: SmallVec::new(), + used_name_set: UsedNames::default(), + used_names: SmallVec::new(), + } + } + + #[inline] + fn add_ident(&mut self, ident: &Ident<'ast>, shorthand: bool, class_expr_with_ident: bool) { + if ident.symbol_id.get().is_none() { + return; + } + let scope = self.semantic.node_scope(ident); + let range = ident.span.into(); + if scope == self.semantic.unresolved_scope_id() { + self + .global_idents + .push(PendingConcatenationScopeIdent { range, shorthand }); + } else if class_expr_with_ident || scope != self.semantic.top_level_scope_id() { + if self.used_name_set.insert(ident.sym) { + self.used_names.push((ident.sym, range)); + } + } else { + self + .top_level_idents + .push(PendingConcatenationScopeIdent { range, shorthand }); + } + } + + pub(crate) fn into_info(self) -> PendingConcatenationScopeInfo { + let mut idents = Vec::with_capacity( + self.top_level_idents.len() + self.global_idents.len() + self.used_names.len(), + ); + idents.extend( + self + .top_level_idents + .into_iter() + .map(|ident| ConcatenationScopeIdent { + range: ident.range, + shorthand: ident.shorthand, + kind: ConcatenationScopeIdentKind::TopLevel, + }), + ); + idents.extend( + self + .global_idents + .into_iter() + .map(|ident| ConcatenationScopeIdent { + range: ident.range, + shorthand: ident.shorthand, + kind: ConcatenationScopeIdentKind::Global, + }), + ); + idents.extend( + self + .used_names + .into_iter() + .map(|(_, range)| ConcatenationScopeIdent { + range, + shorthand: false, + kind: ConcatenationScopeIdentKind::UsedName, + }), + ); + PendingConcatenationScopeInfo { + module_ctxt: self.semantic.top_level_scope_id().raw(), + global_ctxt: self.semantic.unresolved_scope_id().raw(), + idents, + } + } +} + +impl<'ast> Visit<'ast> for PendingConcatenationScopeInfoVisitor<'_, 'ast> { + fn visit_ident(&mut self, ident: &Ident<'ast>) { + self.add_ident(ident, false, false); + } + + fn visit_object_pat_prop(&mut self, prop: &ObjectPatProp<'ast>) { + match prop { + ObjectPatProp::Assign(assign) => { + self.add_ident(&assign.key.id, true, false); + assign.value.visit_with(self); + } + ObjectPatProp::KeyValue(_) | ObjectPatProp::Rest(_) => prop.visit_children_with(self), + } + } + + fn visit_prop(&mut self, prop: &Prop<'ast>) { + if let Prop::Shorthand(ident) = prop { + self.add_ident(ident, true, false); + } else { + prop.visit_children_with(self); + } + } + + fn visit_class_expr(&mut self, class_expr: &ClassExpr<'ast>) { + if let Some(ident) = &class_expr.ident + && class_expr.class.super_class.is_some() + { + self.add_ident(ident, false, true); + class_expr.class.visit_with(self); + } else { + class_expr.visit_children_with(self); + } + } +} diff --git a/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs b/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs index 06a8d7d35c0d..55b8a17b2030 100644 --- a/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs +++ b/crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs @@ -663,6 +663,10 @@ impl<'parser> JavascriptParser<'parser> { } } + pub fn compiler_options(&self) -> &CompilerOptions { + self.compiler_options + } + pub fn add_dependency(&mut self, mut dep: BoxDependency) { if let Some(guard) = &self.current_branch_guard { guard.bind_dependency(dep.as_mut()); diff --git a/crates/rspack_plugin_javascript/src/visitors/mod.rs b/crates/rspack_plugin_javascript/src/visitors/mod.rs index e779d06a2b15..b9a17e13637c 100644 --- a/crates/rspack_plugin_javascript/src/visitors/mod.rs +++ b/crates/rspack_plugin_javascript/src/visitors/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod concatenate_scope_info; mod dependency; pub mod scope_info; pub mod semicolon; diff --git a/crates/rspack_plugin_json/src/lib.rs b/crates/rspack_plugin_json/src/lib.rs index 908ef9e09892..a4da50f93941 100644 --- a/crates/rspack_plugin_json/src/lib.rs +++ b/crates/rspack_plugin_json/src/lib.rs @@ -17,7 +17,7 @@ use rspack_core::{ ParseOption, ParserAndGenerator, ParserOptions, Plugin, RuntimeSpec, SourceType, UsageState, UsedNameItem, diagnostics::ModuleParseError, - rspack_sources::{BoxSource, RawStringSource, Source, SourceExt}, + rspack_sources::{BoxSource, RawStringSource, ReplaceSource, Source, SourceExt}, }; use rspack_error::{Error, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray, error}; use rspack_util::{itoa, location::byte_line_column_to_offset}; @@ -217,18 +217,39 @@ impl ParserAndGenerator for JsonParserAndGenerator { } else { json_str.cow_replace("\"__proto__\":", "[\"__proto__\"]:") }; - let content = if let Some(scope) = concatenation_scope { - scope.register_namespace_export(NAMESPACE_OBJECT_EXPORT); - format!("var {NAMESPACE_OBJECT_EXPORT} = {json_expr}") - } else { - format!( + if let Some(scope) = concatenation_scope { + let faster_module_concatenation = scope.is_faster_module_concatenation(); + let namespace_export = scope.register_generated_namespace_export(NAMESPACE_OBJECT_EXPORT); + let rendered_namespace_export = if faster_module_concatenation { + NAMESPACE_OBJECT_EXPORT + } else { + namespace_export.as_ref() + }; + let content = format!("var {rendered_namespace_export} = {json_expr}"); + let source = RawStringSource::from(content); + if faster_module_concatenation { + let start = "var ".len() as u32; + let mut source = ReplaceSource::new(source); + source.replace( + start, + start + NAMESPACE_OBJECT_EXPORT.len() as u32, + namespace_export.to_string(), + None, + ); + return Ok(source.boxed()); + } + return Ok(source.boxed()); + } + + Ok( + RawStringSource::from(format!( r#"{}.exports = {json_expr}"#, generate_context .runtime_template .render_module_argument(ModuleArgument::Module) - ) - }; - Ok(RawStringSource::from(content).boxed()) + )) + .boxed(), + ) } _ => panic!( "Unsupported source type: {:?}", diff --git a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs index ad8e58f922f6..b11ccddd60ed 100644 --- a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs +++ b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs @@ -1,8 +1,8 @@ use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_core::{ - AsContextDependency, AsModuleDependency, DependencyCodeGeneration, DependencyTemplate, - DependencyTemplateType, DependencyType, InitFragmentExt, InitFragmentKey, InitFragmentStage, - NormalInitFragment, TemplateContext, TemplateReplaceSource, + AsContextDependency, AsModuleDependency, DependencyCodeGeneration, DependencyRange, + DependencyTemplate, DependencyTemplateType, DependencyType, InitFragmentExt, InitFragmentKey, + InitFragmentStage, NormalInitFragment, TemplateContext, TemplateReplaceSource, }; use rspack_util::json_stringify; @@ -17,11 +17,12 @@ pub enum NameType { #[derive(Debug, Clone)] pub struct ModulePathNameDependency { r#type: NameType, + range: Option, } impl ModulePathNameDependency { - pub fn new(r#type: NameType) -> Self { - Self { r#type } + pub fn new(r#type: NameType, range: Option) -> Self { + Self { r#type, range } } } @@ -49,14 +50,10 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { fn render( &self, dep: &dyn DependencyCodeGeneration, - _source: &mut TemplateReplaceSource, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ) { - let TemplateContext { - module, - init_fragments, - .. - } = code_generatable_context; + let module = code_generatable_context.module; let m = module.as_normal_module(); if let Some(m) = m { @@ -69,9 +66,19 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { if dep.r#type == NameType::FileName { if let Some(resource_path) = resource_path { + let rendered_identifier = dep.range.and_then(|range| { + code_generatable_context.remove_original_range(range); + code_generatable_context.ensure_generated_top_level_symbol_in_scope("__filename") + }); + let identifier = rendered_identifier.as_deref().unwrap_or("__filename"); + if let (Some(range), Some(rendered_identifier)) = + (dep.range, rendered_identifier.as_ref()) + { + source.replace(range.start, range.end, rendered_identifier.clone(), None); + } let init = NormalInitFragment::new( format!( - "const __filename = {};\n", + "const {identifier} = {};\n", json_stringify(&resource_path.as_std_path()) ), InitFragmentStage::StageConstants, @@ -80,17 +87,26 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { None, ); - init_fragments.push(init.boxed()); + code_generatable_context.init_fragments.push(init.boxed()); } } else if dep.r#type == NameType::DirName && let Some(resource_path) = resource_path && let Some(parent_path) = resource_path.parent() { + let rendered_identifier = dep.range.and_then(|range| { + code_generatable_context.remove_original_range(range); + code_generatable_context.ensure_generated_top_level_symbol_in_scope("__dirname") + }); + let identifier = rendered_identifier.as_deref().unwrap_or("__dirname"); + if let (Some(range), Some(rendered_identifier)) = (dep.range, rendered_identifier.as_ref()) + { + source.replace(range.start, range.end, rendered_identifier.clone(), None); + } // If the parent path is None, we use an empty string // to avoid issues with the path being undefined. let init = NormalInitFragment::new( format!( - "const __dirname = {};\n", + "const {identifier} = {};\n", json_stringify(parent_path.as_std_path()) ), InitFragmentStage::StageConstants, @@ -99,7 +115,7 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { None, ); - init_fragments.push(init.boxed()); + code_generatable_context.init_fragments.push(init.boxed()); } } } diff --git a/crates/rspack_plugin_rstest/src/parser_plugin.rs b/crates/rspack_plugin_rstest/src/parser_plugin.rs index 8675abb81af5..af70efaa5a42 100644 --- a/crates/rspack_plugin_rstest/src/parser_plugin.rs +++ b/crates/rspack_plugin_rstest/src/parser_plugin.rs @@ -1041,21 +1041,37 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for RstestParserPlugin { fn identifier( &self, parser: &mut JavascriptParser<'p>, - _ident: &Ident, + ident: &Ident, for_name: &str, ) -> Option { if self.options.module_path_name { match for_name { DIR_NAME => { - parser.add_presentational_dependency(Box::new(ModulePathNameDependency::new( - NameType::DirName, - ))); + let range = if parser + .compiler_options() + .experiments + .faster_module_concatenation + { + Some(ident.span.into()) + } else { + None + }; + let dependency = ModulePathNameDependency::new(NameType::DirName, range); + parser.add_presentational_dependency(Box::new(dependency)); return Some(true); } FILE_NAME => { - parser.add_presentational_dependency(Box::new(ModulePathNameDependency::new( - NameType::FileName, - ))); + let range = if parser + .compiler_options() + .experiments + .faster_module_concatenation + { + Some(ident.span.into()) + } else { + None + }; + let dependency = ModulePathNameDependency::new(NameType::FileName, range); + parser.add_presentational_dependency(Box::new(dependency)); return Some(true); } _ => return None, diff --git a/crates/rspack_sources/src/replace_source.rs b/crates/rspack_sources/src/replace_source.rs index 064f71c56050..8be5fe9d86d1 100644 --- a/crates/rspack_sources/src/replace_source.rs +++ b/crates/rspack_sources/src/replace_source.rs @@ -129,6 +129,14 @@ impl ReplaceSource { &self.replacements } + /// Rewrite replacement contents without changing their ranges or ordering. + /// The callback may update owned content in place or replace borrowed content. + pub fn rewrite_replacement_contents(&mut self, mut rewrite: impl FnMut(&mut Cow<'static, str>)) { + for replacement in &mut self.replacements { + rewrite(&mut replacement.content); + } + } + /// Insert a content at start. pub fn insert(&mut self, start: u32, content: String, name: Option) { self.replace(start, start, content, name) diff --git a/crates/rspack_sources/src/source.rs b/crates/rspack_sources/src/source.rs index 897040fa4056..3a5d0ccf288b 100644 --- a/crates/rspack_sources/src/source.rs +++ b/crates/rspack_sources/src/source.rs @@ -205,12 +205,19 @@ impl Hash for dyn Source { pub trait AsAny { fn as_any(&self) -> &dyn Any; + + /// Convert an owned [`Arc`] into an [`Any`] trait object for downcasting. + fn into_any_arc(self: Arc) -> Arc; } -impl AsAny for T { +impl AsAny for T { fn as_any(&self) -> &dyn Any { self } + + fn into_any_arc(self: Arc) -> Arc { + self + } } pub trait DynEq { diff --git a/packages/rspack-test-tools/src/case/normal.ts b/packages/rspack-test-tools/src/case/normal.ts index 4940a1256065..eea61aac23ff 100644 --- a/packages/rspack-test-tools/src/case/normal.ts +++ b/packages/rspack-test-tools/src/case/normal.ts @@ -15,6 +15,10 @@ import { createRunner } from './runner'; const NORMAL_CASES_ROOT = path.resolve(__TEST_PATH__, 'normalCases'); +type TNormalCaseOptions = Partial & { + rspackOptions?: RspackOptions; +}; + const createCaseOptions = ( hot: boolean, mode?: 'development' | 'production', @@ -22,7 +26,7 @@ const createCaseOptions = ( return { clean: true, describe: false, - steps: ({ name }) => [ + steps: ({ name, rspackOptions }) => [ { config: async (context: ITestContext) => { const compiler = context.getCompiler(); @@ -39,6 +43,10 @@ const createCaseOptions = ( ['rspack.config.js', 'webpack.config.js'], options, ); + mergeRspackOptions( + options, + rspackOptions as RspackOptions | undefined, + ); overrideOptions(context, options); compiler.setOptions(options); }, @@ -68,8 +76,15 @@ const createCaseOptions = ( }; const creator = new BasicCaseCreator(createCaseOptions(false)); -export function createNormalCase(name: string, src: string, dist: string) { - creator.create(name, src, dist); +export function createNormalCase( + name: string, + src: string, + dist: string, + rspackOptions?: RspackOptions, +) { + creator.create(name, src, dist, undefined, { + rspackOptions, + } satisfies TNormalCaseOptions); } const hotCreator = new BasicCaseCreator(createCaseOptions(true)); @@ -244,3 +259,16 @@ function overrideOptions(context: ITestContext, options: RspackOptions) { }; } } + +function mergeRspackOptions(options: RspackOptions, override?: RspackOptions) { + if (!override) return; + + const { experiments, ...rest } = override; + Object.assign(options, rest); + if (experiments) { + options.experiments = { + ...options.experiments, + ...experiments, + }; + } +} diff --git a/packages/rspack/src/config/defaults.ts b/packages/rspack/src/config/defaults.ts index 6c3cc5e8da68..13b19717b653 100644 --- a/packages/rspack/src/config/defaults.ts +++ b/packages/rspack/src/config/defaults.ts @@ -283,6 +283,7 @@ const applyExperimentsDefaults = ( D(experiments, 'asyncWebAssembly', true); D(experiments, 'deferImport', false); D(experiments, 'sourceImport', false); + D(experiments, 'fasterModuleConcatenation', false); D(experiments, 'buildHttp', undefined); if (experiments.buildHttp && typeof experiments.buildHttp === 'object') { diff --git a/packages/rspack/src/config/normalization.ts b/packages/rspack/src/config/normalization.ts index b86f533d3fc6..5cb6ffef40ed 100644 --- a/packages/rspack/src/config/normalization.ts +++ b/packages/rspack/src/config/normalization.ts @@ -649,6 +649,7 @@ export interface ExperimentsNormalized { css?: boolean; futureDefaults?: boolean; newCache?: boolean; + fasterModuleConcatenation?: boolean; buildHttp?: HttpUriPluginOptions; useInputFileSystem?: false | RegExp[]; nativeWatcher?: boolean; diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index 6364e32ae482..7936ebc3949e 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -3123,6 +3123,11 @@ export type Experiments = { * @default false */ newCache?: boolean; + /** + * Enable the faster module concatenation implementation. + * @default false + */ + fasterModuleConcatenation?: boolean; /** * Enable loading of modules via HTTP/HTTPS requests. * @default false diff --git a/tests/rspack-test/FasterModuleConcatenation.test.js b/tests/rspack-test/FasterModuleConcatenation.test.js new file mode 100644 index 000000000000..3daabb94cc82 --- /dev/null +++ b/tests/rspack-test/FasterModuleConcatenation.test.js @@ -0,0 +1,35 @@ +const path = require("path"); +const { + createConfigCase, + createNormalCase, + describeByWalk +} = require("@rspack/test-tools"); + +const rspackOptions = { + experiments: { + fasterModuleConcatenation: true + } +}; + +function describeCases(caseType, caseGroup, createCase) { + describeByWalk( + __filename, + (name, src, dist) => { + createCase(`${caseType}/${name}`, src, dist, rspackOptions); + }, + { + source: path.resolve(__dirname, `${caseType}Cases/${caseGroup}`), + dist: path.resolve( + __dirname, + `./js/faster-module-concatenation/${caseType}/${caseGroup}` + ), + level: 1, + describe: (name, fn) => + describe(`${caseType}/${caseGroup}/${name}`, fn) + } + ); +} + +describeCases("normal", "scope-hoisting", createNormalCase); +describeCases("config", "concatenate-modules", createConfigCase); +describeCases("config", "scope-hoisting", createConfigCase); diff --git a/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/index.js b/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/index.js new file mode 100644 index 000000000000..69250e9539b5 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/index.js @@ -0,0 +1,6 @@ +import { value } from "./module"; + +it("should preserve placeholder-looking text in a string", () => { + expect(DEFINED_STRING).toBe("__rspack_module_ref0__._"); + expect(value).toBe(42); +}); diff --git a/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/module.js b/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/module.js new file mode 100644 index 000000000000..7a5066bcf26e --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/module.js @@ -0,0 +1,5 @@ +export let value = 42; + +if (globalThis.CHANGE_VALUE) { + value = 43; +} diff --git a/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/rspack.config.js new file mode 100644 index 000000000000..b6e8db6ff125 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/define-plugin-placeholder-string/rspack.config.js @@ -0,0 +1,13 @@ +const { DefinePlugin } = require('@rspack/core'); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + optimization: { + concatenateModules: true, + }, + plugins: [ + new DefinePlugin({ + DEFINED_STRING: JSON.stringify('__rspack_module_ref0__._'), + }), + ], +}; diff --git a/tests/rspack-test/defaultsCases/default/base.js b/tests/rspack-test/defaultsCases/default/base.js index a773656da2f3..f1fbe839134f 100644 --- a/tests/rspack-test/defaultsCases/default/base.js +++ b/tests/rspack-test/defaultsCases/default/base.js @@ -23,6 +23,7 @@ module.exports = { asyncWebAssembly: true, buildHttp: undefined, deferImport: false, + fasterModuleConcatenation: false, futureDefaults: false, newCache: false, pureFunctions: false, diff --git a/tests/rspack-test/diagnosticsCases/module-parse-failed/concatenate_parse_module/rspack.config.js b/tests/rspack-test/diagnosticsCases/module-parse-failed/concatenate_parse_module/rspack.config.js index 7e70c6d7be03..140951a8254c 100644 --- a/tests/rspack-test/diagnosticsCases/module-parse-failed/concatenate_parse_module/rspack.config.js +++ b/tests/rspack-test/diagnosticsCases/module-parse-failed/concatenate_parse_module/rspack.config.js @@ -2,6 +2,10 @@ const { rspack } = require('@rspack/core'); /** @type {import('@rspack/core').Configuration} */ module.exports = { + experiments: { + // This case verifies the diagnostic produced by the legacy concatenation reparse. + fasterModuleConcatenation: false, + }, plugins: [ new rspack.DefinePlugin({ DEFINE_VAR: '1 2 3', diff --git a/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/index.js b/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/index.js index 4bdcd462ad54..c59927d82f0b 100644 --- a/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/index.js +++ b/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/index.js @@ -1 +1,8 @@ import "../import-with-default"; +import placeholder from "./placeholder.json"; + +it("should not replace placeholder-like strings in source content", () => { + expect(placeholder).toEqual({ + value: "__rspack_generated_top_level_symbol_0__" + }); +}); diff --git a/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/placeholder.json b/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/placeholder.json new file mode 100644 index 000000000000..7b5dd7f0b5e0 --- /dev/null +++ b/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/placeholder.json @@ -0,0 +1,3 @@ +{ + "value": "__rspack_generated_top_level_symbol_0__" +} diff --git a/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/test.config.js b/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/test.config.js new file mode 100644 index 000000000000..59e948b1212b --- /dev/null +++ b/tests/rspack-test/normalCases/json/import-with-default-with-concatenation/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + optimization: { + concatenateModules: true + } +}; diff --git a/website/docs/en/config/experiments.mdx b/website/docs/en/config/experiments.mdx index cceac0fd842c..289a3fb41916 100644 --- a/website/docs/en/config/experiments.mdx +++ b/website/docs/en/config/experiments.mdx @@ -187,6 +187,23 @@ const instance = await WebAssembly.instantiate(wasmModule); For a normal WebAssembly import, Rspack instantiates the module and exposes its exports. For a source phase import, Rspack returns the compiled `WebAssembly.Module` instead, and your code is responsible for calling `WebAssembly.instantiate`. ::: +## experiments.fasterModuleConcatenation + + + +- **Type:** `boolean` +- **Default:** `false` + +Improves the speed of [module concatenation optimization](/config/optimization#optimizationconcatenatemodules) when enabled. + +```js title="rspack.config.mjs" +export default { + experiments: { + fasterModuleConcatenation: true, + }, +}; +``` + ## experiments.futureDefaults - **Type:** `boolean` diff --git a/website/docs/zh/config/experiments.mdx b/website/docs/zh/config/experiments.mdx index 7917b13dac37..6b19dd24e74a 100644 --- a/website/docs/zh/config/experiments.mdx +++ b/website/docs/zh/config/experiments.mdx @@ -188,6 +188,23 @@ const instance = await WebAssembly.instantiate(wasmModule); 普通 WebAssembly 导入会由 Rspack 实例化模块并暴露其导出;source phase 导入会返回编译后的 `WebAssembly.Module`,需要你的代码自行调用 `WebAssembly.instantiate`。 ::: +## experiments.fasterModuleConcatenation + + + +- **类型:** `boolean` +- **默认值:** `false` + +开启后可提升[模块拼接优化](/config/optimization#optimizationconcatenatemodules)的速度。 + +```js title="rspack.config.mjs" +export default { + experiments: { + fasterModuleConcatenation: true, + }, +}; +``` + ## experiments.futureDefaults - **类型:** `boolean` diff --git a/xtask/benchmark/benches/groups/bundle/util.rs b/xtask/benchmark/benches/groups/bundle/util.rs index d93bed374fec..99c8dbcc24a9 100644 --- a/xtask/benchmark/benches/groups/bundle/util.rs +++ b/xtask/benchmark/benches/groups/bundle/util.rs @@ -44,7 +44,11 @@ pub fn basic_compiler_builder(options: BuilderOptions) -> CompilerBuilder { extensions: Some(vec!["...".to_string(), ".jsx".to_string()]), ..Default::default() }) - .experiments(Experiments::builder().css(true)) + .experiments( + Experiments::builder() + .css(true) + .faster_module_concatenation(true), + ) .input_filesystem(Arc::new(NativeFileSystem::new(false))) .output_filesystem(output_filesystem); diff --git a/xtask/benchmark/benches/groups/compilation_stages.rs b/xtask/benchmark/benches/groups/compilation_stages.rs index 7a38c27e994b..c3a8da3d147d 100644 --- a/xtask/benchmark/benches/groups/compilation_stages.rs +++ b/xtask/benchmark/benches/groups/compilation_stages.rs @@ -17,10 +17,10 @@ use rspack_core::{ AssignRuntimeIdsPass, AsyncModulesArtifact, CacheOptions, ChunkByUkey, ChunkContentHash, ChunkGraph, ChunkNamedIdArtifact, ChunkUkey, CodeGenerationJob, CodeGenerationPass, Compilation, CompilationAsset, CompilationAssets, Compiler, CreateChunkAssetsPass, CreateHashPass, - CreateModuleAssetsPass, CreateModuleHashesPass, DEFAULT_DELIMITER, LogType, MangleExportsOption, - Mode, ModuleCodeGenerationContext, ModuleIdsArtifact, Optimization, OptimizeCodeGenerationPass, - OutputOptions, ProcessAssetsPass, RuntimeRequirementsPass, SideEffectsOptimizeArtifact, - SourceType, UsedExportsOption, build_chunk_graph, + CreateModuleAssetsPass, CreateModuleHashesPass, DEFAULT_DELIMITER, Experiments, LogType, + MangleExportsOption, Mode, ModuleCodeGenerationContext, ModuleIdsArtifact, Optimization, + OptimizeCodeGenerationPass, OutputOptions, ProcessAssetsPass, RuntimeRequirementsPass, + SideEffectsOptimizeArtifact, SourceType, UsedExportsOption, build_chunk_graph, build_module_graph::{build_module_graph_pass, finish_build_module_graph}, cache::Cache, incremental::IncrementalOptions, @@ -1128,6 +1128,7 @@ fn create_concatenate_stage_compiler(fs: Arc) -> Compiler { .entry("main", "/src/index.js") .input_filesystem(fs.clone()) .output_filesystem(fs) + .experiments(Experiments::builder().faster_module_concatenation(true)) .optimization( Optimization::builder() .provided_exports(true) @@ -1805,11 +1806,11 @@ async fn compute_concatenated_module_codegen( code_generation_result .runtime_requirements .extend(*runtime_template.runtime_requirements()); - code_generation_result.set_hash_for_concatenated_module( - &job.hash, + code_generation_result.set_hash( &compilation.options.output.hash_function, &compilation.options.output.hash_digest, &compilation.options.output.hash_salt, + Some(&job.hash), ); black_box(code_generation_result); generated += 1; From 8ba6dffab45bcecb70e241ff30102b8ab3f05950 Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Thu, 13 Aug 2026 16:45:59 +0800 Subject: [PATCH 02/10] fix: keep dependency template scope metadata in sync --- .../src/dependency/cached_const_dependency.rs | 2 +- .../src/dependency/const_dependency.rs | 27 ++- .../src/dependency/dependency_template.rs | 154 +++++++++++++++--- .../src/parser_and_generator/generator.rs | 30 +++- .../src/dependency/dyn_import.rs | 2 +- .../commonjs/require_header_dependency.rs | 1 - .../require_resolve_header_dependency.rs | 3 +- .../esm/esm_compatibility_dependency.rs | 5 +- .../esm/esm_export_expression_dependency.rs | 16 +- .../esm/esm_export_header_dependency.rs | 4 +- ...sm_export_imported_specifier_dependency.rs | 52 +++--- .../esm/esm_export_specifier_dependency.rs | 5 +- .../dependency/esm/esm_import_dependency.rs | 23 +-- .../esm/esm_import_specifier_dependency.rs | 30 ++-- .../esm/import_meta_rsc_dependency.rs | 3 +- .../src/dependency/esm/provide_dependency.rs | 3 +- .../src/parser_and_generator/mod.rs | 65 +++++--- .../src/dyn_import_external.rs | 4 +- .../src/module_path_name_dependency.rs | 14 +- 19 files changed, 291 insertions(+), 152 deletions(-) diff --git a/crates/rspack_core/src/dependency/cached_const_dependency.rs b/crates/rspack_core/src/dependency/cached_const_dependency.rs index 46a786899389..13ec9b16dba2 100644 --- a/crates/rspack_core/src/dependency/cached_const_dependency.rs +++ b/crates/rspack_core/src/dependency/cached_const_dependency.rs @@ -130,7 +130,7 @@ impl DependencyTemplate for CachedConstDependencyTemplate { .experiments .faster_module_concatenation; let rendered_identifier = if matches!(dep.place, CachedConstDependencyPlace::Module) { - code_generatable_context.ensure_generated_top_level_symbol(dep.identifier.to_string()) + source.ensure_generated_top_level_symbol(dep.identifier.to_string()) } else { dep.identifier.to_string() }; diff --git a/crates/rspack_core/src/dependency/const_dependency.rs b/crates/rspack_core/src/dependency/const_dependency.rs index 1c6bce1f8c80..428f0e75be82 100644 --- a/crates/rspack_core/src/dependency/const_dependency.rs +++ b/crates/rspack_core/src/dependency/const_dependency.rs @@ -73,29 +73,26 @@ impl DependencyTemplate for ConstDependencyTemplate { &self, dep: &dyn DependencyCodeGeneration, source: &mut TemplateReplaceSource, - code_generatable_context: &mut TemplateContext, + _code_generatable_context: &mut TemplateContext, ) { let dep = dep .as_any() .downcast_ref::() .expect("ConstDependencyTemplate should be used for ConstDependency"); - let rendered_content = - if let Some(scope) = code_generatable_context.faster_concatenation_scope() { - scope.remove_original_range(dep.range); - if let Some(identifier) = &dep.concatenation_scope_identifier { - let placeholder = scope.ensure_generated_top_level_symbol(identifier); - dep - .content - .cow_replace(identifier.as_ref(), placeholder.as_ref()) - .into_owned() - } else { - scope.add_used_names_from_generated_code(&dep.content); - dep.content.to_string() - } + let rendered_content = if let Some(scope) = source.faster_concatenation_scope() { + if let Some(identifier) = &dep.concatenation_scope_identifier { + let placeholder = scope.ensure_generated_top_level_symbol(identifier); + dep + .content + .cow_replace(identifier.as_ref(), placeholder.as_ref()) + .into_owned() } else { dep.content.to_string() - }; + } + } else { + dep.content.to_string() + }; source.replace(dep.range.start, dep.range.end, rendered_content, None); } diff --git a/crates/rspack_core/src/dependency/dependency_template.rs b/crates/rspack_core/src/dependency/dependency_template.rs index 4a878087385c..610b618af6bd 100644 --- a/crates/rspack_core/src/dependency/dependency_template.rs +++ b/crates/rspack_core/src/dependency/dependency_template.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use dyn_clone::{DynClone, clone_trait_object}; use rspack_cacheable::cacheable_dyn; use rspack_hash::RspackHasher; -use rspack_sources::ReplaceSource; +use rspack_sources::{ReplaceSource, ReplacementEnforce}; use rspack_util::ext::AsAny; use crate::{ @@ -11,17 +11,57 @@ use crate::{ DependencyType, Module, ModuleCodeTemplate, ModuleInitFragments, RuntimeSpec, }; -pub struct TemplateContext<'a, 'b, 'c> { +pub struct TemplateContext<'a, 'b> { pub compilation: &'a Compilation, pub module: &'a dyn Module, pub init_fragments: &'a mut ModuleInitFragments<'b>, pub runtime: Option<&'a RuntimeSpec>, - pub concatenation_scope: Option<&'c mut ConcatenationScope>, pub data: &'a mut CodeGenerationData, pub runtime_template: &'a mut ModuleCodeTemplate, } -impl TemplateContext<'_, '_, '_> { +impl TemplateContext<'_, '_> { + pub fn chunk_init_fragments(&mut self) -> &mut ChunkInitFragments { + let data_fragments = self.data.get::(); + if data_fragments.is_some() { + self + .data + .get_mut::() + .expect("should have chunk_init_fragments") + } else { + self.data.insert(ChunkInitFragments::default()); + self + .data + .get_mut::() + .expect("should have chunk_init_fragments") + } + } +} + +/// The only source mutation entry point exposed to dependency templates. +/// +/// Source edits and faster-concatenation scope updates are recorded together, +/// so a template cannot mutate one while forgetting the other. +pub struct TemplateReplaceSource<'a> { + source: &'a mut ReplaceSource, + concatenation_scope: Option<&'a mut ConcatenationScope>, +} + +impl<'a> TemplateReplaceSource<'a> { + pub fn new( + source: &'a mut ReplaceSource, + concatenation_scope: Option<&'a mut ConcatenationScope>, + ) -> Self { + Self { + source, + concatenation_scope, + } + } + + pub fn concatenation_scope(&mut self) -> Option<&mut ConcatenationScope> { + self.concatenation_scope.as_deref_mut() + } + pub fn faster_concatenation_scope(&mut self) -> Option<&mut ConcatenationScope> { self .concatenation_scope @@ -47,30 +87,102 @@ impl TemplateContext<'_, '_, '_> { }) } - pub fn remove_original_range(&mut self, range: DependencyRange) { + fn record_edit(&mut self, start: u32, end: u32, content: &str) { + let Some(scope) = self.faster_concatenation_scope() else { + return; + }; + if start != end { + scope.remove_original_range(DependencyRange::new(start, end)); + } + if !content.is_empty() { + scope.add_used_names_from_generated_code(content); + } + } + + pub fn replace(&mut self, start: u32, end: u32, content: String, name: Option) { + self.record_edit(start, end, &content); + self.source.replace(start, end, content, name); + } + + pub fn replace_static( + &mut self, + start: u32, + end: u32, + content: &'static str, + name: Option<&'static str>, + ) { + self.record_edit(start, end, content); + self.source.replace_static(start, end, content, name); + } + + pub fn replace_static_with_enforce( + &mut self, + start: u32, + end: u32, + content: &'static str, + name: Option<&'static str>, + enforce: ReplacementEnforce, + ) { + self.record_edit(start, end, content); + self + .source + .replace_static_with_enforce(start, end, content, name, enforce); + } + + pub fn insert(&mut self, start: u32, content: String, name: Option) { + self.record_edit(start, start, &content); + self.source.insert(start, content, name); + } + + pub fn insert_static(&mut self, start: u32, content: &'static str, name: Option<&'static str>) { + self.record_edit(start, start, content); + self.source.insert_static(start, content, name); + } + + /// Ignores identifiers in a range even when this template intentionally + /// leaves the source edit to an overlapping dependency. + pub fn ignore_original_scope_range(&mut self, range: DependencyRange) { if let Some(scope) = self.faster_concatenation_scope() { scope.remove_original_range(range); } } - pub fn chunk_init_fragments(&mut self) -> &mut ChunkInitFragments { - let data_fragments = self.data.get::(); - if data_fragments.is_some() { - self - .data - .get_mut::() - .expect("should have chunk_init_fragments") - } else { - self.data.insert(ChunkInitFragments::default()); - self - .data - .get_mut::() - .expect("should have chunk_init_fragments") + /// Expands an object shorthand into a key-value pair where the original + /// shorthand identifier is no longer an identifier reference. + pub fn insert_shorthand_value( + &mut self, + start: u32, + shorthand_range: DependencyRange, + content: String, + name: Option, + ) { + if let Some(scope) = self.faster_concatenation_scope() { + scope.remove_original_range(shorthand_range); + if !content.is_empty() { + scope.add_used_names_from_generated_code(&content); + } } + self.source.insert(start, content, name); } -} -pub type TemplateReplaceSource = ReplaceSource; + /// Expands an object shorthand while preserving the original identifier as + /// the value or binding that still needs concatenation-time renaming. + pub fn insert_non_shorthand( + &mut self, + start: u32, + original_range: DependencyRange, + content: String, + name: Option, + ) { + if let Some(scope) = self.faster_concatenation_scope() { + scope.set_original_range_non_shorthand(original_range); + if !content.is_empty() { + scope.add_used_names_from_generated_code(&content); + } + } + self.source.insert(start, content, name); + } +} clone_trait_object!(DependencyCodeGeneration); @@ -114,7 +226,7 @@ pub trait DependencyTemplate: Debug + Sync + Send { fn render( &self, dep: &dyn DependencyCodeGeneration, - source: &mut ReplaceSource, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ); } diff --git a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs index 5384d28197f4..32c1cb6d43ee 100644 --- a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs +++ b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs @@ -2,11 +2,11 @@ use std::{borrow::Cow, collections::VecDeque}; use concat_string::concat_string; use rspack_core::{ - BoxDependency, ChunkGraph, Context, CssBuildInfo, CssExport, CssExportType, CssExports, - CssModuleRenderCondition, DependencyCodeGeneration, DependencyId, DependencyType, + BoxDependency, ChunkGraph, ConcatenationScope, Context, CssBuildInfo, CssExport, CssExportType, + CssExports, CssModuleRenderCondition, DependencyCodeGeneration, DependencyId, DependencyType, ExportsArgument, GenerateContext, Module, ModuleArgument, ModuleIdentifier, ModuleInitFragments, - RESERVED_IDENTIFIER, RuntimeGlobals, SourceType, TemplateContext, UsageState, UsedNameItem, - css_module_render_conditions_identifier, + RESERVED_IDENTIFIER, RuntimeGlobals, SourceType, TemplateContext, TemplateReplaceSource, + UsageState, UsedNameItem, css_module_render_conditions_identifier, rspack_sources::{ BoxSource, ConcatSource, OriginalSource, RawStringSource, ReplaceSource, Source, SourceExt, }, @@ -55,12 +55,14 @@ fn render_dependency_template( dependency: &dyn DependencyCodeGeneration, source: &mut ReplaceSource, context: &mut TemplateContext, + concatenation_scope: Option<&mut ConcatenationScope>, ) { if let Some(template) = dependency .dependency_template() .and_then(|template_type| context.compilation.get_dependency_template(template_type)) { - template.render(dependency, source, context) + let mut source = TemplateReplaceSource::new(source, concatenation_scope); + template.render(dependency, &mut source, context) } else { panic!( "Can not find dependency template of {:?}", @@ -296,12 +298,12 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { let mut source = ReplaceSource::new(self.source.clone()); let compilation = self.generate_context.compilation; let mut init_fragments = ModuleInitFragments::default(); + let mut concatenation_scope = self.generate_context.concatenation_scope.take(); let mut context = TemplateContext { compilation, module: self.module, runtime: self.generate_context.runtime, init_fragments: &mut init_fragments, - concatenation_scope: self.generate_context.concatenation_scope.take(), data: self.generate_context.data, runtime_template: self.generate_context.runtime_template, }; @@ -311,17 +313,27 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { let dep = module_graph.dependency_by_id(id); if let Some(dependency) = dep.as_dependency_code_generation() { - render_dependency_template(dependency, &mut source, &mut context); + render_dependency_template( + dependency, + &mut source, + &mut context, + concatenation_scope.as_deref_mut(), + ); } }); if let Some(dependencies) = self.module.get_presentational_dependencies() { dependencies.iter().for_each(|dependency| { - render_dependency_template(dependency.as_ref(), &mut source, &mut context); + render_dependency_template( + dependency.as_ref(), + &mut source, + &mut context, + concatenation_scope.as_deref_mut(), + ); }); }; - self.generate_context.concatenation_scope = context.concatenation_scope.take(); + self.generate_context.concatenation_scope = concatenation_scope; source.boxed() } diff --git a/crates/rspack_plugin_esm_library/src/dependency/dyn_import.rs b/crates/rspack_plugin_esm_library/src/dependency/dyn_import.rs index abcb95a0807c..32f74b26abbf 100644 --- a/crates/rspack_plugin_esm_library/src/dependency/dyn_import.rs +++ b/crates/rspack_plugin_esm_library/src/dependency/dyn_import.rs @@ -317,7 +317,7 @@ impl DynamicImportDependencyTemplate { )) }; - let Some(concatenation_scope) = &mut code_generatable_context.concatenation_scope else { + let Some(concatenation_scope) = source.concatenation_scope() else { // if we are not in a concatenation scope, then all its children are not scope hoisted as well // we can safely use __rspack_require to fetch module return Some(format!( diff --git a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs index 3c034e829192..37c24e64466b 100644 --- a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_header_dependency.rs @@ -70,7 +70,6 @@ impl DependencyTemplate for RequireHeaderDependencyTemplate { .downcast_ref::() .expect("RequireHeaderDependencyTemplate should only be used for RequireHeaderDependency"); - code_generatable_context.remove_original_range(dep.range); let TemplateContext { runtime_template, .. } = code_generatable_context; diff --git a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs index ea91670f6a53..22d112a6bec2 100644 --- a/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/commonjs/require_resolve_header_dependency.rs @@ -63,14 +63,13 @@ impl DependencyTemplate for RequireResolveHeaderDependencyTemplate { &self, dep: &dyn DependencyCodeGeneration, source: &mut TemplateReplaceSource, - code_generatable_context: &mut TemplateContext, + _code_generatable_context: &mut TemplateContext, ) { let dep = dep .as_any() .downcast_ref::() .expect("RequireResolveHeaderDependencyTemplate should only be used for RequireResolveHeaderDependency"); - code_generatable_context.remove_original_range(dep.range); source.replace_static(dep.range.start, dep.range.end, "/*require.resolve*/", None); } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_compatibility_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_compatibility_dependency.rs index f519a41cc78c..865956d060dd 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_compatibility_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_compatibility_dependency.rs @@ -33,7 +33,7 @@ impl DependencyTemplate for ESMCompatibilityDependencyTemplate { fn render( &self, _dep: &dyn DependencyCodeGeneration, - _source: &mut TemplateReplaceSource, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ) { let TemplateContext { @@ -41,11 +41,10 @@ impl DependencyTemplate for ESMCompatibilityDependencyTemplate { compilation, module, runtime, - concatenation_scope, runtime_template, .. } = code_generatable_context; - if concatenation_scope.is_some() { + if source.concatenation_scope().is_some() { return; } let module_graph = compilation.get_module_graph(); diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs index 2d40aa9138f9..f9090c8e3e27 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_expression_dependency.rs @@ -182,7 +182,6 @@ impl DependencyTemplate for ESMExportExpressionDependencyTemplate { runtime, module, init_fragments, - concatenation_scope, runtime_template, .. } = code_generatable_context; @@ -196,20 +195,13 @@ impl DependencyTemplate for ESMExportExpressionDependencyTemplate { let name = match declaration { DeclarationId::Id(id) => id.clone(), DeclarationId::Named(id) => { - if let Some(scope) = concatenation_scope.as_mut() { + if let Some(scope) = source.concatenation_scope() { scope.add_scope_ident(id.name.clone().into(), id.range); } id.name.clone() } DeclarationId::Func(func) => { - let generated_name = concatenation_scope.as_mut().map_or_else( - || DEFAULT_EXPORT.to_string(), - |scope| { - scope - .ensure_generated_top_level_symbol(DEFAULT_EXPORT) - .to_string() - }, - ); + let generated_name = source.ensure_generated_top_level_symbol(DEFAULT_EXPORT); source.replace( func.range.start, func.range.end, @@ -220,7 +212,7 @@ impl DependencyTemplate for ESMExportExpressionDependencyTemplate { } }; - if let Some(scope) = concatenation_scope { + if let Some(scope) = source.concatenation_scope() { scope.register_export(JS_DEFAULT_KEYWORD.clone(), name); } else if let Some(used) = compilation .exports_info_artifact @@ -258,7 +250,7 @@ impl DependencyTemplate for ESMExportExpressionDependencyTemplate { } else { // 'var' is a little bit incorrect as TDZ is not correct, but we can't use 'const' let supports_const = compilation.options.output.environment.supports_const(); - let content = if let Some(scope) = concatenation_scope { + let content = if let Some(scope) = source.concatenation_scope() { let generated_name = scope.register_generated_export(JS_DEFAULT_KEYWORD.clone(), DEFAULT_EXPORT); format!( diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs index 3654abe0714f..a53fff9a8f41 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_header_dependency.rs @@ -76,7 +76,7 @@ impl DependencyTemplate for ESMExportHeaderDependencyTemplate { &self, dep: &dyn DependencyCodeGeneration, source: &mut TemplateReplaceSource, - code_generatable_context: &mut TemplateContext, + _code_generatable_context: &mut TemplateContext, ) { let dep = dep .as_any() @@ -89,8 +89,6 @@ impl DependencyTemplate for ESMExportHeaderDependencyTemplate { } else { dep.range.end }; - code_generatable_context - .remove_original_range(DependencyRange::new(dep.range.start, replacement_end)); source.replace_static(dep.range.start, replacement_end, "", None); } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs index ab6a72bd9d9a..f3c27979a7f6 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_imported_specifier_dependency.rs @@ -505,7 +505,12 @@ impl ESMExportImportedSpecifierDependency { None } - pub fn add_export_fragments(&self, ctxt: &mut TemplateContext, mode: ExportMode) { + pub fn add_export_fragments( + &self, + source: &mut TemplateReplaceSource, + ctxt: &mut TemplateContext, + mode: ExportMode, + ) { let module = ctxt.module; let runtime = ctxt.runtime; let compilation = ctxt.compilation; @@ -521,7 +526,7 @@ impl ESMExportImportedSpecifierDependency { self.phase, runtime, ); - let import_var = ctxt.ensure_generated_top_level_symbol(import_var); + let import_var = source.ensure_generated_top_level_symbol(import_var); match mode { ExportMode::Missing | ExportMode::LazyMake | ExportMode::EmptyStar(_) => { ctxt.init_fragments.push( @@ -781,8 +786,8 @@ impl ESMExportImportedSpecifierDependency { let ignored = render_dynamic_reexport_excluded(&ignored); format!("/* reexport */ {reexport}({exports}, {import_var}, {ignored});\n") } else { - let reexport_binding = ctxt.ensure_generated_top_level_symbol("__rspack_reexport"); - let import_key_binding = ctxt.ensure_generated_top_level_symbol("__rspack_import_key"); + let reexport_binding = source.ensure_generated_top_level_symbol("__rspack_reexport"); + let import_key_binding = source.ensure_generated_top_level_symbol("__rspack_import_key"); let environment = compilation.options.output.environment; let supports_arrow_function = environment.supports_arrow_function(); let supports_const = environment.supports_const(); @@ -1002,7 +1007,7 @@ impl ESMExportImportedSpecifierDependency { fn get_conditional_reexport_statement( &self, - ctxt: &mut TemplateContext<'_, '_, '_>, + ctxt: &mut TemplateContext<'_, '_>, key: Atom, name: &String, first_value_key: Atom, @@ -1727,7 +1732,6 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { let TemplateContext { compilation, runtime, - concatenation_scope, .. } = code_generatable_context; @@ -1741,12 +1745,12 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { exports_info_artifact, ); - if concatenation_scope - .as_ref() - .is_some_and(|scope| !scope.is_faster_module_concatenation()) - { - let scope = concatenation_scope - .as_mut() + let is_legacy_concatenation_scope = source + .concatenation_scope() + .is_some_and(|scope| !scope.is_faster_module_concatenation()); + if is_legacy_concatenation_scope { + let scope = source + .concatenation_scope() .expect("concatenation scope was checked above"); if let ExportMode::ReexportUndefined(mode) = mode { scope.register_raw_export( @@ -1760,8 +1764,8 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { let target_module = module_graph .module_identifier_by_dependency_id(dep.id()) .copied(); - let can_use_concatenated_reference = concatenation_scope - .as_ref() + let can_use_concatenated_reference = source + .concatenation_scope() .zip(target_module.as_ref()) .is_some_and(|(scope, target_module)| scope.is_module_in_scope(target_module)); let can_handle_in_concatenation_scope = matches!( @@ -1773,9 +1777,7 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { | ExportMode::ReexportUndefined(_) ) || can_use_concatenated_reference; - if let Some(scope) = concatenation_scope - && can_handle_in_concatenation_scope - { + if can_handle_in_concatenation_scope && source.concatenation_scope().is_some() { // `export default namespace` has a separate ConstDependency that removes the // export prefix. Keep its unused expression so an overlapping // PureExpressionDependency still has an operand to wrap. @@ -1784,8 +1786,12 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { && dep.get_ids(module_graph).is_empty(); if !keep_unused_default_namespace_expression { source.replace(dep.range.start, dep.range.end, String::new(), None); + } else { + source.ignore_original_scope_range(dep.range); } - scope.remove_original_range(dep.range); + let scope = source + .concatenation_scope() + .expect("concatenation scope was checked above"); let create_reference = |scope: &mut rspack_core::ConcatenationScope, ids: Vec| { target_module.map(|target_module| { @@ -1854,8 +1860,14 @@ impl DependencyTemplate for ESMExportImportedSpecifierDependencyTemplate { mode, ExportMode::LazyMake | ExportMode::Unused(_) | ExportMode::EmptyStar(_) ) { - esm_import_dependency_apply(dep, dep.source_order, dep.phase, code_generatable_context); - dep.add_export_fragments(code_generatable_context, mode); + esm_import_dependency_apply( + dep, + dep.source_order, + dep.phase, + source, + code_generatable_context, + ); + dep.add_export_fragments(source, code_generatable_context, mode); } } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs index c75340cd283a..eb99d101ca2a 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_export_specifier_dependency.rs @@ -152,7 +152,7 @@ impl DependencyTemplate for ESMExportSpecifierDependencyTemplate { fn render( &self, dep: &dyn DependencyCodeGeneration, - _source: &mut TemplateReplaceSource, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ) { let dep = dep @@ -166,10 +166,9 @@ impl DependencyTemplate for ESMExportSpecifierDependencyTemplate { compilation, module, runtime, - concatenation_scope, .. } = code_generatable_context; - if let Some(scope) = concatenation_scope { + if let Some(scope) = source.concatenation_scope() { if dep.value_is_generated { scope.register_generated_export(dep.name.clone(), dep.value.as_ref()); } else { diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs index 619f6f0d07d3..dee8ba917d86 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_dependency.rs @@ -113,6 +113,7 @@ pub fn esm_import_dependency_apply( module_dependency: &T, source_order: i32, phase: ImportPhase, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ) { let compilation = code_generatable_context.compilation; @@ -181,7 +182,7 @@ pub fn esm_import_dependency_apply( phase, runtime, ); - let rendered_import_var = code_generatable_context.ensure_generated_top_level_symbol(import_var); + let rendered_import_var = source.ensure_generated_top_level_symbol(import_var); let content: (String, String) = code_generatable_context.runtime_template.import_statement( module, compilation, @@ -193,7 +194,7 @@ pub fn esm_import_dependency_apply( ); let is_async_module = matches!(target_module, Some(target_module) if ModuleGraph::is_async(&compilation.async_modules_artifact, &target_module.identifier())); let async_dependencies_binding = if is_async_module { - code_generatable_context.ensure_generated_top_level_symbol_in_scope("__rspack_async_deps") + source.ensure_generated_top_level_symbol_in_scope("__rspack_async_deps") } else { None }; @@ -726,18 +727,14 @@ impl DependencyTemplate for ESMImportSideEffectDependencyTemplate { fn render( &self, dep: &dyn DependencyCodeGeneration, - _source: &mut TemplateReplaceSource, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ) { let dep = dep .as_any() .downcast_ref::() .expect("ESMImportSideEffectDependencyTemplate should only be used for ESMImportSideEffectDependency"); - let TemplateContext { - compilation, - concatenation_scope, - .. - } = code_generatable_context; + let compilation = code_generatable_context.compilation; let module_graph = compilation.get_module_graph(); let module = module_graph.get_module_by_dependency_id(&dep.id); @@ -756,11 +753,17 @@ impl DependencyTemplate for ESMImportSideEffectDependencyTemplate { } } - if let Some(scope) = concatenation_scope + if let Some(scope) = source.concatenation_scope() && module.is_some_and(|m| scope.is_module_in_scope(&m.identifier())) { return; } - esm_import_dependency_apply(dep, dep.source_order, dep.phase, code_generatable_context); + esm_import_dependency_apply( + dep, + dep.source_order, + dep.phase, + source, + code_generatable_context, + ); } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs index a2efdc6c6c13..d301fb03692d 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs @@ -440,15 +440,15 @@ impl ESMImportSpecifierDependencyTemplate { ids: &[Atom], dep: &ESMImportSpecifierDependency, connection: Option<&ModuleGraphConnection>, + source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, ) -> String { let TemplateContext { compilation, - concatenation_scope, runtime, .. } = code_generatable_context; - if let Some(scope) = concatenation_scope + if let Some(scope) = source.concatenation_scope() && let Some(con) = connection && scope.is_module_in_scope(con.module_identifier()) { @@ -511,9 +511,14 @@ impl ESMImportSpecifierDependencyTemplate { dep.phase, code_generatable_context.runtime, ); - let rendered_import_var = - code_generatable_context.ensure_generated_top_level_symbol(import_var); - esm_import_dependency_apply(dep, dep.source_order, dep.phase, code_generatable_context); + let rendered_import_var = source.ensure_generated_top_level_symbol(import_var); + esm_import_dependency_apply( + dep, + dep.source_order, + dep.phase, + source, + code_generatable_context, + ); let TemplateContext { compilation, module, @@ -551,7 +556,6 @@ impl ESMImportSpecifierDependencyTemplate { let Some(con) = connection else { return; }; - code_generatable_context.remove_original_range(dep.range); let TemplateContext { runtime, module: self_module, @@ -629,12 +633,14 @@ impl ESMImportSpecifierDependencyTemplate { UsedName::Inlined(_) => unreachable!("Inlined must be provided"), }) else { + source.ignore_original_scope_range(dep.range); return; }; let code = self.get_code_for_ids( &ids[..(ids.len() - 1)], dep, connection, + source, code_generatable_context, ); source.replace( @@ -697,12 +703,10 @@ impl DependencyTemplate for ESMImportSpecifierDependencyTemplate { ); } - let export_expr = self.get_code_for_ids(ids, dep, connection, code_generatable_context); - - code_generatable_context.remove_original_range(dep.range); + let export_expr = self.get_code_for_ids(ids, dep, connection, source, code_generatable_context); if dep.shorthand { - source.insert(dep.range.end, format!(": {export_expr}"), None); + source.insert_shorthand_value(dep.range.end, dep.range, format!(": {export_expr}"), None); } else { source.replace(dep.range.start, dep.range.end, export_expr, None); } @@ -766,15 +770,13 @@ impl DependencyTemplate for ESMImportSpecifierDependencyTemplate { let comment = to_normal_comment(prop.id.as_str()); let key = format!("{comment}{new_name}"); - if let Some(scope) = code_generatable_context.faster_concatenation_scope() { + if source.faster_concatenation_scope().is_some() { if prop.shorthand { // Keep the original identifier as the local binding. Its make-time // scope info is still needed by concatenation to deconflict the // binding and all of its references. - scope.set_original_range_non_shorthand(prop.range); - source.insert(prop.range.start, format!("{key}: "), None); + source.insert_non_shorthand(prop.range.start, prop.range, format!("{key}: "), None); } else { - scope.remove_original_range(prop.range); source.replace(prop.range.start, prop.range.end, key, None); } } else { diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs index 145d42301d5b..9e5b6995c4b9 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs @@ -155,8 +155,7 @@ impl DependencyTemplate for ImportMetaRscDependencyTemplate { .as_any() .downcast_ref::() .expect("ImportMetaRscDependencyTemplate should only be used for ImportMetaRscDependency"); - let rendered_binding = - code_generatable_context.ensure_generated_top_level_symbol(IMPORT_META_RSC_BINDING); + let rendered_binding = source.ensure_generated_top_level_symbol(IMPORT_META_RSC_BINDING); let TemplateContext { compilation, diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs index 682af35eb492..90f23de7f527 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs @@ -159,8 +159,7 @@ impl DependencyTemplate for ProvideDependencyTemplate { .as_any() .downcast_ref::() .expect("ProvideDependencyTemplate should only be used for ProvideDependency"); - let rendered_identifier = - code_generatable_context.ensure_generated_top_level_symbol(dep.identifier.clone()); + let rendered_identifier = source.ensure_generated_top_level_symbol(dep.identifier.clone()); let TemplateContext { compilation, diff --git a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs index 098897bf6fc5..8bd6748a0f8e 100644 --- a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs @@ -12,10 +12,10 @@ use rspack_cacheable::{ use rspack_core::{ ArcComputed, AsyncDependenciesBlockIdentifier, BuildMetaExportsType, COLLECTED_TYPESCRIPT_INFO_PARSE_META_KEY, ChunkGraph, CollectedTypeScriptInfo, Compilation, - DependenciesBlock, DependencyId, GenerateContext, ImportMeta, Module, ModuleArgument, - ModuleCodeTemplate, ModuleGraph, ModuleType, ParseContext, ParseResult, ParserAndGenerator, - ResolvedModuleOptions, RuntimeGlobals, RuntimeGlobalsRenderMode, RuntimeVariable, - SideEffectsBailoutItem, SourceType, TemplateContext, TemplateReplaceSource, + ConcatenationScope, DependenciesBlock, DependencyId, GenerateContext, ImportMeta, Module, + ModuleArgument, ModuleCodeTemplate, ModuleGraph, ModuleType, ParseContext, ParseResult, + ParserAndGenerator, ResolvedModuleOptions, RuntimeGlobals, RuntimeGlobalsRenderMode, + RuntimeVariable, SideEffectsBailoutItem, SourceType, TemplateContext, TemplateReplaceSource, diagnostics::map_box_diagnostics_to_module_parse_diagnostics, remove_bom, render_init_fragments, render_init_fragments_to_strings, rspack_sources::{BoxSource, ReplaceSource, Source, SourceExt}, @@ -179,8 +179,9 @@ impl JavaScriptParserAndGenerator { &self, compilation: &Compilation, block_id: &AsyncDependenciesBlockIdentifier, - source: &mut TemplateReplaceSource, + source: &mut ReplaceSource, context: &mut TemplateContext, + concatenation_scope: &mut Option<&mut ConcatenationScope>, ) { let module_graph = compilation.get_module_graph(); let block = module_graph @@ -188,20 +189,26 @@ impl JavaScriptParserAndGenerator { .expect("should have block"); // let block = block_id.expect_get(compilation); block.get_dependencies().iter().for_each(|dependency_id| { - self.source_dependency(compilation, dependency_id, source, context) + self.source_dependency( + compilation, + dependency_id, + source, + context, + concatenation_scope, + ) + }); + block.get_blocks().iter().for_each(|block_id| { + self.source_block(compilation, block_id, source, context, concatenation_scope) }); - block - .get_blocks() - .iter() - .for_each(|block_id| self.source_block(compilation, block_id, source, context)); } fn source_dependency( &self, compilation: &Compilation, dependency_id: &DependencyId, - source: &mut TemplateReplaceSource, + source: &mut ReplaceSource, context: &mut TemplateContext, + concatenation_scope: &mut Option<&mut ConcatenationScope>, ) { if let Some(dependency) = compilation .get_module_graph() @@ -212,7 +219,8 @@ impl JavaScriptParserAndGenerator { .dependency_template() .and_then(|template_type| compilation.get_dependency_template(template_type)) { - template.render(dependency, source, context) + let mut source = TemplateReplaceSource::new(source, concatenation_scope.as_deref_mut()); + template.render(dependency, &mut source, context) } else { panic!( "Can not find dependency template of {:?}", @@ -460,19 +468,25 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { let mut source = ReplaceSource::new(source.clone()); let compilation = generate_context.compilation; let mut init_fragments = vec![]; - let (concatenation_scope, is_concatenated_codegen) = { + let mut concatenation_scope = generate_context.concatenation_scope.take(); + let is_concatenated_codegen = { let mut context = TemplateContext { compilation, module, init_fragments: &mut init_fragments, runtime: generate_context.runtime, - concatenation_scope: generate_context.concatenation_scope.take(), data: generate_context.data, runtime_template: generate_context.runtime_template, }; module.get_dependencies().iter().for_each(|dependency_id| { - self.source_dependency(compilation, dependency_id, &mut source, &mut context) + self.source_dependency( + compilation, + dependency_id, + &mut source, + &mut context, + &mut concatenation_scope, + ) }); if let Some(dependencies) = module.get_presentational_dependencies() { @@ -481,6 +495,8 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { .dependency_template() .and_then(|template_type| compilation.get_dependency_template(template_type)) { + let mut source = + TemplateReplaceSource::new(&mut source, concatenation_scope.as_deref_mut()); template.render(dependency.as_ref(), &mut source, &mut context) } else { panic!( @@ -491,15 +507,18 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { }); }; - module - .get_blocks() - .iter() - .for_each(|block_id| self.source_block(compilation, block_id, &mut source, &mut context)); - let concatenation_scope = context.concatenation_scope.take(); - let is_concatenated_codegen = concatenation_scope + module.get_blocks().iter().for_each(|block_id| { + self.source_block( + compilation, + block_id, + &mut source, + &mut context, + &mut concatenation_scope, + ) + }); + concatenation_scope .as_ref() - .is_some_and(|scope| scope.is_faster_module_concatenation()); - (concatenation_scope, is_concatenated_codegen) + .is_some_and(|scope| scope.is_faster_module_concatenation()) }; if is_concatenated_codegen { let rendered_fragments = diff --git a/crates/rspack_plugin_rslib/src/dyn_import_external.rs b/crates/rspack_plugin_rslib/src/dyn_import_external.rs index 039c79bda9d2..4c177877a926 100644 --- a/crates/rspack_plugin_rslib/src/dyn_import_external.rs +++ b/crates/rspack_plugin_rslib/src/dyn_import_external.rs @@ -248,7 +248,7 @@ impl DependencyTemplate for ImportDependencyTemplate { fn render( &self, dep: &dyn rspack_core::DependencyCodeGeneration, - source: &mut rspack_core::rspack_sources::ReplaceSource, + source: &mut rspack_core::TemplateReplaceSource, code_generatable_context: &mut rspack_core::TemplateContext, ) { let dep = dep @@ -279,7 +279,7 @@ impl DependencyTemplate for ExportImportedDependencyTemplate { fn render( &self, dep: &dyn rspack_core::DependencyCodeGeneration, - source: &mut rspack_core::rspack_sources::ReplaceSource, + source: &mut rspack_core::TemplateReplaceSource, code_generatable_context: &mut rspack_core::TemplateContext, ) { let dep = dep diff --git a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs index b11ccddd60ed..2f12c4fc9ea8 100644 --- a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs +++ b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs @@ -66,10 +66,9 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { if dep.r#type == NameType::FileName { if let Some(resource_path) = resource_path { - let rendered_identifier = dep.range.and_then(|range| { - code_generatable_context.remove_original_range(range); - code_generatable_context.ensure_generated_top_level_symbol_in_scope("__filename") - }); + let rendered_identifier = dep + .range + .and_then(|_range| source.ensure_generated_top_level_symbol_in_scope("__filename")); let identifier = rendered_identifier.as_deref().unwrap_or("__filename"); if let (Some(range), Some(rendered_identifier)) = (dep.range, rendered_identifier.as_ref()) @@ -93,10 +92,9 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { && let Some(resource_path) = resource_path && let Some(parent_path) = resource_path.parent() { - let rendered_identifier = dep.range.and_then(|range| { - code_generatable_context.remove_original_range(range); - code_generatable_context.ensure_generated_top_level_symbol_in_scope("__dirname") - }); + let rendered_identifier = dep + .range + .and_then(|_range| source.ensure_generated_top_level_symbol_in_scope("__dirname")); let identifier = rendered_identifier.as_deref().unwrap_or("__dirname"); if let (Some(range), Some(rendered_identifier)) = (dep.range, rendered_identifier.as_ref()) { From ebac9989e48537d3ba91683522824667a308ba81 Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Thu, 13 Aug 2026 17:49:17 +0800 Subject: [PATCH 03/10] fix: validate faster concatenation scope metadata --- crates/rspack_core/src/concatenated_module.rs | 246 +++++++++--------- .../src/concatenated_module/faster.rs | 23 +- crates/rspack_core/src/external_module.rs | 27 +- crates/rspack_core/src/module.rs | 19 +- crates/rspack_core/src/normal_module.rs | 28 +- .../rspack_core/src/parser_and_generator.rs | 14 + .../src/utils/concatenated_module_visitor.rs | 16 +- crates/rspack_plugin_asset/src/lib.rs | 8 +- .../src/parser_and_generator/mod.rs | 11 +- .../src/parser_and_generator/mod.rs | 10 +- .../src/plugin/module_concatenation_plugin.rs | 40 ++- .../src/visitors/concatenate_scope_info.rs | 8 +- crates/rspack_plugin_json/src/lib.rs | 12 +- crates/rspack_plugin_rslib/src/asset.rs | 9 +- .../src/parser_and_generator.rs | 12 +- 15 files changed, 303 insertions(+), 180 deletions(-) diff --git a/crates/rspack_core/src/concatenated_module.rs b/crates/rspack_core/src/concatenated_module.rs index fa70e9f92842..80494f1a9bb9 100644 --- a/crates/rspack_core/src/concatenated_module.rs +++ b/crates/rspack_core/src/concatenated_module.rs @@ -2711,133 +2711,141 @@ impl ConcatenatedModule { .remove(&SourceType::JavaScript) .expect("should have javascript source"); let mut module_info = concatenation_scope.current_module; - let (result_source, internal_source) = - if let Some(faster_module_concatenation_info) = faster_module_concatenation_info { - // Non-JavaScript modules (for example JSON) do not have pending - // JavaScript scope info. Their generated top-level names are registered by - // dependency templates through the concatenation scope instead. - let empty_pending_scope_info = PendingConcatenationScopeInfo::default(); - let pending_scope_info = module + let (result_source, internal_source) = if let Some(faster_module_concatenation_info) = + faster_module_concatenation_info + { + let pending_scope_info = module .build_info() .pending_concatenation_scope_info - .as_deref(); - module_info.rendered_init_fragments = codegen_res - .data - .get::() - .cloned() - .filter(|fragments| !fragments.is_empty()); - // JavaScript code generation already returns a ReplaceSource whose - // coordinates are based on the make-time parser source. Keep that - // coordinate space so the pending info spans can be applied directly. The - // generated source is uniquely owned here, so recover it from the Arc - // without cloning every dependency replacement. - let result_source = if rendered_source.as_any().is::() { - let source = Arc::downcast::(rendered_source.into_any_arc()) - .expect("source type was checked above"); - Arc::try_unwrap(source).unwrap_or_else(|source| source.as_ref().clone()) - } else { - ReplaceSource::new(rendered_source) - }; + .as_deref() + .ok_or_else(|| { + rspack_error::error!( + "module {module_id} entered faster module concatenation without pending concatenation scope info" + ) + })?; + module_info.rendered_init_fragments = codegen_res + .data + .get::() + .cloned() + .filter(|fragments| !fragments.is_empty()); + // JavaScript code generation already returns a ReplaceSource whose + // coordinates are based on the make-time parser source. Keep that + // coordinate space so the pending info spans can be applied directly. The + // generated source is uniquely owned here, so recover it from the Arc + // without cloning every dependency replacement. + let result_source = if rendered_source.as_any().is::() { + let source = Arc::downcast::(rendered_source.into_any_arc()) + .expect("source type was checked above"); + Arc::try_unwrap(source).unwrap_or_else(|source| source.as_ref().clone()) + } else { + ReplaceSource::new(rendered_source) + }; + + let original_source = if matches!( + pending_scope_info, + PendingConcatenationScopeInfo::Analyzed(_) + ) { assert!( - pending_scope_info.is_none() - || module - .source() - .is_some_and(|source| Arc::ptr_eq(result_source.inner(), source)), + module + .source() + .is_some_and(|source| Arc::ptr_eq(result_source.inner(), source)), "generated ReplaceSource inner should preserve the normal module source" ); - let original_source = result_source.inner().source().into_string_lossy(); - populate_info_from_pending( - pending_scope_info.unwrap_or(&empty_pending_scope_info), - &original_source, - &mut module_info, - &faster_module_concatenation_info, - ); - drop(original_source); - let internal_source = result_source.inner().clone(); - (result_source, internal_source) + result_source.inner().source().into_string_lossy() } else { - let source_code = rendered_source.source().into_string_lossy(); - let jsx = module - .as_ref() - .as_normal_module() - .and_then(|normal_module| normal_module.get_parser_options()) - .and_then(|options: &ParserOptions| { - options - .get_javascript() - .and_then(|js_options| js_options.jsx) - }) - .unwrap_or(false); - - let allocator = Allocator::new(); - let lexer = swc_experimental_ecma_parser::Lexer::new( - &allocator, - Syntax::Es(EsSyntax { - jsx, - ..Default::default() - }), - EsVersion::EsNext, - StringSource::new(source_code.as_ref()), - None, - ); - let mut parser = Parser::new_from(&allocator, lexer); - let parsed_module = match parser.parse_module() { - Ok(module) => module, - Err(err) => { - return Err(Error::from_string( - Some(source_code.into_owned()), - err.span().start.saturating_sub(1) as usize, - err.span().end.saturating_sub(1) as usize, - "JavaScript parse error:\n".to_string(), - err.kind().msg().to_string(), - )); - } - }; - let program = Program::Module(allocator.boxed(parsed_module)); - let semantic = resolver(&program); - let ids = collect_ident(&allocator, &program); - - module_info.module_ctxt = SyntaxContext::from_u32(semantic.top_level_scope_id().raw()); - module_info.global_ctxt = SyntaxContext::from_u32(semantic.unresolved_scope_id().raw()); - - let top_level_scope_id = semantic.top_level_scope_id(); - let mut all_used_names = HashSet::default(); - all_used_names.reserve(ids.len()); - module_info.idents.reserve(ids.len()); - module_info.global_scope_ident.reserve(ids.len()); - let mut binding_to_ref: FxIndexMap<(Atom, SyntaxContext), Vec> = - FxIndexMap::default(); - binding_to_ref.reserve(ids.len()); - - for ident in ids { - let scope = semantic.node_scope(&ident.id); - let is_global = SyntaxContext::from_u32(scope.raw()) == module_info.global_ctxt; - let legacy = if is_global { - let legacy = ident.to_legacy(&semantic); - module_info.global_scope_ident.push(legacy.clone()); - all_used_names.insert(legacy.id.sym.clone()); - Some(legacy) - } else { - None - }; - if ident.is_class_expr_with_ident { - all_used_names.insert(Atom::from(ident.id.sym.as_str())); - continue; - } - if scope != top_level_scope_id { - all_used_names.insert(Atom::from(ident.id.sym.as_str())); - } - let legacy = legacy.unwrap_or_else(|| ident.to_legacy(&semantic)); - module_info.idents.push(legacy.clone()); - binding_to_ref - .entry((legacy.id.sym.clone(), legacy.id.ctxt)) - .or_default() - .push(legacy); + Cow::Borrowed("") + }; + populate_info_from_pending( + pending_scope_info, + &original_source, + &mut module_info, + &faster_module_concatenation_info, + ); + let internal_source = result_source.inner().clone(); + (result_source, internal_source) + } else { + let source_code = rendered_source.source().into_string_lossy(); + let jsx = module + .as_ref() + .as_normal_module() + .and_then(|normal_module| normal_module.get_parser_options()) + .and_then(|options: &ParserOptions| { + options + .get_javascript() + .and_then(|js_options| js_options.jsx) + }) + .unwrap_or(false); + + let allocator = Allocator::new(); + let lexer = swc_experimental_ecma_parser::Lexer::new( + &allocator, + Syntax::Es(EsSyntax { + jsx, + ..Default::default() + }), + EsVersion::EsNext, + StringSource::new(source_code.as_ref()), + None, + ); + let mut parser = Parser::new_from(&allocator, lexer); + let parsed_module = match parser.parse_module() { + Ok(module) => module, + Err(err) => { + return Err(Error::from_string( + Some(source_code.into_owned()), + err.span().start.saturating_sub(1) as usize, + err.span().end.saturating_sub(1) as usize, + "JavaScript parse error:\n".to_string(), + err.kind().msg().to_string(), + )); } - module_info.all_used_names = all_used_names; - module_info.binding_to_ref = binding_to_ref; - let result_source = ReplaceSource::new(rendered_source.clone()); - (result_source, rendered_source) }; + let program = Program::Module(allocator.boxed(parsed_module)); + let semantic = resolver(&program); + let ids = collect_ident(&allocator, &program); + + module_info.module_ctxt = SyntaxContext::from_u32(semantic.top_level_scope_id().raw()); + module_info.global_ctxt = SyntaxContext::from_u32(semantic.unresolved_scope_id().raw()); + + let top_level_scope_id = semantic.top_level_scope_id(); + let mut all_used_names = HashSet::default(); + all_used_names.reserve(ids.len()); + module_info.idents.reserve(ids.len()); + module_info.global_scope_ident.reserve(ids.len()); + let mut binding_to_ref: FxIndexMap<(Atom, SyntaxContext), Vec> = + FxIndexMap::default(); + binding_to_ref.reserve(ids.len()); + + for ident in ids { + let scope = semantic.node_scope(&ident.id); + let is_global = SyntaxContext::from_u32(scope.raw()) == module_info.global_ctxt; + let legacy = if is_global { + let legacy = ident.to_legacy(&semantic); + module_info.global_scope_ident.push(legacy.clone()); + all_used_names.insert(legacy.id.sym.clone()); + Some(legacy) + } else { + None + }; + if ident.is_class_expr_with_ident { + all_used_names.insert(Atom::from(ident.id.sym.as_str())); + continue; + } + if scope != top_level_scope_id { + all_used_names.insert(Atom::from(ident.id.sym.as_str())); + } + let legacy = legacy.unwrap_or_else(|| ident.to_legacy(&semantic)); + module_info.idents.push(legacy.clone()); + binding_to_ref + .entry((legacy.id.sym.clone(), legacy.id.ctxt)) + .or_default() + .push(legacy); + } + module_info.all_used_names = all_used_names; + module_info.binding_to_ref = binding_to_ref; + let result_source = ReplaceSource::new(rendered_source.clone()); + (result_source, rendered_source) + }; module_info.internal_source = Some(internal_source); module_info.has_ast = true; module_info.runtime_requirements = runtime_requirements; diff --git a/crates/rspack_core/src/concatenated_module/faster.rs b/crates/rspack_core/src/concatenated_module/faster.rs index edc5777450e4..f02c49fcc8aa 100644 --- a/crates/rspack_core/src/concatenated_module/faster.rs +++ b/crates/rspack_core/src/concatenated_module/faster.rs @@ -178,6 +178,12 @@ pub(crate) fn populate_info_from_pending( module_info: &mut ConcatenatedModuleInfo, faster_info: &FasterModuleConcatenationInfo, ) { + let (module_ctxt, global_ctxt, pending_idents) = match pending { + PendingConcatenationScopeInfo::Analyzed(info) => { + (info.module_ctxt, info.global_ctxt, info.idents.as_slice()) + } + PendingConcatenationScopeInfo::Generated => (0, 0, &[][..]), + }; let mut symbols = SmallVec::<[(&str, Atom); 8]>::new(); let mut symbol_from_range = |range: DependencyRange| { let symbol = original_source @@ -195,28 +201,25 @@ pub(crate) fn populate_info_from_pending( symbols.push((symbol, interned.clone())); interned }; - module_info.module_ctxt = SyntaxContext::from_u32(pending.module_ctxt); - module_info.global_ctxt = SyntaxContext::from_u32(pending.global_ctxt); + module_info.module_ctxt = SyntaxContext::from_u32(module_ctxt); + module_info.global_ctxt = SyntaxContext::from_u32(global_ctxt); module_info.idents.clear(); module_info.global_scope_ident.clear(); module_info.binding_to_ref.clear(); module_info.all_used_names.clear(); module_info.idents.reserve( - pending - .idents + pending_idents .len() .saturating_add(faster_info.added_scope_idents.len()), ); - module_info.global_scope_ident.reserve(pending.idents.len()); + module_info.global_scope_ident.reserve(pending_idents.len()); module_info.binding_to_ref.reserve( - pending - .idents + pending_idents .len() .saturating_add(faster_info.added_scope_idents.len()), ); module_info.all_used_names.reserve( - pending - .idents + pending_idents .len() .saturating_add(faster_info.added_used_names.len()), ); @@ -234,7 +237,7 @@ pub(crate) fn populate_info_from_pending( } let mut seen = SeenIdents::default(); - for pending_ident in &pending.idents { + for pending_ident in pending_idents { let symbol = symbol_from_range(pending_ident.range); if pending_ident.kind == ConcatenationScopeIdentKind::UsedName { module_info.all_used_names.insert(symbol); diff --git a/crates/rspack_core/src/external_module.rs b/crates/rspack_core/src/external_module.rs index 63f3e51a963a..7aa6617ae11f 100644 --- a/crates/rspack_core/src/external_module.rs +++ b/crates/rspack_core/src/external_module.rs @@ -13,13 +13,14 @@ use serde::Serialize; use crate::{ AsyncDependenciesBlockIdentifier, BoxModule, BuildContext, BuildInfo, BuildMeta, BuildMetaExportsType, BuildResult, ChunkGraph, ChunkInitFragments, ChunkUkey, - CodeGenerationDataUrl, CodeGenerationResult, Compilation, ConcatenationScope, Context, - DependenciesBlock, DependencyId, ExportProvided, ExternalType, FactoryMeta, ImportAttributes, - ImportPhase, InitFragmentExt, InitFragmentKey, InitFragmentStage, LibIdentOptions, Module, - ModuleArgument, ModuleCodeGenerationContext, ModuleCodeTemplate, ModuleGraph, ModuleType, - NAMESPACE_OBJECT_EXPORT, NormalInitFragment, RuntimeGlobals, RuntimeSpec, SourceType, - StaticExportsDependency, StaticExportsSpec, UsageState, UsedExports, UsedNameItem, - extract_url_and_global, impl_module_meta_info, module_update_hash, property_access, + CodeGenerationDataUrl, CodeGenerationResult, Compilation, ConcatenationScope, + ConcatenationScopeInfoMode, Context, DependenciesBlock, DependencyId, ExportProvided, + ExternalType, FactoryMeta, ImportAttributes, ImportPhase, InitFragmentExt, InitFragmentKey, + InitFragmentStage, LibIdentOptions, Module, ModuleArgument, ModuleCodeGenerationContext, + ModuleCodeTemplate, ModuleGraph, ModuleType, NAMESPACE_OBJECT_EXPORT, NormalInitFragment, + PendingConcatenationScopeInfo, RuntimeGlobals, RuntimeSpec, SourceType, StaticExportsDependency, + StaticExportsSpec, UsageState, UsedExports, UsedNameItem, extract_url_and_global, + impl_module_meta_info, module_update_hash, property_access, rspack_sources::{BoxSource, RawStringSource, ReplaceSource, SourceExt}, to_identifier, }; @@ -1165,6 +1166,10 @@ impl DependenciesBlock for ExternalModule { impl Module for ExternalModule { impl_module_meta_info!(); + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + ConcatenationScopeInfoMode::GenerateAtCodegen + } + fn get_concatenation_bailout_reason( &self, _mg: &ModuleGraph, @@ -1245,6 +1250,14 @@ impl Module for ExternalModule { _: Option<&Compilation>, ) -> Result { self.build_info.module = build_context.compiler_options.output.module; + if build_context + .compiler_options + .experiments + .faster_module_concatenation + { + self.build_info.pending_concatenation_scope_info = + Some(Box::new(PendingConcatenationScopeInfo::Generated)); + } let resolved_external_type = self.resolve_external_type(); let request = match &self.request { ExternalRequest::Single(request) => Some(request), diff --git a/crates/rspack_core/src/module.rs b/crates/rspack_core/src/module.rs index 32b9520cb2a9..e783b1e76ff6 100644 --- a/crates/rspack_core/src/module.rs +++ b/crates/rspack_core/src/module.rs @@ -32,13 +32,14 @@ use crate::{ AsyncDependenciesBlock, BindingCell, BoxDependency, BoxDependencyTemplate, BoxModuleDependency, ChunkGraph, ChunkUkey, CodeGenerationResult, CollectedTypeScriptInfo, Compilation, CompilationAsset, CompilationId, CompilerId, CompilerOptions, ConcatenationScope, - ConnectionState, Context, ContextModule, CssExportType, DependenciesBlock, DependencyId, - ExportProvided, ExportsInfoArtifact, ExternalModule, Filename, GetTargetResult, ImportPhase, - ModuleCodeTemplate, ModuleGraph, ModuleGraphCacheArtifact, ModuleLayer, ModuleType, NormalModule, - OptimizationBailoutItem, RawModule, Resolve, ResolverFactory, RuntimeSpec, SelfModule, - SharedPluginDriver, SideEffectsStateArtifact, SourceType, - concatenated_module::ConcatenatedModule, dependencies_block::dependencies_block_update_hash, - get_target, utils::PendingConcatenationScopeInfo, value_cache_versions::ValueCacheVersions, + ConcatenationScopeInfoMode, ConnectionState, Context, ContextModule, CssExportType, + DependenciesBlock, DependencyId, ExportProvided, ExportsInfoArtifact, ExternalModule, Filename, + GetTargetResult, ImportPhase, ModuleCodeTemplate, ModuleGraph, ModuleGraphCacheArtifact, + ModuleLayer, ModuleType, NormalModule, OptimizationBailoutItem, RawModule, Resolve, + ResolverFactory, RuntimeSpec, SelfModule, SharedPluginDriver, SideEffectsStateArtifact, + SourceType, concatenated_module::ConcatenatedModule, + dependencies_block::dependencies_block_update_hash, get_target, + utils::PendingConcatenationScopeInfo, value_cache_versions::ValueCacheVersions, }; pub struct BuildContext { @@ -788,6 +789,10 @@ pub trait Module: None } + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + ConcatenationScopeInfoMode::Unsupported + } + fn get_concatenation_bailout_reason( &self, _mg: &ModuleGraph, diff --git a/crates/rspack_core/src/normal_module.rs b/crates/rspack_core/src/normal_module.rs index c05f3b4f8c2a..1a7ecd023bd6 100644 --- a/crates/rspack_core/src/normal_module.rs +++ b/crates/rspack_core/src/normal_module.rs @@ -28,12 +28,13 @@ use tracing::{Instrument, info_span}; use crate::{ AsyncDependenciesBlockIdentifier, BoxDependencyTemplate, BoxLoader, BoxModule, BoxModuleDependency, BuildContext, BuildInfo, BuildMeta, BuildResult, ChunkGraph, - CodeGenerationResult, Compilation, ConnectionState, Context, DependenciesBlock, DependencyId, - FactoryMeta, GenerateContext, GeneratorOptions, ImportPhase, LibIdentOptions, Module, - ModuleCodeGenerationContext, ModuleGraph, ModuleGraphCacheArtifact, ModuleIdentifier, - ModuleLayer, ModuleType, OptimizationBailoutItem, OutputOptions, ParseContext, ParseResult, - ParserAndGenerator, ParserOptions, Resolve, ResolvedModuleOptions, RspackLoaderRunnerPlugin, - RunnerContext, RuntimeGlobals, RuntimeSpec, SideEffectsStateArtifact, SourceType, contextify, + CodeGenerationResult, Compilation, ConcatenationScopeInfoMode, ConnectionState, Context, + DependenciesBlock, DependencyId, FactoryMeta, GenerateContext, GeneratorOptions, ImportPhase, + LibIdentOptions, Module, ModuleCodeGenerationContext, ModuleGraph, ModuleGraphCacheArtifact, + ModuleIdentifier, ModuleLayer, ModuleType, OptimizationBailoutItem, OutputOptions, ParseContext, + ParseResult, ParserAndGenerator, ParserOptions, PendingConcatenationScopeInfo, Resolve, + ResolvedModuleOptions, RspackLoaderRunnerPlugin, RunnerContext, RuntimeGlobals, RuntimeSpec, + SideEffectsStateArtifact, SourceType, contextify, diagnostics::ModuleBuildError, get_context, module_analyzed_side_effect_free, module_declared_side_effect_free, module_update_hash, @@ -511,6 +512,17 @@ impl Module for NormalModule { .map(Into::into) .collect(); + if build_context + .compiler_options + .experiments + .faster_module_concatenation + && self.parser_and_generator.concatenation_scope_info_mode() + == ConcatenationScopeInfoMode::GenerateAtCodegen + { + self.build_info.pending_concatenation_scope_info = + Some(Box::new(PendingConcatenationScopeInfo::Generated)); + } + if no_parse { self.parsed = false; self.source = Some(source); @@ -808,6 +820,10 @@ impl Module for NormalModule { .get_concatenation_bailout_reason(self, mg, cg) } + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + self.parser_and_generator.concatenation_scope_info_mode() + } + fn factory_meta(&self) -> Option<&FactoryMeta> { self.factory_meta.as_ref() } diff --git a/crates/rspack_core/src/parser_and_generator.rs b/crates/rspack_core/src/parser_and_generator.rs index 166c87d7a1b6..f844c5309163 100644 --- a/crates/rspack_core/src/parser_and_generator.rs +++ b/crates/rspack_core/src/parser_and_generator.rs @@ -121,6 +121,18 @@ pub struct GenerateContext<'a> { pub concatenation_scope: Option<&'a mut ConcatenationScope>, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConcatenationScopeInfoMode { + /// Scope information is collected from the original JavaScript program + /// during make. + AnalyzeAtMake, + /// The module has no original JavaScript scope. Its generator must register + /// every generated binding through `ConcatenationScope`. + GenerateAtCodegen, + /// The parser and generator cannot participate in module concatenation. + Unsupported, +} + #[cacheable_dyn] #[async_trait::async_trait] pub trait ParserAndGenerator: Send + Sync + Debug + AsAny { @@ -141,6 +153,8 @@ pub trait ParserAndGenerator: Send + Sync + Debug + AsAny { generate_context: &mut GenerateContext, ) -> Result; + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode; + fn get_concatenation_bailout_reason( &self, _module: &dyn Module, diff --git a/crates/rspack_core/src/utils/concatenated_module_visitor.rs b/crates/rspack_core/src/utils/concatenated_module_visitor.rs index 488fd5ac9d40..aeb15b6f3608 100644 --- a/crates/rspack_core/src/utils/concatenated_module_visitor.rs +++ b/crates/rspack_core/src/utils/concatenated_module_visitor.rs @@ -30,13 +30,25 @@ pub struct ConcatenationScopeIdent { } #[cacheable] -#[derive(Clone, Debug, Default)] -pub struct PendingConcatenationScopeInfo { +#[derive(Clone, Debug)] +pub struct AnalyzedConcatenationScopeInfo { pub module_ctxt: u32, pub global_ctxt: u32, pub idents: Vec, } +/// Make-time scope information required by faster module concatenation. +/// +/// `Generated` is an explicit declaration that the module has no original +/// JavaScript scope. Any bindings introduced by its generator must be +/// registered through `ConcatenationScope` during code generation. +#[cacheable] +#[derive(Clone, Debug)] +pub enum PendingConcatenationScopeInfo { + Analyzed(AnalyzedConcatenationScopeInfo), + Generated, +} + #[derive(Default)] pub struct IdentCollector { pub ids: Vec, diff --git a/crates/rspack_plugin_asset/src/lib.rs b/crates/rspack_plugin_asset/src/lib.rs index c69c83fc316f..2c9137da7e0b 100644 --- a/crates/rspack_plugin_asset/src/lib.rs +++ b/crates/rspack_plugin_asset/src/lib.rs @@ -8,8 +8,8 @@ use rspack_core::{ AssetInfo, AssetParserDataUrl, BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, ChunkUkey, CodeGenerationDataAssetInfo, CodeGenerationDataFilename, CodeGenerationDataUrl, CodeGenerationPublicPathAutoReplace, Compilation, CompilationRenderManifest, CompilerOptions, - ConcatenationScope, DependencyType, Filename, GenerateContext, GeneratorOptions, - JavascriptParserUrl, ManifestAssetType, Module, ModuleArgument, ModuleGraph, + ConcatenationScope, ConcatenationScopeInfoMode, DependencyType, Filename, GenerateContext, + GeneratorOptions, JavascriptParserUrl, ManifestAssetType, Module, ModuleArgument, ModuleGraph, NAMESPACE_OBJECT_EXPORT, NormalModule, ParseContext, ParserAndGenerator, ParserOptions, PathData, Plugin, PublicPath, RenderManifestEntry, ResourceData, RuntimeGlobals, RuntimeSpec, SourceType, rspack_sources::{BoxSource, RawStringSource, ReplaceSource, SourceExt}, @@ -355,6 +355,10 @@ const DEFAULT_MAX_SIZE: f64 = 8096.0; #[cacheable_dyn] #[async_trait::async_trait] impl ParserAndGenerator for AssetParserAndGenerator { + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + ConcatenationScopeInfoMode::GenerateAtCodegen + } + fn source_types(&self, module: &dyn Module, module_graph: &ModuleGraph) -> &[SourceType] { let module_id = module.identifier(); if self.emit diff --git a/crates/rspack_plugin_css/src/parser_and_generator/mod.rs b/crates/rspack_plugin_css/src/parser_and_generator/mod.rs index d90dd22fd3e0..f9f8df13dfe1 100644 --- a/crates/rspack_plugin_css/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_css/src/parser_and_generator/mod.rs @@ -11,9 +11,10 @@ use regex::Regex; use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_core::{ BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, Compilation, - CssAutoOrModuleParserOptions, CssBuildInfo, CssExportType, DependencyType, ExportsInfoArtifact, - GenerateContext, Module, ModuleGraph, ModuleIdentifier, NormalModule, ParseContext, ParseResult, - ParserAndGenerator, ParserOptions, ResolvedModuleOptions, RuntimeSpec, SourceType, UsageState, + ConcatenationScopeInfoMode, CssAutoOrModuleParserOptions, CssBuildInfo, CssExportType, + DependencyType, ExportsInfoArtifact, GenerateContext, Module, ModuleGraph, ModuleIdentifier, + NormalModule, ParseContext, ParseResult, ParserAndGenerator, ParserOptions, + ResolvedModuleOptions, RuntimeSpec, SourceType, UsageState, rspack_sources::{BoxSource, Source}, }; pub use rspack_core::{CssExport, CssExports}; @@ -195,6 +196,10 @@ static REGEX_CUSTOM_PROPERTY_IDENT: LazyLock = LazyLock::new(|| { #[cacheable_dyn] #[async_trait::async_trait] impl ParserAndGenerator for CssParserAndGenerator { + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + ConcatenationScopeInfoMode::GenerateAtCodegen + } + fn source_types(&self, module: &dyn Module, module_graph: &ModuleGraph) -> &[SourceType] { let export_type = self.effective_export_type(module); if matches!( diff --git a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs index 8bd6748a0f8e..84eabb648f8a 100644 --- a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs @@ -12,9 +12,9 @@ use rspack_cacheable::{ use rspack_core::{ ArcComputed, AsyncDependenciesBlockIdentifier, BuildMetaExportsType, COLLECTED_TYPESCRIPT_INFO_PARSE_META_KEY, ChunkGraph, CollectedTypeScriptInfo, Compilation, - ConcatenationScope, DependenciesBlock, DependencyId, GenerateContext, ImportMeta, Module, - ModuleArgument, ModuleCodeTemplate, ModuleGraph, ModuleType, ParseContext, ParseResult, - ParserAndGenerator, ResolvedModuleOptions, RuntimeGlobals, RuntimeGlobalsRenderMode, + ConcatenationScope, ConcatenationScopeInfoMode, DependenciesBlock, DependencyId, GenerateContext, + ImportMeta, Module, ModuleArgument, ModuleCodeTemplate, ModuleGraph, ModuleType, ParseContext, + ParseResult, ParserAndGenerator, ResolvedModuleOptions, RuntimeGlobals, RuntimeGlobalsRenderMode, RuntimeVariable, SideEffectsBailoutItem, SourceType, TemplateContext, TemplateReplaceSource, diagnostics::map_box_diagnostics_to_module_parse_diagnostics, remove_bom, render_init_fragments, render_init_fragments_to_strings, @@ -236,6 +236,10 @@ static SOURCE_TYPES: &[SourceType; 1] = &[SourceType::JavaScript]; #[cacheable_dyn] #[async_trait::async_trait] impl ParserAndGenerator for JavaScriptParserAndGenerator { + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + ConcatenationScopeInfoMode::AnalyzeAtMake + } + fn source_types(&self, _module: &dyn Module, _module_graph: &ModuleGraph) -> &[SourceType] { SOURCE_TYPES } diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index d781acd09975..43c0cb366e77 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -10,19 +10,19 @@ use rspack_collections::{ Identifiable, IdentifierDashMap, IdentifierIndexSet, IdentifierMap, IdentifierSet, }; use rspack_core::{ - BoxDependency, BoxModule, ChunkUkey, Compilation, CompilationOptimizeChunkModules, DependencyId, - DependencyType, ExportProvided, ExportsInfoArtifact, GetTargetResult, - ImportedByDeferModulesArtifact, LibIdentOptions, Logger, ModuleGraph, ModuleGraphCacheArtifact, - ModuleGraphConnection, ModuleGraphModule, ModuleIdentifier, OptimizationBailoutItem, Plugin, - ProvidedExports, RuntimeCondition, RuntimeSpec, RuntimeSpecMap, SideEffectsStateArtifact, - SourceType, + BoxDependency, BoxModule, ChunkUkey, Compilation, CompilationOptimizeChunkModules, + ConcatenationScopeInfoMode, DependencyId, DependencyType, ExportProvided, ExportsInfoArtifact, + GetTargetResult, ImportedByDeferModulesArtifact, LibIdentOptions, Logger, ModuleGraph, + ModuleGraphCacheArtifact, ModuleGraphConnection, ModuleGraphModule, ModuleIdentifier, + OptimizationBailoutItem, PendingConcatenationScopeInfo, Plugin, ProvidedExports, + RuntimeCondition, RuntimeSpec, RuntimeSpecMap, SideEffectsStateArtifact, SourceType, concatenated_module::{ ConcatenatedInnerModule, ConcatenatedModule, RootModuleContext, is_esm_dep_like, }, filter_runtime, get_cached_readable_identifier, get_target, incremental::IncrementalPasses, }; -use rspack_error::{Result, ToStringResultToRspackResultExt}; +use rspack_error::{Result, ToStringResultToRspackResultExt, error}; use rspack_hook::{plugin, plugin_hook}; use rspack_util::itoa; use rustc_hash::FxHashSet as HashSet; @@ -1069,9 +1069,35 @@ impl ModuleConcatenationPlugin { }) .collect(); + let faster_module_concatenation = compilation.options.experiments.faster_module_concatenation; let module_graph = compilation.get_module_graph_mut(); for (can_be_root, can_be_inner, module_id, bailout_reason) in res { + if faster_module_concatenation && (can_be_root || can_be_inner) { + let module = module_graph + .module_by_identifier(&module_id) + .expect("should have module"); + let mode = module.concatenation_scope_info_mode(); + let pending = module + .build_info() + .pending_concatenation_scope_info + .as_deref(); + let valid = matches!( + (mode, pending), + ( + ConcatenationScopeInfoMode::AnalyzeAtMake, + Some(PendingConcatenationScopeInfo::Analyzed(_)) + ) | ( + ConcatenationScopeInfoMode::GenerateAtCodegen, + Some(PendingConcatenationScopeInfo::Generated) + ) + ); + if !valid { + return Err(error!( + "module {module_id} is eligible for module concatenation, but its concatenation scope info does not match the declared {mode:?} mode" + )); + } + } if can_be_root { relevant_modules.push(module_id); } diff --git a/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs b/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs index 8aab23264b52..fedf603bab00 100644 --- a/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs +++ b/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs @@ -1,6 +1,6 @@ use rspack_core::{ - ConcatenationScopeIdent, ConcatenationScopeIdentKind, DependencyRange, - PendingConcatenationScopeInfo, + AnalyzedConcatenationScopeInfo, ConcatenationScopeIdent, ConcatenationScopeIdentKind, + DependencyRange, PendingConcatenationScopeInfo, }; use rustc_hash::FxHashSet; use smallvec::SmallVec; @@ -124,11 +124,11 @@ impl<'semantic, 'ast> PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { kind: ConcatenationScopeIdentKind::UsedName, }), ); - PendingConcatenationScopeInfo { + PendingConcatenationScopeInfo::Analyzed(AnalyzedConcatenationScopeInfo { module_ctxt: self.semantic.top_level_scope_id().raw(), global_ctxt: self.semantic.unresolved_scope_id().raw(), idents, - } + }) } } diff --git a/crates/rspack_plugin_json/src/lib.rs b/crates/rspack_plugin_json/src/lib.rs index a4da50f93941..969cf0c47b75 100644 --- a/crates/rspack_plugin_json/src/lib.rs +++ b/crates/rspack_plugin_json/src/lib.rs @@ -12,10 +12,10 @@ use json::{ }; use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_core::{ - BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, ExportsInfoArtifact, ExportsInfoData, - GenerateContext, GeneratorOptions, Module, ModuleArgument, ModuleGraph, NAMESPACE_OBJECT_EXPORT, - ParseOption, ParserAndGenerator, ParserOptions, Plugin, RuntimeSpec, SourceType, UsageState, - UsedNameItem, + BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, ConcatenationScopeInfoMode, + ExportsInfoArtifact, ExportsInfoData, GenerateContext, GeneratorOptions, Module, ModuleArgument, + ModuleGraph, NAMESPACE_OBJECT_EXPORT, ParseOption, ParserAndGenerator, ParserOptions, Plugin, + RuntimeSpec, SourceType, UsageState, UsedNameItem, diagnostics::ModuleParseError, rspack_sources::{BoxSource, RawStringSource, ReplaceSource, Source, SourceExt}, }; @@ -37,6 +37,10 @@ struct JsonParserAndGenerator { #[cacheable_dyn] #[async_trait::async_trait] impl ParserAndGenerator for JsonParserAndGenerator { + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + ConcatenationScopeInfoMode::GenerateAtCodegen + } + fn source_types(&self, _module: &dyn Module, _module_graph: &ModuleGraph) -> &[SourceType] { &[SourceType::JavaScript] } diff --git a/crates/rspack_plugin_rslib/src/asset.rs b/crates/rspack_plugin_rslib/src/asset.rs index 1377a539ac22..b2cbef2912e8 100644 --- a/crates/rspack_plugin_rslib/src/asset.rs +++ b/crates/rspack_plugin_rslib/src/asset.rs @@ -2,8 +2,9 @@ use std::borrow::Cow; use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_core::{ - ChunkGraph, Compilation, GenerateContext, Module, ModuleGraph, NormalModule, ParseContext, - ParseResult, ParserAndGenerator, RuntimeSpec, SourceType, rspack_sources::BoxSource, + ChunkGraph, Compilation, ConcatenationScopeInfoMode, GenerateContext, Module, ModuleGraph, + NormalModule, ParseContext, ParseResult, ParserAndGenerator, RuntimeSpec, SourceType, + rspack_sources::BoxSource, }; use rspack_error::{Result, TWithDiagnosticArray}; use rspack_hash::RspackHashDigest; @@ -17,6 +18,10 @@ pub(crate) struct RslibAssetParserAndGenerator(pub AssetParserAndGenerator); #[cacheable_dyn] #[async_trait::async_trait] impl ParserAndGenerator for RslibAssetParserAndGenerator { + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + self.0.concatenation_scope_info_mode() + } + fn source_types(&self, module: &dyn Module, module_graph: &ModuleGraph) -> &[SourceType] { let mut source_types = FxHashSet::default(); let module_id = module.identifier(); diff --git a/crates/rspack_plugin_wasm/src/parser_and_generator.rs b/crates/rspack_plugin_wasm/src/parser_and_generator.rs index 39a758c43b17..caaf4da21c6d 100644 --- a/crates/rspack_plugin_wasm/src/parser_and_generator.rs +++ b/crates/rspack_plugin_wasm/src/parser_and_generator.rs @@ -3,10 +3,10 @@ use std::borrow::Cow; use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_collections::IdentifierIndexMap; use rspack_core::{ - BoxDependency, BuildMetaExportsType, Dependency, DependencyId, DependencyType, ExportsArgument, - GenerateContext, ImportPhase, Module, ModuleArgument, ModuleDependency, ModuleGraph, - ModuleInitFragments, ParseContext, ParseResult, ParserAndGenerator, RuntimeGlobals, SourceType, - StaticExportsDependency, StaticExportsSpec, + BoxDependency, BuildMetaExportsType, ConcatenationScopeInfoMode, Dependency, DependencyId, + DependencyType, ExportsArgument, GenerateContext, ImportPhase, Module, ModuleArgument, + ModuleDependency, ModuleGraph, ModuleInitFragments, ParseContext, ParseResult, + ParserAndGenerator, RuntimeGlobals, SourceType, StaticExportsDependency, StaticExportsSpec, rspack_sources::{BoxSource, RawStringSource, Source, SourceExt}, }; use rspack_error::{Diagnostic, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray}; @@ -33,6 +33,10 @@ struct DepModule<'a> { #[cacheable_dyn] #[async_trait::async_trait] impl ParserAndGenerator for AsyncWasmParserAndGenerator { + fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode { + ConcatenationScopeInfoMode::Unsupported + } + fn source_types(&self, _module: &dyn Module, _module_graph: &ModuleGraph) -> &[SourceType] { WASM_SOURCE_TYPE } From 98f59d5e3ccb544387a8234b92e75c96b08df5ed Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Thu, 13 Aug 2026 22:51:15 +0800 Subject: [PATCH 04/10] perf: reduce faster concatenation scope tracking overhead --- crates/rspack_core/src/concatenated_module.rs | 7 +- .../src/concatenated_module/faster.rs | 8 +- .../src/dependency/cached_const_dependency.rs | 6 +- .../src/dependency/dependency_template.rs | 102 ++++++++++++++---- .../src/utils/concatenation_scope.rs | 87 ++++++++++----- .../esm/esm_import_specifier_dependency.rs | 57 +++++++--- .../esm/import_meta_rsc_dependency.rs | 2 +- .../src/dependency/esm/provide_dependency.rs | 7 +- .../src/module_path_name_dependency.rs | 14 ++- 9 files changed, 215 insertions(+), 75 deletions(-) diff --git a/crates/rspack_core/src/concatenated_module.rs b/crates/rspack_core/src/concatenated_module.rs index 80494f1a9bb9..685c5d5fb825 100644 --- a/crates/rspack_core/src/concatenated_module.rs +++ b/crates/rspack_core/src/concatenated_module.rs @@ -26,6 +26,7 @@ use rspack_util::{ swc::join_atom, }; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; +use smallvec::SmallVec; use swc_core::{ atoms::Atom, common::{BytePos, Spanned, SyntaxContext}, @@ -331,7 +332,7 @@ pub enum OriginalScopeIdentUpdate { #[derive(Debug, Clone, Default)] pub struct FasterModuleConcatenationInfo { /// Corrections to make-time identifiers caused by dependency replacements. - pub original_scope_ident_updates: Vec, + pub original_scope_ident_updates: SmallVec<[OriginalScopeIdentUpdate; 4]>, /// Scope identifiers supplied by dependency templates in addition to the /// make-time collection. pub added_scope_idents: Vec, @@ -2711,7 +2712,7 @@ impl ConcatenatedModule { .remove(&SourceType::JavaScript) .expect("should have javascript source"); let mut module_info = concatenation_scope.current_module; - let (result_source, internal_source) = if let Some(faster_module_concatenation_info) = + let (result_source, internal_source) = if let Some(mut faster_module_concatenation_info) = faster_module_concatenation_info { let pending_scope_info = module @@ -2759,7 +2760,7 @@ impl ConcatenatedModule { pending_scope_info, &original_source, &mut module_info, - &faster_module_concatenation_info, + &mut faster_module_concatenation_info, ); let internal_source = result_source.inner().clone(); (result_source, internal_source) diff --git a/crates/rspack_core/src/concatenated_module/faster.rs b/crates/rspack_core/src/concatenated_module/faster.rs index f02c49fcc8aa..aa5da005f036 100644 --- a/crates/rspack_core/src/concatenated_module/faster.rs +++ b/crates/rspack_core/src/concatenated_module/faster.rs @@ -176,7 +176,7 @@ pub(crate) fn populate_info_from_pending( pending: &PendingConcatenationScopeInfo, original_source: &str, module_info: &mut ConcatenatedModuleInfo, - faster_info: &FasterModuleConcatenationInfo, + faster_info: &mut FasterModuleConcatenationInfo, ) { let (module_ctxt, global_ctxt, pending_idents) = match pending { PendingConcatenationScopeInfo::Analyzed(info) => { @@ -225,7 +225,7 @@ pub(crate) fn populate_info_from_pending( ); module_info .all_used_names - .extend(faster_info.added_used_names.iter().cloned()); + .extend(faster_info.added_used_names.drain(..)); let mut removed_ranges = SmallVec::<[DependencyRange; 4]>::new(); let mut non_shorthand_ranges = SmallVec::<[DependencyRange; 4]>::new(); @@ -277,12 +277,12 @@ pub(crate) fn populate_info_from_pending( ); } - for ident in &faster_info.added_scope_idents { + for ident in faster_info.added_scope_idents.drain(..) { add_ident_to_module_info( module_info, ConcatenatedModuleIdent { id: swc_ecma_ast::Ident::new( - ident.symbol.clone(), + ident.symbol, Span::new( BytePos(ident.range.start.saturating_add(1)), BytePos(ident.range.end.saturating_add(1)), diff --git a/crates/rspack_core/src/dependency/cached_const_dependency.rs b/crates/rspack_core/src/dependency/cached_const_dependency.rs index 13ec9b16dba2..e6f5b3b362fb 100644 --- a/crates/rspack_core/src/dependency/cached_const_dependency.rs +++ b/crates/rspack_core/src/dependency/cached_const_dependency.rs @@ -169,7 +169,11 @@ impl DependencyTemplate for CachedConstDependencyTemplate { } if let Some(range) = dep.range { - source.replace(range.start, range.end, rendered_identifier, None); + if matches!(dep.place, CachedConstDependencyPlace::Module) { + source.replace_with_tracked_used_names(range.start, range.end, rendered_identifier, None); + } else { + source.replace(range.start, range.end, rendered_identifier, None); + } } } } diff --git a/crates/rspack_core/src/dependency/dependency_template.rs b/crates/rspack_core/src/dependency/dependency_template.rs index 610b618af6bd..cc41d099397e 100644 --- a/crates/rspack_core/src/dependency/dependency_template.rs +++ b/crates/rspack_core/src/dependency/dependency_template.rs @@ -47,6 +47,12 @@ pub struct TemplateReplaceSource<'a> { concatenation_scope: Option<&'a mut ConcatenationScope>, } +#[derive(Clone, Copy)] +enum GeneratedCodeUsedNames { + Scan, + AlreadyTracked, +} + impl<'a> TemplateReplaceSource<'a> { pub fn new( source: &'a mut ReplaceSource, @@ -87,20 +93,38 @@ impl<'a> TemplateReplaceSource<'a> { }) } - fn record_edit(&mut self, start: u32, end: u32, content: &str) { - let Some(scope) = self.faster_concatenation_scope() else { + #[inline] + fn record_edit( + &mut self, + start: u32, + end: u32, + content: &str, + used_names: GeneratedCodeUsedNames, + ) { + let Some(scope) = self.concatenation_scope.as_deref_mut() else { return; }; - if start != end { - scope.remove_original_range(DependencyRange::new(start, end)); - } - if !content.is_empty() { - scope.add_used_names_from_generated_code(content); - } + scope.record_source_edit( + (start != end).then(|| DependencyRange::new(start, end)), + matches!(used_names, GeneratedCodeUsedNames::Scan).then_some(content), + ); } pub fn replace(&mut self, start: u32, end: u32, content: String, name: Option) { - self.record_edit(start, end, &content); + self.record_edit(start, end, &content, GeneratedCodeUsedNames::Scan); + self.source.replace(start, end, content, name); + } + + /// Replaces source with code whose used names have already been recorded in + /// the concatenation scope. + pub fn replace_with_tracked_used_names( + &mut self, + start: u32, + end: u32, + content: String, + name: Option, + ) { + self.record_edit(start, end, &content, GeneratedCodeUsedNames::AlreadyTracked); self.source.replace(start, end, content, name); } @@ -111,7 +135,7 @@ impl<'a> TemplateReplaceSource<'a> { content: &'static str, name: Option<&'static str>, ) { - self.record_edit(start, end, content); + self.record_edit(start, end, content, GeneratedCodeUsedNames::Scan); self.source.replace_static(start, end, content, name); } @@ -123,19 +147,19 @@ impl<'a> TemplateReplaceSource<'a> { name: Option<&'static str>, enforce: ReplacementEnforce, ) { - self.record_edit(start, end, content); + self.record_edit(start, end, content, GeneratedCodeUsedNames::Scan); self .source .replace_static_with_enforce(start, end, content, name, enforce); } pub fn insert(&mut self, start: u32, content: String, name: Option) { - self.record_edit(start, start, &content); + self.record_edit(start, start, &content, GeneratedCodeUsedNames::Scan); self.source.insert(start, content, name); } pub fn insert_static(&mut self, start: u32, content: &'static str, name: Option<&'static str>) { - self.record_edit(start, start, content); + self.record_edit(start, start, content, GeneratedCodeUsedNames::Scan); self.source.insert_static(start, content, name); } @@ -156,11 +180,46 @@ impl<'a> TemplateReplaceSource<'a> { content: String, name: Option, ) { - if let Some(scope) = self.faster_concatenation_scope() { - scope.remove_original_range(shorthand_range); - if !content.is_empty() { - scope.add_used_names_from_generated_code(&content); - } + self.insert_shorthand_value_with_used_names( + start, + shorthand_range, + content, + name, + GeneratedCodeUsedNames::Scan, + ); + } + + /// Expands a shorthand using code whose used names have already been + /// recorded in the concatenation scope. + pub fn insert_shorthand_value_with_tracked_used_names( + &mut self, + start: u32, + shorthand_range: DependencyRange, + content: String, + name: Option, + ) { + self.insert_shorthand_value_with_used_names( + start, + shorthand_range, + content, + name, + GeneratedCodeUsedNames::AlreadyTracked, + ); + } + + fn insert_shorthand_value_with_used_names( + &mut self, + start: u32, + shorthand_range: DependencyRange, + content: String, + name: Option, + used_names: GeneratedCodeUsedNames, + ) { + if let Some(scope) = self.concatenation_scope.as_deref_mut() { + scope.record_source_edit( + Some(shorthand_range), + matches!(used_names, GeneratedCodeUsedNames::Scan).then_some(content.as_str()), + ); } self.source.insert(start, content, name); } @@ -174,11 +233,8 @@ impl<'a> TemplateReplaceSource<'a> { content: String, name: Option, ) { - if let Some(scope) = self.faster_concatenation_scope() { - scope.set_original_range_non_shorthand(original_range); - if !content.is_empty() { - scope.add_used_names_from_generated_code(&content); - } + if let Some(scope) = self.concatenation_scope.as_deref_mut() { + scope.record_non_shorthand_source_edit(original_range, &content); } self.source.insert(start, content, name); } diff --git a/crates/rspack_core/src/utils/concatenation_scope.rs b/crates/rspack_core/src/utils/concatenation_scope.rs index f257f46e035f..280247b7349e 100644 --- a/crates/rspack_core/src/utils/concatenation_scope.rs +++ b/crates/rspack_core/src/utils/concatenation_scope.rs @@ -22,6 +22,34 @@ pub const NAMESPACE_OBJECT_EXPORT: &str = "__rspack_ns_object"; pub const DEFAULT_EXPORT: &str = "__rspack_default_export"; const MODULE_REFERENCE_PROPERTY_ACCESS_SUFFIX: &str = "._"; +#[inline] +fn is_ascii_identifier_continue(byte: u8) -> bool { + byte == b'_' || byte == b'$' || byte.is_ascii_alphanumeric() +} + +fn add_used_names_from_generated_code(info: &mut FasterModuleConcatenationInfo, code: &str) { + // The legacy codegen parse reserves names referenced by replacement code. + // Faster concatenation skips that parse, so conservatively collect every + // ASCII identifier-shaped token. False positives only reduce name reuse; + // missing a name could capture a generated global reference. + let bytes = code.as_bytes(); + let mut cursor = 0; + while cursor < bytes.len() { + while cursor < bytes.len() && !is_ascii_identifier_continue(bytes[cursor]) { + cursor += 1; + } + let start = cursor; + while cursor < bytes.len() && is_ascii_identifier_continue(bytes[cursor]) { + cursor += 1; + } + if start < cursor + && (bytes[start] == b'_' || bytes[start] == b'$' || bytes[start].is_ascii_alphabetic()) + { + info.added_used_names.push(code[start..cursor].into()); + } + } +} + #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct ModuleReferenceOptions { pub ids: Vec, @@ -92,11 +120,12 @@ impl ConcatenationScope { ) -> ConcatenatedModuleInfo { let mut current_module = self.current_module.clone(); if let Some(faster_info) = self.faster_module_concatenation_info.as_deref() { + let mut faster_info = faster_info.clone(); crate::concatenated_module::populate_info_from_pending( pending, original_source, &mut current_module, - faster_info, + &mut faster_info, ); } current_module @@ -175,21 +204,49 @@ impl ConcatenationScope { } pub fn remove_original_range(&mut self, range: DependencyRange) { + self.record_source_edit(Some(range), None); + } + + pub fn set_original_range_non_shorthand(&mut self, range: DependencyRange) { + self.record_non_shorthand_source_edit(range, ""); + } + + #[inline] + pub(crate) fn record_source_edit( + &mut self, + removed_range: Option, + generated_code: Option<&str>, + ) { let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { return; }; - info - .original_scope_ident_updates - .push(OriginalScopeIdentUpdate::Remove(range)); + if let Some(range) = removed_range { + info + .original_scope_ident_updates + .push(OriginalScopeIdentUpdate::Remove(range)); + } + if let Some(code) = generated_code + && !code.is_empty() + { + add_used_names_from_generated_code(info, code); + } } - pub fn set_original_range_non_shorthand(&mut self, range: DependencyRange) { + #[inline] + pub(crate) fn record_non_shorthand_source_edit( + &mut self, + range: DependencyRange, + generated_code: &str, + ) { let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { return; }; info .original_scope_ident_updates .push(OriginalScopeIdentUpdate::NonShorthand(range)); + if !generated_code.is_empty() { + add_used_names_from_generated_code(info, generated_code); + } } pub fn add_scope_ident(&mut self, symbol: Atom, range: DependencyRange) { @@ -204,26 +261,6 @@ impl ConcatenationScope { }); } - pub fn add_used_names_from_generated_code(&mut self, code: &str) { - let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { - return; - }; - // The legacy codegen parse reserves names referenced by replacement code. - // Faster concatenation skips that parse, so conservatively collect every - // ASCII identifier-shaped token. False positives only reduce name reuse; - // missing a name could capture a generated global reference. - for candidate in code.split(|ch: char| !(ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())) - { - let mut chars = candidate.chars(); - if chars - .next() - .is_some_and(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphabetic()) - { - info.added_used_names.push(candidate.into()); - } - } - } - pub fn ensure_generated_top_level_symbol(&mut self, preferred_name: &str) -> Atom { let preferred_name = Atom::from(preferred_name); if !self.is_faster_module_concatenation() { diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs index d301fb03692d..0cadb1b1315a 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/esm_import_specifier_dependency.rs @@ -430,6 +430,11 @@ impl DependencyCodeGeneration for ESMImportSpecifierDependency { #[derive(Debug, Clone, Default)] pub struct ESMImportSpecifierDependencyTemplate; +enum ImportExpression { + Generated(String), + Tracked(String), +} + impl ESMImportSpecifierDependencyTemplate { pub fn template_type() -> DependencyTemplateType { DependencyTemplateType::Dependency(DependencyType::EsmImportSpecifier) @@ -442,7 +447,7 @@ impl ESMImportSpecifierDependencyTemplate { connection: Option<&ModuleGraphConnection>, source: &mut TemplateReplaceSource, code_generatable_context: &mut TemplateContext, - ) -> String { + ) -> ImportExpression { let TemplateContext { compilation, runtime, @@ -452,7 +457,7 @@ impl ESMImportSpecifierDependencyTemplate { && let Some(con) = connection && scope.is_module_in_scope(con.module_identifier()) { - if ids.is_empty() { + let expression = if ids.is_empty() { scope.create_module_reference( con.module_identifier(), ModuleReferenceOptions { @@ -500,7 +505,8 @@ impl ESMImportSpecifierDependencyTemplate { ..Default::default() }, ) - } + }; + ImportExpression::Tracked(expression) } else { let mg = code_generatable_context.compilation.get_module_graph(); let target_module = mg.get_module_by_dependency_id(&dep.id); @@ -527,7 +533,7 @@ impl ESMImportSpecifierDependencyTemplate { runtime_template, .. } = code_generatable_context; - runtime_template.export_from_import( + ImportExpression::Generated(runtime_template.export_from_import( compilation, init_fragments, module.identifier(), @@ -541,7 +547,7 @@ impl ESMImportSpecifierDependencyTemplate { !dep.direct_import, Some(dep.shorthand || dep.asi_safe), dep.phase, - ) + )) } } @@ -643,12 +649,20 @@ impl ESMImportSpecifierDependencyTemplate { source, code_generatable_context, ); - source.replace( - dep.range.start, - dep.range.end, - format!("{} in {code}", json_stringify_str(used_name.as_str())), - None, - ) + match code { + ImportExpression::Generated(code) => source.replace( + dep.range.start, + dep.range.end, + format!("{} in {code}", json_stringify_str(used_name.as_str())), + None, + ), + ImportExpression::Tracked(code) => source.replace_with_tracked_used_names( + dep.range.start, + dep.range.end, + format!("{} in {code}", json_stringify_str(used_name.as_str())), + None, + ), + } } } } @@ -705,10 +719,23 @@ impl DependencyTemplate for ESMImportSpecifierDependencyTemplate { let export_expr = self.get_code_for_ids(ids, dep, connection, source, code_generatable_context); - if dep.shorthand { - source.insert_shorthand_value(dep.range.end, dep.range, format!(": {export_expr}"), None); - } else { - source.replace(dep.range.start, dep.range.end, export_expr, None); + match (dep.shorthand, export_expr) { + (true, ImportExpression::Generated(export_expr)) => { + source.insert_shorthand_value(dep.range.end, dep.range, format!(": {export_expr}"), None) + } + (true, ImportExpression::Tracked(export_expr)) => source + .insert_shorthand_value_with_tracked_used_names( + dep.range.end, + dep.range, + format!(": {export_expr}"), + None, + ), + (false, ImportExpression::Generated(export_expr)) => { + source.replace(dep.range.start, dep.range.end, export_expr, None) + } + (false, ImportExpression::Tracked(export_expr)) => { + source.replace_with_tracked_used_names(dep.range.start, dep.range.end, export_expr, None) + } } let module_graph = code_generatable_context.compilation.get_module_graph(); diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs index 9e5b6995c4b9..a3451315e8c3 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/import_meta_rsc_dependency.rs @@ -195,7 +195,7 @@ impl DependencyTemplate for ImportMetaRscDependencyTemplate { init_fragments.push(Box::new(fragment)); if let Some(range) = dependency.range { - source.replace(range.start, range.end, rendered_binding, None); + source.replace_with_tracked_used_names(range.start, range.end, rendered_binding, None); } } } diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs index 90f23de7f527..5b86ad2376fa 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/provide_dependency.rs @@ -202,6 +202,11 @@ impl DependencyTemplate for ProvideDependencyTemplate { ); fragment.set_top_level_decl_symbols(vec![dep.identifier.clone().into()]); init_fragments.push(Box::new(fragment)); - source.replace(dep.range.start, dep.range.end, rendered_identifier, None); + source.replace_with_tracked_used_names( + dep.range.start, + dep.range.end, + rendered_identifier, + None, + ); } } diff --git a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs index 2f12c4fc9ea8..3e9f71798309 100644 --- a/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs +++ b/crates/rspack_plugin_rstest/src/module_path_name_dependency.rs @@ -73,7 +73,12 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { if let (Some(range), Some(rendered_identifier)) = (dep.range, rendered_identifier.as_ref()) { - source.replace(range.start, range.end, rendered_identifier.clone(), None); + source.replace_with_tracked_used_names( + range.start, + range.end, + rendered_identifier.clone(), + None, + ); } let init = NormalInitFragment::new( format!( @@ -98,7 +103,12 @@ impl DependencyTemplate for ModulePathNameDependencyTemplate { let identifier = rendered_identifier.as_deref().unwrap_or("__dirname"); if let (Some(range), Some(rendered_identifier)) = (dep.range, rendered_identifier.as_ref()) { - source.replace(range.start, range.end, rendered_identifier.clone(), None); + source.replace_with_tracked_used_names( + range.start, + range.end, + rendered_identifier.clone(), + None, + ); } // If the parent path is None, we use an empty string // to avoid issues with the path being undefined. From 1f96014b635847ddab907ce163333ee4164b2924 Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Fri, 14 Aug 2026 11:18:47 +0800 Subject: [PATCH 05/10] refactor: type concatenation code generation sources --- .../src/artifacts/code_generation_results.rs | 13 ++- crates/rspack_core/src/concatenated_module.rs | 81 +++++++++++-------- crates/rspack_core/src/external_module.rs | 72 ++++++++++++----- crates/rspack_core/src/normal_module.rs | 53 +++++++++--- .../rspack_core/src/parser_and_generator.rs | 57 ++++++++++++- crates/rspack_plugin_asset/src/lib.rs | 25 +++--- .../src/parser_and_generator/generator.rs | 23 ++++-- .../src/parser_and_generator/mod.rs | 9 ++- .../src/parser_and_generator/mod.rs | 17 ++-- crates/rspack_plugin_json/src/lib.rs | 15 ++-- crates/rspack_plugin_rslib/src/asset.rs | 8 +- .../src/parser_and_generator.rs | 12 +-- 12 files changed, 267 insertions(+), 118 deletions(-) diff --git a/crates/rspack_core/src/artifacts/code_generation_results.rs b/crates/rspack_core/src/artifacts/code_generation_results.rs index 3aaabb1ffa0c..16dde77c7f20 100644 --- a/crates/rspack_core/src/artifacts/code_generation_results.rs +++ b/crates/rspack_core/src/artifacts/code_generation_results.rs @@ -13,9 +13,9 @@ use rustc_hash::{FxHashMap as HashMap, FxHashSet}; use serde::Serialize; use crate::{ - ArtifactExt, AssetInfo, BindingCell, ChunkInitFragments, ConcatenationScope, ModuleIdentifier, - RenderedInitFragments, RuntimeGlobals, RuntimeSpec, RuntimeSpecMap, SourceType, - incremental::IncrementalPasses, + ArtifactExt, AssetInfo, BindingCell, ChunkInitFragments, ConcatenationCodeGenerationSource, + ConcatenationScope, ModuleIdentifier, RenderedInitFragments, RuntimeGlobals, RuntimeSpec, + RuntimeSpecMap, SourceType, incremental::IncrementalPasses, }; #[derive(Clone, Debug)] @@ -135,6 +135,9 @@ impl DerefMut for CodeGenerationData { #[derive(Debug, Default, Clone)] pub struct CodeGenerationResult { pub inner: BindingCell>, + /// Editable JavaScript output used only while generating a concatenated + /// module. Keeping it typed avoids inferring mutability from `BoxSource`. + pub concatenation_source: Option>, /// [definition in webpack](https://github.com/webpack/webpack/blob/4b4ca3bb53f36a5b8fc6bc1bd976ed7af161bd80/lib/Module.js#L75) pub data: CodeGenerationData, pub chunk_init_fragments: ChunkInitFragments, @@ -163,6 +166,10 @@ impl CodeGenerationResult { debug_assert!(result.is_none()); } + pub fn set_concatenation_source(&mut self, source: Box) { + debug_assert!(self.concatenation_source.replace(source).is_none()); + } + pub fn set_hash( &mut self, hash_function: &HashFunction, diff --git a/crates/rspack_core/src/concatenated_module.rs b/crates/rspack_core/src/concatenated_module.rs index 685c5d5fb825..64c8c3659b00 100644 --- a/crates/rspack_core/src/concatenated_module.rs +++ b/crates/rspack_core/src/concatenated_module.rs @@ -45,18 +45,19 @@ use crate::{ BuildMetaExportsType, BuildResult, ChunkGraph, ChunkInitFragments, ChunkRenderContext, CodeGenerationDataTopLevelDeclarations, CodeGenerationPublicPathAutoReplace, CodeGenerationResult, CodeGenerationRuntimeRequirementsWrite, Compilation, - ConcatenatedModuleIdent, ConcatenatedModuleReference, ConcatenationScope, - ConditionalInitFragment, ConnectionState, Context, DEFAULT_EXPORT, DEFAULT_EXPORT_ATOM, - DependenciesBlock, DependencyId, DependencyRange, DependencyType, ExportInfo, ExportProvided, - ExportsArgument, ExportsInfoArtifact, ExportsType, FactoryMeta, ImportedByDeferModulesArtifact, - InitFragment, InitFragmentStage, LibIdentOptions, Module, ModuleArgument, - ModuleCodeGenerationContext, ModuleGraph, ModuleGraphCacheArtifact, ModuleGraphConnection, - ModuleIdentifier, ModuleLayer, ModuleStaticCache, ModuleType, NAMESPACE_OBJECT_EXPORT, - ParserOptions, PendingConcatenationScopeInfo, RenderedInitFragments, RenderedInitFragmentsDigest, - Resolve, RuntimeCondition, RuntimeGlobals, RuntimeSpec, SideEffectsStateArtifact, SourceType, - URLStaticMode, UsageState, UsedName, UsedNameItem, escape_identifier, fast_set, filter_runtime, - find_target, get_runtime_key, impl_source_map_config, merge_runtime_condition, - merge_runtime_condition_non_false, module_update_hash, property_access, property_name, + ConcatenatedModuleIdent, ConcatenatedModuleReference, ConcatenationCodeGenerationSource, + ConcatenationScope, ConditionalInitFragment, ConnectionState, Context, DEFAULT_EXPORT, + DEFAULT_EXPORT_ATOM, DependenciesBlock, DependencyId, DependencyRange, DependencyType, + ExportInfo, ExportProvided, ExportsArgument, ExportsInfoArtifact, ExportsType, FactoryMeta, + ImportedByDeferModulesArtifact, InitFragment, InitFragmentStage, LibIdentOptions, Module, + ModuleArgument, ModuleCodeGenerationContext, ModuleGraph, ModuleGraphCacheArtifact, + ModuleGraphConnection, ModuleIdentifier, ModuleLayer, ModuleStaticCache, ModuleType, + NAMESPACE_OBJECT_EXPORT, ParserOptions, PendingConcatenationScopeInfo, RenderedInitFragments, + RenderedInitFragmentsDigest, Resolve, RuntimeCondition, RuntimeGlobals, RuntimeSpec, + SideEffectsStateArtifact, SourceType, URLStaticMode, UsageState, UsedName, UsedNameItem, + escape_identifier, fast_set, filter_runtime, find_target, get_runtime_key, + impl_source_map_config, merge_runtime_condition, merge_runtime_condition_non_false, + module_update_hash, property_access, property_name, render_make_deferred_namespace_mode_from_exports_type, reserved_names::RESERVED_NAMES_ATOM_SET, subtract_runtime_condition, to_identifier_with_escaped, to_normal_comment, @@ -2693,6 +2694,7 @@ impl ConcatenatedModule { let CodeGenerationResult { mut inner, + mut concatenation_source, mut chunk_init_fragments, mut runtime_requirements, concatenation_scope, @@ -2708,9 +2710,6 @@ impl ConcatenatedModule { let mut concatenation_scope = concatenation_scope.expect("should have concatenation_scope"); let faster_module_concatenation_info = concatenation_scope.take_faster_module_concatenation_info(); - let rendered_source = inner - .remove(&SourceType::JavaScript) - .expect("should have javascript source"); let mut module_info = concatenation_scope.current_module; let (result_source, internal_source) = if let Some(mut faster_module_concatenation_info) = faster_module_concatenation_info @@ -2724,35 +2723,46 @@ impl ConcatenatedModule { "module {module_id} entered faster module concatenation without pending concatenation scope info" ) })?; + let concatenation_source = concatenation_source.take().ok_or_else(|| { + rspack_error::error!( + "module {module_id} entered faster module concatenation without an editable concatenation source" + ) + })?; + let source_kind_matches = matches!( + (pending_scope_info, concatenation_source.as_ref()), + ( + PendingConcatenationScopeInfo::Analyzed(_), + ConcatenationCodeGenerationSource::Analyzed(_) + ) | ( + PendingConcatenationScopeInfo::Generated, + ConcatenationCodeGenerationSource::Generated(_) + ) + ); + if !source_kind_matches { + return Err(rspack_error::error!( + "module {module_id} returned a concatenation source that does not match its pending scope info" + )); + } module_info.rendered_init_fragments = codegen_res .data .get::() .cloned() .filter(|fragments| !fragments.is_empty()); - // JavaScript code generation already returns a ReplaceSource whose - // coordinates are based on the make-time parser source. Keep that - // coordinate space so the pending info spans can be applied directly. The - // generated source is uniquely owned here, so recover it from the Arc - // without cloning every dependency replacement. - let result_source = if rendered_source.as_any().is::() { - let source = Arc::downcast::(rendered_source.into_any_arc()) - .expect("source type was checked above"); - Arc::try_unwrap(source).unwrap_or_else(|source| source.as_ref().clone()) - } else { - ReplaceSource::new(rendered_source) - }; + let result_source = (*concatenation_source).into_source(); let original_source = if matches!( pending_scope_info, PendingConcatenationScopeInfo::Analyzed(_) ) { - assert!( - module - .source() - .is_some_and(|source| Arc::ptr_eq(result_source.inner(), source)), - "generated ReplaceSource inner should preserve the normal module source" - ); - result_source.inner().source().into_string_lossy() + module + .source() + .ok_or_else(|| { + rspack_error::error!( + "module {module_id} has analyzed concatenation scope info without a make-time source" + ) + })? + .source() + .into_string_lossy() } else { Cow::Borrowed("") }; @@ -2765,6 +2775,9 @@ impl ConcatenatedModule { let internal_source = result_source.inner().clone(); (result_source, internal_source) } else { + let rendered_source = inner + .remove(&SourceType::JavaScript) + .expect("should have javascript source"); let source_code = rendered_source.source().into_string_lossy(); let jsx = module .as_ref() diff --git a/crates/rspack_core/src/external_module.rs b/crates/rspack_core/src/external_module.rs index 7aa6617ae11f..0f74d0476b3c 100644 --- a/crates/rspack_core/src/external_module.rs +++ b/crates/rspack_core/src/external_module.rs @@ -15,12 +15,12 @@ use crate::{ BuildMetaExportsType, BuildResult, ChunkGraph, ChunkInitFragments, ChunkUkey, CodeGenerationDataUrl, CodeGenerationResult, Compilation, ConcatenationScope, ConcatenationScopeInfoMode, Context, DependenciesBlock, DependencyId, ExportProvided, - ExternalType, FactoryMeta, ImportAttributes, ImportPhase, InitFragmentExt, InitFragmentKey, - InitFragmentStage, LibIdentOptions, Module, ModuleArgument, ModuleCodeGenerationContext, - ModuleCodeTemplate, ModuleGraph, ModuleType, NAMESPACE_OBJECT_EXPORT, NormalInitFragment, - PendingConcatenationScopeInfo, RuntimeGlobals, RuntimeSpec, SourceType, StaticExportsDependency, - StaticExportsSpec, UsageState, UsedExports, UsedNameItem, extract_url_and_global, - impl_module_meta_info, module_update_hash, property_access, + ExternalType, FactoryMeta, GeneratedSource, ImportAttributes, ImportPhase, InitFragmentExt, + InitFragmentKey, InitFragmentStage, LibIdentOptions, Module, ModuleArgument, + ModuleCodeGenerationContext, ModuleCodeTemplate, ModuleGraph, ModuleType, + NAMESPACE_OBJECT_EXPORT, NormalInitFragment, PendingConcatenationScopeInfo, RuntimeGlobals, + RuntimeSpec, SourceType, StaticExportsDependency, StaticExportsSpec, UsageState, UsedExports, + UsedNameItem, extract_url_and_global, impl_module_meta_info, module_update_hash, property_access, rspack_sources::{BoxSource, RawStringSource, ReplaceSource, SourceExt}, to_identifier, }; @@ -596,7 +596,7 @@ impl ExternalModule { runtime: Option<&RuntimeSpec>, mut concatenation_scope: Option<&mut ConcatenationScope>, runtime_template: &mut ModuleCodeTemplate, - ) -> Result<(BoxSource, ChunkInitFragments)> { + ) -> Result<(GeneratedSource, ChunkInitFragments)> { let mut chunk_init_fragments: ChunkInitFragments = Default::default(); let supports_const = compilation.options.output.environment.supports_const(); let resolved_external_type = self.resolve_external_type(); @@ -1100,9 +1100,11 @@ if(typeof {global} !== "undefined") return resolve(); ) } }; + let faster_module_concatenation = concatenation_scope + .as_deref() + .is_some_and(|scope| scope.is_faster_module_concatenation()); let generated_namespace_export = concatenation_scope.as_deref().and_then(|scope| { - scope - .is_faster_module_concatenation() + faster_module_concatenation .then(|| { scope .current_module @@ -1113,7 +1115,13 @@ if(typeof {global} !== "undefined") return resolve(); .flatten() }); let Some(generated_namespace_export) = generated_namespace_export else { - return Ok((RawStringSource::from(source).boxed(), chunk_init_fragments)); + let source = RawStringSource::from(source); + let source = if faster_module_concatenation { + GeneratedSource::generated_concatenation(ReplaceSource::new(source)) + } else { + source.boxed().into() + }; + return Ok((source, chunk_init_fragments)); }; let declaration_prefix = if supports_const { "const " } else { "var " }; @@ -1129,7 +1137,10 @@ if(typeof {global} !== "undefined") return resolve(); generated_namespace_export.placeholder.to_string(), None, ); - Ok((rendered_source.boxed(), chunk_init_fragments)) + Ok(( + GeneratedSource::generated_concatenation(rendered_source), + chunk_init_fragments, + )) } } @@ -1328,15 +1339,31 @@ impl Module for ExternalModule { match self.external_type.as_str() { "asset" if request.is_some() => { let request = request.expect("request should be some"); - cgr.add( - SourceType::JavaScript, - RawStringSource::from(format!( - "{}.exports = {};", - runtime_template.render_module_argument(ModuleArgument::Module), - rspack_util::json_stringify_str(request.primary()) - )) - .boxed(), - ); + if concatenation_scope.is_some() { + let (source, chunk_init_fragments) = self.get_source( + compilation, + Some(request), + external_type, + *runtime, + concatenation_scope.as_mut(), + runtime_template, + )?; + match source { + GeneratedSource::Source(source) => cgr.add(SourceType::JavaScript, source), + GeneratedSource::Concatenation(source) => cgr.set_concatenation_source(source), + } + cgr.chunk_init_fragments = chunk_init_fragments; + } else { + cgr.add( + SourceType::JavaScript, + RawStringSource::from(format!( + "{}.exports = {};", + runtime_template.render_module_argument(ModuleArgument::Module), + rspack_util::json_stringify_str(request.primary()) + )) + .boxed(), + ); + } cgr .data .insert(CodeGenerationDataUrl::new(request.primary().to_string())); @@ -1361,7 +1388,10 @@ impl Module for ExternalModule { concatenation_scope.as_mut(), runtime_template, )?; - cgr.add(SourceType::JavaScript, source); + match source { + GeneratedSource::Source(source) => cgr.add(SourceType::JavaScript, source), + GeneratedSource::Concatenation(source) => cgr.set_concatenation_source(source), + } cgr.chunk_init_fragments = chunk_init_fragments; } }; diff --git a/crates/rspack_core/src/normal_module.rs b/crates/rspack_core/src/normal_module.rs index 1a7ecd023bd6..3443e44bbe79 100644 --- a/crates/rspack_core/src/normal_module.rs +++ b/crates/rspack_core/src/normal_module.rs @@ -18,8 +18,8 @@ use rspack_hash::{RspackHash, RspackHashDigest, RspackHasher}; use rspack_hook::define_hook; use rspack_loader_runner::{AdditionalData, Content, LoaderContext, ResourceData, run_loaders}; use rspack_sources::{ - BoxSource, CachedSource, OriginalSource, RawBufferSource, RawStringSource, SourceExt, SourceMap, - SourceMapSource, WithoutOriginalOptions, + BoxSource, CachedSource, OriginalSource, RawBufferSource, RawStringSource, ReplaceSource, + SourceExt, SourceMap, SourceMapSource, WithoutOriginalOptions, }; use rspack_util::source_map::{ModuleSourceMapConfig, SourceMapKind}; use serde_json::json; @@ -29,12 +29,12 @@ use crate::{ AsyncDependenciesBlockIdentifier, BoxDependencyTemplate, BoxLoader, BoxModule, BoxModuleDependency, BuildContext, BuildInfo, BuildMeta, BuildResult, ChunkGraph, CodeGenerationResult, Compilation, ConcatenationScopeInfoMode, ConnectionState, Context, - DependenciesBlock, DependencyId, FactoryMeta, GenerateContext, GeneratorOptions, ImportPhase, - LibIdentOptions, Module, ModuleCodeGenerationContext, ModuleGraph, ModuleGraphCacheArtifact, - ModuleIdentifier, ModuleLayer, ModuleType, OptimizationBailoutItem, OutputOptions, ParseContext, - ParseResult, ParserAndGenerator, ParserOptions, PendingConcatenationScopeInfo, Resolve, - ResolvedModuleOptions, RspackLoaderRunnerPlugin, RunnerContext, RuntimeGlobals, RuntimeSpec, - SideEffectsStateArtifact, SourceType, contextify, + DependenciesBlock, DependencyId, FactoryMeta, GenerateContext, GeneratedSource, GeneratorOptions, + ImportPhase, LibIdentOptions, Module, ModuleCodeGenerationContext, ModuleGraph, + ModuleGraphCacheArtifact, ModuleIdentifier, ModuleLayer, ModuleType, OptimizationBailoutItem, + OutputOptions, ParseContext, ParseResult, ParserAndGenerator, ParserOptions, + PendingConcatenationScopeInfo, Resolve, ResolvedModuleOptions, RspackLoaderRunnerPlugin, + RunnerContext, RuntimeGlobals, RuntimeSpec, SideEffectsStateArtifact, SourceType, contextify, diagnostics::ModuleBuildError, get_context, module_analyzed_side_effect_free, module_declared_side_effect_free, module_update_hash, @@ -656,6 +656,15 @@ impl Module for NormalModule { let module_graph = compilation.get_module_graph(); for source_type in self.source_types(module_graph) { let in_concatenation = concatenation_scope.is_some(); + let faster_concatenation = concatenation_scope + .as_ref() + .is_some_and(|scope| scope.is_faster_module_concatenation()); + let expects_concatenation_source = + faster_concatenation && *source_type == SourceType::JavaScript; + let analyzed_concatenation_source = (expects_concatenation_source + && self.parser_and_generator.concatenation_scope_info_mode() + == ConcatenationScopeInfoMode::AnalyzeAtMake) + .then(|| ReplaceSource::new(source.clone())); let generation_result = self .parser_and_generator .generate( @@ -670,13 +679,33 @@ impl Module for NormalModule { module_generator_options: self.parser_and_generator_options.generator_options(), runtime: *runtime, concatenation_scope: concatenation_scope.as_mut(), + analyzed_concatenation_source, }, ) .await?; - if in_concatenation { - code_generation_result.add(*source_type, generation_result); - } else { - code_generation_result.add(*source_type, CachedSource::new(generation_result).boxed()); + match (expects_concatenation_source, generation_result) { + (true, GeneratedSource::Concatenation(source)) => { + code_generation_result.set_concatenation_source(source); + } + (true, GeneratedSource::Source(_)) => { + return Err(error!( + "module {} entered faster module concatenation, but its generator returned an untyped JavaScript source", + self.identifier() + )); + } + (false, GeneratedSource::Concatenation(_)) => { + return Err(error!( + "module {} returned a faster concatenation source outside faster JavaScript concatenation", + self.identifier() + )); + } + (false, GeneratedSource::Source(source)) => { + if in_concatenation { + code_generation_result.add(*source_type, source); + } else { + code_generation_result.add(*source_type, CachedSource::new(source).boxed()); + } + } } } code_generation_result.concatenation_scope = std::mem::take(concatenation_scope); diff --git a/crates/rspack_core/src/parser_and_generator.rs b/crates/rspack_core/src/parser_and_generator.rs index f844c5309163..8acbdf5a584f 100644 --- a/crates/rspack_core/src/parser_and_generator.rs +++ b/crates/rspack_core/src/parser_and_generator.rs @@ -8,7 +8,7 @@ use rspack_cacheable::{ use rspack_error::{Result, TWithDiagnosticArray}; use rspack_hash::RspackHashDigest; use rspack_loader_runner::{AdditionalData, ParseMeta, ResourceData}; -use rspack_sources::BoxSource; +use rspack_sources::{BoxSource, ReplaceSource}; use rspack_util::{ext::AsAny, source_map::SourceMapKind}; use rustc_hash::{FxHashMap, FxHashSet}; use swc_core::atoms::Atom; @@ -119,6 +119,59 @@ pub struct GenerateContext<'a> { pub module_generator_options: Option<&'a GeneratorOptions>, pub runtime: Option<&'a RuntimeSpec>, pub concatenation_scope: Option<&'a mut ConcatenationScope>, + pub(crate) analyzed_concatenation_source: Option, +} + +impl GenerateContext<'_> { + /// Takes the editable source initialized from the make-time module source. + /// + /// This is only available to generators using `AnalyzeAtMake` during faster + /// module concatenation. + pub fn take_analyzed_concatenation_source(&mut self) -> Option { + self.analyzed_concatenation_source.take() + } +} + +#[derive(Debug, Clone)] +pub enum ConcatenationCodeGenerationSource { + /// Dependency-template edits are based on the make-time module source. + Analyzed(ReplaceSource), + /// The complete JavaScript source was produced during code generation. + Generated(ReplaceSource), +} + +impl ConcatenationCodeGenerationSource { + pub fn into_source(self) -> ReplaceSource { + match self { + Self::Analyzed(source) | Self::Generated(source) => source, + } + } +} + +#[derive(Debug)] +pub enum GeneratedSource { + Source(BoxSource), + Concatenation(Box), +} + +impl GeneratedSource { + pub fn analyzed_concatenation(source: ReplaceSource) -> Self { + Self::Concatenation(Box::new(ConcatenationCodeGenerationSource::Analyzed( + source, + ))) + } + + pub fn generated_concatenation(source: ReplaceSource) -> Self { + Self::Concatenation(Box::new(ConcatenationCodeGenerationSource::Generated( + source, + ))) + } +} + +impl From for GeneratedSource { + fn from(source: BoxSource) -> Self { + Self::Source(source) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -151,7 +204,7 @@ pub trait ParserAndGenerator: Send + Sync + Debug + AsAny { source: &BoxSource, module: &dyn Module, generate_context: &mut GenerateContext, - ) -> Result; + ) -> Result; fn concatenation_scope_info_mode(&self) -> ConcatenationScopeInfoMode; diff --git a/crates/rspack_plugin_asset/src/lib.rs b/crates/rspack_plugin_asset/src/lib.rs index 2c9137da7e0b..55730ff8845e 100644 --- a/crates/rspack_plugin_asset/src/lib.rs +++ b/crates/rspack_plugin_asset/src/lib.rs @@ -9,9 +9,10 @@ use rspack_core::{ ChunkUkey, CodeGenerationDataAssetInfo, CodeGenerationDataFilename, CodeGenerationDataUrl, CodeGenerationPublicPathAutoReplace, Compilation, CompilationRenderManifest, CompilerOptions, ConcatenationScope, ConcatenationScopeInfoMode, DependencyType, Filename, GenerateContext, - GeneratorOptions, JavascriptParserUrl, ManifestAssetType, Module, ModuleArgument, ModuleGraph, - NAMESPACE_OBJECT_EXPORT, NormalModule, ParseContext, ParserAndGenerator, ParserOptions, PathData, - Plugin, PublicPath, RenderManifestEntry, ResourceData, RuntimeGlobals, RuntimeSpec, SourceType, + GeneratedSource, GeneratorOptions, JavascriptParserUrl, ManifestAssetType, Module, + ModuleArgument, ModuleGraph, NAMESPACE_OBJECT_EXPORT, NormalModule, ParseContext, + ParserAndGenerator, ParserOptions, PathData, Plugin, PublicPath, RenderManifestEntry, + ResourceData, RuntimeGlobals, RuntimeSpec, SourceType, rspack_sources::{BoxSource, RawStringSource, ReplaceSource, SourceExt}, }; use rspack_error::{Diagnostic, IntoTWithDiagnosticArray, Result, error}; @@ -48,7 +49,7 @@ fn render_concatenated_asset_source( scope: &mut ConcatenationScope, prefix: &str, suffix: String, -) -> BoxSource { +) -> GeneratedSource { let faster_module_concatenation = scope.is_faster_module_concatenation(); let namespace_export = scope.register_generated_namespace_export(NAMESPACE_OBJECT_EXPORT); let rendered_namespace_export = if faster_module_concatenation { @@ -58,7 +59,7 @@ fn render_concatenated_asset_source( }; let source = RawStringSource::from(format!("{prefix}{rendered_namespace_export}{suffix}")); if !faster_module_concatenation { - return source.boxed(); + return source.boxed().into(); } let start = prefix.len() as u32; @@ -69,7 +70,7 @@ fn render_concatenated_asset_source( namespace_export.to_string(), None, ); - source.boxed() + GeneratedSource::generated_concatenation(source) } #[cacheable] @@ -548,7 +549,7 @@ impl ParserAndGenerator for AssetParserAndGenerator { source: &BoxSource, module: &dyn Module, generate_context: &mut GenerateContext, - ) -> Result { + ) -> Result { let compilation = generate_context.compilation; let asset_build_info = module .build_info() @@ -690,7 +691,7 @@ impl ParserAndGenerator for AssetParserAndGenerator { }; if generate_context.requested_source_type == SourceType::CssUrl { - return Ok(RawStringSource::from_static("").boxed()); + return Ok(RawStringSource::from_static("").boxed().into()); } if import_mode.is_preserve() && parsed_asset_config.is_resource() { @@ -719,7 +720,8 @@ impl ParserAndGenerator for AssetParserAndGenerator { .runtime_template .render_module_argument(ModuleArgument::Module) )) - .boxed(), + .boxed() + .into(), ); } }; @@ -740,7 +742,8 @@ impl ParserAndGenerator for AssetParserAndGenerator { .runtime_template .render_module_argument(ModuleArgument::Module) )) - .boxed(), + .boxed() + .into(), ) } } @@ -782,7 +785,7 @@ impl ParserAndGenerator for AssetParserAndGenerator { .data .insert(CodeGenerationDataAssetInfo::new(asset_info)); - Ok(source.clone().boxed()) + Ok(source.clone().boxed().into()) } } _ => panic!( diff --git a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs index 32c1cb6d43ee..30c4b9b14a85 100644 --- a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs +++ b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs @@ -4,9 +4,9 @@ use concat_string::concat_string; use rspack_core::{ BoxDependency, ChunkGraph, ConcatenationScope, Context, CssBuildInfo, CssExport, CssExportType, CssExports, CssModuleRenderCondition, DependencyCodeGeneration, DependencyId, DependencyType, - ExportsArgument, GenerateContext, Module, ModuleArgument, ModuleIdentifier, ModuleInitFragments, - RESERVED_IDENTIFIER, RuntimeGlobals, SourceType, TemplateContext, TemplateReplaceSource, - UsageState, UsedNameItem, css_module_render_conditions_identifier, + ExportsArgument, GenerateContext, GeneratedSource, Module, ModuleArgument, ModuleIdentifier, + ModuleInitFragments, RESERVED_IDENTIFIER, RuntimeGlobals, SourceType, TemplateContext, + TemplateReplaceSource, UsageState, UsedNameItem, css_module_render_conditions_identifier, rspack_sources::{ BoxSource, ConcatSource, OriginalSource, RawStringSource, ReplaceSource, Source, SourceExt, }, @@ -188,7 +188,7 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { self.render_css_module_source() } - pub fn generate_javascript_source(mut self) -> Result { + pub fn generate_javascript_source(mut self) -> Result { match self.export_type { Some(CssExportType::Text) => { let css = self.css_text_expr_with_imports(); @@ -224,15 +224,24 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { } else { RawStringSource::from(generated_source).boxed() }; - if self.generated_symbol_replacements.is_empty() { - return Ok(source); + let faster_module_concatenation = self + .generate_context + .concatenation_scope + .as_ref() + .is_some_and(|scope| scope.is_faster_module_concatenation()); + if self.generated_symbol_replacements.is_empty() && !faster_module_concatenation { + return Ok(source.into()); } let mut source = ReplaceSource::new(source); for (start, end, symbol) in self.generated_symbol_replacements { source.replace(start, end, symbol, None); } - Ok(source.boxed()) + if faster_module_concatenation { + Ok(GeneratedSource::generated_concatenation(source)) + } else { + Ok(source.boxed().into()) + } } fn generate_js_exports(&mut self) -> Result<()> { diff --git a/crates/rspack_plugin_css/src/parser_and_generator/mod.rs b/crates/rspack_plugin_css/src/parser_and_generator/mod.rs index f9f8df13dfe1..7ee0ed29e92a 100644 --- a/crates/rspack_plugin_css/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_css/src/parser_and_generator/mod.rs @@ -12,8 +12,8 @@ use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_core::{ BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, Compilation, ConcatenationScopeInfoMode, CssAutoOrModuleParserOptions, CssBuildInfo, CssExportType, - DependencyType, ExportsInfoArtifact, GenerateContext, Module, ModuleGraph, ModuleIdentifier, - NormalModule, ParseContext, ParseResult, ParserAndGenerator, ParserOptions, + DependencyType, ExportsInfoArtifact, GenerateContext, GeneratedSource, Module, ModuleGraph, + ModuleIdentifier, NormalModule, ParseContext, ParseResult, ParserAndGenerator, ParserOptions, ResolvedModuleOptions, RuntimeSpec, SourceType, UsageState, rspack_sources::{BoxSource, Source}, }; @@ -289,7 +289,7 @@ impl ParserAndGenerator for CssParserAndGenerator { source: &BoxSource, module: &dyn rspack_core::Module, generate_context: &mut GenerateContext, - ) -> Result { + ) -> Result { match generate_context.requested_source_type { SourceType::Css => Ok( CssModuleGenerator::new( @@ -299,7 +299,8 @@ impl ParserAndGenerator for CssParserAndGenerator { self.hot, self.es_module, ) - .generate_css_source(), + .generate_css_source() + .into(), ), SourceType::JavaScript => CssModuleGenerator::new( source.clone(), diff --git a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs index 84eabb648f8a..379b475b3eda 100644 --- a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs @@ -13,9 +13,10 @@ use rspack_core::{ ArcComputed, AsyncDependenciesBlockIdentifier, BuildMetaExportsType, COLLECTED_TYPESCRIPT_INFO_PARSE_META_KEY, ChunkGraph, CollectedTypeScriptInfo, Compilation, ConcatenationScope, ConcatenationScopeInfoMode, DependenciesBlock, DependencyId, GenerateContext, - ImportMeta, Module, ModuleArgument, ModuleCodeTemplate, ModuleGraph, ModuleType, ParseContext, - ParseResult, ParserAndGenerator, ResolvedModuleOptions, RuntimeGlobals, RuntimeGlobalsRenderMode, - RuntimeVariable, SideEffectsBailoutItem, SourceType, TemplateContext, TemplateReplaceSource, + GeneratedSource, ImportMeta, Module, ModuleArgument, ModuleCodeTemplate, ModuleGraph, ModuleType, + ParseContext, ParseResult, ParserAndGenerator, ResolvedModuleOptions, RuntimeGlobals, + RuntimeGlobalsRenderMode, RuntimeVariable, SideEffectsBailoutItem, SourceType, TemplateContext, + TemplateReplaceSource, diagnostics::map_box_diagnostics_to_module_parse_diagnostics, remove_bom, render_init_fragments, render_init_fragments_to_strings, rspack_sources::{BoxSource, ReplaceSource, Source, SourceExt}, @@ -464,12 +465,14 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { source: &BoxSource, module: &dyn Module, generate_context: &mut GenerateContext, - ) -> Result { + ) -> Result { if matches!( generate_context.requested_source_type, SourceType::JavaScript ) { - let mut source = ReplaceSource::new(source.clone()); + let mut source = generate_context + .take_analyzed_concatenation_source() + .unwrap_or_else(|| ReplaceSource::new(source.clone())); let compilation = generate_context.compilation; let mut init_fragments = vec![]; let mut concatenation_scope = generate_context.concatenation_scope.take(); @@ -531,10 +534,10 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { generate_context.data.insert(rendered_fragments); } generate_context.concatenation_scope = concatenation_scope; - return Ok(source.boxed()); + return Ok(GeneratedSource::analyzed_concatenation(source)); } generate_context.concatenation_scope = concatenation_scope; - render_init_fragments(source.boxed(), init_fragments, generate_context) + render_init_fragments(source.boxed(), init_fragments, generate_context).map(Into::into) } else { panic!( "Unsupported source type: {:?}", diff --git a/crates/rspack_plugin_json/src/lib.rs b/crates/rspack_plugin_json/src/lib.rs index 969cf0c47b75..0d2776ea0716 100644 --- a/crates/rspack_plugin_json/src/lib.rs +++ b/crates/rspack_plugin_json/src/lib.rs @@ -13,9 +13,9 @@ use json::{ use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_core::{ BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, ConcatenationScopeInfoMode, - ExportsInfoArtifact, ExportsInfoData, GenerateContext, GeneratorOptions, Module, ModuleArgument, - ModuleGraph, NAMESPACE_OBJECT_EXPORT, ParseOption, ParserAndGenerator, ParserOptions, Plugin, - RuntimeSpec, SourceType, UsageState, UsedNameItem, + ExportsInfoArtifact, ExportsInfoData, GenerateContext, GeneratedSource, GeneratorOptions, Module, + ModuleArgument, ModuleGraph, NAMESPACE_OBJECT_EXPORT, ParseOption, ParserAndGenerator, + ParserOptions, Plugin, RuntimeSpec, SourceType, UsageState, UsedNameItem, diagnostics::ModuleParseError, rspack_sources::{BoxSource, RawStringSource, ReplaceSource, Source, SourceExt}, }; @@ -172,7 +172,7 @@ impl ParserAndGenerator for JsonParserAndGenerator { _source: &BoxSource, module: &dyn rspack_core::Module, generate_context: &mut GenerateContext, - ) -> Result { + ) -> Result { let GenerateContext { compilation, runtime, @@ -240,9 +240,9 @@ impl ParserAndGenerator for JsonParserAndGenerator { namespace_export.to_string(), None, ); - return Ok(source.boxed()); + return Ok(GeneratedSource::generated_concatenation(source)); } - return Ok(source.boxed()); + return Ok(source.boxed().into()); } Ok( @@ -252,7 +252,8 @@ impl ParserAndGenerator for JsonParserAndGenerator { .runtime_template .render_module_argument(ModuleArgument::Module) )) - .boxed(), + .boxed() + .into(), ) } _ => panic!( diff --git a/crates/rspack_plugin_rslib/src/asset.rs b/crates/rspack_plugin_rslib/src/asset.rs index b2cbef2912e8..0a75886449a5 100644 --- a/crates/rspack_plugin_rslib/src/asset.rs +++ b/crates/rspack_plugin_rslib/src/asset.rs @@ -2,9 +2,9 @@ use std::borrow::Cow; use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_core::{ - ChunkGraph, Compilation, ConcatenationScopeInfoMode, GenerateContext, Module, ModuleGraph, - NormalModule, ParseContext, ParseResult, ParserAndGenerator, RuntimeSpec, SourceType, - rspack_sources::BoxSource, + ChunkGraph, Compilation, ConcatenationScopeInfoMode, GenerateContext, GeneratedSource, Module, + ModuleGraph, NormalModule, ParseContext, ParseResult, ParserAndGenerator, RuntimeSpec, + SourceType, rspack_sources::BoxSource, }; use rspack_error::{Result, TWithDiagnosticArray}; use rspack_hash::RspackHashDigest; @@ -65,7 +65,7 @@ impl ParserAndGenerator for RslibAssetParserAndGenerator { source: &BoxSource, module: &dyn Module, generate_context: &mut GenerateContext, - ) -> Result { + ) -> Result { self.0.generate(source, module, generate_context).await } diff --git a/crates/rspack_plugin_wasm/src/parser_and_generator.rs b/crates/rspack_plugin_wasm/src/parser_and_generator.rs index caaf4da21c6d..3aa47794c39a 100644 --- a/crates/rspack_plugin_wasm/src/parser_and_generator.rs +++ b/crates/rspack_plugin_wasm/src/parser_and_generator.rs @@ -4,8 +4,8 @@ use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_collections::IdentifierIndexMap; use rspack_core::{ BoxDependency, BuildMetaExportsType, ConcatenationScopeInfoMode, Dependency, DependencyId, - DependencyType, ExportsArgument, GenerateContext, ImportPhase, Module, ModuleArgument, - ModuleDependency, ModuleGraph, ModuleInitFragments, ParseContext, ParseResult, + DependencyType, ExportsArgument, GenerateContext, GeneratedSource, ImportPhase, Module, + ModuleArgument, ModuleDependency, ModuleGraph, ModuleInitFragments, ParseContext, ParseResult, ParserAndGenerator, RuntimeGlobals, SourceType, StaticExportsDependency, StaticExportsSpec, rspack_sources::{BoxSource, RawStringSource, Source, SourceExt}, }; @@ -161,7 +161,7 @@ impl ParserAndGenerator for AsyncWasmParserAndGenerator { source: &BoxSource, module: &dyn Module, generate_context: &mut GenerateContext, - ) -> Result { + ) -> Result { let GenerateContext { compilation, runtime, @@ -197,7 +197,7 @@ impl ParserAndGenerator for AsyncWasmParserAndGenerator { runtime_template.render_runtime_globals(&RuntimeGlobals::ASYNC_MODULE), runtime_template.render_runtime_globals(&RuntimeGlobals::DEFINE_PROPERTY_GETTERS), )); - return Ok(source.boxed()); + return Ok(source.boxed().into()); } let mut dep_modules = IdentifierIndexMap::::default(); @@ -340,9 +340,9 @@ impl ParserAndGenerator for AsyncWasmParserAndGenerator { )) }; - Ok(source.boxed()) + Ok(source.boxed().into()) } - _ => Ok(source.clone()), + _ => Ok(source.clone().into()), } } From 8c2b0c262994ab42ea60bfc36f3ec166cd811eb6 Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Fri, 14 Aug 2026 12:34:28 +0800 Subject: [PATCH 06/10] fix: preserve concatenation source in release builds --- crates/rspack_core/src/artifacts/code_generation_results.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rspack_core/src/artifacts/code_generation_results.rs b/crates/rspack_core/src/artifacts/code_generation_results.rs index 16dde77c7f20..3741b13df600 100644 --- a/crates/rspack_core/src/artifacts/code_generation_results.rs +++ b/crates/rspack_core/src/artifacts/code_generation_results.rs @@ -167,7 +167,8 @@ impl CodeGenerationResult { } pub fn set_concatenation_source(&mut self, source: Box) { - debug_assert!(self.concatenation_source.replace(source).is_none()); + let previous = self.concatenation_source.replace(source); + debug_assert!(previous.is_none()); } pub fn set_hash( From c2d7e4f6572eae5c9232e0d1d35d684c7a89afb5 Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Fri, 14 Aug 2026 15:39:22 +0800 Subject: [PATCH 07/10] fix: preserve generated bindings in faster concatenation --- crates/rspack_core/src/concatenated_module.rs | 21 ++- .../src/concatenated_module/faster.rs | 33 +++- .../src/dependency/const_dependency.rs | 35 ++++- .../src/dependency/dependency_template.rs | 52 +++++- crates/rspack_core/src/external_module.rs | 5 +- .../src/utils/concatenation_scope.rs | 48 +++++- .../src/parser_plugin/const/if_stmt.rs | 148 ++++++++++++------ .../src/parser_plugin/const/mod.rs | 8 +- .../hoisted-declaration-rebinding/direct.js | 1 + .../hoisted-declaration-rebinding/index.js | 13 ++ .../hoisted-declaration-rebinding/removed.js | 7 + .../test.config.js | 6 + 12 files changed, 306 insertions(+), 71 deletions(-) create mode 100644 tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/direct.js create mode 100644 tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/index.js create mode 100644 tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/removed.js create mode 100644 tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/test.config.js diff --git a/crates/rspack_core/src/concatenated_module.rs b/crates/rspack_core/src/concatenated_module.rs index 64c8c3659b00..14f223dc8a96 100644 --- a/crates/rspack_core/src/concatenated_module.rs +++ b/crates/rspack_core/src/concatenated_module.rs @@ -308,6 +308,18 @@ pub type ConcatenatedImportMap = pub struct GeneratedTopLevelSymbol { pub preferred_name: Atom, pub placeholder: Atom, + pub target: GeneratedTopLevelSymbolTarget, + /// Make-time binding resolved from `original_range` for a rebound symbol. + pub resolved_binding: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GeneratedTopLevelSymbolTarget { + /// A new binding created during code generation. + New, + /// A declaration recreated during code generation that must stay bound to + /// the original make-time binding. + Rebind { original_range: DependencyRange }, } /// Scope identifier supplied by dependency templates because it cannot be @@ -1201,7 +1213,9 @@ impl Module for ConcatenatedModule { let name = info .generated_top_level_symbols .iter() - .find(|symbol| symbol.placeholder == id.0) + .find(|symbol| { + symbol.target == GeneratedTopLevelSymbolTarget::New && symbol.placeholder == id.0 + }) .map_or(&id.0, |symbol| &symbol.preferred_name); escaped_names .entry(name.clone()) @@ -1282,7 +1296,10 @@ impl Module for ConcatenatedModule { let name = info .generated_top_level_symbols .iter() - .find(|symbol| symbol.placeholder == *binding_name) + .find(|symbol| { + symbol.target == GeneratedTopLevelSymbolTarget::New + && symbol.placeholder == *binding_name + }) .map_or(binding_name, |symbol| &symbol.preferred_name); // Check if the name is already used let final_name = if name_allocator.contains(name) { diff --git a/crates/rspack_core/src/concatenated_module/faster.rs b/crates/rspack_core/src/concatenated_module/faster.rs index aa5da005f036..63e5a7bacea0 100644 --- a/crates/rspack_core/src/concatenated_module/faster.rs +++ b/crates/rspack_core/src/concatenated_module/faster.rs @@ -13,8 +13,8 @@ use swc_core::{ use super::{ CONCATENATION_PLACEHOLDER_PREFIX, ConcatenatedModuleInfo, FasterModuleConcatenationInfo, - GENERATED_TOP_LEVEL_SYMBOL_PREFIX, GeneratedTopLevelSymbol, MODULE_REFERENCE_PLACEHOLDER_PREFIX, - MODULE_REFERENCE_SUFFIX, OriginalScopeIdentUpdate, + GENERATED_TOP_LEVEL_SYMBOL_PREFIX, GeneratedTopLevelSymbol, GeneratedTopLevelSymbolTarget, + MODULE_REFERENCE_PLACEHOLDER_PREFIX, MODULE_REFERENCE_SUFFIX, OriginalScopeIdentUpdate, }; use crate::{ ConcatenatedModuleIdent, ConcatenationScope, ConcatenationScopeIdentKind, DependencyRange, @@ -105,10 +105,11 @@ impl PlaceholderReplacements<'_> { .generated_symbols .get(index) .filter(|symbol| symbol.placeholder == placeholder)?; - self - .internal_names - .get(&symbol.placeholder) - .map(Atom::as_str) + let binding = match symbol.target { + GeneratedTopLevelSymbolTarget::New => &symbol.placeholder, + GeneratedTopLevelSymbolTarget::Rebind { .. } => symbol.resolved_binding.as_ref()?, + }; + self.internal_names.get(binding).map(Atom::as_str) } } @@ -296,10 +297,26 @@ pub(crate) fn populate_info_from_pending( ); } - for symbol in &module_info.generated_top_level_symbols { + for symbol in &mut module_info.generated_top_level_symbols { + let binding = match symbol.target { + GeneratedTopLevelSymbolTarget::New => symbol.placeholder.clone(), + GeneratedTopLevelSymbolTarget::Rebind { original_range } => { + assert!( + pending_idents.iter().any(|ident| { + ident.kind == ConcatenationScopeIdentKind::TopLevel && ident.range == original_range + }), + "rebound concatenation symbol range {}..{} should refer to a make-time top-level identifier", + original_range.start, + original_range.end + ); + let binding = symbol_from_range(original_range); + symbol.resolved_binding = Some(binding.clone()); + binding + } + }; module_info .binding_to_ref - .entry((symbol.placeholder.clone(), module_info.module_ctxt)) + .entry((binding, module_info.module_ctxt)) .or_default(); } if let Some(export_map) = &module_info.export_map { diff --git a/crates/rspack_core/src/dependency/const_dependency.rs b/crates/rspack_core/src/dependency/const_dependency.rs index 428f0e75be82..cd04f7896fc5 100644 --- a/crates/rspack_core/src/dependency/const_dependency.rs +++ b/crates/rspack_core/src/dependency/const_dependency.rs @@ -4,8 +4,8 @@ use rspack_hash::{RspackHash, RspackHasher}; use super::DependencyRange; use crate::{ - Compilation, DependencyCodeGeneration, DependencyTemplate, DependencyTemplateType, RuntimeSpec, - TemplateContext, TemplateReplaceSource, + Compilation, DependencyCodeGeneration, DependencyTemplate, DependencyTemplateType, + GeneratedCodeRebinding, RuntimeSpec, TemplateContext, TemplateReplaceSource, }; #[cacheable] @@ -17,6 +17,8 @@ pub struct ConstDependency { /// Identifier introduced by `content` that must participate in top-level /// deconfliction when faster module concatenation skips the codegen parse. pub concatenation_scope_identifier: Option>, + /// Generated declarations that recreate bindings from the replaced source. + pub generated_code_rebindings: Vec, } impl ConstDependency { @@ -25,12 +27,25 @@ impl ConstDependency { range, content, concatenation_scope_identifier: None, + generated_code_rebindings: Vec::new(), } } pub fn set_concatenation_scope_identifier(&mut self, identifier: Box) { + assert!( + self.generated_code_rebindings.is_empty(), + "a const dependency cannot create and rebind the same generated identifier" + ); self.concatenation_scope_identifier = Some(identifier); } + + pub fn set_generated_code_rebindings(&mut self, rebindings: Vec) { + assert!( + self.concatenation_scope_identifier.is_none(), + "a const dependency cannot create and rebind the same generated identifier" + ); + self.generated_code_rebindings = rebindings; + } } impl RspackHash for ConstDependency { @@ -39,6 +54,10 @@ impl RspackHash for ConstDependency { state.write(b"|"); self.content.hash(state); self.concatenation_scope_identifier.hash(state); + if !self.generated_code_rebindings.is_empty() { + state.write(b"|rebindings|"); + self.generated_code_rebindings.hash(state); + } } } @@ -94,6 +113,16 @@ impl DependencyTemplate for ConstDependencyTemplate { dep.content.to_string() }; - source.replace(dep.range.start, dep.range.end, rendered_content, None); + if dep.generated_code_rebindings.is_empty() { + source.replace(dep.range.start, dep.range.end, rendered_content, None); + } else { + source.replace_with_rebindings( + dep.range.start, + dep.range.end, + rendered_content, + None, + &dep.generated_code_rebindings, + ); + } } } diff --git a/crates/rspack_core/src/dependency/dependency_template.rs b/crates/rspack_core/src/dependency/dependency_template.rs index cc41d099397e..b62bfb6caa6d 100644 --- a/crates/rspack_core/src/dependency/dependency_template.rs +++ b/crates/rspack_core/src/dependency/dependency_template.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use dyn_clone::{DynClone, clone_trait_object}; -use rspack_cacheable::cacheable_dyn; +use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_hash::RspackHasher; use rspack_sources::{ReplaceSource, ReplacementEnforce}; use rspack_util::ext::AsAny; @@ -20,6 +20,17 @@ pub struct TemplateContext<'a, 'b> { pub runtime_template: &'a mut ModuleCodeTemplate, } +/// Maps an identifier emitted by generated code back to the make-time binding +/// it redeclares. +#[cacheable] +#[derive(Debug, Clone, Copy, rspack_hash::RspackHash)] +pub struct GeneratedCodeRebinding { + /// Identifier range in the original module source. + pub original_range: DependencyRange, + /// Identifier range relative to the generated replacement content. + pub generated_range: DependencyRange, +} + impl TemplateContext<'_, '_> { pub fn chunk_init_fragments(&mut self) -> &mut ChunkInitFragments { let data_fragments = self.data.get::(); @@ -115,6 +126,45 @@ impl<'a> TemplateReplaceSource<'a> { self.source.replace(start, end, content, name); } + /// Replaces source while preserving declarations that recreate existing + /// make-time bindings. + pub fn replace_with_rebindings( + &mut self, + start: u32, + end: u32, + mut content: String, + name: Option, + rebindings: &[GeneratedCodeRebinding], + ) { + if let Some(scope) = self.faster_concatenation_scope() { + assert!( + rebindings + .windows(2) + .all(|pair| { pair[0].generated_range.end <= pair[1].generated_range.start }), + "generated code rebinding ranges should be sorted and non-overlapping" + ); + for rebinding in rebindings.iter().rev() { + let generated_range = rebinding.generated_range; + let preferred_name = content + .get(generated_range.start as usize..generated_range.end as usize) + .unwrap_or_else(|| { + panic!( + "generated code rebinding range {}..{} should be in replacement content", + generated_range.start, generated_range.end + ) + }) + .to_string(); + let placeholder = + scope.rebind_generated_top_level_symbol(&preferred_name, rebinding.original_range); + content.replace_range( + generated_range.start as usize..generated_range.end as usize, + placeholder.as_ref(), + ); + } + } + self.replace(start, end, content, name); + } + /// Replaces source with code whose used names have already been recorded in /// the concatenation scope. pub fn replace_with_tracked_used_names( diff --git a/crates/rspack_core/src/external_module.rs b/crates/rspack_core/src/external_module.rs index 0f74d0476b3c..daddc7b4f88a 100644 --- a/crates/rspack_core/src/external_module.rs +++ b/crates/rspack_core/src/external_module.rs @@ -1110,7 +1110,10 @@ if(typeof {global} !== "undefined") return resolve(); .current_module .generated_top_level_symbols .iter() - .find(|symbol| symbol.preferred_name == NAMESPACE_OBJECT_EXPORT) + .find(|symbol| { + symbol.target == crate::GeneratedTopLevelSymbolTarget::New + && symbol.preferred_name == NAMESPACE_OBJECT_EXPORT + }) }) .flatten() }); diff --git a/crates/rspack_core/src/utils/concatenation_scope.rs b/crates/rspack_core/src/utils/concatenation_scope.rs index 280247b7349e..7b176bbb93ac 100644 --- a/crates/rspack_core/src/utils/concatenation_scope.rs +++ b/crates/rspack_core/src/utils/concatenation_scope.rs @@ -12,8 +12,8 @@ use crate::{ DependencyRange, ExportMode, ModuleIdentifier, PendingConcatenationScopeInfo, concatenated_module::{ ConcatenatedModuleInfo, FasterModuleConcatenationInfo, GENERATED_TOP_LEVEL_SYMBOL_PREFIX, - MODULE_REFERENCE_PLACEHOLDER_PREFIX, MODULE_REFERENCE_PREFIX, MODULE_REFERENCE_SUFFIX, - ModuleInfo, OriginalScopeIdentUpdate, + GeneratedTopLevelSymbolTarget, MODULE_REFERENCE_PLACEHOLDER_PREFIX, MODULE_REFERENCE_PREFIX, + MODULE_REFERENCE_SUFFIX, ModuleInfo, OriginalScopeIdentUpdate, }, }; @@ -270,7 +270,10 @@ impl ConcatenationScope { .current_module .generated_top_level_symbols .iter() - .find(|symbol| symbol.preferred_name == preferred_name) + .find(|symbol| { + symbol.target == GeneratedTopLevelSymbolTarget::New + && symbol.preferred_name == preferred_name + }) { return existing.placeholder.clone(); } @@ -287,6 +290,45 @@ impl ConcatenationScope { .push(crate::GeneratedTopLevelSymbol { preferred_name, placeholder: placeholder.clone(), + target: GeneratedTopLevelSymbolTarget::New, + resolved_binding: None, + }); + placeholder + } + + pub fn rebind_generated_top_level_symbol( + &mut self, + preferred_name: &str, + original_range: DependencyRange, + ) -> Atom { + let preferred_name = Atom::from(preferred_name); + if !self.is_faster_module_concatenation() { + return preferred_name; + } + if let Some(existing) = self + .current_module + .generated_top_level_symbols + .iter() + .find(|symbol| { + symbol.target == GeneratedTopLevelSymbolTarget::Rebind { original_range } + && symbol.preferred_name == preferred_name + }) + { + return existing.placeholder.clone(); + } + + let placeholder = Atom::from(format!( + "{GENERATED_TOP_LEVEL_SYMBOL_PREFIX}{}__", + self.current_module.generated_top_level_symbols.len() + )); + self + .current_module + .generated_top_level_symbols + .push(crate::GeneratedTopLevelSymbol { + preferred_name, + placeholder: placeholder.clone(), + target: GeneratedTopLevelSymbolTarget::Rebind { original_range }, + resolved_binding: None, }); placeholder } diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/const/if_stmt.rs b/crates/rspack_plugin_javascript/src/parser_plugin/const/if_stmt.rs index a3094905f6e8..a24fbeb4bae5 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/const/if_stmt.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/const/if_stmt.rs @@ -1,5 +1,4 @@ -use itertools::Itertools; -use rspack_core::ConstDependency; +use rspack_core::{ConstDependency, DependencyRange, GeneratedCodeRebinding}; use rspack_util::SpanExt; use rustc_hash::FxHashSet; use swc_experimental_ecma_ast::{ @@ -8,45 +7,66 @@ use swc_experimental_ecma_ast::{ use crate::visitors::{JavascriptParser, Statement, VariableDeclarationKind}; -/// Collects hoisted `var` and (in non-strict) function/class declaration idents from a statement. -pub fn get_hoisted_declarations<'a>( - stmt: Statement<'a>, - include_function_declarations: bool, -) -> FxHashSet<&'a str> { - let mut declarations = FxHashSet::default(); - let mut stmt_stack = vec![stmt]; +#[derive(Debug, Clone, Copy)] +pub struct HoistedDeclaration<'a> { + pub name: &'a str, + pub original_range: DependencyRange, +} - let collect_declaration_from_ident = |ident: &'a Ident, declarations: &mut FxHashSet<&'a str>| { - declarations.insert(ident.sym.as_str()); - }; +fn collect_declaration_from_ident<'a>( + ident: &'a Ident, + names: &mut FxHashSet<&'a str>, + declarations: &mut Vec>, +) { + let name = ident.sym.as_str(); + if names.insert(name) { + declarations.push(HoistedDeclaration { + name, + original_range: ident.span().into(), + }); + } +} - let collect_declaration_from_pat = |pattern: &'a Pat, declarations: &mut FxHashSet<&'a str>| { - let mut stack = vec![pattern]; - while let Some(node) = stack.pop() { - match node { - Pat::Ident(ident) => collect_declaration_from_ident(&ident.id, declarations), - Pat::Array(array) => { - for element in array.elems.iter().flatten() { - stack.push(element); - } +fn collect_declaration_from_pat<'a>( + pattern: &'a Pat, + names: &mut FxHashSet<&'a str>, + declarations: &mut Vec>, +) { + let mut stack = vec![pattern]; + while let Some(node) = stack.pop() { + match node { + Pat::Ident(ident) => collect_declaration_from_ident(&ident.id, names, declarations), + Pat::Array(array) => { + for element in array.elems.iter().flatten() { + stack.push(element); } - Pat::Assign(assign) => stack.push(&assign.left), - Pat::Object(object) => { - for property in &object.props { - match property { - ObjectPatProp::KeyValue(key_value) => stack.push(&key_value.value), - ObjectPatProp::Assign(assign) => { - collect_declaration_from_ident(&assign.key.id, declarations); - } - ObjectPatProp::Rest(rest) => stack.push(&rest.arg), + } + Pat::Assign(assign) => stack.push(&assign.left), + Pat::Object(object) => { + for property in &object.props { + match property { + ObjectPatProp::KeyValue(key_value) => stack.push(&key_value.value), + ObjectPatProp::Assign(assign) => { + collect_declaration_from_ident(&assign.key.id, names, declarations); } + ObjectPatProp::Rest(rest) => stack.push(&rest.arg), } } - Pat::Rest(rest) => stack.push(&rest.arg), - _ => {} } + Pat::Rest(rest) => stack.push(&rest.arg), + _ => {} } - }; + } +} + +/// Collects hoisted `var` and (in non-strict) function/class declaration idents from a statement. +pub fn get_hoisted_declarations<'a>( + stmt: Statement<'a>, + include_function_declarations: bool, +) -> Vec> { + let mut names = FxHashSet::default(); + let mut declarations = Vec::new(); + let mut stmt_stack = vec![stmt]; while let Some(node) = stmt_stack.pop() { #[allow(clippy::collapsible_match)] @@ -68,7 +88,7 @@ pub fn get_hoisted_declarations<'a>( && matches!(var_decl.kind, VarDeclKind::Var) { for decl in &var_decl.decls { - collect_declaration_from_pat(&decl.name, &mut declarations); + collect_declaration_from_pat(&decl.name, &mut names, &mut declarations); } } stmt_stack.push(Statement::from(&r#for.body)); @@ -78,7 +98,7 @@ pub fn get_hoisted_declarations<'a>( && matches!(var_decl.kind, VarDeclKind::Var) { for decl in &var_decl.decls { - collect_declaration_from_pat(&decl.name, &mut declarations); + collect_declaration_from_pat(&decl.name, &mut names, &mut declarations); } } stmt_stack.push(Statement::from(&stmt.body)); @@ -88,7 +108,7 @@ pub fn get_hoisted_declarations<'a>( && matches!(var_decl.kind, VarDeclKind::Var) { for decl in &var_decl.decls { - collect_declaration_from_pat(&decl.name, &mut declarations); + collect_declaration_from_pat(&decl.name, &mut names, &mut declarations); } } stmt_stack.push(Statement::from(&stmt.body)); @@ -126,13 +146,13 @@ pub fn get_hoisted_declarations<'a>( } Statement::Fn(fn_decl) if include_function_declarations => { if let Some(ident) = fn_decl.ident() { - collect_declaration_from_ident(ident, &mut declarations); + collect_declaration_from_ident(ident, &mut names, &mut declarations); } } Statement::Var(var_decl) => { if var_decl.kind() == VariableDeclarationKind::Var { for decl in var_decl.declarators() { - collect_declaration_from_pat(&decl.name, &mut declarations); + collect_declaration_from_pat(&decl.name, &mut names, &mut declarations); } } } @@ -140,9 +160,41 @@ pub fn get_hoisted_declarations<'a>( } } + declarations.sort_unstable_by_key(|declaration| declaration.name); declarations } +pub fn render_hoisted_declarations( + declarations: &[HoistedDeclaration<'_>], + track_rebindings: bool, +) -> (String, Vec) { + if declarations.is_empty() { + return ("{}".to_string(), Vec::new()); + } + + let mut replacement = String::from("{ var "); + let mut rebindings = if track_rebindings { + Vec::with_capacity(declarations.len()) + } else { + Vec::new() + }; + for (index, declaration) in declarations.iter().enumerate() { + if index != 0 { + replacement.push_str(", "); + } + let start = replacement.len() as u32; + replacement.push_str(declaration.name); + if track_rebindings { + rebindings.push(GeneratedCodeRebinding { + original_range: declaration.original_range, + generated_range: DependencyRange::new(start, replacement.len() as u32), + }); + } + } + replacement.push_str(" }"); + (replacement, rebindings) +} + pub fn statement_if<'p>(scanner: &mut JavascriptParser<'p>, stmt: &IfStmt<'_>) -> Option { let param = scanner.evaluate_expression(&stmt.test); let boolean = param.as_bool()?; @@ -168,19 +220,23 @@ pub fn statement_if<'p>(scanner: &mut JavascriptParser<'p>, stmt: &IfStmt<'_>) - } else { get_hoisted_declarations(branch_stmt, true) }; - let replacement = if declarations.is_empty() { - "{}".to_string() - } else { - format!("{{ var {} }}", declarations.iter().join(", ")) - }; - - scanner.add_presentational_dependency(Box::new(ConstDependency::new( + let track_rebindings = scanner.is_top_level_scope() + && scanner + .compiler_options() + .experiments + .faster_module_concatenation; + let (replacement, rebindings) = render_hoisted_declarations(&declarations, track_rebindings); + let mut dependency = ConstDependency::new( { let span = branch_to_remove.span(); (span.real_lo(), span.real_hi()).into() }, replacement.into_boxed_str(), - ))) + ); + if track_rebindings { + dependency.set_generated_code_rebindings(rebindings); + } + scanner.add_presentational_dependency(Box::new(dependency)) } Some(boolean) } diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/const/mod.rs b/crates/rspack_plugin_javascript/src/parser_plugin/const/mod.rs index 953cc68244ef..9b5a1db25c9e 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/const/mod.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/const/mod.rs @@ -132,13 +132,7 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for ConstPlugin { let include_function_declarations = !parser.is_strict(); let declarations = self::if_stmt::get_hoisted_declarations(stmt, include_function_declarations); - let replacement_body = if declarations.is_empty() { - "{}".to_string() - } else { - let mut names: Vec<&str> = declarations.iter().copied().collect(); - names.sort_unstable(); - format!("{{ var {} }}", names.join(", ")) - }; + let (replacement_body, _) = self::if_stmt::render_hoisted_declarations(&declarations, false); // Prepend the same comment as webpack for easier debugging. let mut replacement = String::from("// removed by dead control flow\n"); diff --git a/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/direct.js b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/direct.js new file mode 100644 index 000000000000..943bbac87a6e --- /dev/null +++ b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/direct.js @@ -0,0 +1 @@ +export const value = "direct"; diff --git a/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/index.js b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/index.js new file mode 100644 index 000000000000..990f90841f3a --- /dev/null +++ b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/index.js @@ -0,0 +1,13 @@ +import { + value as removedValue, + data as removedData, + a as removedA +} from "./removed"; +import { value as directValue } from "./direct"; + +it("should preserve bindings recreated by dead-code elimination", () => { + expect(removedValue).toBeUndefined(); + expect(removedData).toBeUndefined(); + expect(removedA).toBeUndefined(); + expect(directValue).toBe("direct"); +}); diff --git a/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/removed.js b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/removed.js new file mode 100644 index 000000000000..aedc3ad4f842 --- /dev/null +++ b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/removed.js @@ -0,0 +1,7 @@ +if (false) { + var value = "unreachable"; + var { data } = { data: "unreachable" }; + var [a] = ["unreachable"]; +} + +export { value, data, a }; diff --git a/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/test.config.js b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/test.config.js new file mode 100644 index 000000000000..7d4cc233bfe4 --- /dev/null +++ b/tests/rspack-test/normalCases/scope-hoisting/hoisted-declaration-rebinding/test.config.js @@ -0,0 +1,6 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + optimization: { + concatenateModules: true + } +}; From 01273cb18d8a57461658ddcad7d9c8115fee076f Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Fri, 14 Aug 2026 16:22:30 +0800 Subject: [PATCH 08/10] fix: preserve Unicode identifiers in faster concatenation --- .../src/concatenated_module/faster.rs | 42 ++++-- .../src/utils/concatenated_module_visitor.rs | 20 ++- .../src/utils/concatenation_scope.rs | 142 +++++++++++++++++- .../src/visitors/concatenate_scope_info.rs | 33 +++- .../unicode-identifiers/escaped.js | 1 + .../unicode-identifiers/index.js | 18 +++ .../unicode-identifiers/plain.js | 8 + .../unicode-identifiers/rspack.config.js | 14 ++ 8 files changed, 246 insertions(+), 32 deletions(-) create mode 100644 tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/escaped.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/index.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/plain.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js diff --git a/crates/rspack_core/src/concatenated_module/faster.rs b/crates/rspack_core/src/concatenated_module/faster.rs index 63e5a7bacea0..41b3159e6e8c 100644 --- a/crates/rspack_core/src/concatenated_module/faster.rs +++ b/crates/rspack_core/src/concatenated_module/faster.rs @@ -179,11 +179,14 @@ pub(crate) fn populate_info_from_pending( module_info: &mut ConcatenatedModuleInfo, faster_info: &mut FasterModuleConcatenationInfo, ) { - let (module_ctxt, global_ctxt, pending_idents) = match pending { - PendingConcatenationScopeInfo::Analyzed(info) => { - (info.module_ctxt, info.global_ctxt, info.idents.as_slice()) - } - PendingConcatenationScopeInfo::Generated => (0, 0, &[][..]), + let (module_ctxt, global_ctxt, pending_idents, canonical_names) = match pending { + PendingConcatenationScopeInfo::Analyzed(info) => ( + info.module_ctxt, + info.global_ctxt, + info.idents.as_slice(), + info.canonical_names.as_slice(), + ), + PendingConcatenationScopeInfo::Generated => (0, 0, &[][..], &[][..]), }; let mut symbols = SmallVec::<[(&str, Atom); 8]>::new(); let mut symbol_from_range = |range: DependencyRange| { @@ -239,7 +242,11 @@ pub(crate) fn populate_info_from_pending( let mut seen = SeenIdents::default(); for pending_ident in pending_idents { - let symbol = symbol_from_range(pending_ident.range); + let symbol = canonical_names + .iter() + .find(|canonical_name| canonical_name.range == pending_ident.range) + .map(|canonical_name| canonical_name.name.clone()) + .unwrap_or_else(|| symbol_from_range(pending_ident.range)); if pending_ident.kind == ConcatenationScopeIdentKind::UsedName { module_info.all_used_names.insert(symbol); continue; @@ -301,15 +308,22 @@ pub(crate) fn populate_info_from_pending( let binding = match symbol.target { GeneratedTopLevelSymbolTarget::New => symbol.placeholder.clone(), GeneratedTopLevelSymbolTarget::Rebind { original_range } => { - assert!( - pending_idents.iter().any(|ident| { + let original_ident = pending_idents + .iter() + .find(|ident| { ident.kind == ConcatenationScopeIdentKind::TopLevel && ident.range == original_range - }), - "rebound concatenation symbol range {}..{} should refer to a make-time top-level identifier", - original_range.start, - original_range.end - ); - let binding = symbol_from_range(original_range); + }) + .unwrap_or_else(|| { + panic!( + "rebound concatenation symbol range {}..{} should refer to a make-time top-level identifier", + original_range.start, original_range.end + ) + }); + let binding = canonical_names + .iter() + .find(|canonical_name| canonical_name.range == original_ident.range) + .map(|canonical_name| canonical_name.name.clone()) + .unwrap_or_else(|| symbol_from_range(original_range)); symbol.resolved_binding = Some(binding.clone()); binding } diff --git a/crates/rspack_core/src/utils/concatenated_module_visitor.rs b/crates/rspack_core/src/utils/concatenated_module_visitor.rs index aeb15b6f3608..13618acd692a 100644 --- a/crates/rspack_core/src/utils/concatenated_module_visitor.rs +++ b/crates/rspack_core/src/utils/concatenated_module_visitor.rs @@ -1,7 +1,10 @@ -use rspack_cacheable::cacheable; -use swc_core::ecma::{ - ast::{ClassExpr, Ident, ObjectPatProp, Prop}, - visit::{Visit, VisitWith, noop_visit_type}, +use rspack_cacheable::{cacheable, with::AsPreset}; +use swc_core::{ + atoms::Atom, + ecma::{ + ast::{ClassExpr, Ident, ObjectPatProp, Prop}, + visit::{Visit, VisitWith, noop_visit_type}, + }, }; use crate::DependencyRange; @@ -29,12 +32,21 @@ pub struct ConcatenationScopeIdent { pub kind: ConcatenationScopeIdentKind, } +#[cacheable] +#[derive(Clone, Debug)] +pub struct ConcatenationScopeCanonicalName { + pub range: DependencyRange, + #[cacheable(with=AsPreset)] + pub name: Atom, +} + #[cacheable] #[derive(Clone, Debug)] pub struct AnalyzedConcatenationScopeInfo { pub module_ctxt: u32, pub global_ctxt: u32, pub idents: Vec, + pub canonical_names: Vec, } /// Make-time scope information required by faster module concatenation. diff --git a/crates/rspack_core/src/utils/concatenation_scope.rs b/crates/rspack_core/src/utils/concatenation_scope.rs index 7b176bbb93ac..68f35779331c 100644 --- a/crates/rspack_core/src/utils/concatenation_scope.rs +++ b/crates/rspack_core/src/utils/concatenation_scope.rs @@ -7,6 +7,7 @@ use rspack_util::{ itoa, }; use swc_core::atoms::Atom; +use swc_experimental_ecma_ast::{is_valid_continue, is_valid_start}; use crate::{ DependencyRange, ExportMode, ModuleIdentifier, PendingConcatenationScopeInfo, @@ -27,24 +28,153 @@ fn is_ascii_identifier_continue(byte: u8) -> bool { byte == b'_' || byte == b'$' || byte.is_ascii_alphanumeric() } +#[inline] +fn is_ascii_identifier_start(byte: u8) -> bool { + byte == b'_' || byte == b'$' || byte.is_ascii_alphabetic() +} + +#[inline] +fn hex_value(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some((byte - b'0') as u32), + b'a'..=b'f' => Some((byte - b'a' + 10) as u32), + b'A'..=b'F' => Some((byte - b'A' + 10) as u32), + _ => None, + } +} + +fn parse_unicode_escape(code: &[u8], start: usize) -> Option<(char, usize)> { + if code.get(start..start + 2)? != b"\\u" { + return None; + } + + let mut cursor = start + 2; + let mut value = 0u32; + if code.get(cursor) == Some(&b'{') { + cursor += 1; + let digits_start = cursor; + while let Some(digit) = code.get(cursor).and_then(|byte| hex_value(*byte)) { + if cursor - digits_start == 6 { + return None; + } + value = value.checked_mul(16)?.checked_add(digit)?; + cursor += 1; + } + if cursor == digits_start || code.get(cursor) != Some(&b'}') { + return None; + } + cursor += 1; + } else { + for _ in 0..4 { + value = value + .checked_mul(16)? + .checked_add(hex_value(*code.get(cursor)?)?)?; + cursor += 1; + } + } + + char::from_u32(value).map(|character| (character, cursor)) +} + +/// Returns the end offset and, only when escapes were present, the decoded +/// canonical identifier name. +fn scan_generated_identifier(code: &str, start: usize) -> Option<(usize, Option)> { + let bytes = code.as_bytes(); + let mut cursor = start; + let mut canonical_name: Option = None; + let mut is_start = true; + + while cursor < bytes.len() { + let (character, end, escaped) = if bytes[cursor] == b'\\' { + let Some((character, end)) = parse_unicode_escape(bytes, cursor) else { + if is_start { + return None; + } + break; + }; + (character, end, true) + } else { + let character = code[cursor..].chars().next()?; + (character, cursor + character.len_utf8(), false) + }; + if if is_start { + !is_valid_start(character) + } else { + !is_valid_continue(character) + } { + break; + } + + if escaped { + canonical_name + .get_or_insert_with(|| { + let mut name = String::with_capacity(end - start); + name.push_str(&code[start..cursor]); + name + }) + .push(character); + } else if let Some(name) = &mut canonical_name { + name.push(character); + } + cursor = end; + is_start = false; + } + + (cursor != start).then_some((cursor, canonical_name)) +} + fn add_used_names_from_generated_code(info: &mut FasterModuleConcatenationInfo, code: &str) { // The legacy codegen parse reserves names referenced by replacement code. // Faster concatenation skips that parse, so conservatively collect every - // ASCII identifier-shaped token. False positives only reduce name reuse; - // missing a name could capture a generated global reference. + // identifier-shaped token. False positives only reduce name reuse; missing + // a name could capture a generated global reference. let bytes = code.as_bytes(); let mut cursor = 0; while cursor < bytes.len() { - while cursor < bytes.len() && !is_ascii_identifier_continue(bytes[cursor]) { + while cursor < bytes.len() + && bytes[cursor].is_ascii() + && !is_ascii_identifier_continue(bytes[cursor]) + && bytes[cursor] != b'\\' + { cursor += 1; } + if cursor == bytes.len() { + break; + } + + if !bytes[cursor].is_ascii() || bytes[cursor] == b'\\' { + if let Some((end, canonical_name)) = scan_generated_identifier(code, cursor) { + let name = canonical_name.as_deref().unwrap_or(&code[cursor..end]); + info.added_used_names.push(name.into()); + cursor = end; + } else { + cursor += if bytes[cursor].is_ascii() { + 1 + } else { + code[cursor..] + .chars() + .next() + .expect("cursor should be on a character boundary") + .len_utf8() + }; + } + continue; + } + let start = cursor; while cursor < bytes.len() && is_ascii_identifier_continue(bytes[cursor]) { cursor += 1; } - if start < cursor - && (bytes[start] == b'_' || bytes[start] == b'$' || bytes[start].is_ascii_alphabetic()) - { + if !is_ascii_identifier_start(bytes[start]) { + continue; + } + if cursor < bytes.len() && (!bytes[cursor].is_ascii() || bytes[cursor] == b'\\') { + let (end, canonical_name) = scan_generated_identifier(code, start) + .expect("ASCII identifier start should produce an identifier"); + let name = canonical_name.as_deref().unwrap_or(&code[start..end]); + info.added_used_names.push(name.into()); + cursor = end; + } else { info.added_used_names.push(code[start..cursor].into()); } } diff --git a/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs b/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs index fedf603bab00..acd2d31e57db 100644 --- a/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs +++ b/crates/rspack_plugin_javascript/src/visitors/concatenate_scope_info.rs @@ -1,7 +1,8 @@ use rspack_core::{ - AnalyzedConcatenationScopeInfo, ConcatenationScopeIdent, ConcatenationScopeIdentKind, - DependencyRange, PendingConcatenationScopeInfo, + AnalyzedConcatenationScopeInfo, ConcatenationScopeCanonicalName, ConcatenationScopeIdent, + ConcatenationScopeIdentKind, DependencyRange, PendingConcatenationScopeInfo, }; +use rspack_util::atom::Atom; use rustc_hash::FxHashSet; use smallvec::SmallVec; use swc_experimental_allocator::atom::Atom as AstAtom; @@ -55,6 +56,7 @@ pub(crate) struct PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { global_idents: SmallVec<[PendingConcatenationScopeIdent; 4]>, used_name_set: UsedNames<'ast>, used_names: SmallVec<[(AstAtom<'ast>, DependencyRange); 8]>, + canonical_names: SmallVec<[ConcatenationScopeCanonicalName; 1]>, } impl<'semantic, 'ast> PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { @@ -65,6 +67,20 @@ impl<'semantic, 'ast> PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { global_idents: SmallVec::new(), used_name_set: UsedNames::default(), used_names: SmallVec::new(), + canonical_names: SmallVec::new(), + } + } + + #[inline] + fn add_canonical_name(&mut self, ident: &Ident<'ast>, range: DependencyRange) { + // An escape-spelled identifier is longer than its parser-canonical name. + // Persist the canonical form only for this uncommon case, keeping the + // common per-identifier metadata compact. + if (range.end - range.start) as usize != ident.sym.as_str().len() { + self.canonical_names.push(ConcatenationScopeCanonicalName { + range, + name: Atom::from(ident.sym.as_str()), + }); } } @@ -75,18 +91,18 @@ impl<'semantic, 'ast> PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { } let scope = self.semantic.node_scope(ident); let range = ident.span.into(); + let pending_ident = PendingConcatenationScopeIdent { range, shorthand }; if scope == self.semantic.unresolved_scope_id() { - self - .global_idents - .push(PendingConcatenationScopeIdent { range, shorthand }); + self.add_canonical_name(ident, range); + self.global_idents.push(pending_ident); } else if class_expr_with_ident || scope != self.semantic.top_level_scope_id() { if self.used_name_set.insert(ident.sym) { + self.add_canonical_name(ident, range); self.used_names.push((ident.sym, range)); } } else { - self - .top_level_idents - .push(PendingConcatenationScopeIdent { range, shorthand }); + self.add_canonical_name(ident, range); + self.top_level_idents.push(pending_ident); } } @@ -128,6 +144,7 @@ impl<'semantic, 'ast> PendingConcatenationScopeInfoVisitor<'semantic, 'ast> { module_ctxt: self.semantic.top_level_scope_id().raw(), global_ctxt: self.semantic.unresolved_scope_id().raw(), idents, + canonical_names: self.canonical_names.into_vec(), }) } } diff --git a/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/escaped.js b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/escaped.js new file mode 100644 index 000000000000..6b9c1a33793e --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/escaped.js @@ -0,0 +1 @@ +export const \u0061 = "escaped"; diff --git a/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/index.js b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/index.js new file mode 100644 index 000000000000..ccc39733859c --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/index.js @@ -0,0 +1,18 @@ +import { a as escaped } from "./escaped"; +import { + a, + 原始拼接全局引用 as localUnicode, + 转义拼接全局引用 as localEscapedUnicode +} from "./plain"; + +it("should preserve canonical names for escaped identifiers", () => { + expect(escaped).toBe("escaped"); + expect(a).toBe("plain"); +}); + +it("should reserve Unicode names referenced by generated code", () => { + expect(GENERATED_UNICODE).toBe("undefined"); + expect(GENERATED_ESCAPED_UNICODE).toBe("undefined"); + expect(localUnicode).toBe("local raw"); + expect(localEscapedUnicode).toBe("local escaped"); +}); diff --git a/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/plain.js b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/plain.js new file mode 100644 index 000000000000..6e774489c7d4 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/plain.js @@ -0,0 +1,8 @@ +export const a = "plain"; +export let 原始拼接全局引用 = "local raw"; +export let 转义拼接全局引用 = "local escaped"; + +if (globalThis.CHANGE_UNICODE_IDENTIFIER) { + 原始拼接全局引用 = "changed"; + 转义拼接全局引用 = "changed"; +} diff --git a/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js new file mode 100644 index 000000000000..598eb837d192 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js @@ -0,0 +1,14 @@ +const { DefinePlugin } = require("@rspack/core"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + optimization: { + concatenateModules: true + }, + plugins: [ + new DefinePlugin({ + GENERATED_UNICODE: "typeof 原始拼接全局引用", + GENERATED_ESCAPED_UNICODE: String.raw`typeof \u8f6c\u4e49\u62fc\u63a5\u5168\u5c40\u5f15\u7528` + }) + ] +}; From 18857fddb4e43edddf0fc8ace834c9b8e033e774 Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Fri, 14 Aug 2026 17:43:00 +0800 Subject: [PATCH 09/10] fix: preserve generated scope references --- .../rspack_core/src/parser_and_generator.rs | 17 ++++++++++++-- .../src/utils/concatenation_scope.rs | 14 +++++++++++ crates/rspack_plugin_asset/src/lib.rs | 9 +++----- .../src/parser_and_generator/generator.rs | 7 +++--- .../src/parser_and_generator/mod.rs | 4 ++++ crates/rspack_plugin_json/src/lib.rs | 6 ++++- .../generated-asset-used-name/asset.txt | 1 + .../generated-asset-used-name/index.js | 7 ++++++ .../generated-asset-used-name/local.js | 5 ++++ .../rspack.config.js | 16 +++++++++++++ .../generated-css-used-name/index.js | 7 ++++++ .../generated-css-used-name/local.js | 3 +++ .../generated-css-used-name/rspack.config.js | 23 +++++++++++++++++++ .../generated-css-used-name/style.css | 3 +++ .../generated-json-used-name/data.json | 3 +++ .../generated-json-used-name/index.js | 7 ++++++ .../generated-json-used-name/local.js | 3 +++ .../generated-json-used-name/rspack.config.js | 8 +++++++ .../init-fragment-used-name/index.js | 6 +++++ .../init-fragment-used-name/local.js | 3 +++ .../init-fragment-used-name/provided.cjs | 1 + .../init-fragment-used-name/rspack.config.js | 15 ++++++++++++ 22 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/asset.txt create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/index.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/local.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/index.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/local.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/style.css create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/data.json create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/index.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/local.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/index.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/local.js create mode 100644 tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/provided.cjs create mode 100644 tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js diff --git a/crates/rspack_core/src/parser_and_generator.rs b/crates/rspack_core/src/parser_and_generator.rs index 8acbdf5a584f..2d5648f9daad 100644 --- a/crates/rspack_core/src/parser_and_generator.rs +++ b/crates/rspack_core/src/parser_and_generator.rs @@ -18,7 +18,7 @@ use crate::{ BuildInfo, BuildMeta, ChunkGraph, CodeGenerationData, Compilation, CompilerOptions, ConcatenationScope, Context, DependencyLocation, DependencyRange, EvaluatedInlinableValue, FactoryMeta, GeneratorOptions, Module, ModuleCodeTemplate, ModuleGraph, ModuleIdentifier, - ModuleLayer, ModuleType, NormalModule, ParserOptions, RuntimeSpec, SourceType, + ModuleLayer, ModuleType, NormalModule, ParserOptions, RuntimeGlobals, RuntimeSpec, SourceType, }; #[derive(Debug)] @@ -130,6 +130,18 @@ impl GenerateContext<'_> { pub fn take_analyzed_concatenation_source(&mut self) -> Option { self.analyzed_concatenation_source.take() } + + /// Renders a runtime global and records the identifiers referenced by the + /// rendered expression for generated concatenation sources. + pub fn render_runtime_globals(&mut self, runtime_globals: &RuntimeGlobals) -> String { + let rendered = self + .runtime_template + .render_runtime_globals(runtime_globals); + if let Some(scope) = self.concatenation_scope.as_deref_mut() { + scope.register_used_names_from_generated_code(&rendered); + } + rendered + } } #[derive(Debug, Clone)] @@ -180,7 +192,8 @@ pub enum ConcatenationScopeInfoMode { /// during make. AnalyzeAtMake, /// The module has no original JavaScript scope. Its generator must register - /// every generated binding through `ConcatenationScope`. + /// every generated binding and referenced identifier through + /// `ConcatenationScope`. GenerateAtCodegen, /// The parser and generator cannot participate in module concatenation. Unsupported, diff --git a/crates/rspack_core/src/utils/concatenation_scope.rs b/crates/rspack_core/src/utils/concatenation_scope.rs index 68f35779331c..604d2f4b0a72 100644 --- a/crates/rspack_core/src/utils/concatenation_scope.rs +++ b/crates/rspack_core/src/utils/concatenation_scope.rs @@ -333,6 +333,20 @@ impl ConcatenationScope { symbol } + pub fn register_used_name(&mut self, name: Atom) { + let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { + return; + }; + info.added_used_names.push(name); + } + + pub fn register_used_names_from_generated_code(&mut self, code: &str) { + let Some(info) = self.faster_module_concatenation_info.as_deref_mut() else { + return; + }; + add_used_names_from_generated_code(info, code); + } + pub fn remove_original_range(&mut self, range: DependencyRange) { self.record_source_edit(Some(range), None); } diff --git a/crates/rspack_plugin_asset/src/lib.rs b/crates/rspack_plugin_asset/src/lib.rs index 55730ff8845e..9a8ffb680341 100644 --- a/crates/rspack_plugin_asset/src/lib.rs +++ b/crates/rspack_plugin_asset/src/lib.rs @@ -577,9 +577,7 @@ impl ParserAndGenerator for AssetParserAndGenerator { } else { format!( "{}({})", - generate_context - .runtime_template - .render_runtime_globals(&RuntimeGlobals::TO_BINARY), + generate_context.render_runtime_globals(&RuntimeGlobals::TO_BINARY), rspack_util::json_stringify_str(&encoded_source) ) } @@ -658,9 +656,7 @@ impl ParserAndGenerator for AssetParserAndGenerator { } else { format!( r#"{} + "{}""#, - generate_context - .runtime_template - .render_runtime_globals(&RuntimeGlobals::PUBLIC_PATH), + generate_context.render_runtime_globals(&RuntimeGlobals::PUBLIC_PATH), original_filename ) }; @@ -706,6 +702,7 @@ impl ParserAndGenerator for AssetParserAndGenerator { } else { let supports_const = compilation.options.output.environment.supports_const(); let declaration_kind = if supports_const { "const" } else { "var" }; + scope.register_used_name("require".into()); return Ok(render_concatenated_asset_source( scope, &format!("{declaration_kind} "), diff --git a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs index 30c4b9b14a85..04b7425f6247 100644 --- a/crates/rspack_plugin_css/src/parser_and_generator/generator.rs +++ b/crates/rspack_plugin_css/src/parser_and_generator/generator.rs @@ -139,7 +139,6 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { self.css_inject_style.get_or_insert_with(|| { self .generate_context - .runtime_template .render_runtime_globals(&RuntimeGlobals::CSS_INJECT_STYLE) }) } @@ -148,7 +147,6 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { self.css_style_sheet.get_or_insert_with(|| { self .generate_context - .runtime_template .render_runtime_globals(&RuntimeGlobals::CSS_STYLE_SHEET) }) } @@ -470,7 +468,6 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { ( self .generate_context - .runtime_template .render_runtime_globals(&RuntimeGlobals::REQUIRE), "(", ")", @@ -495,7 +492,6 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { ( self .generate_context - .runtime_template .render_runtime_globals(&RuntimeGlobals::MAKE_NAMESPACE_OBJECT), "(", ")", @@ -745,6 +741,9 @@ impl<'a, 'g> CssModuleGenerator<'a, 'g> { .generate_context .runtime_template .define_es_module_flag_statement(exports_argument); + if let Some(scope) = self.generate_context.concatenation_scope.as_deref_mut() { + scope.register_used_names_from_generated_code(&esm_flag); + } self.concat_source.add(RawStringSource::from(esm_flag)); } diff --git a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs index 379b475b3eda..983d4e4c6e2f 100644 --- a/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs +++ b/crates/rspack_plugin_javascript/src/parser_and_generator/mod.rs @@ -530,6 +530,10 @@ impl ParserAndGenerator for JavaScriptParserAndGenerator { if is_concatenated_codegen { let rendered_fragments = render_init_fragments_to_strings(init_fragments, generate_context)?; + if let Some(scope) = concatenation_scope.as_deref_mut() { + scope.register_used_names_from_generated_code(&rendered_fragments.start); + scope.register_used_names_from_generated_code(&rendered_fragments.end); + } if !rendered_fragments.is_empty() { generate_context.data.insert(rendered_fragments); } diff --git a/crates/rspack_plugin_json/src/lib.rs b/crates/rspack_plugin_json/src/lib.rs index 0d2776ea0716..77a53494dbfa 100644 --- a/crates/rspack_plugin_json/src/lib.rs +++ b/crates/rspack_plugin_json/src/lib.rs @@ -213,7 +213,8 @@ impl ParserAndGenerator for JsonParserAndGenerator { let is_js_object = final_json.is_object() || final_json.is_array(); let final_json_string = stringify(final_json); let json_str = utils::escape_json(&final_json_string); - let json_expr = if self.json_parse && is_js_object && json_str.len() > 20 { + let use_json_parse = self.json_parse && is_js_object && json_str.len() > 20; + let json_expr = if use_json_parse { Cow::Owned(format!( "/*#__PURE__*/JSON.parse('{}')", json_str.cow_replace('\\', r"\\").cow_replace('\'', r"\'") @@ -222,6 +223,9 @@ impl ParserAndGenerator for JsonParserAndGenerator { json_str.cow_replace("\"__proto__\":", "[\"__proto__\"]:") }; if let Some(scope) = concatenation_scope { + if use_json_parse { + scope.register_used_name("JSON".into()); + } let faster_module_concatenation = scope.is_faster_module_concatenation(); let namespace_export = scope.register_generated_namespace_export(NAMESPACE_OBJECT_EXPORT); let rendered_namespace_export = if faster_module_concatenation { diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/asset.txt b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/asset.txt new file mode 100644 index 000000000000..2bd69c0fe881 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/asset.txt @@ -0,0 +1 @@ +asset diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/index.js b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/index.js new file mode 100644 index 000000000000..0abba92f63cf --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/index.js @@ -0,0 +1,7 @@ +import { __webpack_require__ as localRequire } from "./local"; +import assetUrl from "./asset.txt"; + +it("should preserve runtime globals referenced by generated asset modules", () => { + expect(localRequire()).toBe("local"); + expect(assetUrl).not.toContain("captured/"); +}); diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/local.js b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/local.js new file mode 100644 index 000000000000..2a4c3ca84fca --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/local.js @@ -0,0 +1,5 @@ +export function __webpack_require__() { + return "local"; +} + +__webpack_require__.p = "captured/"; diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js new file mode 100644 index 000000000000..8ea8081fc553 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js @@ -0,0 +1,16 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + mode: "development", + optimization: { + concatenateModules: true, + minimize: false + }, + module: { + rules: [ + { + test: /\.txt$/, + type: "asset/resource" + } + ] + } +}; diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/index.js b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/index.js new file mode 100644 index 000000000000..00b033b6ebed --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/index.js @@ -0,0 +1,7 @@ +import { __webpack_require__ as localRequire } from "./local"; +import styles from "./style.css"; + +it("should preserve runtime globals referenced by generated CSS modules", () => { + expect(localRequire()).toBe("local"); + expect(styles["class-name"]).toBe("class-name"); +}); diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/local.js b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/local.js new file mode 100644 index 000000000000..b0401fc4c745 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/local.js @@ -0,0 +1,3 @@ +export function __webpack_require__() { + return "local"; +} diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js new file mode 100644 index 000000000000..0edd28b3308f --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js @@ -0,0 +1,23 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + mode: "development", + optimization: { + concatenateModules: true, + minimize: false + }, + module: { + rules: [ + { + test: /\.css$/, + type: "css/module", + parser: { + namedExports: false + }, + generator: { + exportsOnly: true, + localIdentName: "[local]" + } + } + ] + } +}; diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/style.css b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/style.css new file mode 100644 index 000000000000..63bbadb4f664 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/style.css @@ -0,0 +1,3 @@ +.class-name { + color: red; +} diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/data.json b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/data.json new file mode 100644 index 000000000000..4c19a914d02a --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/data.json @@ -0,0 +1,3 @@ +{ + "message": "generated JSON must use the global parser" +} diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/index.js b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/index.js new file mode 100644 index 000000000000..3626468fdbf0 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/index.js @@ -0,0 +1,7 @@ +import { JSON as localJSON } from "./local"; +import data from "./data.json"; + +it("should preserve globals referenced by generated JSON modules", () => { + expect(localJSON()).toBe("local"); + expect(data.message).toBe("generated JSON must use the global parser"); +}); diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/local.js b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/local.js new file mode 100644 index 000000000000..6ef73a06b15c --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/local.js @@ -0,0 +1,3 @@ +export function JSON() { + return "local"; +} diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js new file mode 100644 index 000000000000..03da9cf3e1b5 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js @@ -0,0 +1,8 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + mode: "development", + optimization: { + concatenateModules: true, + minimize: false + } +}; diff --git a/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/index.js b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/index.js new file mode 100644 index 000000000000..901a6410b5e9 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/index.js @@ -0,0 +1,6 @@ +import { __webpack_require__ as localRequire } from "./local"; + +it("should preserve runtime globals referenced by init fragments", () => { + expect(localRequire()).toBe("captured"); + expect(PROVIDED_VALUE).toBe("provided"); +}); diff --git a/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/local.js b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/local.js new file mode 100644 index 000000000000..658d81b9f4d6 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/local.js @@ -0,0 +1,3 @@ +export function __webpack_require__() { + return "captured"; +} diff --git a/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/provided.cjs b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/provided.cjs new file mode 100644 index 000000000000..9a016a173e18 --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/provided.cjs @@ -0,0 +1 @@ +module.exports = "provided"; diff --git a/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js new file mode 100644 index 000000000000..f095240407be --- /dev/null +++ b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js @@ -0,0 +1,15 @@ +const { rspack } = require("@rspack/core"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + mode: "development", + optimization: { + concatenateModules: true, + minimize: false + }, + plugins: [ + new rspack.ProvidePlugin({ + PROVIDED_VALUE: "./provided.cjs" + }) + ] +}; From d84ab09b6a43c623b787a2675d78ade90a4605ab Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Fri, 14 Aug 2026 17:56:00 +0800 Subject: [PATCH 10/10] chore: fix lint and formatting --- .../src/concatenated_module/faster.rs | 12 ++++-- .../rspack.config.js | 26 ++++++------ .../generated-css-used-name/rspack.config.js | 40 +++++++++---------- .../generated-json-used-name/rspack.config.js | 10 ++--- .../init-fragment-used-name/rspack.config.js | 22 +++++----- .../unicode-identifiers/rspack.config.js | 20 +++++----- 6 files changed, 67 insertions(+), 63 deletions(-) diff --git a/crates/rspack_core/src/concatenated_module/faster.rs b/crates/rspack_core/src/concatenated_module/faster.rs index 41b3159e6e8c..d1d3579567f8 100644 --- a/crates/rspack_core/src/concatenated_module/faster.rs +++ b/crates/rspack_core/src/concatenated_module/faster.rs @@ -245,8 +245,10 @@ pub(crate) fn populate_info_from_pending( let symbol = canonical_names .iter() .find(|canonical_name| canonical_name.range == pending_ident.range) - .map(|canonical_name| canonical_name.name.clone()) - .unwrap_or_else(|| symbol_from_range(pending_ident.range)); + .map_or_else( + || symbol_from_range(pending_ident.range), + |canonical_name| canonical_name.name.clone(), + ); if pending_ident.kind == ConcatenationScopeIdentKind::UsedName { module_info.all_used_names.insert(symbol); continue; @@ -322,8 +324,10 @@ pub(crate) fn populate_info_from_pending( let binding = canonical_names .iter() .find(|canonical_name| canonical_name.range == original_ident.range) - .map(|canonical_name| canonical_name.name.clone()) - .unwrap_or_else(|| symbol_from_range(original_range)); + .map_or_else( + || symbol_from_range(original_range), + |canonical_name| canonical_name.name.clone(), + ); symbol.resolved_binding = Some(binding.clone()); binding } diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js index 8ea8081fc553..e7f10d54b3c9 100644 --- a/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js +++ b/tests/rspack-test/configCases/concatenate-modules/generated-asset-used-name/rspack.config.js @@ -1,16 +1,16 @@ /** @type {import("@rspack/core").Configuration} */ module.exports = { - mode: "development", - optimization: { - concatenateModules: true, - minimize: false - }, - module: { - rules: [ - { - test: /\.txt$/, - type: "asset/resource" - } - ] - } + mode: 'development', + optimization: { + concatenateModules: true, + minimize: false, + }, + module: { + rules: [ + { + test: /\.txt$/, + type: 'asset/resource', + }, + ], + }, }; diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js index 0edd28b3308f..d6e74f3579c1 100644 --- a/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js +++ b/tests/rspack-test/configCases/concatenate-modules/generated-css-used-name/rspack.config.js @@ -1,23 +1,23 @@ /** @type {import("@rspack/core").Configuration} */ module.exports = { - mode: "development", - optimization: { - concatenateModules: true, - minimize: false - }, - module: { - rules: [ - { - test: /\.css$/, - type: "css/module", - parser: { - namedExports: false - }, - generator: { - exportsOnly: true, - localIdentName: "[local]" - } - } - ] - } + mode: 'development', + optimization: { + concatenateModules: true, + minimize: false, + }, + module: { + rules: [ + { + test: /\.css$/, + type: 'css/module', + parser: { + namedExports: false, + }, + generator: { + exportsOnly: true, + localIdentName: '[local]', + }, + }, + ], + }, }; diff --git a/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js index 03da9cf3e1b5..51b568563bbe 100644 --- a/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js +++ b/tests/rspack-test/configCases/concatenate-modules/generated-json-used-name/rspack.config.js @@ -1,8 +1,8 @@ /** @type {import("@rspack/core").Configuration} */ module.exports = { - mode: "development", - optimization: { - concatenateModules: true, - minimize: false - } + mode: 'development', + optimization: { + concatenateModules: true, + minimize: false, + }, }; diff --git a/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js index f095240407be..b82fd6be00d3 100644 --- a/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js +++ b/tests/rspack-test/configCases/concatenate-modules/init-fragment-used-name/rspack.config.js @@ -1,15 +1,15 @@ -const { rspack } = require("@rspack/core"); +const { rspack } = require('@rspack/core'); /** @type {import("@rspack/core").Configuration} */ module.exports = { - mode: "development", - optimization: { - concatenateModules: true, - minimize: false - }, - plugins: [ - new rspack.ProvidePlugin({ - PROVIDED_VALUE: "./provided.cjs" - }) - ] + mode: 'development', + optimization: { + concatenateModules: true, + minimize: false, + }, + plugins: [ + new rspack.ProvidePlugin({ + PROVIDED_VALUE: './provided.cjs', + }), + ], }; diff --git a/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js index 598eb837d192..ef6c7009b975 100644 --- a/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js +++ b/tests/rspack-test/configCases/concatenate-modules/unicode-identifiers/rspack.config.js @@ -1,14 +1,14 @@ -const { DefinePlugin } = require("@rspack/core"); +const { DefinePlugin } = require('@rspack/core'); /** @type {import("@rspack/core").Configuration} */ module.exports = { - optimization: { - concatenateModules: true - }, - plugins: [ - new DefinePlugin({ - GENERATED_UNICODE: "typeof 原始拼接全局引用", - GENERATED_ESCAPED_UNICODE: String.raw`typeof \u8f6c\u4e49\u62fc\u63a5\u5168\u5c40\u5f15\u7528` - }) - ] + optimization: { + concatenateModules: true, + }, + plugins: [ + new DefinePlugin({ + GENERATED_UNICODE: 'typeof 原始拼接全局引用', + GENERATED_ESCAPED_UNICODE: String.raw`typeof \u8f6c\u4e49\u62fc\u63a5\u5168\u5c40\u5f15\u7528`, + }), + ], };