Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c6f2eab
fix(typecheck): improve error message when tsc outputs help text
Ujjwaljain16 Dec 8, 2025
5ce857b
test(typecheck): add unit tests for help text detection
Ujjwaljain16 Dec 10, 2025
441a6e9
Merge branch 'main' into fix/typecheck-tsconfig-missing-error
Ujjwaljain16 Dec 10, 2025
f8f1165
Merge branch 'main' into fix/typecheck-tsconfig-missing-error
Ujjwaljain16 Dec 12, 2025
26f4b71
fix(typecheck): fix Windows CI failure for non-existing typechecker c…
Ujjwaljain16 Dec 12, 2025
ecc15fd
fix(typecheck): move winTimeout declaration before use to fix linting…
Ujjwaljain16 Dec 12, 2025
0691f0d
test(typecheck): replace mocked tests with integration test
Ujjwaljain16 Dec 12, 2025
46bf95c
fix(typecheck): improve error message when tsconfig is missing\n\nDet…
Ujjwaljain16 Dec 17, 2025
a2d9ba2
Merge branch 'main' into fix/typecheck-tsconfig-missing-error
Ujjwaljain16 Dec 17, 2025
07b408c
style: fix linting errors
Ujjwaljain16 Dec 17, 2025
609930f
fix: use proper typecheck test file pattern
Ujjwaljain16 Dec 17, 2025
cc0a388
Merge branch 'fix/typecheck-tsconfig-missing-error' of https://github…
Ujjwaljain16 Dec 17, 2025
f09c5d6
fix: use test-d.ts pattern to trigger typecheck execution
Ujjwaljain16 Dec 17, 2025
c12e7f4
chore: remove debug log and document test approach
Ujjwaljain16 Dec 17, 2025
dfe2b54
fix lint
Ujjwaljain16 Dec 17, 2025
130d597
refactor(test): use createFile utility and static imports per review …
Ujjwaljain16 Dec 23, 2025
3468560
Merge branch 'main' into fix/typecheck-tsconfig-missing-error
Ujjwaljain16 Dec 23, 2025
7874e85
Merge branch 'main' into fix/typecheck-tsconfig-missing-error
Ujjwaljain16 Dec 23, 2025
a0e8efb
refactor: remove console.error and redundant mkdirSync per review
Ujjwaljain16 Dec 23, 2025
d5ec671
refactor: remove console.error and redundant mkdirSync per review
Ujjwaljain16 Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions packages/vitest/src/typecheck/typechecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class Typechecker {

protected files: string[] = []

constructor(protected project: TestProject) {}
constructor(protected project: TestProject) { }

public setFiles(files: string[]): void {
this.files = files
Expand Down Expand Up @@ -123,6 +123,22 @@ export class Typechecker {
sourceErrors: TestError[]
time: number
}> {
// Detect if tsc output is help text instead of error output
// This happens when tsconfig.json is missing and tsc can't find any config
if (output.includes('The TypeScript Compiler - Version') || output.includes('COMMON COMMANDS')) {
const { typecheck } = this.project.config
const tsconfigPath = typecheck.tsconfig || 'tsconfig.json'
const msg = `TypeScript compiler returned help text instead of type checking results.\n`
+ `This usually means the tsconfig file was not found.\n\n`
+ `Possible solutions:\n`
+ ` 1. Ensure '${tsconfigPath}' exists in your project root\n`
+ ` 2. If using a custom tsconfig, verify the path in your Vitest config:\n`
+ ` test: { typecheck: { tsconfig: 'path/to/tsconfig.json' } }\n`
+ ` 3. Check that the tsconfig file is valid JSON`

throw new Error(msg)
}

const typeErrors = await this.parseTscLikeOutput(output)
const testFiles = new Set(this.getFiles())

Expand Down Expand Up @@ -319,6 +335,8 @@ export class Typechecker {
return
}

let resolved = false

child.process.stdout.on('data', (chunk) => {
dataReceived = true
this._output += chunk
Expand All @@ -343,13 +361,25 @@ export class Typechecker {
}
})

// Also capture stderr for configuration errors like missing tsconfig
child.process.stderr?.on('data', (chunk) => {
this._output += chunk
})

const timeout = setTimeout(
() => reject(new Error(`${typecheck.checker} spawn timed out`)),
this.project.config.typecheck.spawnTimeout,
)

let winTimeout: NodeJS.Timeout | undefined

function onError(cause: Error) {
if (resolved) {
return
}
clearTimeout(timeout)
clearTimeout(winTimeout)
resolved = true
reject(new Error('Spawning typechecker failed - is typescript installed?', { cause }))
}

Expand All @@ -361,11 +391,13 @@ export class Typechecker {
// on Windows, the process might be spawned but fail to start
// we wait for a potential error here. if "close" event didn't trigger,
// we resolve the promise
setTimeout(() => {
winTimeout = setTimeout(() => {
resolved = true
resolve({ result: child })
}, 200)
}
else {
resolved = true
resolve({ result: child })
}
})
Expand Down
51 changes: 51 additions & 0 deletions test/typescript/test/typecheck-error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { createFile, runInlineTests } from '../../test-utils'

describe('Typechecker Error Handling', () => {
it('throws helpful error when tsc outputs help text (missing config)', async () => {
// TESTING APPROACH:
// We cannot reliably trigger tsc's help text output in normal usage because:
// 1. tsc only shows help when called with NO arguments or INVALID arguments
// 2. Vitest always calls tsc with proper arguments (--noEmit, --pretty, etc.)
// 3. Invalid tsconfig causes ERROR output, not help text
//
// SOLUTION: Use a test executable that mimics tsc help output
// This is NOT a mock (no jest.mock or similar), but a real executable script
// that Vitest spawns and executes, validating the error handling logic works.

// Create a temporary directory for our fake tsc
const tmpDir = path.join(os.tmpdir(), `vitest-test-${Date.now()}`)
Comment thread
sheremet-va marked this conversation as resolved.

// Create fake tsc script - cross-platform executable
// Using createFile ensures cleanup even if test fails
const fakeTscPath = path.join(tmpDir, 'fake-tsc')
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'

createFile(fakeTscPath, scriptContent)
fs.chmodSync(fakeTscPath, '755')

const configContent = `import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
typecheck: {
enabled: true,
checker: '${fakeTscPath.replace(/\\/g, '/')}',
},
},
})`

const { stderr, stdout } = await runInlineTests({
'vitest.config.ts': configContent,
'example.test-d.ts': 'import { expectTypeOf, test } from \'vitest\'\ntest(\'dummy type test\', () => { expectTypeOf(1).toEqualTypeOf<number>() })',
})

// Assert that Vitest caught the help text and threw the descriptive error
const output = stderr + stdout
expect(output).toContain('TypeScript compiler returned help text instead of type checking results')
expect(output).toContain('This usually means the tsconfig file was not found')
expect(output).toContain('Ensure \'tsconfig.json\' exists in your project root')
})
})
18 changes: 18 additions & 0 deletions test/typescript/test/typechecker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { resolve } from 'pathe'
import { describe, expect, it } from 'vitest'
import { runVitest } from '../../test-utils'

describe('Typechecker', () => {
it('handles non-existing typechecker command gracefully', async () => {
const { stderr } = await runVitest({
root: resolve(import.meta.dirname, '../fixtures/source-error'),
typecheck: {
enabled: true,
checker: 'non-existing-tsc-command',
},
})

// Should show proper error when typechecker doesn't exist
expect(stderr).toContain('Spawning typechecker failed')
})
})
Loading