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::{
12            ArrowExpr, ArrowFunctionBody, Expr, ExprStmt, Function, Lit, ModuleItem, Pass, Program,
13            Stmt,
14        },
15        preset_env::{self, Feature, FeatureOrModule, Targets},
16        transforms::{
17            base::{
18                assumptions::Assumptions,
19                helpers::{HELPERS, HelperData, Helpers},
20            },
21            react::react,
22            typescript::{Config, typescript},
23        },
24        utils::IsDirective,
25        visit::{Visit, VisitWith},
26    },
27    quote,
28};
29use turbo_rcstr::{RcStr, rcstr};
30use turbo_tasks::{ResolvedVc, Vc};
31use turbo_tasks_fs::FileSystemPath;
32use turbopack_core::{
33    environment::Environment,
34    issue::{Issue, IssueSeverity, IssueSource, IssueStage, StyledString},
35    source::Source,
36};
37
38use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH};
39
40/// Additional options for SWC's preset-env, beyond the browserslist-derived
41/// targets that are already provided by the `Environment`.
42///
43/// These correspond to the fields documented at
44/// <https://swc.rs/docs/configuration/supported-browsers>.
45#[turbo_tasks::value(shared)]
46#[derive(Default, Clone, Debug)]
47pub struct PresetEnvConfig {
48    /// Polyfill injection mode (`"usage"` or `"entry"`), matching Babel's
49    /// `useBuiltIns`.
50    pub mode: Option<RcStr>,
51    /// The core-js version string (e.g. `"3.38"`).
52    pub core_js: Option<RcStr>,
53    /// Core-js modules or SWC transform passes to skip.
54    pub skip: Option<Vec<RcStr>>,
55    /// Core-js modules or SWC transform passes to always include.
56    pub include: Option<Vec<RcStr>>,
57    /// Core-js modules or SWC transform passes to always exclude.
58    pub exclude: Option<Vec<RcStr>>,
59    /// Enable shipped TC39 proposals.
60    pub shipped_proposals: Option<bool>,
61    /// Force all transforms regardless of targets.
62    pub force_all_transforms: Option<bool>,
63    /// Enable debug output.
64    pub debug: Option<bool>,
65    /// Enable loose mode for transforms.
66    pub loose: Option<bool>,
67}
68
69#[turbo_tasks::value]
70#[derive(Debug, Clone, Hash)]
71pub enum EcmascriptInputTransform {
72    Plugin(ResolvedVc<TransformPlugin>),
73    PresetEnv(ResolvedVc<Environment>, ResolvedVc<PresetEnvConfig>),
74    React {
75        development: bool,
76        refresh: bool,
77        // swc.jsc.transform.react.importSource
78        import_source: ResolvedVc<Option<RcStr>>,
79        // swc.jsc.transform.react.runtime,
80        runtime: ResolvedVc<Option<RcStr>>,
81    },
82    // These options are subset of swc_core::ecma::transforms::typescript::Config, but
83    // it doesn't derive `Copy` so repeating values in here
84    TypeScript {
85        use_define_for_class_fields: bool,
86        verbatim_module_syntax: bool,
87    },
88    Decorators {
89        is_legacy: bool,
90        is_ecma: bool,
91        emit_decorators_metadata: bool,
92        use_define_for_class_fields: bool,
93    },
94    ReactCompilerRust {
95        options: ReactCompilerTransformOptions,
96        target: ReactCompilerTarget,
97    },
98}
99
100#[turbo_tasks::value(shared, operation)]
101#[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ReactCompilerTransformOptions {
104    #[serde(default)]
105    pub compilation_mode: ReactCompilerCompilationMode,
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub enable_preserve_existing_memoization_guarantees: Option<bool>,
108}
109
110#[turbo_tasks::value(shared, operation)]
111#[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub enum ReactCompilerCompilationMode {
114    #[default]
115    Infer,
116    Annotation,
117    All,
118}
119
120impl ReactCompilerCompilationMode {
121    pub fn as_str(self) -> &'static str {
122        match self {
123            ReactCompilerCompilationMode::Infer => "infer",
124            ReactCompilerCompilationMode::Annotation => "annotation",
125            ReactCompilerCompilationMode::All => "all",
126        }
127    }
128}
129
130#[turbo_tasks::value(transparent)]
131pub struct OptionReactCompilerTransformOptions(Option<ReactCompilerTransformOptions>);
132
133#[turbo_tasks::value(shared, operation)]
134#[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)]
135pub enum ReactCompilerTarget {
136    #[default]
137    #[serde(rename = "19")]
138    React19,
139    #[serde(rename = "18")]
140    React18,
141}
142
143impl ReactCompilerTarget {
144    pub fn as_str(self) -> &'static str {
145        match self {
146            ReactCompilerTarget::React19 => "19",
147            ReactCompilerTarget::React18 => "18",
148        }
149    }
150}
151
152/// The CustomTransformer trait allows you to implement your own custom SWC
153/// transformer to run over all ECMAScript files imported in the graph.
154#[async_trait]
155pub trait CustomTransformer: Debug {
156    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
157}
158
159/// A wrapper around a TransformPlugin instance, allowing it to operate with
160/// the turbo_task caching requirements.
161#[turbo_tasks::value(transparent, serialization = "skip", eq = "manual", cell = "new")]
162#[derive(Debug)]
163pub struct TransformPlugin(#[turbo_tasks(unsafe_ignore)] Box<dyn CustomTransformer + Send + Sync>);
164
165#[async_trait]
166impl CustomTransformer for TransformPlugin {
167    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
168        self.0.transform(program, ctx).await
169    }
170}
171
172#[turbo_tasks::value(transparent)]
173#[derive(Debug, Clone, Hash)]
174pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
175
176#[turbo_tasks::value_impl]
177impl EcmascriptInputTransforms {
178    #[turbo_tasks::function]
179    pub fn empty() -> Vc<Self> {
180        Vc::cell(Vec::new())
181    }
182
183    #[turbo_tasks::function]
184    pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
185        let mut transforms = self.owned().await?;
186        transforms.extend(other.owned().await?);
187        Ok(Vc::cell(transforms))
188    }
189}
190
191pub struct TransformContext<'a> {
192    pub comments: &'a SwcComments,
193    pub top_level_mark: Mark,
194    pub unresolved_mark: Mark,
195    pub source_map: &'a Arc<SourceMap>,
196    pub file_path_str: &'a str,
197    pub file_name_str: &'a str,
198    pub file_name_hash: u128,
199    pub query_str: RcStr,
200    pub file_path: FileSystemPath,
201    pub source: ResolvedVc<Box<dyn Source>>,
202    /// Original source text; used by transforms that need the raw text (e.g.
203    /// `swc_ecma_react_compiler`).
204    pub source_text: &'a str,
205    /// The value of `process.env.NODE_ENV` for this compilation
206    /// (e.g. `"development"` or `"production"`).
207    pub node_env: RcStr,
208}
209
210impl EcmascriptInputTransform {
211    pub async fn apply(
212        &self,
213        program: &mut Program,
214        ctx: &TransformContext<'_>,
215        helpers: HelperData,
216    ) -> Result<HelperData> {
217        let &TransformContext {
218            comments,
219            source_map,
220            top_level_mark,
221            unresolved_mark,
222            ..
223        } = ctx;
224
225        Ok(match self {
226            EcmascriptInputTransform::React {
227                development,
228                refresh,
229                import_source,
230                runtime,
231            } => {
232                use swc_core::ecma::transforms::react::{Options, Runtime};
233                let runtime = if let Some(runtime) = &*runtime.await? {
234                    match runtime.as_str() {
235                        "classic" => Runtime::Classic,
236                        "automatic" => Runtime::Automatic,
237                        _ => {
238                            bail!(
239                                "Invalid value for swc.jsc.transform.react.runtime: {}",
240                                runtime
241                            );
242                        }
243                    }
244                } else {
245                    Runtime::Automatic
246                };
247
248                let config = Options {
249                    runtime: Some(runtime),
250                    development: Some(*development),
251                    import_source: import_source.await?.as_deref().map(Atom::from),
252                    refresh: if *refresh {
253                        debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
254                        Some(swc_core::ecma::transforms::react::RefreshOptions {
255                            refresh_reg: atom!("__turbopack_context__.k.register"),
256                            refresh_sig: atom!("__turbopack_context__.k.signature"),
257                            ..Default::default()
258                        })
259                    } else {
260                        None
261                    },
262                    ..Default::default()
263                };
264
265                // Explicit type annotation to ensure that we don't duplicate transforms in the
266                // final binary
267                let helpers = apply_transform(
268                    program,
269                    helpers,
270                    react::<&dyn Comments>(
271                        source_map.clone(),
272                        Some(&comments),
273                        config,
274                        top_level_mark,
275                        unresolved_mark,
276                    ),
277                );
278
279                if *refresh {
280                    debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
281                    debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
282                    let stmt = quote!(
283                        // No-JS mode does not inject these helpers
284                        "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
285                         globalThis.$RefreshHelpers !== null) { \
286                         __turbopack_context__.k.registerExports(__turbopack_context__.m, \
287                         globalThis.$RefreshHelpers$); }" as Stmt
288                    );
289
290                    match program {
291                        Program::Module(module) => {
292                            module.body.push(ModuleItem::Stmt(stmt));
293                        }
294                        Program::Script(script) => {
295                            script.body.push(stmt);
296                        }
297                    }
298                }
299
300                helpers
301            }
302            EcmascriptInputTransform::PresetEnv(env, preset_env_config) => {
303                let versions = env.runtime_versions().await?;
304                let extra = preset_env_config.await?;
305
306                let mode = match extra.mode.as_deref() {
307                    Some("usage") => Some(preset_env::Mode::Usage),
308                    Some("entry") => Some(preset_env::Mode::Entry),
309                    _ => None,
310                };
311
312                let core_js = extra.core_js.as_ref().and_then(|v| {
313                    let parts: Vec<&str> = v.split('.').collect();
314                    Some(preset_env::Version {
315                        major: parts.first()?.parse().ok()?,
316                        minor: parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
317                        patch: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
318                    })
319                });
320
321                let skip = extra
322                    .skip
323                    .as_ref()
324                    .map(|v| v.iter().map(|s| Atom::from(s.as_str())).collect())
325                    .unwrap_or_default();
326
327                let parse_feature_or_module = |s: &str| -> FeatureOrModule {
328                    if let Ok(feature) = s.parse::<Feature>() {
329                        FeatureOrModule::Feature(feature)
330                    } else {
331                        FeatureOrModule::CoreJsModule(s.to_string())
332                    }
333                };
334
335                let include: Vec<FeatureOrModule> = extra
336                    .include
337                    .as_ref()
338                    .map(|v| v.iter().map(|s| parse_feature_or_module(s)).collect())
339                    .unwrap_or_default();
340
341                // Disable some ancient ES3 transforms; ReservedWords breaks resolving of
342                // some ident references.
343                let mut exclude: Vec<FeatureOrModule> = vec![
344                    FeatureOrModule::Feature(Feature::ReservedWords),
345                    FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
346                    FeatureOrModule::Feature(Feature::PropertyLiterals),
347                ];
348                if let Some(user_exclude) = &extra.exclude {
349                    for s in user_exclude {
350                        exclude.push(parse_feature_or_module(s));
351                    }
352                }
353
354                let config = swc_core::ecma::preset_env::EnvConfig::from(
355                    swc_core::ecma::preset_env::Config {
356                        targets: Some(Targets::Versions(*versions)),
357                        mode,
358                        core_js,
359                        skip,
360                        include,
361                        exclude,
362                        shipped_proposals: extra.shipped_proposals.unwrap_or(false),
363                        force_all_transforms: extra.force_all_transforms.unwrap_or(false),
364                        debug: extra.debug.unwrap_or(false),
365                        loose: extra.loose.unwrap_or(false),
366                        ..Default::default()
367                    },
368                );
369
370                // Explicit type annotation to ensure that we don't duplicate transforms in the
371                // final binary
372                apply_transform(
373                    program,
374                    helpers,
375                    preset_env::transform_from_env::<&'_ dyn Comments>(
376                        unresolved_mark,
377                        Some(&comments),
378                        config,
379                        Assumptions::default(),
380                    ),
381                )
382            }
383            EcmascriptInputTransform::TypeScript {
384                // TODO(WEB-1213)
385                use_define_for_class_fields: _use_define_for_class_fields,
386                verbatim_module_syntax,
387            } => {
388                let config = Config {
389                    verbatim_module_syntax: *verbatim_module_syntax,
390                    ..Default::default()
391                };
392                apply_transform(
393                    program,
394                    helpers,
395                    typescript(config, unresolved_mark, top_level_mark),
396                )
397            }
398            EcmascriptInputTransform::Decorators {
399                is_legacy,
400                is_ecma: _,
401                emit_decorators_metadata,
402                // TODO(WEB-1213)
403                use_define_for_class_fields: _use_define_for_class_fields,
404            } => {
405                use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
406                let config = Config {
407                    legacy: *is_legacy,
408                    emit_metadata: *emit_decorators_metadata,
409                    ..Default::default()
410                };
411
412                apply_transform(program, helpers, decorators(config))
413            }
414            EcmascriptInputTransform::ReactCompilerRust { options, target } => {
415                apply_rust_react_compiler(program, ctx, helpers, *options, *target).await?
416            }
417            EcmascriptInputTransform::Plugin(transform) => {
418                // We cannot pass helpers to plugins, so we return them as is
419                transform.await?.transform(program, ctx).await?;
420                helpers
421            }
422        })
423    }
424}
425
426#[turbo_tasks::value]
427struct ReactCompilerIssue {
428    source: IssueSource,
429    message: RcStr,
430    severity: IssueSeverity,
431}
432
433#[async_trait]
434#[turbo_tasks::value_impl]
435impl Issue for ReactCompilerIssue {
436    fn severity(&self) -> IssueSeverity {
437        self.severity
438    }
439
440    async fn file_path(&self) -> anyhow::Result<FileSystemPath> {
441        self.source.file_path().await
442    }
443
444    fn source(&self) -> Option<IssueSource> {
445        Some(self.source)
446    }
447
448    fn stage(&self) -> IssueStage {
449        IssueStage::Transform
450    }
451
452    async fn title(&self) -> anyhow::Result<StyledString> {
453        Ok(StyledString::Text(rcstr!("React Compiler")))
454    }
455
456    async fn description(&self) -> anyhow::Result<Option<StyledString>> {
457        Ok(Some(StyledString::Text(self.message.clone())))
458    }
459}
460
461// Keep this in sync with React Compiler's annotation-mode opt-ins. Next.js does not configure
462// `dynamic_gating`, so only the standard `use memo` and legacy `use forget` directives enable a
463// function.
464fn has_react_compiler_opt_in_directive(statements: &[Stmt]) -> bool {
465    for statement in statements {
466        if !statement.directive_continue() {
467            break;
468        }
469
470        let Stmt::Expr(expression) = statement else {
471            continue;
472        };
473        let Expr::Lit(Lit::Str(value)) = &*expression.expr else {
474            continue;
475        };
476        if value
477            .value
478            .as_str()
479            .is_some_and(|value| matches!(value, "use memo" | "use forget"))
480        {
481            return true;
482        }
483    }
484
485    false
486}
487
488#[derive(Default)]
489struct ReactCompilerAnnotationFinder {
490    found: bool,
491}
492
493impl Visit for ReactCompilerAnnotationFinder {
494    fn visit_arrow_expr(&mut self, node: &ArrowExpr) {
495        if self.found {
496            return;
497        }
498        if let ArrowFunctionBody::FunctionBody(body) = &*node.body
499            && has_react_compiler_opt_in_directive(&body.stmts)
500        {
501            self.found = true;
502            return;
503        }
504
505        node.visit_children_with(self);
506    }
507
508    fn visit_function(&mut self, node: &Function) {
509        if self.found {
510            return;
511        }
512        if node
513            .body
514            .as_ref()
515            .is_some_and(|body| has_react_compiler_opt_in_directive(&body.stmts))
516        {
517            self.found = true;
518            return;
519        }
520
521        node.visit_children_with(self);
522    }
523}
524
525fn has_react_compiler_annotation(program: &Program) -> bool {
526    let mut finder = ReactCompilerAnnotationFinder::default();
527    finder.visit_program(program);
528    finder.found
529}
530
531fn should_run_rust_react_compiler(
532    program: &Program,
533    compilation_mode: ReactCompilerCompilationMode,
534) -> bool {
535    match compilation_mode {
536        ReactCompilerCompilationMode::Infer => {
537            swc_ecma_react_compiler::fast_check::is_required(program)
538        }
539        ReactCompilerCompilationMode::Annotation => has_react_compiler_annotation(program),
540        ReactCompilerCompilationMode::All => true,
541    }
542}
543
544async fn apply_rust_react_compiler(
545    program: &mut Program,
546    ctx: &TransformContext<'_>,
547    helpers: HelperData,
548    options: ReactCompilerTransformOptions,
549    target: ReactCompilerTarget,
550) -> Result<HelperData> {
551    let Program::Module(_) = program else {
552        return Ok(helpers);
553    };
554
555    // Avoid invoking the compiler when the selected mode cannot change this module. These checks
556    // run on the SWC AST we already parsed, before converting it to the compiler AST. `All` mode
557    // remains unconditional because every function is eligible.
558    if !should_run_rust_react_compiler(program, options.compilation_mode) {
559        return Ok(helpers);
560    }
561
562    let single_threaded_comments =
563        crate::swc_comments::swc_comments_to_single_threaded(ctx.comments);
564    let result = swc_ecma_react_compiler::transform(
565        program,
566        swc_ecma_react_compiler::SourceType::from_program(program),
567        ctx.source_text,
568        Some(&single_threaded_comments),
569        react_compiler_options(ctx.node_env.as_str(), ctx.file_name_str, options, target),
570    );
571
572    // TODO: Emit these diagnostics with an Info level once there's a way of adjusting log levels in
573    //       general. By default React Compiler is silent, as de-opts align closely with feedback
574    //       from tools like React's lint rules.
575
576    if let Some(compiled_program) = result.program {
577        *program = compiled_program;
578
579        // TODO(react-compiler-swc): The Rust React Compiler emits every identifier with
580        // `SyntaxContext::empty()` in `convert_ast_reverse.rs`.
581        //
582        // Remove this once `swc_ecma_react_compiler`
583        // preserves/assigns contexts on the converted AST.
584        program.mutate(swc_core::ecma::transforms::base::resolver(
585            ctx.unresolved_mark,
586            ctx.top_level_mark,
587            true,
588        ));
589    }
590
591    Ok(helpers)
592}
593
594fn react_compiler_options(
595    node_env: &str,
596    file_name: &str,
597    options: ReactCompilerTransformOptions,
598    target: ReactCompilerTarget,
599) -> react_compiler::entrypoint::plugin_options::PluginOptions {
600    use react_compiler::entrypoint::plugin_options::{CompilerTarget, PluginOptions};
601
602    let mut plugin_options = PluginOptions {
603        should_compile: true,
604        enable_reanimated: false,
605        is_dev: node_env != "production",
606        filename: Some(file_name.to_string()),
607        compilation_mode: options.compilation_mode.as_str().to_string(),
608        panic_threshold: "none".to_string(),
609        target: CompilerTarget::Version(target.as_str().to_string()),
610        gating: None,
611        dynamic_gating: None,
612        no_emit: false,
613        output_mode: None,
614        eslint_suppression_rules: None,
615        flow_suppressions: false,
616        ignore_use_no_forget: false,
617        custom_opt_out_directives: None,
618        environment: Default::default(),
619        source_code: None,
620        profiling: false,
621        debug: false,
622    };
623    if let Some(enable_preserve_existing_memoization_guarantees) =
624        options.enable_preserve_existing_memoization_guarantees
625    {
626        plugin_options
627            .environment
628            .enable_preserve_existing_memoization_guarantees =
629            enable_preserve_existing_memoization_guarantees;
630    }
631
632    plugin_options
633}
634
635fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
636    let helpers = Helpers::from_data(helpers);
637    HELPERS.set(&helpers, || {
638        program.mutate(op);
639    });
640    helpers.data()
641}
642
643pub fn remove_shebang(program: &mut Program) {
644    match program {
645        Program::Module(m) => {
646            m.shebang = None;
647        }
648        Program::Script(s) => {
649            s.shebang = None;
650        }
651    }
652}
653
654pub fn remove_directives(program: &mut Program) {
655    match program {
656        Program::Module(module) => {
657            let directive_count = module
658                .body
659                .iter()
660                .take_while(|i| match i {
661                    ModuleItem::Stmt(stmt) => stmt.directive_continue(),
662                    ModuleItem::ModuleDecl(_) => false,
663                })
664                .take_while(|i| match i {
665                    ModuleItem::Stmt(stmt) => match stmt {
666                        Stmt::Expr(ExprStmt { expr, .. }) => expr
667                            .as_lit()
668                            .and_then(|lit| lit.as_str())
669                            .and_then(|str| str.raw.as_ref())
670                            .is_some_and(|raw| {
671                                raw.starts_with("\"use ") || raw.starts_with("'use ")
672                            }),
673                        _ => false,
674                    },
675                    ModuleItem::ModuleDecl(_) => false,
676                })
677                .count();
678            module.body.drain(0..directive_count);
679        }
680        Program::Script(script) => {
681            let directive_count = script
682                .body
683                .iter()
684                .take_while(|stmt| stmt.directive_continue())
685                .take_while(|stmt| match stmt {
686                    Stmt::Expr(ExprStmt { expr, .. }) => expr
687                        .as_lit()
688                        .and_then(|lit| lit.as_str())
689                        .and_then(|str| str.raw.as_ref())
690                        .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
691                    _ => false,
692                })
693                .count();
694            script.body.drain(0..directive_count);
695        }
696    }
697}
698
699#[cfg(test)]
700mod react_compiler_tests {
701    use swc_core::{
702        common::{DUMMY_SP, FileName, GLOBALS, SourceMap},
703        ecma::{
704            ast::{EsVersion, Module},
705            parser::{Syntax, TsSyntax, parse_file_as_program},
706        },
707    };
708
709    use super::*;
710
711    #[test]
712    fn preserves_default_environment_without_explicit_option() {
713        let options = react_compiler_options(
714            "production",
715            "input.js",
716            ReactCompilerTransformOptions::default(),
717            ReactCompilerTarget::React19,
718        );
719
720        assert!(
721            options
722                .environment
723                .enable_preserve_existing_memoization_guarantees
724        );
725    }
726
727    #[test]
728    fn forwards_explicit_memoization_preservation_option() {
729        let options = react_compiler_options(
730            "production",
731            "input.js",
732            ReactCompilerTransformOptions {
733                compilation_mode: ReactCompilerCompilationMode::Infer,
734                enable_preserve_existing_memoization_guarantees: Some(false),
735            },
736            ReactCompilerTarget::React19,
737        );
738
739        assert!(
740            !options
741                .environment
742                .enable_preserve_existing_memoization_guarantees
743        );
744    }
745
746    fn parse_program(source: &str) -> Program {
747        GLOBALS.set(&Default::default(), || {
748            let cm = SourceMap::default();
749            let fm = cm.new_source_file(
750                FileName::Custom("test.tsx".into()).into(),
751                source.to_owned(),
752            );
753            let mut errors = Vec::new();
754            let program = parse_file_as_program(
755                &fm,
756                Syntax::Typescript(TsSyntax {
757                    tsx: true,
758                    ..Default::default()
759                }),
760                EsVersion::EsNext,
761                None,
762                &mut errors,
763            )
764            .expect("test fixture should parse");
765            assert!(errors.is_empty(), "test fixture should not recover errors");
766            program
767        })
768    }
769
770    #[test]
771    fn compilation_modes_use_their_respective_fast_checks() {
772        let program = Program::Module(Module {
773            span: DUMMY_SP,
774            body: Vec::new(),
775            shebang: None,
776        });
777
778        for mode in [
779            ReactCompilerCompilationMode::Infer,
780            ReactCompilerCompilationMode::Annotation,
781        ] {
782            assert!(!should_run_rust_react_compiler(&program, mode));
783        }
784        assert!(should_run_rust_react_compiler(
785            &program,
786            ReactCompilerCompilationMode::All,
787        ));
788    }
789
790    #[test]
791    fn infer_mode_uses_upstream_conservative_fast_check() {
792        for source in [
793            "const Button = React.forwardRef((props, ref) => <button ref={ref} />);",
794            "function useCounter() { return React.useState(0); }",
795            "function helper() { 'use memo'; return 1; }",
796        ] {
797            assert!(should_run_rust_react_compiler(
798                &parse_program(source),
799                ReactCompilerCompilationMode::Infer
800            ));
801        }
802
803        for source in [
804            "export const answer = 42;",
805            "const user = getUser();",
806            "function helper() { log(); 'use memo'; }",
807        ] {
808            assert!(!should_run_rust_react_compiler(
809                &parse_program(source),
810                ReactCompilerCompilationMode::Infer
811            ));
812        }
813    }
814
815    #[test]
816    fn annotation_mode_only_runs_for_function_opt_in_directives() {
817        for source in [
818            "function helper() { 'use memo'; return 1; }",
819            "const helper = () => { 'use forget'; return 1; };",
820            "function outer() { function inner() { 'use memo'; return 1; } }",
821        ] {
822            assert!(should_run_rust_react_compiler(
823                &parse_program(source),
824                ReactCompilerCompilationMode::Annotation,
825            ));
826        }
827
828        for source in [
829            "function Component() { return <div />; }",
830            "function useCounter() { return useState(0); }",
831            "function helper() { log(); 'use memo'; }",
832            "'use memo'; export const answer = 42;",
833            "function helper() { 'use memo if(featureFlag)'; return 1; }",
834            "function helper() { 'use no memo'; return 1; }",
835        ] {
836            assert!(!should_run_rust_react_compiler(
837                &parse_program(source),
838                ReactCompilerCompilationMode::Annotation,
839            ));
840        }
841    }
842
843    #[test]
844    fn all_mode_remains_unconditional() {
845        for source in [
846            "export const answer = 42;",
847            "function helper() { return 1; }",
848        ] {
849            assert!(should_run_rust_react_compiler(
850                &parse_program(source),
851                ReactCompilerCompilationMode::All,
852            ));
853        }
854    }
855}