Repository: rolldown/rolldown
Pull request #3790: feat: treeshake.unknownGlobalSideEffects
Author: login: IWANABETHATGUY
Created: 2025-03-08T14:18:06Z | Merged: 2025-03-09T03:04:16Z

<!-- Thank you for contributing! -->

### Description
1. reference https://rollupjs.org/configuration-options/#treeshake-unknownglobalsideeffects
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->



Diff:
diff --git a/crates/rolldown/src/ast_scanner/side_effect_detector/mod.rs b/crates/rolldown/src/ast_scanner/side_effect_detector/mod.rs
index b09c306f3c48..803456a64272 100644
--- a/crates/rolldown/src/ast_scanner/side_effect_detector/mod.rs
+++ b/crates/rolldown/src/ast_scanner/side_effect_detector/mod.rs
@@ -521,7 +521,9 @@ impl<'a> SideEffectDetector<'a> {
 
   #[inline]
   fn detect_side_effect_of_identifier(&self, ident_ref: &IdentifierReference) -> bool {
-    self.is_unresolved_reference(ident_ref) && !is_global_ident_ref(&ident_ref.name)
+    self.is_unresolved_reference(ident_ref)
+      && self.options.treeshake.unknown_global_side_effects()
+      && !is_global_ident_ref(&ident_ref.name)
   }
 
   #[allow(clippy::too_many_lines)]
diff --git a/crates/rolldown/src/bundler.rs b/crates/rolldown/src/bundler.rs
index ac13946ac6a7..a6a8ca4dc97a 100644
--- a/crates/rolldown/src/bundler.rs
+++ b/crates/rolldown/src/bundler.rs
@@ -181,6 +181,7 @@ impl Bundler {
     Ok(output)
   }
 
+  #[inline]
   pub fn options(&self) -> &NormalizedBundlerOptions {
     &self.options
   }
diff --git a/crates/rolldown_binding/src/options/binding_input_options/binding_treeshake.rs b/crates/rolldown_binding/src/options/binding_input_options/binding_treeshake.rs
index 344f7d3189d6..2c5bed48f03d 100644
--- a/crates/rolldown_binding/src/options/binding_input_options/binding_treeshake.rs
+++ b/crates/rolldown_binding/src/options/binding_input_options/binding_treeshake.rs
@@ -27,6 +27,7 @@ pub struct BindingTreeshake {
   pub module_side_effects: BindingModuleSideEffects,
   pub annotations: Option<bool>,
   pub manual_pure_functions: Option<Vec<String>>,
+  pub unknown_global_side_effects: Option<bool>,
 }
 
 #[napi_derive::napi(object, object_to_js = false)]
@@ -73,6 +74,7 @@ impl TryFrom<BindingTreeshake> for rolldown::TreeshakeOptions {
       module_side_effects,
       annotations: value.annotations,
       manual_pure_functions: value.manual_pure_functions.map(FxHashSet::from_iter),
+      unknown_global_side_effects: value.unknown_global_side_effects,
     }))
   }
 
diff --git a/crates/rolldown_common/src/inner_bundler_options/mod.rs b/crates/rolldown_common/src/inner_bundler_options/mod.rs
index 5cf423decc16..1fc73cee1cb4 100644
--- a/crates/rolldown_common/src/inner_bundler_options/mod.rs
+++ b/crates/rolldown_common/src/inner_bundler_options/mod.rs
@@ -279,6 +279,7 @@ where
         module_side_effects: types::treeshake::ModuleSideEffects::Boolean(true),
         annotations: Some(true),
         manual_pure_functions: None,
+        unknown_global_side_effects: None,
       }))
     }
     Some(Value::Object(obj)) => {
@@ -296,6 +297,15 @@ where
           _ => Err(serde::de::Error::custom("annotations should be a `true` or `false`")),
         },
       )?;
