Repository: vercel/next.js
Pull request: Show relative path from cwd for Proxy Middleware file conflict error

Show the relative path from cwd when a Proxy Middleware file conflict error occurs to better inform the users. Also, good for double-checking the detection logic.

Closes NEXT-4740

Changed files:
filename: packages/next/errors.json
patch: @@ -881,5 +881,6 @@
   "880": "Config options `experimental.proxyPrefetch` and `experimental.middlewarePrefetch` cannot be set at the same time. Please use `experimental.proxyPrefetch` instead.",
   "881": "Invalid render stage: %s",
   "882": "The %s file \"./%s\" must export a function named \\`%s\\` or a default function.",
-  "883": "The %s file \"%s\" must export a function named \\`%s\\` or a default function."
+  "883": "The %s file \"%s\" must export a function named \\`%s\\` or a default function.",
+  "884": "Both %s file \"./%s\" and %s file \"./%s\" are detected. Please use \"./%s\" only."
 }

filename: packages/next/src/build/index.ts
patch: @@ -1190,24 +1190,53 @@ export default async function build(
         .sort(sortByPageExts(config.pageExtensions))
         .map((file) => path.join(rootDir, file).replace(dir, ''))
 
-      const hasInstrumentationHook = rootPaths.some((p) =>
-        p.includes(INSTRUMENTATION_HOOK_FILENAME)
-      )
-      const hasMiddlewareFile = rootPaths.some((p) =>
-        p.includes(MIDDLEWARE_FILENAME)
-      )
-      const hasProxyFile = rootPaths.some((p) => p.includes(PROXY_FILENAME))
-      if (hasMiddlewareFile) {
-        if (hasProxyFile) {
+      let instrumentationHookFilePath: string | undefined
+      let proxyFilePath: string | undefined
+      let middlewareFilePath: string | undefined
+
+      for (const rootPath of rootPaths) {
+        const { name: fileBaseName, dir: fileDir } = path.parse(rootPath)
+        const isAtConventionLevel =
+          fileDir === '/' ||
+          fileDir === '/src' ||
+          // rootPaths are currently relative paths from the root directory.
+          // Add safety check here for unexpected future changes.
+          fileDir === dir ||
+          fileDir === path.join(dir, 'src')
+
+        if (isAtConventionLevel && fileBaseName === MIDDLEWARE_FILENAME) {
+          middlewareFilePath = rootPath
+        }
+        if (isAtConventionLevel && fileBaseName === PROXY_FILENAME) {
+          proxyFilePath = rootPath
+        }
+        if (
+          isAtConventionLevel &&
+          fileBaseName === INSTRUMENTATION_HOOK_FILENAME
+        ) {
+          instrumentationHookFilePath = rootPath
+        }
+      }
+
+      if (middlewareFilePath) {
+        if (proxyFilePath) {
+          const cwd = process.cwd()
+          const absoluteProxyPath = path.join(rootDir, proxyFilePath)
+          const absoluteMiddlewarePath = path.join(rootDir, middlewareFilePath)
+
           throw new Error(
-            `Both "${MIDDLEWARE_FILENAME}" and "${PROXY_FILENAME}" files are detected. Please use "${PROXY_FILENAME}" instead.`
+            `Both ${MIDDLEWARE_FILENAME} file "./${path.relative(cwd, absoluteMiddlewarePath)}" and ${PROXY_FILENAME} file "./${path.relative(cwd, absoluteProxyPath)}" are detected. Please use "./${path.relative(cwd, absoluteProxyPath)}" only.`
           )
         }
         Log.warnOnce(
           `The "${MIDDLEWARE_FILENAME}" file convention is deprecated. Please use "${PROXY_FILENAME}" instead.`
         )
       }
 
+      const hasInstrumentationHook = Boolean(instrumentationHookFilePath)
+      const hasMiddlewareFile = Boolean(middlewareFilePath)
+      const hasProxyFile = Boolean(proxyFilePath)
+
       NextBuildContext.hasInstrumentationHook = hasInstrumentationHook
 
       const previewProps: __ApiPreviewProps = await generatePreviewKeys({

filename: packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
patch: @@ -391,8 +391,8 @@ async function startWatcher(
         sortByPageExts(nextConfig.pageExtensions)
       )
 
-      let hasMiddlewareFile = false
-      let hasProxyFile = false
+      let proxyFilePath: string | undefined
+      let middlewareFilePath: string | undefined
 
       for (const fileName of sortedKnownFiles) {
         if (
@@ -408,16 +408,18 @@ async function startWatcher(
           fileDir === dir || fileDir === path.join(dir, 'src')
 
         if (isAtConventionLevel && fileBaseName === MIDDLEWARE_FILENAME) {
-          hasMiddlewareFile = true
+          middlewareFilePath = fileName
         }
         if (isAtConventionLevel && fileBaseName === PROXY_FILENAME) {
-          hasProxyFile = true
+          proxyFilePath = fileName
         }
 
-        if (hasMiddlewareFile) {
-          if (hasProxyFile) {
+        if (middlewareFilePath) {
+          if (proxyFilePath) {
+            const cwd = process.cwd()
+
             throw new Error(
-              `Both "${MIDDLEWARE_FILENAME}" and "${PROXY_FILENAME}" files are detected. Please use "${PROXY_FILENAME}" instead.`
+              `Both ${MIDDLEWARE_FILENAME} file "./${path.relative(cwd, middlewareFilePath)}" and ${PROXY_FILENAME} file "./${path.relative(cwd, proxyFilePath)}" are detected. Please use "./${path.relative(cwd, proxyFilePath)}" only.`
             )
           }
           Log.warnOnce(

filename: test/e2e/app-dir/proxy-with-middleware/proxy-with-middleware.test.ts
patch: @@ -12,16 +12,15 @@ describe('proxy-with-middleware', () => {
   }
 
   it('should error when both middleware and proxy files are detected', async () => {
+    const message =
+      'Both middleware file "./middleware.ts" and proxy file "./proxy.ts" are detected. Please use "./proxy.ts" only.'
+
     if (isNextDev) {
       await next.start().catch(() => {})
-      expect(next.cliOutput).toContain(
-        'Both "middleware" and "proxy" files are detected. Please use "proxy" instead.'
-      )
+      expect(next.cliOutput).toContain(message)
     } else {
       const { cliOutput } = await next.build()
-      expect(cliOutput).toContain(
-        'Both "middleware" and "proxy" files are detected. Please use "proxy" instead.'
-      )
+      expect(cliOutput).toContain(message)
     }
   })
 })