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 disable_page_config: bool,
45
46    #[serde(default)]
47    pub pages_dir: Option<PathBuf>,
48
49    #[serde(default)]
50    pub app_dir: Option<PathBuf>,
51
52    #[serde(default)]
53    pub is_page_file: bool,
54
55    #[serde(default)]
56    pub is_development: bool,
57
58    #[serde(default)]
59    pub is_server_compiler: bool,
60
61    #[serde(default)]
62    pub prefer_esm: bool,
63
64    #[serde(default)]
65    pub server_components: Option<react_server_components::Config>,
66
67    #[serde(default)]
68    pub styled_jsx: BoolOr<styled_jsx::visitor::Config>,
69
70    #[serde(default)]
71    pub styled_components: Option<styled_components::Config>,
72
73    #[serde(default)]
74    pub remove_console: Option<remove_console::Config>,
75
76    #[serde(default)]
77    pub react_remove_properties: Option<react_remove_properties::Config>,
78
79    #[serde(default)]
80    #[cfg(not(target_arch = "wasm32"))]
81    pub relay: Option<swc_relay::Config>,
82
83    #[allow(unused)]
84    #[serde(default)]
85    #[cfg(target_arch = "wasm32")]
86    /// Accept any value
87    pub relay: Option<serde_json::Value>,
88
89    #[serde(default)]
90    pub shake_exports: Option<crate::transforms::shake_exports::Config>,
91
92    #[serde(default)]
93    pub emotion: Option<swc_emotion::EmotionOptions>,
94
95    #[serde(default)]
96    pub modularize_imports: Option<modularize_imports::Config>,
97
98    #[serde(default)]
99    pub auto_modularize_imports: Option<crate::transforms::named_import_transform::Config>,
100
101    #[serde(default)]
102    pub optimize_barrel_exports: Option<crate::transforms::optimize_barrel::Config>,
103
104    #[serde(default)]
105    pub font_loaders: Option<crate::transforms::fonts::Config>,
106
107    #[serde(default)]
108    pub server_actions: Option<crate::transforms::server_actions::Config>,
109
110    #[serde(default)]
111    pub cjs_require_optimizer: Option<crate::transforms::cjs_optimizer::Config>,
112
113    #[serde(default)]
114    pub optimize_server_react: Option<crate::transforms::optimize_server_react::Config>,
115
116    #[serde(default)]
117    pub debug_function_name: bool,
118
119    #[serde(default)]
120    pub lint_codemod_comments: bool,
121
122    #[serde(default)]
123    pub css_env: Option<swc_core::ecma::preset_env::Config>,
124
125    #[serde(default)]
126    pub track_dynamic_imports: bool,
127}
128
129pub fn custom_before_pass<'a, C>(
130    cm: Arc<SourceMap>,
131    file: Arc<SourceFile>,
132    opts: &'a TransformOptions,
133    comments: C,
134    eliminated_packages: Rc<RefCell<FxHashSet<Atom>>>,
135    unresolved_mark: Mark,
136    use_cache_telemetry_tracker: Rc<RefCell<FxHashMap<String, usize>>>,
137) -> impl Pass + 'a
138where
139    C: Clone + Comments + 'a,
140{
141    let file_path_str = file.name.to_string();
142
143    #[cfg(target_arch = "wasm32")]
144    let relay_plugin = noop_pass();
145
146    #[cfg(not(target_arch = "wasm32"))]
147    let relay_plugin = {
148        if let Some(config) = &opts.relay {
149            Either::Left(swc_relay::relay(
150                Arc::new(config.clone()),
151                (*file.name).clone(),
152                std::env::current_dir().unwrap(),
153                opts.pages_dir.clone(),
154                None,
155            ))
156        } else {
157            Either::Right(noop_pass())
158        }
159    };
160
161    let styled_jsx = {
162        let cm = cm.clone();
163        let file = file.clone();
164
165        fn_pass(move |program| {
166            if let Some(config) = opts.styled_jsx.to_option() {
167                let target_browsers = opts
168                    .css_env
169                    .as_ref()
170                    .map(|env| {
171                        targets_to_versions(env.targets.clone(), None)
172                            .expect("failed to parse env.targets")
173                    })
174                    .unwrap_or_default();
175
176                program.mutate(styled_jsx::visitor::styled_jsx(
177                    cm.clone(),
178                    &file.name,
179                    &styled_jsx::visitor::Config {
180                        use_lightningcss: config.use_lightningcss,
181                        browsers: *target_browsers,
182                    },
183                    &styled_jsx::visitor::NativeConfig { process_css: None },
184                ))
185            }
186        })
187    };
188
189    let styled_components = {
190        let file = file.clone();
191
192        fn_pass(move |program| {
193            if let Some(config) = &opts.styled_components {
194                program.mutate(styled_components::styled_components(
195                    Some(&file_path_str),
196                    file.src_hash,
197                    config,
198                    NoopComments,
199                ))
200            }
201        })
202    };
203
204    let emotion = {
205        let cm = cm.clone();
206        let file = file.clone();
207        let comments = comments.clone();
208
209        fn_pass(move |program| {
210            if let Some(config) = opts.emotion.as_ref() {
211                if !config.enabled.unwrap_or(false) {
212                    return;
213                }
214                if let FileName::Real(path) = &*file.name {
215                    program.mutate(swc_emotion::emotion(
216                        config,
217                        path,
218                        file.src_hash as u32,
219                        cm.clone(),
220                        comments.clone(),
221                    ));
222                }
223            }
224        })
225    };
226
227    let modularize_imports = fn_pass(move |program| {
228        if let Some(config) = opts.modularize_imports.as_ref() {
229            program.mutate(modularize_imports::modularize_imports(config));
230        }
231    });
232
233    (
234        (
235            crate::transforms::disallow_re_export_all_in_page::disallow_re_export_all_in_page(
236                opts.is_page_file,
237            ),
238            match &opts.server_components {
239                Some(config) if config.truthy() => {
240                    Either::Left(react_server_components::server_components(
241                        file.name.clone(),
242                        config.clone(),
243                        comments.clone(),
244                        opts.app_dir.clone(),
245                    ))
246                }
247                _ => Either::Right(noop_pass()),
248            },
249            styled_jsx,
250            styled_components,
251            Optional::new(
252                crate::transforms::next_ssg::next_ssg(eliminated_packages),
253                !opts.disable_next_ssg,
254            ),
255            crate::transforms::amp_attributes::amp_attributes(),
256            next_dynamic(
257                opts.is_development,
258                opts.is_server_compiler,
259                match &opts.server_components {
260                    Some(config) if config.truthy() => match config {
261                        // Always enable the Server Components mode for both
262                        // server and client layers.
263                        react_server_components::Config::WithOptions(config) => {
264                            config.is_react_server_layer
265                        }
266                        _ => false,
267                    },
268                    _ => false,
269                },
270                opts.prefer_esm,
271                NextDynamicMode::Webpack,
272                file.name.clone(),
273                opts.pages_dir.clone().or_else(|| opts.app_dir.clone()),
274            ),
275            Optional::new(
276                crate::transforms::page_config::page_config(opts.is_development, opts.is_page_file),
277                !opts.disable_page_config,
278            ),
279            relay_plugin,
280            match &opts.remove_console {
281                Some(config) if config.truthy() => Either::Left(remove_console::remove_console(
282                    config.clone(),
283                    SyntaxContext::empty().apply_mark(unresolved_mark),
284                )),
285                _ => Either::Right(noop_pass()),
286            },
287            match &opts.react_remove_properties {
288                Some(config) if config.truthy() => Either::Left(
289                    react_remove_properties::react_remove_properties(config.clone()),
290                ),
291                _ => Either::Right(noop_pass()),
292            },
293            match &opts.shake_exports {
294                Some(config) => Either::Left(crate::transforms::shake_exports::shake_exports(
295                    config.clone(),
296                )),
297                None => Either::Right(noop_pass()),
298            },
299        ),
300        (
301            match &opts.auto_modularize_imports {
302                Some(config) => Either::Left(
303                    crate::transforms::named_import_transform::named_import_transform(
304                        config.clone(),
305                    ),
306                ),
307                None => Either::Right(noop_pass()),
308            },
309            match &opts.optimize_barrel_exports {
310                Some(config) => Either::Left(crate::transforms::optimize_barrel::optimize_barrel(
311                    config.clone(),
312                )),
313                _ => Either::Right(noop_pass()),
314            },
315            match &opts.optimize_server_react {
316                Some(config) => Either::Left(
317                    crate::transforms::optimize_server_react::optimize_server_react(config.clone()),
318                ),
319                _ => Either::Right(noop_pass()),
320            },
321            emotion,
322            modularize_imports,
323            match &opts.font_loaders {
324                Some(config) => Either::Left(next_font_loaders(config.clone())),
325                None => Either::Right(noop_pass()),
326            },
327            match &opts.server_actions {
328                Some(config) => Either::Left(crate::transforms::server_actions::server_actions(
329                    &file.name,
330                    None,
331                    config.clone(),
332                    comments.clone(),
333                    cm.clone(),
334                    use_cache_telemetry_tracker,
335                    ServerActionsMode::Webpack,
336                )),
337                None => Either::Right(noop_pass()),
338            },
339            match &opts.track_dynamic_imports {
340                true => Either::Left(
341                    crate::transforms::track_dynamic_imports::track_dynamic_imports(
342                        unresolved_mark,
343                    ),
344                ),
345                false => Either::Right(noop_pass()),
346            },
347            match &opts.cjs_require_optimizer {
348                Some(config) => Either::Left(visit_mut_pass(
349                    crate::transforms::cjs_optimizer::cjs_optimizer(
350                        config.clone(),
351                        SyntaxContext::empty().apply_mark(unresolved_mark),
352                    ),
353                )),
354                None => Either::Right(noop_pass()),
355            },
356            Optional::new(
357                crate::transforms::debug_fn_name::debug_fn_name(),
358                opts.debug_function_name,
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            Obj(T),
436            EmptyObject(EmptyStruct),
437        }
438
439        #[derive(Deserialize)]
440        #[serde(deny_unknown_fields)]
441        struct EmptyStruct {}
442
443        use serde::__private::de;
444
445        let content = de::Content::deserialize(deserializer)?;
446
447        let deserializer = de::ContentRefDeserializer::<D::Error>::new(&content);
448
449        let res = Deser::deserialize(deserializer);
450
451        match res {
452            Ok(v) => Ok(match v {
453                Deser::Bool(v) => BoolOr::Bool(v),
454                Deser::Obj(v) => BoolOr::Data(v),
455                Deser::EmptyObject(_) => BoolOr::Bool(true),
456            }),
457            Err(..) => {
458                let d = de::ContentDeserializer::<D::Error>::new(content);
459                Ok(BoolOr::Data(T::deserialize(d)?))
460            }
461        }
462    }
463}