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
30#[turbo_tasks::value]
31#[derive(Debug, Clone, Hash)]
32pub enum EcmascriptInputTransform {
33 Plugin(ResolvedVc<TransformPlugin>),
34 PresetEnv(ResolvedVc<Environment>),
35 React {
36 #[serde(default)]
37 development: bool,
38 #[serde(default)]
39 refresh: bool,
40 import_source: ResolvedVc<Option<RcStr>>,
42 runtime: ResolvedVc<Option<RcStr>>,
44 },
45 GlobalTypeofs {
46 window_value: String,
47 },
48 TypeScript {
51 #[serde(default)]
52 use_define_for_class_fields: bool,
53 },
54 Decorators {
55 #[serde(default)]
56 is_legacy: bool,
57 #[serde(default)]
58 is_ecma: bool,
59 #[serde(default)]
60 emit_decorators_metadata: bool,
61 #[serde(default)]
62 use_define_for_class_fields: bool,
63 },
64}
65
66#[async_trait]
69pub trait CustomTransformer: Debug {
70 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
71}
72
73#[turbo_tasks::value(
76 transparent,
77 serialization = "none",
78 eq = "manual",
79 into = "new",
80 cell = "new"
81)]
82#[derive(Debug)]
83pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
84
85#[async_trait]
86impl CustomTransformer for TransformPlugin {
87 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
88 self.0.transform(program, ctx).await
89 }
90}
91
92#[turbo_tasks::value(transparent)]
93#[derive(Debug, Clone, Hash)]
94pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
95
96#[turbo_tasks::value_impl]
97impl EcmascriptInputTransforms {
98 #[turbo_tasks::function]
99 pub fn empty() -> Vc<Self> {
100 Vc::cell(Vec::new())
101 }
102
103 #[turbo_tasks::function]
104 pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
105 let mut transforms = self.owned().await?;
106 transforms.extend(other.owned().await?);
107 Ok(Vc::cell(transforms))
108 }
109}
110
111pub struct TransformContext<'a> {
112 pub comments: &'a SwcComments,
113 pub top_level_mark: Mark,
114 pub unresolved_mark: Mark,
115 pub source_map: &'a Arc<SourceMap>,
116 pub file_path_str: &'a str,
117 pub file_name_str: &'a str,
118 pub file_name_hash: u128,
119 pub query_str: RcStr,
120 pub file_path: FileSystemPath,
121 pub source: ResolvedVc<Box<dyn Source>>,
122}
123
124impl EcmascriptInputTransform {
125 pub async fn apply(
126 &self,
127 program: &mut Program,
128 ctx: &TransformContext<'_>,
129 helpers: HelperData,
130 ) -> Result<HelperData> {
131 let &TransformContext {
132 comments,
133 source_map,
134 top_level_mark,
135 unresolved_mark,
136 ..
137 } = ctx;
138
139 Ok(match self {
140 EcmascriptInputTransform::GlobalTypeofs { window_value } => {
141 let mut typeofs: FxHashMap<Atom, Atom> = Default::default();
142 typeofs.insert(Atom::from("window"), Atom::from(&**window_value));
143
144 apply_transform(
145 program,
146 helpers,
147 inline_globals(
148 unresolved_mark,
149 Default::default(),
150 Default::default(),
151 Default::default(),
152 Arc::new(typeofs),
153 ),
154 )
155 }
156 EcmascriptInputTransform::React {
157 development,
158 refresh,
159 import_source,
160 runtime,
161 } => {
162 use swc_core::ecma::transforms::react::{Options, Runtime};
163 let runtime = if let Some(runtime) = &*runtime.await? {
164 match runtime.as_str() {
165 "classic" => Runtime::Classic,
166 "automatic" => Runtime::Automatic,
167 _ => {
168 return Err(anyhow::anyhow!(
169 "Invalid value for swc.jsc.transform.react.runtime: {}",
170 runtime
171 ));
172 }
173 }
174 } else {
175 Runtime::Automatic
176 };
177
178 let config = Options {
179 runtime: Some(runtime),
180 development: Some(*development),
181 import_source: import_source.await?.as_deref().map(Atom::from),
182 refresh: if *refresh {
183 Some(swc_core::ecma::transforms::react::RefreshOptions {
184 refresh_reg: atom!("__turbopack_context__.k.register"),
186 refresh_sig: atom!("__turbopack_context__.k.signature"),
187 ..Default::default()
188 })
189 } else {
190 None
191 },
192 ..Default::default()
193 };
194
195 let helpers = apply_transform(
198 program,
199 helpers,
200 react::<&dyn Comments>(
201 source_map.clone(),
202 Some(&comments),
203 config,
204 top_level_mark,
205 unresolved_mark,
206 ),
207 );
208
209 if *refresh {
210 let stmt = quote!(
211 "\nif (typeof globalThis.$RefreshHelpers$ === 'object' && \
213 globalThis.$RefreshHelpers !== null) { \
214 __turbopack_context__.k.registerExports(module, \
215 globalThis.$RefreshHelpers$); }\n" as Stmt
216 );
217
218 match program {
219 Program::Module(module) => {
220 module.body.push(ModuleItem::Stmt(stmt));
221 }
222 Program::Script(script) => {
223 script.body.push(stmt);
224 }
225 }
226 }
227
228 helpers
229 }
230 EcmascriptInputTransform::PresetEnv(env) => {
231 let versions = env.runtime_versions().await?;
232 let config = swc_core::ecma::preset_env::EnvConfig::from(
233 swc_core::ecma::preset_env::Config {
234 targets: Some(Targets::Versions(*versions)),
235 mode: None, ..Default::default()
237 },
238 );
239
240 apply_transform(
243 program,
244 helpers,
245 preset_env::transform_from_env::<&'_ dyn Comments>(
246 unresolved_mark,
247 Some(&comments),
248 config,
249 Assumptions::default(),
250 ),
251 )
252 }
253 EcmascriptInputTransform::TypeScript {
254 use_define_for_class_fields: _use_define_for_class_fields,
256 } => {
257 use swc_core::ecma::transforms::typescript::typescript;
258 let config = Default::default();
259 apply_transform(
260 program,
261 helpers,
262 typescript(config, unresolved_mark, top_level_mark),
263 )
264 }
265 EcmascriptInputTransform::Decorators {
266 is_legacy,
267 is_ecma: _,
268 emit_decorators_metadata,
269 use_define_for_class_fields: _use_define_for_class_fields,
271 } => {
272 use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
273 let config = Config {
274 legacy: *is_legacy,
275 emit_metadata: *emit_decorators_metadata,
276 ..Default::default()
277 };
278
279 apply_transform(program, helpers, decorators(config))
280 }
281 EcmascriptInputTransform::Plugin(transform) => {
282 transform.await?.transform(program, ctx).await?;
284 helpers
285 }
286 })
287 }
288}
289
290fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
291 let helpers = Helpers::from_data(helpers);
292 HELPERS.set(&helpers, || {
293 program.mutate(op);
294 });
295 helpers.data()
296}
297
298pub fn remove_shebang(program: &mut Program) {
299 match program {
300 Program::Module(m) => {
301 m.shebang = None;
302 }
303 Program::Script(s) => {
304 s.shebang = None;
305 }
306 }
307}
308
309pub fn remove_directives(program: &mut Program) {
310 match program {
311 Program::Module(module) => {
312 let directive_count = module
313 .body
314 .iter()
315 .take_while(|i| match i {
316 ModuleItem::Stmt(stmt) => stmt.directive_continue(),
317 ModuleItem::ModuleDecl(_) => false,
318 })
319 .take_while(|i| match i {
320 ModuleItem::Stmt(stmt) => match stmt {
321 Stmt::Expr(ExprStmt { expr, .. }) => expr
322 .as_lit()
323 .and_then(|lit| lit.as_str())
324 .and_then(|str| str.raw.as_ref())
325 .is_some_and(|raw| {
326 raw.starts_with("\"use ") || raw.starts_with("'use ")
327 }),
328 _ => false,
329 },
330 ModuleItem::ModuleDecl(_) => false,
331 })
332 .count();
333 module.body.drain(0..directive_count);
334 }
335 Program::Script(script) => {
336 let directive_count = script
337 .body
338 .iter()
339 .take_while(|stmt| stmt.directive_continue())
340 .take_while(|stmt| match stmt {
341 Stmt::Expr(ExprStmt { expr, .. }) => expr
342 .as_lit()
343 .and_then(|lit| lit.as_str())
344 .and_then(|str| str.raw.as_ref())
345 .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
346 _ => false,
347 })
348 .count();
349 script.body.drain(0..directive_count);
350 }
351 }
352}