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
141    #[cfg(target_arch = "wasm32")]
142    let relay_plugin = noop_pass();
143
144    #[cfg(not(target_arch = "wasm32"))]
145    let relay_plugin = {
146        if let Some(config) = &opts.relay {
147            Either::Left(swc_relay::relay(
148                Arc::new(config.clone()),
149                (*file.name).clone(),
150                std::env::current_dir().unwrap(),
151                opts.pages_dir.clone(),
152                None,
153            ))
154        } else {
155            Either::Right(noop_pass())
156        }
157    };
158
159    let styled_jsx = {
160        let cm = cm.clone();
161        let file = file.clone();
162
163        fn_pass(move |program| {
164            if let Some(config) = opts.styled_jsx.to_option() {
165                let target_browsers = opts.css_env.as_ref().map(|env| {
166                    targets_to_versions(env.targets.clone(), None)
167                        .expect("failed to parse env.targets")
168                });
169
170                program.mutate(styled_jsx::visitor::styled_jsx(
171                    cm.clone(),
172                    &file.name,
173                    &styled_jsx::visitor::Config {
174                        use_lightningcss: config.use_lightningcss,
175                        browsers: *target_browsers.map(|t| t.versions).unwrap_or_default(),
176                    },
177                    &styled_jsx::visitor::NativeConfig { process_css: None },
178                ))
179            }
180        })
181    };
182
183    let styled_components = {
184        let file = file.clone();
185
186        fn_pass(move |program| {
187            if let Some(config) = &opts.styled_components {
188                program.mutate(styled_components::styled_components(
189                    Some(&file_path_str),
190                    file.src_hash,
191                    config,
192                    NoopComments,
193                ))
194            }
195        })
196    };
197
198    let emotion = {
199        let cm = cm.clone();
200        let file = file.clone();
201        let comments = comments.clone();
202
203        fn_pass(move |program| {
204            if let Some(config) = opts.emotion.as_ref() {
205                if !config.enabled.unwrap_or(false) {
206                    return;
207                }
208                if let FileName::Real(path) = &*file.name {
209                    program.mutate(swc_emotion::emotion(
210                        config,
211                        path,
212                        file.src_hash as u32,
213                        cm.clone(),
214                        comments.clone(),
215                    ));
216                }
217            }
218        })
219    };
220
221    let modularize_imports = fn_pass(move |program| {
222        if let Some(config) = opts.modularize_imports.as_ref() {
223            program.mutate(modularize_imports::modularize_imports(config));
224        }
225    });
226
227    (
228        (
229            crate::transforms::disallow_re_export_all_in_page::disallow_re_export_all_in_page(
230                opts.is_page_file,
231            ),
232            match &opts.server_components {
233                Some(config) if config.truthy() => {
234                    Either::Left(react_server_components::server_components(
235                        file.name.clone(),
236                        config.clone(),
237                        comments.clone(),
238                        opts.app_dir.clone(),
239                    ))
240                }
241                _ => Either::Right(noop_pass()),
242            },
243            styled_jsx,
244            styled_components,
245            Optional::new(
246                crate::transforms::next_ssg::next_ssg(eliminated_packages),
247                !opts.disable_next_ssg,
248            ),
249            next_dynamic(
250                opts.is_development,
251                opts.is_server_compiler,
252                match &opts.server_components {
253                    Some(config) if config.truthy() => match config {
254                        // Always enable the Server Components mode for both
255                        // server and client layers.
256                        react_server_components::Config::WithOptions(config) => {
257                            config.is_react_server_layer
258                        }
259                        _ => false,
260                    },
261                    _ => false,
262                },
263                opts.prefer_esm,
264                NextDynamicMode::Webpack,
265                file.name.clone(),
266                opts.pages_dir.clone().or_else(|| opts.app_dir.clone()),
267            ),
268            relay_plugin,
269            match &opts.remove_console {
270                Some(config) if config.truthy() => Either::Left(remove_console::remove_console(
271                    config.clone(),
272                    SyntaxContext::empty().apply_mark(unresolved_mark),
273                )),
274                _ => Either::Right(noop_pass()),
275            },
276            match &opts.react_remove_properties {
277                Some(config) if config.truthy() => Either::Left(
278                    react_remove_properties::react_remove_properties(config.clone()),
279                ),
280                _ => Either::Right(noop_pass()),
281            },
282            match &opts.shake_exports {
283                Some(config) => Either::Left(crate::transforms::shake_exports::shake_exports(
284                    config.clone(),
285                )),
286                None => Either::Right(noop_pass()),
287            },
288        ),
289        (
290            match &opts.auto_modularize_imports {
291                Some(config) => Either::Left(
292                    crate::transforms::named_import_transform::named_import_transform(
293                        config.clone(),
294                    ),
295                ),
296                None => Either::Right(noop_pass()),
297            },
298            match &opts.optimize_barrel_exports {
299                Some(config) => Either::Left(crate::transforms::optimize_barrel::optimize_barrel(
300                    config.clone(),
301                )),
302                _ => Either::Right(noop_pass()),
303            },
304            match &opts.optimize_server_react {
305                Some(config) => Either::Left(
306                    crate::transforms::optimize_server_react::optimize_server_react(config.clone()),
307                ),
308                _ => Either::Right(noop_pass()),
309            },
310            emotion,
311            modularize_imports,
312            match &opts.font_loaders {
313                Some(config) => Either::Left(next_font_loaders(config.clone())),
314                None => Either::Right(noop_pass()),
315            },
316            match &opts.server_actions {
317                Some(config) => Either::Left(crate::transforms::server_actions::server_actions(
318                    &file.name,
319                    None,
320                    config.clone(),
321                    comments.clone(),
322                    unresolved_mark,
323                    cm.clone(),
324                    use_cache_telemetry_tracker,
325                    ServerActionsMode::Webpack,
326                )),
327                None => Either::Right(noop_pass()),
328            },
329            match &opts.track_dynamic_imports {
330                true => Either::Left(
331                    crate::transforms::track_dynamic_imports::track_dynamic_imports(
332                        unresolved_mark,
333                        comments.clone(),
334                    ),
335                ),
336                false => Either::Right(noop_pass()),
337            },
338            match &opts.cjs_require_optimizer {
339                Some(config) => Either::Left(visit_mut_pass(
340                    crate::transforms::cjs_optimizer::cjs_optimizer(
341                        config.clone(),
342                        SyntaxContext::empty().apply_mark(unresolved_mark),
343                    ),
344                )),
345                None => Either::Right(noop_pass()),
346            },
347            Optional::new(
348                crate::transforms::debug_fn_name::debug_fn_name(),
349                opts.debug_function_name,
350            ),
351            crate::transforms::debug_instant_stack::debug_instant_stack(
352                file_path_for_instant_stack,
353                match &opts.server_components {
354                    Some(react_server_components::Config::WithOptions(options)) => {
355                        options.page_extensions.clone()
356                    }
357                    _ => vec![],
358                },
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/// Defaults to false
397
398#[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            EmptyObject(EmptyStruct),
436            #[serde(untagged)]
437            Obj(T),
438        }
439
440        #[derive(Deserialize)]
441        #[serde(deny_unknown_fields)]
442        struct EmptyStruct {}
443
444        let res = Deser::deserialize(deserializer)?;
445        Ok(match res {
446            Deser::Bool(v) => BoolOr::Bool(v),
447            Deser::EmptyObject(_) => BoolOr::Bool(true),
448            Deser::Obj(v) => BoolOr::Data(v),
449        })
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use serde::Deserialize;
456    use serde_json::json;
457
458    use super::BoolOr;
459
460    #[test]
461    fn test_bool_or() {
462        let v: BoolOr<usize> = serde_json::from_value(json!(false)).unwrap();
463        assert_eq!(v, BoolOr::Bool(false));
464
465        let v: BoolOr<usize> = serde_json::from_value(json!(true)).unwrap();
466        assert_eq!(v, BoolOr::Bool(true));
467
468        let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
469        assert_eq!(v, BoolOr::Bool(true));
470
471        let v: Result<BoolOr<usize>, _> = serde_json::from_value(json!({"a": 1}));
472        assert!(v.is_err());
473
474        let v: BoolOr<usize> = serde_json::from_value(json!(1)).unwrap();
475        assert_eq!(v, BoolOr::Data(1));
476
477        let v: BoolOr<usize> = serde_json::from_value(json!({})).unwrap();
478        assert_eq!(v, BoolOr::Bool(true));
479
480        #[derive(Debug, Eq, PartialEq, Deserialize)]
481        struct SomeStruct {
482            field: Option<usize>,
483        }
484
485        let v: BoolOr<SomeStruct> = serde_json::from_value(json!({})).unwrap();
486        assert_eq!(v, BoolOr::Bool(true));
487
488        let v: BoolOr<SomeStruct> = serde_json::from_value(json!({"field": 32})).unwrap();
489        assert_eq!(v, BoolOr::Data(SomeStruct { field: Some(32) }));
490    }
491}