Skip to content

Commit 127ec70

Browse files
committed
fix(nuxi): resolve upgrade lockfile across workspace and app cwd
1 parent 9face47 commit 127ec70

File tree

2 files changed

+238
-5
lines changed

2 files changed

+238
-5
lines changed

packages/nuxi/src/commands/upgrade.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export default defineCommand({
139139
// Force install
140140
const toRemove = ['node_modules']
141141

142-
const lockFile = normaliseLockFile(workspaceDir, lockFileCandidates)
142+
const lockFile = normaliseLockFile([workspaceDir, cwd], lockFileCandidates)
143143
if (lockFile) {
144144
toRemove.push(lockFile)
145145
}
@@ -269,16 +269,22 @@ export default defineCommand({
269269
},
270270
})
271271

272-
// Find which lock file is in use since `nypm.detectPackageManager` doesn't return this
273-
function normaliseLockFile(cwd: string, lockFiles: string | Array<string> | undefined) {
272+
// Find which lock file is in use since `nypm.detectPackageManager` doesn't return this.
273+
export function normaliseLockFile(cwds: string | Array<string>, lockFiles: string | Array<string> | undefined) {
274+
if (typeof cwds === 'string') {
275+
cwds = [cwds]
276+
}
277+
const searchDirs = [...new Set(cwds)]
278+
274279
if (typeof lockFiles === 'string') {
275280
lockFiles = [lockFiles]
276281
}
277282

278-
const lockFile = lockFiles?.find(file => existsSync(resolve(cwd, file)))
283+
const lockFile = lockFiles?.find(file => searchDirs.some(cwd => existsSync(resolve(cwd, file))))
279284

280285
if (lockFile === undefined) {
281-
logger.error(`Unable to find any lock files in ${colors.cyan(relativeToProcess(cwd))}.`)
286+
const resolvedDirs = searchDirs.map(cwd => colors.cyan(relativeToProcess(cwd)))
287+
logger.error(`Unable to find any lock files in ${resolvedDirs.join(', ')}.`)
282288
return undefined
283289
}
284290

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import upgradeCommand, { normaliseLockFile } from '../../../src/commands/upgrade'
3+
4+
const {
5+
existsSync,
6+
loggerError,
7+
loggerStep,
8+
loggerSuccess,
9+
loggerInfo,
10+
detectPackageManager,
11+
addDependency,
12+
dedupeDependencies,
13+
findWorkspaceDir,
14+
readPackageJSON,
15+
getNuxtVersion,
16+
cleanupNuxtDirs,
17+
loadKit,
18+
getPackageManagerVersion,
19+
intro,
20+
note,
21+
outro,
22+
cancel,
23+
select,
24+
isCancel,
25+
tasks,
26+
spinStart,
27+
spinStop,
28+
nuxtVersionToGitIdentifier,
29+
} = vi.hoisted(() => {
30+
return {
31+
existsSync: vi.fn(),
32+
loggerError: vi.fn(),
33+
loggerStep: vi.fn(),
34+
loggerSuccess: vi.fn(),
35+
loggerInfo: vi.fn(),
36+
detectPackageManager: vi.fn(),
37+
addDependency: vi.fn(),
38+
dedupeDependencies: vi.fn(),
39+
findWorkspaceDir: vi.fn(),
40+
readPackageJSON: vi.fn(),
41+
getNuxtVersion: vi.fn(),
42+
cleanupNuxtDirs: vi.fn(),
43+
loadKit: vi.fn(),
44+
getPackageManagerVersion: vi.fn(),
45+
intro: vi.fn(),
46+
note: vi.fn(),
47+
outro: vi.fn(),
48+
cancel: vi.fn(),
49+
select: vi.fn(),
50+
isCancel: vi.fn(() => false),
51+
tasks: vi.fn(async (taskEntries: Array<{ task?: () => Promise<unknown> }>) => {
52+
for (const taskEntry of taskEntries) {
53+
await taskEntry.task?.()
54+
}
55+
}),
56+
spinStart: vi.fn(),
57+
spinStop: vi.fn(),
58+
nuxtVersionToGitIdentifier: vi.fn((version: string) => version),
59+
}
60+
})
61+
62+
vi.mock('node:fs', async () => {
63+
const actual = await vi.importActual<typeof import('node:fs')>('node:fs')
64+
return {
65+
...actual,
66+
existsSync,
67+
}
68+
})
69+
70+
vi.mock('@clack/prompts', async () => {
71+
return {
72+
intro,
73+
note,
74+
outro,
75+
cancel,
76+
select,
77+
isCancel,
78+
tasks,
79+
spinner: () => ({
80+
start: spinStart,
81+
stop: spinStop,
82+
}),
83+
}
84+
})
85+
86+
vi.mock('nypm', async () => {
87+
return {
88+
detectPackageManager,
89+
addDependency,
90+
dedupeDependencies,
91+
}
92+
})
93+
94+
vi.mock('pkg-types', async () => {
95+
return {
96+
findWorkspaceDir,
97+
readPackageJSON,
98+
}
99+
})
100+
101+
vi.mock('../../../src/utils/versions', async () => {
102+
return {
103+
getNuxtVersion,
104+
}
105+
})
106+
107+
vi.mock('../../../src/utils/nuxt', async () => {
108+
return {
109+
cleanupNuxtDirs,
110+
nuxtVersionToGitIdentifier,
111+
}
112+
})
113+
114+
vi.mock('../../../src/utils/kit', async () => {
115+
return {
116+
loadKit,
117+
}
118+
})
119+
120+
vi.mock('../../../src/utils/packageManagers', async () => {
121+
return {
122+
getPackageManagerVersion,
123+
}
124+
})
125+
126+
vi.mock('../../../src/utils/logger', async () => {
127+
return {
128+
logger: {
129+
error: loggerError,
130+
step: loggerStep,
131+
success: loggerSuccess,
132+
info: loggerInfo,
133+
},
134+
}
135+
})
136+
137+
describe('normaliseLockFile', () => {
138+
beforeEach(() => {
139+
vi.clearAllMocks()
140+
141+
detectPackageManager.mockResolvedValue({
142+
name: 'npm',
143+
lockFile: ['package-lock.json'],
144+
})
145+
findWorkspaceDir.mockResolvedValue('/workspace')
146+
readPackageJSON.mockResolvedValue({ dependencies: { nuxt: '^4.3.1' } })
147+
getNuxtVersion.mockResolvedValueOnce('4.3.1').mockResolvedValueOnce('4.3.2')
148+
addDependency.mockResolvedValue(undefined)
149+
dedupeDependencies.mockResolvedValue(undefined)
150+
cleanupNuxtDirs.mockResolvedValue(undefined)
151+
loadKit.mockResolvedValue({
152+
loadNuxtConfig: vi.fn().mockResolvedValue({ buildDir: '.nuxt' }),
153+
})
154+
getPackageManagerVersion.mockReturnValue('10.9.4')
155+
})
156+
157+
it('resolves lockfiles across workspace and project directories', () => {
158+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/package-lock.json'))
159+
160+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], ['pnpm-lock.yaml', 'package-lock.json'])
161+
162+
expect(lockFile).toBe('package-lock.json')
163+
expect(loggerError).not.toHaveBeenCalled()
164+
})
165+
166+
it('logs an error when no lockfile is found in any candidate directory', () => {
167+
existsSync.mockReturnValue(false)
168+
169+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], ['pnpm-lock.yaml'])
170+
171+
expect(lockFile).toBeUndefined()
172+
expect(loggerError).toHaveBeenCalledTimes(1)
173+
})
174+
175+
it('supports string inputs for cwd and lockfile candidates', () => {
176+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/pnpm-lock.yaml'))
177+
178+
const lockFile = normaliseLockFile('/apps/web', 'pnpm-lock.yaml')
179+
180+
expect(lockFile).toBe('pnpm-lock.yaml')
181+
expect(loggerError).not.toHaveBeenCalled()
182+
})
183+
184+
it('returns undefined when lockfile candidates are missing', () => {
185+
existsSync.mockReturnValue(false)
186+
187+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], undefined)
188+
189+
expect(lockFile).toBeUndefined()
190+
expect(loggerError).toHaveBeenCalledTimes(1)
191+
})
192+
193+
it('handles duplicate directories in cwd candidates', () => {
194+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/package-lock.json'))
195+
196+
const lockFile = normaliseLockFile(['/apps/web', '/apps/web'], ['package-lock.json'])
197+
198+
expect(lockFile).toBe('package-lock.json')
199+
expect(loggerError).not.toHaveBeenCalled()
200+
})
201+
202+
it('returns lockfile when it exists in the first directory', () => {
203+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/workspace/pnpm-lock.yaml'))
204+
205+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], ['pnpm-lock.yaml'])
206+
207+
expect(lockFile).toBe('pnpm-lock.yaml')
208+
expect(loggerError).not.toHaveBeenCalled()
209+
})
210+
211+
it('checks both workspace and project directories during upgrade command run', async () => {
212+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/package-lock.json'))
213+
214+
await upgradeCommand.run!({
215+
args: {
216+
cwd: '/apps/web',
217+
rootDir: '/apps/web',
218+
dedupe: true,
219+
force: false,
220+
channel: 'stable',
221+
},
222+
} as any)
223+
224+
expect(existsSync).toHaveBeenCalledWith('/workspace/package-lock.json')
225+
expect(existsSync).toHaveBeenCalledWith('/apps/web/package-lock.json')
226+
})
227+
})

0 commit comments

Comments
 (0)