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 disable_page_config: bool,
45
46 #[serde(default)]
47 pub pages_dir: Option<PathBuf>,
48
49 #[serde(default)]
50 pub app_dir: Option<PathBuf>,
51
52 #[serde(default)]
53 pub is_page_file: bool,
54
55 #[serde(default)]
56 pub is_development: bool,
57
58 #[serde(default)]
59 pub is_server_compiler: bool,
60
61 #[serde(default)]
62 pub prefer_esm: bool,
63
64 #[serde(default)]
65 pub server_components: Option<react_server_components::Config>,
66
67 #[serde(default)]
68 pub styled_jsx: BoolOr<styled_jsx::visitor::Config>,
69
70 #[serde(default)]
71 pub styled_components: Option<styled_components::Config>,
72
73 #[serde(default)]
74 pub remove_console: Option<remove_console::Config>,
75
76 #[serde(default)]
77 pub react_remove_properties: Option<react_remove_properties::Config>,
78
79 #[serde(default)]
80 #[cfg(not(target_arch = "wasm32"))]
81 pub relay: Option<swc_relay::Config>,
82
83 #[allow(unused)]
84 #[serde(default)]
85 #[cfg(target_arch = "wasm32")]
86 pub relay: Option<serde_json::Value>,
88
89 #[serde(default)]
90 pub shake_exports: Option<crate::transforms::shake_exports::Config>,
91
92 #[serde(default)]
93 pub emotion: Option<swc_emotion::EmotionOptions>,
94
95 #[serde(default)]
96 pub modularize_imports: Option<modularize_imports::Config>,
97
98 #[serde(default)]
99 pub auto_modularize_imports: Option<crate::transforms::named_import_transform::Config>,
100
101 #[serde(default)]
102 pub optimize_barrel_exports: Option<crate::transforms::optimize_barrel::Config>,
103
104 #[serde(default)]
105 pub font_loaders: Option<crate::transforms::fonts::Config>,
106
107 #[serde(default)]
108 pub server_actions: Option<crate::transforms::server_actions::Config>,
109
110 #[serde(default)]
111 pub cjs_require_optimizer: Option<crate::transforms::cjs_optimizer::Config>,
112
113 #[serde(default)]
114 pub optimize_server_react: Option<crate::transforms::optimize_server_react::Config>,
115
116 #[serde(default)]
117 pub debug_function_name: bool,
118
119 #[serde(default)]
120 pub lint_codemod_comments: bool,
121
122 #[serde(default)]
123 pub css_env: Option<swc_core::ecma::preset_env::Config>,
124
125 #[serde(default)]
126 pub track_dynamic_imports: bool,
127}
128
129pub fn custom_before_pass<'a, C>(
130 cm: Arc<SourceMap>,
131 file: Arc<SourceFile>,
132 opts: &'a TransformOptions,
133 comments: C,
134 eliminated_packages: Rc<RefCell<FxHashSet<Atom>>>,
135 unresolved_mark: Mark,
136 use_cache_telemetry_tracker: Rc<RefCell<FxHashMap<String, usize>>>,
137) -> impl Pass + 'a
138where
139 C: Clone + Comments + 'a,
140{
141 let file_path_str = file.name.to_string();
142
143 #[cfg(target_arch = "wasm32")]
144 let relay_plugin = noop_pass();
145
146 #[cfg(not(target_arch = "wasm32"))]
147 let relay_plugin = {
148 if let Some(config) = &opts.relay {
149 Either::Left(swc_relay::relay(
150 Arc::new(config.clone()),
151 (*file.name).clone(),
152 std::env::current_dir().unwrap(),
153 opts.pages_dir.clone(),
154 None,
155 ))
156 } else {
157 Either::Right(noop_pass())
158 }
159 };
160
161 let styled_jsx = {
162 let cm = cm.clone();
163 let file = file.clone();
164
165 fn_pass(move |program| {
166 if let Some(config) = opts.styled_jsx.to_option() {
167 let target_browsers = opts
168 .css_env
169 .as_ref()
170 .map(|env| {
171 targets_to_versions(env.targets.clone(), None)
172 .expect("failed to parse env.targets")
173 })
174 .unwrap_or_default();
175
176 program.mutate(styled_jsx::visitor::styled_jsx(
177 cm.clone(),
178 &file.name,
179 &styled_jsx::visitor::Config {
180 use_lightningcss: config.use_lightningcss,
181 browsers: *target_browsers,
182 },
183 &styled_jsx::visitor::NativeConfig { process_css: None },
184 ))
185 }
186 })
187 };
188
189 let styled_components = {
190 let file = file.clone();
191
192 fn_pass(move |program| {
193 if let Some(config) = &opts.styled_components {
194 program.mutate(styled_components::styled_components(
195 Some(&file_path_str),
196 file.src_hash,
197 config,
198 NoopComments,
199 ))
200 }
201 })
202 };
203
204 let emotion = {
205 let cm = cm.clone();
206 let file = file.clone();
207 let comments = comments.clone();
208
209 fn_pass(move |program| {
210 if let Some(config) = opts.emotion.as_ref() {
211 if !config.enabled.unwrap_or(false) {
212 return;
213 }
214 if let FileName::Real(path) = &*file.name {
215 program.mutate(swc_emotion::emotion(
216 config,
217 path,
218 file.src_hash as u32,
219 cm.clone(),
220 comments.clone(),
221 ));
222 }
223 }
224 })
225 };
226
227 let modularize_imports = fn_pass(move |program| {
228 if let Some(config) = opts.modularize_imports.as_ref() {
229 program.mutate(modularize_imports::modularize_imports(config));
230 }
231 });
232
233 (
234 (
235 crate::transforms::disallow_re_export_all_in_page::disallow_re_export_all_in_page(
236 opts.is_page_file,
237 ),
238 match &opts.server_components {
239 Some(config) if config.truthy() => {
240 Either::Left(react_server_components::server_components(
241 file.name.clone(),
242 config.clone(),
243 comments.clone(),
244 opts.app_dir.clone(),
245 ))
246 }
247 _ => Either::Right(noop_pass()),
248 },
249 styled_jsx,
250 styled_components,
251 Optional::new(
252 crate::transforms::next_ssg::next_ssg(eliminated_packages),
253 !opts.disable_next_ssg,
254 ),
255 crate::transforms::amp_attributes::amp_attributes(),
256 next_dynamic(
257 opts.is_development,
258 opts.is_server_compiler,
259 match &opts.server_components {
260 Some(config) if config.truthy() => match config {
261 react_server_components::Config::WithOptions(config) => {
264 config.is_react_server_layer
265 }
266 _ => false,
267 },
268 _ => false,
269 },
270 opts.prefer_esm,
271 NextDynamicMode::Webpack,
272 file.name.clone(),
273 opts.pages_dir.clone().or_else(|| opts.app_dir.clone()),
274 ),
275 Optional::new(
276 crate::transforms::page_config::page_config(opts.is_development, opts.is_page_file),
277 !opts.disable_page_config,
278 ),
279 relay_plugin,
280 match &opts.remove_console {
281 Some(config) if config.truthy() => Either::Left(remove_console::remove_console(
282 config.clone(),
283 SyntaxContext::empty().apply_mark(unresolved_mark),
284 )),
285 _ => Either::Right(noop_pass()),
286 },
287 match &opts.react_remove_properties {
288 Some(config) if config.truthy() => Either::Left(
289 react_remove_properties::react_remove_properties(config.clone()),
290 ),
291 _ => Either::Right(noop_pass()),
292 },
293 match &opts.shake_exports {
294 Some(config) => Either::Left(crate::transforms::shake_exports::shake_exports(
295 config.clone(),
296 )),
297 None => Either::Right(noop_pass()),
298 },
299 ),
300 (
301 match &opts.auto_modularize_imports {
302 Some(config) => Either::Left(
303 crate::transforms::named_import_transform::named_import_transform(
304 config.clone(),
305 ),
306 ),
307 None => Either::Right(noop_pass()),
308 },
309 match &opts.optimize_barrel_exports {
310 Some(config) => Either::Left(crate::transforms::optimize_barrel::optimize_barrel(
311 config.clone(),
312 )),
313 _ => Either::Right(noop_pass()),
314 },
315 match &opts.optimize_server_react {
316 Some(config) => Either::Left(
317 crate::transforms::optimize_server_react::optimize_server_react(config.clone()),
318 ),
319 _ => Either::Right(noop_pass()),
320 },
321 emotion,
322 modularize_imports,
323 match &opts.font_loaders {
324 Some(config) => Either::Left(next_font_loaders(config.clone())),
325 None => Either::Right(noop_pass()),
326 },
327 match &opts.server_actions {
328 Some(config) => Either::Left(crate::transforms::server_actions::server_actions(
329 &file.name,
330 None,
331 config.clone(),
332 comments.clone(),
333 cm.clone(),
334 use_cache_telemetry_tracker,
335 ServerActionsMode::Webpack,
336 )),
337 None => Either::Right(noop_pass()),
338 },
339 match &opts.track_dynamic_imports {
340 true => Either::Left(
341 crate::transforms::track_dynamic_imports::track_dynamic_imports(
342 unresolved_mark,
343 ),
344 ),
345 false => Either::Right(noop_pass()),
346 },
347 match &opts.cjs_require_optimizer {
348 Some(config) => Either::Left(visit_mut_pass(
349 crate::transforms::cjs_optimizer::cjs_optimizer(
350 config.clone(),
351 SyntaxContext::empty().apply_mark(unresolved_mark),
352 ),
353 )),
354 None => Either::Right(noop_pass()),
355 },
356 Optional::new(
357 crate::transforms::debug_fn_name::debug_fn_name(),
358 opts.debug_function_name,
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 Obj(T),
436 EmptyObject(EmptyStruct),
437 }
438
439 #[derive(Deserialize)]
440 #[serde(deny_unknown_fields)]
441 struct EmptyStruct {}
442
443 use serde::__private::de;
444
445 let content = de::Content::deserialize(deserializer)?;
446
447 let deserializer = de::ContentRefDeserializer::<D::Error>::new(&content);
448
449 let res = Deser::deserialize(deserializer);
450
451 match res {
452 Ok(v) => Ok(match v {
453 Deser::Bool(v) => BoolOr::Bool(v),
454 Deser::Obj(v) => BoolOr::Data(v),
455 Deser::EmptyObject(_) => BoolOr::Bool(true),
456 }),
457 Err(..) => {
458 let d = de::ContentDeserializer::<D::Error>::new(content);
459 Ok(BoolOr::Data(T::deserialize(d)?))
460 }
461 }
462 }
463}