Skip to content

Commit 7b10ab4

Browse files
authored
fix(typecheck): improve error message when tsc outputs help text (#9214)
1 parent 5d84eeb commit 7b10ab4

File tree

3 files changed

+103
-2
lines changed

3 files changed

+103
-2
lines changed

packages/vitest/src/typecheck/typechecker.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class Typechecker {
5353

5454
protected files: string[] = []
5555

56-
constructor(protected project: TestProject) {}
56+
constructor(protected project: TestProject) { }
5757

5858
public setFiles(files: string[]): void {
5959
this.files = files
@@ -123,6 +123,22 @@ export class Typechecker {
123123
sourceErrors: TestError[]
124124
time: number
125125
}> {
126+
// Detect if tsc output is help text instead of error output
127+
// This happens when tsconfig.json is missing and tsc can't find any config
128+
if (output.includes('The TypeScript Compiler - Version') || output.includes('COMMON COMMANDS')) {
129+
const { typecheck } = this.project.config
130+
const tsconfigPath = typecheck.tsconfig || 'tsconfig.json'
131+
const msg = `TypeScript compiler returned help text instead of type checking results.\n`
132+
+ `This usually means the tsconfig file was not found.\n\n`
133+
+ `Possible solutions:\n`
134+
+ ` 1. Ensure '${tsconfigPath}' exists in your project root\n`
135+
+ ` 2. If using a custom tsconfig, verify the path in your Vitest config:\n`
136+
+ ` test: { typecheck: { tsconfig: 'path/to/tsconfig.json' } }\n`
137+
+ ` 3. Check that the tsconfig file is valid JSON`
138+
139+
throw new Error(msg)
140+
}
141+
126142
const typeErrors = await this.parseTscLikeOutput(output)
127143
const testFiles = new Set(this.getFiles())
128144

@@ -319,6 +335,8 @@ export class Typechecker {
319335
return
320336
}
321337

338+
let resolved = false
339+
322340
child.process.stdout.on('data', (chunk) => {
323341
dataReceived = true
324342
this._output += chunk
@@ -343,13 +361,25 @@ export class Typechecker {
343361
}
344362
})
345363

364+
// Also capture stderr for configuration errors like missing tsconfig
365+
child.process.stderr?.on('data', (chunk) => {
366+
this._output += chunk
367+
})
368+
346369
const timeout = setTimeout(
347370
() => reject(new Error(`${typecheck.checker} spawn timed out`)),
348371
this.project.config.typecheck.spawnTimeout,
349372
)
350373

374+
let winTimeout: NodeJS.Timeout | undefined
375+
351376
function onError(cause: Error) {
377+
if (resolved) {
378+
return
379+
}
352380
clearTimeout(timeout)
381+
clearTimeout(winTimeout)
382+
resolved = true
353383
reject(new Error('Spawning typechecker failed - is typescript installed?', { cause }))
354384
}
355385

@@ -361,11 +391,13 @@ export class Typechecker {
361391
// on Windows, the process might be spawned but fail to start
362392
// we wait for a potential error here. if "close" event didn't trigger,
363393
// we resolve the promise
364-
setTimeout(() => {
394+
winTimeout = setTimeout(() => {
395+
resolved = true
365396
resolve({ result: child })
366397
}, 200)
367398
}
368399
else {
400+
resolved = true
369401
resolve({ result: child })
370402
}
371403
})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import fs from 'node:fs'
2+
import os from 'node:os'
3+
import path from 'node:path'
4+
import { describe, expect, it } from 'vitest'
5+
import { createFile, runInlineTests } from '../../test-utils'
6+
7+
describe('Typechecker Error Handling', () => {
8+
it('throws helpful error when tsc outputs help text (missing config)', async () => {
9+
// TESTING APPROACH:
10+
// We cannot reliably trigger tsc's help text output in normal usage because:
11+
// 1. tsc only shows help when called with NO arguments or INVALID arguments
12+
// 2. Vitest always calls tsc with proper arguments (--noEmit, --pretty, etc.)
13+
// 3. Invalid tsconfig causes ERROR output, not help text
14+
//
15+
// SOLUTION: Use a test executable that mimics tsc help output
16+
// This is NOT a mock (no jest.mock or similar), but a real executable script
17+
// that Vitest spawns and executes, validating the error handling logic works.
18+
19+
// Create a temporary directory for our fake tsc
20+
const tmpDir = path.join(os.tmpdir(), `vitest-test-${Date.now()}`)
21+
22+
// Create fake tsc script - cross-platform executable
23+
// Using createFile ensures cleanup even if test fails
24+
const fakeTscPath = path.join(tmpDir, 'fake-tsc')
25+
const scriptContent = '#!/usr/bin/env node\nconsole.log(\'Version 5.3.3\');\nconsole.log(\'tsc: The TypeScript Compiler - Version 5.3.3\');\nconsole.log(\'\');\nconsole.log(\'COMMON COMMANDS\');\n'
26+
27+
createFile(fakeTscPath, scriptContent)
28+
fs.chmodSync(fakeTscPath, '755')
29+
30+
const configContent = `import { defineConfig } from 'vitest/config'
31+
export default defineConfig({
32+
test: {
33+
typecheck: {
34+
enabled: true,
35+
checker: '${fakeTscPath.replace(/\\/g, '/')}',
36+
},
37+
},
38+
})`
39+
40+
const { stderr, stdout } = await runInlineTests({
41+
'vitest.config.ts': configContent,
42+
'example.test-d.ts': 'import { expectTypeOf, test } from \'vitest\'\ntest(\'dummy type test\', () => { expectTypeOf(1).toEqualTypeOf<number>() })',
43+
})
44+
45+
// Assert that Vitest caught the help text and threw the descriptive error
46+
const output = stderr + stdout
47+
expect(output).toContain('TypeScript compiler returned help text instead of type checking results')
48+
expect(output).toContain('This usually means the tsconfig file was not found')
49+
expect(output).toContain('Ensure \'tsconfig.json\' exists in your project root')
50+
})
51+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { resolve } from 'pathe'
2+
import { describe, expect, it } from 'vitest'
3+
import { runVitest } from '../../test-utils'
4+
5+
describe('Typechecker', () => {
6+
it('handles non-existing typechecker command gracefully', async () => {
7+
const { stderr } = await runVitest({
8+
root: resolve(import.meta.dirname, '../fixtures/source-error'),
9+
typecheck: {
10+
enabled: true,
11+
checker: 'non-existing-tsc-command',
12+
},
13+
})
14+
15+
// Should show proper error when typechecker doesn't exist
16+
expect(stderr).toContain('Spawning typechecker failed')
17+
})
18+
})

0 commit comments

Comments
 (0)