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 FileName, Mark, SourceFile, SourceMap, SyntaxContext,
12 comments::{Comments, NoopComments},
13 pass::Optional,
14 },
15 ecma::{
16 ast::{EsVersion, Pass, fn_pass, noop_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::{NextDynamicMode, next_dynamic},
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 let file_path_for_instant_stack = file_path_str.clone();
140 let file_path_for_empty_gsp = file_path_str.clone();
141
142 #[cfg(target_arch = "wasm32")]
143 let relay_plugin = noop_pass();
144
145 #[cfg(not(target_arch = "wasm32"))]
146 let relay_plugin = {
147 if let Some(config) = &opts.relay {
148 Either::Left(swc_relay::relay(
149 Arc::new(config.clone()),
150 (*file.name).clone(),
151 std::env::current_dir().unwrap(),
152 opts.pages_dir.clone(),
153 None,
154 ))
155 } else {
156 Either::Right(noop_pass())
157 }
158 };
159
160 let styled_jsx = {
161 let cm = cm.clone();
162 let file = file.clone();
163
164 fn_pass(move |program| {
165 if let Some(config) = opts.styled_jsx.to_option() {
166 let target_browsers = opts.css_env.as_ref().map(|env| {
167 targets_to_versions(env.targets.clone(), None)
168 .expect("failed to parse env.targets")
169 });
170
171 program.mutate(styled_jsx::visitor::styled_jsx(
172 cm.clone(),
173 &file.name,
174 &styled_jsx::visitor::Config {
175 use_lightningcss: config.use_lightningcss,
176 browsers: *target_browsers.map(|t| t.versions).unwrap_or_default(),
177 },
178 &styled_jsx::visitor::NativeConfig { process_css: None },
179 ))
180 }
181 })
182 };
183
184 let styled_components = {
185 let file = file.clone();
186
187 fn_pass(move |program| {
188 if let Some(config) = &opts.styled_components {
189 program.mutate(styled_components::styled_components(
190 Some(&file_path_str),
191 file.src_hash(),
192 config,
193 NoopComments,
194 ))
195 }
196 })
197 };
198
199 let emotion = {
200 let cm = cm.clone();
201 let file = file.clone();
202 let comments = comments.clone();
203
204 fn_pass(move |program| {
205 if let Some(config) = opts.emotion.as_ref() {
206 if !config.enabled.unwrap_or(false) {
207 return;
208 }
209 if let FileName::Real(path) = &*file.name {
210 program.mutate(swc_emotion::emotion(
211 config,
212 path,
213 file.src_hash() as u32,
214 cm.clone(),
215 comments.clone(),
216 ));
217 }
218 }
219 })
220 };
221
222 let modularize_imports = fn_pass(move |program| {
223 if let Some(config) = opts.modularize_imports.as_ref() {
224 program.mutate(modularize_imports::modularize_imports(config));
225 }
226 });
227
228 (
229 (
230 crate::transforms::disallow_re_export_all_in_page::disallow_re_export_all_in_page(
231 opts.is_page_file,
232 ),
233 match &opts.server_components {
234 Some(config) if config.truthy() => {
235 Either::Left(react_server_components::server_components(
236 file.name.clone(),
237 config.clone(),
238 comments.clone(),
239 opts.app_dir.clone(),
240 ))
241 }
242 _ => Either::Right(noop_pass()),
243 },
244 styled_jsx,
245 styled_components,
246 Optional::new(
247 crate::transforms::next_ssg::next_ssg(eliminated_packages),
248 !opts.disable_next_ssg,
249 ),
250 next_dynamic(
251 opts.is_development,
252 opts.is_server_compiler,
253 match &opts.server_components {
254 Some(config) if config.truthy() => match config {
255 react_server_components::Config::WithOptions(config) => {
258 config.is_react_server_layer
259 }
260 _ => false,
261 },
262 _ => false,
263 },
264 opts.prefer_esm,
265 NextDynamicMode::Webpack,
266 file.name.clone(),
267 opts.pages_dir.clone().or_else(|| opts.app_dir.clone()),
268 ),
269 relay_plugin,
270 match &opts.remove_console {
271 Some(config) if config.truthy() => Either::Left(remove_console::remove_console(
272 config.clone(),
273 SyntaxContext::empty().apply_mark(unresolved_mark),
274 )),
275 _ => Either::Right(noop_pass()),
276 },
277 match &opts.react_remove_properties {
278 Some(config) if config.truthy() => Either::Left(
279 react_remove_properties::react_remove_properties(config.clone()),
280 ),
281 _ => Either::Right(noop_pass()),
282 },
283 match &opts.shake_exports {
284 Some(config) => Either::Left(crate::transforms::shake_exports::shake_exports(
285 config.clone(),
286 )),
287 None => Either::Right(noop_pass()),
288 },
289 ),
290 (
291 match &opts.auto_modularize_imports {
292 Some(config) => Either::Left(
293 crate::transforms::named_import_transform::named_import_transform(
294 config.clone(),
295 ),
296 ),
297 None => Either::Right(noop_pass()),
298 },
299 match &opts.optimize_barrel_exports {
300 Some(config) => Either::Left(crate::transforms::optimize_barrel::optimize_barrel(
301 config.clone(),
302 )),
303 _ => Either::Right(noop_pass()),
304 },
305 match &opts.optimize_server_react {
306 Some(config) => Either::Left(
307 crate::transforms::optimize_server_react::optimize_server_react(config.clone()),
308 ),
309 _ => Either::Right(noop_pass()),
310 },
311 emotion,
312 modularize_imports,
313 match &opts.font_loaders {
314 Some(config) => Either::Left(next_font_loaders(config.clone())),
315 None => Either::Right(noop_pass()),
316 },
317 match &opts.server_actions {
318 Some(config) => Either::Left(crate::transforms::server_actions::server_actions(
319 &file.name,
320 None,
321 config.clone(),
322 comments.clone(),
323 unresolved_mark,
324 cm.clone(),
325 use_cache_telemetry_tracker,
326 ServerActionsMode::Webpack,
327 )),
328 None => Either::Right(noop_pass()),
329 },
330 match &opts.track_dynamic_imports {
331 true => Either::Left(
332 crate::transforms::track_dynamic_imports::track_dynamic_imports(
333 unresolved_mark,
334 comments.clone(),
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 (
353 crate::transforms::debug_instant_stack::DebugInstantStack::new(
354 match &opts.server_components {
355 Some(react_server_components::Config::WithOptions(options)) => {
356 options.page_extensions.clone()
357 }
358 _ => vec![],
359 },
360 )
361 .get_pass(file_path_for_instant_stack),
362 Optional::new(
363 crate::transforms::empty_gsp::EmptyGenerateStaticParams::new(
364 match &opts.server_components {
365 Some(react_server_components::Config::WithOptions(options)) => {
366 options.page_extensions.clone()
367 }
368 _ => vec![],
369 },
370 )
371 .get_pass(file_path_for_empty_gsp),
372 matches!(
373 &opts.server_components,
374 Some(react_server_components::Config::WithOptions(options))
375 if options.cache_components_enabled
376 ),
377 ),
378 ),
379 visit_mut_pass(crate::transforms::pure::pure_magic(comments.clone())),
380 Optional::new(
381 linter(lint_codemod_comments(comments)),
382 opts.lint_codemod_comments,
383 ),
384 ),
385 )
386}
387
388impl TransformOptions {
389 pub fn patch(mut self, fm: &SourceFile) -> Self {
390 self.swc.swcrc = false;
391
392 let should_enable_commonjs = self.swc.config.module.is_none()
393 && (fm.src.contains("module.exports")
394 || fm.src.contains("exports.")
395 || fm.src.contains("__esModule"))
396 && {
397 let syntax = self.swc.config.jsc.syntax.unwrap_or_default();
398 let target = self.swc.config.jsc.target.unwrap_or_else(EsVersion::latest);
399
400 parse_file_as_module(fm, syntax, target, None, &mut vec![])
401 .map(|m| contains_cjs(&m))
402 .unwrap_or_default()
403 };
404
405 if should_enable_commonjs {
406 self.swc.config.module = Some(
407 serde_json::from_str(r#"{ "type": "commonjs", "ignoreDynamic": true }"#).unwrap(),
408 );
409 }
410
411 self
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
418pub enum BoolOr<T> {
419 Bool(bool),
420 Data(T),
421}
422
423impl<T> Default for BoolOr<T> {
424 fn default() -> Self {
425 BoolOr::Bool(false)
426 }
427}
428
429impl<T> BoolOr<T> {
430 pub fn to_option(&self) -> Option<T>
431 where
432 T: Default + Clone,
433 {
434 match self {
435 BoolOr::Bool(false) => None,
436 BoolOr::Bool(true) => Some(Default::default()),
437 BoolOr::Data(v) => Some(v.clone()),
438 }
439 }
440}
441
442impl<'de, T> Deserialize<'de> for BoolOr<T>
443where
444 T: Deserialize<'de>,
445{
446 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
447 where
448 D: serde::Deserializer<'de>,
449 {
450 #[derive(Deserialize)]
451 #[serde(untagged)]
452 enum Deser<T> {
453 Bool(bool),
454 EmptyObject(EmptyStruct),
455 #[serde(untagged)]
456 Obj(T),
457 }
458
459 #[derive(Deserialize)]
460 #[serde(deny_unknown_fields)]
461 struct EmptyStruct {}
462
463 let res = Deser::deserialize(deserializer)?;
464 Ok(match res {
465 Deser::Bool(v) => BoolOr::Bool(v),
466 Deser::EmptyObject(_) => BoolOr::Bool(true),
467 Deser::Obj(v) => BoolOr::Data(v),
468 })
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use serde::Deserialize;
475 use serde_json::json;
476
477 use super::BoolOr;
478
479 #[test]
480 fn test_bool_or() {
481 let v: BoolOr<usize> = serde_json::from_value(json!(false)).unwrap();
482 assert_eq!(v, BoolOr::Bool(false));
483
484 let v: BoolOr<usize> = serde_json::from_value(json!(true)).unwrap();
485 assert_eq!(v, BoolOr::Bool(true));
486
487 let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
488 assert_eq!(v, BoolOr::Bool(true));
489
490 let v: Result<BoolOr<usize>, _> = serde_json::from_value(json!({"a": 1}));
491 assert!(v.is_err());
492
493 let v: BoolOr<usize> = serde_json::from_value(json!(1)).unwrap();
494 assert_eq!(v, BoolOr::Data(1));
495
496 let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
497 assert_eq!(v, BoolOr::Bool(true));
498
499 #[derive(Debug, Eq, PartialEq, Deserialize)]
500 struct SomeStruct {
501 field: Option<usize>,
502 }
503
504 let v: BoolOr<SomeStruct> = serde_json::from_value(json!({})).unwrap();
505 assert_eq!(v, BoolOr::Bool(true));
506
507 let v: BoolOr<SomeStruct> = serde_json::from_value(json!({"field": 32})).unwrap();
508 assert_eq!(v, BoolOr::Data(SomeStruct { field: Some(32) }));
509 }
510}