Repository: vercel/next.js
Pull request: fix: use `turbopack.root` value for `outputFileTracingRoot` to have consistent tracing root

Regressed from https://github.com/vercel/next.js/pull/82164

Previously, `outputFileTracingRoot` was always set via `findRoot()` if the option was not explicitly given. However, https://github.com/vercel/next.js/pull/82164 changed the behavior to run `findRoot()` only if neither `outputFileTracingRoot` nor `turbopack.root` was given.

Modified to:

- When BOTH options are given, ensure they match, or else WARN and use `outputFileTracingRoot`.
- When EITHER option is given, map the other value as the other.
- When NONE is given, call `findDir()` and use its value.
- After the processes above, if the root dir value is not assigned, throw an error as a Next.js bug.

Fixes https://github.com/vercel/next.js/issues/82626

Changed files:
filename: packages/next/errors.json
patch: @@ -779,5 +779,6 @@
   "778": "`prerenderAndAbortInSequentialTasksWithStages` should not be called in edge runtime.",
   "779": "Route %s used \"searchParams\" inside \"use cache\". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await \"searchParams\" outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache",
   "780": "Invariant: failed to find parent dynamic route for notFound route %s",
-  "781": "No LRU node to remove"
+  "781": "No LRU node to remove",
+  "782": "Failed to find the root directory of the project. This is a bug in Next.js."
 }

filename: packages/next/src/lib/find-root.ts
patch: @@ -17,9 +17,9 @@ export function findRootLockFile(cwd: string) {
   )
 }
 
-export function findRootDir(cwd: string) {
+export function findRootDir(cwd: string): string {
   const lockFile = findRootLockFile(cwd)
-  if (!lockFile) return undefined
+  if (!lockFile) return cwd
 
   const lockFiles = [lockFile]
   while (true) {

filename: packages/next/src/server/config.ts
patch: @@ -746,20 +746,29 @@ function assignDefaults(
     result.deploymentId = process.env.NEXT_DEPLOYMENT_ID
   }
 
-  if (result?.outputFileTracingRoot && !result?.turbopack?.root) {
-    dset(result, ['turbopack', 'root'], result.outputFileTracingRoot)
+  const tracingRoot = result?.outputFileTracingRoot
+  const turbopackRoot = result?.turbopack?.root
+
+  // If both provided, validate they match. If not, use outputFileTracingRoot.
+  if (tracingRoot && turbopackRoot && tracingRoot !== turbopackRoot) {
+    Log.warn(
+      `Both \`outputFileTracingRoot\` and \`turbopack.root\` are set, but they must have the same value.\n` +
+        `Using \`outputFileTracingRoot\` value: ${tracingRoot}.`
+    )
   }
 
-  // use the highest level lockfile as tracing root
-  if (!result?.outputFileTracingRoot && !result?.turbopack?.root) {
-    let rootDir = findRootDir(dir)
+  const rootDir = tracingRoot || turbopackRoot || findRootDir(dir)
 
-    if (rootDir) {
-      result.outputFileTracingRoot = rootDir
-      dset(result, ['turbopack', 'root'], rootDir)
-    }
+  if (!rootDir) {
+    throw new Error(
+      'Failed to find the root directory of the project. This is a bug in Next.js.'
+    )
   }
 
+  // Ensure both properties are set to the same value
+  result.outputFileTracingRoot = rootDir
+  dset(result, ['turbopack', 'root'], rootDir)
+
   setHttpClientAndAgentOptions(result || defaultConfig)
 
   if (result.i18n) {

filename: test/unit/isolated/config.test.ts
patch: @@ -117,4 +117,56 @@ describe('config', () => {
     )
     expect(config.__test__ext).toBe('ts')
   })
+
+  describe('outputFileTracingRoot and turbopack.root consistency', () => {
+    it('Should set both outputFileTracingRoot and turbopack.root to the same value when only outputFileTracingRoot is provided', async () => {
+      const config = await loadConfig(PHASE_DEVELOPMENT_SERVER, '<rootDir>', {
+        customConfig: {
+          outputFileTracingRoot: '/custom/root',
+        },
+      })
+      expect(config.outputFileTracingRoot).toBe('/custom/root')
+      expect(config.turbopack.root).toBe('/custom/root')
+    })
+
+    it('Should set both outputFileTracingRoot and turbopack.root to the same value when only turbopack.root is provided', async () => {
+      const config = await loadConfig(PHASE_DEVELOPMENT_SERVER, '<rootDir>', {
+        customConfig: {
+          turbopack: { root: '/custom/root' },
+        },
+      })
+      expect(config.outputFileTracingRoot).toBe('/custom/root')
+      expect(config.turbopack.root).toBe('/custom/root')
+    })
+
+    it('Should use outputFileTracingRoot value when both are provided with different values', async () => {
+      const config = await loadConfig(PHASE_DEVELOPMENT_SERVER, '<rootDir>', {
+        customConfig: {
+          outputFileTracingRoot: '/tracing/root',
+          turbopack: { root: '/turbo/root' },
+        },
+      })
+      expect(config.outputFileTracingRoot).toBe('/tracing/root')
+      expect(config.turbopack.root).toBe('/tracing/root')
+    })
+
+    it('Should keep the same value when both are provided with matching values', async () => {
+      const config = await loadConfig(PHASE_DEVELOPMENT_SERVER, '<rootDir>', {
+        customConfig: {
+          outputFileTracingRoot: '/same/root',
+          turbopack: { root: '/same/root' },
+        },
+      })
+      expect(config.outputFileTracingRoot).toBe('/same/root')
+      expect(config.turbopack.root).toBe('/same/root')
+    })
+
+    it('Should set both to findRootDir result when neither is provided', async () => {
+      const config = await loadConfig(PHASE_DEVELOPMENT_SERVER, '<rootDir>', {
+        customConfig: {},
+      })
+      expect(config.outputFileTracingRoot).toBeDefined()
+      expect(config.turbopack.root).toBe(config.outputFileTracingRoot)
+    })
+  })
 })