Skip to content

Commit 1e3e803

Browse files
committed
test: cover typecheck checker selection, install advice and build mode
1 parent 841fbc1 commit 1e3e803

1 file changed

Lines changed: 189 additions & 62 deletions

File tree

Lines changed: 189 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,221 @@
1-
import{fileURLToPath}from'node:url'
2-
3-
import{beforeEach,describe,expect,it,vi}from'vitest'
4-
5-
import{runCommand}from'../../../src/run'
6-
import{logger}from'../../../src/utils/logger'
7-
8-
const{ buildNuxt, closeNuxt, loadKit, resolveModulePath, writeTypes, x }=vi.hoisted(()=>{
9-
constbuildNuxt=vi.fn(()=>Promise.resolve())
10-
constcloseNuxt=vi.fn(()=>Promise.resolve())
11-
constwriteTypes=vi.fn(()=>Promise.resolve())
12-
return{
13-
buildNuxt,
14-
closeNuxt,
1+
import{mkdir,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{ addDevDependency, answers, resolveModulePath, tinyexec, writeTypes }=vi.hoisted(()=>({
9+
addDevDependency: vi.fn(()=>Promise.resolve()),
10+
answers: {select: []asunknown[],confirm: []asunknown[]},
11+
resolveModulePath: vi.fn(),
12+
tinyexec: vi.fn(()=>Promise.resolve({exitCode: 0,stdout: '',stderr: ''})),
13+
writeTypes: vi.fn(()=>Promise.resolve()),
14+
}))
15+
16+
vi.mock('tinyexec',()=>({x: tinyexec}))
17+
18+
vi.mock('exsolve',asyncimportOriginal=>({
19+
...awaitimportOriginal<typeofimport('exsolve')>(),
20+
resolveModulePath,
21+
}))
22+
23+
vi.mock('nypm',asyncimportOriginal=>({
24+
...awaitimportOriginal<typeofimport('nypm')>(),
25+
addDevDependency,
26+
detectPackageManager: ()=>Promise.resolve({name: 'pnpm',command: 'pnpm'}),
27+
}))
28+
29+
vi.mock('../../../src/utils/kit',()=>({
30+
loadKit: ()=>Promise.resolve({
31+
loadNuxt: ()=>Promise.resolve({close: ()=>Promise.resolve(),options: {}}),
32+
buildNuxt: ()=>Promise.resolve(),
1533
writeTypes,
16-
x: vi.fn((_bin: string,_args: string[])=>Promise.resolve({exitCode: 0})),
17-
loadKit: vi.fn(()=>Promise.resolve({
18-
loadNuxt: ()=>Promise.resolve({close: closeNuxt}),
19-
buildNuxt,
20-
writeTypes,
21-
})),
22-
resolveModulePath: vi.fn((id: string): string|undefined=>id.includes('vue-tsc') ? '/node_modules/vue-tsc/bin/vue-tsc.js' : '/node_modules/typescript/index.js'),
34+
}),
35+
tryResolveNuxt: ()=>undefined,
36+
}))
37+
38+
vi.mock('std-env',asyncimportOriginal=>({
39+
...awaitimportOriginal<typeofimport('std-env')>(),
40+
hasTTY: true,
41+
}))
42+
43+
vi.mock('@clack/prompts',async(importOriginal)=>{
44+
constoriginal=awaitimportOriginal<typeofimport('@clack/prompts')>()
45+
constnext=(queue: unknown[],fallback: unknown)=>Promise.resolve(queue.length ? queue.shift() : fallback)
46+
return{
47+
...original,
48+
select: vi.fn(()=>next(answers.select,'vue-tsc')),
49+
confirm: vi.fn(()=>next(answers.confirm,false)),
50+
spinner: vi.fn(()=>({start: vi.fn(),stop: vi.fn(),error: vi.fn(),message: vi.fn()})),
2351
}
2452
})
2553

26-
vi.mock('tinyexec',()=>({ x }))
27-
vi.mock('../../../src/utils/kit',()=>({ loadKit }))
28-
vi.mock('exsolve',()=>({ resolveModulePath }))
54+
const{ runCommandDef }=awaitimport('../../../src/run-command')
55+
const{ render, screen }=awaitimport('../../utils/terminal')
56+
consttypecheck=awaitimport('../../../src/commands/typecheck').then(r=>r.default)
2957

30-
functionfixture(name: string){
31-
returnfileURLToPath(newURL(`../../fixtures/typecheck/${name}`,import.meta.url))
58+
letcwd: string
59+
60+
/** Pretend the named packages are installed in the project. */
61+
functioninstalled(...names: string[]): void{
62+
resolveModulePath.mockImplementation((id: string)=>{
63+
if(names.some(name=>id===name||id.startsWith(`${name}/`))){
64+
returnjoin(cwd,'node_modules',id)
65+
}
66+
returnundefined
67+
})
3268
}
3369

34-
asyncfunctionrun(cwd: string, ...args: string[]){
35-
awaitrunCommand('typecheck',['--cwd',cwd, ...args])
36-
returnx.mock.calls[0]?.[1]
70+
asyncfunctionrunTypecheck(argv: string[]=[]): Promise<{output: string,exitCode: number|undefined}>{
71+
process.exitCode=undefined
72+
constrenderer=awaitrender(async()=>{
73+
awaitrunCommandDef(typecheck,[`--cwd=${cwd}`, ...argv])
74+
})
75+
constexitCode=process.exitCodeasnumber|undefined
76+
process.exitCode=0
77+
return{output: `${renderer.frames.join('\n')}\n${screen(renderer)}`, exitCode }
3778
}
3879

39-
describe('nuxt typecheck command',()=>{
40-
beforeEach(()=>{
41-
vi.clearAllMocks()
42-
process.exitCode=undefined
43-
resolveModulePath.mockImplementation((id: string)=>id.includes('vue-tsc') ? '/node_modules/vue-tsc/bin/vue-tsc.js' : '/node_modules/typescript/index.js')
44-
x.mockResolvedValue({exitCode: 0})
80+
beforeEach(async()=>{
81+
cwd=awaitmkdtemp(join(tmpdir(),'nuxt-typecheck-'))
82+
answers.select.length=0
83+
answers.confirm.length=0
84+
vi.clearAllMocks()
85+
tinyexec.mockResolvedValue({exitCode: 0,stdout: '',stderr: ''})
86+
addDevDependency.mockImplementation(()=>Promise.resolve())
87+
installed('typescript','vue-tsc/bin/vue-tsc.js')
88+
awaitwriteFile(join(cwd,'package.json'),JSON.stringify({name: 'app',type: 'module'}))
89+
})
90+
91+
afterEach(async()=>{
92+
awaitrm(cwd,{recursive: true,force: true})
93+
vi.restoreAllMocks()
94+
})
95+
96+
describe('typecheck checker selection',()=>{
97+
it('should reject a checker it does not know',async()=>{
98+
const{ output, exitCode }=awaitrunTypecheck(['--checker=tsc'])
99+
100+
expect(exitCode).toBe(1)
101+
expect(output).toContain('Unknown type checker')
102+
expect(output).toContain('vue-tsc')
103+
expect(tinyexec).not.toHaveBeenCalled()
104+
})
105+
106+
it('should run the checker that is installed',async()=>{
107+
awaitrunTypecheck()
108+
109+
expect(tinyexec).toHaveBeenCalledWith(expect.stringContaining('vue-tsc'),['--noEmit'],expect.objectContaining({
110+
nodeOptions: expect.objectContaining({ cwd }),
111+
}))
112+
})
113+
114+
it('should prefer golar when the project has a golar config',async()=>{
115+
awaitwriteFile(join(cwd,'golar.config.ts'),'export default {}')
116+
awaitmkdir(join(cwd,'node_modules/golar'),{recursive: true})
117+
awaitwriteFile(join(cwd,'node_modules/golar/package.json'),JSON.stringify({name: 'golar',bin: './bin.js'}))
118+
installed('typescript','vue-tsc/bin/vue-tsc.js','golar/unstable','@golar/vue')
119+
120+
awaitrunTypecheck()
121+
122+
expect(tinyexec).toHaveBeenCalledWith(expect.stringContaining('golar'),['tsc','--noEmit'],expect.anything())
45123
})
46124

47-
it('should use build mode for Nuxt project references',async()=>{
48-
expect(awaitrun(fixture('nuxt-references'))).toEqual(['-b','--noEmit'])
125+
it('should create a golar config the first time golar is used',async()=>{
126+
awaitmkdir(join(cwd,'node_modules/golar'),{recursive: true})
127+
awaitwriteFile(join(cwd,'node_modules/golar/package.json'),JSON.stringify({name: 'golar',bin: './bin.js'}))
128+
installed('golar/unstable','@golar/vue')
129+
130+
awaitrunTypecheck(['--checker=golar'])
131+
132+
expect(awaitreadFile(join(cwd,'golar.config.ts'),'utf8')).toContain('defineConfig')
49133
})
134+
})
50135

51-
it('should not use build mode when the tsconfig has input files of its own',async()=>{
52-
expect(awaitrun(fixture('legacy-references/app'))).toEqual(['--noEmit'])
136+
describe('typecheck installation advice',()=>{
137+
it('should offer to install a missing checker and run it afterwards',async()=>{
138+
installed()
139+
answers.select.push('vue-tsc')
140+
answers.confirm.push(true)
141+
addDevDependency.mockImplementation(()=>{
142+
installed('typescript','vue-tsc/bin/vue-tsc.js')
143+
returnPromise.resolve()
144+
})
145+
146+
awaitrunTypecheck()
147+
148+
expect(addDevDependency).toHaveBeenCalledWith(['typescript','vue-tsc'],expect.objectContaining({ cwd }))
149+
expect(tinyexec).toHaveBeenCalledTimes(1)
53150
})
54151

55-
it('should respect an explicit --build flag',async()=>{
56-
expect(awaitrun(fixture('legacy-references/app'),'--build')).toEqual(['-b','--noEmit'])
152+
it('should print the install command when the user declines',async()=>{
153+
installed()
154+
answers.select.push('vue-tsc')
155+
answers.confirm.push(false)
156+
157+
const{ output, exitCode }=awaitrunTypecheck()
158+
159+
expect(exitCode).toBe(1)
160+
expect(output).toContain('pnpm add -D typescript vue-tsc')
161+
expect(tinyexec).not.toHaveBeenCalled()
57162
})
58163

59-
it('should respect an explicit --no-build flag',async()=>{
60-
expect(awaitrun(fixture('nuxt-references'),'--no-build')).toEqual(['--noEmit'])
164+
it('should explain when the checker is still missing after installing',async()=>{
165+
installed()
166+
answers.select.push('vue-tsc')
167+
answers.confirm.push(true)
168+
169+
const{ output, exitCode }=awaitrunTypecheck()
170+
171+
expect(exitCode).toBe(1)
172+
expect(output).toContain('Failed to resolve')
173+
expect(tinyexec).not.toHaveBeenCalled()
61174
})
175+
})
176+
177+
describe('typecheck build mode',()=>{
178+
it('should use project references for a solution-style tsconfig',async()=>{
179+
awaitwriteFile(join(cwd,'tsconfig.json'),JSON.stringify({files: [],references: [{path: './.nuxt/tsconfig.app.json'}]}))
180+
181+
awaitrunTypecheck()
62182

63-
it('should warn when Nuxt project references are referenced alongside input files',async()=>{
64-
constwarn=vi.spyOn(logger,'warn').mockImplementation(()=>{})
65-
awaitrun(fixture('incomplete-references'))
66-
expect(warn).toHaveBeenCalledWith(expect.stringContaining('"files": []'))
67-
warn.mockRestore()
183+
expect(tinyexec).toHaveBeenCalledWith(expect.anything(),['-b','--noEmit'],expect.anything())
68184
})
69185

70-
it('should not prepare Nuxt when the requested checker is unavailable',async()=>{
71-
resolveModulePath.mockReturnValue(undefined)
186+
it('should warn when a tsconfig references nuxt projects but has files of its own',async()=>{
187+
awaitwriteFile(join(cwd,'tsconfig.json'),JSON.stringify({
188+
include: ['src'],
189+
references: [{path: './.nuxt/tsconfig.app.json'}],
190+
}))
72191

73-
awaitrun(fixture('nuxt-references'),'--checker','vue-tsc')
192+
const{ output }=awaitrunTypecheck()
74193

75-
expect(process.exitCode).toBe(1)
76-
expect(loadKit).not.toHaveBeenCalled()
194+
expect(output).toContain('"files": []')
195+
expect(tinyexec).toHaveBeenCalledWith(expect.anything(),['--noEmit'],expect.anything())
77196
})
78197

79-
it('should close Nuxt when preparing types fails',async()=>{
80-
writeTypes.mockRejectedValueOnce(newError('could not write types'))
198+
it('should let `--build` override the detection',async()=>{
199+
awaitrunTypecheck(['--build'])
200+
201+
expect(tinyexec).toHaveBeenCalledWith(expect.anything(),['-b','--noEmit'],expect.anything())
202+
})
203+
})
204+
205+
describe('typecheck results',()=>{
206+
it('should report a passing type check',async()=>{
207+
const{ output, exitCode }=awaitrunTypecheck()
81208

82-
awaitexpect(run(fixture('nuxt-references'))).rejects.toThrow('could not write types')
83-
expect(buildNuxt).not.toHaveBeenCalled()
84-
expect(closeNuxt).toHaveBeenCalledOnce()
209+
expect(output).toContain('Type check passed')
210+
expect(exitCode).toBeFalsy()
85211
})
86212

87-
it('should propagate the checker exit code',async()=>{
88-
x.mockResolvedValueOnce({exitCode: 2})
213+
it('should carry the checker exit code through',async()=>{
214+
tinyexec.mockResolvedValue({exitCode: 2,stdout: '',stderr: ''})
89215

90-
awaitrun(fixture('nuxt-references'))
216+
const{ output, exitCode }=awaitrunTypecheck()
91217

92-
expect(process.exitCode).toBe(2)
218+
expect(output).toContain('Type check failed')
219+
expect(exitCode).toBe(2)
93220
})
94221
})

0 commit comments

Comments
 (0)