next_custom_transforms/
chain_transforms.rs

1use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::Arc};
2
3use either::Either;
4use fxhash::FxHashSet;
5use modularize_imports;
6use preset_env_base::query::targets_to_versions;
7use serde::Deserialize;
8use swc_core::{
9    common::{
10        comments::{Comments, NoopComments},
11        pass::Optional,
12        FileName, Mark, SourceFile, SourceMap, SyntaxContext,
13    },
14    ecma::{
15        ast::{noop_pass, EsVersion, Pass},
16        parser::parse_file_as_module,
17        visit::{fold_pass, visit_mut_pass},
18    },
19};
20
21use crate::{
22    linter::linter,
23    transforms::{
24        cjs_finder::contains_cjs,
25        dynamic::{next_dynamic, NextDynamicMode},
26        fonts::next_font_loaders,
27        lint_codemod_comments::lint_codemod_comments,
28        react_server_components,
29    },
30};
31
32#[derive(Clone, Debug, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct TransformOptions {
35    #[serde(flatten)]
36    pub swc: swc_core::base::config::Options,
37
38    #[serde(default)]
39    pub disable_next_ssg: bool,
40
41    #[serde(default)]
42    pub disable_page_config: bool,
43
44    #[serde(default)]
45    pub pages_dir: Option<PathBuf>,
46
47    #[serde(default)]
48    pub app_dir: Option<PathBuf>,
49
50    #[serde(default)]
51    pub is_page_file: bool,
52
53    #[serde(default)]
54    pub is_development: bool,
55
56    #[serde(default)]
57    pub is_server_compiler: bool,
58
59    #[serde(default)]
60    pub prefer_esm: bool,
61
62    #[serde(default)]
63    pub server_components: Option<react_server_components::Config>,
64
65    #[serde(default)]
66    pub styled_jsx: BoolOr<styled_jsx::visitor::Config>,
67
68    #[serde(default)]
69    pub styled_components: Option<styled_components::Config>,
70
71    #[serde(default)]
72    pub remove_console: Option<remove_console::Config>,
73
74    #[serde(default)]
75    pub react_remove_properties: Option<react_remove_properties::Config>,
76
77    #[serde(default)]
78    #[cfg(not(target_arch = "wasm32"))]
79    pub relay: Option<swc_relay::Config>,
80
81    #[allow(unused)]
82    #[serde(default)]
83    #[cfg(target_arch = "wasm32")]
84    /// Accept any value
85    pub relay: Option<serde_json::Value>,
86
87    #[serde(default)]
88    pub shake_exports: Option<crate::transforms::shake_exports::Config>,
89
90    #[serde(default)]
91    pub emotion: Option<swc_emotion::EmotionOptions>,
92
93    #[serde(default)]
94    pub modularize_imports: Option<modularize_imports::Config>,
95
96    #[serde(default)]
97    pub auto_modularize_imports: Option<crate::transforms::named_import_transform::Config>,
98
99    #[serde(default)]
100    pub optimize_barrel_exports: Option<crate::transforms::optimize_barrel::Config>,
101
102    #[serde(default)]
103    pub font_loaders: Option<crate::transforms::fonts::Config>,
104
105    #[serde(default)]
106    pub server_actions: Option<crate::transforms::server_actions::Config>,
107
108    #[serde(default)]
109    pub cjs_require_optimizer: Option<crate::transforms::cjs_optimizer::Config>,
110
111    #[serde(default)]
112    pub optimize_server_react: Option<crate::transforms::optimize_server_react::Config>,
113
114    #[serde(default)]
115    pub debug_function_name: bool,
116
117    #[serde(default)]
118    pub lint_codemod_comments: bool,
119}
120
121pub fn custom_before_pass<'a, C>(
122    cm: Arc<SourceMap>,
123    file: Arc<SourceFile>,
124    opts: &'a TransformOptions,
125    comments: C,
126    eliminated_packages: Rc<RefCell<FxHashSet<String>>>,
127    unresolved_mark: Mark,
128) -> impl Pass + 'a
129where
130    C: Clone + Comments + 'a,
131{
132    #[cfg(target_arch = "wasm32")]
133    let relay_plugin = noop_pass();
134
135    #[cfg(not(target_arch = "wasm32"))]
136    let relay_plugin = {
137        if let Some(config) = &opts.relay {
138            Either::Left(swc_relay::relay(
139                Arc::new(config.clone()),
140                (*file.name).clone(),
141                std::env::current_dir().unwrap(),
142                opts.pages_dir.clone(),
143                None,
144            ))
145        } else {
146            Either::Right(noop_pass())
147        }
148    };
149
150    let modularize_imports_config = match &opts.modularize_imports {
151        Some(config) => config.clone(),
152        None => modularize_imports::Config {
153            packages: std::collections::HashMap::new(),
154        },
155    };
156
157    let target_browsers = opts
158        .swc
159        .config
160        .env
161        .as_ref()
162        .map(|env| targets_to_versions(env.targets.clone()).expect("failed to parse env.targets"))
163        .unwrap_or_default();
164
165    let styled_jsx = if let Some(config) = opts.styled_jsx.to_option() {
166        Either::Left(styled_jsx::visitor::styled_jsx(
167            cm.clone(),
168            (*file.name).clone(),
169            styled_jsx::visitor::Config {
170                use_lightningcss: config.use_lightningcss,
171                browsers: target_browsers,
172            },
173            styled_jsx::visitor::NativeConfig { process_css: None },
174        ))
175    } else {
176        Either::Right(noop_pass())
177    };
178
179    (
180        (
181            crate::transforms::disallow_re_export_all_in_page::disallow_re_export_all_in_page(
182                opts.is_page_file,
183            ),
184            match &opts.server_components {
185                Some(config) if config.truthy() => {
186                    Either::Left(react_server_components::server_components(
187                        file.name.clone(),
188                        config.clone(),
189                        comments.clone(),
190                        opts.app_dir.clone(),
191                    ))
192                }
193                _ => Either::Right(noop_pass()),
194            },
195            styled_jsx,
196            match &opts.styled_components {
197                Some(config) => Either::Left(styled_components::styled_components(
198                    file.name.clone(),
199                    file.src_hash,
200                    config.clone(),
201                    NoopComments,
202                )),
203                None => Either::Right(noop_pass()),
204            },
205            Optional::new(
206                crate::transforms::next_ssg::next_ssg(eliminated_packages),
207                !opts.disable_next_ssg,
208            ),
209            crate::transforms::amp_attributes::amp_attributes(),
210            next_dynamic(
211                opts.is_development,
212                opts.is_server_compiler,
213                match &opts.server_components {
214                    Some(config) if config.truthy() => match config {
215                        // Always enable the Server Components mode for both
216                        // server and client layers.
217                        react_server_components::Config::WithOptions(config) => {
218                            config.is_react_server_layer
219                        }
220                        _ => false,
221                    },
222                    _ => false,
223                },
224                opts.prefer_esm,
225                NextDynamicMode::Webpack,
226                file.name.clone(),
227                opts.pages_dir.clone().or_else(|| opts.app_dir.clone()),
228            ),
229            Optional::new(
230                crate::transforms::page_config::page_config(opts.is_development, opts.is_page_file),
231                !opts.disable_page_config,
232            ),
233            relay_plugin,
234            match &opts.remove_console {
235                Some(config) if config.truthy() => Either::Left(remove_console::remove_console(
236                    config.clone(),
237                    SyntaxContext::empty().apply_mark(unresolved_mark),
238                )),
239                _ => Either::Right(noop_pass()),
240            },
241            match &opts.react_remove_properties {
242                Some(config) if config.truthy() => Either::Left(
243                    react_remove_properties::react_remove_properties(config.clone()),
244                ),
245                _ => Either::Right(noop_pass()),
246            },
247            match &opts.shake_exports {
248                Some(config) => Either::Left(crate::transforms::shake_exports::shake_exports(
249                    config.clone(),
250                )),
251                None => Either::Right(noop_pass()),
252            },
253        ),
254        (
255            match &opts.auto_modularize_imports {
256                Some(config) => Either::Left(
257                    crate::transforms::named_import_transform::named_import_transform(
258                        config.clone(),
259                    ),
260                ),
261                None => Either::Right(noop_pass()),
262            },
263            match &opts.optimize_barrel_exports {
264                Some(config) => Either::Left(crate::transforms::optimize_barrel::optimize_barrel(
265                    config.clone(),
266                )),
267                _ => Either::Right(noop_pass()),
268            },
269            match &opts.optimize_server_react {
270                Some(config) => Either::Left(
271                    crate::transforms::optimize_server_react::optimize_server_react(config.clone()),
272                ),
273                _ => Either::Right(noop_pass()),
274            },
275            opts.emotion
276                .as_ref()
277                .and_then(|config| {
278                    if !config.enabled.unwrap_or(false) {
279                        return None;
280                    }
281                    if let FileName::Real(path) = &*file.name {
282                        path.to_str().map(|_| {
283                            Either::Left(fold_pass(swc_emotion::EmotionTransformer::new(
284                                config.clone(),
285                                path,
286                                file.src_hash as u32,
287                                cm,
288                                comments.clone(),
289                            )))
290                        })
291                    } else {
292                        None
293                    }
294                })
295                .unwrap_or_else(|| Either::Right(noop_pass())),
296            modularize_imports::modularize_imports(modularize_imports_config),
297            match &opts.font_loaders {
298                Some(config) => Either::Left(next_font_loaders(config.clone())),
299                None => Either::Right(noop_pass()),
300            },
301            match &opts.server_actions {
302                Some(config) => Either::Left(crate::transforms::server_actions::server_actions(
303                    &file.name,
304                    config.clone(),
305                    comments.clone(),
306                )),
307                None => Either::Right(noop_pass()),
308            },
309            match &opts.cjs_require_optimizer {
310                Some(config) => Either::Left(visit_mut_pass(
311                    crate::transforms::cjs_optimizer::cjs_optimizer(
312                        config.clone(),
313                        SyntaxContext::empty().apply_mark(unresolved_mark),
314                    ),
315                )),
316                None => Either::Right(noop_pass()),
317            },
318            Optional::new(
319                crate::transforms::debug_fn_name::debug_fn_name(),
320                opts.debug_function_name,
321            ),
322            visit_mut_pass(crate::transforms::pure::pure_magic(comments.clone())),
323            Optional::new(
324                linter(lint_codemod_comments(comments)),
325                opts.lint_codemod_comments,
326            ),
327        ),
328    )
329}
330
331impl TransformOptions {
332    pub fn patch(mut self, fm: &SourceFile) -> Self {
333        self.swc.swcrc = false;
334
335        let should_enable_commonjs = self.swc.config.module.is_none()
336            && (fm.src.contains("module.exports")
337                || fm.src.contains("exports.")
338                || fm.src.contains("__esModule"))
339            && {
340                let syntax = self.swc.config.jsc.syntax.unwrap_or_default();
341                let target = self.swc.config.jsc.target.unwrap_or_else(EsVersion::latest);
342
343                parse_file_as_module(fm, syntax, target, None, &mut vec![])
344                    .map(|m| contains_cjs(&m))
345                    .unwrap_or_default()
346            };
347
348        if should_enable_commonjs {
349            self.swc.config.module = Some(
350                serde_json::from_str(r#"{ "type": "commonjs", "ignoreDynamic": true }"#).unwrap(),
351            );
352        }
353
354        self
355    }
356}
357
358/// Defaults to false
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
361pub enum BoolOr<T> {
362    Bool(bool),
363    Data(T),
364}
365
366impl<T> Default for BoolOr<T> {
367    fn default() -> Self {
368        BoolOr::Bool(false)
369    }
370}
371
372impl<T> BoolOr<T> {
373    pub fn to_option(&self) -> Option<T>
374    where
375        T: Default + Clone,
376    {
377        match self {
378            BoolOr::Bool(false) => None,
379            BoolOr::Bool(true) => Some(Default::default()),
380            BoolOr::Data(v) => Some(v.clone()),
381        }
382    }
383}
384
385impl<'de, T> Deserialize<'de> for BoolOr<T>
386where
387    T: Deserialize<'de>,
388{
389    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
390    where
391        D: serde::Deserializer<'de>,
392    {
393        #[derive(Deserialize)]
394        #[serde(untagged)]
395        enum Deser<T> {
396            Bool(bool),
397            Obj(T),
398            EmptyObject(EmptyStruct),
399        }
400
401        #[derive(Deserialize)]
402        #[serde(deny_unknown_fields)]
403        struct EmptyStruct {}
404
405        use serde::__private::de;
406
407        let content = de::Content::deserialize(deserializer)?;
408
409        let deserializer = de::ContentRefDeserializer::<D::Error>::new(&content);
410
411        let res = Deser::deserialize(deserializer);
412
413        match res {
414            Ok(v) => Ok(match v {
415                Deser::Bool(v) => BoolOr::Bool(v),
416                Deser::Obj(v) => BoolOr::Data(v),
417                Deser::EmptyObject(_) => BoolOr::Bool(true),
418            }),
419            Err(..) => {
420                let d = de::ContentDeserializer::<D::Error>::new(content);
421                Ok(BoolOr::Data(T::deserialize(d)?))
422            }
423        }
424    }
425}