diff --git a/crates/swc/tests/tsc-references/es6modulekindWithES5Target9.2.minified.js b/crates/swc/tests/tsc-references/es6modulekindWithES5Target9.2.minified.js index 0718b59bb1b4..892c1c4500f7 100644 --- a/crates/swc/tests/tsc-references/es6modulekindWithES5Target9.2.minified.js +++ b/crates/swc/tests/tsc-references/es6modulekindWithES5Target9.2.minified.js @@ -1,7 +1,7 @@ //// [es6modulekindWithES5Target9.ts] +import d, { a } from "mod"; +import * as M from "mod"; export * from "mod"; export { b } from "mod"; export default d; -import d, { a } from "mod"; -import * as M from "mod"; export { a, M, d }; diff --git a/crates/swc/tests/tsc-references/esnextmodulekindWithES5Target9.2.minified.js b/crates/swc/tests/tsc-references/esnextmodulekindWithES5Target9.2.minified.js index cae21b6a9918..c58abcb045ef 100644 --- a/crates/swc/tests/tsc-references/esnextmodulekindWithES5Target9.2.minified.js +++ b/crates/swc/tests/tsc-references/esnextmodulekindWithES5Target9.2.minified.js @@ -1,7 +1,7 @@ //// [esnextmodulekindWithES5Target9.ts] +import d, { a } from "mod"; +import * as M from "mod"; export * from "mod"; export { b } from "mod"; export default d; -import d, { a } from "mod"; -import * as M from "mod"; export { a, M, d }; diff --git a/crates/swc/tests/tsc-references/exportNamespace12.2.minified.js b/crates/swc/tests/tsc-references/exportNamespace12.2.minified.js index 8bb0418b1cf9..ad1dc21b6c41 100644 --- a/crates/swc/tests/tsc-references/exportNamespace12.2.minified.js +++ b/crates/swc/tests/tsc-references/exportNamespace12.2.minified.js @@ -1,7 +1,7 @@ //// [main.ts] -console.log(c), console.log(types.c); import * as types from "./types"; import { c } from "./types"; +console.log(c), console.log(types.c); //// [types.ts] export { }; //// [values.ts] diff --git a/crates/swc_ecma_minifier/src/pass/postcompress.rs b/crates/swc_ecma_minifier/src/pass/postcompress.rs index ea9491e37e8a..6c7e890733d4 100644 --- a/crates/swc_ecma_minifier/src/pass/postcompress.rs +++ b/crates/swc_ecma_minifier/src/pass/postcompress.rs @@ -1,4 +1,4 @@ -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use swc_common::{util::take::Take, DUMMY_SP}; use swc_ecma_ast::*; @@ -132,10 +132,10 @@ impl SpecifierKey { /// This optimization reduces bundle size by combining multiple imports from /// the same source into a single import declaration. fn merge_imports_in_module(module: &mut Module) { - // Group imports by source and metadata - let mut import_groups: FxHashMap> = FxHashMap::default(); + // Group imports by source and metadata, also track the first occurrence index + let mut import_groups: FxHashMap)> = FxHashMap::default(); - for item in module.body.iter() { + for (idx, item) in module.body.iter().enumerate() { if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item { // Skip side-effect only imports (no specifiers) if import_decl.specifiers.is_empty() { @@ -145,41 +145,65 @@ fn merge_imports_in_module(module: &mut Module) { let key = ImportKey::from_import_decl(import_decl); import_groups .entry(key) - .or_default() + .or_insert_with(|| (idx, Vec::new())) + .1 .push(import_decl.clone()); } } + // Build a map of indices where merged imports should be inserted + let mut inserts_at: FxHashMap> = FxHashMap::default(); + + for (key, (first_idx, import_decls)) in import_groups.iter() { + if import_decls.len() > 1 { + let merged_imports = merge_import_decls(import_decls, key); + inserts_at.insert(*first_idx, merged_imports); + } + } + // Remove all imports that will be merged (except side-effect imports) - module.body.retain(|item| { + // and insert merged imports at the position of the first occurrence + let mut new_body = Vec::new(); + let mut processed_indices = FxHashSet::default(); + + for (idx, item) in module.body.iter().enumerate() { if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item { // Keep side-effect imports if import_decl.specifiers.is_empty() { - return true; + new_body.push(item.clone()); + continue; } let key = ImportKey::from_import_decl(import_decl); - // Only keep if there's just one import for this key (no merging needed) - import_groups.get(&key).map_or(true, |v| v.len() <= 1) - } else { - true - } - }); - // Create merged imports and add them back - for (key, import_decls) in import_groups.iter() { - if import_decls.len() <= 1 { - // No merging needed, already retained above - continue; - } + // Check if this import is part of a merge group + if let Some((first_idx, decls)) = import_groups.get(&key) { + if decls.len() > 1 { + // This import needs to be merged + if idx == *first_idx && processed_indices.insert(*first_idx) { + // This is the first occurrence - insert merged imports here + for merged in inserts_at.get(first_idx).expect( + "Invariant violated: first_idx should always be present in inserts_at \ + due to import group construction", + ) { + new_body + .push(ModuleItem::ModuleDecl(ModuleDecl::Import(merged.clone()))); + } + } + // Skip this individual import (it's been merged) + continue; + } + } - let merged_imports = merge_import_decls(import_decls, key); - for merged in merged_imports { - module - .body - .push(ModuleItem::ModuleDecl(ModuleDecl::Import(merged))); + // Keep imports that don't need merging + new_body.push(item.clone()); + } else { + // Keep all non-import items + new_body.push(item.clone()); } } + + module.body = new_body; } /// Merge multiple ImportDecl nodes. diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js index d64658503a70..fa7de9a78f74 100644 --- a/crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js +++ b/crates/swc_ecma_minifier/tests/fixture/issues/11133/output.js @@ -1,23 +1,23 @@ // Test case 1: Basic duplicate named imports +import { add, subtract, multiply } from "math"; +// Test case 2: Same export imported with different local names (should preserve both) +import { add as a, add as b } from "calculator"; +// Test case 3: Mix of default and named imports +import defaultExport, { namedExport } from "module1"; +// Test case 4: Namespace import with named imports (CANNOT be merged - incompatible) +import * as utils from "utils"; +import { helper } from "utils"; +// Test case 4b: Default with namespace (CAN be merged) +import defUtils, * as utils2 from "utils2"; // Test case 5: Side-effect import (should not be merged) import 'polyfill'; import 'polyfill'; // Test case 6: Different sources (should not be merged) import { foo } from 'lib1'; import { foo } from 'lib2'; -// Use all imports to avoid dead code elimination -console.log(add, subtract, multiply), console.log(a, b), console.log(defaultExport, namedExport), console.log(utils, helper), console.log(defUtils, utils2), console.log(foo), console.log(duplicate), console.log(thing, renamedThing, otherThing); -// Test case 4: Namespace import with named imports (CANNOT be merged - incompatible) -import * as utils from "utils"; -import { helper } from "utils"; -// Test case 8: Mix of named imports with and without aliases -import { thing, thing as renamedThing, otherThing } from "things"; -// Test case 2: Same export imported with different local names (should preserve both) -import { add as a, add as b } from "calculator"; -// Test case 4b: Default with namespace (CAN be merged) -import defUtils, * as utils2 from "utils2"; -// Test case 3: Mix of default and named imports -import defaultExport, { namedExport } from "module1"; -import { add, subtract, multiply } from "math"; // Test case 7: Duplicate named imports (exact same specifier) import { duplicate } from "dups"; +// Test case 8: Mix of named imports with and without aliases +import { thing, thing as renamedThing, otherThing } from "things"; +// Use all imports to avoid dead code elimination +console.log(add, subtract, multiply), console.log(a, b), console.log(defaultExport, namedExport), console.log(utils, helper), console.log(defUtils, utils2), console.log(foo), console.log(duplicate), console.log(thing, renamedThing, otherThing); diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/11257/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/11257/input.js new file mode 100644 index 000000000000..9f80405f2fc2 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/issues/11257/input.js @@ -0,0 +1,6 @@ +import { v1 } from 'a'; +import { v2 } from 'b'; +import { v3 } from 'b'; +import { v4 } from 'c'; + +console.log(v1, v2, v3, v4); diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/11257/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/11257/output.js new file mode 100644 index 000000000000..0b62ab27e6c9 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/issues/11257/output.js @@ -0,0 +1,4 @@ +import { v1 } from 'a'; +import { v2, v3 } from "b"; +import { v4 } from 'c'; +console.log(v1, v2, v3, v4);