Skip to main content

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