Skip to content

Conversation

@delino
Copy link
Contributor

@delino delino bot commented Oct 19, 2025

Summary

Implements a new optimization pass in the SWC ECMAScript minifier to merge duplicate import statements from the same module source, reducing bundle size by eliminating redundant imports.

Fixes #11133

Changes

Core Implementation

  • New file: crates/swc_ecma_minifier/src/pass/merge_imports.rs
    • Groups imports by source module and metadata (type_only, phase, with clause)
    • Merges compatible import specifiers while respecting ES module syntax constraints
    • Handles all valid import combinations:
      • ✅ Multiple named imports → merged into single import
      • ✅ Default + named imports → merged
      • ✅ Default + namespace imports → merged
      • ❌ Namespace + named imports (without default) → kept separate (invalid ES syntax)
    • Deduplicates exact duplicate imports
    • Preserves different aliases for the same export

Integration

  • Modified: crates/swc_ecma_minifier/src/pass/mod.rs - Added merge_imports module
  • Modified: crates/swc_ecma_minifier/src/lib.rs - Integrated pass into minifier pipeline (runs before merge_exports)
  • Modified: crates/swc_ecma_minifier/src/option/mod.rs - Added merge_imports: bool config option (default: true)
  • Modified: crates/swc_ecma_minifier/src/option/terser.rs - Added terser compatibility

Testing

  • New: crates/swc_ecma_minifier/tests/fixture/issues/11133/
    • Comprehensive test cases covering all scenarios
    • Tests verified passing with cargo test --test compress -- 11133

Example

Before

import { add } from 'math';
import { subtract } from 'math';
import { multiply } from 'math';

After

import { add, subtract, multiply } from 'math';

Test Plan

Configuration

Users can control this optimization via the compress.merge_imports option in .swcrc:

{
  "jsc": {
    "minify": {
      "compress": {
        "merge_imports": true
      }
    }
  }
}

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

Implements a new optimization pass that merges duplicate import statements
from the same module source, reducing bundle size by eliminating redundant imports.

Fixes #11133

## Implementation

- Added `merge_imports.rs` pass that groups imports by source and merges compatible ones
- Handles all valid ES module import combinations:
  - Multiple named imports → merged into single import
  - Default + named imports → merged
  - Default + namespace imports → merged
  - Namespace + named imports (without default) → kept separate (no valid syntax)
- Preserves import attributes/assertions (`with` clause)
- Preserves type-only imports separately
- Preserves side-effect imports (no specifiers)
- Deduplicates exact duplicate imports
- Preserves different aliases for the same export

## Configuration

- Added `merge_imports: bool` option to `CompressOptions` (default: `true`)
- Integrated into minifier pipeline before `merge_exports`

## Testing

