Skip to content

Commit 5c00643

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

2 files changed

Lines changed: 259 additions & 5 deletions

File tree

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: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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+
142+
it('resolves lockfiles across workspace and project directories', () => {
143+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/package-lock.json'))
144+
145+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], ['pnpm-lock.yaml', 'package-lock.json'])
146+
147+
expect(lockFile).toBe('package-lock.json')
148+
expect(loggerError).not.toHaveBeenCalled()
149+
})
150+
151+
it('logs an error when no lockfile is found in any candidate directory', () => {
152+
existsSync.mockReturnValue(false)
153+
154+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], ['pnpm-lock.yaml'])
155+
156+
expect(lockFile).toBeUndefined()
157+
expect(loggerError).toHaveBeenCalledTimes(1)
158+
const [errorMessage] = loggerError.mock.calls[0]!
159+
expect(errorMessage).toContain('workspace')
160+
expect(errorMessage).toContain('apps/web')
161+
})
162+
163+
it('supports string inputs for cwd and lockfile candidates', () => {
164+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/pnpm-lock.yaml'))
165+
166+
const lockFile = normaliseLockFile('/apps/web', 'pnpm-lock.yaml')
167+
168+
expect(lockFile).toBe('pnpm-lock.yaml')
169+
expect(loggerError).not.toHaveBeenCalled()
170+
})
171+
172+
it('returns undefined when lockfile candidates are missing', () => {
173+
existsSync.mockReturnValue(false)
174+
175+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], undefined)
176+
177+
expect(lockFile).toBeUndefined()
178+
expect(existsSync).not.toHaveBeenCalled()
179+
expect(loggerError).toHaveBeenCalledTimes(1)
180+
})
181+
182+
it('handles duplicate directories in cwd candidates', () => {
183+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/package-lock.json'))
184+
185+
const lockFile = normaliseLockFile(['/apps/web', '/apps/web'], ['package-lock.json'])
186+
187+
expect(lockFile).toBe('package-lock.json')
188+
expect(loggerError).not.toHaveBeenCalled()
189+
})
190+
191+
it('returns lockfile when it exists in the first directory', () => {
192+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/workspace/pnpm-lock.yaml'))
193+
194+
const lockFile = normaliseLockFile(['/workspace', '/apps/web'], ['pnpm-lock.yaml'])
195+
196+
expect(lockFile).toBe('pnpm-lock.yaml')
197+
expect(loggerError).not.toHaveBeenCalled()
198+
})
199+
200+
describe('upgrade command integration', () => {
201+
beforeEach(() => {
202+
detectPackageManager.mockResolvedValue({
203+
name: 'npm',
204+
lockFile: ['package-lock.json'],
205+
})
206+
findWorkspaceDir.mockResolvedValue('/workspace')
207+
readPackageJSON.mockResolvedValue({ dependencies: { nuxt: '^4.3.1' } })
208+
getNuxtVersion.mockResolvedValueOnce('4.3.1').mockResolvedValueOnce('4.3.2')
209+
addDependency.mockResolvedValue(undefined)
210+
dedupeDependencies.mockResolvedValue(undefined)
211+
cleanupNuxtDirs.mockResolvedValue(undefined)
212+
loadKit.mockResolvedValue({
213+
loadNuxtConfig: vi.fn().mockResolvedValue({ buildDir: '.nuxt' }),
214+
})
215+
getPackageManagerVersion.mockReturnValue('10.9.4')
216+
select.mockImplementation(async (opts: { message: string }) => opts.message.includes('nightly') ? '4.x' : 'dedupe')
217+
})
218+
219+
it('checks both workspace and project directories during upgrade command run', async () => {
220+
existsSync.mockImplementation((filePath: string) => filePath.endsWith('/apps/web/package-lock.json'))
221+
222+
const run = upgradeCommand.run
223+
if (!run) {
224+
throw new Error('upgrade command run handler is missing')
225+
}
226+
227+
await run({
228+
args: {
229+
cwd: '/apps/web',
230+
rootDir: '/apps/web',
231+
dedupe: true,
232+
force: false,
233+
channel: 'stable',
234+
},
235+
} as any)
236+
237+
const lockfileChecks = existsSync.mock.calls
238+
.map(([filePath]) => String(filePath))
239+
.filter(filePath => filePath.endsWith('package-lock.json'))
240+
241+
expect(lockfileChecks).toEqual(expect.arrayContaining([
242+
expect.stringContaining('workspace'),
243+
expect.stringContaining('apps/web'),
244+
]))
245+
expect(addDependency).toHaveBeenCalledTimes(1)
246+
})
247+
})
248+
})

0 commit comments

Comments
 (0)