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#[turbo_tasks::value(shared)]
42#[derive(Default, Clone, Debug)]
43pub struct PresetEnvConfig {
44 pub mode: Option<RcStr>,
47 pub core_js: Option<RcStr>,
49 pub skip: Option<Vec<RcStr>>,
51 pub include: Option<Vec<RcStr>>,
53 pub exclude: Option<Vec<RcStr>>,
55 pub shipped_proposals: Option<bool>,
57 pub force_all_transforms: Option<bool>,
59 pub debug: Option<bool>,
61 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 import_source: ResolvedVc<Option<RcStr>>,
75 runtime: ResolvedVc<Option<RcStr>>,
77 },
78 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#[async_trait]
141pub trait CustomTransformer: Debug {
142 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
143}
144
145#[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 pub source_text: &'a str,
191 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 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 "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 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 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 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 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 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 if let Some(compiled_program) = result.program {
476 *program = compiled_program;
477
478 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}