|
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(), |
15 | 33 | 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()})), |
23 | 51 | } |
24 | 52 | }) |
25 | 53 |
|
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) |
29 | 57 |
|
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 | +}) |
32 | 68 | } |
33 | 69 |
|
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 } |
37 | 78 | } |
38 | 79 |
|
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()) |
45 | 123 | }) |
46 | 124 |
|
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') |
49 | 133 | }) |
| 134 | +}) |
50 | 135 |
|
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) |
53 | 150 | }) |
54 | 151 |
|
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() |
57 | 162 | }) |
58 | 163 |
|
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() |
61 | 174 | }) |
| 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() |
62 | 182 |
|
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()) |
68 | 184 | }) |
69 | 185 |
|
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 | +})) |
72 | 191 |
|
73 | | -awaitrun(fixture('nuxt-references'),'--checker','vue-tsc') |
| 192 | +const{ output }=awaitrunTypecheck() |
74 | 193 |
|
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()) |
77 | 196 | }) |
78 | 197 |
|
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() |
81 | 208 |
|
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() |
85 | 211 | }) |
86 | 212 |
|
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: ''}) |
89 | 215 |
|
90 | | -awaitrun(fixture('nuxt-references')) |
| 216 | +const{ output, exitCode }=awaitrunTypecheck() |
91 | 217 |
|
92 | | -expect(process.exitCode).toBe(2) |
| 218 | +expect(output).toContain('Type check failed') |
| 219 | +expect(exitCode).toBe(2) |
93 | 220 | }) |
94 | 221 | }) |
0 commit comments