Repository: swc-project/swc (rust)

Issue:
Could `minify()` support `module: "unknown"`? 
### Describe the feature

I'm working on migrating a large codebase from Terser to SWC for minification. We have a mix of scripts and modules in our output. Most of the time we can differentiate between them based on their file extension (`.js` vs `.mjs`) and use that to set `minify()`'s `module` option correctly. I'm finding that we have a handful of files with a `.js` extension that are actually modules. Terser seems to process the module files without failing even when `module` is set to `false`. SWC (understandably) fails. 

<img width="583" alt="image" src="https://github.com/swc-project/swc/assets/1189357/291c9264-5755-4450-bd83-dcdf9f4e0046">

Would it be possible to add an `"unknown"` option to `minify()` similar to `transform()`'s `isModule`? 

Related: 
* https://github.com/swc-project/swc/issues/2541
* https://github.com/swc-project/swc/issues/3493

### Babel plugin or link to the feature description

_No response_

### Additional context

_No response_

Pull request:
feat(es/minifier): Support `module: "unknown"`
**Description:**


**Related issue:**

 - Closes #8571

Patch:
diff --git a/crates/swc/src/lib.rs b/crates/swc/src/lib.rs
index 0ead40dfde..3a4b521755 100644
--- a/crates/swc/src/lib.rs
+++ b/crates/swc/src/lib.rs
@@ -804,20 +804,6 @@ impl Compiler {
 
             // https://github.com/swc-project/swc/issues/2254
 
-            if opts.module {
-                if let Some(opts) = &mut min_opts.compress {
-                    if opts.top_level.is_none() {
-                        opts.top_level = Some(TopLevelOptions { functions: true });
-                    }
-                }
-
-                if let Some(opts) = &mut min_opts.mangle {
-                    if opts.top_level.is_none() {
-                        opts.top_level = Some(true);
-                    }
-                }
-            }
-
             if opts.keep_fnames {
                 if let Some(opts) = &mut min_opts.compress {
                     opts.keep_fnames = true;
@@ -829,7 +815,7 @@ impl Compiler {
 
             let comments = SingleThreadedComments::default();
 
-            let module = self
+            let program = self
                 .parse_js(
                     fm.clone(),
                     handler,
@@ -841,17 +827,31 @@ impl Compiler {
                         import_attributes: true,
                         ..Default::default()
                     }),
-                    IsModule::Bool(opts.module),
+                    opts.module,
                     Some(&comments),
                 )
                 .context("failed to parse input file")?;
 
+            if program.is_module() {
+                if let Some(opts) = &mut min_opts.compress {
+                    if opts.top_level.is_none() {
+                        opts.top_level = Some(TopLevelOptions { functions: true });
+                    }
+                }
+
+                if let Some(opts) = &mut min_opts.mangle {
+                    if opts.top_level.is_none() {
+                        opts.top_level = Some(true);
+                    }
+                }
+            }
+
             let source_map_names = if source_map.enabled() {
                 let mut v = swc_compiler_base::IdentCollector {
                     names: Default::default(),
                 };
 
-                module.visit_with(&mut v);
+                program.visit_with(&mut v);
 
                 v.names
             } else {
@@ -864,7 +864,7 @@ impl Compiler {
             let is_mangler_enabled = min_opts.mangle.is_some();
 
             let module = self.run_transform(handler, false, || {
-                let module = module.fold_with(&mut paren_remover(Some(&comments)));
+                let module = program.fold_with(&mut paren_remover(Some(&comments)));
 
                 let module =
                     module.fold_with(&mut resolver(unresolved_mark, top_level_mark, false));
diff --git a/crates/swc_compiler_base/src/lib.rs b/crates/swc_compiler_base/src/lib.rs
index bd202ac7c8..7d43309dec 100644
--- a/crates/swc_compiler_base/src/lib.rs
+++ b/crates/swc_compiler_base/src/lib.rs
@@ -1,15 +1,12 @@
 use std::{
-    env, fmt,
+    env,
     path::{Path, PathBuf},
 };
 
 use anyhow::{Context, Error};
 use base64::prelude::{Engine, BASE64_STANDARD};
 use once_cell::sync::Lazy;
-use serde::{
-    de::{Unexpected, Visitor},
-    Deserialize, Deserializer, Serialize, Serializer,
-};
+use serde::{Deserialize, Serialize};
 use swc_atoms::JsWord;
 use swc_common::{
     collections::AHashMap,
@@ -19,7 +16,8 @@ use swc_common::{
     sync::Lrc,
     BytePos, FileName, SourceFile, SourceMap,
 };
-use swc_config::{config_types::BoolOr, merge::Merge};
+use swc_config::config_types::BoolOr;
+pub use swc_config::IsModule;
 use swc_ecma_ast::{EsVersion, Ident, Program};
 use swc_ecma_codegen::{text_writer::WriteJs, Emitter, Node};
 use swc_ecma_minifier::js::JsMinifyCommentOption;
@@ -384,74 +382,6 @@ impl Default for SourceMapsConfig {
     }
 }
 
-#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum IsModule {
-    Bool(bool),
-    Unknown,
-}
-
-impl Default for IsModule {
-    fn default() -> Self {
-        IsModule::Bool(true)
-    }
-}
-
-impl Merge for IsModule {
-    fn merge(&mut self, other: Self) {
-        if *self == Default::default() {
-            *self = other;
-        }
-    }
-}
-
-impl Serialize for IsModule {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        match *self {
-            IsModule::Bool(ref b) => b.serialize(serializer),
-            IsModule::Unknown => "unknown".serialize(serializer),
-        }
-    }
-}
-
-impl<'de> Deserialize<'de> for IsModule {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        struct IsModuleVisitor;
-
-        impl<'de> Visitor<'de> for IsModuleVisitor {
-            type Value = IsModule;
-
-            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-                formatter.write_str("a boolean or the string 'unknown'")
-            }
-
-            fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
-            where
-                E: serde::de::Error,
-            {
-                Ok(IsModule::Bool(b))
-            }
-
-            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
-            where
-                E: serde::de::Error,
-            {
-                match s {
-                    "unknown" => Ok(IsModule::Unknown),
-                    _ => Err(serde::de::Error::invalid_value(Unexpected::Str(s), &self)),
-                }
-            }
-        }
-
-        deserializer.deserialize_any(IsModuleVisitor)
-    }
-}
-
 pub struct IdentCollector {
     pub names: AHashMap<BytePos, JsWord>,
 }
diff --git a/crates/swc_config/src/lib.rs b/crates/swc_config/src/lib.rs
index bc7aa5b24c..0ca3d8cf6d 100644
--- a/crates/swc_config/src/lib.rs
+++ b/crates/swc_config/src/lib.rs
@@ -2,10 +2,12 @@
 
 pub mod config_types;
 pub mod merge;
+mod module;
 #[cfg(feature = "sourcemap")]
 mod source_map;
 
 pub use swc_cached::{regex::CachedRegex, Error};
 
+pub use crate::module::IsModule;
 #[cfg(feature = "sourcemap")]
 pub use crate::source_map::*;
diff --git a/crates/swc_config/src/module.rs b/crates/swc_config/src/module.rs
new file mode 100644
index 0000000000..61c730df64
--- /dev/null
+++ b/crates/swc_config/src/module.rs
@@ -0,0 +1,76 @@
+use std::fmt;
+
+use serde::{
+    de::{Unexpected, Visitor},
+    Deserialize, Deserializer, Serialize, Serializer,
+};
+
+use crate::merge::Merge;
+
+#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum IsModule {
+    Bool(bool),
+    Unknown,
+}
+
+impl Default for IsModule {
+    fn default() -> Self {
+        IsModule::Bool(true)
+    }
+}
+
+impl Merge for IsModule {
+    fn merge(&mut self, other: Self) {
+        if *self == Default::default() {
+            *self = other;
+        }
+    }
+}
+
+impl Serialize for IsModule {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        match *self {
+            IsModule::Bool(ref b) => b.serialize(serializer),
+            IsModule::Unknown => "unknown".serialize(serializer),
+        }
+    }
+}
+
+struct IsModuleVisitor;
+
+impl<'de> Visitor<'de> for IsModuleVisitor {
+    type Value = IsModule;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str("a boolean or the string 'unknown'")
+    }
+
+    fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
+    where
+        E: serde::de::Error,
+    {
+        Ok(IsModule::Bool(b))
+    }
+
+    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+    where
+        E: serde::de::Error,
+    {
+        match s {
+            "unknown" => Ok(IsModule::Unknown),
+            _ => Err(serde::de::Error::invalid_value(Unexpected::Str(s), &self)),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for IsModule {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        deserializer.deserialize_any(IsModuleVisitor)
+    }
+}
diff --git a/crates/swc_ecma_minifier/src/js.rs b/crates/swc_ecma_minifier/src/js.rs
index 21afc8b7ef..10835b2a91 100644
--- a/crates/swc_ecma_minifier/src/js.rs
+++ b/crates/swc_ecma_minifier/src/js.rs
@@ -1,7 +1,7 @@
 //! NOT A PUBLIC API
 
 use serde::{Deserialize, Serialize};
-use swc_config::{config_types::BoolOrDataConfig, SourceMapContent};
+use swc_config::{config_types::BoolOrDataConfig, IsModule, SourceMapContent};
 
 use crate::option::{
     terser::{TerserCompressorOptions, TerserEcmaVersion},
@@ -33,8 +33,8 @@ pub struct JsMinifyOptions {
     #[serde(default, alias = "keep_fnames")]
     pub keep_fnames: bool,
 
-    #[serde(default)]
-    pub module: bool,
+    #[serde(default = "default_module")]
+    pub module: IsModule,
 
     #[serde(default)]
     pub safari10: bool,
@@ -210,3 +210,7 @@ pub enum JsMinifyCommentOption {
     #[serde(rename = "all")]
     PreserveAllComments,
 }
+
+fn default_module() -> IsModule {
+    IsModule::Bool(false)
+}
diff --git a/packages/types/index.ts b/packages/types/index.ts
index dbaebf256b..99be5f70af 100644
--- a/packages/types/index.ts
+++ b/packages/types/index.ts
@@ -27,7 +27,7 @@ export interface JsMinifyOptions {
 
     keep_fnames?: boolean;
 
-    module?: boolean;
+    module?: boolean | "unknown";
 
     safari10?: boolean;
 
diff --git a/packages/types/package.json b/packages/types/package.json
index e27814e2c3..088691c86c 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,7 +1,7 @@
 {
     "name": "@swc/types",
     "packageManager": "yarn@4.0.2",
-    "version": "0.1.7",
+    "version": "0.1.8",
     "description": "Typings for the swc project.",
     "sideEffects": false,
     "scripts": {


Test patch:
diff --git a/bindings/binding_core_wasm/__tests__/simple.js b/bindings/binding_core_wasm/__tests__/simple.js
index fc650c5c4a..805e79f7bd 100644
--- a/bindings/binding_core_wasm/__tests__/simple.js
+++ b/bindings/binding_core_wasm/__tests__/simple.js
@@ -190,7 +190,8 @@ describe("parse", () => {
 describe("minify", () => {
     it("should work", () => {
         const output = swc.minifySync(
-            "const somename = 1; console.log(somename);"
+            "const somename = 1; console.log(somename);",
+            { module: false }
         );
 
         expect(output).toMatchInlineSnapshot(`
@@ -202,7 +203,8 @@ describe("minify", () => {
 
     it("should work with async facade", async () => {
         const output = await swc.minify(
-            "const somename = 1; console.log(somename);"
+            "const somename = 1; console.log(somename);",
+            { module: false }
         );
 
         expect(output).toMatchInlineSnapshot(`
diff --git a/crates/swc/tests/projects.rs b/crates/swc/tests/projects.rs
index 2bc29f0d8d..a0c7388c49 100644
--- a/crates/swc/tests/projects.rs
+++ b/crates/swc/tests/projects.rs
@@ -1107,7 +1107,7 @@ fn issue_7513_2() {
                     fm,
                     handler,
                     &JsMinifyOptions {
-                        module: true,
+                        module: IsModule::Bool(true),
                         compress: BoolOrDataConfig::from_bool(true),
                         mangle: BoolOrDataConfig::from_obj(MangleOptions {
                             props: None,
diff --git a/packages/core/__tests__/minify/issue_8437_test.mjs b/packages/core/__tests__/minify/issue_8437_test.mjs
index ffa556c356..a14177a220 100644
--- a/packages/core/__tests__/minify/issue_8437_test.mjs
+++ b/packages/core/__tests__/minify/issue_8437_test.mjs
@@ -37,7 +37,8 @@ toFixed(1.2345, 2, Math.round, 1);
   async function minify() {
     const { code } = await swc.minify(origin, {
       compress: true,
-      mangle: false
+      mangle: false,
+      module: false
     });
     return code;
   }
diff --git a/packages/core/__tests__/minify_test.mjs b/packages/core/__tests__/minify_test.mjs
index 1250b45e2e..aff82ccf86 100644
--- a/packages/core/__tests__/minify_test.mjs
+++ b/packages/core/__tests__/minify_test.mjs
@@ -260,7 +260,9 @@ it("should accept non-strict code", async () => {
     a = 1;
     delete a;
     console.log(a);
-    `);
+    `, {
+        module: false
+    });
 
     expect(code).toMatchInlineSnapshot(`"a=1,delete a,console.log(a);"`);
 });