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