Skip to main content

turbopack_ecmascript/transform/
mod.rs

1use std::{fmt::Debug, hash::Hash, sync::Arc};
2
3use anyhow::{Result, bail};
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            typescript::{Config, typescript},
19        },
20        utils::IsDirective,
21    },
22    quote,
23};
24use turbo_rcstr::RcStr;
25use turbo_tasks::{ResolvedVc, Vc};
26use turbo_tasks_fs::FileSystemPath;
27use turbopack_core::{environment::Environment, source::Source};
28
29use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH};
30
31/// Additional options for SWC's preset-env, beyond the browserslist-derived
32/// targets that are already provided by the `Environment`.
33///
34/// These correspond to the fields documented at
35/// <https://swc.rs/docs/configuration/supported-browsers>.
36#[turbo_tasks::value(shared)]
37#[derive(Default, Clone, Debug)]
38pub struct PresetEnvConfig {
39    /// Polyfill injection mode (`"usage"` or `"entry"`), matching Babel's
40    /// `useBuiltIns`.
41    pub mode: Option<RcStr>,
42    /// The core-js version string (e.g. `"3.38"`).
43    pub core_js: Option<RcStr>,
44    /// Core-js modules or SWC transform passes to skip.
45    pub skip: Option<Vec<RcStr>>,
46    /// Core-js modules or SWC transform passes to always include.
47    pub include: Option<Vec<RcStr>>,
48    /// Core-js modules or SWC transform passes to always exclude.
49    pub exclude: Option<Vec<RcStr>>,
50    /// Enable shipped TC39 proposals.
51    pub shipped_proposals: Option<bool>,
52    /// Force all transforms regardless of targets.
53    pub force_all_transforms: Option<bool>,
54    /// Enable debug output.
55    pub debug: Option<bool>,
56    /// Enable loose mode for transforms.
57    pub loose: Option<bool>,
58}
59
60#[turbo_tasks::value]
61#[derive(Debug, Clone, Hash)]
62pub enum EcmascriptInputTransform {
63    Plugin(ResolvedVc<TransformPlugin>),
64    PresetEnv(ResolvedVc<Environment>, ResolvedVc<PresetEnvConfig>),
65    React {
66        development: bool,
67        refresh: bool,
68        // swc.jsc.transform.react.importSource
69        import_source: ResolvedVc<Option<RcStr>>,
70        // swc.jsc.transform.react.runtime,
71        runtime: ResolvedVc<Option<RcStr>>,
72    },
73    // These options are subset of swc_core::ecma::transforms::typescript::Config, but
74    // it doesn't derive `Copy` so repeating values in here
75    TypeScript {
76        use_define_for_class_fields: bool,
77        verbatim_module_syntax: bool,
78    },
79    Decorators {
80        is_legacy: bool,
81        is_ecma: bool,
82        emit_decorators_metadata: bool,
83        use_define_for_class_fields: bool,
84    },
85}
86
87/// The CustomTransformer trait allows you to implement your own custom SWC
88/// transformer to run over all ECMAScript files imported in the graph.
89#[async_trait]
90pub trait CustomTransformer: Debug {
91    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
92}
93
94/// A wrapper around a TransformPlugin instance, allowing it to operate with
95/// the turbo_task caching requirements.
96#[turbo_tasks::value(transparent, serialization = "none", eq = "manual", cell = "new")]
97#[derive(Debug)]
98pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
99
100#[async_trait]
101impl CustomTransformer for TransformPlugin {
102    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
103        self.0.transform(program, ctx).await
104    }
105}
106
107#[turbo_tasks::value(transparent)]
108#[derive(Debug, Clone, Hash)]
109pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
110
111#[turbo_tasks::value_impl]
112impl EcmascriptInputTransforms {
113    #[turbo_tasks::function]
114    pub fn empty() -> Vc<Self> {
115        Vc::cell(Vec::new())
116    }
117
118    #[turbo_tasks::function]
119    pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
120        let mut transforms = self.owned().await?;
121        transforms.extend(other.owned().await?);
122        Ok(Vc::cell(transforms))
123    }
124}
125
126pub struct TransformContext<'a> {
127    pub comments: &'a SwcComments,
128    pub top_level_mark: Mark,
129    pub unresolved_mark: Mark,
130    pub source_map: &'a Arc<SourceMap>,
131    pub file_path_str: &'a str,
132    pub file_name_str: &'a str,
133    pub file_name_hash: u128,
134    pub query_str: RcStr,
135    pub file_path: FileSystemPath,
136    pub source: ResolvedVc<Box<dyn Source>>,
137}
138
139impl EcmascriptInputTransform {
140    pub async fn apply(
141        &self,
142        program: &mut Program,
143        ctx: &TransformContext<'_>,
144        helpers: HelperData,
145    ) -> Result<HelperData> {
146        let &TransformContext {
147            comments,
148            source_map,
149            top_level_mark,
150            unresolved_mark,
151            ..
152        } = ctx;
153
154        Ok(match self {
155            EcmascriptInputTransform::React {
156                development,
157                refresh,
158                import_source,
159                runtime,
160            } => {
161                use swc_core::ecma::transforms::react::{Options, Runtime};
162                let runtime = if let Some(runtime) = &*runtime.await? {
163                    match runtime.as_str() {
164                        "classic" => Runtime::Classic,
165                        "automatic" => Runtime::Automatic,
166                        _ => {
167                            bail!(
168                                "Invalid value for swc.jsc.transform.react.runtime: {}",
169                                runtime
170                            );
171                        }
172                    }
173                } else {
174                    Runtime::Automatic
175                };
176
177                let config = Options {
178                    runtime: Some(runtime),
179                    development: Some(*development),
180                    import_source: import_source.await?.as_deref().map(Atom::from),
181                    refresh: if *refresh {
182                        debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
183                        Some(swc_core::ecma::transforms::react::RefreshOptions {
184                            refresh_reg: atom!("__turbopack_context__.k.register"),
185                            refresh_sig: atom!("__turbopack_context__.k.signature"),
186                            ..Default::default()
187                        })
188                    } else {
189                        None
190                    },
191                    ..Default::default()
192                };
193
194                // Explicit type annotation to ensure that we don't duplicate transforms in the
195                // final binary
196                let helpers = apply_transform(
197                    program,
198                    helpers,
199                    react::<&dyn Comments>(
200                        source_map.clone(),
201                        Some(&comments),
202                        config,
203                        top_level_mark,
204                        unresolved_mark,
205                    ),
206                );
207
208                if *refresh {
209                    debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
210                    debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
211                    let stmt = quote!(
212                        // No-JS mode does not inject these helpers
213                        "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
214                         globalThis.$RefreshHelpers !== null) { \
215                         __turbopack_context__.k.registerExports(__turbopack_context__.m, \
216                         globalThis.$RefreshHelpers$); }" as Stmt
217                    );
218
219                    match program {
220                        Program::Module(module) => {
221                            module.body.push(ModuleItem::Stmt(stmt));
222                        }
223                        Program::Script(script) => {
224                            script.body.push(stmt);
225                        }
226                    }
227                }
228
229                helpers
230            }
231            EcmascriptInputTransform::PresetEnv(env, preset_env_config) => {
232                let versions = env.runtime_versions().await?;
233                let extra = preset_env_config.await?;
234
235                let mode = match extra.mode.as_deref() {
236                    Some("usage") => Some(preset_env::Mode::Usage),
237                    Some("entry") => Some(preset_env::Mode::Entry),
238                    _ => None,
239                };
240
241                let core_js = extra.core_js.as_ref().and_then(|v| {
242                    let parts: Vec<&str> = v.split('.').collect();
243                    Some(preset_env::Version {
244                        major: parts.first()?.parse().ok()?,
245                        minor: parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
246                        patch: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
247                    })
248                });
249
250                let skip = extra
251                    .skip
252                    .as_ref()
253                    .map(|v| v.iter().map(|s| Atom::from(s.as_str())).collect())
254                    .unwrap_or_default();
255
256                let parse_feature_or_module = |s: &str| -> FeatureOrModule {
257                    if let Ok(feature) = s.parse::<Feature>() {
258                        FeatureOrModule::Feature(feature)
259                    } else {
260                        FeatureOrModule::CoreJsModule(s.to_string())
261                    }
262                };
263
264                let include: Vec<FeatureOrModule> = extra
265                    .include
266                    .as_ref()
267                    .map(|v| v.iter().map(|s| parse_feature_or_module(s)).collect())
268                    .unwrap_or_default();
269
270                // Disable some ancient ES3 transforms; ReservedWords breaks resolving of
271                // some ident references.
272                let mut exclude: Vec<FeatureOrModule> = vec![
273                    FeatureOrModule::Feature(Feature::ReservedWords),
274                    FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
275                    FeatureOrModule::Feature(Feature::PropertyLiterals),
276                ];
277                if let Some(user_exclude) = &extra.exclude {
278                    for s in user_exclude {
279                        exclude.push(parse_feature_or_module(s));
280                    }
281                }
282
283                let config = swc_core::ecma::preset_env::EnvConfig::from(
284                    swc_core::ecma::preset_env::Config {
285                        targets: Some(Targets::Versions(*versions)),
286                        mode,
287                        core_js,
288                        skip,
289                        include,
290                        exclude,
291                        shipped_proposals: extra.shipped_proposals.unwrap_or(false),
292                        force_all_transforms: extra.force_all_transforms.unwrap_or(false),
293                        debug: extra.debug.unwrap_or(false),
294                        loose: extra.loose.unwrap_or(false),
295                        ..Default::default()
296                    },
297                );
298
299                // Explicit type annotation to ensure that we don't duplicate transforms in the
300                // final binary
301                apply_transform(
302                    program,
303                    helpers,
304                    preset_env::transform_from_env::<&'_ dyn Comments>(
305                        unresolved_mark,
306                        Some(&comments),
307                        config,
308                        Assumptions::default(),
309                    ),
310                )
311            }
312            EcmascriptInputTransform::TypeScript {
313                // TODO(WEB-1213)
314                use_define_for_class_fields: _use_define_for_class_fields,
315                verbatim_module_syntax,
316            } => {
317                let config = Config {
318                    verbatim_module_syntax: *verbatim_module_syntax,
319                    ..Default::default()
320                };
321                apply_transform(
322                    program,
323                    helpers,
324                    typescript(config, unresolved_mark, top_level_mark),
325                )
326            }
327            EcmascriptInputTransform::Decorators {
328                is_legacy,
329                is_ecma: _,
330                emit_decorators_metadata,
331                // TODO(WEB-1213)
332                use_define_for_class_fields: _use_define_for_class_fields,
333            } => {
334                use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
335                let config = Config {
336                    legacy: *is_legacy,
337                    emit_metadata: *emit_decorators_metadata,
338                    ..Default::default()
339                };
340
341                apply_transform(program, helpers, decorators(config))
342            }
343            EcmascriptInputTransform::Plugin(transform) => {
344                // We cannot pass helpers to plugins, so we return them as is
345                transform.await?.transform(program, ctx).await?;
346                helpers
347            }
348        })
349    }
350}
351
352fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
353    let helpers = Helpers::from_data(helpers);
354    HELPERS.set(&helpers, || {
355        program.mutate(op);
356    });
357    helpers.data()
358}
359
360pub fn remove_shebang(program: &mut Program) {
361    match program {
362        Program::Module(m) => {
363            m.shebang = None;
364        }
365        Program::Script(s) => {
366            s.shebang = None;
367        }
368    }
369}
370
371pub fn remove_directives(program: &mut Program) {
372    match program {
373        Program::Module(module) => {
374            let directive_count = module
375                .body
376                .iter()
377                .take_while(|i| match i {
378                    ModuleItem::Stmt(stmt) => stmt.directive_continue(),
379                    ModuleItem::ModuleDecl(_) => false,
380                })
381                .take_while(|i| match i {
382                    ModuleItem::Stmt(stmt) => match stmt {
383                        Stmt::Expr(ExprStmt { expr, .. }) => expr
384                            .as_lit()
385                            .and_then(|lit| lit.as_str())
386                            .and_then(|str| str.raw.as_ref())
387                            .is_some_and(|raw| {
388                                raw.starts_with("\"use ") || raw.starts_with("'use ")
389                            }),
390                        _ => false,
391                    },
392                    ModuleItem::ModuleDecl(_) => false,
393                })
394                .count();
395            module.body.drain(0..directive_count);
396        }
397        Program::Script(script) => {
398            let directive_count = script
399                .body
400                .iter()
401                .take_while(|stmt| stmt.directive_continue())
402                .take_while(|stmt| match stmt {
403                    Stmt::Expr(ExprStmt { expr, .. }) => expr
404                        .as_lit()
405                        .and_then(|lit| lit.as_str())
406                        .and_then(|str| str.raw.as_ref())
407                        .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
408                    _ => false,
409                })
410                .count();
411            script.body.drain(0..directive_count);
412        }
413    }
414}