Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,13 @@ By default, if Vitest finds source error, it will fail test suite.

Path to custom tsconfig, relative to the project root.

#### typecheck.spawnTimeout

- **Type**: `number`
- **Default**: `10_000`

Minimum time in milliseconds it takes to spawn the typechecker.

### slowTestThreshold<NonProjectOption />

- **Type**: `number`
Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,10 @@ export const cliOptionsConfig: VitestCLIOptions = {
argument: '<path>',
normalize: true,
},
spawnTimeout: {
description: 'Minimum time in milliseconds it takes to spawn the typechecker',
argument: '<time>',
},
include: null,
exclude: null,
},
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/pools/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function createTypecheckPool(vitest: Vitest): ProcessPool {

async function startTypechecker(project: TestProject, files: string[]) {
if (project.typechecker) {
return project.typechecker
return
}
const checker = await createWorkspaceTypechecker(project, files)
await checker.collectTests()
Expand Down Expand Up @@ -154,7 +154,7 @@ export function createTypecheckPool(vitest: Vitest): ProcessPool {
}
promises.push(promise)
promisesMap.set(project, promise)
startTypechecker(project, files)
promises.push(startTypechecker(project, files))
}

await Promise.all(promises)
Expand Down
5 changes: 5 additions & 0 deletions packages/vitest/src/node/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,11 @@ export interface TypecheckConfig {
* Path to tsconfig, relative to the project root.
*/
tsconfig?: string
/**
* Minimum time in milliseconds it takes to spawn the typechecker.
* @default 10_000
*/
spawnTimeout?: number
}

export interface UserConfig extends InlineConfig {
Expand Down
98 changes: 76 additions & 22 deletions packages/vitest/src/typecheck/typechecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { File, Task, TaskEventPack, TaskResultPack, TaskState } from '@vite
import type { ParsedStack } from '@vitest/utils'
import type { EachMapping } from '@vitest/utils/source-map'
import type { ChildProcess } from 'node:child_process'
import type { Result } from 'tinyexec'
import type { Vitest } from '../node/core'
import type { TestProject } from '../node/project'
import type { Awaitable } from '../types/general'
Expand Down Expand Up @@ -277,11 +278,7 @@ export class Typechecker {
return this._output
}

public async start(): Promise<void> {
if (this.process) {
return
}

private async spawn() {
const { root, watch, typecheck } = this.project.config

const args = [
Expand Down Expand Up @@ -314,31 +311,88 @@ export class Typechecker {
},
throwOnError: false,
})

this.process = child.process
await this._onParseStart?.()

let rerunTriggered = false
child.process?.stdout?.on('data', (chunk) => {
this._output += chunk
if (!watch) {
let dataReceived = false

return new Promise<{ result: Result }>((resolve, reject) => {
if (!child.process || !child.process.stdout) {
reject(new Error(`Failed to initialize ${typecheck.checker}. This is a bug in Vitest - please, open an issue with reproduction.`))
return
}
if (this._output.includes('File change detected') && !rerunTriggered) {
this._onWatcherRerun?.()
this._startTime = performance.now()
this._result.sourceErrors = []
this._result.files = []
this._tests = null // test structure might've changed
rerunTriggered = true

child.process.stdout.on('data', (chunk) => {
dataReceived = true
this._output += chunk
if (!watch) {
return
}
if (this._output.includes('File change detected') && !rerunTriggered) {
this._onWatcherRerun?.()
this._startTime = performance.now()
this._result.sourceErrors = []
this._result.files = []
this._tests = null // test structure might've changed
rerunTriggered = true
}
if (/Found \w+ errors*. Watching for/.test(this._output)) {
rerunTriggered = false
this.prepareResults(this._output).then((result) => {
this._result = result
this._onParseEnd?.(result)
})
this._output = ''
}
})

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

function onError(cause: Error) {
clearTimeout(timeout)
reject(new Error('Spawning typechecker failed - is typescript installed?', { cause }))
}
if (/Found \w+ errors*. Watching for/.test(this._output)) {
rerunTriggered = false
this.prepareResults(this._output).then((result) => {
this._result = result
this._onParseEnd?.(result)

child.process.once('spawn', () => {
this._onParseStart?.()
child.process?.off('error', onError)
clearTimeout(timeout)
if (process.platform === 'win32') {
// 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(() => {
resolve({ result: child })
}, 200)
}
else {
resolve({ result: child })
}
})

if (process.platform === 'win32') {
child.process.once('close', (code) => {
if (code != null && code !== 0 && !dataReceived) {
onError(new Error(`The ${typecheck.checker} command exited with code ${code}.`))
}
})
this._output = ''
}
child.process.once('error', onError)
})
}

public async start(): Promise<void> {
if (this.process) {
return
}

const { watch } = this.project.config
const { result: child } = await this.spawn()

if (!watch) {
await child
this._result = await this.prepareResults(this._output)
Expand Down
17 changes: 17 additions & 0 deletions test/typescript/test/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,20 @@ describe('when the title is dynamic', () => {
expect(vitest.stdout).toContain('✓ (() => "some name")()')
})
})

it('throws an error if typechecker process exists', async () => {
const { stderr } = await runVitest({
root: resolve(__dirname, '../fixtures/source-error'),
typecheck: {
enabled: true,
checker: 'non-existing-command',
},
})
expect(stderr).toContain('Error: Spawning typechecker failed - is typescript installed?')
if (process.platform === 'win32') {
expect(stderr).toContain('Error: The non-existing-command command exited with code 1.')
}
else {
expect(stderr).toContain('Error: spawn non-existing-command ENOENT')
}
})
Loading