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