- Added comprehensive test fixtures in `tests/fixture/issues/11133/`
- Tests cover:
  - Basic duplicate named imports
  - Same export with different local names (aliases)
  - Default + named imports
  - Default + namespace imports
  - Namespace + named imports (incompatible, not merged)
  - Side-effect imports (preserved)
  - Different sources (not merged)
  - Exact duplicates (deduplicated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Oct 19, 2025

🦋 Changeset detected

Latest commit: a68df99

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant
Copy link

CLAassistant commented Oct 19, 2025

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ kdy1
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Member

kdy1 commented Oct 19, 2025

🤖 This pull request has been linked to AutoDev Task #771

View the task details and manage the automated development workflow in AutoDev.

Copy link
Member

kdy1 commented Oct 19, 2025

📋 AutoDev Task Prompt

Objective

Implement a new optimization pass in the SWC ECMAScript minifier to merge duplicate import statements from the same module source, reducing bundle size by eliminating redundant imports.

Background

GitHub Issue: #11133

Currently, when multiple source files import from the same module, the minified output contains duplicate import statements with different aliases, unnecessarily bloating the bundle size.

Example of current behavior:

// Input
import { add } from 'math-library';
import { add } from 'math-library';

// Current output (undesired)
import { add as a } from 'math-library';
import { add as b } from 'math-library';

// Expected output (desired)
import { add } from 'math-library';

Technical Context

SWC Architecture Documentation

Key SWC Structures (from codebase analysis)

ImportDecl Structure (swc_ecma_ast/src/module_decl.rs:116-137):

pub struct ImportDecl {
    pub span: Span,
    pub specifiers: Vec<ImportSpecifier>,
    pub src: Box<Str>,  // The module source path
    pub type_only: bool,
    pub with: Option<Box<ObjectLit>>,
    pub phase: ImportPhase,
}

ImportSpecifier Enum (swc_ecma_ast/src/module_decl.rs:265-297):

pub enum ImportSpecifier {
    Named(ImportNamedSpecifier),    // import { foo } or import { foo as bar }
    Default(ImportDefaultSpecifier), // import foo
    Namespace(ImportStarAsSpecifier), // import * as foo
}

ImportNamedSpecifier (swc_ecma_ast/src/module_decl.rs:322-336):

pub struct ImportNamedSpecifier {
    pub span: Span,
    pub local: Ident,                      // Local binding name
    pub imported: Option<ModuleExportName>, // Original export name (if aliased)
    pub is_type_only: bool,
}

Existing Similar Pattern

The codebase already has a merge_exports pass (swc_ecma_minifier/src/pass/merge_exports.rs) that merges export statements. This should be used as a reference for implementing import merging:

// merge_exports.rs groups exports from the same source
pub(crate) fn merge_exports() -> impl VisitMut {
    Merger::default()
}

Implementation Scope

Files to Create/Modify

  1. Create: crates/swc_ecma_minifier/src/pass/merge_imports.rs

    • New pass to merge duplicate import declarations
    • Use VisitMut trait to traverse and transform AST
    • Group imports by source path (src field)
    • Merge specifiers while handling naming conflicts
  2. Modify: crates/swc_ecma_minifier/src/pass/mod.rs

    • Add module declaration for merge_imports
    • Export the merge_imports function
  3. Modify: crates/swc_ecma_minifier/src/lib.rs

    • Integrate merge_imports pass into the optimization pipeline
    • Add it near line 227 where merge_exports is called
    • Ensure proper ordering (run after other optimizations but before merge_exports)
  4. Modify: crates/swc_ecma_minifier/src/option/mod.rs

    • Add configuration option merge_imports: bool to CompressOptions
    • Default to true (enabled by default like other optimizations)
    • Add serde attributes for .swcrc configuration support
  5. Create: Test fixtures in crates/swc_ecma_minifier/tests/fixture/issues/11133/

    • input.js: Test cases with duplicate imports
    • output.js: Expected minified output
    • config.json: Test configuration (if needed)

Algorithm Design

The merge_imports pass should:

  1. Group Phase: Traverse module items and group ImportDecl nodes by their source path

    • Key: The src.value (module path as string)
    • Value: List of ImportDecl nodes from that source
    • Handle import assertions/attributes (with field) correctly
    • Preserve type-only imports separately
  2. Merge Phase: For each group of imports from the same source:

    • Collect all specifiers from all ImportDecl nodes
    • Detect and resolve naming conflicts:
      • If same export imported multiple times with different local names, keep first occurrence
      • Preserve namespace imports (* as foo)
      • Preserve default imports
    • Create single merged ImportDecl with combined specifiers
    • Maintain original span information for source maps
  3. Replace Phase: Replace all duplicate ImportDecl nodes with the single merged version

    • Keep the first occurrence's position
    • Remove duplicate declarations

Naming Conflict Resolution Strategy

// Example conflict scenarios to handle:

// Case 1: Same named import, same local name -> merge (easy case)
import { add } from 'math';
import { add } from 'math';
// Result: import { add } from 'math';

// Case 2: Same named import, different local names -> keep separate bindings
import { add as a } from 'math';
import { add as b } from 'math';
// Result: import { add as a, add as b } from 'math';

// Case 3: Mix of default and named imports
import foo from 'math';
import { add } from 'math';
// Result: import foo, { add } from 'math';

// Case 4: Namespace import with named imports -> keep separate
import * as math from 'math';
import { add } from 'math';
// Result: import * as math, { add } from 'math';

// Case 5: Type-only imports (TypeScript)
import type { Foo } from 'types';
import { Bar } from 'types';
// Result: Keep separate (type_only flag differs)

Implementation References

Rust VisitMut Pattern (from merge_exports.rs):

use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};

struct ImportMerger {
    // Store grouped imports: HashMap<module_source, Vec<ImportDecl>>
    imports_by_source: FxHashMap<String, Vec<ImportDecl>>,
}

impl VisitMut for ImportMerger {
    noop_visit_mut_type!(fail);

    fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
        // 1. Collect and group imports
        // 2. Merge duplicates
        // 3. Replace in items vector
    }
}

Rust HashMap Grouping (references from web search):

Testing Strategy

Create comprehensive test fixtures following SWC conventions:

  1. Basic Duplicate Imports:

    • Input: Multiple identical import statements
    • Expected: Single merged import
  2. Named Imports with Aliases:

    • Input: Same exports with different local aliases
    • Expected: Single import with multiple specifiers
  3. Mixed Import Types:

    • Input: Default + named + namespace imports from same source
    • Expected: Properly merged single import statement
  4. Type-Only Imports (TypeScript):

    • Input: Mix of type-only and regular imports
    • Expected: Kept separate due to different semantics
  5. Import Attributes/Assertions:

    • Input: Imports with with clauses
    • Expected: Only merge if attributes match exactly
  6. Edge Cases:

    • Side-effect imports: import 'module';
    • Dynamic imports: Should not be affected
    • Re-exports: Should not interfere

