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 = "skip", 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    /// The value of `process.env.NODE_ENV` for this compilation
138    /// (e.g. `"development"` or `"production"`).
139    pub node_env: RcStr,
140}
141
142impl EcmascriptInputTransform {
143    pub async fn apply(
144        &self,
145        program: &mut Program,
146        ctx: &TransformContext<'_>,
147        helpers: HelperData,
148    ) -> Result<HelperData> {
149        let &TransformContext {
150            comments,
151            source_map,
152            top_level_mark,
153            unresolved_mark,
154            ..
155        } = ctx;
156
157        Ok(match self {
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                            bail!(
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                        // 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, preset_env_config) => {
235                let versions = env.runtime_versions().await?;
236                let extra = preset_env_config.await?;
237
238                let mode = match extra.mode.as_deref() {
239                    Some("usage") => Some(preset_env::Mode::Usage),
240                    Some("entry") => Some(preset_env::Mode::Entry),
241                    _ => None,
242                };
243
244                let core_js = extra.core_js.as_ref().and_then(|v| {
245                    let parts: Vec<&str> = v.split('.').collect();
246                    Some(preset_env::Version {
247                        major: parts.first()?.parse().ok()?,
248                        minor: parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
249                        patch: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
250                    })
251                });
252
253                let skip = extra
254                    .skip
255                    .as_ref()
256                    .map(|v| v.iter().map(|s| Atom::from(s.as_str())).collect())
257                    .unwrap_or_default();
258
259                let parse_feature_or_module = |s: &str| -> FeatureOrModule {
260                    if let Ok(feature) = s.parse::<Feature>() {
261                        FeatureOrModule::Feature(feature)
262                    } else {
263                        FeatureOrModule::CoreJsModule(s.to_string())
264                    }
265                };
266
267                let include: Vec<FeatureOrModule> = extra
268                    .include
269                    .as_ref()
270                    .map(|v| v.iter().map(|s| parse_feature_or_module(s)).collect())
271                    .unwrap_or_default();
272
273                // Disable some ancient ES3 transforms; ReservedWords breaks resolving of
274                // some ident references.
275                let mut exclude: Vec<FeatureOrModule> = vec![
276                    FeatureOrModule::Feature(Feature::ReservedWords),
277                    FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
278                    FeatureOrModule::Feature(Feature::PropertyLiterals),
279                ];
280                if let Some(user_exclude) = &extra.exclude {
281                    for s in user_exclude {
282                        exclude.push(parse_feature_or_module(s));
283                    }
284                }
285
286                let config = swc_core::ecma::preset_env::EnvConfig::from(
287                    swc_core::ecma::preset_env::Config {
288                        targets: Some(Targets::Versions(*versions)),
289                        mode,
290                        core_js,
291                        skip,
292                        include,
293                        exclude,
294                        shipped_proposals: extra.shipped_proposals.unwrap_or(false),
295                        force_all_transforms: extra.force_all_transforms.unwrap_or(false),
296                        debug: extra.debug.unwrap_or(false),
297                        loose: extra.loose.unwrap_or(false),
298                        ..Default::default()
299                    },
300                );
301
302                // Explicit type annotation to ensure that we don't duplicate transforms in the
303                // final binary
304                apply_transform(
305                    program,
306                    helpers,
307                    preset_env::transform_from_env::<&'_ dyn Comments>(
308                        unresolved_mark,
309                        Some(&comments),
310                        config,
311                        Assumptions::default(),
312                    ),
313                )
314            }
315            EcmascriptInputTransform::TypeScript {
316                // TODO(WEB-1213)
317                use_define_for_class_fields: _use_define_for_class_fields,
318                verbatim_module_syntax,
319            } => {
320                let config = Config {
321                    verbatim_module_syntax: *verbatim_module_syntax,
322                    ..Default::default()
323                };
324                apply_transform(
325                    program,
326                    helpers,
327                    typescript(config, unresolved_mark, top_level_mark),
328                )
329            }
330            EcmascriptInputTransform::Decorators {
331                is_legacy,
332                is_ecma: _,
333                emit_decorators_metadata,
334                // TODO(WEB-1213)
335                use_define_for_class_fields: _use_define_for_class_fields,
336            } => {
337                use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
338                let config = Config {
339                    legacy: *is_legacy,
340                    emit_metadata: *emit_decorators_metadata,
341                    ..Default::default()
342                };
343
344                apply_transform(program, helpers, decorators(config))
345            }
346            EcmascriptInputTransform::Plugin(transform) => {
347                // We cannot pass helpers to plugins, so we return them as is
348                transform.await?.transform(program, ctx).await?;
349                helpers
350            }
351        })
352    }
353}
354
355fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
356    let helpers = Helpers::from_data(helpers);
357    HELPERS.set(&helpers, || {
358        program.mutate(op);
359    });
360    helpers.data()
361}
362
363pub fn remove_shebang(program: &mut Program) {
364    match program {
365        Program::Module(m) => {
366            m.shebang = None;
367        }
368        Program::Script(s) => {
369            s.shebang = None;
370        }
371    }
372}
373
374pub fn remove_directives(program: &mut Program) {
375    match program {
376        Program::Module(module) => {
377            let directive_count = module
378                .body
379                .iter()
380                .take_while(|i| match i {
381                    ModuleItem::Stmt(stmt) => stmt.directive_continue(),
382                    ModuleItem::ModuleDecl(_) => false,
383                })
384                .take_while(|i| match i {
385                    ModuleItem::Stmt(stmt) => match stmt {
386                        Stmt::Expr(ExprStmt { expr, .. }) => expr
387                            .as_lit()
388                            .and_then(|lit| lit.as_str())
389                            .and_then(|str| str.raw.as_ref())
390                            .is_some_and(|raw| {
391                                raw.starts_with("\"use ") || raw.starts_with("'use ")
392                            }),
393                        _ => false,
394                    },
395                    ModuleItem::ModuleDecl(_) => false,
396                })
397                .count();
398            module.body.drain(0..directive_count);
399        }
400        Program::Script(script) => {
401            let directive_count = script
402                .body
403                .iter()
404                .take_while(|stmt| stmt.directive_continue())
405                .take_while(|stmt| match stmt {
406                    Stmt::Expr(ExprStmt { expr, .. }) => expr
407                        .as_lit()
408                        .and_then(|lit| lit.as_str())
409                        .and_then(|str| str.raw.as_ref())
410                        .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
411                    _ => false,
412                })
413                .count();
414            script.body.drain(0..directive_count);
415        }
416    }
417}