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