Skip to content

Commit 68bb9a9

Browse files
Fix workspace root selection for stray parent lockfiles
1 parent 23b1977 commit 68bb9a9

2 files changed

Lines changed: 160 additions & 22 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
2+
import { tmpdir } from 'node:os'
3+
import { join } from 'node:path'
4+
import { findRootDirAndLockFiles } from './find-root'
5+
6+
describe('findRootDirAndLockFiles()', () => {
7+
it('ignores stray parent lockfiles without a package manifest', async () => {
8+
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))
9+
10+
try {
11+
const appDir = join(rootDir, 'app')
12+
const parentLockfile = join(rootDir, 'package-lock.json')
13+
const appLockfile = join(appDir, 'package-lock.json')
14+
15+
await mkdir(appDir)
16+
await writeFile(parentLockfile, '{}')
17+
await writeFile(join(appDir, 'package.json'), '{}')
18+
await writeFile(appLockfile, '{}')
19+
20+
const result = findRootDirAndLockFiles(appDir)
21+
22+
expect(result.rootDir).toBe(appDir)
23+
expect(result.lockFiles).toEqual([appLockfile])
24+
} finally {
25+
await rm(rootDir, { recursive: true, force: true })
26+
}
27+
})
28+
29+
it('ignores parent package roots that do not declare workspaces', async () => {
30+
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))
31+
32+
try {
33+
const appDir = join(rootDir, 'app')
34+
const parentLockfile = join(rootDir, 'bun.lock')
35+
const appLockfile = join(appDir, 'bun.lock')
36+
37+
await mkdir(appDir)
38+
await writeFile(join(rootDir, 'package.json'), '{}')
39+
await writeFile(parentLockfile, '')
40+
await writeFile(join(appDir, 'package.json'), '{}')
41+
await writeFile(appLockfile, '')
42+
43+
const result = findRootDirAndLockFiles(appDir)
44+
45+
expect(result.rootDir).toBe(appDir)
46+
expect(result.lockFiles).toEqual([appLockfile])
47+
} finally {
48+
await rm(rootDir, { recursive: true, force: true })
49+
}
50+
})
51+
52+
it('keeps higher package roots when they declare workspaces', async () => {
53+
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))
54+
55+
try {
56+
const appDir = join(rootDir, 'apps', 'docs')
57+
const parentLockfile = join(rootDir, 'package-lock.json')
58+
const appLockfile = join(appDir, 'package-lock.json')
59+
60+
await mkdir(appDir, { recursive: true })
61+
await writeFile(
62+
join(rootDir, 'package.json'),
63+
JSON.stringify({ workspaces: ['apps/*'] })
64+
)
65+
await writeFile(parentLockfile, '{}')
66+
await writeFile(join(appDir, 'package.json'), '{}')
67+
await writeFile(appLockfile, '{}')
68+
69+
const result = findRootDirAndLockFiles(appDir)
70+
71+
expect(result.rootDir).toBe(rootDir)
72+
expect(result.lockFiles).toEqual([appLockfile, parentLockfile])
73+
} finally {
74+
await rm(rootDir, { recursive: true, force: true })
75+
}
76+
})
77+
78+
it('keeps higher pnpm workspace roots ahead of nested lockfiles', async () => {
79+
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))
80+
81+
try {
82+
const appDir = join(rootDir, 'apps', 'docs')
83+
const workspaceFile = join(rootDir, 'pnpm-workspace.yaml')
84+
85+
await mkdir(appDir, { recursive: true })
86+
await writeFile(workspaceFile, 'packages:\n - apps/*\n')
87+
await writeFile(join(appDir, 'package.json'), '{}')
88+
await writeFile(join(appDir, 'pnpm-lock.yaml'), '')
89+
90+
const result = findRootDirAndLockFiles(appDir)
91+
92+
expect(result.rootDir).toBe(rootDir)
93+
expect(result.lockFiles).toEqual([workspaceFile])
94+
} finally {
95+
await rm(rootDir, { recursive: true, force: true })
96+
}
97+
})
98+
})

packages/next/src/lib/find-root.ts

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,74 @@
1-
import { dirname } from 'path'
2-
import findUp from 'next/dist/compiled/find-up'
1+
import { existsSync, readFileSync } from 'fs'
2+
import { dirname, join } from 'path'
33
import * as Log from '../build/output/log'
44

5-
function findWorkRoot(cwd: string) {
6-
// Find-up evaluates the list of files at each level.
7-
// For pnpm-workspace.yaml we first want to look up before searching for lockfiles as those can be included in the application directory by accident.
8-
const pnpmWorkspaceFile = findUp.sync(
9-
'pnpm-workspace.yaml',
5+
const lockFileNames = [
6+
'pnpm-lock.yaml',
7+
'package-lock.json',
8+
'yarn.lock',
9+
'bun.lock',
10+
'bun.lockb',
11+
]
1012

11-
{
12-
cwd,
13+
function findUpFile(
14+
fileNames: string[],
15+
cwd: string,
16+
isCandidate = (_file: string) => true
17+
) {
18+
let currentDir = cwd
19+
20+
while (true) {
21+
for (const fileName of fileNames) {
22+
const file = join(currentDir, fileName)
23+
if (existsSync(file) && isCandidate(file)) {
24+
return file
25+
}
26+
}
27+
28+
const parentDir = dirname(currentDir)
29+
if (parentDir === currentDir) {
30+
return undefined
1331
}
14-
)
32+
currentDir = parentDir
33+
}
34+
}
35+
36+
function hasPackageJson(dir: string) {
37+
return existsSync(join(dir, 'package.json'))
38+
}
39+
40+
function hasWorkspacePackageJson(dir: string) {
41+
try {
42+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
43+
const workspaces = pkg?.workspaces
44+
return Array.isArray(workspaces)
45+
? workspaces.length > 0
46+
: Array.isArray(workspaces?.packages) && workspaces.packages.length > 0
47+
} catch {
48+
return false
49+
}
50+
}
51+
52+
function isLockFile(file: string) {
53+
return lockFileNames.some((fileName) => file.endsWith(fileName))
54+
}
55+
56+
function findWorkRoot(cwd: string, requireWorkspaceRoot = false) {
57+
// pnpm-workspace.yaml is an explicit workspace root marker and should win
58+
// before nested lockfiles are considered.
59+
const pnpmWorkspaceFile = findUpFile(['pnpm-workspace.yaml'], cwd)
1560

1661
if (pnpmWorkspaceFile) {
1762
return pnpmWorkspaceFile
1863
}
1964

20-
return findUp.sync(
21-
[
22-
'pnpm-lock.yaml',
23-
'package-lock.json',
24-
'yarn.lock',
25-
'bun.lock',
26-
'bun.lockb',
27-
],
28-
{
29-
cwd,
65+
return findUpFile(lockFileNames, cwd, (file) => {
66+
if (!isLockFile(file) || !hasPackageJson(dirname(file))) {
67+
return false
3068
}
31-
)
69+
70+
return !requireWorkspaceRoot || hasWorkspacePackageJson(dirname(file))
71+
})
3272
}
3373

3474
export function findRootDirAndLockFiles(cwd: string): {
@@ -51,7 +91,7 @@ export function findRootDirAndLockFiles(cwd: string): {
5191
// dirname('/')==='/' so if we happen to reach the FS root (as might happen in a container we need to quit to avoid looping forever
5292
if (parentDir === currentDir) break
5393

54-
const newLockFile = findWorkRoot(parentDir)
94+
const newLockFile = findWorkRoot(parentDir, true)
5595

5696
if (!newLockFile) break
5797

0 commit comments

Comments
 (0)