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, Feature, FeatureOrModule, 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 development: bool,
37 refresh: bool,
38 import_source: ResolvedVc<Option<RcStr>>,
40 runtime: ResolvedVc<Option<RcStr>>,
42 },
43 TypeScript {
46 use_define_for_class_fields: bool,
47 },
48 Decorators {
49 is_legacy: bool,
50 is_ecma: bool,
51 emit_decorators_metadata: bool,
52 use_define_for_class_fields: bool,
53 },
54}
55
56#[async_trait]
59pub trait CustomTransformer: Debug {
60 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
61}
62
63#[turbo_tasks::value(transparent, serialization = "none", eq = "manual", cell = "new")]
66#[derive(Debug)]
67pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
68
69#[async_trait]
70impl CustomTransformer for TransformPlugin {
71 async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
72 self.0.transform(program, ctx).await
73 }
74}
75
76#[turbo_tasks::value(transparent)]
77#[derive(Debug, Clone, Hash)]
78pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
79
80#[turbo_tasks::value_impl]
81impl EcmascriptInputTransforms {
82 #[turbo_tasks::function]
83 pub fn empty() -> Vc<Self> {
84 Vc::cell(Vec::new())
85 }
86
87 #[turbo_tasks::function]
88 pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
89 let mut transforms = self.owned().await?;
90 transforms.extend(other.owned().await?);
91 Ok(Vc::cell(transforms))
92 }
93}
94
95pub struct TransformContext<'a> {
96 pub comments: &'a SwcComments,
97 pub top_level_mark: Mark,
98 pub unresolved_mark: Mark,
99 pub source_map: &'a Arc<SourceMap>,
100 pub file_path_str: &'a str,
101 pub file_name_str: &'a str,
102 pub file_name_hash: u128,
103 pub query_str: RcStr,
104 pub file_path: FileSystemPath,
105 pub source: ResolvedVc<Box<dyn Source>>,
106}
107
108impl EcmascriptInputTransform {
109 pub async fn apply(
110 &self,
111 program: &mut Program,
112 ctx: &TransformContext<'_>,
113 helpers: HelperData,
114 ) -> Result<HelperData> {
115 let &TransformContext {
116 comments,
117 source_map,
118 top_level_mark,
119 unresolved_mark,
120 ..
121 } = ctx;
122
123 Ok(match self {
124 EcmascriptInputTransform::React {
125 development,
126 refresh,
127 import_source,
128 runtime,
129 } => {
130 use swc_core::ecma::transforms::react::{Options, Runtime};
131 let runtime = if let Some(runtime) = &*runtime.await? {
132 match runtime.as_str() {
133 "classic" => Runtime::Classic,
134 "automatic" => Runtime::Automatic,
135 _ => {
136 return Err(anyhow::anyhow!(
137 "Invalid value for swc.jsc.transform.react.runtime: {}",
138 runtime
139 ));
140 }
141 }
142 } else {
143 Runtime::Automatic
144 };
145
146 let config = Options {
147 runtime: Some(runtime),
148 development: Some(*development),
149 import_source: import_source.await?.as_deref().map(Atom::from),
150 refresh: if *refresh {
151 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
152 Some(swc_core::ecma::transforms::react::RefreshOptions {
153 refresh_reg: atom!("__turbopack_context__.k.register"),
154 refresh_sig: atom!("__turbopack_context__.k.signature"),
155 ..Default::default()
156 })
157 } else {
158 None
159 },
160 ..Default::default()
161 };
162
163 let helpers = apply_transform(
166 program,
167 helpers,
168 react::<&dyn Comments>(
169 source_map.clone(),
170 Some(&comments),
171 config,
172 top_level_mark,
173 unresolved_mark,
174 ),
175 );
176
177 if *refresh {
178 debug_assert_eq!(TURBOPACK_REFRESH.full, "__turbopack_context__.k");
179 debug_assert_eq!(TURBOPACK_MODULE.full, "__turbopack_context__.m");
180 let stmt = quote!(
181 "if (typeof globalThis.$RefreshHelpers$ === 'object' && \
183 globalThis.$RefreshHelpers !== null) { \
184 __turbopack_context__.k.registerExports(__turbopack_context__.m, \
185 globalThis.$RefreshHelpers$); }" as Stmt
186 );
187
188 match program {
189 Program::Module(module) => {
190 module.body.push(ModuleItem::Stmt(stmt));
191 }
192 Program::Script(script) => {
193 script.body.push(stmt);
194 }
195 }
196 }
197
198 helpers
199 }
200 EcmascriptInputTransform::PresetEnv(env) => {
201 let versions = env.runtime_versions().await?;
202 let config = swc_core::ecma::preset_env::EnvConfig::from(
203 swc_core::ecma::preset_env::Config {
204 targets: Some(Targets::Versions(*versions)),
205 mode: None, exclude: vec![
209 FeatureOrModule::Feature(Feature::ReservedWords),
210 FeatureOrModule::Feature(Feature::MemberExpressionLiterals),
211 FeatureOrModule::Feature(Feature::PropertyLiterals),
212 ],
213 ..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}