turbopack_ecmascript/transform/
mod.rs1use std::{fmt::Debug, hash::Hash, sync::Arc};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use rustc_hash::FxHashMap;
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, Targets},
13 transforms::{
14 base::{
15 assumptions::Assumptions,
16 helpers::{HELPERS, HelperData, Helpers},
17 },
18 optimization::inline_globals,
19 react::react,
20 },
21 utils::IsDirective,
22 },
23 quote,
24};
25use turbo_rcstr::RcStr;
26use turbo_tasks::{ResolvedVc, Vc};
27use turbo_tasks_fs::FileSystemPath;
28use turbopack_core::{environment::Environment, source::Source};
29
30use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH};
31
32#[turbo_tasks::value]
33#[derive(Debug, Clone, Hash)]
34pub enum EcmascriptInputTransform {
35 Plugin(ResolvedVc<TransformPlugin>),
36 PresetEnv(ResolvedVc<Environment>),
37 React {
38 #[serde(default)]
39 development: bool,
40 #[serde(default)]
41 refresh: bool,
42 import_source: ResolvedVc<Option<RcStr>>,
44 runtime: ResolvedVc<Option<RcStr>>,
46 },
47 GlobalTypeofs {
48 window_value: RcStr,
49 },
50 TypeScript {
53 #[serde(default)]
54 use_define_for_class_fields: bool,
55 },
56 Decorators {
57 #[serde(default)]
58 is_legacy: bool,
59 #[serde(default)]
60 is_ecma: bool,
61 #[serde(default)]
62 emit_decorators_metadata: bool,
63 #[serde(default)]
64 use_define_for_class_fields: bool,
65 },
66}
67
68#[async_trait]
71pub trait CustomTransformer: Debug {
72 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
73}
74
75#[turbo_tasks::value(
78 transparent,
79 serialization = "none",
80 eq = "manual",
81 into = "new",
82 cell = "new"
83)]
84#[derive(Debug)]
85pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
86
87#[async_trait]
88impl CustomTransformer for TransformPlugin {
89 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
90 self.0.transform(program, ctx).await
91 }
92}
93
94#[turbo_tasks::value(transparent)]
95#[derive(Debug, Clone, Hash)]
96pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
97
98#[turbo_tasks::value_impl]
99impl EcmascriptInputTransforms {
100 #[turbo_tasks::function]
101 pub fn empty() -> Vc<Self> {
102 Vc::cell(Vec::new())
103 }
104
105 #[turbo_tasks::function]
106 pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
107 let mut transforms = self.owned().await?;
108 transforms.extend(other.owned().await?);
109 Ok(Vc::cell(transforms))
110 }
111}
112
113pub struct TransformContext<'a> {
114 pub comments: &'a SwcComments,
115 pub top_level_mark: Mark,
116 pub unresolved_mark: Mark,
117 pub source_map: &'a Arc<SourceMap>,
118 pub file_path_str: &'a str,
119 pub file_name_str: &'a str,
120 pub file_name_hash: u128,
121 pub query_str: RcStr,
122 pub file_path: FileSystemPath,
123 pub source: ResolvedVc<Box<dyn Source>>,
124}
125
126impl EcmascriptInputTransform {
127 pub async fn apply(
128 &self,
129 program: &mut Program,
130 ctx: &TransformContext<'_>,
131 helpers: HelperData,
132 ) -> Result<HelperData> {
133 let &TransformContext {
134 comments,
135 source_map,
136 top_level_mark,
137 unresolved_mark,
138 ..
139 } = ctx;
140
141 Ok(match self {
142 EcmascriptInputTransform::GlobalTypeofs { window_value } => {
143 let mut typeofs: FxHashMap<Atom, Atom> = Default::default();
144 typeofs.insert(Atom::from("window"), Atom::from(&**window_value));
145
146 apply_transform(
147 program,
148 helpers,
149 inline_globals(
150 unresolved_mark,
151 Default::default(),
152 Default::default(),
153 Default::default(),
154 Arc::new(typeofs),
155 ),
156 )
157 }
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 return Err(anyhow::anyhow!(
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) => {
235 let versions = env.runtime_versions().await?;
236 let config = swc_core::ecma::preset_env::EnvConfig::from(
237 swc_core::ecma::preset_env::Config {
238 targets: Some(Targets::Versions(*versions)),
239 mode: None, ..Default::default()
241 },
242 );
243
244 apply_transform(
247 program,
248 helpers,
249 preset_env::transform_from_env::<&'_ dyn Comments>(
250 unresolved_mark,
251 Some(&comments),
252 config,
253 Assumptions::default(),
254 ),
255 )
256 }
257 EcmascriptInputTransform::TypeScript {
258 use_define_for_class_fields: _use_define_for_class_fields,
260 } => {
261 use swc_core::ecma::transforms::typescript::typescript;
262 let config = Default::default();
263 apply_transform(
264 program,
265 helpers,
266 typescript(config, unresolved_mark, top_level_mark),
267 )
268 }
269 EcmascriptInputTransform::Decorators {
270 is_legacy,
271 is_ecma: _,
272 emit_decorators_metadata,
273 use_define_for_class_fields: _use_define_for_class_fields,
275 } => {
276 use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
277 let config = Config {
278 legacy: *is_legacy,
279 emit_metadata: *emit_decorators_metadata,
280 ..Default::default()
281 };
282
283 apply_transform(program, helpers, decorators(config))
284 }
285 EcmascriptInputTransform::Plugin(transform) => {
286 transform.await?.transform(program, ctx).await?;
288 helpers
289 }
290 })
291 }
292}
293
294fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
295 let helpers = Helpers::from_data(helpers);
296 HELPERS.set(&helpers, || {
297 program.mutate(op);
298 });
299 helpers.data()
300}
301
302pub fn remove_shebang(program: &mut Program) {
303 match program {
304 Program::Module(m) => {
305 m.shebang = None;
306 }
307 Program::Script(s) => {
308 s.shebang = None;
309 }
310 }
311}
312
313pub fn remove_directives(program: &mut Program) {
314 match program {
315 Program::Module(module) => {
316 let directive_count = module
317 .body
318 .iter()
319 .take_while(|i| match i {
320 ModuleItem::Stmt(stmt) => stmt.directive_continue(),
321 ModuleItem::ModuleDecl(_) => false,
322 })
323 .take_while(|i| match i {
324 ModuleItem::Stmt(stmt) => match stmt {
325 Stmt::Expr(ExprStmt { expr, .. }) => expr
326 .as_lit()
327 .and_then(|lit| lit.as_str())
328 .and_then(|str| str.raw.as_ref())
329 .is_some_and(|raw| {
330 raw.starts_with("\"use ") || raw.starts_with("'use ")
331 }),
332 _ => false,
333 },
334 ModuleItem::ModuleDecl(_) => false,
335 })
336 .count();
337 module.body.drain(0..directive_count);
338 }
339 Program::Script(script) => {
340 let directive_count = script
341 .body
342 .iter()
343 .take_while(|stmt| stmt.directive_continue())
344 .take_while(|stmt| match stmt {
345 Stmt::Expr(ExprStmt { expr, .. }) => expr
346 .as_lit()
347 .and_then(|lit| lit.as_str())
348 .and_then(|str| str.raw.as_ref())
349 .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
350 _ => false,
351 })
352 .count();
353 script.body.drain(0..directive_count);
354 }
355 }
356}