turbopack_ecmascript/transform/
mod.rs1use std::{fmt::Debug, hash::Hash, sync::Arc};
2
3use anyhow::Result;
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, Targets},
12 transforms::{
13 base::{
14 assumptions::Assumptions,
15 helpers::{HELPERS, HelperData, Helpers},
16 },
17 react::react,
18 },
19 utils::IsDirective,
20 },
21 quote,
22};
23use turbo_rcstr::RcStr;
24use turbo_tasks::{ResolvedVc, Vc};
25use turbo_tasks_fs::FileSystemPath;
26use turbopack_core::{environment::Environment, source::Source};
27
28use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH};
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 TypeScript {
48 #[serde(default)]
49 use_define_for_class_fields: bool,
50 },
51 Decorators {
52 #[serde(default)]
53 is_legacy: bool,
54 #[serde(default)]
55 is_ecma: bool,
56 #[serde(default)]
57 emit_decorators_metadata: bool,
58 #[serde(default)]
59 use_define_for_class_fields: bool,
60 },
61}
62
63#[async_trait]
66pub trait CustomTransformer: Debug {
67 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
68}
69
70#[turbo_tasks::value(transparent, serialization = "none", eq = "manual", cell = "new")]
73#[derive(Debug)]
74pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
75
76#[async_trait]
77impl CustomTransformer for TransformPlugin {
78 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
79 self.0.transform(program, ctx).await
80 }
81}
82
83#[turbo_tasks::value(transparent)]
84#[derive(Debug, Clone, Hash)]
85pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
86
87#[turbo_tasks::value_impl]
88impl EcmascriptInputTransforms {
89 #[turbo_tasks::function]
90 pub fn empty() -> Vc<Self> {
91 Vc::cell(Vec::new())
92 }
93
94 #[turbo_tasks::function]
95 pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
96 let mut transforms = self.owned().await?;
97 transforms.extend(other.owned().await?);
98 Ok(Vc::cell(transforms))
99 }
100}
101
102pub struct TransformContext<'a> {
103 pub comments: &'a SwcComments,
104 pub top_level_mark: Mark,
105 pub unresolved_mark: Mark,
106 pub source_map: &'a Arc<SourceMap>,
107 pub file_path_str: &'a str,
108 pub file_name_str: &'a str,
109 pub file_name_hash: u128,
110 pub query_str: RcStr,
111 pub file_path: FileSystemPath,
112 pub source: ResolvedVc<Box<dyn Source>>,
113}
114
115impl EcmascriptInputTransform {
116 pub async fn apply(
117 &self,
118 program: &mut Program,
119 ctx: &TransformContext<'_>,
120 helpers: HelperData,
121 ) -> Result<HelperData> {
122 let &TransformContext {
123 comments,
124 source_map,
125 top_level_mark,
126 unresolved_mark,
127 ..
128 } = ctx;
129
130 Ok(match self {
131 EcmascriptInputTransform::React {
132 development,
133 refresh,
134 import_source,
135 runtime,
136 } => {
137 use swc_core::ecma::transforms::react::{Options, Runtime};
138 let runtime = if let Some(runtime) = &*runtime.await? {
139 match runtime.as_str() {
140 "classic" => Runtime::Classic,
141 "automatic" => Runtime::Automatic,
142 _ => {
143 return Err(anyhow::anyhow!(
144 "Invalid value for swc.jsc.transform.react.runtime: {}",
145 runtime
146 ));
147 }
148 }
149 } else {
150 Runtime::Automatic
151 };
152
153 let config = Options {
154 runtime: Some(runtime),
155 development: Some(*development),
156 import_source: import_source.await?.as_deref().map(Atom::from),
157 refresh: if *refresh {
158 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
159 Some(swc_core::ecma::transforms::react::RefreshOptions {
160 refresh_reg: atom!("__turbopack_context__.k.register"),
161 refresh_sig: atom!("__turbopack_context__.k.signature"),
162 ..Default::default()
163 })
164 } else {
165 None
166 },
167 ..Default::default()
168 };
169
170 let helpers = apply_transform(
173 program,
174 helpers,
175 react::<&dyn Comments>(
176 source_map.clone(),
177 Some(&comments),
178 config,
179 top_level_mark,
180 unresolved_mark,
181 ),
182 );
183
184 if *refresh {
185 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
186 debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
187 let stmt = quote!(
188 "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
190 globalThis.$RefreshHelpers !== null) { \
191 __turbopack_context__.k.registerExports(__turbopack_context__.m, \
192 globalThis.$RefreshHelpers$); }" as Stmt
193 );
194
195 match program {
196 Program::Module(module) => {
197 module.body.push(ModuleItem::Stmt(stmt));
198 }
199 Program::Script(script) => {
200 script.body.push(stmt);
201 }
202 }
203 }
204
205 helpers
206 }
207 EcmascriptInputTransform::PresetEnv(env) => {
208 let versions = env.runtime_versions().await?;
209 let config = swc_core::ecma::preset_env::EnvConfig::from(
210 swc_core::ecma::preset_env::Config {
211 targets: Some(Targets::Versions(*versions)),
212 mode: None, ..Default::default()
214 },
215 );
216
217 apply_transform(
220 program,
221 helpers,
222 preset_env::transform_from_env::<&'_ dyn Comments>(
223 unresolved_mark,
224 Some(&comments),
225 config,
226 Assumptions::default(),
227 ),
228 )
229 }
230 EcmascriptInputTransform::TypeScript {
231 use_define_for_class_fields: _use_define_for_class_fields,
233 } => {
234 use swc_core::ecma::transforms::typescript::typescript;
235 let config = Default::default();
236 apply_transform(
237 program,
238 helpers,
239 typescript(config, unresolved_mark, top_level_mark),
240 )
241 }
242 EcmascriptInputTransform::Decorators {
243 is_legacy,
244 is_ecma: _,
245 emit_decorators_metadata,
246 use_define_for_class_fields: _use_define_for_class_fields,
248 } => {
249 use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
250 let config = Config {
251 legacy: *is_legacy,
252 emit_metadata: *emit_decorators_metadata,
253 ..Default::default()
254 };
255
256 apply_transform(program, helpers, decorators(config))
257 }
258 EcmascriptInputTransform::Plugin(transform) => {
259 transform.await?.transform(program, ctx).await?;
261 helpers
262 }
263 })
264 }
265}
266
267fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {
268 let helpers = Helpers::from_data(helpers);
269 HELPERS.set(&helpers, || {
270 program.mutate(op);
271 });
272 helpers.data()
273}
274
275pub fn remove_shebang(program: &mut Program) {
276 match program {
277 Program::Module(m) => {
278 m.shebang = None;
279 }
280 Program::Script(s) => {
281 s.shebang = None;
282 }
283 }
284}
285
286pub fn remove_directives(program: &mut Program) {
287 match program {
288 Program::Module(module) => {
289 let directive_count = module
290 .body
291 .iter()
292 .take_while(|i| match i {
293 ModuleItem::Stmt(stmt) => stmt.directive_continue(),
294 ModuleItem::ModuleDecl(_) => false,
295 })
296 .take_while(|i| match i {
297 ModuleItem::Stmt(stmt) => match stmt {
298 Stmt::Expr(ExprStmt { expr, .. }) => expr
299 .as_lit()
300 .and_then(|lit| lit.as_str())
301 .and_then(|str| str.raw.as_ref())
302 .is_some_and(|raw| {
303 raw.starts_with("\"use ") || raw.starts_with("'use ")
304 }),
305 _ => false,
306 },
307 ModuleItem::ModuleDecl(_) => false,
308 })
309 .count();
310 module.body.drain(0..directive_count);
311 }
312 Program::Script(script) => {
313 let directive_count = script
314 .body
315 .iter()
316 .take_while(|stmt| stmt.directive_continue())
317 .take_while(|stmt| match stmt {
318 Stmt::Expr(ExprStmt { expr, .. }) => expr
319 .as_lit()
320 .and_then(|lit| lit.as_str())
321 .and_then(|str| str.raw.as_ref())
322 .is_some_and(|raw| raw.starts_with("\"use ") || raw.starts_with("'use ")),
323 _ => false,
324 })
325 .count();
326 script.body.drain(0..directive_count);
327 }
328 }
329}