|
| 1 | +import{mkdtemp,readFile,rm,writeFile}from'node:fs/promises' |
| 2 | +import{tmpdir}from'node:os' |
| 3 | +importprocessfrom'node:process' |
| 4 | + |
| 5 | +import{join}from'pathe' |
| 6 | +import{afterEach,beforeEach,describe,expect,it,vi}from'vitest' |
| 7 | + |
| 8 | +const{ answers, progress, detectPackageManager, getNuxtVersion, resolveRegistryVersion, runDedupe, runInstall }=vi.hoisted(()=>({ |
| 9 | +answers: {select: []asunknown[]}, |
| 10 | +progress: []asstring[], |
| 11 | +detectPackageManager: vi.fn(), |
| 12 | +getNuxtVersion: vi.fn(), |
| 13 | +resolveRegistryVersion: vi.fn(), |
| 14 | +runDedupe: vi.fn(), |
| 15 | +runInstall: vi.fn(), |
| 16 | +})) |
| 17 | + |
| 18 | +vi.mock('@clack/prompts',async(importOriginal)=>{ |
| 19 | +constoriginal=awaitimportOriginal<typeofimport('@clack/prompts')>() |
| 20 | +return{ |
| 21 | + ...original, |
| 22 | +select: vi.fn(()=>Promise.resolve(answers.select.length ? answers.select.shift() : 'skip')), |
| 23 | +spinner: vi.fn(()=>{ |
| 24 | +constrecord=(message?: string)=>void(message&&progress.push(message)) |
| 25 | +return{start: record,stop: record,error: record,message: record} |
| 26 | +}), |
| 27 | +} |
| 28 | +}) |
| 29 | + |
| 30 | +vi.mock('nypm',asyncimportOriginal=>({ |
| 31 | + ...awaitimportOriginal<typeofimport('nypm')>(), |
| 32 | + detectPackageManager, |
| 33 | +})) |
| 34 | + |
| 35 | +vi.mock('../../../src/utils/install',asyncimportOriginal=>({ |
| 36 | + ...awaitimportOriginal<typeofimport('../../../src/utils/install')>(), |
| 37 | + runDedupe, |
| 38 | + runInstall, |
| 39 | +})) |
| 40 | + |
| 41 | +vi.mock('../../../src/utils/versions',asyncimportOriginal=>({ |
| 42 | + ...awaitimportOriginal<typeofimport('../../../src/utils/versions')>(), |
| 43 | + getNuxtVersion, |
| 44 | + resolveRegistryVersion, |
| 45 | +})) |
| 46 | + |
| 47 | +vi.mock('../../../src/utils/kit',()=>({ |
| 48 | +loadKit: ()=>Promise.resolve({loadNuxtConfig: ()=>Promise.resolve({buildDir: '.nuxt'})}), |
| 49 | +tryResolveNuxt: ()=>undefined, |
| 50 | +})) |
| 51 | + |
| 52 | +const{runCommandDef}=awaitimport('../../../src/run-command') |
| 53 | +const{render,screen}=awaitimport('../../utils/terminal') |
| 54 | +constupgrade=awaitimport('../../../src/commands/upgrade').then(r=>r.default) |
| 55 | + |
| 56 | +classExitErrorextendsError{ |
| 57 | +constructor(readonlycode: number |undefined){ |
| 58 | +super(`process.exit(${code})`) |
| 59 | +} |
| 60 | +} |
| 61 | + |
| 62 | +letcwd: string |
| 63 | + |
| 64 | +asyncfunctionrunUpgrade(argv: string[]=[]): Promise<{output: string,exitCode: number|undefined}>{ |
| 65 | +letexitCode: number|undefined |
| 66 | +constexit=vi.spyOn(process,'exit').mockImplementation((code)=>{ |
| 67 | +exitCode=codeasnumber|undefined |
| 68 | +thrownewExitError(exitCode) |
| 69 | +}) |
| 70 | + |
| 71 | +constrenderer=awaitrender(async()=>{ |
| 72 | +awaitrunCommandDef(upgrade,[`--cwd=${cwd}`, ...argv]).catch((error)=>{ |
| 73 | +if(!(errorinstanceofExitError)){ |
| 74 | +throwerror |
| 75 | +} |
| 76 | +}) |
| 77 | +}) |
| 78 | + |
| 79 | +exit.mockRestore() |
| 80 | +return{output: [...progress,renderer.frames.join('\n'),screen(renderer)].join('\n'),exitCode} |
| 81 | +} |
| 82 | + |
| 83 | +asyncfunctionwriteProject(files: Record<string,string>): Promise<void>{ |
| 84 | +for(const[name,contents]ofObject.entries(files)){ |
| 85 | +awaitwriteFile(join(cwd,name),contents) |
| 86 | +} |
| 87 | +} |
| 88 | + |
| 89 | +constinstalled={success: true,output: '',command: 'pnpm install',ignoredBuilds: []} |
| 90 | + |
| 91 | +beforeEach(async()=>{ |
| 92 | +cwd=awaitmkdtemp(join(tmpdir(),'nuxt-upgrade-')) |
| 93 | +answers.select.length=0 |
| 94 | +progress.length=0 |
| 95 | +vi.clearAllMocks() |
| 96 | +detectPackageManager.mockResolvedValue({name: 'pnpm',command: 'pnpm',lockFile: ['pnpm-lock.yaml']}) |
| 97 | +getNuxtVersion.mockResolvedValue('4.0.0') |
| 98 | +resolveRegistryVersion.mockResolvedValue('4.1.0') |
| 99 | +runInstall.mockResolvedValue(installed) |
| 100 | +runDedupe.mockResolvedValue(installed) |
| 101 | +awaitwriteProject({ |
| 102 | +'package.json': JSON.stringify({name: 'app',dependencies: {nuxt: '^4.0.0'}}), |
| 103 | +'pnpm-lock.yaml': '', |
| 104 | +}) |
| 105 | +}) |
| 106 | + |
| 107 | +afterEach(async()=>{ |
| 108 | +awaitrm(cwd,{recursive: true,force: true}) |
| 109 | +vi.restoreAllMocks() |
| 110 | +}) |
| 111 | + |
| 112 | +describe('upgrade preconditions',()=>{ |
| 113 | +it('should explain how to fix an undetectable package manager',async()=>{ |
| 114 | +detectPackageManager.mockResolvedValue(undefined) |
| 115 | + |
| 116 | +const{ output, exitCode }=awaitrunUpgrade() |
| 117 | + |
| 118 | +expect(exitCode).toBe(1) |
| 119 | +expect(output).toContain('Unable to determine the package manager') |
| 120 | +expect(output).toContain('packageManager') |
| 121 | +expect(runInstall).not.toHaveBeenCalled() |
| 122 | +}) |
| 123 | + |
| 124 | +it('should carry on without a lock file but say it is missing',async()=>{ |
| 125 | +awaitrm(join(cwd,'pnpm-lock.yaml')) |
| 126 | + |
| 127 | +const{ output }=awaitrunUpgrade() |
| 128 | + |
| 129 | +expect(output).toContain('Unable to find a pnpm lock file') |
| 130 | +expect(runInstall).toHaveBeenCalledTimes(1) |
| 131 | +}) |
| 132 | +}) |
| 133 | + |
| 134 | +describe('upgrade install',()=>{ |
| 135 | +it('should install the requested channel',async()=>{ |
| 136 | +awaitrunUpgrade(['--channel=v3']) |
| 137 | + |
| 138 | +expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({dependencies: ['nuxt@3']})) |
| 139 | +}) |
| 140 | + |
| 141 | +it('should upgrade the core packages the project depends on',async()=>{ |
| 142 | +awaitwriteProject({'package.json': JSON.stringify({name: 'app',devDependencies: {'nuxt': '^4.0.0','@nuxt/kit': '^4.0.0'}})}) |
| 143 | + |
| 144 | +awaitrunUpgrade() |
| 145 | + |
| 146 | +expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({ |
| 147 | +dependencies: ['nuxt@latest','@nuxt/kit@latest'], |
| 148 | +dev: true, |
| 149 | +})) |
| 150 | +}) |
| 151 | + |
| 152 | +it('should stop when the install fails',async()=>{ |
| 153 | +runInstall.mockResolvedValue({ ...installed,success: false,error: 'pnpm ERR! offline',output: 'pnpm ERR! offline'}) |
| 154 | + |
| 155 | +const{ output, exitCode }=awaitrunUpgrade() |
| 156 | + |
| 157 | +expect(exitCode).toBe(1) |
| 158 | +expect(output).toContain('pnpm ERR! offline') |
| 159 | +expect(runDedupe).not.toHaveBeenCalled() |
| 160 | +}) |
| 161 | + |
| 162 | +it('should dedupe when asked to',async()=>{ |
| 163 | +awaitrunUpgrade(['--dedupe']) |
| 164 | + |
| 165 | +expect(runDedupe).toHaveBeenCalledWith(expect.objectContaining({recreateLockfile: false})) |
| 166 | +}) |
| 167 | + |
| 168 | +it('should recreate the lockfile when forced, and say how to undo it',async()=>{ |
| 169 | +const{ output }=awaitrunUpgrade(['--force']) |
| 170 | + |
| 171 | +expect(runDedupe).toHaveBeenCalledWith(expect.objectContaining({recreateLockfile: true})) |
| 172 | +expect(output).toContain('--no-force') |
| 173 | +}) |
| 174 | + |
| 175 | +it('should report an upgrade that changed the version',async()=>{ |
| 176 | +getNuxtVersion.mockResolvedValueOnce('4.0.0').mockResolvedValueOnce('4.1.0') |
| 177 | + |
| 178 | +const{ output }=awaitrunUpgrade() |
| 179 | + |
| 180 | +expect(output).toContain('Successfully upgraded Nuxt') |
| 181 | +expect(output).toContain('4.1.0') |
| 182 | +}) |
| 183 | + |
| 184 | +it('should say when the project was already up to date',async()=>{ |
| 185 | +const{ output }=awaitrunUpgrade() |
| 186 | + |
| 187 | +expect(output).toContain('already using the latest version') |
| 188 | +}) |
| 189 | +}) |
| 190 | + |
| 191 | +describe('upgrade with a pnpm catalog',()=>{ |
| 192 | +constworkspace='packages:\n - .\ncatalog:\n nuxt: ^4.0.0\n' |
| 193 | + |
| 194 | +beforeEach(async()=>{ |
| 195 | +awaitwriteProject({ |
| 196 | +'package.json': JSON.stringify({name: 'app',dependencies: {nuxt: 'catalog:'}}), |
| 197 | +'pnpm-workspace.yaml': workspace, |
| 198 | +}) |
| 199 | +}) |
| 200 | + |
| 201 | +it('should update the catalog entry rather than pinning it in package.json',async()=>{ |
| 202 | +const{ output }=awaitrunUpgrade() |
| 203 | + |
| 204 | +expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toContain('nuxt: ^4.1.0') |
| 205 | +expect(JSON.parse(awaitreadFile(join(cwd,'package.json'),'utf8')).dependencies.nuxt).toBe('catalog:') |
| 206 | +expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({dependencies: []})) |
| 207 | +expect(output).toContain('Catalog entries updated') |
| 208 | +}) |
| 209 | + |
| 210 | +it('should leave the catalog alone for a package manager without catalogs',async()=>{ |
| 211 | +detectPackageManager.mockResolvedValue({name: 'npm',command: 'npm',lockFile: ['package-lock.json']}) |
| 212 | +awaitwriteProject({'package-lock.json': '{}'}) |
| 213 | + |
| 214 | +awaitrunUpgrade() |
| 215 | + |
| 216 | +expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toBe(workspace) |
| 217 | +expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({dependencies: ['nuxt@latest']})) |
| 218 | +}) |
| 219 | + |
| 220 | +it('should stop when nuxt cannot be resolved for the catalog',async()=>{ |
| 221 | +resolveRegistryVersion.mockResolvedValue(undefined) |
| 222 | + |
| 223 | +const{ output, exitCode }=awaitrunUpgrade() |
| 224 | + |
| 225 | +expect(exitCode).toBe(1) |
| 226 | +expect(output).toContain('Unable to resolve a latest stable version') |
| 227 | +expect(output).toContain('DEBUG=nuxi') |
| 228 | +expect(output).toContain('Unable to upgrade') |
| 229 | +expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toBe(workspace) |
| 230 | +expect(runInstall).not.toHaveBeenCalled() |
| 231 | +}) |
| 232 | + |
| 233 | +it('should stop when the workspace file cannot be rewritten',async()=>{ |
| 234 | +awaitwriteProject({'pnpm-workspace.yaml': 'catalog:\n nuxt: *missing-anchor\n'}) |
| 235 | + |
| 236 | +const{ output, exitCode }=awaitrunUpgrade() |
| 237 | + |
| 238 | +expect(exitCode).toBe(1) |
| 239 | +expect(output).toContain('pnpm-workspace.yaml') |
| 240 | +expect(runInstall).not.toHaveBeenCalled() |
| 241 | +}) |
| 242 | + |
| 243 | +it('should tell the user the catalog moved on when the install then fails',async()=>{ |
| 244 | +runInstall.mockResolvedValue({ ...installed,success: false,error: 'pnpm ERR! offline'}) |
| 245 | + |
| 246 | +const{ output, exitCode }=awaitrunUpgrade() |
| 247 | + |
| 248 | +expect(exitCode).toBe(1) |
| 249 | +expect(output).toContain('catalog entries were already updated') |
| 250 | +}) |
| 251 | + |
| 252 | +it('should keep the range operator the catalog entry used',async()=>{ |
| 253 | +awaitwriteProject({'pnpm-workspace.yaml': 'catalog:\n nuxt: 4.0.0\n'}) |
| 254 | + |
| 255 | +awaitrunUpgrade() |
| 256 | + |
| 257 | +expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toContain('nuxt: 4.1.0') |
| 258 | +}) |
| 259 | +}) |
0 commit comments