Repository: vercel/next.js
Pull request: Turbopack: proper error message for swcPlugins

Show a redbox when specifying a non-existent plugin instead of crashing Turbopack

The error location could be better, but we don't actually know the line/column here

![Bildschirmfoto 2025-04-10 um 08 56 39](https://github.com/user-attachments/assets/c816704d-b5c6-48bc-8471-92082abe5614)


Closes PACK-4329

Changed files:
filename: crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs
patch: @@ -35,8 +35,8 @@ pub async fn get_swc_ecma_transform_rule_impl(
     plugin_configs: &[(RcStr, serde_json::Value)],
     enable_mdx_rs: bool,
 ) -> Result<Option<ModuleRule>> {
-    use anyhow::{bail, Context};
-    use turbo_tasks::{TryJoinIterExt, Value};
+    use anyhow::bail;
+    use turbo_tasks::{TryFlatJoinIterExt, Value};
     use turbo_tasks_fs::FileContent;
     use turbopack::{resolve_options, resolve_options_context::ResolveOptionsContext};
     use turbopack_core::{
@@ -78,30 +78,34 @@ pub async fn get_swc_ecma_transform_rule_impl(
                 )
                 .as_raw_module_result(),
                 Value::new(ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined)),
+                // TODO proper error location
                 *project_path,
                 request,
                 resolve_options,
                 false,
+                // TODO proper error location
                 None,
             )
             .await?;
-            let plugin_module = plugin_wasm_module_resolve_result
-                .first_module()
-                .await?
-                .context("Expected to find module")?;
 
-            let content = &*plugin_module.content().file_content().await?;
+            let Some(plugin_module) = &*plugin_wasm_module_resolve_result.first_module().await?
+            else {
+                // Ignore unresolveable plugin modules, handle_resolve_error has already emitted an
+                // issue.
+                return Ok(None);
+            };
 
+            let content = &*plugin_module.content().file_content().await?;
             let FileContent::Content(file) = content else {
                 bail!("Expected file content for plugin module");
             };
 
-            Ok((
+            Ok(Some((
                 SwcPluginModule::new(name, file.content().to_bytes()?.to_vec()).resolved_cell(),
                 config.clone(),
-            ))
+            )))
         })
-        .try_join()
+        .try_flat_join()
         .await?;
 
     Ok(Some(get_ecma_transform_rule(

filename: test/e2e/swc-plugins/app/layout.js
patch: @@ -0,0 +1,9 @@
+import React from 'react'
+
+export default function RootLayout({ children }) {
+  return (
+    <html lang="en">
+      <body>{children}</body>
+    </html>
+  )
+}

filename: test/e2e/swc-plugins/app/page.js
patch: @@ -0,0 +1,5 @@
+import React from 'react'
+
+export default function Page({ children }) {
+  return <div data-custom-attribute>Hello World</div>
+}

filename: test/e2e/swc-plugins/index.test.ts
patch: @@ -0,0 +1,58 @@
+import { nextTestSetup, isNextDev } from 'e2e-utils'
+
+describe('swcPlugins', () => {
+  describe('supports swcPlugins', () => {
+    const { next, skipped } = nextTestSetup({
+      files: __dirname,
+      skipDeployment: true,
+      dependencies: {
+        '@swc/plugin-react-remove-properties': '7.0.2',
+      },
+    })
+    if (skipped) return
+
+    it('basic case', async () => {
+      const html = await next.render('/')
+      expect(html).toContain('Hello World')
+      expect(html).not.toContain('data-custom-attribute')
+    })
+  })
+  ;(isNextDev ? describe : describe.skip)('invalid plugin name', () => {
+    const { next, skipped, isTurbopack } = nextTestSetup({
+      files: __dirname,
+      skipDeployment: true,
+      overrideFiles: {
+        'next.config.js': `
+module.exports = {
+  experimental: {
+    swcPlugins: [['@swc/plugin-nonexistent', {}]],
+  },
+}`,
+      },
+    })
+    if (skipped) return
+
+    it('shows a redbox in dev', async () => {
+      const browser = await next.browser('/')
+
+      if (isTurbopack) {
+        await expect(browser).toDisplayRedbox(`
+         {
+           "description": "Module not found: Can't resolve '@swc/plugin-nonexistent'",
+           "environmentLabel": null,
+           "label": "Build Error",
+           "source": "./
+         Module not found: Can't resolve '@swc/plugin-nonexistent'
+         https://nextjs.org/docs/messages/module-not-found",
+           "stack": [],
+         }
+        `)
+      } else {
+        // TODO missing proper error with Webpack
+        await expect(browser).toDisplayRedbox(
+          `"Expected Redbox but found no visible one."`
+        )
+      }
+    })
+  })
+})

filename: test/e2e/swc-plugins/next.config.js
patch: @@ -0,0 +1,13 @@
+/** @type {import('next').NextConfig} */
+module.exports = {
+  experimental: {
+    swcPlugins: [
+      [
+        '@swc/plugin-react-remove-properties',
+        {
+          properties: ['^data-custom-attribute$'],
+        },
+      ],
+    ],
+  },
+}