Summary
On macOS, fspy records no file reads at all for a task whose command type-checks with tsgo (@typescript/native-preview, the Go implementation of TypeScript). The tracked input set is empty, so every subsequent run is a cache hit — including after a real type error is introduced. vp run replays the previous output and exits 0 while the underlying type-check exits 1.
The same task tracks correctly:
- on Linux (same vp version, same compiler version) — reads are recorded and the cache misses as expected;
- on macOS with
tsc (the JS compiler, a plain Node process) — reads are recorded, exactly one task misses;
- on macOS with a non-Node native child (
sh -c "cat file") — the read is recorded.
So this looks specific to the Go binary rather than to native children in general.
This is the same class as #532 (Bun subprocess reads missed → stale replay), which was fixed for Bun. That issue explicitly asked whether the fix would generalise to other native binaries; from what we measured, it does not cover tsgo.
Why this may deserve priority
tsgo is the preview of TypeScript 7 and is intended to become tsc. The combination "Vite+ task cache + TypeScript type-check" will become a mainstream configuration rather than an edge case, and the current failure mode is silent: a green cache hit on code that does not compile.
Environment
Failing (macOS)
- vite-plus
0.2.7
- macOS
darwin 25.5.0, arm64
- Node
22.22.0, pnpm 9.15.0
@typescript/native-preview 7.0.0-dev.20260707.2
Working (Linux, same test)
- Image
ghcr.io/voidzero-dev/vite-plus:0.2.7 (digest sha256:2f9560ae2f5d133abed91bf768c60aa526040c7ebd68010d110d1482634a8e44)
- Debian 12 (bookworm),
linux/arm64, Node 24.19.0
@typescript/native-preview + @typescript/native-preview-linux-arm64 7.0.0-dev.20260707.2
Reproduction
mkdir vp-tsgo-repro && cd vp-tsgo-repro
npm init -y
npm i -D @typescript/native-preview vite-plus@0.2.7
mkdir src
echo 'export const value: string = "ok";' > src/good.ts
printf '{ "compilerOptions": { "strict": true, "noEmit": true }, "include": ["src"] }\n' > tsconfig.json
vite.config.ts:
import { defineConfig } from 'vite-plus';
export default defineConfig({
run: {
tasks: {
typecheck: { command: 'node_modules/.bin/tsgo -p tsconfig.json --noEmit' },
},
},
});
# 1. prime the cache (green)
npx vp run typecheck # miss, exit 0
npx vp run typecheck # hit, exit 0 <- expected
# 2. introduce a real type error
echo 'export const broken: number = "not-a-number";' > src/good.ts
# 3. ground truth
node_modules/.bin/tsgo -p tsconfig.json --noEmit # error TS2322, exit 1
# 4. what Vite+ reports
npx vp run typecheck --verbose # cache hit, output replayed, exit 0
Expected
Step 4 is a cache miss: src/good.ts is an input of the task, it changed, the task re-runs and exits 1.
Actual
Step 4 is a cache hit and exits 0. vp run --last-details shows Cache hit - output replayed with no warning. The tracked input set appears to be empty: modifying any file (including package.json or the lockfile) does not invalidate the entry.
Controls we ran
| Task command (macOS) |
Tracking |
sh -c "cat probe-data.txt" (non-Node native child) |
correct — cache miss: 'probe-data.txt' modified |
Node wrapper → tsc (TSGO=0, JS compiler) |
correct — exactly one miss, the right one |
Node wrapper → tsgo (Go binary) |
no reads recorded, always a hit |
The same Node-wrapper → tsgo command on Linux (both invoking the platform binary directly and going through the full node_modules/.bin/tsgo → sh shim → node → lib/tsgo.js → process.execve(<Go binary>) chain) records reads correctly and misses as expected. Note that lib/tsgo.js uses process.execve to replace its own process image with the Go binary, which may be relevant to how the macOS backend follows the process.
Userland workaround, and why it is not sufficient
We can restore most of the tracking from userland: run tsgo -p <config> --noEmit --listFiles, then re-read each listed path from Node (whose reads are tracked), plus readdirSync over the project directories so that added/removed files change a tracked directory listing.
That recovers correct behaviour for file contents and for file membership, at a cost of roughly 15 µs per listed file. But it cannot cover what --listFiles does not emit — the metadata that determines the program: tsconfig files reached through extends, and package.json of dependencies. We tested this on a monorepo with 100+ libraries under Nx: editing the single root tsconfig.base.json, which all 102 type-checked projects extend, still yields a full green cache replay across the workspace. So this remains a workaround, not a fix.
Secondary request
Would it be possible to surface a diagnostic when a cache-enabled task ends with an empty tracked input set? Any task in that state is guaranteed to be a permanent cache hit. Turning that into a warning (or an opt-in error) would convert this whole class of failures — #532, this one, and any future runtime that bypasses the interposed symbols — from silent false greens into something visible. This overlaps with the fail-open discussion in #533.
Related
Summary
On macOS,
fspyrecords no file reads at all for a task whose command type-checks withtsgo(@typescript/native-preview, the Go implementation of TypeScript). The tracked input set is empty, so every subsequent run is a cache hit — including after a real type error is introduced.vp runreplays the previous output and exits0while the underlying type-check exits1.The same task tracks correctly:
tsc(the JS compiler, a plain Node process) — reads are recorded, exactly one task misses;sh -c "cat file") — the read is recorded.So this looks specific to the Go binary rather than to native children in general.
This is the same class as #532 (Bun subprocess reads missed → stale replay), which was fixed for Bun. That issue explicitly asked whether the fix would generalise to other native binaries; from what we measured, it does not cover
tsgo.Why this may deserve priority
tsgois the preview of TypeScript 7 and is intended to becometsc. The combination "Vite+ task cache + TypeScript type-check" will become a mainstream configuration rather than an edge case, and the current failure mode is silent: a green cache hit on code that does not compile.Environment
Failing (macOS)
0.2.7darwin 25.5.0, arm6422.22.0, pnpm9.15.0@typescript/native-preview7.0.0-dev.20260707.2Working (Linux, same test)
ghcr.io/voidzero-dev/vite-plus:0.2.7(digestsha256:2f9560ae2f5d133abed91bf768c60aa526040c7ebd68010d110d1482634a8e44)linux/arm64, Node24.19.0@typescript/native-preview+@typescript/native-preview-linux-arm647.0.0-dev.20260707.2Reproduction
vite.config.ts:Expected
Step 4 is a cache miss:
src/good.tsis an input of the task, it changed, the task re-runs and exits1.Actual
Step 4 is a cache hit and exits
0.vp run --last-detailsshowsCache hit - output replayedwith no warning. The tracked input set appears to be empty: modifying any file (includingpackage.jsonor the lockfile) does not invalidate the entry.Controls we ran
sh -c "cat probe-data.txt"(non-Node native child)cache miss: 'probe-data.txt' modifiedtsc(TSGO=0, JS compiler)tsgo(Go binary)The same Node-wrapper →
tsgocommand on Linux (both invoking the platform binary directly and going through the fullnode_modules/.bin/tsgo→shshim →node→lib/tsgo.js→process.execve(<Go binary>)chain) records reads correctly and misses as expected. Note thatlib/tsgo.jsusesprocess.execveto replace its own process image with the Go binary, which may be relevant to how the macOS backend follows the process.Userland workaround, and why it is not sufficient
We can restore most of the tracking from userland: run
tsgo -p <config> --noEmit --listFiles, then re-read each listed path from Node (whose reads are tracked), plusreaddirSyncover the project directories so that added/removed files change a tracked directory listing.That recovers correct behaviour for file contents and for file membership, at a cost of roughly 15 µs per listed file. But it cannot cover what
--listFilesdoes not emit — the metadata that determines the program:tsconfigfiles reached throughextends, andpackage.jsonof dependencies. We tested this on a monorepo with 100+ libraries under Nx: editing the single roottsconfig.base.json, which all 102 type-checked projects extend, still yields a full green cache replay across the workspace. So this remains a workaround, not a fix.Secondary request
Would it be possible to surface a diagnostic when a cache-enabled task ends with an empty tracked input set? Any task in that state is guaranteed to be a permanent cache hit. Turning that into a warning (or an opt-in error) would convert this whole class of failures — #532, this one, and any future runtime that bypasses the interposed symbols — from silent false greens into something visible. This overlaps with the fail-open discussion in #533.
Related