next_custom_transforms/
chain_transforms.rs

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    /// Accept any value
84    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                        // Always enable the Server Components mode for both
258                        // server and client layers.
259                        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/// Defaults to false
389
390#[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}