Repository: swc-project/swc (rust)

Issue:
Dropping unused imports by simplifier()
### Describe the feature

While i understand why it's not an always safe optimization, we rely on it at Qwik. This bug fix is preventing us to update to latest SWC.
https://github.com/swc-project/swc/issues/6503

It would be great to have an option to enable the previous behaviour where unused imports are dropped!

### Babel plugin or link to the feature description

_No response_

### Additional context

_No response_

Pull request:
feat: add preserve_imports_with_side_effects option for simplify optimization
<!-- Note: CI script will automatically rebase your PR so please do not rebase unless required -->

<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->

**Description:** Fix #6771

This implementation is still rough, and there are still some implementation decisions that need to be decided on that I mentioned in my comments.

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->

**BREAKING CHANGE:** Yes

<!--
If this PR introduces a breaking change, it must contain a notice for it to be included in the CHANGELOG. Add description or remove entirely if not breaking.

You may need to update `MIGRATION.md` for the breaking changes.
-->

**Related issue (if exists):**


Patch:
diff --git a/crates/swc/src/config/mod.rs b/crates/swc/src/config/mod.rs
index 265c2a2d4c..101729756a 100644
--- a/crates/swc/src/config/mod.rs
+++ b/crates/swc/src/config/mod.rs
@@ -67,7 +67,11 @@ use swc_ecma_transforms::{
     Assumptions,
 };
 use swc_ecma_transforms_compat::es2015::regenerator;
-use swc_ecma_transforms_optimization::{inline_globals2, GlobalExprMap};
+use swc_ecma_transforms_optimization::{
+    inline_globals2,
+    simplify::{dce::Config as DceConfig, Config as SimplifyConfig},
+    GlobalExprMap,
+};
 use swc_ecma_visit::{Fold, VisitMutWith};
 
 pub use crate::plugin::PluginConfig;
@@ -451,10 +455,32 @@ impl Options {
             }
         };
 
-        let enable_simplifier = optimizer
-            .as_ref()
-            .map(|v| v.simplify.into_bool())
-            .unwrap_or_default();
+        let simplifier_pass = {
+            if let Some(ref opts) = optimizer.as_ref().and_then(|o| o.simplify) {
+                match opts {
+                    SimplifyOption::Bool(allow_simplify) => {
+                        if *allow_simplify {
+                            Either::Left(simplifier(top_level_mark, Default::default()))
+                        } else {
+                            Either::Right(noop())
+                        }
+                    }
+                    SimplifyOption::Json(cfg) => Either::Left(simplifier(
+                        top_level_mark,
+                        SimplifyConfig {
+                            dce: DceConfig {
+                                preserve_imports_with_side_effects: cfg
+                                    .preserve_imports_with_side_effects,
+                                ..Default::default()
+                            },
+                            ..Default::default()
+                        },
+                    )),
+                }
+            } else {
+                Either::Right(noop())
+            }
+        };
 
         let optimization = {
             if let Some(opts) = optimizer.and_then(|o| o.globals) {
@@ -471,10 +497,7 @@ impl Options {
             const_modules,
             optimization,
             Optional::new(export_default_from(), syntax.export_default_from()),
-            Optional::new(
-                simplifier(top_level_mark, Default::default()),
-                enable_simplifier
-            ),
+            simplifier_pass,
             json_parse_pass
         );
 
@@ -1478,12 +1501,36 @@ pub struct OptimizerConfig {
     pub globals: Option<GlobalPassOption>,
 
     #[serde(default)]
-    pub simplify: BoolConfig<true>,
+    pub simplify: Option<SimplifyOption>,
 
     #[serde(default)]
     pub jsonify: Option<JsonifyOption>,
 }
 
+#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum SimplifyOption {
+    Bool(bool),
+    Json(SimplifyJsonOption),
+}
+
+impl Default for SimplifyOption {
+    fn default() -> Self {
+        SimplifyOption::Bool(true)
+    }
+}
+
+#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
+#[serde(deny_unknown_fields, rename_all = "camelCase")]
+pub struct SimplifyJsonOption {
+    #[serde(default = "default_preserve_imports_with_side_effects")]
+    pub preserve_imports_with_side_effects: bool,
+}
+
+fn default_preserve_imports_with_side_effects() -> bool {
+    true
+}
+
 #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
 #[serde(deny_unknown_fields, rename_all = "camelCase")]
 pub struct JsonifyOption {
diff --git a/crates/swc/tests/fixture/issues-6xxx/6771/output/index.js b/crates/swc/tests/fixture/issues-6xxx/6771/output/index.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/crates/swc_bundler/src/bundler/optimize.rs b/crates/swc_bundler/src/bundler/optimize.rs
index ea58fc3bf8..fd4998af6c 100644
--- a/crates/swc_bundler/src/bundler/optimize.rs
+++ b/crates/swc_bundler/src/bundler/optimize.rs
@@ -26,6 +26,7 @@ where
                         module_mark: None,
                         top_level: true,
                         top_retain: Default::default(),
+                        preserve_imports_with_side_effects: true,
                     },
                     self.unresolved_mark,
                 )));