Testing Commands

From crates/swc_ecma_minifier directory:

  • Run specific test: ./scripts/test.sh 11133
  • Run all tests: ./scripts/test.sh
  • Run with debug: ./scripts/exec.sh

Documentation: https://swc.rs/docs/contributing/es-minifier/profiling

Success Criteria

  1. ✅ All duplicate imports from the same module source are merged into a single import statement
  2. ✅ Naming conflicts are handled correctly (different local names for same export preserved)
  3. ✅ Type-only imports (TypeScript) are handled correctly
  4. ✅ Import attributes/assertions are respected (only merge if they match)
  5. ✅ Side-effect imports are preserved
  6. ✅ Configuration option merge_imports is available in CompressOptions
  7. ✅ All existing minifier tests continue to pass
  8. ✅ New test fixtures for issue [FR] Merge duplicate named imports from the same module during minification #11133 pass
  9. ✅ Source maps remain accurate after transformation
  10. ✅ The optimization produces smaller bundle sizes for duplicate imports

Additional Resources

Notes

  • Follow existing SWC code style and patterns (see merge_exports.rs as reference)
  • Use FxHashMap for performance (already used throughout SWC codebase)
  • Preserve span information for accurate source maps
  • Consider performance impact - this pass will run on every minification
  • The optimization should be conservative - if unsure whether imports can be merged, keep them separate
  • Ensure the pass is idempotent (running it multiple times produces same result)

This comment was automatically added by AutoDev. You can disable this feature in AutoDev Settings.

@claude
Copy link
Contributor

claude bot commented Oct 19, 2025

Claude finished @DeLino[bot]'s task —— View job


Code Review Complete

I'll review this PR implementation of the merge_imports optimization pass. Let me analyze the code quality, potential issues, and test coverage.

Review Tasks:

  • Read repository guidelines (CLAUDE.md) - Not found, proceeding with standard Rust/SWC practices
  • Review core implementation approach and code quality
  • Check for potential bugs and edge cases
  • Evaluate performance considerations
  • Assess security concerns
  • Review test coverage and quality
  • Check if reviewer feedback has been addressed
  • Provide comprehensive feedback

Code Review Summary

This PR implements a solid merge_imports optimization pass with good attention to ES module syntax constraints. The implementation correctly addresses the reviewer's feedback and follows SWC patterns well.

Strengths

Architecture & Design:

  • Correctly implemented as a method in postcompress_optimizer instead of a separate visitor (addresses reviewer feedback)
  • Proper integration into the minifier pipeline at crates/swc_ecma_minifier/src/pass/postcompress.rs:59-61
  • Well-structured key-based grouping system with ImportKey and SpecifierKey
  • Handles complex ES module syntax rules correctly (namespace + named imports require splitting)

ES Module Compliance:

  • Correctly handles invalid combinations (namespace + named without default)
  • Preserves side-effect imports (import 'polyfill')
  • Respects import assertions/attributes via with_hash
  • Maintains TypeScript type-only imports separately
  • Proper deduplication of exact duplicate specifiers

Configuration:

  • Proper integration in CompressOptions with merge_imports: bool (default: true)
  • Terser compatibility added in terser.rs:346
  • Good changeset documentation

⚠️ Issues & Concerns

Potential Bugs:

  1. Hash collision risk (postcompress.rs:82-84): Using format!("{w:?}") for hashing import assertions could be fragile. Consider using a more stable serialization method.
// Current approach - potentially unstable
format!("{w:?}").hash(&mut hasher);

// Suggested: More stable approach needed
  1. Span handling (postcompress.rs:241-278): Using DUMMY_SP for new imports loses source map information. Consider preserving the original span or using the first declaration's span consistently.

Performance Considerations:

  1. Memory allocation (postcompress.rs:136-149): The implementation clones ImportDecl nodes during grouping, which could be expensive for large files. Consider using references where possible.

  2. HashMap operations: Multiple HashMap lookups per import could be optimized with entry API patterns.

Test Coverage Gaps:

  1. Import assertions/attributes: No test cases for imports with with clauses
  2. TypeScript type-only imports: Missing comprehensive type-only import scenarios
  3. Error handling: No tests for malformed import declarations

Code Quality:

  1. Magic numbers: Line 163-172 has complex logic that could benefit from helper methods for readability
  2. Documentation: The function merge_import_decls has good comments but could use more examples

