Repository: vercel/next.js
Pull request: [devtools] Fix "open in editor" for locations in stackframes

This broke when we started sending relative paths as sourcemapped locations.

Manually tested that `test/e2e/app-dir/server-source-maps/fixtures/default` can open files in stackframes and Route Info for both Webpack and Turbopack.



Changed files:
filename: packages/next/src/next-devtools/server/launch-editor.ts
patch: @@ -27,6 +27,7 @@ import fs from 'fs'
 import fsp from 'fs/promises'
 import os from 'os'
 import path from 'path'
+import { fileURLToPath } from 'url'
 import shellQuote from 'next/dist/compiled/shell-quote'
 
 function isTerminalEditor(editor: string) {
@@ -426,15 +427,33 @@ export function launchEditor(fileName: string, line1: number, column1: number) {
   }
 }
 
-// Open the file in editor if exists, otherwise return an error
+/**
+ * Open the file in editor if exists, otherwise return an error
+ * @param file `file:` URL, or absolute path or relative path. Relative paths must
+ *             relative to `nextRootDirectory`.
+ */
 export async function openFileInEditor(
-  filePath: string,
+  file: string,
   line1: number,
-  column1: number
-) {
+  column1: number,
+  nextRootDirectory: string
+): Promise<{ found: boolean; error: unknown | null }> {
+  let filePath: string
+  if (file.startsWith('file://')) {
+    try {
+      filePath = fileURLToPath(file)
+    } catch (error) {
+      return { found: false, error }
+    }
+  } else if (path.isAbsolute(file)) {
+    filePath = file
+  } else {
+    filePath = path.join(nextRootDirectory, file)
+  }
+
   const result = {
     found: false,
-    error: null as Error | null,
+    error: null as unknown | null,
   }
   const existed = await fsp.access(filePath, fs.constants.F_OK).then(
     () => true,
@@ -445,7 +464,7 @@ export async function openFileInEditor(
       launchEditor(filePath, line1, column1)
       result.found = true
     } catch (err) {
-      result.error = err as Error
+      result.error = err
     }
   }
   return result

filename: packages/next/src/server/dev/middleware-turbopack.ts
patch: @@ -370,25 +370,28 @@ export function getOverlayMiddleware({
       let openEditorResult
       if (isAppRelativePath) {
         const relativeFilePath = searchParams.get('file') || ''
-        const absoluteFilePath = path.join(
-          projectPath,
+        const appPath = path.join(
           'app',
           isSrcDir ? 'src' : '',
           relativeFilePath
         )
-        openEditorResult = await openFileInEditor(absoluteFilePath, 1, 1)
+        openEditorResult = await openFileInEditor(appPath, 1, 1, projectPath)
       } else {
         const frame = createStackFrame(searchParams)
         if (!frame) return middlewareResponse.badRequest(res)
         openEditorResult = await openFileInEditor(
           frame.file,
           frame.line ?? 1,
-          frame.column ?? 1
+          frame.column ?? 1,
+          projectPath
         )
       }
 
       if (openEditorResult.error) {
-        return middlewareResponse.internalServerError(res)
+        return middlewareResponse.internalServerError(
+          res,
+          openEditorResult.error
+        )
       }
       if (!openEditorResult.found) {
         return middlewareResponse.notFound(res)

filename: packages/next/src/server/dev/middleware-webpack.ts
patch: @@ -600,23 +600,21 @@ export function getOverlayMiddleware(options: {
       const isAppRelativePath = searchParams.get('isAppRelativePath') === '1'
       if (isAppRelativePath) {
         const relativeFilePath = searchParams.get('file') || ''
-        const absoluteFilePath = path.join(
-          rootDirectory,
+        const appPath = path.join(
           'app',
           isSrcDir ? 'src' : '',
           relativeFilePath
         )
-        openEditorResult = await openFileInEditor(absoluteFilePath, 1, 1)
+        openEditorResult = await openFileInEditor(appPath, 1, 1, rootDirectory)
       } else {
+        // TODO: How do we differentiate layers and actual file paths with round brackets?
         // frame files may start with their webpack layer, like (middleware)/middleware.js
-        const filePath = path.resolve(
-          rootDirectory,
-          frame.file.replace(/^\([^)]+\)\//, '')
-        )
+        const filePath = frame.file.replace(/^\([^)]+\)\//, '')
         openEditorResult = await openFileInEditor(
           filePath,
           frame.line1,
-          frame.column1 ?? 1
+          frame.column1 ?? 1,
+          rootDirectory
         )
       }
       if (openEditorResult.error) {