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 serde::{Deserialize, Serialize};
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, Feature, FeatureOrModule, Targets},
13        transforms::{
14            base::{
15                assumptions::Assumptions,
16                helpers::{HELPERS, HelperData, Helpers},
17            },
18            react::react,
19            typescript::{Config, typescript},
20        },
21        utils::IsDirective,
22    },
23    quote,
24};
25use turbo_rcstr::{RcStr, rcstr};
26use turbo_tasks::{ResolvedVc, Vc};
27use turbo_tasks_fs::FileSystemPath;
28use turbopack_core::{
29    environment::Environment,
30    issue::{Issue, IssueSeverity, IssueSource, IssueStage, StyledString},
31    source::Source,
32};
33
34use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH};
35
36/// Additional options for SWC's preset-env, beyond the browserslist-derived
37/// targets that are already provided by the `Environment`.
38///
39/// These correspond to the fields documented at
40/// <https://swc.rs/docs/configuration/supported-browsers>.
41#[turbo_tasks::value(shared)]
42#[derive(Default, Clone, Debug)]
43pub struct PresetEnvConfig {
44    /// Polyfill injection mode (`"usage"` or `"entry"`), matching Babel's
45    /// `useBuiltIns`.
46    pub mode: Option<RcStr>,
47    /// The core-js version string (e.g. `"3.38"`).
48    pub core_js: Option<RcStr>,
49    /// Core-js modules or SWC transform passes to skip.
50    pub skip: Option<Vec<RcStr>>,
51    /// Core-js modules or SWC transform passes to always include.
52    pub include: Option<Vec<RcStr>>,
53    /// Core-js modules or SWC transform passes to always exclude.
54    pub exclude: Option<Vec<RcStr>>,
55    /// Enable shipped TC39 proposals.
56    pub shipped_proposals: Option<bool>,
57    /// Force all transforms regardless of targets.
58    pub force_all_transforms: Option<bool>,
59    /// Enable debug output.
60    pub debug: Option<bool>,
61    /// Enable loose mode for transforms.
62    pub loose: Option<bool>,
63}
64
65#[turbo_tasks::value]
66#[derive(Debug, Clone, Hash)]
67pub enum EcmascriptInputTransform {
68    Plugin(ResolvedVc<TransformPlugin>),
69    PresetEnv(ResolvedVc<Environment>, ResolvedVc<PresetEnvConfig>),
70    React {
71        development: bool,
72        refresh: bool,
73        // swc.jsc.transform.react.importSource
74        import_source: ResolvedVc<Option<RcStr>>,
75        // swc.jsc.transform.react.runtime,
76        runtime: ResolvedVc<Option<RcStr>>,
77    },
78    // These options are subset of swc_core::ecma::transforms::typescript::Config, but
79    // it doesn't derive `Copy` so repeating values in here
80    TypeScript {
81        use_define_for_class_fields: bool,
82        verbatim_module_syntax: bool,
83    },
84    Decorators {
85        is_legacy: bool,
86        is_ecma: bool,
87        emit_decorators_metadata: bool,
88        use_define_for_class_fields: bool,
89    },
90    ReactCompilerRust {
91        compilation_mode: ReactCompilerCompilationMode,
92        target: ReactCompilerTarget,
93    },
94}
95
96#[turbo_tasks::value(shared, operation)]
97#[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub enum ReactCompilerCompilationMode {
100    #[default]
101    Infer,
102    Annotation,
103    All,
104}
105
106impl ReactCompilerCompilationMode {
107    pub fn as_str(self) -> &'static str {
108        match self {
109            ReactCompilerCompilationMode::Infer => "infer",
110            ReactCompilerCompilationMode::Annotation => "annotation",
111            ReactCompilerCompilationMode::All => "all",
112        }
113    }
114}
115
116#[turbo_tasks::value(transparent)]
117pub struct OptionReactCompilerCompilationMode(Option<ReactCompilerCompilationMode>);
118
119#[turbo_tasks::value(shared, operation)]
120#[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)]
121pub enum ReactCompilerTarget {
122    #[default]
123    #[serde(rename = "19")]
124    React19,
125    #[serde(rename = "18")]
126    React18,
127}
128
129impl ReactCompilerTarget {
130    pub fn as_str(self) -> &'static str {
131        match self {
132            ReactCompilerTarget::React19 => "19",
133            ReactCompilerTarget::React18 => "18",
134        }
135    }
136}
137
138/// The CustomTransformer trait allows you to implement your own custom SWC
139/// transformer to run over all ECMAScript files imported in the graph.
140#[async_trait]
141pub trait CustomTransformer: Debug {
142    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
143}
144
145/// A wrapper around a TransformPlugin instance, allowing it to operate with
146/// the turbo_task caching requirements.
147#[turbo_tasks::value(transparent, serialization = "skip", eq = "manual", cell = "new")]
148#[derive(Debug)]
149pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
150
151#[async_trait]
152impl CustomTransformer for TransformPlugin {
153    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
154        self.0.transform(program, ctx).await
155    }
156}
157
158#[turbo_tasks::value(transparent)]
159#[derive(Debug, Clone, Hash)]
160pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
161
162#[turbo_tasks::value_impl]
163impl EcmascriptInputTransforms {
164    #[turbo_tasks::function]
165    pub fn empty() -> Vc<Self> {
166        Vc::cell(Vec::new())
167    }
168
169    #[turbo_tasks::function]
170    pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
171        let mut transforms = self.owned().await?;
172        transforms.extend(other.owned().await?);
173        Ok(Vc::cell(transforms))
174    }
175}
176
177pub struct TransformContext<'a> {
178    pub comments: &'a SwcComments,
179    pub top_level_mark: Mark,
180    pub unresolved_mark: Mark,
181    pub source_map: &'a Arc<SourceMap>,
182    pub file_path_str: &'a str,
183    pub file_name_str: &'a str,
184    pub file_name_hash: u128,
185    pub query_str: RcStr,
186    pub file_path: FileSystemPath,
187    pub source: ResolvedVc<Box<dyn Source>>,
188    /// Original source text; used by transforms that need the raw text (e.g.
189    /// `swc_ecma_react_compiler`).
190    pub source_text: &'a str,
191    /// The value of `process.env.NODE_ENV` for this compilation
192    /// (e.g. `"development"` or `"production"`).
193    pub node_env: RcStr,
194}
195
196impl EcmascriptInputTransform {
197    pub async fn apply(
198        &self,
199        program: &mut Program,
200        ctx: &TransformContext<'_>,
201        helpers: HelperData,
202    ) -> Result<HelperData> {
203        let &TransformContext {
204            comments,
205            source_map,
206            top_level_mark,
207            unresolved_mark,
208            ..
209        } = ctx;
210
211        Ok(match self {
212            EcmascriptInputTransform::React {
213                development,
214                refresh,
215                import_source,
216                runtime,
217            } => {
218                use swc_core::ecma::transforms::react::{Options, Runtime};
219                let runtime = if let Some(runtime) = &*runtime.await? {
220                    match runtime.as_str() {
221                        "classic" => Runtime::Classic,
222                        "automatic" => Runtime::Automatic,
223                        _ => {
224                            bail!(
225                                "Invalid value for swc.jsc.transform.react.runtime: {}",
226                                runtime
227                            );
228                        }
229                    }
230                } else {
231                    Runtime::Automatic
232                };
233
234                let config = Options {
235                    runtime: Some(runtime),
236                    development: Some(*development),
237                    import_source: import_source.await?.as_deref().map(Atom::from),
238                    refresh: if *refresh {
239                        debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
240                        Some(swc_core::ecma::transforms::react::RefreshOptions {
241                            refresh_reg: atom!("__turbopack_context__.k.register"),
242                            refresh_sig: atom!("__turbopack_context__.k.signature"),
243                            ..Default::default()
244                        })
245                    } else {
246                        None
247                    },
248                    ..Default::default()
249                };
250
251                // Explicit type annotation to ensure that we don't duplicate transforms in the
252                // final binary
253                let helpers = apply_transform(
254                    program,
255                    helpers,
256                    react::<&dyn Comments>(
257                        source_map.clone(),
258                        Some(&comments),
259                        config,
260                        top_level_mark,
261                        unresolved_mark,
262                    ),
263                );
264
265                if *refresh {
266                    debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
267                    debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
268                    let stmt = quote!(
269                        // No-JS mode does not inject these helpers
270                        "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
271                         globalThis.$RefreshHelpers !== null) { \
272                         __turbopack_context__.k.registerExports(__turbopack_context__.m, \
273                         globalThis.$RefreshHelpers$); }" as Stmt
274                    );
275
276                    match program {
277                        Program::Module(module) => {
278                            module.body.push(ModuleItem::Stmt(stmt));
279                        }
280                        Program::Script(script) => {
281                            script.body.push(stmt);
282                        }
283                    }
284                }
285
286                helpers
287            }
288            EcmascriptInputTransform::PresetEnv(env, preset_env_config) => {
289                let versions = env.runtime_versions().await?;
290                let extra = preset_env_config.await?;
291
292                let mode = match extra.mode.as_deref() {
293                    Some("usage") => Some(preset_env::Mode::Usage),
294                    Some("entry") => Some(preset_env::Mode::Entry),
295                    _ => None,
296                };
297
298                let core_js = extra.core_js.as_ref().and_then(|v| {
299                    let parts: Vec<&str> = v.split('.').collect();
300                    Some(preset_env::Version {
301                        major: parts.first()?.parse().ok()?,
302                        minor: parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
303                        patch: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
304                    })
305                });
306
307                let skip = extra
308                    .skip
309                    .as_ref()
310                    .map(|v| v.iter().map(|s| Atom::from(s.as_str())).collect())
311                    .unwrap_or_default();
312
313                let parse_feature_or_module = |s: &str| -> FeatureOrModule {
314                    if let Ok(feature) = s.parse::<Feature>() {
315                        FeatureOrModule::Feature(feature)
316                    } else {
317                        FeatureOrModule::CoreJsModule(s.to_string())
318                    }
319                };
320
321                let include: Vec<FeatureOrModule> = extra
322                    .include
323                    .as_ref()
324                    .map(|v| v.iter().map(|s| parse_feature_or_module(s)).collect())
325                    .unwrap_or_default();
326
327                // Disable some ancient ES3 transforms; ReservedWords breaks resolving of
328                // some ident references.
329                let mut exclude: Vec<FeatureOrModule> = vec![
330                    FeatureOrModule::Feature(Feature::ReservedWords),
331                    FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
332                    FeatureOrModule::Feature(Feature::PropertyLiterals),
333                ];
334                if let Some(user_exclude) = &extra.exclude {
335                    for s in user_exclude {
336                        exclude.push(parse_feature_or_module(s));
337                    }
338                }
339
340                let config = swc_core::ecma::preset_env::EnvConfig::from(
341                    swc_core::ecma::preset_env::Config {
342                        targets: Some(Targets::Versions(*versions)),
343                        mode,
344                        core_js,
345                        skip,
346                        include,
347                        exclude,
348                        shipped_proposals: extra.shipped_proposals.unwrap_or(false),
349                        force_all_transforms: extra.force_all_transforms.unwrap_or(false),
350                        debug: extra.debug.unwrap_or(false),
351                        loose: extra.loose.unwrap_or(false),
352                        ..Default::default()
353                    },
354                );
355
356                // Explicit type annotation to ensure that we don't duplicate transforms in the
357                // final binary
358                apply_transform(
359                    program,
360                    helpers,
361                    preset_env::transform_from_env::<&'_ dyn Comments>(
362                        unresolved_mark,
363                        Some(&comments),
364                        config,
365                        Assumptions::default(),
366                    ),
367                )
368            }
369            EcmascriptInputTransform::TypeScript {
370                // TODO(WEB-1213)
371                use_define_for_class_fields: _use_define_for_class_fields,
372                verbatim_module_syntax,
373            } => {
374                let config = Config {
375                    verbatim_module_syntax: *verbatim_module_syntax,
376                    ..Default::default()
377                };
378                apply_transform(
379                    program,
380                    helpers,
381                    typescript(config, unresolved_mark, top_level_mark),
382                )
383            }
384            EcmascriptInputTransform::Decorators {
385                is_legacy,
386                is_ecma: _,
387                emit_decorators_metadata,
388                // TODO(WEB-1213)
389                use_define_for_class_fields: _use_define_for_class_fields,
390            } => {
391                use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
392                let config = Config {
393                    legacy: *is_legacy,
394                    emit_metadata: *emit_decorators_metadata,
395                    ..Default::default()
396                };
397
398                apply_transform(program, helpers, decorators(config))
399            }
400            EcmascriptInputTransform::ReactCompilerRust {
401                compilation_mode,
402                target,
403            } => {
404                apply_rust_react_compiler(program, ctx, helpers, *compilation_mode, *target).await?
405            }
406            EcmascriptInputTransform::Plugin(transform) => {
407                // We cannot pass helpers to plugins, so we return them as is
408                transform.await?.transform(program, ctx).await?;
409                helpers
410            }
411        })
412    }
413}
414
415#[turbo_tasks::value]
416struct ReactCompilerIssue {
417    source: IssueSource,
418    message: RcStr,
419    severity: IssueSeverity,
420}
421
422#[async_trait]
423#[turbo_tasks::value_impl]
424impl Issue for ReactCompilerIssue {
425    fn severity(&self) -> IssueSeverity {
426        self.severity
427    }
428
429    async fn file_path(&self) -> anyhow::Result<FileSystemPath> {
430        self.source.file_path().await
431    }
432
433    fn source(&self) -> Option<IssueSource> {
434        Some(self.source)
435    }
436
437    fn stage(&self) -> IssueStage {
438        IssueStage::Transform
439    }
440
441    async fn title(&self) -> anyhow::Result<StyledString> {
442        Ok(StyledString::Text(rcstr!("React Compiler")))
443    }
444
445    async fn description(&self) -> anyhow::Result<Option<StyledString>> {
446        Ok(Some(StyledString::Text(self.message.clone())))
447    }
448}
449
450async fn apply_rust_react_compiler(
451    program: &mut Program,
452    ctx: &TransformContext<'_>,
453    helpers: HelperData,
454    compilation_mode: ReactCompilerCompilationMode,
455    target: ReactCompilerTarget,
456) -> Result<HelperData> {
457    let Program::Module(_) = program else {
458        return Ok(helpers);
459    };
460
461    let single_threaded_comments =
462        crate::swc_comments::swc_comments_to_single_threaded(ctx.comments);
463    let result = swc_ecma_react_compiler::transform(
464        program,
465        swc_ecma_react_compiler::SourceType::from_program(program),
466        ctx.source_text,
467        Some(&single_threaded_comments),
468        react_compiler_options(ctx, compilation_mode, target),
469    );
470
471    // TODO: Emit these diagnostics with an Info level once there's a way of adjusting log levels in
472    //       general. By default React Compiler is silent, as de-opts align closely with feedback
473    //       from tools like React's lint rules.
474
475    if let Some(compiled_program) = result.program {
476        *program = compiled_program;
477
478        // TODO(react-compiler-swc): The Rust React Compiler emits every identifier with
479        // `SyntaxContext::empty()` in `convert_ast_reverse.rs`.
480        //
481        // Remove this once `swc_ecma_react_compiler`
482        // preserves/assigns contexts on the converted AST.
483        program.mutate(swc_core::ecma::transforms::base::resolver(
484            ctx.unresolved_mark,
485            ctx.top_level_mark,
486            true,
487        ));
488    }
489
490    Ok(helpers)
491}
492
493fn react_compiler_options(
494    ctx: &TransformContext<'_>,
495    compilation_mode: ReactCompilerCompilationMode,
496    target: ReactCompilerTarget,
497) -> react_compiler::entrypoint::plugin_options::PluginOptions {
498    use react_compiler::entrypoint::plugin_options::{CompilerTarget, PluginOptions};
499
500    PluginOptions {
501        should_compile: true,
502        enable_reanimated: false,
503        is_dev: ctx.node_env != "production",
504        filename: Some(ctx.file_name_str.to_string()),
505        compilation_mode: compilation_mode.as_str().to_string(),
506        panic_threshold: "none".to_string(),
507        target: CompilerTarget::Version(target.as_str().to_string()),
508        gating: None,
509        dynamic_gating: None,
510        no_emit: false,
511        output_mode: None,
512        eslint_suppression_rules: None,
513        flow_suppressions: false,
514        ignore_use_no_forget: false,
515        custom_opt_out_directives: None,
516        environment: Default::default(),
517        source_code: None,
518        profiling: false,
519        debug: false,
520    }
521}
522
523fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
524    let helpers = Helpers::from_data(helpers);
525    HELPERS.set(&helpers, || {
526        program.mutate(op);
527    });
528    helpers.data()
529}
530
531pub fn remove_shebang(program: &mut Program) {
532    match program {
533        Program::Module(m) => {
534            m.shebang = None;
535        }
536        Program::Script(s) => {
537            s.shebang = None;
538        }
539    }
540}
541
542pub fn remove_directives(program: &mut Program) {
543    match program {
544        Program::Module(module) => {
545            let directive_count = module
546                .body
547                .iter()
548                .take_while(|i| match i {
549                    ModuleItem::Stmt(stmt) => stmt.directive_continue(),
550                    ModuleItem::ModuleDecl(_) => false,
551                })
552                .take_while(|i| match i {
553                    ModuleItem::Stmt(stmt) => match stmt {
554                        Stmt::Expr(ExprStmt { expr, .. }) => expr
555                            .as_lit()
556                            .and_then(|lit| lit.as_str())
557                            .and_then(|str| str.raw.as_ref())
558                            .is_some_and(|raw| {
559                                raw.starts_with("\"use ") || raw.starts_with("'use ")
560                            }),
561                        _ => false,
562                    },
563                    ModuleItem::ModuleDecl(_) => false,
564                })
565                .count();
566            module.body.drain(0..directive_count);
567        }
568        Program::Script(script) => {
569            let directive_count = script
570                .body
571                .iter()
572                .take_while(|stmt| stmt.directive_continue())
573                .take_while(|stmt| match stmt {
574                    Stmt::Expr(ExprStmt { expr, .. }) => expr
575                        .as_lit()
576                        .and_then(|lit| lit.as_str())
577                        .and_then(|str| str.raw.as_ref())
578                        .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
579                    _ => false,
580                })
581                .count();
582            script.body.drain(0..directive_count);
583        }
584    }
585}