+      let unknown_global_side_effects = obj.get("unknown_global_side_effects").map_or_else(
+        || Ok(Some(true)),
+        |v| match v {
+          Value::Bool(b) => Ok(Some(*b)),
+          _ => Err(serde::de::Error::custom(
+            "unknown_global_side_effects should be a `true` or `false`",
+          )),
+        },
+      )?;
       let manual_pure_functions = obj.get("manualPureFunctions").map_or_else(
         || Ok(FxHashSet::default()),
         |v| match v {
@@ -313,6 +323,7 @@ where
         module_side_effects,
         annotations,
         manual_pure_functions: Some(manual_pure_functions),
+        unknown_global_side_effects,
       }))
     }
     _ => Err(serde::de::Error::custom("treeshake should be a boolean or an object")),
diff --git a/crates/rolldown_common/src/inner_bundler_options/types/treeshake.rs b/crates/rolldown_common/src/inner_bundler_options/types/treeshake.rs
index cd1f5fb4d089..a682d0d6830d 100644
--- a/crates/rolldown_common/src/inner_bundler_options/types/treeshake.rs
+++ b/crates/rolldown_common/src/inner_bundler_options/types/treeshake.rs
@@ -26,6 +26,7 @@ impl Default for TreeshakeOptions {
       module_side_effects: ModuleSideEffects::Boolean(true),
       annotations: Some(true),
       manual_pure_functions: None,
+      unknown_global_side_effects: None,
     })
   }
 }
@@ -101,10 +102,18 @@ impl TreeshakeOptions {
   pub fn enabled(&self) -> bool {
     matches!(self, TreeshakeOptions::Option(_))
   }
+
   pub fn annotations(&self) -> bool {
     match self {
       TreeshakeOptions::Boolean(v) => *v,
-      TreeshakeOptions::Option(inner) => inner.annotations.unwrap_or_default(),
+      TreeshakeOptions::Option(inner) => inner.annotations.unwrap_or(true),
+    }
+  }
+
+  pub fn unknown_global_side_effects(&self) -> bool {
+    match self {
+      TreeshakeOptions::Boolean(_) => true,
+      TreeshakeOptions::Option(inner) => inner.unknown_global_side_effects.unwrap_or(true),
     }
   }
 
@@ -132,6 +141,7 @@ pub struct InnerOptions {
   pub module_side_effects: ModuleSideEffects,
   pub annotations: Option<bool>,
   pub manual_pure_functions: Option<FxHashSet<String>>,
+  pub unknown_global_side_effects: Option<bool>,
 }
 
 #[cfg(feature = "deserialize_bundler_options")]
diff --git a/crates/rolldown_testing/_config.schema.json b/crates/rolldown_testing/_config.schema.json
index 732771963f0a..6ce9f6681dda 100644
--- a/crates/rolldown_testing/_config.schema.json
+++ b/crates/rolldown_testing/_config.schema.json
@@ -721,6 +721,12 @@
             "boolean",
             "null"
           ]
+        },
+        "unknownGlobalSideEffects": {
+          "type": [
+            "boolean",
+            "null"
+          ]
         }
       },
       "additionalProperties": false
diff --git a/packages/rolldown/src/binding.d.ts b/packages/rolldown/src/binding.d.ts
index 1e714761d662..5dadac19b764 100644
--- a/packages/rolldown/src/binding.d.ts
+++ b/packages/rolldown/src/binding.d.ts
@@ -664,6 +664,7 @@ export interface BindingTreeshake {
   moduleSideEffects: boolean | BindingModuleSideEffectsRule[] | ((id: string, is_external: boolean) => boolean | undefined)
   annotations?: boolean
   manualPureFunctions?: Array<string>
+  unknownGlobalSideEffects?: boolean
 }
 
 export interface BindingViteResolvePluginConfig {
diff --git a/packages/rolldown/src/types/module-side-effects.ts b/packages/rolldown/src/types/module-side-effects.ts
index 29d00ba0e685..a435ab52c58e 100644
--- a/packages/rolldown/src/types/module-side-effects.ts
+++ b/packages/rolldown/src/types/module-side-effects.ts
@@ -15,5 +15,6 @@ export type TreeshakingOptions =
       moduleSideEffects?: ModuleSideEffectsOption
       annotations?: boolean
       manualPureFunctions?: string[]
+      unknownGlobalSideEffects?: boolean
     }
   | boolean
