Skip to main content

turbopack_ecmascript_plugins/transform/
swc_ecma_transform_plugins.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use swc_core::ecma::ast::Program;
4use turbo_rcstr::{RcStr, rcstr};
5use turbo_tasks::Vc;
6use turbo_tasks_fs::FileSystemPath;
7use turbopack_core::issue::{Issue, IssueSeverity, IssueStage, OptionStyledString, StyledString};
8use turbopack_ecmascript::{CustomTransformer, TransformContext};
9
10/// A wrapper around an SWC's ecma transform wasm plugin module bytes, allowing
11/// it to operate with the turbo_tasks caching requirements.
12///
13/// Internally this contains a `CompiledPluginModuleBytes`, which points to the
14/// compiled, serialized wasmer::Module instead of raw file bytes to reduce the
15/// cost of the compilation.
16#[turbo_tasks::value(serialization = "none", eq = "manual", cell = "new", shared)]
17pub struct SwcPluginModule {
18    pub name: RcStr,
19    #[turbo_tasks(trace_ignore, debug_ignore)]
20    #[cfg(feature = "swc_ecma_transform_plugin")]
21    pub plugin: swc_core::plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
22}
23
24impl SwcPluginModule {
25    pub fn new(plugin_name: RcStr, plugin_bytes: Vec<u8>) -> Self {
26        #[cfg(feature = "swc_ecma_transform_plugin")]
27        {
28            use swc_core::plugin_runner::plugin_module_bytes::{
29                CompiledPluginModuleBytes, RawPluginModuleBytes,
30            };
31            use swc_plugin_backend_wasmer::WasmerRuntime;
32
33            Self {
34                plugin: CompiledPluginModuleBytes::from_raw_module(
35                    &WasmerRuntime,
36                    RawPluginModuleBytes::new(plugin_name.to_string(), plugin_bytes),
37                ),
38                name: plugin_name,
39            }
40        }
41
42        #[cfg(not(feature = "swc_ecma_transform_plugin"))]
43        {
44            let _ = plugin_bytes;
45            Self { name: plugin_name }
46        }
47    }
48}
49
50#[turbo_tasks::value(shared)]
51struct UnsupportedSwcEcmaTransformPluginsIssue {
52    pub file_path: FileSystemPath,
53}
54
55#[turbo_tasks::value_impl]
56impl Issue for UnsupportedSwcEcmaTransformPluginsIssue {
57    fn severity(&self) -> IssueSeverity {
58        IssueSeverity::Warning
59    }
60
61    #[turbo_tasks::function]
62    fn stage(&self) -> Vc<IssueStage> {
63        IssueStage::Transform.cell()
64    }
65
66    #[turbo_tasks::function]
67    fn title(&self) -> Vc<StyledString> {
68        StyledString::Text(rcstr!(
69            "Unsupported SWC EcmaScript transform plugins on this platform."
70        ))
71        .cell()
72    }
73
74    #[turbo_tasks::function]
75    fn file_path(&self) -> Vc<FileSystemPath> {
76        self.file_path.clone().cell()
77    }
78
79    #[turbo_tasks::function]
80    fn description(&self) -> Vc<OptionStyledString> {
81        Vc::cell(Some(
82            StyledString::Text(rcstr!(
83                "Turbopack does not yet support running SWC EcmaScript transform plugins on this \
84                 platform."
85            ))
86            .resolved_cell(),
87        ))
88    }
89}
90
91#[turbo_tasks::value(shared)]
92struct SwcEcmaTransformFailureIssue {
93    pub file_path: FileSystemPath,
94    pub description: StyledString,
95}
96
97#[turbo_tasks::value_impl]
98impl Issue for SwcEcmaTransformFailureIssue {
99    fn severity(&self) -> IssueSeverity {
100        IssueSeverity::Error
101    }
102
103    #[turbo_tasks::function]
104    fn stage(&self) -> Vc<IssueStage> {
105        IssueStage::Transform.cell()
106    }
107
108    #[turbo_tasks::function]
109    fn title(&self) -> Vc<StyledString> {
110        StyledString::Text(rcstr!("Failed to execute SWC plugin")).cell()
111    }
112
113    #[turbo_tasks::function]
114    fn file_path(&self) -> Vc<FileSystemPath> {
115        self.file_path.clone().cell()
116    }
117
118    #[turbo_tasks::function]
119    fn description(&self) -> Vc<OptionStyledString> {
120        Vc::cell(Some(
121            StyledString::Stack(vec![
122                StyledString::Text(rcstr!(
123                    "An unexpected error occurred when executing an SWC EcmaScript transform \
124                     plugin."
125                )),
126                StyledString::Text(rcstr!(
127                    "This might be due to a version mismatch between the plugin and Next.js. \
128                    https://plugins.swc.rs/ can help you find the correct plugin version to use."
129                )),
130                StyledString::Text(Default::default()),
131                self.description.clone(),
132            ])
133            .resolved_cell(),
134        ))
135    }
136}
137
138/// A custom transformer plugin to execute SWC's transform plugins.
139#[derive(Debug)]
140pub struct SwcEcmaTransformPluginsTransformer {
141    #[cfg(feature = "swc_ecma_transform_plugin")]
142    plugins: Vec<(turbo_tasks::ResolvedVc<SwcPluginModule>, serde_json::Value)>,
143}
144
145impl SwcEcmaTransformPluginsTransformer {
146    #[cfg(feature = "swc_ecma_transform_plugin")]
147    pub fn new(
148        plugins: Vec<(turbo_tasks::ResolvedVc<SwcPluginModule>, serde_json::Value)>,
149    ) -> Self {
150        Self { plugins }
151    }
152
153    // [TODO] Due to WEB-1102 putting this module itself behind compile time feature
154    // doesn't work. Instead allow to instantiate dummy instance.
155    #[cfg(not(feature = "swc_ecma_transform_plugin"))]
156    #[allow(clippy::new_without_default)]
157    pub fn new() -> Self {
158        Self {}
159    }
160}
161
162#[async_trait]
163impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
164    #[cfg_attr(not(feature = "swc_ecma_transform_plugin"), allow(unused))]
165    #[tracing::instrument(level = tracing::Level::TRACE, name = "swc_ecma_transform_plugin", skip_all)]
166    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
167        #[cfg(feature = "swc_ecma_transform_plugin")]
168        {
169            use std::{cell::RefCell, rc::Rc, sync::Arc};
170
171            use anyhow::Context;
172            use swc_core::{
173                common::{
174                    comments::SingleThreadedComments,
175                    plugin::{
176                        metadata::TransformPluginMetadataContext, serialized::PluginSerializedBytes,
177                    },
178                    util::take::Take,
179                },
180                ecma::ast::Module,
181                plugin::proxies::{COMMENTS, HostCommentsStorage},
182                plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
183            };
184            use swc_plugin_backend_wasmer::WasmerRuntime;
185            use turbo_tasks::TryJoinIterExt;
186
187            let plugins = self
188                .plugins
189                .iter()
190                .map(async |(plugin_module, config)| {
191                    let plugin_module = plugin_module.await?;
192                    Ok((
193                        plugin_module.name.clone(),
194                        config.clone(),
195                        Box::new(plugin_module.plugin.clone_module(&WasmerRuntime)),
196                    ))
197                })
198                .try_join()
199                .await?;
200
201            let should_enable_comments_proxy =
202                !ctx.comments.leading.is_empty() && !ctx.comments.trailing.is_empty();
203
204            //[TODO]: as same as swc/core does, we should set should_enable_comments_proxy
205            // depends on the src's comments availability. For now, check naively if leading
206            // / trailing comments are empty.
207            let comments = if should_enable_comments_proxy {
208                // Plugin only able to accept singlethreaded comments, interop from
209                // multithreaded comments.
210                let mut leading =
211                    swc_core::common::comments::SingleThreadedCommentsMapInner::default();
212                ctx.comments.leading.as_ref().into_iter().for_each(|c| {
213                    leading.insert(*c.key(), c.value().clone());
214                });
215
216                let mut trailing =
217                    swc_core::common::comments::SingleThreadedCommentsMapInner::default();
218                ctx.comments.trailing.as_ref().into_iter().for_each(|c| {
219                    trailing.insert(*c.key(), c.value().clone());
220                });
221
222                Some(SingleThreadedComments::from_leading_and_trailing(
223                    Rc::new(RefCell::new(leading)),
224                    Rc::new(RefCell::new(trailing)),
225                ))
226            } else {
227                None
228            };
229
230            fn transform(
231                original_serialized_program: &PluginSerializedBytes,
232                ctx: &TransformContext<'_>,
233                plugins: Vec<(RcStr, serde_json::Value, Box<CompiledPluginModuleBytes>)>,
234                should_enable_comments_proxy: bool,
235            ) -> Result<Program> {
236                use either::Either;
237
238                let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new(
239                    Some(ctx.file_path_str.to_string()),
240                    //[TODO]: Support env-related variable injection, i.e process.env.NODE_ENV
241                    "development".to_string(),
242                    None,
243                ));
244
245                let mut serialized_program = Either::Left(original_serialized_program);
246
247                // Run plugin transformation against current program.
248                // We do not serialize / deserialize between each plugin execution but
249                // copies raw transformed bytes directly into plugin's memory space.
250                // Note: This doesn't mean plugin won't perform any se/deserialization: it
251                // still have to construct from raw bytes internally to perform actual
252                // transform.
253                for (plugin_name, plugin_config, plugin_module) in plugins {
254                    let mut transform_plugin_executor =
255                        swc_core::plugin_runner::create_plugin_transform_executor(
256                            ctx.source_map,
257                            &ctx.unresolved_mark,
258                            &transform_metadata_context,
259                            None,
260                            plugin_module,
261                            Some(plugin_config),
262                            Arc::new(WasmerRuntime),
263                        );
264
265                    serialized_program = Either::Right(
266                        transform_plugin_executor
267                            .transform(
268                                serialized_program.as_ref().either(|p| *p, |p| p),
269                                Some(should_enable_comments_proxy),
270                            )
271                            .with_context(|| format!("Failed to execute {plugin_name}"))?,
272                    );
273                }
274
275                serialized_program
276                    .as_ref()
277                    .either(|p| *p, |p| p)
278                    .deserialize()
279                    .map(|v| v.into_inner())
280            }
281
282            let transformed_program =
283                COMMENTS.set(&HostCommentsStorage { inner: comments }, || {
284                    let module_program =
285                        std::mem::replace(program, Program::Module(Module::dummy()));
286                    let module_program =
287                        swc_core::common::plugin::serialized::VersionedSerializable::new(
288                            module_program,
289                        );
290                    let serialized_program = PluginSerializedBytes::try_serialize(&module_program)?;
291
292                    match transform(
293                        &serialized_program,
294                        ctx,
295                        plugins,
296                        should_enable_comments_proxy,
297                    ) {
298                        Ok(program) => anyhow::Ok(program),
299                        Err(e) => {
300                            use turbopack_core::issue::IssueExt;
301
302                            SwcEcmaTransformFailureIssue {
303                                file_path: ctx.file_path.clone(),
304                                description: StyledString::Text(format!("{:?}", e).into()),
305                            }
306                            .resolved_cell()
307                            .emit();
308
309                            // On failure, return the original program.
310                            Ok(module_program.into_inner())
311                        }
312                    }
313                })?;
314
315            *program = transformed_program;
316        }
317
318        #[cfg(not(feature = "swc_ecma_transform_plugin"))]
319        {
320            use turbopack_core::issue::IssueExt;
321
322            UnsupportedSwcEcmaTransformPluginsIssue {
323                file_path: ctx.file_path.clone(),
324            }
325            .resolved_cell()
326            .emit();
327        }
328
329        Ok(())
330    }
331}