turbopack_ecmascript/transform/
mod.rs

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