Repository: vercel/next.js
Pull request: Turbopack: improve error message for incompatible swc plugins

Before:
```
Module not found: Can't resolve 'next/dist/esm/build/templates/helpers'
failed to analyze ecmascript module '[project]/node_modules/.pnpm/next@file+..+next-repo-2bf56e024dbccc85257a8c308492f2375d690b206153b02d3bea2029707d4c38+packa_tacnzof7rtmb5bwqa7bdnfcdbq/node_modules/next/dist/esm/build/templates/helpers.js [ssr] (ecmascript)'

Caused by:
- failed to parse [project]/node_modules/.pnpm/next@file+..+next-repo-2bf56e024dbccc85257a8c308492f2375d690b206153b02d3bea2029707d4c38+packa_tacnzof7rtmb5bwqa7bdnfcdbq/node_modules/next/dist/esm/build/templates/helpers.js
- Transforming and/or parsing of [project]/node_modules/.pnpm/next@file+..+next-repo-2bf56e024dbccc85257a8c308492f2375d690b206153b02d3bea2029707d4c38+packa_tacnzof7rtmb5bwqa7bdnfcdbq/node_modules/next/dist/esm/build/templates/helpers.js failed
- failed to deserialize `swc_common::plugin::diagnostics::PluginCorePkgDiagnostics`
- Mismatch { name: "array", found: 48 }

Debug info:
- Execution of <ModuleAssetContext as AssetContext>::process_resolve_result failed
- Execution of <ModuleAssetContext as AssetContext>::process failed
- Execution of <EcmascriptModuleAsset as Module>::side_effects failed
- Execution of analyze_ecmascript_module failed
- failed to analyze ecmascript module '[project]/node_modules/.pnpm/next@file+..+next-repo-2bf56e024dbccc85257a8c308492f2375d690b206153b02d3bea2029707d4c38+packa_tacnzof7rtmb5bwqa7bdnfcdbq/node_modules/next/dist/esm/build/templates/helpers.js [ssr] (ecmascript)'
- Execution of <EcmascriptModuleAsset as EcmascriptParsable>::failsafe_parse failed
- Execution of parse failed
- failed to parse [project]/node_modules/.pnpm/next@file+..+next-repo-2bf56e024dbccc85257a8c308492f2375d690b206153b02d3bea2029707d4c38+packa_tacnzof7rtmb5bwqa7bdnfcdbq/node_modules/next/dist/esm/build/templates/helpers.js
- Transforming and/or parsing of [project]/node_modules/.pnpm/next@file+..+next-repo-2bf56e024dbccc85257a8c308492f2375d690b206153b02d3bea2029707d4c38+packa_tacnzof7rtmb5bwqa7bdnfcdbq/node_modules/next/dist/esm/build/templates/helpers.js failed
- failed to deserialize `swc_common::plugin::diagnostics::PluginCorePkgDiagnostics`
- Mismatch { name: "array", found: 48 }
Import map: aliased to module 'next' with subpath '/dist/esm/build/templates/helpers' inside of [project]/


https://nextjs.org/docs/messages/module-not-found
```

After:
```
./packages/next/document.js
Failed to execute SWC plugin
An unexpected error occurred when executing an SWC EcmaScript transform plugin.
This might be due to a version mismatch between the plugin and Next.js.

Failed to execute @swc/plugin-react-remove-properties

Caused by:
    0: failed to deserialize `swc_common::plugin::diagnostics::PluginCorePkgDiagnostics`
    1: Mismatch { name: "array", found: 48 }
```

Changed files:
filename: Cargo.lock
patch: @@ -9903,6 +9903,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "bincode 2.0.1",
+ "either",
  "indexmap 2.9.0",
  "rustc-hash 2.1.1",
  "serde",

filename: crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs
patch: @@ -139,7 +139,8 @@ pub async fn get_swc_ecma_transform_rule_impl(
                 };
 
                 Ok(Some((
-                    SwcPluginModule::new(name, file.content().to_bytes().to_vec()).resolved_cell(),
+                    SwcPluginModule::new(name.clone(), file.content().to_bytes().to_vec())
+                        .resolved_cell(),
                     config.clone(),
                 )))
             }

