Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions packages/next/src/lib/find-root.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { findRootDirAndLockFiles } from './find-root'

describe('findRootDirAndLockFiles()', () => {
it('ignores stray parent lockfiles without a package manifest', async () => {
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))

try {
const appDir = join(rootDir, 'app')
const parentLockfile = join(rootDir, 'package-lock.json')
const appLockfile = join(appDir, 'package-lock.json')

await mkdir(appDir)
await writeFile(parentLockfile, '{}')
await writeFile(join(appDir, 'package.json'), '{}')
await writeFile(appLockfile, '{}')

const result = findRootDirAndLockFiles(appDir)

expect(result.rootDir).toBe(appDir)
expect(result.lockFiles).toEqual([appLockfile])
} finally {
await rm(rootDir, { recursive: true, force: true })
}
})

it('ignores parent package roots that do not declare workspaces', async () => {
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))

try {
const appDir = join(rootDir, 'app')
const parentLockfile = join(rootDir, 'bun.lock')
const appLockfile = join(appDir, 'bun.lock')

await mkdir(appDir)
await writeFile(join(rootDir, 'package.json'), '{}')
await writeFile(parentLockfile, '')
await writeFile(join(appDir, 'package.json'), '{}')
await writeFile(appLockfile, '')

const result = findRootDirAndLockFiles(appDir)

expect(result.rootDir).toBe(appDir)
expect(result.lockFiles).toEqual([appLockfile])
} finally {
await rm(rootDir, { recursive: true, force: true })
}
})

it('keeps higher package roots when they declare workspaces', async () => {
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))

try {
const appDir = join(rootDir, 'apps', 'docs')
const parentLockfile = join(rootDir, 'package-lock.json')
const appLockfile = join(appDir, 'package-lock.json')

await mkdir(appDir, { recursive: true })
await writeFile(
join(rootDir, 'package.json'),
JSON.stringify({ workspaces: ['apps/*'] })
)
await writeFile(parentLockfile, '{}')
await writeFile(join(appDir, 'package.json'), '{}')
await writeFile(appLockfile, '{}')

const result = findRootDirAndLockFiles(appDir)

expect(result.rootDir).toBe(rootDir)
expect(result.lockFiles).toEqual([appLockfile, parentLockfile])
} finally {
await rm(rootDir, { recursive: true, force: true })
}
})

it('keeps higher pnpm workspace roots ahead of nested lockfiles', async () => {
const rootDir = await mkdtemp(join(tmpdir(), 'nextjs-find-root-'))

try {
const appDir = join(rootDir, 'apps', 'docs')
const workspaceFile = join(rootDir, 'pnpm-workspace.yaml')

await mkdir(appDir, { recursive: true })
await writeFile(workspaceFile, 'packages:\n - apps/*\n')
await writeFile(join(appDir, 'package.json'), '{}')
await writeFile(join(appDir, 'pnpm-lock.yaml'), '')

const result = findRootDirAndLockFiles(appDir)

expect(result.rootDir).toBe(rootDir)
expect(result.lockFiles).toEqual([workspaceFile])
} finally {
await rm(rootDir, { recursive: true, force: true })
}
})
})
84 changes: 62 additions & 22 deletions packages/next/src/lib/find-root.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,74 @@
import { dirname } from 'path'
import findUp from 'next/dist/compiled/find-up'
import { existsSync, readFileSync } from 'fs'
import { dirname, join } from 'path'
import * as Log from '../build/output/log'

function findWorkRoot(cwd: string) {
// Find-up evaluates the list of files at each level.
// 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.
const pnpmWorkspaceFile = findUp.sync(
'pnpm-workspace.yaml',
const lockFileNames = [
'pnpm-lock.yaml',
'package-lock.json',
'yarn.lock',
'bun.lock',
'bun.lockb',
]

{
cwd,
function findUpFile(
fileNames: string[],
cwd: string,
isCandidate = (_file: string) => true
) {
let currentDir = cwd

while (true) {
for (const fileName of fileNames) {
const file = join(currentDir, fileName)
if (existsSync(file) && isCandidate(file)) {
return file
}
}

const parentDir = dirname(currentDir)
if (parentDir === currentDir) {
return undefined
}
)
currentDir = parentDir
}
}

function hasPackageJson(dir: string) {
return existsSync(join(dir, 'package.json'))
}

function hasWorkspacePackageJson(dir: string) {
try {
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
const workspaces = pkg?.workspaces
return Array.isArray(workspaces)
? workspaces.length > 0
: Array.isArray(workspaces?.packages) && workspaces.packages.length > 0
} catch {
return false
}
}

function isLockFile(file: string) {
return lockFileNames.some((fileName) => file.endsWith(fileName))
}

function findWorkRoot(cwd: string, requireWorkspaceRoot = false) {
// pnpm-workspace.yaml is an explicit workspace root marker and should win
// before nested lockfiles are considered.
const pnpmWorkspaceFile = findUpFile(['pnpm-workspace.yaml'], cwd)

if (pnpmWorkspaceFile) {
return pnpmWorkspaceFile
}

return findUp.sync(
[
'pnpm-lock.yaml',
'package-lock.json',
'yarn.lock',
'bun.lock',
'bun.lockb',
],
{
cwd,
return findUpFile(lockFileNames, cwd, (file) => {
if (!isLockFile(file) || !hasPackageJson(dirname(file))) {
Comment thread
vercel[bot] marked this conversation as resolved.
return false
}
)

return !requireWorkspaceRoot || hasWorkspacePackageJson(dirname(file))
})
}

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

const newLockFile = findWorkRoot(parentDir)
const newLockFile = findWorkRoot(parentDir, true)

if (!newLockFile) break

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ describe('multiple-lockfiles - has-output-file-tracing-root', () => {
app: new FileRef(join(__dirname, 'app')),
// This will silence the multiple lockfiles warning.
'next.config.js': `module.exports = { outputFileTracingRoot: __dirname }`,
// Write a package-lock.json file to the parent directory to simulate
// multiple lockfiles.
// Write workspace metadata and a package-lock.json file to the parent
// directory to simulate multiple valid lockfiles.
'../package.json': JSON.stringify({
name: 'parent-workspace',
private: true,
workspaces: ['test'],
}),
'../package-lock.json': JSON.stringify({
name: 'parent-workspace',
version: '1.0.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ describe('multiple-lockfiles - has-turbo-root', () => {
app: new FileRef(join(__dirname, 'app')),
// This will silence the multiple lockfiles warning.
'next.config.js': `module.exports = { turbopack: { root: __dirname } }`,
// Write a package-lock.json file to the parent directory to simulate
// multiple lockfiles.
// Write workspace metadata and a package-lock.json file to the parent
// directory to simulate multiple valid lockfiles.
'../package.json': JSON.stringify({
name: 'parent-workspace',
private: true,
workspaces: ['test'],
}),
'../package-lock.json': JSON.stringify({
name: 'parent-workspace',
version: '1.0.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ describe('multiple-lockfiles', () => {
const { next, skipped, isTurbopack } = nextTestSetup({
files: {
app: new FileRef(join(__dirname, 'app')),
// Write a package-lock.json file to the parent directory to simulate
// multiple lockfiles.
// Write workspace metadata and a package-lock.json file to the parent
// directory to simulate multiple valid lockfiles.
'../package.json': JSON.stringify({
name: 'parent-workspace',
private: true,
workspaces: ['test'],
}),
'../package-lock.json': JSON.stringify({
name: 'parent-workspace',
version: '1.0.0',
Expand Down