From 77aac87ddd8062c70c46aa47f8bd76ac91753c72 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 10 Aug 2026 15:19:54 +0200 Subject: [PATCH] fix(typecheck): report a checker crash on Windows instead of a spawn failure (#10907) --- .../vitest/src/node/pools/workers/typecheckWorker.ts | 5 ++--- packages/vitest/src/typecheck/typechecker.ts | 7 ++++++- test/typescript/fixtures/typecheck-crash/fake-tsc.cmd | 2 ++ test/typescript/test/typechecker.test.ts | 9 ++++++--- 4 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 test/typescript/fixtures/typecheck-crash/fake-tsc.cmd diff --git a/packages/vitest/src/node/pools/workers/typecheckWorker.ts b/packages/vitest/src/node/pools/workers/typecheckWorker.ts index 954af48bd..3f099ddc7 100644 --- a/packages/vitest/src/node/pools/workers/typecheckWorker.ts +++ b/packages/vitest/src/node/pools/workers/typecheckWorker.ts @@ -7,7 +7,7 @@ import type { TestRunEndReason } from '../../types/reporter' import type { PoolOptions, PoolWorker, WorkerRequest, WorkerResponse } from '../types' import EventEmitter from 'node:events' import { createDefer } from '@vitest/utils/helpers' -import { Typechecker } from '../../../typecheck/typechecker' +import { OOM_OUTPUT_PATTERN, Typechecker } from '../../../typecheck/typechecker' import { hasFailed } from '../../../utils/tasks' /** @experimental */ @@ -130,8 +130,7 @@ function createRunner(vitest: Vitest) { if (exitCode || signal) { const output = checker.getOutput() - const looksLikeOom = signal === 'SIGABRT' - || /JavaScript heap out of memory|Reached heap limit|Allocation failed/i.test(output) + const looksLikeOom = signal === 'SIGABRT' || OOM_OUTPUT_PATTERN.test(output) let message: string if (signal || looksLikeOom) { diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index efbdf934d..9355db5b1 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -18,6 +18,9 @@ import { createLocationsIndexMap } from '../utils/base' import { convertTasksToEvents } from '../utils/tasks' import { getRawErrsMapFromTsCompile } from './parse' +// the V8 fatal output of a checker that ran out of memory +export const OOM_OUTPUT_PATTERN: RegExp = /JavaScript heap out of memory|Reached heap limit|Allocation failed/i + export class TypeCheckError extends Error { name = 'TypeCheckError' @@ -428,7 +431,9 @@ export class Typechecker { if (process.platform === 'win32') { child.process.once('close', (code) => { - if (code != null && code !== 0 && !dataReceived) { + // an OOM abort writes only to stderr, but the checker did start; + // `start` awaits the process and reports the crash from its output + if (code != null && code !== 0 && !dataReceived && !OOM_OUTPUT_PATTERN.test(this._output)) { onError(new Error(`The ${typecheck.checker} command exited with code ${code}.`)) } else if (!resolved) { diff --git a/test/typescript/fixtures/typecheck-crash/fake-tsc.cmd b/test/typescript/fixtures/typecheck-crash/fake-tsc.cmd new file mode 100644 index 000000000..e3b5d003b --- /dev/null +++ b/test/typescript/fixtures/typecheck-crash/fake-tsc.cmd @@ -0,0 +1,2 @@ +@rem Windows can't execute .mjs files directly, run the fake checker with node +@node "%~dp0fake-tsc.mjs" %* diff --git a/test/typescript/test/typechecker.test.ts b/test/typescript/test/typechecker.test.ts index cff2d96ae..9e53a191a 100644 --- a/test/typescript/test/typechecker.test.ts +++ b/test/typescript/test/typechecker.test.ts @@ -23,15 +23,18 @@ describe('Typechecker', () => { enabled: true, checker: resolve( import.meta.dirname, - '../fixtures/typecheck-crash/fake-tsc.mjs', + // Windows can't execute an .mjs file, the .cmd shim runs it with node + process.platform === 'win32' + ? '../fixtures/typecheck-crash/fake-tsc.cmd' + : '../fixtures/typecheck-crash/fake-tsc.mjs', ), }, }) // A checker that aborts (OOM) without producing diagnostics must NOT be // reported as passing — the run has to fail with a clear error. The abort - // surfaces as a signal (SIGABRT) on POSIX and as exit code 134 on Windows; - // both paths must be treated as an abnormal, failing exit. + // surfaces as a signal (SIGABRT) on POSIX and as a non-zero exit code on + // Windows; both paths must be treated as an abnormal, failing exit. expect(exitCode).toBe(1) expect(stderr).toContain('Typecheck Error') expect(stderr).toContain('before type checking finished') -- 2.51.2