1use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::Arc};
2
3use either::Either;
4use modularize_imports;
5use preset_env_base::query::targets_to_versions;
6use rustc_hash::{FxHashMap, FxHashSet};
7use serde::Deserialize;
8use swc_core::{
9 atoms::Atom,
10 common::{
11 comments::{Comments, NoopComments},
12 pass::Optional,
13 FileName, Mark, SourceFile, SourceMap, SyntaxContext,
14 },
15 ecma::{
16 ast::{fn_pass, noop_pass, EsVersion, Pass},
17 parser::parse_file_as_module,
18 visit::visit_mut_pass,
19 },
20};
21
22use crate::{
23 linter::linter,
24 transforms::{
25 cjs_finder::contains_cjs,
26 dynamic::{next_dynamic, NextDynamicMode},
27 fonts::next_font_loaders,
28 lint_codemod_comments::lint_codemod_comments,
29 react_server_components,
30 server_actions::ServerActionsMode,
31 },
32};
33
34#[derive(Clone, Debug, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct TransformOptions {
37 #[serde(flatten)]
38 pub swc: swc_core::base::config::Options,
39
40 #[serde(default)]
41 pub disable_next_ssg: bool,
42
43 #[serde(default)]
44 pub pages_dir: Option<PathBuf>,
45
46 #[serde(default)]
47 pub app_dir: Option<PathBuf>,
48
49 #[serde(default)]
50 pub is_page_file: bool,
51
52 #[serde(default)]
53 pub is_development: bool,
54
55 #[serde(default)]
56 pub is_server_compiler: bool,
57
58 #[serde(default)]
59 pub prefer_esm: bool,
60
61 #[serde(default)]
62 pub server_components: Option<react_server_components::Config>,
63
64 #[serde(default)]
65 pub styled_jsx: BoolOr<styled_jsx::visitor::Config>,
66
67 #[serde(default)]
68 pub styled_components: Option<styled_components::Config>,
69
70 #[serde(default)]
71 pub remove_console: Option<remove_console::Config>,
72
73 #[serde(default)]
74 pub react_remove_properties: Option<react_remove_properties::Config>,
75
76 #[serde(default)]
77 #[cfg(not(target_arch = "wasm32"))]
78 pub relay: Option<swc_relay::Config>,
79
80 #[allow(unused)]
81 #[serde(default)]
82 #[cfg(target_arch = "wasm32")]
83 pub relay: Option<serde_json::Value>,
85
86 #[serde(default)]
87 pub shake_exports: Option<crate::transforms::shake_exports::Config>,
88
89 #[serde(default)]
90 pub emotion: Option<swc_emotion::EmotionOptions>,
91
92 #[serde(default)]
93 pub modularize_imports: Option<modularize_imports::Config>,
94
95 #[serde(default)]
96 pub auto_modularize_imports: Option<crate::transforms::named_import_transform::Config>,
97
98 #[serde(default)]
99 pub optimize_barrel_exports: Option<crate::transforms::optimize_barrel::Config>,
100
101 #[serde(default)]
102 pub font_loaders: Option<crate::transforms::fonts::Config>,
103
104 #[serde(default)]
105 pub server_actions: Option<crate::transforms::server_actions::Config>,
106
107 #[serde(default)]
108 pub cjs_require_optimizer: Option<crate::transforms::cjs_optimizer::Config>,
109
110 #[serde(default)]
111 pub optimize_server_react: Option<crate::transforms::optimize_server_react::Config>,
112
113 #[serde(default)]
114 pub debug_function_name: bool,
115
116 #[serde(default)]
117 pub lint_codemod_comments: bool,
118
119 #[serde(default)]
120 pub css_env: Option<swc_core::ecma::preset_env::Config>,
121
122 #[serde(default)]
123 pub track_dynamic_imports: bool,
124}
125
126pub fn custom_before_pass<'a, C>(
127 cm: Arc<SourceMap>,
128 file: Arc<SourceFile>,
129 opts: &'a TransformOptions,
130 comments: C,
131 eliminated_packages: Rc<RefCell<FxHashSet<Atom>>>,
132 unresolved_mark: Mark,
133 use_cache_telemetry_tracker: Rc<RefCell<FxHashMap<String, usize>>>,
134) -> impl Pass + 'a
135where
136 C: Clone + Comments + 'a,
137{
138 let file_path_str = file.name.to_string();
139
140 #[cfg(target_arch = "wasm32")]
141 let relay_plugin = noop_pass();
142
143 #[cfg(not(target_arch = "wasm32"))]
144 let relay_plugin = {
145 if let Some(config) = &opts.relay {
146 Either::Left(swc_relay::relay(
147 Arc::new(config.clone()),
148 (*file.name).clone(),
149 std::env::current_dir().unwrap(),
150 opts.pages_dir.clone(),
151 None,
152 ))
153 } else {
154 Either::Right(noop_pass())
155 }
156 };
157
158 let styled_jsx = {
159 let cm = cm.clone();
160 let file = file.clone();
161
162 fn_pass(move |program| {
163 if let Some(config) = opts.styled_jsx.to_option() {
164 let target_browsers = opts
165 .css_env
166 .as_ref()
167 .map(|env| {
168 targets_to_versions(env.targets.clone(), None)
169 .expect("failed to parse env.targets")
170 })
171 .unwrap_or_default();
172
173 program.mutate(styled_jsx::visitor::styled_jsx(
174 cm.clone(),
175 &file.name,
176 &styled_jsx::visitor::Config {
177 use_lightningcss: config.use_lightningcss,
178 browsers: *target_browsers,
179 },
180 &styled_jsx::visitor::NativeConfig { process_css: None },
181 ))
182 }
183 })
184 };
185
186 let styled_components = {
187 let file = file.clone();
188
189 fn_pass(move |program| {
190 if let Some(config) = &opts.styled_components {
191 program.mutate(styled_components::styled_components(
192 Some(&file_path_str),
193 file.src_hash,
194 config,
195 NoopComments,
196 ))
197 }
198 })
199 };
200
201 let emotion = {
202 let cm = cm.clone();
203 let file = file.clone();
204 let comments = comments.clone();
205
206 fn_pass(move |program| {
207 if let Some(config) = opts.emotion.as_ref() {
208 if !config.enabled.unwrap_or(false) {
209 return;
210 }
211 if let FileName::Real(path) = &*file.name {
212 program.mutate(swc_emotion::emotion(
213 config,
214 path,
215 file.src_hash as u32,
216 cm.clone(),
217 comments.clone(),
218 ));
219 }
220 }
221 })
222 };
223
224 let modularize_imports = fn_pass(move |program| {
225 if let Some(config) = opts.modularize_imports.as_ref() {
226 program.mutate(modularize_imports::modularize_imports(config));
227 }
228 });
229
230 (
231 (
232 crate::transforms::disallow_re_export_all_in_page::disallow_re_export_all_in_page(
233 opts.is_page_file,
234 ),
235 match &opts.server_components {
236 Some(config) if config.truthy() => {
237 Either::Left(react_server_components::server_components(
238 file.name.clone(),
239 config.clone(),
240 comments.clone(),
241 opts.app_dir.clone(),
242 ))
243 }
244 _ => Either::Right(noop_pass()),
245 },
246 styled_jsx,
247 styled_components,
248 Optional::new(
249 crate::transforms::next_ssg::next_ssg(eliminated_packages),
250 !opts.disable_next_ssg,
251 ),
252 next_dynamic(
253 opts.is_development,
254 opts.is_server_compiler,
255 match &opts.server_components {
256 Some(config) if config.truthy() => match config {
257 react_server_components::Config::WithOptions(config) => {
260 config.is_react_server_layer
261 }
262 _ => false,
263 },
264 _ => false,
265 },
266 opts.prefer_esm,
267 NextDynamicMode::Webpack,
268 file.name.clone(),
269 opts.pages_dir.clone().or_else(|| opts.app_dir.clone()),
270 ),
271 relay_plugin,
272 match &opts.remove_console {
273 Some(config) if config.truthy() => Either::Left(remove_console::remove_console(
274 config.clone(),
275 SyntaxContext::empty().apply_mark(unresolved_mark),
276 )),
277 _ => Either::Right(noop_pass()),
278 },
279 match &opts.react_remove_properties {
280 Some(config) if config.truthy() => Either::Left(
281 react_remove_properties::react_remove_properties(config.clone()),
282 ),
283 _ => Either::Right(noop_pass()),
284 },
285 match &opts.shake_exports {
286 Some(config) => Either::Left(crate::transforms::shake_exports::shake_exports(
287 config.clone(),
288 )),
289 None => Either::Right(noop_pass()),
290 },
291 ),
292 (
293 match &opts.auto_modularize_imports {
294 Some(config) => Either::Left(
295 crate::transforms::named_import_transform::named_import_transform(
296 config.clone(),
297 ),
298 ),
299 None => Either::Right(noop_pass()),
300 },
301 match &opts.optimize_barrel_exports {
302 Some(config) => Either::Left(crate::transforms::optimize_barrel::optimize_barrel(
303 config.clone(),
304 )),
305 _ => Either::Right(noop_pass()),
306 },
307 match &opts.optimize_server_react {
308 Some(config) => Either::Left(
309 crate::transforms::optimize_server_react::optimize_server_react(config.clone()),
310 ),
311 _ => Either::Right(noop_pass()),
312 },
313 emotion,
314 modularize_imports,
315 match &opts.font_loaders {
316 Some(config) => Either::Left(next_font_loaders(config.clone())),
317 None => Either::Right(noop_pass()),
318 },
319 match &opts.server_actions {
320 Some(config) => Either::Left(crate::transforms::server_actions::server_actions(
321 &file.name,
322 None,
323 config.clone(),
324 comments.clone(),
325 cm.clone(),
326 use_cache_telemetry_tracker,
327 ServerActionsMode::Webpack,
328 )),
329 None => Either::Right(noop_pass()),
330 },
331 match &opts.track_dynamic_imports {
332 true => Either::Left(
333 crate::transforms::track_dynamic_imports::track_dynamic_imports(
334 unresolved_mark,
335 ),
336 ),
337 false => Either::Right(noop_pass()),
338 },
339 match &opts.cjs_require_optimizer {
340 Some(config) => Either::Left(visit_mut_pass(
341 crate::transforms::cjs_optimizer::cjs_optimizer(
342 config.clone(),
343 SyntaxContext::empty().apply_mark(unresolved_mark),
344 ),
345 )),
346 None => Either::Right(noop_pass()),
347 },
348 Optional::new(
349 crate::transforms::debug_fn_name::debug_fn_name(),
350 opts.debug_function_name,
351 ),
352 visit_mut_pass(crate::transforms::pure::pure_magic(comments.clone())),
353 Optional::new(
354 linter(lint_codemod_comments(comments)),
355 opts.lint_codemod_comments,
356 ),
357 ),
358 )
359}
360
361impl TransformOptions {
362 pub fn patch(mut self, fm: &SourceFile) -> Self {
363 self.swc.swcrc = false;
364
365 let should_enable_commonjs = self.swc.config.module.is_none()
366 && (fm.src.contains("module.exports")
367 || fm.src.contains("exports.")
368 || fm.src.contains("__esModule"))
369 && {
370 let syntax = self.swc.config.jsc.syntax.unwrap_or_default();
371 let target = self.swc.config.jsc.target.unwrap_or_else(EsVersion::latest);
372
373 parse_file_as_module(fm, syntax, target, None, &mut vec![])
374 .map(|m| contains_cjs(&m))
375 .unwrap_or_default()
376 };
377
378 if should_enable_commonjs {
379 self.swc.config.module = Some(
380 serde_json::from_str(r#"{ "type": "commonjs", "ignoreDynamic": true }"#).unwrap(),
381 );
382 }
383
384 self
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
391pub enum BoolOr<T> {
392 Bool(bool),
393 Data(T),
394}
395
396impl<T> Default for BoolOr<T> {
397 fn default() -> Self {
398 BoolOr::Bool(false)
399 }
400}
401
402impl<T> BoolOr<T> {
403 pub fn to_option(&self) -> Option<T>
404 where
405 T: Default + Clone,
406 {
407 match self {
408 BoolOr::Bool(false) => None,
409 BoolOr::Bool(true) => Some(Default::default()),
410 BoolOr::Data(v) => Some(v.clone()),
411 }
412 }
413}
414
415impl<'de, T> Deserialize<'de> for BoolOr<T>
416where
417 T: Deserialize<'de>,
418{
419 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
420 where
421 D: serde::Deserializer<'de>,
422 {
423 #[derive(Deserialize)]
424 #[serde(untagged)]
425 enum Deser<T> {
426 Bool(bool),
427 EmptyObject(EmptyStruct),
428 #[serde(untagged)]
429 Obj(T),
430 }
431
432 #[derive(Deserialize)]
433 #[serde(deny_unknown_fields)]
434 struct EmptyStruct {}
435
436 let res = Deser::deserialize(deserializer)?;
437 Ok(match res {
438 Deser::Bool(v) => BoolOr::Bool(v),
439 Deser::EmptyObject(_) => BoolOr::Bool(true),
440 Deser::Obj(v) => BoolOr::Data(v),
441 })
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use serde::Deserialize;
448 use serde_json::json;
449
450 use super::BoolOr;
451
452 #[test]
453 fn test_bool_or() {
454 let v: BoolOr<usize> = serde_json::from_value(json!(false)).unwrap();
455 assert_eq!(v, BoolOr::Bool(false));
456
457 let v: BoolOr<usize> = serde_json::from_value(json!(true)).unwrap();
458 assert_eq!(v, BoolOr::Bool(true));
459
460 let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
461 assert_eq!(v, BoolOr::Bool(true));
462
463 let v: Result<BoolOr<usize>, _> = serde_json::from_value(json!({"a": 1}));
464 assert!(v.is_err());
465
466 let v: BoolOr<usize> = serde_json::from_value(json!(1)).unwrap();
467 assert_eq!(v, BoolOr::Data(1));
468
469 let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
470 assert_eq!(v, BoolOr::Bool(true));
471
472 #[derive(Debug, Eq, PartialEq, Deserialize)]
473 struct SomeStruct {
474 field: Option<usize>,
475 }
476
477 let v: BoolOr<SomeStruct> = serde_json::from_value(json!({})).unwrap();
478 assert_eq!(v, BoolOr::Bool(true));
479
480 let v: BoolOr<SomeStruct> = serde_json::from_value(json!({"field": 32})).unwrap();
481 assert_eq!(v, BoolOr::Data(SomeStruct { field: Some(32) }));
482 }
483}