Skip to content

fspy auto input-tracking misses tsgo (TypeScript native / Go binary) reads on macOS — cached tasks replay stale results and report success on broken code #587

Description

@jderboven

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/tsgosh shim → nodelib/tsgo.jsprocess.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions