Repository: rolldown/rolldown
Pull request #4154: refactor(rolldown_plugin_transform): support custom `TransformOptions`
Author: login: shulaoda
Created: 2025-04-17T08:01:03Z | Merged: 2025-04-17T11:31:14Z

### Description

Related to #3968


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 2ac21bd1a44d..2a772ddb33f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2773,6 +2773,7 @@ dependencies = [
 name = "rolldown_plugin_transform"
 version = "0.1.0"
 dependencies = [
+ "itertools",
  "oxc",
  "rolldown_common",
  "rolldown_plugin",
diff --git a/crates/rolldown_plugin_transform/Cargo.toml b/crates/rolldown_plugin_transform/Cargo.toml
index d641c1cb9da3..6c3d93e75d38 100644
--- a/crates/rolldown_plugin_transform/Cargo.toml
+++ b/crates/rolldown_plugin_transform/Cargo.toml
@@ -15,6 +15,7 @@ doctest = false
 workspace = true
 
 [dependencies]
+itertools = { workspace = true }
 oxc = { workspace = true }
 rolldown_common = { workspace = true }
 rolldown_plugin = { workspace = true }
diff --git a/crates/rolldown_plugin_transform/src/lib.rs b/crates/rolldown_plugin_transform/src/lib.rs
index 6d2356a4b9b2..7726a439f96c 100644
--- a/crates/rolldown_plugin_transform/src/lib.rs
+++ b/crates/rolldown_plugin_transform/src/lib.rs
@@ -1,14 +1,15 @@
+mod types;
 mod utils;
 
 use std::borrow::Cow;
 use std::path::Path;
 
-use oxc::{span::SourceType, transformer::TransformOptions};
-
 use rolldown_common::ModuleType;
 use rolldown_plugin::{Plugin, SharedTransformPluginContext};
 use rolldown_utils::{clean_url::clean_url, pattern_filter::StringOrRegex};
 
+use types::transform_options::TransformOptions;
+
 #[derive(Debug, Default)]
 pub struct TransformPlugin {
   pub include: Vec<StringOrRegex>,
@@ -19,7 +20,6 @@ pub struct TransformPlugin {
   pub jsx_inject: Option<String>,
   pub environment_consumer: String,
 
-  pub source_type: Option<SourceType>,
   pub transform_options: TransformOptions,
 }
 
diff --git a/crates/rolldown_plugin_transform/src/types/jsx_options.rs b/crates/rolldown_plugin_transform/src/types/jsx_options.rs
new file mode 100644
index 000000000000..f057ed5e2bb9
--- /dev/null
+++ b/crates/rolldown_plugin_transform/src/types/jsx_options.rs
@@ -0,0 +1,96 @@
+use itertools::Either;
+
+#[derive(Debug, Clone)]
+pub struct JsxOptions {
+  /// Decides which runtime to use.
+  ///
+  /// - 'automatic' - auto-import the correct JSX factories
+  /// - 'classic' - no auto-import
+  ///
+  /// @default 'automatic'
+  pub runtime: Option<String>,
+
+  /// Emit development-specific information, such as `__source` and `__self`.
+  ///
+  /// @default false
+  ///
+  /// @see {@link https://babeljs.io/docs/babel-plugin-transform-react-jsx-development}
+  pub development: Option<bool>,
+
+  /// Toggles whether or not to throw an error if an XML namespaced tag name
+  /// is used.
+  ///
+  /// Though the JSX spec allows this, it is disabled by default since React's
+  /// JSX does not currently have support for it.
+  ///
+  /// @default true
+  pub throw_if_namespace: Option<bool>,
+
+  /// Enables `@babel/plugin-transform-react-pure-annotations`.
+  ///
+  /// It will mark top-level React method calls as pure for tree shaking.
+  ///
+  /// @see {@link https://babeljs.io/docs/en/babel-plugin-transform-react-pure-annotations}
+  ///
+  /// @default true
+  pub pure: Option<bool>,
+
+  /// Replaces the import source when importing functions.
+  ///
+  /// @default 'react'
+  pub import_source: Option<String>,
+
+  /// Replace the function used when compiling JSX expressions. It should be a
+  /// qualified name (e.g. `React.createElement`) or an identifier (e.g.
+  /// `createElement`).
+  ///
+  /// Only used for `classic` {@link runtime}.
+  ///
+  /// @default 'React.createElement'
+  pub pragma: Option<String>,
+
+  /// Replace the component used when compiling JSX fragments. It should be a
+  /// valid JSX tag name.
+  ///
+  /// Only used for `classic` {@link runtime}.
+  ///
+  /// @default 'React.Fragment'
+  pub pragma_frag: Option<String>,
+
+  /// When spreading props, use `Object.assign` directly instead of an extend helper.
+  ///
+  /// Only used for `classic` {@link runtime}.
+  ///
+  /// @default false
+  pub use_built_ins: Option<bool>,
+
+  /// When spreading props, use inline object with spread elements directly
+  /// instead of an extend helper or Object.assign.
+  ///
+  /// Only used for `classic` {@link runtime}.
+  ///
+  /// @default false
+  pub use_spread: Option<bool>,
+
+  /// Enable React Fast Refresh .
+  ///
+  /// Conforms to the implementation in {@link https://github.com/facebook/react/tree/v18.3.1/packages/react-refresh}
+  ///
+  /// @default false
+  pub refresh: Option<Either<bool, ReactRefreshOptions>>,
+}
+
+#[derive(Debug, Clone)]
+pub struct ReactRefreshOptions {
+  /// Specify the identifier of the refresh registration variable.
+  ///
+  /// @default `$RefreshReg$`.
+  pub refresh_reg: Option<String>,
+
+  /// Specify the identifier of the refresh signature variable.
+  ///
+  /// @default `$RefreshSig$`.
+  pub refresh_sig: Option<String>,
+
+  pub emit_full_signatures: Option<bool>,
+}
diff --git a/crates/rolldown_plugin_transform/src/types/mod.rs b/crates/rolldown_plugin_transform/src/types/mod.rs
new file mode 100644
index 000000000000..274dc6bbaa3f
--- /dev/null
+++ b/crates/rolldown_plugin_transform/src/types/mod.rs
@@ -0,0 +1,2 @@
+pub mod jsx_options;
+pub mod transform_options;
diff --git a/crates/rolldown_plugin_transform/src/types/transform_options.rs b/crates/rolldown_plugin_transform/src/types/transform_options.rs
new file mode 100644
index 000000000000..666299463bd1
--- /dev/null
+++ b/crates/rolldown_plugin_transform/src/types/transform_options.rs
@@ -0,0 +1,10 @@
+use itertools::Either;
+
+use super::jsx_options::JsxOptions;
+
+#[derive(Debug, Default, Clone)]
+pub struct TransformOptions {
+  pub lang: Option<String>,
+
+  pub jsx: Option<Either<String, JsxOptions>>,
+}
diff --git a/crates/rolldown_plugin_transform/src/utils.rs b/crates/rolldown_plugin_transform/src/utils.rs
index f4bf8ce3e3e7..cea8c1dcef21 100644
--- a/crates/rolldown_plugin_transform/src/utils.rs
+++ b/crates/rolldown_plugin_transform/src/utils.rs
@@ -1,5 +1,6 @@
 use std::borrow::Cow;
 
+use itertools::Either;
 use oxc::{span::SourceType, transformer::TransformOptions};
 use rolldown_common::ModuleType;
 use rolldown_utils::{clean_url::clean_url, pattern_filter::filter as pattern_filter};
@@ -52,41 +53,38 @@ impl TransformPlugin {
     cwd: &str,
     ext: Option<&str>,
   ) -> (SourceType, Cow<'_, TransformOptions>) {
-    let is_refresh_disabled = self.transform_options.jsx.refresh.is_some()
-      && (self.environment_consumer == "server"
-        || matches!(self.jsx_refresh_filter(id, cwd), JsxRefreshFilter::False));
+    let is_refresh_disabled = self.environment_consumer == "server"
+      || matches!(self.jsx_refresh_filter(id, cwd), JsxRefreshFilter::False);
 
     let is_js_lang = matches!(self.jsx_refresh_filter(id, cwd), JsxRefreshFilter::True)
       && ext.is_some_and(|ext| ["js", "jsx", "ts", "tsx", "mjs"].contains(&ext));
 
     let source_type = if is_js_lang {
       SourceType::mjs()
-    } else if let Some(source_type) = self.source_type {
-      source_type
     } else {
-      match ext {
+      match self.transform_options.lang.as_deref().xor(ext) {
         Some("js") => SourceType::mjs(),
         Some("jsx") => SourceType::jsx(),
         Some("ts") => SourceType::ts(),
         Some("tsx") => SourceType::tsx(),
         None | Some(_) => {
-          panic!("Failed to detect the lang of {id}. Please specify `transformOptions.lang`")
+          if let Some(lang) = &self.transform_options.lang {
+            panic!("Invalid value for `transformOptions.lang`: `{lang}`.")
+          } else {
+            panic!("Failed to detect the lang of {id}. Please specify `transformOptions.lang`.")
+          }
         }
       }
     };
 
-    let transform_options = if is_refresh_disabled {
-      let mut transform_options = self.transform_options.clone();
+    let mut transform_options = self.transform_options.clone();
 
-      if is_refresh_disabled {
-        transform_options.jsx.refresh = None;
+    if let Some(Either::Right(jsx)) = &mut transform_options.jsx {
+      if jsx.refresh.is_some() && is_refresh_disabled {
+        jsx.refresh = None;
       }
+    }
 
-      Cow::Owned(transform_options)
-    } else {
-      Cow::Borrowed(&self.transform_options)
-    };
-
-    (source_type, transform_options)
+    (source_type, Cow::Owned(TransformOptions::default()))
   }
 }


Reviews:
user: login: IWANABETHATGUY
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:08:08Z
user: login: shulaoda
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:09:33Z
user: login: IWANABETHATGUY
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T08:19:37Z
user: login: IWANABETHATGUY
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:20:44Z
user: login: shulaoda
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:24:00Z
user: login: IWANABETHATGUY
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T08:31:57Z

Review comments:
diff_hunk: @@ -0,0 +1,96 @@
+use super::either::Either;
+
+#[derive(Debug, Clone)]
+pub struct JsxOptions {
path: crates/rolldown_plugin_transform/src/types/jsx_options.rs
user: login: IWANABETHATGUY
body: Why not just expose the type from oxc? 
created_at: 2025-04-17T08:08:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,96 @@
+use super::either::Either;
+
+#[derive(Debug, Clone)]
+pub struct JsxOptions {
path: crates/rolldown_plugin_transform/src/types/jsx_options.rs
user: login: shulaoda
body: This ports the NAPI-side `JsxOptions`.
created_at: 2025-04-17T08:09:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+#[derive(Debug, Clone)]
path: crates/rolldown_plugin_transform/src/types/either.rs
user: login: IWANABETHATGUY
body: There already exists one `Either` in https://docs.rs/itertools/0.14.0/itertools/enum.Either.html
created_at: 2025-04-17T08:20:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+#[derive(Debug, Clone)]
path: crates/rolldown_plugin_transform/src/types/either.rs
user: login: shulaoda
body: Got it. That explains why it seemed familiar but wasn't available – I hadn't added it as a dependency.

created_at: 2025-04-17T08:23:59Z
author_association: MEMBER

PR comments:
user: login: shulaoda
created_at: 2025-04-17T08:01:20Z
body: * **#4161** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4161?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#4159** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4159?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#4154** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4154?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/4154?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#4152** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4152?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: github-actions[bot]
created_at: 2025-04-17T08:13:51Z
body: ### Benchmarks Rust
- target: `main`(5a5d56b5d09b51e5ef38cbcecf36c482f42b7f69)
- pr: `04-17-refactor_rolldown_plugin_transform_support_custom_transformoptions`(2f3d9edea2c43d1dc6c015cf77bec50f6c053902)
```
group                                                               pr                                     target
-----                                                               --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol                     1.06     81.0±1.43ms        ? ?/sec    1.00     76.7±2.26ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify              1.03    100.8±2.39ms        ? ?/sec    1.00     97.7±2.42ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify-sourcemap    1.00    115.0±3.22ms        ? ?/sec    1.01   116.0±12.25ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap           1.04     90.3±1.94ms        ? ?/sec    1.00     86.6±3.42ms        ? ?/sec
bundle/bundle@rome-ts                                               1.03    130.8±2.81ms        ? ?/sec    1.00    126.6±4.13ms        ? ?/sec
bundle/bundle@rome-ts-minify                                        1.09    229.2±5.74ms        ? ?/sec    1.00    210.8±8.73ms        ? ?/sec
bundle/bundle@rome-ts-minify-sourcemap                              1.02    266.2±3.75ms        ? ?/sec    1.00    259.9±7.02ms        ? ?/sec
bundle/bundle@rome-ts-sourcemap                                     1.00    141.4±1.20ms        ? ?/sec    1.01    142.4±2.78ms        ? ?/sec
bundle/bundle@threejs                                               1.07     45.6±0.69ms        ? ?/sec    1.00     42.4±0.74ms        ? ?/sec
bundle/bundle@threejs-minify                                        1.08     93.9±3.30ms        ? ?/sec    1.00     87.2±1.10ms        ? ?/sec
bundle/bundle@threejs-minify-sourcemap                              1.05    111.0±3.38ms        ? ?/sec    1.00    105.8±2.07ms        ? ?/sec
bundle/bundle@threejs-sourcemap                                     1.04     51.3±0.56ms        ? ?/sec    1.00     49.4±0.99ms        ? ?/sec
bundle/bundle@threejs10x                                            1.02    444.0±6.59ms        ? ?/sec    1.00    435.1±4.55ms        ? ?/sec
bundle/bundle@threejs10x-minify                                     1.02  1101.6±10.79ms        ? ?/sec    1.00   1075.9±6.75ms        ? ?/sec
bundle/bundle@threejs10x-minify-sourcemap                           1.01  1281.9±10.21ms        ? ?/sec    1.00  1263.6±11.56ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                                  1.02    519.6±5.27ms        ? ?/sec    1.00    511.3±6.89ms        ? ?/sec
remapping/remapping                                                 1.11     27.5±0.38ms        ? ?/sec    1.00     24.9±0.26ms        ? ?/sec
remapping/render-chunk-remapping                                    1.00     68.0±5.05ms        ? ?/sec    1.01     68.6±5.91ms        ? ?/sec
scan/scan@rome-ts                                                   1.00     98.6±2.81ms        ? ?/sec    1.02    100.8±1.94ms        ? ?/sec
scan/scan@threejs                                                   1.00     32.1±0.50ms        ? ?/sec    1.01     32.3±1.03ms        ? ?/sec
scan/scan@threejs10x                                                1.00    323.1±4.80ms        ? ?/sec    1.01    325.5±7.53ms        ? ?/sec
```
author_association: CONTRIBUTOR
user: login: graphite-app[bot]
created_at: 2025-04-17T10:54:30Z
body: ### Merge activity

* **Apr 17, 6:54 AM EDT**: [Graphite](https://app.graphite.dev/github/pr/rolldown/rolldown/4154) 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-04-17T10:55:02Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *rolldown-rs* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 7d796a8fe9f990ec747568e3a71e40d20317bac8 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/rolldown-rs/deploys/6800ddc7b2d0fd0008c205f0 |
author_association: NONE
user: login: netlify[bot]
created_at: 2025-04-17T10:55:34Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *rolldown-rs* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 2f3d9edea2c43d1dc6c015cf77bec50f6c053902 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/rolldown-rs/deploys/6800dddaca2c8b00084155ad |
author_association: NONE