[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(typecheck): improve error message when tsc outputs help text (#9214)

authored by

Ujjwaljain16 and committed by
GitHub
(Dec 23, 2025, 6:15 PM +0100) 7b10ab4c 5d84eeb9

+103 -2
+51
test/typescript/test/typecheck-error.test.ts
··· 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 + })
+18
test/typescript/test/typechecker.test.ts
··· 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 + })
+34 -2
packages/vitest/src/typecheck/typechecker.ts
··· 53 53 54 54 protected files: string[] = [] 55 55 56 - constructor(protected project: TestProject) {} 56 + constructor(protected project: TestProject) { } 57 57 58 58 public setFiles(files: string[]): void { 59 59 this.files = files ··· 123 123 sourceErrors: TestError[] 124 124 time: number 125 125 }> { 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 + 126 142 const typeErrors = await this.parseTscLikeOutput(output) 127 143 const testFiles = new Set(this.getFiles()) 128 144 ··· 319 335 return 320 336 } 321 337 338 + let resolved = false 339 + 322 340 child.process.stdout.on('data', (chunk) => { 323 341 dataReceived = true 324 342 this._output += chunk ··· 343 361 } 344 362 }) 345 363 364 + // Also capture stderr for configuration errors like missing tsconfig 365 + child.process.stderr?.on('data', (chunk) => { 366 + this._output += chunk 367 + }) 368 + 346 369 const timeout = setTimeout( 347 370 () => reject(new Error(`${typecheck.checker} spawn timed out`)), 348 371 this.project.config.typecheck.spawnTimeout, 349 372 ) 350 373 374 + let winTimeout: NodeJS.Timeout | undefined 375 + 351 376 function onError(cause: Error) { 377 + if (resolved) { 378 + return 379 + } 352 380 clearTimeout(timeout) 381 + clearTimeout(winTimeout) 382 + resolved = true 353 383 reject(new Error('Spawning typechecker failed - is typescript installed?', { cause })) 354 384 } 355 385 ··· 361 391 // on Windows, the process might be spawned but fail to start 362 392 // we wait for a potential error here. if "close" event didn't trigger, 363 393 // we resolve the promise 364 - setTimeout(() => { 394 + winTimeout = setTimeout(() => { 395 + resolved = true 365 396 resolve({ result: child }) 366 397 }, 200) 367 398 } 368 399 else { 400 + resolved = true 369 401 resolve({ result: child }) 370 402 } 371 403 })