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                    unresolved_mark,
326                    cm.clone(),
327                    use_cache_telemetry_tracker,
328                    ServerActionsMode::Webpack,
329                )),
330                None => Either::Right(noop_pass()),
331            },
332            match &opts.track_dynamic_imports {
333                true => Either::Left(
334                    crate::transforms::track_dynamic_imports::track_dynamic_imports(
335                        unresolved_mark,
336                    ),
337                ),
338                false => Either::Right(noop_pass()),
339            },
340            match &opts.cjs_require_optimizer {
341                Some(config) => Either::Left(visit_mut_pass(
342                    crate::transforms::cjs_optimizer::cjs_optimizer(
343                        config.clone(),
344                        SyntaxContext::empty().apply_mark(unresolved_mark),
345                    ),
346                )),
347                None => Either::Right(noop_pass()),
348            },
349            Optional::new(
350                crate::transforms::debug_fn_name::debug_fn_name(),
351                opts.debug_function_name,
352            ),
353            visit_mut_pass(crate::transforms::pure::pure_magic(comments.clone())),
354            Optional::new(
355                linter(lint_codemod_comments(comments)),
356                opts.lint_codemod_comments,
357            ),
358        ),
359    )
360}
361
362impl TransformOptions {
363    pub fn patch(mut self, fm: &SourceFile) -> Self {
364        self.swc.swcrc = false;
365
366        let should_enable_commonjs = self.swc.config.module.is_none()
367            && (fm.src.contains("module.exports")
368                || fm.src.contains("exports.")
369                || fm.src.contains("__esModule"))
370            && {
371                let syntax = self.swc.config.jsc.syntax.unwrap_or_default();
372                let target = self.swc.config.jsc.target.unwrap_or_else(EsVersion::latest);
373
374                parse_file_as_module(fm, syntax, target, None, &mut vec![])
375                    .map(|m| contains_cjs(&m))
376                    .unwrap_or_default()
377            };
378
379        if should_enable_commonjs {
380            self.swc.config.module = Some(
381                serde_json::from_str(r#"{ "type": "commonjs", "ignoreDynamic": true }"#).unwrap(),
382            );
383        }
384
385        self
386    }
387}
388
389/// Defaults to false
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
392pub enum BoolOr<T> {
393    Bool(bool),
394    Data(T),
395}
396
397impl<T> Default for BoolOr<T> {
398    fn default() -> Self {
399        BoolOr::Bool(false)
400    }
401}
402
403impl<T> BoolOr<T> {
404    pub fn to_option(&self) -> Option<T>
405    where
406        T: Default + Clone,
407    {
408        match self {
409            BoolOr::Bool(false) => None,
410            BoolOr::Bool(true) => Some(Default::default()),
411            BoolOr::Data(v) => Some(v.clone()),
412        }
413    }
414}
415
416impl<'de, T> Deserialize<'de> for BoolOr<T>
417where
418    T: Deserialize<'de>,
419{
420    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
421    where
422        D: serde::Deserializer<'de>,
423    {
424        #[derive(Deserialize)]
425        #[serde(untagged)]
426        enum Deser<T> {
427            Bool(bool),
428            EmptyObject(EmptyStruct),
429            #[serde(untagged)]
430            Obj(T),
431        }
432
433        #[derive(Deserialize)]
434        #[serde(deny_unknown_fields)]
435        struct EmptyStruct {}
436
437        let res = Deser::deserialize(deserializer)?;
438        Ok(match res {
439            Deser::Bool(v) => BoolOr::Bool(v),
440            Deser::EmptyObject(_) => BoolOr::Bool(true),
441            Deser::Obj(v) => BoolOr::Data(v),
442        })
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use serde::Deserialize;
449    use serde_json::json;
450
451    use super::BoolOr;
452
453    #[test]
454    fn test_bool_or() {
455        let v: BoolOr<usize> = serde_json::from_value(json!(false)).unwrap();
456        assert_eq!(v, BoolOr::Bool(false));
457
458        let v: BoolOr<usize> = serde_json::from_value(json!(true)).unwrap();
459        assert_eq!(v, BoolOr::Bool(true));
460
461        let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
462        assert_eq!(v, BoolOr::Bool(true));
463
464        let v: Result<BoolOr<usize>, _> = serde_json::from_value(json!({"a": 1}));
465        assert!(v.is_err());
466
467        let v: BoolOr<usize> = serde_json::from_value(json!(1)).unwrap();
468        assert_eq!(v, BoolOr::Data(1));
469
470        let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
471        assert_eq!(v, BoolOr::Bool(true));
472
473        #[derive(Debug, Eq, PartialEq, Deserialize)]
474        struct SomeStruct {
475            field: Option<usize>,
476        }
477
478        let v: BoolOr<SomeStruct> = serde_json::from_value(json!({})).unwrap();
479        assert_eq!(v, BoolOr::Bool(true));
480
481        let v: BoolOr<SomeStruct> = serde_json::from_value(json!({"field": 32})).unwrap();
482        assert_eq!(v, BoolOr::Data(SomeStruct { field: Some(32) }));
483    }
484}