turbopack_ecmascript/transform/
mod.rs

1use 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, util::take::Take},
10    ecma::{
11        ast::{Module, ModuleItem, Program, Script},
12        preset_env::{self, Targets},
13        transforms::{
14            base::{assumptions::Assumptions, helpers::inject_helpers},
15            optimization::inline_globals,
16            react::react,
17        },
18    },
19    quote,
20};
21use turbo_rcstr::RcStr;
22use turbo_tasks::{ResolvedVc, Vc};
23use turbo_tasks_fs::FileSystemPath;
24use turbopack_core::{
25    environment::Environment,
26    issue::{Issue, IssueSeverity, IssueStage, StyledString},
27};
28
29#[turbo_tasks::value(serialization = "auto_for_input")]
30#[derive(Debug, Clone, Hash)]
31pub enum EcmascriptInputTransform {
32    Plugin(ResolvedVc<TransformPlugin>),
33    PresetEnv(ResolvedVc<Environment>),
34    React {
35        #[serde(default)]
36        development: bool,
37        #[serde(default)]
38        refresh: bool,
39        // swc.jsc.transform.react.importSource
40        import_source: ResolvedVc<Option<RcStr>>,
41        // swc.jsc.transform.react.runtime,
42        runtime: ResolvedVc<Option<RcStr>>,
43    },
44    GlobalTypeofs {
45        window_value: String,
46    },
47    // These options are subset of swc_core::ecma::transforms::typescript::Config, but
48    // it doesn't derive `Copy` so repeating values in here
49    TypeScript {
50        #[serde(default)]
51        use_define_for_class_fields: bool,
52    },
53    Decorators {
54        #[serde(default)]
55        is_legacy: bool,
56        #[serde(default)]
57        is_ecma: bool,
58        #[serde(default)]
59        emit_decorators_metadata: bool,
60        #[serde(default)]
61        use_define_for_class_fields: bool,
62    },
63}
64
65/// The CustomTransformer trait allows you to implement your own custom SWC
66/// transformer to run over all ECMAScript files imported in the graph.
67#[async_trait]
68pub trait CustomTransformer: Debug {
69    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()>;
70}
71
72/// A wrapper around a TransformPlugin instance, allowing it to operate with
73/// the turbo_task caching requirements.
74#[turbo_tasks::value(
75    transparent,
76    serialization = "none",
77    eq = "manual",
78    into = "new",
79    cell = "new"
80)]
81#[derive(Debug)]
82pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
83
84#[async_trait]
85impl CustomTransformer for TransformPlugin {
86    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
87        self.0.transform(program, ctx).await
88    }
89}
90
91#[turbo_tasks::value(transparent, serialization = "auto_for_input")]
92#[derive(Debug, Clone, Hash)]
93pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
94
95#[turbo_tasks::value_impl]
96impl EcmascriptInputTransforms {
97    #[turbo_tasks::function]
98    pub fn empty() -> Vc<Self> {
99        Vc::cell(Vec::new())
100    }
101
102    #[turbo_tasks::function]
103    pub async fn extend(self: Vc<Self>, other: Vc<EcmascriptInputTransforms>) -> Result<Vc<Self>> {
104        let mut transforms = self.owned().await?;
105        transforms.extend(other.owned().await?);
106        Ok(Vc::cell(transforms))
107    }
108}
109
110pub struct TransformContext<'a> {
111    pub comments: &'a SwcComments,
112    pub top_level_mark: Mark,
113    pub unresolved_mark: Mark,
114    pub source_map: &'a Arc<SourceMap>,
115    pub file_path_str: &'a str,
116    pub file_name_str: &'a str,
117    pub file_name_hash: u128,
118    pub query_str: RcStr,
119    pub file_path: ResolvedVc<FileSystemPath>,
120}
121
122impl EcmascriptInputTransform {
123    pub async fn apply(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
124        let &TransformContext {
125            comments,
126            source_map,
127            top_level_mark,
128            unresolved_mark,
129            ..
130        } = ctx;
131        match self {
132            EcmascriptInputTransform::GlobalTypeofs { window_value } => {
133                let mut typeofs: FxHashMap<Atom, Atom> = Default::default();
134                typeofs.insert(Atom::from("window"), Atom::from(&**window_value));
135
136                program.mutate(inline_globals(
137                    unresolved_mark,
138                    Default::default(),
139                    Default::default(),
140                    Default::default(),
141                    Arc::new(typeofs),
142                ));
143            }
144            EcmascriptInputTransform::React {
145                development,
146                refresh,
147                import_source,
148                runtime,
149            } => {
150                use swc_core::ecma::transforms::react::{Options, Runtime};
151                let runtime = if let Some(runtime) = &*runtime.await? {
152                    match runtime.as_str() {
153                        "classic" => Runtime::Classic,
154                        "automatic" => Runtime::Automatic,
155                        _ => {
156                            return Err(anyhow::anyhow!(
157                                "Invalid value for swc.jsc.transform.react.runtime: {}",
158                                runtime
159                            ));
160                        }
161                    }
162                } else {
163                    Runtime::Automatic
164                };
165
166                let config = Options {
167                    runtime: Some(runtime),
168                    development: Some(*development),
169                    import_source: import_source.await?.as_deref().map(Atom::from),
170                    refresh: if *refresh {
171                        Some(swc_core::ecma::transforms::react::RefreshOptions {
172                            // __turbopack_context__.k is __turbopack_refresh__
173                            refresh_reg: atom!("__turbopack_context__.k.register"),
174                            refresh_sig: atom!("__turbopack_context__.k.signature"),
175                            ..Default::default()
176                        })
177                    } else {
178                        None
179                    },
180                    ..Default::default()
181                };
182
183                // Explicit type annotation to ensure that we don't duplicate transforms in the
184                // final binary
185                program.mutate(react::<&dyn Comments>(
186                    source_map.clone(),
187                    Some(&comments),
188                    config,
189                    top_level_mark,
190                    unresolved_mark,
191                ));
192
193                if *refresh {
194                    let stmt = quote!(
195                        // AMP / No-JS mode does not inject these helpers
196                        "\nif (typeof globalThis.$RefreshHelpers$ === 'object' && \
197                         globalThis.$RefreshHelpers !== null) { \
198                         __turbopack_context__.k.registerExports(module, \
199                         globalThis.$RefreshHelpers$); }\n" as Stmt
200                    );
201
202                    match program {
203                        Program::Module(module) => {
204                            module.body.push(ModuleItem::Stmt(stmt));
205                        }
206                        Program::Script(script) => {
207                            script.body.push(stmt);
208                        }
209                    }
210                }
211            }
212            EcmascriptInputTransform::PresetEnv(env) => {
213                let versions = env.runtime_versions().await?;
214                let config = swc_core::ecma::preset_env::EnvConfig::from(
215                    swc_core::ecma::preset_env::Config {
216                        targets: Some(Targets::Versions(*versions)),
217                        mode: None, // Don't insert core-js polyfills
218                        ..Default::default()
219                    },
220                );
221
222                let module_program = std::mem::replace(program, Program::Module(Module::dummy()));
223
224                let module_program = if let Program::Script(Script {
225                    span,
226                    mut body,
227                    shebang,
228                }) = module_program
229                {
230                    Program::Module(Module {
231                        span,
232                        body: body.drain(..).map(ModuleItem::Stmt).collect(),
233                        shebang,
234                    })
235                } else {
236                    module_program
237                };
238
239                // Explicit type annotation to ensure that we don't duplicate transforms in the
240                // final binary
241                *program = module_program.apply((
242                    preset_env::transform_from_env::<&'_ dyn Comments>(
243                        top_level_mark,
244                        Some(&comments),
245                        config,
246                        Assumptions::default(),
247                    ),
248                    inject_helpers(unresolved_mark),
249                ));
250            }
251            EcmascriptInputTransform::TypeScript {
252                // TODO(WEB-1213)
253                use_define_for_class_fields: _use_define_for_class_fields,
254            } => {
255                use swc_core::ecma::transforms::typescript::typescript;
256                let config = Default::default();
257                program.mutate(typescript(config, unresolved_mark, top_level_mark));
258            }
259            EcmascriptInputTransform::Decorators {
260                is_legacy,
261                is_ecma: _,
262                emit_decorators_metadata,
263                // TODO(WEB-1213)
264                use_define_for_class_fields: _use_define_for_class_fields,
265            } => {
266                use swc_core::ecma::transforms::proposal::decorators::{Config, decorators};
267                let config = Config {
268                    legacy: *is_legacy,
269                    emit_metadata: *emit_decorators_metadata,
270                    ..Default::default()
271                };
272
273                program.mutate((decorators(config), inject_helpers(unresolved_mark)));
274            }
275            EcmascriptInputTransform::Plugin(transform) => {
276                transform.await?.transform(program, ctx).await?
277            }
278        }
279        Ok(())
280    }
281}
282
283pub fn remove_shebang(program: &mut Program) {
284    match program {
285        Program::Module(m) => {
286            m.shebang = None;
287        }
288        Program::Script(s) => {
289            s.shebang = None;
290        }
291    }
292}
293
294#[turbo_tasks::value(shared)]
295pub struct UnsupportedServerActionIssue {
296    pub file_path: ResolvedVc<FileSystemPath>,
297}
298
299#[turbo_tasks::value_impl]
300impl Issue for UnsupportedServerActionIssue {
301    #[turbo_tasks::function]
302    fn severity(&self) -> Vc<IssueSeverity> {
303        IssueSeverity::Error.into()
304    }
305
306    #[turbo_tasks::function]
307    fn title(&self) -> Vc<StyledString> {
308        StyledString::Text(
309            "Server actions (\"use server\") are not yet supported in Turbopack".into(),
310        )
311        .cell()
312    }
313
314    #[turbo_tasks::function]
315    fn file_path(&self) -> Vc<FileSystemPath> {
316        *self.file_path
317    }
318
319    #[turbo_tasks::function]
320    fn stage(&self) -> Vc<IssueStage> {
321        IssueStage::Transform.cell()
322    }
323}