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#[turbo_tasks::value(shared)]
46#[derive(Default, Clone, Debug)]
47pub struct PresetEnvConfig {
48 pub mode: Option<RcStr>,
51 pub core_js: Option<RcStr>,
53 pub skip: Option<Vec<RcStr>>,
55 pub include: Option<Vec<RcStr>>,
57 pub exclude: Option<Vec<RcStr>>,
59 pub shipped_proposals: Option<bool>,
61 pub force_all_transforms: Option<bool>,
63 pub debug: Option<bool>,
65 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 import_source: ResolvedVc<Option<RcStr>>,
79 runtime: ResolvedVc<Option<RcStr>>,
81 },
82 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#[async_trait]
145pub trait CustomTransformer: Debug {
146 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
147}
148
149#[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 pub source_text: &'a str,
195 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 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 "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 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 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 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 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 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
454fn 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 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 if let Some(compiled_program) = result.program {
570 *program = compiled_program;
571
572 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}