Skip to main content

turbopack_ecmascript_plugins/transform/
swc_ecma_transform_plugins.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use swc_core::{
4    ecma::ast::Program,
5    plugin_runner::plugin_module_bytes::{CompiledPluginModuleBytes, RawPluginModuleBytes},
6};
7use swc_plugin_backend_wasmtime::WasmtimeRuntime;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks_fs::FileSystemPath;
10use turbopack_core::issue::{Issue, IssueSeverity, IssueStage, StyledString};
11use turbopack_ecmascript::{CustomTransformer, TransformContext};
12
13/// A wrapper around an SWC's ecma transform wasm plugin module bytes, allowing
14/// it to operate with the turbo_tasks caching requirements.
15///
16/// Internally this contains a `CompiledPluginModuleBytes`, which points to the
17/// compiled, serialized WASM module instead of raw file bytes to reduce the
18/// cost of the compilation.
19///
20/// Tagged `evict = "last"` so eviction prefers evicting cheaper cells first —
21/// re-deriving a compiled module is pure but pays a non-trivial WASM compile.
22#[turbo_tasks::value(
23    serialization = "skip",
24    evict = "last",
25    eq = "manual",
26    cell = "new",
27    shared
28)]
29pub struct SwcPluginModule {
30    pub name: RcStr,
31    #[turbo_tasks(trace_ignore, debug_ignore)]
32    pub plugin: swc_core::plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
33}
34
35impl SwcPluginModule {
36    pub fn new(plugin_name: RcStr, plugin_bytes: Vec<u8>) -> Self {
37        Self {
38            plugin: CompiledPluginModuleBytes::from_raw_module(
39                &WasmtimeRuntime,
40                RawPluginModuleBytes::new(plugin_name.to_string(), plugin_bytes),
41            ),
42            name: plugin_name,
43        }
44    }
45}
46
47#[turbo_tasks::value(shared)]
48struct SwcEcmaTransformFailureIssue {
49    pub file_path: FileSystemPath,
50    pub description: StyledString,
51}
52
53#[async_trait]
54#[turbo_tasks::value_impl]
55impl Issue for SwcEcmaTransformFailureIssue {
56    fn severity(&self) -> IssueSeverity {
57        IssueSeverity::Error
58    }
59
60    fn stage(&self) -> IssueStage {
61        IssueStage::Transform
62    }
63
64    async fn title(&self) -> Result<StyledString> {
65        Ok(StyledString::Text(rcstr!("Failed to execute SWC plugin")))
66    }
67
68    async fn file_path(&self) -> Result<FileSystemPath> {
69        Ok(self.file_path.clone())
70    }
71
72    async fn description(&self) -> Result<Option<StyledString>> {
73        Ok(Some(StyledString::Stack(vec![
74            StyledString::Text(rcstr!(
75                "An unexpected error occurred when executing an SWC EcmaScript transform plugin."
76            )),
77            StyledString::Text(rcstr!(
78                "This might be due to a version mismatch between the plugin and Next.js. \
79                https://plugins.swc.rs/ can help you find the correct plugin version to use."
80            )),
81            StyledString::Text(Default::default()),
82            self.description.clone(),
83        ])))
84    }
85}
86
87/// A custom transformer plugin to execute SWC's transform plugins.
88#[derive(Debug)]
89pub struct SwcEcmaTransformPluginsTransformer {
90    plugins: Vec<(turbo_tasks::ResolvedVc<SwcPluginModule>, serde_json::Value)>,
91}
92
93impl SwcEcmaTransformPluginsTransformer {
94    pub fn new(
95        plugins: Vec<(turbo_tasks::ResolvedVc<SwcPluginModule>, serde_json::Value)>,
96    ) -> Self {
97        Self { plugins }
98    }
99}
100
101#[async_trait]
102impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
103    #[tracing::instrument(level = tracing::Level::TRACE, name = "swc_ecma_transform_plugin", skip_all)]
104    async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
105        use std::{cell::RefCell, rc::Rc, sync::Arc};
106
107        use anyhow::Context;
108        use swc_core::{
109            common::{
110                comments::SingleThreadedComments,
111                plugin::{
112                    metadata::TransformPluginMetadataContext, serialized::PluginSerializedBytes,
113                },
114                util::take::Take,
115            },
116            ecma::ast::Module,
117            plugin::proxies::{COMMENTS, HostCommentsStorage},
118            plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
119        };
120        use swc_plugin_backend_wasmtime::WasmtimeRuntime;
121        use turbo_tasks::TryJoinIterExt;
122
123        let plugins = self
124            .plugins
125            .iter()
126            .map(async |(plugin_module, config)| {
127                let plugin_module = plugin_module.await?;
128                Ok((
129                    plugin_module.name.clone(),
130                    config.clone(),
131                    Box::new(plugin_module.plugin.clone_module(&WasmtimeRuntime)),
132                ))
133            })
134            .try_join()
135            .await?;
136
137        let should_enable_comments_proxy =
138            !ctx.comments.leading.is_empty() && !ctx.comments.trailing.is_empty();
139
140        //[TODO]: as same as swc/core does, we should set should_enable_comments_proxy
141        // depends on the src's comments availability. For now, check naively if leading
142        // / trailing comments are empty.
143        let comments = if should_enable_comments_proxy {
144            // Plugin only able to accept singlethreaded comments, interop from
145            // multithreaded comments.
146            let mut leading = swc_core::common::comments::SingleThreadedCommentsMapInner::default();
147            ctx.comments.leading.as_ref().into_iter().for_each(|c| {
148                leading.insert(*c.key(), c.value().clone());
149            });
150
151            let mut trailing =
152                swc_core::common::comments::SingleThreadedCommentsMapInner::default();
153            ctx.comments.trailing.as_ref().into_iter().for_each(|c| {
154                trailing.insert(*c.key(), c.value().clone());
155            });
156
157            Some(SingleThreadedComments::from_leading_and_trailing(
158                Rc::new(RefCell::new(leading)),
159                Rc::new(RefCell::new(trailing)),
160            ))
161        } else {
162            None
163        };
164
165        fn transform(
166            original_serialized_program: &PluginSerializedBytes,
167            ctx: &TransformContext<'_>,
168            plugins: Vec<(RcStr, serde_json::Value, Box<CompiledPluginModuleBytes>)>,
169            should_enable_comments_proxy: bool,
170        ) -> Result<Program> {
171            use either::Either;
172
173            let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new(
174                Some(ctx.file_path_str.to_string()),
175                ctx.node_env.to_string(),
176                None,
177            ));
178
179            let mut serialized_program = Either::Left(original_serialized_program);
180
181            // Run plugin transformation against current program.
182            // We do not serialize / deserialize between each plugin execution but
183            // copies raw transformed bytes directly into plugin's memory space.
184            // Note: This doesn't mean plugin won't perform any se/deserialization: it
185            // still have to construct from raw bytes internally to perform actual
186            // transform.
187            for (plugin_name, plugin_config, plugin_module) in plugins {
188                let mut transform_plugin_executor =
189                    swc_core::plugin_runner::create_plugin_transform_executor(
190                        ctx.source_map,
191                        &ctx.unresolved_mark,
192                        &transform_metadata_context,
193                        None,
194                        plugin_module,
195                        Some(plugin_config),
196                        Arc::new(WasmtimeRuntime),
197                    );
198
199                serialized_program = Either::Right(
200                    transform_plugin_executor
201                        .transform(
202                            serialized_program.as_ref().either(|p| *p, |p| p),
203                            Some(should_enable_comments_proxy),
204                        )
205                        .with_context(|| format!("Failed to execute {plugin_name}"))?,
206                );
207            }
208
209            serialized_program
210                .as_ref()
211                .either(|p| *p, |p| p)
212                .deserialize()
213                .map(|v| v.into_inner())
214        }
215
216        let transformed_program = COMMENTS.set(&HostCommentsStorage { inner: comments }, || {
217            let module_program = std::mem::replace(program, Program::Module(Module::dummy()));
218            let module_program =
219                swc_core::common::plugin::serialized::VersionedSerializable::new(module_program);
220            let serialized_program = PluginSerializedBytes::try_serialize(&module_program)?;
221
222            match transform(
223                &serialized_program,
224                ctx,
225                plugins,
226                should_enable_comments_proxy,
227            ) {
228                Ok(program) => anyhow::Ok(program),
229                Err(e) => {
230                    use turbopack_core::issue::IssueExt;
231
232                    // Format the error chain without backtrace.
233                    // Using `{:?}` would include the backtrace when
234                    // RUST_BACKTRACE=1, which is not useful in
235                    // user-facing error messages.
236                    let mut description = e.to_string();
237                    let mut causes = e.chain().skip(1).peekable();
238                    if causes.peek().is_some() {
239                        description.push_str("\n\nCaused by:");
240                        for (i, cause) in causes.enumerate() {
241                            description.push_str(&format!("\n    {i}: {cause}"));
242                        }
243                    }
244
245                    SwcEcmaTransformFailureIssue {
246                        file_path: ctx.file_path.clone(),
247                        description: StyledString::Text(description.into()),
248                    }
249                    .resolved_cell()
250                    .emit();
251
252                    // On failure, return the original program.
253                    Ok(module_program.into_inner())
254                }
255            }
256        })?;
257
258        *program = transformed_program;
259
260        Ok(())
261    }
262}