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::sync::Arc;
106
107        use anyhow::Context;
108        use swc_core::{
109            common::{
110                plugin::{
111                    metadata::TransformPluginMetadataContext, serialized::PluginSerializedBytes,
112                },
113                util::take::Take,
114            },
115            ecma::ast::Module,
116            plugin::proxies::{COMMENTS, HostCommentsStorage},
117            plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
118        };
119        use swc_plugin_backend_wasmtime::WasmtimeRuntime;
120        use turbo_tasks::TryJoinIterExt;
121
122        let plugins = self
123            .plugins
124            .iter()
125            .map(async |(plugin_module, config)| {
126                let plugin_module = plugin_module.await?;
127                Ok((
128                    plugin_module.name.clone(),
129                    config.clone(),
130                    Box::new(plugin_module.plugin.clone_module(&WasmtimeRuntime)),
131                ))
132            })
133            .try_join()
134            .await?;
135
136        let should_enable_comments_proxy =
137            !ctx.comments.leading.is_empty() && !ctx.comments.trailing.is_empty();
138
139        //[TODO]: as same as swc/core does, we should set should_enable_comments_proxy
140        // depends on the src's comments availability. For now, check naively if leading
141        // / trailing comments are empty.
142        let comments = if should_enable_comments_proxy {
143            Some(turbopack_ecmascript::swc_comments_to_single_threaded(
144                ctx.comments,
145            ))
146        } else {
147            None
148        };
149
150        fn transform(
151            original_serialized_program: &PluginSerializedBytes,
152            ctx: &TransformContext<'_>,
153            plugins: Vec<(RcStr, serde_json::Value, Box<CompiledPluginModuleBytes>)>,
154            should_enable_comments_proxy: bool,
155        ) -> Result<Program> {
156            use either::Either;
157
158            let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new(
159                Some(ctx.file_path_str.to_string()),
160                ctx.node_env.to_string(),
161                None,
162            ));
163
164            let mut serialized_program = Either::Left(original_serialized_program);
165
166            // Run plugin transformation against current program.
167            // We do not serialize / deserialize between each plugin execution but
168            // copies raw transformed bytes directly into plugin's memory space.
169            // Note: This doesn't mean plugin won't perform any se/deserialization: it
170            // still have to construct from raw bytes internally to perform actual
171            // transform.
172            for (plugin_name, plugin_config, plugin_module) in plugins {
173                let mut transform_plugin_executor =
174                    swc_core::plugin_runner::create_plugin_transform_executor(
175                        ctx.source_map,
176                        &ctx.unresolved_mark,
177                        &transform_metadata_context,
178                        None,
179                        plugin_module,
180                        Some(plugin_config),
181                        Arc::new(WasmtimeRuntime),
182                    );
183
184                serialized_program = Either::Right(
185                    transform_plugin_executor
186                        .transform(
187                            serialized_program.as_ref().either(|p| *p, |p| p),
188                            Some(should_enable_comments_proxy),
189                        )
190                        .with_context(|| format!("Failed to execute {plugin_name}"))?,
191                );
192            }
193
194            serialized_program
195                .as_ref()
196                .either(|p| *p, |p| p)
197                .deserialize()
198                .map(|v| v.into_inner())
199        }
200
201        let transformed_program = COMMENTS.set(&HostCommentsStorage { inner: comments }, || {
202            let module_program = std::mem::replace(program, Program::Module(Module::dummy()));
203            let module_program =
204                swc_core::common::plugin::serialized::VersionedSerializable::new(module_program);
205            let serialized_program = PluginSerializedBytes::try_serialize(&module_program)?;
206
207            match transform(
208                &serialized_program,
209                ctx,
210                plugins,
211                should_enable_comments_proxy,
212            ) {
213                Ok(program) => anyhow::Ok(program),
214                Err(e) => {
215                    use turbopack_core::issue::IssueExt;
216
217                    // Format the error chain without backtrace.
218                    // Using `{:?}` would include the backtrace when
219                    // RUST_BACKTRACE=1, which is not useful in
220                    // user-facing error messages.
221                    let mut description = e.to_string();
222                    let mut causes = e.chain().skip(1).peekable();
223                    if causes.peek().is_some() {
224                        description.push_str("\n\nCaused by:");
225                        for (i, cause) in causes.enumerate() {
226                            description.push_str(&format!("\n    {i}: {cause}"));
227                        }
228                    }
229
230                    SwcEcmaTransformFailureIssue {
231                        file_path: ctx.file_path.clone(),
232                        description: StyledString::Text(description.into()),
233                    }
234                    .resolved_cell()
235                    .emit();
236
237                    // On failure, return the original program.
238                    Ok(module_program.into_inner())
239                }
240            }
241        })?;
242
243        *program = transformed_program;
244
245        Ok(())
246    }
247}