turbopack_ecmascript/transform/
mod.rs

1use std::{fmt::Debug, hash::Hash, sync::Arc};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use swc_core::{
6    atoms::{Atom, atom},
7    base::SwcComments,
8    common::{Mark, SourceMap, comments::Comments},
9    ecma::{
10        ast::{ExprStmt, ModuleItem, Pass, Program, Stmt},
11        preset_env::{self, Targets},
12        transforms::{
13            base::{
14                assumptions::Assumptions,
15                helpers::{HELPERS, HelperData, Helpers},
16            },
17            react::react,
18        },
19        utils::IsDirective,
20    },
21    quote,
22};
23use turbo_rcstr::RcStr;
24use turbo_tasks::{ResolvedVc, Vc};
25use turbo_tasks_fs::FileSystemPath;
26use turbopack_core::{environment::Environment, source::Source};
27
28use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH};
29
30#[turbo_tasks::value]
31#[derive(Debug, Clone, Hash)]
32pub enum EcmascriptInputTransform {
33    Plugin(ResolvedVc<TransformPlugin>),
34    PresetEnv(ResolvedVc<Environment>),
35    React {
36        #[serde(default)]
37        development: bool,
38        #[serde(default)]
39        refresh: bool,
40        // swc.jsc.transform.react.importSource
41        import_source: ResolvedVc<Option<RcStr>>,
42        // swc.jsc.transform.react.runtime,
43        runtime: ResolvedVc<Option<RcStr>>,
44    },
45    // These options are subset of swc_core::ecma::transforms::typescript::Config, but
46    // it doesn't derive `Copy` so repeating values in here
47    TypeScript {
48        #[serde(default)]
49        use_define_for_class_fields: bool,
50    },
51    Decorators {
52        #[serde(default)]
53        is_legacy: bool,
54        #[serde(default)]
55        is_ecma: bool,
56        #[serde(default)]
57        emit_decorators_metadata: bool,
58        #[serde(default)]
59        use_define_for_class_fields: bool,
60    },
61}
62
63/// The CustomTransformer trait allows you to implement your own custom SWC
64/// transformer to run over all ECMAScript files imported in the graph.
65#[async_trait]
66pub trait CustomTransformer: Debug {
67    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
68}
69
70/// A wrapper around a TransformPlugin instance, allowing it to operate with
71/// the turbo_task caching requirements.
72#[turbo_tasks::value(transparent, serialization = "none", eq = "manual", cell = "new")]
73#[derive(Debug)]
74pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
75
76#[async_trait]
77impl CustomTransformer for TransformPlugin {
78    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
79        self.0.transform(program, ctx).await
80    }
81}
82
83#[turbo_tasks::value(transparent)]
84#[derive(Debug, Clone, Hash)]
85pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
86
87#[turbo_tasks::value_impl]
88impl EcmascriptInputTransforms {
89    #[turbo_tasks::function]
90    pub fn empty() -> Vc<Self> {
91        Vc::cell(Vec::new())
92    }
93
94    #[turbo_tasks::function]
95    pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
96        let mut transforms = self.owned().await?;
97        transforms.extend(other.owned().await?);
98        Ok(Vc::cell(transforms))
99    }
100}
101
102pub struct TransformContext<'a> {
103    pub comments: &'a SwcComments,
104    pub top_level_mark: Mark,
105    pub unresolved_mark: Mark,
106    pub source_map: &'a Arc<SourceMap>,
107    pub file_path_str: &'a str,
108    pub file_name_str: &'a str,
109    pub file_name_hash: u128,
110    pub query_str: RcStr,
111    pub file_path: FileSystemPath,
112    pub source: ResolvedVc<Box<dyn Source>>,
113}
114
115impl EcmascriptInputTransform {
116    pub async fn apply(
117        &self,
118        program: &mut Program,
119        ctx: &TransformContext<'_>,
120        helpers: HelperData,
121    ) -> Result<HelperData> {
122        let &TransformContext {
123            comments,
124            source_map,
125            top_level_mark,
126            unresolved_mark,
127            ..
128        } = ctx;
129
130        Ok(match self {
131            EcmascriptInputTransform::React {
132                development,
133                refresh,
134                import_source,
135                runtime,
136            } => {
137                use swc_core::ecma::transforms::react::{Options, Runtime};
138                let runtime = if let Some(runtime) = &*runtime.await? {
139                    match runtime.as_str() {
140                        "classic" => Runtime::Classic,
141                        "automatic" => Runtime::Automatic,
142                        _ => {
143                            return Err(anyhow::anyhow!(
144                                "Invalid value for swc.jsc.transform.react.runtime: {}",
145                                runtime
146                            ));
147                        }
148                    }
149                } else {
150                    Runtime::Automatic
151                };
152
153                let config = Options {
154                    runtime: Some(runtime),
155                    development: Some(*development),
156                    import_source: import_source.await?.as_deref().map(Atom::from),
157                    refresh: if *refresh {
158                        debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
159                        Some(swc_core::ecma::transforms::react::RefreshOptions {
160                            refresh_reg: atom!("__turbopack_context__.k.register"),
161                            refresh_sig: atom!("__turbopack_context__.k.signature"),
162                            ..Default::default()
163                        })
164                    } else {
165                        None
166                    },
167                    ..Default::default()
168                };
169
170                // Explicit type annotation to ensure that we don't duplicate transforms in the
171                // final binary
172                let helpers = apply_transform(
173                    program,
174                    helpers,
175                    react::<&dyn Comments>(
176                        source_map.clone(),
177                        Some(&comments),
178                        config,
179                        top_level_mark,
180                        unresolved_mark,
181                    ),
182                );
183
184                if *refresh {
185                    debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
186                    debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
187                    let stmt = quote!(
188                        // No-JS mode does not inject these helpers
189                        "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
190                         globalThis.$RefreshHelpers !== null) { \
191                         __turbopack_context__.k.registerExports(__turbopack_context__.m, \
192                         globalThis.$RefreshHelpers$); }" as Stmt
193                    );
194
195                    match program {
196                        Program::Module(module) => {
197                            module.body.push(ModuleItem::Stmt(stmt));
198                        }
199                        Program::Script(script) => {
200                            script.body.push(stmt);
201                        }
202                    }
203                }
204
205                helpers
206            }
207            EcmascriptInputTransform::PresetEnv(env) => {
208                let versions = env.runtime_versions().await?;
209                let config = swc_core::ecma::preset_env::EnvConfig::from(
210                    swc_core::ecma::preset_env::Config {
211                        targets: Some(Targets::Versions(*versions)),
212                        mode: None, // Don't insert core-js polyfills
213                        ..Default::default()
214                    },
215                );
216
217                // Explicit type annotation to ensure that we don't duplicate transforms in the
218                // final binary
219                apply_transform(
220                    program,
221                    helpers,
222                    preset_env::transform_from_env::<&'_ dyn Comments>(
223                        unresolved_mark,
224                        Some(&comments),
225                        config,
226                        Assumptions::default(),
227                    ),
228                )
229            }
230            EcmascriptInputTransform::TypeScript {
231                // TODO(WEB-1213)
232                use_define_for_class_fields: _use_define_for_class_fields,
233            } => {
234                use swc_core::ecma::transforms::typescript::typescript;
235                let config = Default::default();
236                apply_transform(
237                    program,
238                    helpers,
239                    typescript(config, unresolved_mark, top_level_mark),
240                )
241            }
242            EcmascriptInputTransform::Decorators {
243                is_legacy,
244                is_ecma: _,
245                emit_decorators_metadata,
246                // TODO(WEB-1213)
247                use_define_for_class_fields: _use_define_for_class_fields,
248            } => {
249                use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
250                let config = Config {
251                    legacy: *is_legacy,
252                    emit_metadata: *emit_decorators_metadata,
253                    ..Default::default()
254                };
255
256                apply_transform(program, helpers, decorators(config))
257            }
258            EcmascriptInputTransform::Plugin(transform) => {
259                // We cannot pass helpers to plugins, so we return them as is
260                transform.await?.transform(program, ctx).await?;
261                helpers
262            }
263        })
264    }
265}
266
267fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
268    let helpers = Helpers::from_data(helpers);
269    HELPERS.set(&helpers, || {
270        program.mutate(op);
271    });
272    helpers.data()
273}
274
275pub fn remove_shebang(program: &mut Program) {
276    match program {
277        Program::Module(m) => {
278            m.shebang = None;
279        }
280        Program::Script(s) => {
281            s.shebang = None;
282        }
283    }
284}
285
286pub fn remove_directives(program: &mut Program) {
287    match program {
288        Program::Module(module) => {
289            let directive_count = module
290                .body
291                .iter()
292                .take_while(|i| match i {
293                    ModuleItem::Stmt(stmt) => stmt.directive_continue(),
294                    ModuleItem::ModuleDecl(_) => false,
295                })
296                .take_while(|i| match i {
297                    ModuleItem::Stmt(stmt) => match stmt {
298                        Stmt::Expr(ExprStmt { expr, .. }) => expr
299                            .as_lit()
300                            .and_then(|lit| lit.as_str())
301                            .and_then(|str| str.raw.as_ref())
302                            .is_some_and(|raw| {
303                                raw.starts_with("\"use ") || raw.starts_with("'use ")
304                            }),
305                        _ => false,
306                    },
307                    ModuleItem::ModuleDecl(_) => false,
308                })
309                .count();
310            module.body.drain(0..directive_count);
311        }
312        Program::Script(script) => {
313            let directive_count = script
314                .body
315                .iter()
316                .take_while(|stmt| stmt.directive_continue())
317                .take_while(|stmt| match stmt {
318                    Stmt::Expr(ExprStmt { expr, .. }) => expr
319                        .as_lit()
320                        .and_then(|lit| lit.as_str())
321                        .and_then(|str| str.raw.as_ref())
322                        .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
323                    _ => false,
324                })
325                .count();
326            script.body.drain(0..directive_count);
327        }
328    }
329}