1use std::{fmt::Debug, hash::Hash, sync::Arc};
2
3use anyhow::{Result, bail};
4use async_trait::async_trait;
5use swc_core::{
6 atoms::{Atom, atom},
7 base::SwcComments,
8 common::{Mark, SourceMap, comments::Comments},
9 ecma::{
10 ast::{ExprStmt, ModuleItem, Pass, Program, Stmt},
11 preset_env::{self, Feature, FeatureOrModule, Targets},
12 transforms::{
13 base::{
14 assumptions::Assumptions,
15 helpers::{HELPERS, HelperData, Helpers},
16 },
17 react::react,
18 typescript::{Config, typescript},
19 },
20 utils::IsDirective,
21 },
22 quote,
23};
24use turbo_rcstr::RcStr;
25use turbo_tasks::{ResolvedVc, Vc};
26use turbo_tasks_fs::FileSystemPath;
27use turbopack_core::{environment::Environment, source::Source};
28
29use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH};
30
31#[turbo_tasks::value(shared)]
37#[derive(Default, Clone, Debug)]
38pub struct PresetEnvConfig {
39 pub mode: Option<RcStr>,
42 pub core_js: Option<RcStr>,
44 pub skip: Option<Vec<RcStr>>,
46 pub include: Option<Vec<RcStr>>,
48 pub exclude: Option<Vec<RcStr>>,
50 pub shipped_proposals: Option<bool>,
52 pub force_all_transforms: Option<bool>,
54 pub debug: Option<bool>,
56 pub loose: Option<bool>,
58}
59
60#[turbo_tasks::value]
61#[derive(Debug, Clone, Hash)]
62pub enum EcmascriptInputTransform {
63 Plugin(ResolvedVc<TransformPlugin>),
64 PresetEnv(ResolvedVc<Environment>, ResolvedVc<PresetEnvConfig>),
65 React {
66 development: bool,
67 refresh: bool,
68 import_source: ResolvedVc<Option<RcStr>>,
70 runtime: ResolvedVc<Option<RcStr>>,
72 },
73 TypeScript {
76 use_define_for_class_fields: bool,
77 verbatim_module_syntax: bool,
78 },
79 Decorators {
80 is_legacy: bool,
81 is_ecma: bool,
82 emit_decorators_metadata: bool,
83 use_define_for_class_fields: bool,
84 },
85}
86
87#[async_trait]
90pub trait CustomTransformer: Debug {
91 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
92}
93
94#[turbo_tasks::value(transparent, serialization = "skip", eq = "manual", cell = "new")]
97#[derive(Debug)]
98pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
99
100#[async_trait]
101impl CustomTransformer for TransformPlugin {
102 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
103 self.0.transform(program, ctx).await
104 }
105}
106
107#[turbo_tasks::value(transparent)]
108#[derive(Debug, Clone, Hash)]
109pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
110
111#[turbo_tasks::value_impl]
112impl EcmascriptInputTransforms {
113 #[turbo_tasks::function]
114 pub fn empty() -> Vc<Self> {
115 Vc::cell(Vec::new())
116 }
117
118 #[turbo_tasks::function]
119 pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
120 let mut transforms = self.owned().await?;
121 transforms.extend(other.owned().await?);
122 Ok(Vc::cell(transforms))
123 }
124}
125
126pub struct TransformContext<'a> {
127 pub comments: &'a SwcComments,
128 pub top_level_mark: Mark,
129 pub unresolved_mark: Mark,
130 pub source_map: &'a Arc<SourceMap>,
131 pub file_path_str: &'a str,
132 pub file_name_str: &'a str,
133 pub file_name_hash: u128,
134 pub query_str: RcStr,
135 pub file_path: FileSystemPath,
136 pub source: ResolvedVc<Box<dyn Source>>,
137 pub node_env: RcStr,
140}
141
142impl EcmascriptInputTransform {
143 pub async fn apply(
144 &self,
145 program: &mut Program,
146 ctx: &TransformContext<'_>,
147 helpers: HelperData,
148 ) -> Result<HelperData> {
149 let &TransformContext {
150 comments,
151 source_map,
152 top_level_mark,
153 unresolved_mark,
154 ..
155 } = ctx;
156
157 Ok(match self {
158 EcmascriptInputTransform::React {
159 development,
160 refresh,
161 import_source,
162 runtime,
163 } => {
164 use swc_core::ecma::transforms::react::{Options, Runtime};
165 let runtime = if let Some(runtime) = &*runtime.await? {
166 match runtime.as_str() {
167 "classic" => Runtime::Classic,
168 "automatic" => Runtime::Automatic,
169 _ => {
170 bail!(
171 "Invalid value for swc.jsc.transform.react.runtime: {}",
172 runtime
173 );
174 }
175 }
176 } else {
177 Runtime::Automatic
178 };
179
180 let config = Options {
181 runtime: Some(runtime),
182 development: Some(*development),
183 import_source: import_source.await?.as_deref().map(Atom::from),
184 refresh: if *refresh {
185 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
186 Some(swc_core::ecma::transforms::react::RefreshOptions {
187 refresh_reg: atom!("__turbopack_context__.k.register"),
188 refresh_sig: atom!("__turbopack_context__.k.signature"),
189 ..Default::default()
190 })
191 } else {
192 None
193 },
194 ..Default::default()
195 };
196
197 let helpers = apply_transform(
200 program,
201 helpers,
202 react::<&dyn Comments>(
203 source_map.clone(),
204 Some(&comments),
205 config,
206 top_level_mark,
207 unresolved_mark,
208 ),
209 );
210
211 if *refresh {
212 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
213 debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
214 let stmt = quote!(
215 "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
217 globalThis.$RefreshHelpers !== null) { \
218 __turbopack_context__.k.registerExports(__turbopack_context__.m, \
219 globalThis.$RefreshHelpers$); }" as Stmt
220 );
221
222 match program {
223 Program::Module(module) => {
224 module.body.push(ModuleItem::Stmt(stmt));
225 }
226 Program::Script(script) => {
227 script.body.push(stmt);
228 }
229 }
230 }
231
232 helpers
233 }
234 EcmascriptInputTransform::PresetEnv(env, preset_env_config) => {
235 let versions = env.runtime_versions().await?;
236 let extra = preset_env_config.await?;
237
238 let mode = match extra.mode.as_deref() {
239 Some("usage") => Some(preset_env::Mode::Usage),
240 Some("entry") => Some(preset_env::Mode::Entry),
241 _ => None,
242 };
243
244 let core_js = extra.core_js.as_ref().and_then(|v| {
245 let parts: Vec<&str> = v.split('.').collect();
246 Some(preset_env::Version {
247 major: parts.first()?.parse().ok()?,
248 minor: parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
249 patch: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
250 })
251 });
252
253 let skip = extra
254 .skip
255 .as_ref()
256 .map(|v| v.iter().map(|s| Atom::from(s.as_str())).collect())
257 .unwrap_or_default();
258
259 let parse_feature_or_module = |s: &str| -> FeatureOrModule {
260 if let Ok(feature) = s.parse::<Feature>() {
261 FeatureOrModule::Feature(feature)
262 } else {
263 FeatureOrModule::CoreJsModule(s.to_string())
264 }
265 };
266
267 let include: Vec<FeatureOrModule> = extra
268 .include
269 .as_ref()
270 .map(|v| v.iter().map(|s| parse_feature_or_module(s)).collect())
271 .unwrap_or_default();
272
273 let mut exclude: Vec<FeatureOrModule> = vec![
276 FeatureOrModule::Feature(Feature::ReservedWords),
277 FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
278 FeatureOrModule::Feature(Feature::PropertyLiterals),
279 ];
280 if let Some(user_exclude) = &extra.exclude {
281 for s in user_exclude {
282 exclude.push(parse_feature_or_module(s));
283 }
284 }
285
286 let config = swc_core::ecma::preset_env::EnvConfig::from(
287 swc_core::ecma::preset_env::Config {
288 targets: Some(Targets::Versions(*versions)),
289 mode,
290 core_js,
291 skip,
292 include,
293 exclude,
294 shipped_proposals: extra.shipped_proposals.unwrap_or(false),
295 force_all_transforms: extra.force_all_transforms.unwrap_or(false),
296 debug: extra.debug.unwrap_or(false),
297 loose: extra.loose.unwrap_or(false),
298 ..Default::default()
299 },
300 );
301
302 apply_transform(
305 program,
306 helpers,
307 preset_env::transform_from_env::<&'_ dyn Comments>(
308 unresolved_mark,
309 Some(&comments),
310 config,
311 Assumptions::default(),
312 ),
313 )
314 }
315 EcmascriptInputTransform::TypeScript {
316 use_define_for_class_fields: _use_define_for_class_fields,
318 verbatim_module_syntax,
319 } => {
320 let config = Config {
321 verbatim_module_syntax: *verbatim_module_syntax,
322 ..Default::default()
323 };
324 apply_transform(
325 program,
326 helpers,
327 typescript(config, unresolved_mark, top_level_mark),
328 )
329 }
330 EcmascriptInputTransform::Decorators {
331 is_legacy,
332 is_ecma: _,
333 emit_decorators_metadata,
334 use_define_for_class_fields: _use_define_for_class_fields,
336 } => {
337 use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
338 let config = Config {
339 legacy: *is_legacy,
340 emit_metadata: *emit_decorators_metadata,
341 ..Default::default()
342 };
343
344 apply_transform(program, helpers, decorators(config))
345 }
346 EcmascriptInputTransform::Plugin(transform) => {
347 transform.await?.transform(program, ctx).await?;
349 helpers
350 }
351 })
352 }
353}
354
355fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
356 let helpers = Helpers::from_data(helpers);
357 HELPERS.set(&helpers, || {
358 program.mutate(op);
359 });
360 helpers.data()
361}
362
363pub fn remove_shebang(program: &mut Program) {
364 match program {
365 Program::Module(m) => {
366 m.shebang = None;
367 }
368 Program::Script(s) => {
369 s.shebang = None;
370 }
371 }
372}
373
374pub fn remove_directives(program: &mut Program) {
375 match program {
376 Program::Module(module) => {
377 let directive_count = module
378 .body
379 .iter()
380 .take_while(|i| match i {
381 ModuleItem::Stmt(stmt) => stmt.directive_continue(),
382 ModuleItem::ModuleDecl(_) => false,
383 })
384 .take_while(|i| match i {
385 ModuleItem::Stmt(stmt) => match stmt {
386 Stmt::Expr(ExprStmt { expr, .. }) => expr
387 .as_lit()
388 .and_then(|lit| lit.as_str())
389 .and_then(|str| str.raw.as_ref())
390 .is_some_and(|raw| {
391 raw.starts_with("\"use ") || raw.starts_with("'use ")
392 }),
393 _ => false,
394 },
395 ModuleItem::ModuleDecl(_) => false,
396 })
397 .count();
398 module.body.drain(0..directive_count);
399 }
400 Program::Script(script) => {
401 let directive_count = script
402 .body
403 .iter()
404 .take_while(|stmt| stmt.directive_continue())
405 .take_while(|stmt| match stmt {
406 Stmt::Expr(ExprStmt { expr, .. }) => expr
407 .as_lit()
408 .and_then(|lit| lit.as_str())
409 .and_then(|str| str.raw.as_ref())
410 .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
411 _ => false,
412 })
413 .count();
414 script.body.drain(0..directive_count);
415 }
416 }
417}