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, Feature, FeatureOrModule, 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        development: bool,
37        refresh: bool,
38        // swc.jsc.transform.react.importSource
39        import_source: ResolvedVc<Option<RcStr>>,
40        // swc.jsc.transform.react.runtime,
41        runtime: ResolvedVc<Option<RcStr>>,
42    },
43    // These options are subset of swc_core::ecma::transforms::typescript::Config, but
44    // it doesn't derive `Copy` so repeating values in here
45    TypeScript {
46        use_define_for_class_fields: bool,
47    },
48    Decorators {
49        is_legacy: bool,
50        is_ecma: bool,
51        emit_decorators_metadata: bool,
52        use_define_for_class_fields: bool,
53    },
54}
55
56/// The CustomTransformer trait allows you to implement your own custom SWC
57/// transformer to run over all ECMAScript files imported in the graph.
58#[async_trait]
59pub trait CustomTransformer: Debug {
60    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
61}
62
63/// A wrapper around a TransformPlugin instance, allowing it to operate with
64/// the turbo_task caching requirements.
65#[turbo_tasks::value(transparent, serialization = "none", eq = "manual", cell = "new")]
66#[derive(Debug)]
67pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
68
69#[async_trait]
70impl CustomTransformer for TransformPlugin {
71    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
72        self.0.transform(program, ctx).await
73    }
74}
75
76#[turbo_tasks::value(transparent)]
77#[derive(Debug, Clone, Hash)]
78pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
79
80#[turbo_tasks::value_impl]
81impl EcmascriptInputTransforms {
82    #[turbo_tasks::function]
83    pub fn empty() -> Vc<Self> {
84        Vc::cell(Vec::new())
85    }
86
87    #[turbo_tasks::function]
88    pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
89        let mut transforms = self.owned().await?;
90        transforms.extend(other.owned().await?);
91        Ok(Vc::cell(transforms))
92    }
93}
94
95pub struct TransformContext<'a> {
96    pub comments: &'a SwcComments,
97    pub top_level_mark: Mark,
98    pub unresolved_mark: Mark,
99    pub source_map: &'a Arc<SourceMap>,
100    pub file_path_str: &'a str,
101    pub file_name_str: &'a str,
102    pub file_name_hash: u128,
103    pub query_str: RcStr,
104    pub file_path: FileSystemPath,
105    pub source: ResolvedVc<Box<dyn Source>>,
106}
107
108impl EcmascriptInputTransform {
109    pub async fn apply(
110        &self,
111        program: &mut Program,
112        ctx: &TransformContext<'_>,
113        helpers: HelperData,
114    ) -> Result<HelperData> {
115        let &TransformContext {
116            comments,
117            source_map,
118            top_level_mark,
119            unresolved_mark,
120            ..
121        } = ctx;
122
123        Ok(match self {
124            EcmascriptInputTransform::React {
125                development,
126                refresh,
127                import_source,
128                runtime,
129            } => {
130                use swc_core::ecma::transforms::react::{Options, Runtime};
131                let runtime = if let Some(runtime) = &*runtime.await? {
132                    match runtime.as_str() {
133                        "classic" => Runtime::Classic,
134                        "automatic" => Runtime::Automatic,
135                        _ => {
136                            return Err(anyhow::anyhow!(
137                                "Invalid value for swc.jsc.transform.react.runtime: {}",
138                                runtime
139                            ));
140                        }
141                    }
142                } else {
143                    Runtime::Automatic
144                };
145
146                let config = Options {
147                    runtime: Some(runtime),
148                    development: Some(*development),
149                    import_source: import_source.await?.as_deref().map(Atom::from),
150                    refresh: if *refresh {
151                        debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
152                        Some(swc_core::ecma::transforms::react::RefreshOptions {
153                            refresh_reg: atom!("__turbopack_context__.k.register"),
154                            refresh_sig: atom!("__turbopack_context__.k.signature"),
155                            ..Default::default()
156                        })
157                    } else {
158                        None
159                    },
160                    ..Default::default()
161                };
162
163                // Explicit type annotation to ensure that we don't duplicate transforms in the
164                // final binary
165                let helpers = apply_transform(
166                    program,
167                    helpers,
168                    react::<&dyn Comments>(
169                        source_map.clone(),
170                        Some(&comments),
171                        config,
172                        top_level_mark,
173                        unresolved_mark,
174                    ),
175                );
176
177                if *refresh {
178                    debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
179                    debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
180                    let stmt = quote!(
181                        // No-JS mode does not inject these helpers
182                        "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
183                         globalThis.$RefreshHelpers !== null) { \
184                         __turbopack_context__.k.registerExports(__turbopack_context__.m, \
185                         globalThis.$RefreshHelpers$); }" as Stmt
186                    );
187
188                    match program {
189                        Program::Module(module) => {
190                            module.body.push(ModuleItem::Stmt(stmt));
191                        }
192                        Program::Script(script) => {
193                            script.body.push(stmt);
194                        }
195                    }
196                }
197
198                helpers
199            }
200            EcmascriptInputTransform::PresetEnv(env) => {
201                let versions = env.runtime_versions().await?;
202                let config = swc_core::ecma::preset_env::EnvConfig::from(
203                    swc_core::ecma::preset_env::Config {
204                        targets: Some(Targets::Versions(*versions)),
205                        mode: None, // Don't insert core-js polyfills
206                        // Disable some ancient ES3 transforms, ReservedWords breaks resolving of
207                        // some idents references
208                        exclude: vec![
209                            FeatureOrModule::Feature(Feature::ReservedWords),
210                            FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
211                            FeatureOrModule::Feature(Feature::PropertyLiterals),
212                        ],
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}