🔧 Addressing Reviewer Feedback

✅ Resolved:

  • "Do not use a new visitor" - Correctly integrated into postcompress_optimizer
  • "cargo fmt" - Code appears formatted (commit 037b2d3)

❓ Status unclear:

  • "Fix CI" - Need to verify current CI status
  • "Run UPDATE=1 cargo test -p swc --test projects --test tsc" - TSC reference files were updated, suggesting this was run

📋 Recommendations

High Priority:

  1. Improve hash stability for import assertions
  2. Add comprehensive test cases for import assertions and type-only imports
  3. Preserve source spans instead of using DUMMY_SP

Medium Priority:

  1. Optimize memory usage by reducing clones during grouping
  2. Add helper methods to break down complex merging logic
  3. Add error handling for edge cases

Low Priority:

  1. Performance profiling for large codebases with many imports
  2. Consider configuration granularity (per-module vs global)

🧪 Test Quality Assessment

The test cases in tests/fixture/issues/11133/ are comprehensive and cover the major scenarios well:

  • ✅ Basic duplicate merging
  • ✅ Alias preservation
  • ✅ Default + named combinations
  • ✅ Namespace handling (both valid and invalid combinations)
  • ✅ Side-effect preservation
  • ✅ Cross-module separation

Missing test coverage:

  • Import assertions (import foo from 'mod' with { type: 'json' })
  • TypeScript type-only imports
  • Very large numbers of imports (performance)
  • Malformed import statements

🔒 Security Assessment

Low Risk: This optimization pass operates on AST nodes and doesn't introduce security vulnerabilities. The hash-based grouping doesn't expose sensitive data.

📊 Performance Impact

Positive: Reduces bundle size by eliminating duplicate imports
Potential Concern: Additional processing during minification, but impact should be minimal given the optimization's value


Overall Assessment: APPROVE with minor suggestions

This is a well-implemented feature that addresses a real optimization need. The core logic is sound, handles ES module complexities correctly, and follows SWC patterns. The suggested improvements are mainly for robustness and test coverage rather than fundamental issues.


@kdy1 kdy1 self-assigned this Oct 19, 2025
Copy link
Member

@kdy1 kdy1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use a new visitor. Add a method to the Optimizer instead

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 19, 2025

CodSpeed Performance Report

Merging #11151 will not alter performance

Comparing autodev/merge-duplicate-imports (a68df99) with main (e347c5b)1

Summary

✅ 140 untouched

Footnotes

  1. No successful run was found on main (c2e75f3) during the generation of this report, so e347c5b was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Move merge_imports logic from a standalone visitor into the
postcompress_optimizer function as a method, following the
feedback from PR review.

This change:
- Removes the standalone merge_imports visitor
- Adds merge_imports_in_module function to postcompress.rs
- Integrates the logic directly into the postcompress optimization flow
- Maintains all existing functionality and test compatibility

Addresses PR review feedback: #11151 (review)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Member

@kdy1 kdy1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cargo fmt

Address PR review feedback to run cargo fmt on the codebase.
Changes:
- Restructure use statements to use nested imports
- Remove extraneous blank line in merge_imports_in_module

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Member

@kdy1 kdy1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix CI

- Use inline format args for better readability
- Replace or_insert_with(Vec::new) with or_default()
- Use Entry API instead of contains_key + insert pattern

These changes address clippy warnings that were causing CI failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Member

@kdy1 kdy1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run UPDATE=1 cargo test -p swc --test projects --test tsc

…amed correctly

The previous implementation incorrectly tried to merge default, namespace,
and named imports into a single import declaration, which is not valid ES
module syntax and caused a panic in the code generator.

This fix ensures that when all three import types are present, they are
properly split into separate import declarations:
- default + named imports in one declaration
- namespace import in a separate declaration

This follows the ES module specification which only allows:
- default + named
- default + namespace (only these two, no named)
- namespace alone
- named alone

Addresses review feedback from PR #11151

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@kdy1 kdy1 marked this pull request as ready for review October 19, 2025 13:55
@kdy1 kdy1 requested a review from a team as a code owner October 19, 2025 13:55
@kdy1 kdy1 requested a review from a team as a code owner October 19, 2025 14:00
@kdy1 kdy1 merged commit a01dee1 into main Oct 19, 2025
173 of 174 checks passed
@kdy1 kdy1 deleted the autodev/merge-duplicate-imports branch October 19, 2025 14:31
@kdy1 kdy1 modified the milestones: Planned, v1.13.21 Oct 20, 2025
@swc-project swc-project locked as resolved and limited conversation to collaborators Nov 23, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[FR] Merge duplicate named imports from the same module during minification

3 participants