diff --git a/packages/rolldown/src/utils/bindingify-input-options.ts b/packages/rolldown/src/utils/bindingify-input-options.ts
index f3a40068f2fd..7432c1be1d93 100644
--- a/packages/rolldown/src/utils/bindingify-input-options.ts
+++ b/packages/rolldown/src/utils/bindingify-input-options.ts
@@ -279,16 +279,18 @@ function bindingifyTreeshakeOptions(
   if (config === false) {
     return undefined
   }
+
   if (config === true || config === undefined) {
     return {
       moduleSideEffects: true,
-      annotations: true,
     }
   }
+
   let normalizedConfig: BindingInputOptions['treeshake'] = {
     moduleSideEffects: true,
-    annotations: config.annotations ?? true,
+    annotations: config.annotations,
     manualPureFunctions: config.manualPureFunctions,
+    unknownGlobalSideEffects: config.unknownGlobalSideEffects,
   }
   if (config.moduleSideEffects === undefined) {
     normalizedConfig.moduleSideEffects = true
diff --git a/packages/rolldown/src/utils/validator.ts b/packages/rolldown/src/utils/validator.ts
index 7e67dce82fad..a4a9a1ee330d 100644
--- a/packages/rolldown/src/utils/validator.ts
+++ b/packages/rolldown/src/utils/validator.ts
@@ -217,6 +217,7 @@ const TreeshakingOptionsSchema = v.union([
   v.looseObject({
     annotations: v.optional(v.boolean()),
     manualPureFunctions: v.optional(v.array(v.string())),
+    unknownGlobalSideEffects: v.optional(v.boolean()),
   }),
 ])
 
diff --git a/packages/rolldown/tests/fixtures/tree-shake/manual-pure-functions/_config.ts b/packages/rolldown/tests/fixtures/tree-shake/manual-pure-functions/_config.ts
index 3c4569f17683..3955d641c424 100644
--- a/packages/rolldown/tests/fixtures/tree-shake/manual-pure-functions/_config.ts
+++ b/packages/rolldown/tests/fixtures/tree-shake/manual-pure-functions/_config.ts
@@ -1,4 +1,3 @@
-import type { OutputChunk as RolldownOutputChunk } from 'rolldown'
 import { defineTest } from 'rolldown-tests'
 import { expect } from 'vitest'
 
@@ -10,12 +9,9 @@ export default defineTest({
     external: ['styled-components'],
   },
   afterTest: (output) => {
-    output.output
-      .filter(({ type }) => type === 'chunk')
-      .forEach((chunk) => {
-        let code = (chunk as RolldownOutputChunk).code
+    let code = output.output[0].code
 
-        expect(code).toMatchInlineSnapshot(`
+    expect(code).toMatchInlineSnapshot(`
           "import "styled-components";
 
           //#region main.js
@@ -24,6 +20,5 @@ export default defineTest({
 
           //#endregion"
         `)
-      })
   },
 })
diff --git a/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects1/_config.ts b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects1/_config.ts
new file mode 100644
index 000000000000..8acd29599534
--- /dev/null
+++ b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects1/_config.ts
@@ -0,0 +1,17 @@
+import { defineTest } from 'rolldown-tests'
+import { expect } from 'vitest'
+
+export default defineTest({
+  afterTest: (output) => {
+    let code = output.output[0].code
+
+    expect(code).toMatchInlineSnapshot(`
+			"
+			//#region main.js
+			const jQuery = $;
+			const element = angular.element;
+
+			//#endregion"
+		`)
+  },
+})
diff --git a/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects1/main.js b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects1/main.js
new file mode 100644
index 000000000000..3caf57557cab
--- /dev/null
+++ b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects1/main.js
@@ -0,0 +1,4 @@
+const jQuery = $;
+const requestTimeout = setTimeout;
+const element = angular.element;
+
diff --git a/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects2/_config.ts b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects2/_config.ts
new file mode 100644
index 000000000000..e8fed71fd950
--- /dev/null
+++ b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects2/_config.ts
@@ -0,0 +1,21 @@
+import { defineTest } from 'rolldown-tests'
+import { expect } from 'vitest'
+
+export default defineTest({
+  config: {
+    treeshake: {
+      unknownGlobalSideEffects: false,
+    },
+  },
+  afterTest: (output) => {
+    let code = output.output[0].code
+
+    expect(code).toMatchInlineSnapshot(`
+			"
+			//#region main.js
+			const element = angular.element;
+
+			//#endregion"
+		`)
+  },
+})
diff --git a/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects2/main.js b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects2/main.js
new file mode 100644
index 000000000000..d672b4b73e6d
--- /dev/null
+++ b/packages/rolldown/tests/fixtures/tree-shake/unknown-global-side-effects2/main.js
@@ -0,0 +1,5 @@
+const jQuery = $;
+const requestTimeout = setTimeout;
+const element = angular.element;
+
+


Reviews:
user: login: underfin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T02:19:08Z
user: login: underfin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T02:19:26Z
user: login: IWANABETHATGUY
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T02:42:25Z

Review comments:
diff_hunk: @@ -279,16 +279,19 @@ function bindingifyTreeshakeOptions(
   if (config === false) {
     return undefined
   }
+
   if (config === true || config === undefined) {
     return {
       moduleSideEffects: true,
       annotations: true,
     }
   }
+
   let normalizedConfig: BindingInputOptions['treeshake'] = {
     moduleSideEffects: true,
     annotations: config.annotations ?? true,
     manualPureFunctions: config.manualPureFunctions,
+    unknownGlobalSideEffects: config.unknownGlobalSideEffects ?? true,
path: packages/rolldown/src/utils/bindingify-input-options.ts
user: login: underfin
body: The [rust side](https://github.com/rolldown/rolldown/commit/c764388bfb8751cf457dacf230d05ef87f4fd67a#diff-fef0515e8da9bf5d47508ef403741c46e31b0e52647ad29efd4c037df8e42ecbR301) already set default value, here `true` could be removed.
created_at: 2025-03-09T02:19:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,16 +279,19 @@ function bindingifyTreeshakeOptions(
   if (config === false) {
     return undefined
   }
+
   if (config === true || config === undefined) {
     return {
       moduleSideEffects: true,
       annotations: true,
     }
   }
+
   let normalizedConfig: BindingInputOptions['treeshake'] = {
     moduleSideEffects: true,
     annotations: config.annotations ?? true,
     manualPureFunctions: config.manualPureFunctions,
+    unknownGlobalSideEffects: config.unknownGlobalSideEffects ?? true,
path: packages/rolldown/src/utils/bindingify-input-options.ts
user: login: IWANABETHATGUY
body: nice catch!
created_at: 2025-03-09T02:42:25Z
author_association: MEMBER

PR comments:
user: login: IWANABETHATGUY
created_at: 2025-03-08T14:18:24Z
body: * **#3790** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/3790?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/3790?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#3789** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/3789?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: graphite-app[bot]
created_at: 2025-03-08T14:32:56Z
body: ### Merge activity

* **Mar 8, 9:32 AM EST**: [Graphite](https://app.graphite.dev/github/pr/rolldown/rolldown/3790) rebased this pull request after merging its parent, because this pull request is set to merge when ready.

author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-03-08T14:33:20Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *rolldown-rs* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 29a2b337906af8ba80c37f54265150f8eefd75c4 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/rolldown-rs/deploys/67cc54fa5712db0008c3e488 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-3790--rolldown-rs.netlify.app](https://deploy-preview-3790--rolldown-rs.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTM3OTAtLXJvbGxkb3duLXJzLm5ldGxpZnkuYXBwIn0.FJvH2xfC65Wfc8TjVgzAMIMwGnvfuSpwmReGLEQDNtk)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [rolldown-rs Preview](https://deploy-preview-3790--rolldown-rs.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/rolldown-rs/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-08T14:34:12Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *rolldown-rs* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 8137d70cb1e3386aa2414a9aea7c84fa3d24a14d |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/rolldown-rs/deploys/67cd00417abba400087708ba |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-3790--rolldown-rs.netlify.app](https://deploy-preview-3790--rolldown-rs.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTM3OTAtLXJvbGxkb3duLXJzLm5ldGxpZnkuYXBwIn0.FJvH2xfC65Wfc8TjVgzAMIMwGnvfuSpwmReGLEQDNtk)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [rolldown-rs Preview](https://deploy-preview-3790--rolldown-rs.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/rolldown-rs/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-08T14:47:59Z
body: ### Benchmarks Rust
- target: `main`(7b0c517d0e84e5954019698452b3d537c9a1e9f9)
- pr: `03-08-feat_treeshake.unknownglobalsideeffects`(8137d70cb1e3386aa2414a9aea7c84fa3d24a14d)
```
group                                                               pr                                     target
-----                                                               --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol                     1.06     77.4±5.20ms        ? ?/sec    1.00     72.9±2.28ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify              1.00     94.5±1.25ms        ? ?/sec    1.00     94.3±1.29ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify-sourcemap    1.01    107.7±1.62ms        ? ?/sec    1.00    107.2±0.99ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap           1.01     82.4±1.96ms        ? ?/sec    1.00     81.4±2.19ms        ? ?/sec
bundle/bundle@rome-ts                                               1.00    123.6±1.07ms        ? ?/sec    1.00    123.2±1.70ms        ? ?/sec
bundle/bundle@rome-ts-minify                                        1.01    206.0±2.27ms        ? ?/sec    1.00    204.6±1.92ms        ? ?/sec
bundle/bundle@rome-ts-minify-sourcemap                              1.01    239.7±1.99ms        ? ?/sec    1.00    237.9±1.69ms        ? ?/sec
bundle/bundle@rome-ts-sourcemap                                     1.00    135.5±1.15ms        ? ?/sec    1.01    136.7±1.55ms        ? ?/sec
bundle/bundle@threejs                                               1.00     43.6±3.90ms        ? ?/sec    1.00     43.8±3.82ms        ? ?/sec
bundle/bundle@threejs-minify                                        1.00     86.3±1.24ms        ? ?/sec    1.00     86.5±0.82ms        ? ?/sec
bundle/bundle@threejs-minify-sourcemap                              1.00    105.9±2.80ms        ? ?/sec    1.03    109.3±2.79ms        ? ?/sec
bundle/bundle@threejs-sourcemap                                     1.00     47.7±0.26ms        ? ?/sec    1.01     48.3±0.39ms        ? ?/sec
bundle/bundle@threejs10x                                            1.00    434.9±3.51ms        ? ?/sec    1.01    439.5±5.40ms        ? ?/sec
bundle/bundle@threejs10x-minify                                     1.00   1079.8±5.29ms        ? ?/sec    1.00   1084.1±5.55ms        ? ?/sec
bundle/bundle@threejs10x-minify-sourcemap                           1.00  1265.4±10.36ms        ? ?/sec    1.00   1262.1±7.83ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                                  1.00    507.4±6.80ms        ? ?/sec    1.00    509.7±6.74ms        ? ?/sec
remapping/remapping                                                 1.00     24.6±1.44ms        ? ?/sec    1.10     27.1±0.74ms        ? ?/sec
remapping/render-chunk-remapping                                    1.00     63.7±1.30ms        ? ?/sec    1.02     65.0±2.05ms        ? ?/sec
scan/scan@rome-ts                                                   1.00     95.5±0.74ms        ? ?/sec    1.03     98.4±1.70ms        ? ?/sec
scan/scan@threejs                                                   1.00     31.6±1.11ms        ? ?/sec    1.02     32.2±1.13ms        ? ?/sec
scan/scan@threejs10x                                                1.00    319.9±4.43ms        ? ?/sec    1.01    323.5±3.91ms        ? ?/sec
```
author_association: CONTRIBUTOR