rolldown_plugin_transform/
utils.rs

1use std::path::{Path, PathBuf};
2
3use itertools::Either;
4use memchr::memmem;
5use oxc::{span::SourceType, transformer::TransformOptions};
6use rolldown_common::{JsxOptions, ModuleType};
7use rolldown_plugin::SharedTransformPluginContext;
8use rolldown_utils::{pattern_filter::filter as pattern_filter, url::clean_url};
9
10use super::TransformPlugin;
11
12pub enum JsxRefreshFilter {
13  None,
14  True,
15  False,
16}
17
18impl TransformPlugin {
19  pub fn filter(&self, id: &str, cwd: &str, module_type: &Option<ModuleType>) -> bool {
20    // rollup `createFilter` always skips when id includes null byte
21    // https://github.com/rollup/plugins/blob/ad58c8d87c5ab4864e25b5a777290fdf12a3879f/packages/pluginutils/src/createFilter.ts#L51
22    if memmem::find(id.as_bytes(), b"\0").is_some() {
23      return false;
24    }
25
26    if self.include.is_empty() && self.exclude.is_empty() {
27      return matches!(module_type, Some(ModuleType::Jsx | ModuleType::Tsx | ModuleType::Ts));
28    }
29
30    let exclude = (!self.exclude.is_empty()).then_some(self.exclude.as_slice());
31    let include = (!self.include.is_empty()).then_some(self.include.as_slice());
32
33    if pattern_filter(exclude, include, id, cwd).inner() {
34      return true;
35    }
36
37    let cleaned_id = clean_url(id);
38    if cleaned_id != id && pattern_filter(exclude, include, cleaned_id, cwd).inner() {
39      return true;
40    }
41
42    matches!(self.jsx_refresh_filter(id, cwd), JsxRefreshFilter::True)
43  }
44
45  pub fn jsx_refresh_filter(&self, id: &str, cwd: &str) -> JsxRefreshFilter {
46    if self.jsx_refresh_include.is_empty() && self.jsx_refresh_exclude.is_empty() {
47      return JsxRefreshFilter::None;
48    }
49
50    let jsx_refresh_exclude =
51      (!self.jsx_refresh_exclude.is_empty()).then_some(self.jsx_refresh_exclude.as_slice());
52    let jsx_refresh_include =
53      (!self.jsx_refresh_include.is_empty()).then_some(self.jsx_refresh_include.as_slice());
54
55    if pattern_filter(jsx_refresh_exclude, jsx_refresh_include, id, cwd).inner() {
56      return JsxRefreshFilter::True;
57    }
58
59    JsxRefreshFilter::False
60  }
61
62  pub fn get_modified_transform_options(
63    &self,
64    ctx: &SharedTransformPluginContext,
65    id: &str,
66    cwd: &str,
67    ext: Option<&str>,
68    code: &str,
69  ) -> anyhow::Result<(SourceType, TransformOptions)> {
70    let is_jsx_refresh_lang = matches!(self.jsx_refresh_filter(id, cwd), JsxRefreshFilter::True)
71      && ext.is_none_or(|ext| ["js", "jsx", "mjs", "ts", "tsx"].binary_search(&ext).is_err());
72
73    let source_type = if is_jsx_refresh_lang {
74      SourceType::mjs()
75    } else {
76      match ext {
77        Some("js" | "cjs" | "mjs") => SourceType::mjs(),
78        Some("jsx") => SourceType::jsx(),
79        Some("ts" | "cts" | "mts") => SourceType::ts(),
80        Some("tsx") => SourceType::tsx(),
81        None | Some(_) => Err(anyhow::anyhow!("Failed to detect the lang of {id}."))?,
82      }
83    };
84
85    let mut transform_options = self.transform_options.clone();
86
87    if let Some(Either::Right(jsx)) = &mut transform_options.jsx {
88      let is_refresh_disabled = self.is_server_consumer
89        || matches!(self.jsx_refresh_filter(id, cwd), JsxRefreshFilter::False)
90        || !(ext.is_some_and(|v| v.ends_with('x')) || {
91          let jsx_import_source = self
92            .transform_options
93            .jsx
94            .as_ref()
95            .and_then(|v| match v {
96              Either::Right(jsx) => jsx.import_source.as_deref(),
97              Either::Left(_) => None,
98            })
99            .unwrap_or("react");
100
101          let bytes = code.as_bytes();
102          let prefix = jsx_import_source.as_bytes();
103
104          let mut found = false;
105          for pos in memchr::memmem::find_iter(bytes, prefix) {
106            let rest = &bytes[pos + prefix.len()..];
107            if rest.starts_with(b"/jsx-runtime") || rest.starts_with(b"/jsx-dev-runtime") {
108              found = true;
109              break;
110            }
111          }
112          found
113        });
114
115      if is_refresh_disabled && jsx.refresh.is_some() {
116        jsx.refresh = None;
117      }
118    }
119
120    if source_type.is_typescript() {
121      let path = Path::new(cwd).join(id).parent().and_then(find_tsconfig_json_for_file);
122      let tsconfig = path.map(|path| ctx.resolver().resolve_tsconfig(&path)).transpose()?;
123
124      if let Some(tsconfig) = tsconfig {
125        // Tsconfig could be out of root, make sure it is watched
126        let tsconfig_path = tsconfig.path.to_string_lossy();
127        if !tsconfig_path.starts_with(cwd) {
128          ctx.add_watch_file(&tsconfig_path);
129        }
130
131        let compiler_options = &tsconfig.compiler_options;
132
133        // when both the normal options and tsconfig is set,
134        // we want to prioritize the normal options
135        if compiler_options.jsx.as_deref() == Some("preserve")
136          && transform_options
137            .jsx
138            .as_ref()
139            .is_none_or(|jsx| matches!(jsx, Either::Right(right) if right.runtime.is_none()))
140        {
141          transform_options.jsx = Some(Either::Left(String::from("preserve")));
142        }
143        if !matches!(&transform_options.jsx, Some(Either::Left(left)) if left == "preserve") {
144          let mut jsx = if let Some(Either::Right(jsx)) = transform_options.jsx {
145            jsx
146          } else {
147            JsxOptions::default()
148          };
149
150          if compiler_options.jsx_factory.is_some() && jsx.pragma.is_none() {
151            jsx.pragma.clone_from(&compiler_options.jsx_factory);
152          }
153          if compiler_options.jsx_import_source.is_some() && jsx.import_source.is_none() {
154            jsx.import_source.clone_from(&compiler_options.jsx_import_source);
155          }
156          if compiler_options.jsx_fragment_factory.is_some() && jsx.pragma_frag.is_none() {
157            jsx.pragma_frag.clone_from(&compiler_options.jsx_fragment_factory);
158          }
159
160          if jsx.runtime.is_none() {
161            match compiler_options.jsx.as_deref() {
162              Some("react") => {
163                jsx.runtime = Some(String::from("classic"));
164                // this option should not be set when using classic runtime
165                jsx.import_source = None;
166              }
167              Some("react-jsx") => {
168                jsx.runtime = Some(String::from("automatic"));
169                // these options should not be set when using automatic runtime
170                jsx.pragma = None;
171                jsx.pragma_frag = None;
172              }
173              Some("react-jsxdev") => jsx.development = Some(true),
174              _ => {}
175            }
176          }
177          transform_options.jsx = Some(Either::Right(jsx));
178        }
179
180        if transform_options.decorator.as_ref().is_none_or(|decorator| decorator.legacy.is_none()) {
181          let mut decorator = transform_options.decorator.unwrap_or_default();
182
183          if compiler_options.experimental_decorators.is_some() {
184            decorator.legacy = compiler_options.experimental_decorators;
185          }
186
187          if compiler_options.emit_decorator_metadata.is_some() {
188            decorator.emit_decorator_metadata = compiler_options.emit_decorator_metadata;
189          }
190
191          transform_options.decorator = Some(decorator);
192        }
193
194        // | preserveValueImports | importsNotUsedAsValues | verbatimModuleSyntax | onlyRemoveTypeImports |
195        // | -------------------- | ---------------------- | -------------------- |---------------------- |
196        // | false                | remove                 | false                | false                 |
197        // | false                | preserve, error        | -                    | -                     |
198        // | true                 | remove                 | -                    | -                     |
199        // | true                 | preserve, error        | true                 | true                  |
200        let mut typescript = transform_options.typescript.unwrap_or_default();
201        if typescript.only_remove_type_imports.is_none() {
202          if compiler_options.verbatim_module_syntax.is_some() {
203            typescript.only_remove_type_imports = compiler_options.verbatim_module_syntax;
204          } else if compiler_options.preserve_value_imports.is_some()
205            || compiler_options.imports_not_used_as_values.is_some()
206          {
207            let preserve_value_imports = compiler_options.preserve_value_imports.unwrap_or(false);
208            let imports_not_used_as_values =
209              compiler_options.imports_not_used_as_values.as_deref().unwrap_or("remove");
210            typescript.only_remove_type_imports = if !preserve_value_imports
211              && imports_not_used_as_values == "remove"
212            {
213              Some(true)
214            } else if preserve_value_imports
215              && (imports_not_used_as_values == "preserve" || imports_not_used_as_values == "error")
216            {
217              Some(false)
218            } else {
219              // warnings.push(
220              //   `preserveValueImports=${preserveValueImports} + importsNotUsedAsValues=${importsNotUsedAsValues} is not supported by oxc.` +
221              //     'Please migrate to the new verbatimModuleSyntax option.',
222              // )
223              Some(false)
224            };
225          }
226        }
227
228        let disable_use_define_for_class_fields = !compiler_options
229          .use_define_for_class_fields
230          .unwrap_or_else(|| is_use_define_for_class_fields(compiler_options.target.as_deref()));
231
232        let mut assumptions = transform_options.assumptions.unwrap_or_default();
233        assumptions.set_public_class_fields = Some(disable_use_define_for_class_fields);
234        typescript.remove_class_fields_without_initializer =
235          Some(disable_use_define_for_class_fields);
236
237        transform_options.typescript = Some(typescript);
238        transform_options.assumptions = Some(assumptions);
239      }
240    }
241
242    Ok((source_type, transform_options.try_into().map_err(|err: String| anyhow::anyhow!(err))?))
243  }
244}
245
246fn find_tsconfig_json_for_file(path: &Path) -> Option<PathBuf> {
247  // don't load tsconfig for paths in node_modules like esbuild
248  if rolldown_plugin_utils::is_in_node_modules(path) {
249    return None;
250  }
251
252  let mut dir = path.to_path_buf();
253
254  loop {
255    let tsconfig_json = dir.join("tsconfig.json");
256    if tsconfig_json.exists() {
257      return Some(tsconfig_json);
258    }
259
260    let Some(parent) = dir.parent() else { break };
261    dir = parent.to_path_buf();
262  }
263
264  None
265}
266
267fn is_use_define_for_class_fields(target: Option<&str>) -> bool {
268  let Some(target) = target else { return false };
269
270  if target.len() < 3 || !&target[..2].eq_ignore_ascii_case("es") {
271    return false;
272  }
273
274  let reset = &target[2..];
275  if reset.eq_ignore_ascii_case("next") {
276    return true;
277  }
278
279  reset.parse::<usize>().is_ok_and(|x| x > 2021)
280}