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 = "none", 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}
138
139impl EcmascriptInputTransform {
140 pub async fn apply(
141 &self,
142 program: &mut Program,
143 ctx: &TransformContext<'_>,
144 helpers: HelperData,
145 ) -> Result<HelperData> {
146 let &TransformContext {
147 comments,
148 source_map,
149 top_level_mark,
150 unresolved_mark,
151 ..
152 } = ctx;
153
154 Ok(match self {
155 EcmascriptInputTransform::React {
156 development,
157 refresh,
158 import_source,
159 runtime,
160 } => {
161 use swc_core::ecma::transforms::react::{Options, Runtime};
162 let runtime = if let Some(runtime) = &*runtime.await? {
163 match runtime.as_str() {
164 "classic" => Runtime::Classic,
165 "automatic" => Runtime::Automatic,
166 _ => {
167 bail!(
168 "Invalid value for swc.jsc.transform.react.runtime: {}",
169 runtime
170 );
171 }
172 }
173 } else {
174 Runtime::Automatic
175 };
176
177 let config = Options {
178 runtime: Some(runtime),
179 development: Some(*development),
180 import_source: import_source.await?.as_deref().map(Atom::from),
181 refresh: if *refresh {
182 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
183 Some(swc_core::ecma::transforms::react::RefreshOptions {
184 refresh_reg: atom!("__turbopack_context__.k.register"),
185 refresh_sig: atom!("__turbopack_context__.k.signature"),
186 ..Default::default()
187 })
188 } else {
189 None
190 },
191 ..Default::default()
192 };
193
194 let helpers = apply_transform(
197 program,
198 helpers,
199 react::<&dyn Comments>(
200 source_map.clone(),
201 Some(&comments),
202 config,
203 top_level_mark,
204 unresolved_mark,
205 ),
206 );
207
208 if *refresh {
209 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
210 debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
211 let stmt = quote!(
212 "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
214 globalThis.$RefreshHelpers !== null) { \
215 __turbopack_context__.k.registerExports(__turbopack_context__.m, \
216 globalThis.$RefreshHelpers$); }" as Stmt
217 );
218
219 match program {
220 Program::Module(module) => {
221 module.body.push(ModuleItem::Stmt(stmt));
222 }
223 Program::Script(script) => {
224 script.body.push(stmt);
225 }
226 }
227 }
228
229 helpers
230 }
231 EcmascriptInputTransform::PresetEnv(env, preset_env_config) => {
232 let versions = env.runtime_versions().await?;
233 let extra = preset_env_config.await?;
234
235 let mode = match extra.mode.as_deref() {
236 Some("usage") => Some(preset_env::Mode::Usage),
237 Some("entry") => Some(preset_env::Mode::Entry),
238 _ => None,
239 };
240
241 let core_js = extra.core_js.as_ref().and_then(|v| {
242 let parts: Vec<&str> = v.split('.').collect();
243 Some(preset_env::Version {
244 major: parts.first()?.parse().ok()?,
245 minor: parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
246 patch: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
247 })
248 });
249
250 let skip = extra
251 .skip
252 .as_ref()
253 .map(|v| v.iter().map(|s| Atom::from(s.as_str())).collect())
254 .unwrap_or_default();
255
256 let parse_feature_or_module = |s: &str| -> FeatureOrModule {
257 if let Ok(feature) = s.parse::<Feature>() {
258 FeatureOrModule::Feature(feature)
259 } else {
260 FeatureOrModule::CoreJsModule(s.to_string())
261 }
262 };
263
264 let include: Vec<FeatureOrModule> = extra
265 .include
266 .as_ref()
267 .map(|v| v.iter().map(|s| parse_feature_or_module(s)).collect())
268 .unwrap_or_default();
269
270 let mut exclude: Vec<FeatureOrModule> = vec![
273 FeatureOrModule::Feature(Feature::ReservedWords),
274 FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
275 FeatureOrModule::Feature(Feature::PropertyLiterals),
276 ];
277 if let Some(user_exclude) = &extra.exclude {
278 for s in user_exclude {
279 exclude.push(parse_feature_or_module(s));
280 }
281 }
282
283 let config = swc_core::ecma::preset_env::EnvConfig::from(
284 swc_core::ecma::preset_env::Config {
285 targets: Some(Targets::Versions(*versions)),
286 mode,
287 core_js,
288 skip,
289 include,
290 exclude,
291 shipped_proposals: extra.shipped_proposals.unwrap_or(false),
292 force_all_transforms: extra.force_all_transforms.unwrap_or(false),
293 debug: extra.debug.unwrap_or(false),
294 loose: extra.loose.unwrap_or(false),
295 ..Default::default()
296 },
297 );
298
299 apply_transform(
302 program,
303 helpers,
304 preset_env::transform_from_env::<&'_ dyn Comments>(
305 unresolved_mark,
306 Some(&comments),
307 config,
308 Assumptions::default(),
309 ),
310 )
311 }
312 EcmascriptInputTransform::TypeScript {
313 use_define_for_class_fields: _use_define_for_class_fields,
315 verbatim_module_syntax,
316 } => {
317 let config = Config {
318 verbatim_module_syntax: *verbatim_module_syntax,
319 ..Default::default()
320 };
321 apply_transform(
322 program,
323 helpers,
324 typescript(config, unresolved_mark, top_level_mark),
325 )
326 }
327 EcmascriptInputTransform::Decorators {
328 is_legacy,
329 is_ecma: _,
330 emit_decorators_metadata,
331 use_define_for_class_fields: _use_define_for_class_fields,
333 } => {
334 use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
335 let config = Config {
336 legacy: *is_legacy,
337 emit_metadata: *emit_decorators_metadata,
338 ..Default::default()
339 };
340
341 apply_transform(program, helpers, decorators(config))
342 }
343 EcmascriptInputTransform::Plugin(transform) => {
344 transform.await?.transform(program, ctx).await?;
346 helpers
347 }
348 })
349 }
350}
351
352fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
353 let helpers = Helpers::from_data(helpers);
354 HELPERS.set(&helpers, || {
355 program.mutate(op);
356 });
357 helpers.data()
358}
359
360pub fn remove_shebang(program: &mut Program) {
361 match program {
362 Program::Module(m) => {
363 m.shebang = None;
364 }
365 Program::Script(s) => {
366 s.shebang = None;
367 }
368 }
369}
370
371pub fn remove_directives(program: &mut Program) {
372 match program {
373 Program::Module(module) => {
374 let directive_count = module
375 .body
376 .iter()
377 .take_while(|i| match i {
378 ModuleItem::Stmt(stmt) => stmt.directive_continue(),
379 ModuleItem::ModuleDecl(_) => false,
380 })
381 .take_while(|i| match i {
382 ModuleItem::Stmt(stmt) => match stmt {
383 Stmt::Expr(ExprStmt { expr, .. }) => expr
384 .as_lit()
385 .and_then(|lit| lit.as_str())
386 .and_then(|str| str.raw.as_ref())
387 .is_some_and(|raw| {
388 raw.starts_with("\"use ") || raw.starts_with("'use ")
389 }),
390 _ => false,
391 },
392 ModuleItem::ModuleDecl(_) => false,
393 })
394 .count();
395 module.body.drain(0..directive_count);
396 }
397 Program::Script(script) => {
398 let directive_count = script
399 .body
400 .iter()
401 .take_while(|stmt| stmt.directive_continue())
402 .take_while(|stmt| match stmt {
403 Stmt::Expr(ExprStmt { expr, .. }) => expr
404 .as_lit()
405 .and_then(|lit| lit.as_str())
406 .and_then(|str| str.raw.as_ref())
407 .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
408 _ => false,
409 })
410 .count();
411 script.body.drain(0..directive_count);
412 }
413 }
414}