Repository: vercel/next.js
Pull request: [errors] fix edge server initial error is not sent via hmr

### Issue

When there's a webpack build error in edge runtime occurred, the error is not sent through hmr to client, until you trigger the hmr again then it would show up. This is because the error is not sent through the hmr "sync" event, but when you trigger a hmr then the "building" event is triggered again which carries the error.

### Fix

It's mostly because we have a guard for middleware file in edge compiler hmr handling, since previously we only check if it contains the middleware file then it's a legit edge compiler hmr. For other edge routes it's not from the middleware routes.

Changed files:
filename: packages/next/src/server/dev/hot-middleware.ts
patch: @@ -172,10 +172,10 @@ export class WebpackHotMiddleware {
   }
 
   onEdgeServerDone = (statsResult: webpack.Stats) => {
+    if (this.closed) return
     if (!isMiddlewareStats(statsResult)) {
       this.onServerInvalid()
       this.onServerDone(statsResult)
-      return
     }
 
     if (statsResult.hasErrors()) {

filename: test/development/app-dir/ssr-in-rsc/ssr-in-rsc.test.ts
patch: @@ -266,44 +266,48 @@ describe('react-dom/server in React Server environment', () => {
     )
 
     if (isTurbopack) {
-      await assertHasRedbox(browser)
-    } else {
-      // FIXME: why no redbox when there is an error?
-      await assertNoRedbox(browser)
-      // error happens too early it seems to be caught by browser.log()
-      // but the layout not being rendered indicates that it actually crashed
-      expect(await browser.elementByCss('body').text()).toMatchInlineSnapshot(
-        `""`
-      )
-    }
-    const redbox = {
-      description: await getRedboxDescription(browser),
-      source: await getRedboxSource(browser),
-    }
-    if (isTurbopack) {
-      expect(redbox).toMatchInlineSnapshot(`
+      expect(browser).toDisplayRedbox(`
        {
          "description": "Ecmascript file had an error",
+         "environmentLabel": null,
+         "label": "Build Error",
          "source": "./app/exports/app-code/react-dom-server-edge-implicit/page.js (3:1)
        Ecmascript file had an error
-         1 | import * as ReactDOMServerEdge from 'react-dom/server'
-         2 | // Fine to drop once React is on ESM
        > 3 | import ReactDOMServerEdgeDefault from 'react-dom/server'
-           | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-         4 |
-         5 | export const runtime = 'edge'
-         6 |
-
-       You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.
-       Learn more: https://nextjs.org/docs/app/building-your-application/rendering",
+           | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
+         "stack": [],
        }
       `)
     } else {
-      expect(redbox).toMatchInlineSnapshot(`
-        {
-          "description": null,
-          "source": null,
-        }
+      // FIXME: the source map of source file path is not correct
+      // Expected: `./app/exports/app-code/react-dom-server-edge-implicit/page.js`
+      // Observed: `./node_modules/.pnpm/next@file+..+next-repo.../page.js?__next_edge_ssr_entry__
+      expect(browser).toDisplayRedbox(`
+       {
+         "description": "Error:   x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.",
+         "environmentLabel": null,
+         "label": "Build Error",
+         "source": "<FIXME-nextjs-internal-source>
+       Error:   x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.
+         | Learn more: https://nextjs.org/docs/app/building-your-application/rendering
+          ,-[1:1]
+        1 | import * as ReactDOMServerEdge from 'react-dom/server'
+          : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+        2 | // Fine to drop once React is on ESM
+        3 | import ReactDOMServerEdgeDefault from 'react-dom/server'
+          \`----
+         x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.
+         | Learn more: https://nextjs.org/docs/app/building-your-application/rendering
+          ,-[3:1]
+        1 | import * as ReactDOMServerEdge from 'react-dom/server'
+        2 | // Fine to drop once React is on ESM
+        3 | import ReactDOMServerEdgeDefault from 'react-dom/server'
+          : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+        4 |
+        5 | export const runtime = 'edge'
+          \`----",
+         "stack": [],
+       }
       `)
     }
   })

filename: test/e2e/app-dir/use-cache-segment-configs/use-cache-segment-configs.test.ts
patch: @@ -1,10 +1,5 @@
 import { nextTestSetup } from 'e2e-utils'
-import {
-  assertHasRedbox,
-  assertNoRedbox,
-  getRedboxDescription,
-  getRedboxSource,
-} from 'next-test-utils'
+import { assertHasRedbox } from 'next-test-utils'
 import stripAnsi from 'strip-ansi'
 
 describe('use-cache-segment-configs', () => {
@@ -22,31 +17,40 @@ describe('use-cache-segment-configs', () => {
     if (isNextDev) {
       const browser = await next.browser('/runtime')
 
-      if (isTurbopack) {
-        await assertHasRedbox(browser)
-
-        const description = await getRedboxDescription(browser)
-        expect(description).toMatchInlineSnapshot(
-          `"Ecmascript file had an error"`
-        )
-        const source = await getRedboxSource(browser)
+      await assertHasRedbox(browser)
 
-        expect(source).toMatchInlineSnapshot(`
-         "./app/runtime/page.tsx (1:14)
+      if (isTurbopack) {
+        expect(browser).toDisplayRedbox(`
+         {
+           "description": "Ecmascript file had an error",
+           "environmentLabel": null,
+           "label": "Build Error",
+           "source": "./app/runtime/page.tsx (1:14)
          Ecmascript file had an error
          > 1 | export const runtime = 'edge'
-             |              ^^^^^^^
-           2 |
-           3 | export default function Page() {
-           4 |   return <div>This page uses \`export const runtime\`.</div>
-
-         Route segment config "runtime" is not compatible with \`nextConfig.experimental.useCache\`. Please remove it."
+             |              ^^^^^^^",
+           "stack": [],
+         }
         `)
       } else {
-        // TODO(veil): Figure out why dev overlay is not shown with Webpack when
-        // the runtime is 'edge'. It's possibly related to the import trace
-        // being wrong (pointing at the Webpack loader resource).
-        await assertNoRedbox(browser)
+        // FIXME: Fix broken import trace for Webpack loader resource.
+        expect(browser).toDisplayRedbox(`
+         {
+           "description": "Error:   x Route segment config "runtime" is not compatible with \`nextConfig.experimental.useCache\`. Please remove it.",
+           "environmentLabel": null,
+           "label": "Build Error",
+           "source": "<FIXME-nextjs-internal-source>
+         Error:   x Route segment config "runtime" is not compatible with \`nextConfig.experimental.useCache\`. Please remove it.
+            ,-[1:1]
+          1 | export const runtime = 'edge'
+            :              ^^^^^^^
+          2 |
+          3 | export default function Page() {
+          4 |   return <div>This page uses \`export const runtime\`.</div>
+            \`----",
+           "stack": [],
+         }
+        `)
       }
     } else {
       const { cliOutput } = await next.build()

filename: test/lib/add-redbox-matchers.ts
patch: @@ -134,6 +134,21 @@ async function createErrorSnapshot(
         '<FIXME-project-root>'
       )
     }
+
+    // This is the processed path the nextjs file from node_modules,
+    // likely not being processed properly and it's not deterministic among tests.
+    // e.g. it could be a encoded url of loader path:
+    // ../packages/next/dist/build/webpack/loaders/next-app-loader/index.js...
+    const sourceLines = focusedSource.split('\n')
+    if (
+      sourceLines[0].startsWith('./node_modules/.pnpm/next@file+') ||
+      sourceLines[0].startsWith('./node_modules/.pnpm/file+')
+    ) {
+      focusedSource =
+        `<FIXME-nextjs-internal-source>` +
+        '\n' +
+        sourceLines.slice(1).join('\n')
+    }
   }
 
   const snapshot: ErrorSnapshot = {