filename: test/e2e/swc-plugins/index.test.ts
patch: @@ -17,6 +17,44 @@ describe('swcPlugins', () => {
       expect(html).not.toContain('data-custom-attribute')
     })
   })
+  ;(isNextDev ? describe : describe.skip)('incompatible plugin version', () => {
+    const { next, skipped, isTurbopack } = nextTestSetup({
+      files: __dirname,
+      skipDeployment: true,
+      dependencies: {
+        '@swc/plugin-react-remove-properties': '7.0.2',
+      },
+    })
+    if (skipped) return
+
+    it('shows a redbox in dev', async () => {
+      const browser = await next.browser('/')
+
+      if (isTurbopack) {
+        await expect(browser).toDisplayRedbox(`
+         {
+           "description": "Failed to execute SWC plugin",
+           "environmentLabel": null,
+           "label": "Build Error",
+           "source": "./app/layout.js
+         Failed to execute SWC plugin
+         An unexpected error occurred when executing an SWC EcmaScript transform plugin.
+         This might be due to a version mismatch between the plugin and Next.js. https://plugins.swc.rs/ can help you find the correct plugin version to use.
+         Failed to execute @swc/plugin-react-remove-properties
+         Caused by:
+             0: failed to deserialize \`swc_common::plugin::diagnostics::PluginCorePkgDiagnostics\`
+             1: Mismatch { name: "array", found: 48 }",
+           "stack": [],
+         }
+        `)
+      } else {
+        // TODO missing proper error with Webpack
+        await expect(browser).toDisplayRedbox(
+          `"Expected Redbox but found no visible one."`
+        )
+      }
+    })
+  })
   ;(isNextDev ? describe : describe.skip)('invalid plugin name', () => {
     const { next, skipped, isTurbopack } = nextTestSetup({
       files: __dirname,
@@ -32,6 +70,7 @@ module.exports = {
     })
     if (skipped) return
 
+    // eslint-disable-next-line jest/no-identical-title
     it('shows a redbox in dev', async () => {
       const browser = await next.browser('/')
 

filename: turbopack/crates/turbopack-ecmascript-plugins/Cargo.toml
patch: @@ -26,6 +26,7 @@ workspace = true
 anyhow = { workspace = true }
 async-trait = { workspace = true }
 bincode = { workspace = true }
+either = { workspace = true }
 indexmap = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }

filename: turbopack/crates/turbopack-ecmascript-plugins/src/transform/swc_ecma_transform_plugins.rs
patch: @@ -1,7 +1,7 @@
 use anyhow::Result;
 use async_trait::async_trait;
 use swc_core::ecma::ast::Program;
-use turbo_rcstr::rcstr;
+use turbo_rcstr::{RcStr, rcstr};
 use turbo_tasks::Vc;
 use turbo_tasks_fs::FileSystemPath;
 use turbopack_core::issue::{Issue, IssueSeverity, IssueStage, OptionStyledString, StyledString};
@@ -14,36 +14,35 @@ use turbopack_ecmascript::{CustomTransformer, TransformContext};
 /// compiled, serialized wasmer::Module instead of raw file bytes to reduce the
 /// cost of the compilation.
 #[turbo_tasks::value(serialization = "none", eq = "manual", cell = "new", shared)]
-pub struct SwcPluginModule(
+pub struct SwcPluginModule {
+    pub name: RcStr,
     #[turbo_tasks(trace_ignore, debug_ignore)]
     #[cfg(feature = "swc_ecma_transform_plugin")]
-    pub swc_core::plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
-    // Dummy field to avoid turbo_tasks macro complaining about empty struct.
-    // This is because we can't import CompiledPluginModuleBytes by default, it should be only
-    // available for the target / platforms that support swc plugins (which can build wasmer)
-    #[cfg(not(feature = "swc_ecma_transform_plugin"))] pub (),
-);
+    pub plugin: swc_core::plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
+}
 
 impl SwcPluginModule {
-    pub fn new(plugin_name: &str, plugin_bytes: Vec<u8>) -> Self {
+    pub fn new(plugin_name: RcStr, plugin_bytes: Vec<u8>) -> Self {
         #[cfg(feature = "swc_ecma_transform_plugin")]
         {
             use swc_core::plugin_runner::plugin_module_bytes::{
                 CompiledPluginModuleBytes, RawPluginModuleBytes,
             };
             use swc_plugin_backend_wasmer::WasmerRuntime;
 
-            Self(CompiledPluginModuleBytes::from_raw_module(
-                &WasmerRuntime,
-                RawPluginModuleBytes::new(plugin_name.to_string(), plugin_bytes),
-            ))
+            Self {
+                plugin: CompiledPluginModuleBytes::from_raw_module(
+                    &WasmerRuntime,
+                    RawPluginModuleBytes::new(plugin_name.to_string(), plugin_bytes),
+                ),
+                name: plugin_name,
+            }
         }
 
         #[cfg(not(feature = "swc_ecma_transform_plugin"))]
         {
-            let _ = plugin_name;
             let _ = plugin_bytes;
-            Self(())
+            Self { name: plugin_name }
         }
     }
 }
@@ -89,6 +88,53 @@ impl Issue for UnsupportedSwcEcmaTransformPluginsIssue {
     }
 }
 
+#[turbo_tasks::value(shared)]
+struct SwcEcmaTransformFailureIssue {
+    pub file_path: FileSystemPath,
+    pub description: StyledString,
+}
+
+#[turbo_tasks::value_impl]
+impl Issue for SwcEcmaTransformFailureIssue {
+    fn severity(&self) -> IssueSeverity {
+        IssueSeverity::Error
+    }
+
+    #[turbo_tasks::function]
+    fn stage(&self) -> Vc<IssueStage> {
+        IssueStage::Transform.cell()
+    }
+
+    #[turbo_tasks::function]
+    fn title(&self) -> Vc<StyledString> {
+        StyledString::Text(rcstr!("Failed to execute SWC plugin")).cell()
+    }
+
+    #[turbo_tasks::function]
+    fn file_path(&self) -> Vc<FileSystemPath> {
+        self.file_path.clone().cell()
+    }
+
+    #[turbo_tasks::function]
+    fn description(&self) -> Vc<OptionStyledString> {
+        Vc::cell(Some(
+            StyledString::Stack(vec![
+                StyledString::Text(rcstr!(
+                    "An unexpected error occurred when executing an SWC EcmaScript transform \
+                     plugin."
+                )),
+                StyledString::Text(rcstr!(
+                    "This might be due to a version mismatch between the plugin and Next.js. \
+                    https://plugins.swc.rs/ can help you find the correct plugin version to use."
+                )),
+                StyledString::Text(Default::default()),
+                self.description.clone(),
+            ])
+            .resolved_cell(),
+        ))
+    }
+}
+
 /// A custom transformer plugin to execute SWC's transform plugins.
 #[derive(Debug)]
 pub struct SwcEcmaTransformPluginsTransformer {
@@ -122,6 +168,7 @@ impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
         {
             use std::{cell::RefCell, rc::Rc, sync::Arc};
 
+            use anyhow::Context;
             use swc_core::{
                 common::{
                     comments::SingleThreadedComments,
@@ -132,6 +179,7 @@ impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
                 },
                 ecma::ast::Module,
                 plugin::proxies::{COMMENTS, HostCommentsStorage},
+                plugin_runner::plugin_module_bytes::CompiledPluginModuleBytes,
             };
             use swc_plugin_backend_wasmer::WasmerRuntime;
             use turbo_tasks::TryJoinIterExt;
@@ -142,8 +190,9 @@ impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
                 .map(async |(plugin_module, config)| {
                     let plugin_module = plugin_module.await?;
                     Ok((
+                        plugin_module.name.clone(),
                         config.clone(),
-                        Box::new(plugin_module.0.clone_module(&WasmerRuntime)),
+                        Box::new(plugin_module.plugin.clone_module(&WasmerRuntime)),
                     ))
                 })
                 .try_join()
@@ -178,6 +227,58 @@ impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
                 None
             };
 
+            fn transform(
+                original_serialized_program: &PluginSerializedBytes,
+                ctx: &TransformContext<'_>,
+                plugins: Vec<(RcStr, serde_json::Value, Box<CompiledPluginModuleBytes>)>,
+                should_enable_comments_proxy: bool,
+            ) -> Result<Program> {
+                use either::Either;
+
+                let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new(
+                    Some(ctx.file_path_str.to_string()),
+                    //[TODO]: Support env-related variable injection, i.e process.env.NODE_ENV
+                    "development".to_string(),
+                    None,
+                ));
+
+                let mut serialized_program = Either::Left(original_serialized_program);
+
+                // Run plugin transformation against current program.
+                // We do not serialize / deserialize between each plugin execution but
+                // copies raw transformed bytes directly into plugin's memory space.
+                // Note: This doesn't mean plugin won't perform any se/deserialization: it
+                // still have to construct from raw bytes internally to perform actual
+                // transform.
+                for (plugin_name, plugin_config, plugin_module) in plugins {
+                    let mut transform_plugin_executor =
+                        swc_core::plugin_runner::create_plugin_transform_executor(
+                            ctx.source_map,
+                            &ctx.unresolved_mark,
+                            &transform_metadata_context,
+                            None,
+                            plugin_module,
+                            Some(plugin_config),
+                            Arc::new(WasmerRuntime),
+                        );
+
+                    serialized_program = Either::Right(
+                        transform_plugin_executor
+                            .transform(
+                                serialized_program.as_ref().either(|p| *p, |p| p),
+                                Some(should_enable_comments_proxy),
+                            )
+                            .with_context(|| format!("Failed to execute {plugin_name}"))?,
+                    );
+                }
+
+                serialized_program
+                    .as_ref()
+                    .either(|p| *p, |p| p)
+                    .deserialize()
+                    .map(|v| v.into_inner())
+            }
+
             let transformed_program =
                 COMMENTS.set(&HostCommentsStorage { inner: comments }, || {
                     let module_program =
@@ -186,39 +287,29 @@ impl CustomTransformer for SwcEcmaTransformPluginsTransformer {
                         swc_core::common::plugin::serialized::VersionedSerializable::new(
                             module_program,
                         );
-                    let mut serialized_program =
-                        PluginSerializedBytes::try_serialize(&module_program)?;
-
-                    let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new(
-                        Some(ctx.file_path_str.to_string()),
-                        //[TODO]: Support env-related variable injection, i.e process.env.NODE_ENV
-                        "development".to_string(),
-                        None,
-                    ));
-
-                    // Run plugin transformation against current program.
-                    // We do not serialize / deserialize between each plugin execution but
-                    // copies raw transformed bytes directly into plugin's memory space.
-                    // Note: This doesn't mean plugin won't perform any se/deserialization: it
-                    // still have to construct from raw bytes internally to perform actual
-                    // transform.
-                    for (plugin_config, plugin_module) in plugins {
-                        let mut transform_plugin_executor =
-                            swc_core::plugin_runner::create_plugin_transform_executor(
-                                ctx.source_map,
-                                &ctx.unresolved_mark,
-                                &transform_metadata_context,
-                                None,
-                                plugin_module,
-                                Some(plugin_config),
-                                Arc::new(WasmerRuntime),
-                            );
-
-                        serialized_program = transform_plugin_executor
-                            .transform(&serialized_program, Some(should_enable_comments_proxy))?;
-                    }
+                    let serialized_program = PluginSerializedBytes::try_serialize(&module_program)?;
+
+                    match transform(
+                        &serialized_program,
+                        ctx,
+                        plugins,
+                        should_enable_comments_proxy,
+                    ) {
+                        Ok(program) => anyhow::Ok(program),
+                        Err(e) => {
+                            use turbopack_core::issue::IssueExt;
 
-                    serialized_program.deserialize().map(|v| v.into_inner())
+                            SwcEcmaTransformFailureIssue {
+                                file_path: ctx.file_path.clone(),
+                                description: StyledString::Text(format!("{:?}", e).into()),
+                            }
+                            .resolved_cell()
+                            .emit();
+
+                            // On failure, return the original program.
+                            Ok(module_program.into_inner())
+                        }
+                    }
                 })?;
 
             *program = transformed_program;