swc_ecma_minifier/pass/
postcompress.rs

1use rustc_hash::FxHashMap;
2use swc_common::{util::take::Take, DUMMY_SP};
3use swc_ecma_ast::*;
4
5use crate::option::CompressOptions;
6
7pub fn postcompress_optimizer(program: &mut Program, options: &CompressOptions) {
8    let Some(module) = program.as_mut_module() else {
9        return;
10    };
11
12    for item in &mut module.body {
13        let Some(m) = item.as_mut_module_decl() else {
14            continue;
15        };
16
17        if let ModuleDecl::ExportDefaultExpr(e) = m {
18            match &mut *e.expr {
19                Expr::Fn(f) => {
20                    if f.ident.is_some() {
21                        if options.top_level() {
22                            *m = ExportDefaultDecl {
23                                span: e.span,
24                                decl: DefaultDecl::Fn(f.take()),
25                            }
26                            .into()
27                        }
28                    } else {
29                        *m = ExportDefaultDecl {
30                            span: e.span,
31                            decl: DefaultDecl::Fn(f.take()),
32                        }
33                        .into()
34                    }
35                }
36                Expr::Class(c) => {
37                    if c.ident.is_some() {
38                        if options.top_level() {
39                            *m = ExportDefaultDecl {
40                                span: e.span,
41                                decl: DefaultDecl::Class(c.take()),
42                            }
43                            .into()
44                        }
45                    } else {
46                        *m = ExportDefaultDecl {
47                            span: e.span,
48                            decl: DefaultDecl::Class(c.take()),
49                        }
50                        .into()
51                    }
52                }
53                _ => (),
54            }
55        }
56    }
57
58    // Merge duplicate imports if enabled
59    if options.merge_imports {
60        merge_imports_in_module(module);
61    }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65struct ImportKey {
66    src: String,
67    type_only: bool,
68    phase: ImportPhase,
69    /// Hash of the `with` clause to group imports with the same assertions
70    with_hash: Option<u64>,
71}
72
73impl ImportKey {
74    fn from_import_decl(decl: &ImportDecl) -> Self {
75        use std::{
76            collections::hash_map::DefaultHasher,
77            hash::{Hash, Hasher},
78        };
79
80        let with_hash = decl.with.as_ref().map(|w| {
81            let mut hasher = DefaultHasher::new();
82            // Hash the with clause structure
83            format!("{w:?}").hash(&mut hasher);
84            hasher.finish()
85        });
86
87        Self {
88            src: decl.src.value.to_string_lossy().to_string(),
89            type_only: decl.type_only,
90            phase: decl.phase,
91            with_hash,
92        }
93    }
94}
95
96/// Key to identify unique import specifiers.
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98enum SpecifierKey {
99    /// Named import: (imported name, local name, is_type_only)
100    Named(String, String, bool),
101    /// Default import: (local name)
102    Default(String),
103    /// Namespace import: (local name)
104    Namespace(String),
105}
106
107impl SpecifierKey {
108    fn from_specifier(spec: &ImportSpecifier) -> Self {
109        match spec {
110            ImportSpecifier::Named(named) => {
111                let imported = named
112                    .imported
113                    .as_ref()
114                    .map(|n| match n {
115                        ModuleExportName::Ident(id) => id.sym.to_string(),
116                        ModuleExportName::Str(s) => s.value.to_string_lossy().to_string(),
117                    })
118                    .unwrap_or_else(|| named.local.sym.to_string());
119
120                SpecifierKey::Named(imported, named.local.sym.to_string(), named.is_type_only)
121            }
122            ImportSpecifier::Default(default) => {
123                SpecifierKey::Default(default.local.sym.to_string())
124            }
125            ImportSpecifier::Namespace(ns) => SpecifierKey::Namespace(ns.local.sym.to_string()),
126        }
127    }
128}
129
130/// Merge duplicate import statements from the same module source.
131///
132/// This optimization reduces bundle size by combining multiple imports from
133/// the same source into a single import declaration.
134fn merge_imports_in_module(module: &mut Module) {
135    // Group imports by source and metadata
136    let mut import_groups: FxHashMap<ImportKey, Vec<ImportDecl>> = FxHashMap::default();
137
138    for item in module.body.iter() {
139        if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item {
140            // Skip side-effect only imports (no specifiers)
141            if import_decl.specifiers.is_empty() {
142                continue;
143            }
144
145            let key = ImportKey::from_import_decl(import_decl);
146            import_groups
147                .entry(key)
148                .or_default()
149                .push(import_decl.clone());
150        }
151    }
152
153    // Remove all imports that will be merged (except side-effect imports)
154    module.body.retain(|item| {
155        if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item {
156            // Keep side-effect imports
157            if import_decl.specifiers.is_empty() {
158                return true;
159            }
160
161            let key = ImportKey::from_import_decl(import_decl);
162            // Only keep if there's just one import for this key (no merging needed)
163            import_groups.get(&key).map_or(true, |v| v.len() <= 1)
164        } else {
165            true
166        }
167    });
168
169    // Create merged imports and add them back
170    for (key, import_decls) in import_groups.iter() {
171        if import_decls.len() <= 1 {
172            // No merging needed, already retained above
173            continue;
174        }
175
176        let merged_imports = merge_import_decls(import_decls, key);
177        for merged in merged_imports {
178            module
179                .body
180                .push(ModuleItem::ModuleDecl(ModuleDecl::Import(merged)));
181        }
182    }
183}
184
185/// Merge multiple ImportDecl nodes.
186/// Returns a Vec because in some cases (namespace + named), we need to create
187/// multiple import statements since they cannot be combined in valid ES syntax.
188fn merge_import_decls(decls: &[ImportDecl], key: &ImportKey) -> Vec<ImportDecl> {
189    let mut default_spec: Option<ImportSpecifier> = None;
190    let mut namespace_spec: Option<ImportSpecifier> = None;
191    let mut named_specs: Vec<ImportSpecifier> = Vec::new();
192    let mut seen_named: FxHashMap<SpecifierKey, ()> = FxHashMap::default();
193
194    let first_decl = &decls[0];
195    let span = first_decl.span;
196
197    // Separate specifiers by type
198    for decl in decls {
199        for spec in &decl.specifiers {
200            match spec {
201                ImportSpecifier::Default(_) => {
202                    if default_spec.is_none() {
203                        default_spec = Some(spec.clone());
204                    }
205                }
206                ImportSpecifier::Namespace(_) => {
207                    if namespace_spec.is_none() {
208                        namespace_spec = Some(spec.clone());
209                    }
210                }
211                ImportSpecifier::Named(_) => {
212                    let spec_key = SpecifierKey::from_specifier(spec);
213                    if let std::collections::hash_map::Entry::Vacant(e) = seen_named.entry(spec_key)
214                    {
215                        e.insert(());
216                        named_specs.push(spec.clone());
217                    }
218                }
219            }
220        }
221    }
222
223    let mut result = Vec::new();
224
225    // Valid combinations in ES modules:
226    // - default only
227    // - namespace only
228    // - named only
229    // - default + named
230    // - default + namespace (ONLY these two, no named allowed)
231    // Note: namespace + named (without default) is NOT valid - must split
232    // Note: default + namespace + named is NOT valid - must split
233
234    if let Some(namespace) = namespace_spec {
235        if default_spec.is_some() {
236            if named_specs.is_empty() {
237                // default + namespace only (valid combination)
238                result.push(ImportDecl {
239                    span,
240                    specifiers: vec![default_spec.unwrap(), namespace],
241                    src: Box::new(Str {
242                        span: DUMMY_SP,
243                        value: key.src.clone().into(),
244                        raw: None,
245                    }),
246                    type_only: key.type_only,
247                    with: first_decl.with.clone(),
248                    phase: key.phase,
249                });
250            } else {
251                // default + namespace + named - MUST SPLIT
252                // Create one import for default + named
253                let mut specs = vec![default_spec.unwrap()];
254                specs.extend(named_specs);
255                result.push(ImportDecl {
256                    span,
257                    specifiers: specs,
258                    src: Box::new(Str {
259                        span: DUMMY_SP,
260                        value: key.src.clone().into(),
261                        raw: None,
262                    }),
263                    type_only: key.type_only,
264                    with: first_decl.with.clone(),
265                    phase: key.phase,
266                });
267                // Create one import for namespace
268                result.push(ImportDecl {
269                    span,
270                    specifiers: vec![namespace],
271                    src: Box::new(Str {
272                        span: DUMMY_SP,
273                        value: key.src.clone().into(),
274                        raw: None,
275                    }),
276                    type_only: key.type_only,
277                    with: first_decl.with.clone(),
278                    phase: key.phase,
279                });
280            }
281        } else if named_specs.is_empty() {
282            // Just namespace
283            result.push(ImportDecl {
284                span,
285                specifiers: vec![namespace],
286                src: Box::new(Str {
287                    span: DUMMY_SP,
288                    value: key.src.clone().into(),
289                    raw: None,
290                }),
291                type_only: key.type_only,
292                with: first_decl.with.clone(),
293                phase: key.phase,
294            });
295        } else {
296            // namespace + named without default - MUST SPLIT
297            // Create one import for namespace
298            result.push(ImportDecl {
299                span,
300                specifiers: vec![namespace],
301                src: Box::new(Str {
302                    span: DUMMY_SP,
303                    value: key.src.clone().into(),
304                    raw: None,
305                }),
306                type_only: key.type_only,
307                with: first_decl.with.clone(),
308                phase: key.phase,
309            });
310            // Create one import for named
311            result.push(ImportDecl {
312                span,
313                specifiers: named_specs,
314                src: Box::new(Str {
315                    span: DUMMY_SP,
316                    value: key.src.clone().into(),
317                    raw: None,
318                }),
319                type_only: key.type_only,
320                with: first_decl.with.clone(),
321                phase: key.phase,
322            });
323        }
324    } else {
325        // No namespace - merge default and/or named
326        let mut specs = Vec::new();
327        if let Some(default) = default_spec {
328            specs.push(default);
329        }
330        specs.extend(named_specs);
331
332        result.push(ImportDecl {
333            span,
334            specifiers: specs,
335            src: Box::new(Str {
336                span: DUMMY_SP,
337                value: key.src.clone().into(),
338                raw: None,
339            }),
340            type_only: key.type_only,
341            with: first_decl.with.clone(),
342            phase: key.phase,
343        });
344    }
345
346    result
347}