From a21cd1cbafe2c95864d72b3af0f9181ade5aff96 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:30:29 +0000 Subject: [PATCH 1/2] feat(minifier): add merge_duplicate_imports option to compress settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement feature to merge duplicate named imports from the same module during minification to reduce bundle size. - Add merge_duplicate_imports option to CompressOptions (defaults to false) - Create merge_duplicate_imports function in pure optimizer that directly iterates over ModuleItems - Handle named, default, and namespace imports correctly without creating new visitor - Preserve import semantics while merging compatible imports - Add test case for various import merging scenarios Fixes #11133 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Donny/강동윤 --- .../src/compress/pure/mod.rs | 119 ++++++++++++++++++ crates/swc_ecma_minifier/src/option/mod.rs | 5 + crates/swc_ecma_minifier/src/option/terser.rs | 1 + .../tests/fixture/issues/11133/config.json | 5 + .../tests/fixture/issues/11133/input.js | 10 ++ .../tests/fixture/issues/11133/output.js | 6 + 6 files changed, 146 insertions(+) create mode 100644 crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json create mode 100644 crates/swc_ecma_minifier/tests/fixture/issues/11133/input.js create mode 100644 crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js diff --git a/crates/swc_ecma_minifier/src/compress/pure/mod.rs b/crates/swc_ecma_minifier/src/compress/pure/mod.rs index a7cf4ebd8703..993e443f69c0 100644 --- a/crates/swc_ecma_minifier/src/compress/pure/mod.rs +++ b/crates/swc_ecma_minifier/src/compress/pure/mod.rs @@ -97,6 +97,121 @@ impl Repeated for Pure<'_> { } impl Pure<'_> { + fn merge_duplicate_imports(&mut self, items: &mut Vec) { + use std::collections::HashMap; + + let mut import_map: HashMap> = HashMap::new(); + + // First pass: group imports by source + for (idx, item) in items.iter().enumerate() { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item { + let src = import_decl.src.value.to_string(); + import_map.entry(src).or_default().push(idx); + } + } + + let mut indices_to_remove = Vec::new(); + + // Second pass: merge imports for each source + for (_, mut indices) in import_map { + if indices.len() > 1 { + // Sort indices to keep the first one and merge others into it + indices.sort(); + let first_idx = indices[0]; + + let mut merged_specifiers = Vec::new(); + + // Collect all specifiers from all imports with this source + for &idx in &indices { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = &items[idx] { + for spec in &import_decl.specifiers { + // Check if we already have this specifier + let mut should_add = true; + + match spec { + ImportSpecifier::Named(named) => { + // Check for duplicates based on imported name + for existing_spec in &merged_specifiers { + if let ImportSpecifier::Named(existing_named) = + existing_spec + { + let named_imported = match &named.imported { + Some(ModuleExportName::Ident(ident)) => &ident.sym, + None => &named.local.sym, + _ => continue, + }; + let existing_imported = match &existing_named.imported { + Some(ModuleExportName::Ident(ident)) => &ident.sym, + None => &existing_named.local.sym, + _ => continue, + }; + if named_imported == existing_imported { + should_add = false; + break; + } + } + } + } + ImportSpecifier::Default(_) => { + // Check if we already have a default import + for existing_spec in &merged_specifiers { + if matches!(existing_spec, ImportSpecifier::Default(_)) { + should_add = false; + break; + } + } + } + ImportSpecifier::Namespace(_) => { + // Don't merge namespace imports with others if other types + // exist + for existing_spec in &merged_specifiers { + if !matches!(existing_spec, ImportSpecifier::Namespace(_)) { + should_add = false; + break; + } + } + // Also don't add other types if namespace exists + if !merged_specifiers.is_empty() + && !matches!( + merged_specifiers[0], + ImportSpecifier::Namespace(_) + ) + { + should_add = false; + } + } + } + + if should_add { + merged_specifiers.push(spec.clone()); + } + } + } + } + + // Update the first import with merged specifiers + if let ModuleItem::ModuleDecl(ModuleDecl::Import(ref mut import_decl)) = + &mut items[first_idx] + { + import_decl.specifiers = merged_specifiers; + } + + // Mark other imports for removal + for &idx in &indices[1..] { + indices_to_remove.push(idx); + } + + self.changed = true; + } + } + + // Third pass: remove duplicate imports (in reverse order to maintain indices) + indices_to_remove.sort_by(|a, b| b.cmp(a)); + for idx in indices_to_remove { + items.remove(idx); + } + } + fn handle_stmt_likes(&mut self, stmts: &mut Vec) where T: ModuleItemExt + Take, @@ -808,6 +923,10 @@ impl VisitMut for Pure<'_> { fn visit_mut_module_items(&mut self, items: &mut Vec) { self.visit_par(1, items); + if self.options.merge_duplicate_imports { + self.merge_duplicate_imports(items); + } + self.handle_stmt_likes(items); } diff --git a/crates/swc_ecma_minifier/src/option/mod.rs b/crates/swc_ecma_minifier/src/option/mod.rs index 2099a6f2aed7..cd92086183e5 100644 --- a/crates/swc_ecma_minifier/src/option/mod.rs +++ b/crates/swc_ecma_minifier/src/option/mod.rs @@ -385,6 +385,10 @@ pub struct CompressOptions { #[cfg_attr(feature = "extra-serde", serde(default))] pub experimental: CompressExperimentalOptions, + + #[cfg_attr(feature = "extra-serde", serde(default))] + #[cfg_attr(feature = "extra-serde", serde(alias = "merge_duplicate_imports"))] + pub merge_duplicate_imports: bool, } impl CompressOptions { @@ -479,6 +483,7 @@ impl Default for CompressOptions { const_to_let: true, pristine_globals: true, experimental: Default::default(), + merge_duplicate_imports: false, } } } diff --git a/crates/swc_ecma_minifier/src/option/terser.rs b/crates/swc_ecma_minifier/src/option/terser.rs index 73a57583076c..e32c7129c460 100644 --- a/crates/swc_ecma_minifier/src/option/terser.rs +++ b/crates/swc_ecma_minifier/src/option/terser.rs @@ -415,6 +415,7 @@ impl TerserCompressorOptions { ) }) .unwrap_or(CompressExperimentalOptions::from_defaults(self.defaults)), + merge_duplicate_imports: false, } } } diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json b/crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json new file mode 100644 index 000000000000..b3bb7034e714 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json @@ -0,0 +1,5 @@ +{ + "compress": { + "mergeDuplicateImports": true + } +} \ No newline at end of file diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/11133/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/11133/input.js new file mode 100644 index 000000000000..12aac59be53b --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/issues/11133/input.js @@ -0,0 +1,10 @@ +import { add } from 'math-library'; +import { subtract, multiply } from 'math-library'; +import { divide } from 'math-library'; +import { add as addAgain } from 'math-library'; + +console.log(add(1, 2)); +console.log(subtract(5, 3)); +console.log(multiply(2, 4)); +console.log(divide(10, 2)); +console.log(addAgain(3, 4)); \ No newline at end of file diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js new file mode 100644 index 000000000000..30d23bfb20fd --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js @@ -0,0 +1,6 @@ +import { add, subtract, multiply, divide } from 'math-library'; +console.log(add(1, 2)); +console.log(subtract(5, 3)); +console.log(multiply(2, 4)); +console.log(divide(10, 2)); +console.log(add(3, 4)); \ No newline at end of file From 1a2188cfb8c797212cc113a31c0f78fa609a3f82 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:49:44 +0000 Subject: [PATCH 2/2] feat(es/minifier): Make import deduplication unconditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove merge_duplicate_imports option and make import deduplication run unconditionally during minification to reduce bundle size. - Remove merge_duplicate_imports option from CompressOptions - Update pure optimizer to always merge duplicate imports - Remove option from Terser compatibility layer - Update test configuration to reflect unconditional behavior Fixes #11133 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Donny/강동윤 --- crates/swc_ecma_minifier/src/compress/pure/mod.rs | 4 +--- crates/swc_ecma_minifier/src/option/mod.rs | 4 ---- crates/swc_ecma_minifier/src/option/terser.rs | 1 - .../swc_ecma_minifier/tests/fixture/issues/11133/config.json | 4 +--- 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/crates/swc_ecma_minifier/src/compress/pure/mod.rs b/crates/swc_ecma_minifier/src/compress/pure/mod.rs index 993e443f69c0..5bd6ee537e17 100644 --- a/crates/swc_ecma_minifier/src/compress/pure/mod.rs +++ b/crates/swc_ecma_minifier/src/compress/pure/mod.rs @@ -923,9 +923,7 @@ impl VisitMut for Pure<'_> { fn visit_mut_module_items(&mut self, items: &mut Vec) { self.visit_par(1, items); - if self.options.merge_duplicate_imports { - self.merge_duplicate_imports(items); - } + self.merge_duplicate_imports(items); self.handle_stmt_likes(items); } diff --git a/crates/swc_ecma_minifier/src/option/mod.rs b/crates/swc_ecma_minifier/src/option/mod.rs index cd92086183e5..8190a9c7a4a3 100644 --- a/crates/swc_ecma_minifier/src/option/mod.rs +++ b/crates/swc_ecma_minifier/src/option/mod.rs @@ -386,9 +386,6 @@ pub struct CompressOptions { #[cfg_attr(feature = "extra-serde", serde(default))] pub experimental: CompressExperimentalOptions, - #[cfg_attr(feature = "extra-serde", serde(default))] - #[cfg_attr(feature = "extra-serde", serde(alias = "merge_duplicate_imports"))] - pub merge_duplicate_imports: bool, } impl CompressOptions { @@ -483,7 +480,6 @@ impl Default for CompressOptions { const_to_let: true, pristine_globals: true, experimental: Default::default(), - merge_duplicate_imports: false, } } } diff --git a/crates/swc_ecma_minifier/src/option/terser.rs b/crates/swc_ecma_minifier/src/option/terser.rs index e32c7129c460..73a57583076c 100644 --- a/crates/swc_ecma_minifier/src/option/terser.rs +++ b/crates/swc_ecma_minifier/src/option/terser.rs @@ -415,7 +415,6 @@ impl TerserCompressorOptions { ) }) .unwrap_or(CompressExperimentalOptions::from_defaults(self.defaults)), - merge_duplicate_imports: false, } } } diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json b/crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json index b3bb7034e714..cebe35a0fc44 100644 --- a/crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json +++ b/crates/swc_ecma_minifier/tests/fixture/issues/11133/config.json @@ -1,5 +1,3 @@ { - "compress": { - "mergeDuplicateImports": true - } + "compress": {} } \ No newline at end of file