rolldown_plugin_transform/
utils.rs1use 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 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 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 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 jsx.import_source = None;
166 }
167 Some("react-jsx") => {
168 jsx.runtime = Some(String::from("automatic"));
169 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 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 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 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}