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