Files
Sam PoderandGitHub dcf4607f8f Improve root detection to handle worktrees, more workspaces & stray lockfiles (#96159)
Watching too many files can hurt performance, and impacts how much work
Turbopack is doing. See for example the conversation around
https://github.com/vercel/next.js/pull/94597.

This PR handles the following four cases:

- Git worktrees: we can include a guard to prevent exiting a worktree
(by looking at the `.git` file).
- Git repository boundaries: we can include a guard to prevent us from
exiting a checked-out repository and watching the repository’s parent
directory.
    - We will log a warning if this happens.
- Empty / bogus `package.lock` files & `package-lock.json` files (and
equivalents using other package managers): if we spot a
`package-lock.json` file but it has no packages or a `package-lock.json`
w/o an associated `package.json` file. We can ignore them. This handles
the case of accidentally running `npm install` in any directory.
- Home directory detection: we can detect if we’re going to start
watching your home dir and rollback up if this is the case.
    - We will log a warning if this happens.
- If your Next config is in your home dir, we will still run your app
there as Next config is the furthest we can rollback
- NPM, Yarn, Lerna & Bun workspaces: based on the `workspaces` field in
the `package.json` or `lerna.json`

I would recommend using split-view to review. I also cleaned up the
`warnDuplicatedLockFiles()` function because it was a bit unnecessary to
repeat the full message.

Closes https://github.com/vercel/next.js/issues/92978
2026-07-28 13:35:17 -07:00

73 lines
2.1 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import { nextTestSetup } from 'e2e-utils'
import type { NextAdapter } from 'next'
describe('adapter-root', () => {
describe.each([
{ name: 'via lockfile', env: {} },
// The Vercel CLI sets this to the repo root
{ name: 'via NEXT_PRIVATE_OUTPUT_TRACE_ROOT', setEnvVar: true },
])('$name', ({ setEnvVar }) => {
const { next } = nextTestSetup({
files: path.join(__dirname, 'fixture'),
subDir: 'sub',
skipStart: true,
// The workspace file would be treated as the workspace root, changing
// the repoRoot this test asserts on.
deleteWorkspaceFile: true,
overrideFiles: setEnvVar
? undefined
: {
'../package.json': JSON.stringify({
name: 'parent-workspace',
version: '1.0.0',
}),
'../package-lock.json': JSON.stringify({
name: 'parent-workspace',
version: '1.0.0',
lockfileVersion: 3,
packages: { '': { name: 'parent-workspace', version: '1.0.0' } },
}),
},
})
it('should correctly determine repoRoot', async () => {
const expectedRepoRoot = path.dirname(next.testDir)
if (setEnvVar) {
next.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = expectedRepoRoot
}
await next.build()
expect(next.cliOutput).not.toContain(
'We detected multiple lockfiles and selected the directory'
)
const {
outputs,
repoRoot,
projectDir,
}: Parameters<NextAdapter['onBuildComplete']>[0] = await next.readJSON(
'build-complete.json'
)
expect(projectDir).toBe(next.testDir)
expect(repoRoot).toBe(expectedRepoRoot)
const combinedRouteOutputs = [
...outputs.appPages,
...outputs.appRoutes,
...outputs.pages,
...outputs.pagesApi,
]
for (const output of combinedRouteOutputs) {
for (const [asset, source] of Object.entries(output.assets)) {
expect(asset).toStartWith('sub/')
expect(fs.existsSync(source)).toBeTrue()
}
}
})
})
})