diff --git a/crates/swc_ecma_minifier/src/lib.rs b/crates/swc_ecma_minifier/src/lib.rs
index 6d26318c79..7dff7819c3 100644
--- a/crates/swc_ecma_minifier/src/lib.rs
+++ b/crates/swc_ecma_minifier/src/lib.rs
@@ -304,6 +304,7 @@ fn perform_dce(m: &mut Program, options: &CompressOptions, extra: &ExtraOptions)
             module_mark: None,
             top_level: options.top_level(),
             top_retain: options.top_retain.clone(),
+            preserve_imports_with_side_effects: true,
         },
         extra.unresolved_mark,
     );
diff --git a/crates/swc_ecma_transforms_optimization/src/simplify/dce/mod.rs b/crates/swc_ecma_transforms_optimization/src/simplify/dce/mod.rs
index d495a27478..d74955f026 100644
--- a/crates/swc_ecma_transforms_optimization/src/simplify/dce/mod.rs
+++ b/crates/swc_ecma_transforms_optimization/src/simplify/dce/mod.rs
@@ -63,6 +63,9 @@ pub struct Config {
 
     /// Declarations with a symbol in this set will be preserved.
     pub top_retain: Vec<JsWord>,
+
+    /// If false, imports with side effects will be removed.
+    pub preserve_imports_with_side_effects: bool,
 }
 
 impl Default for Config {
@@ -71,6 +74,7 @@ impl Default for Config {
             module_mark: Default::default(),
             top_level: true,
             top_retain: Default::default(),
+            preserve_imports_with_side_effects: true,
         }
     }
 }
@@ -911,7 +915,25 @@ impl VisitMut for TreeShaker {
     }
 
     fn visit_mut_module_item(&mut self, n: &mut ModuleItem) {
-        n.visit_mut_children_with(self);
+        match n {
+            ModuleItem::ModuleDecl(ModuleDecl::Import(i)) => {
+                let is_for_side_effect = i.specifiers.is_empty();
+
+                i.visit_mut_with(self);
+
+                if !self.config.preserve_imports_with_side_effects
+                    && !is_for_side_effect
+                    && i.specifiers.is_empty()
+                {
+                    debug!("Dropping an import because it's not used");
+                    self.changed = true;
+                    *n = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
+                }
+            }
+            _ => {
+                n.visit_mut_children_with(self);
+            }
+        }
         debug_assert_valid(n);
     }
 


Test patch:
diff --git a/crates/swc/tests/fixture/issues-6xxx/6771/input/.swcrc b/crates/swc/tests/fixture/issues-6xxx/6771/input/.swcrc
new file mode 100644
index 0000000000..505327adc4
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6771/input/.swcrc
@@ -0,0 +1,16 @@
+{
+    "jsc": {
+        "transform": {
+            "optimizer": {
+                "simplify": {
+                    "preserveImportsWithSideEffects": false
+                },
+                "globals": {
+                    "envs": {
+                        "NODE_ENV_ALT": "true"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/crates/swc/tests/fixture/issues-6xxx/6771/input/index.js b/crates/swc/tests/fixture/issues-6xxx/6771/input/index.js
new file mode 100644
index 0000000000..78d43db980
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6771/input/index.js
@@ -0,0 +1,1 @@
+import path from 'path';
diff --git a/crates/swc_ecma_transforms_optimization/tests/remove_imports_with_side_effects.rs b/crates/swc_ecma_transforms_optimization/tests/remove_imports_with_side_effects.rs
new file mode 100644
index 0000000000..f40b6430be
--- /dev/null
+++ b/crates/swc_ecma_transforms_optimization/tests/remove_imports_with_side_effects.rs
@@ -0,0 +1,76 @@
+use swc_common::{chain, pass::Repeat, Mark};
+use swc_ecma_parser::{EsConfig, Syntax};
+use swc_ecma_transforms_base::resolver;
+use swc_ecma_transforms_optimization::simplify::dce::{dce, Config};
+use swc_ecma_transforms_testing::test;
+use swc_ecma_visit::Fold;
+
+fn tr() -> impl Fold {
+    Repeat::new(dce(
+        Config {
+            top_level: true,
+            preserve_imports_with_side_effects: false,
+            ..Default::default()
+        },
+        Mark::new(),
+    ))
+}
+
+macro_rules! to {
+    ($name:ident, $src:expr, $expected:expr) => {
+        test!(
+            Syntax::Es(EsConfig {
+                decorators: true,
+                ..Default::default()
+            }),
+            |_| chain!(resolver(Mark::new(), Mark::new(), false), tr()),
+            $name,
+            $src,
+            $expected
+        );
+    };
+}
+
+macro_rules! optimized_out {
+    ($name:ident, $src:expr) => {
+        to!($name, $src, "");
+    };
+}
+
+macro_rules! noop {
+    ($name:ident, $src:expr) => {
+        to!($name, $src, $src);
+    };
+}
+
+to!(
+    single_pass,
+    "
+    const a = 1;
+
+    if (a) {
+        const b = 2;
+    }
+    ",
+    "
+    const a = 1;
+    if (a) {}
+    "
+);
+
+optimized_out!(import_default_unused, "import foo from 'foo'");
+
+optimized_out!(import_specific_unused, "import {foo} from 'foo'");
+
+optimized_out!(import_mixed_unused, "import foo, { bar } from 'foo'");
+
+noop!(
+    import_export_named,
+    "import foo from 'src'; export { foo };"
+);
+
+to!(
+    import_unused_export_named,
+    "import foo, { bar } from 'src'; export { foo }; ",
+    "import foo from 'src'; export { foo }; "
+);