Skip to content

Fix flaky Type check: stop type-check from clobbering the build's tsbuildinfo - #8983

Merged
isaacroldan merged 1 commit into
mainfrom
rcb/fix-type-check-tsbuildinfo-race
Oct 22, 2026
Merged

isaacroldan merged 1 commit into
mainfrom
rcb/fix-type-check-tsbuildinfo-race

Conversation

@ryancbahan

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

The Type check CI job is flaky. It failed on #8982 (run) with:

theme: src/cli/services/duplicate.ts(3,31): error TS2306: File '.../packages/cli-kit/dist/public/node/themes/urls.d.ts' is not a module.

…and passed on re-run with no code change. Root cause is a race in how type-check and build share TypeScript's incremental state:

  1. configurations/tsconfig.json sets composite: true, which implies incremental. So tsc --noEmit (the type-check target) still writes a tsbuildinfo.
  2. Each package's tsconfig.json and tsconfig.build.json share tsBuildInfoFile: dist/tsconfig.tsbuildinfo. So X:type-check overwrites the file that X:build (tsc -b tsconfig.build.json) owns — with a different program (test files included) and every file flagged affectedFilesPendingEmit.
  3. nx run-many --target=type-check runs X:type-check in parallel with X:build (type-check only dependsOn: ^build). On CI, cli-kit's type-check finishes after its build, so the clobbered state wins.
  4. Every downstream tsc -b that references: ../cli-kit then sees "out of date because buildinfo indicates that some of the changes were not emitted" and re-emits cli-kit/dist/** — concurrently with sibling *:type-check tasks reading those .d.ts files. A reader that catches a file between truncate and write sees an empty .d.ts → TS2306: not a module.

This is pre-existing and timing-dependent; it isn't caused by the Node bump in #8982, that PR just happened to lose the race.

WHAT is this pull request doing?

One-line change to every type-check target: point its tsbuildinfo at node_modules/.cache/tsc/type-check.tsbuildinfo so it never touches the build's incremental state.

-"command": "pnpm tsc --noEmit",
+"command": "pnpm tsc --noEmit --tsBuildInfoFile node_modules/.cache/tsc/type-check.tsbuildinfo",

Why this location:

  • node_modules/ is gitignored, so nx doesn't hash it as a task input — type-check cache hits are unaffected (verified: warm run = 21/21 tasks from cache).
  • Not under dist/, so it can't perturb the bundle target's dependentTasksOutputFiles hash or get wiped by build cache restores.
  • Keeps type-check incremental locally (re-check of cli-kit drops from ~3.4s to ~1.5s on my machine).

Why not simply disable incremental: --incremental false errors with TS6379 (Composite projects may not disable incremental compilation). --composite false does work, but relaxes composite's "every file must be listed in include" check for type-check and gives up incremental speed, so the explicit separate file seemed cleaner. e2e has composite: false so the flag is a no-op there; kept for uniformity.

Verification (local, nx run-many --all --target=type-check from a clean dist/ + nx reset, inspecting packages/cli-kit/dist/tsconfig.tsbuildinfo afterwards):

fileNames *.test.ts roots pending emit *.test.* files in cli-kit/dist
Before (main) 2537 179 0 (after downstream re-emit) 537
After (3 cold runs, identical) 2136 0 0 81†
Standalone clean cli-kit:build (reference) 2136 0 0 81†

After the fix, the post-run state is byte-identical to a standalone build, and tsc -b --dry --verbose from theme reports cli-kit "is up to date" — no downstream rebuild, no concurrent writers to dist/.

† 27 *.test.tsx files (81 outputs) are emitted by a plain build today because tsconfig.build.json excludes **/*.test.ts but not .tsx. Pre-existing and unrelated; noted as a possible follow-up below.

How to manually test your changes?

rm -rf packages/*/dist packages/*/node_modules/.cache/tsc && pnpm nx reset
pnpm nx run-many --all --target=type-check --output-style=stream
# build tsbuildinfo untouched by type-check: no *.test.ts roots
node -e 'const p=JSON.parse(require("fs").readFileSync("packages/cli-kit/dist/tsconfig.tsbuildinfo","utf8"));console.log(p.fileNames.filter(f=>/\.test\.ts$/.test(f)).length)'  # → 0 (was 179)
# downstream tsc -b agrees cli-kit is up to date
cd packages/theme && pnpm tsc -b ./tsconfig.build.json --dry --verbose | grep cli-kit

Possible follow-ups (not in this PR)

  • Point references at ../cli-kit/tsconfig.build.json rather than ../cli-kit, so downstream tsc -b evaluates the same program that cli-kit:build emits. Today a locally-edited test file can still make a downstream tsc -b rebuild cli-kit with the full tsconfig.json program and emit test outputs into dist/.
  • Add **/*.test.tsx to the exclude in tsconfig.build.json files so builds stop emitting test outputs.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows) — relative path, no shell features; tsc creates the directory itself.
  • I've considered possible documentation changes — none, internal tooling only.
  • I've considered analytics changes to measure impact — n/a.
  • The change is not user-facing — no changeset.

The shared tsconfig sets `composite: true`, which implies `incremental`.
That means `tsc --noEmit` (the `type-check` target) still writes a
tsbuildinfo, and because tsconfig.json and tsconfig.build.json share
`tsBuildInfoFile: dist/tsconfig.tsbuildinfo`, type-check overwrites the
file that `build` (`tsc -b tsconfig.build.json`) owns.

`nx run-many --target=type-check` runs `X:type-check` in parallel with
`X:build` (type-check only dependsOn ^build). When type-check finishes
last, the build's tsbuildinfo now describes a different program (test
files included) with every file marked as pending emit. Every downstream
`tsc -b` that references `../X` then considers X out of date and
re-emits X/dist concurrently, while sibling type-check tasks are reading
those .d.ts files. A reader can catch a truncated file mid-write, which
surfaces as:

  error TS2306: File '.../cli-kit/dist/public/node/themes/urls.d.ts'
  is not a module.

Point type-check's tsbuildinfo at node_modules/.cache/tsc/ instead so it
never touches the build's incremental state. node_modules is gitignored,
so nx does not hash the file as a task input (cache hits are unaffected),
and type-check keeps its incremental speed-up locally.

After this change, a full `nx run-many --all --target=type-check` leaves
cli-kit/dist/tsconfig.tsbuildinfo byte-identical to a standalone clean
build (2136 root files, 0 pending emits), whereas before it ended up
with 2537 root files including 179 *.test.ts and 537 test outputs
emitted into cli-kit/dist by downstream rebuilds.
@ryancbahan
ryancbahan requested review from a team as code owners October 22, 2026 03:49
@github-actions github-actions Bot added the no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. label Oct 22, 2026
@isaacroldan
isaacroldan enabled auto-merge October 22, 2026 07:31

@karreiro karreiro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @ryancbahan!

@isaacroldan
isaacroldan added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 8eaf7ad Oct 22, 2026
30 checks passed
@isaacroldan
isaacroldan deleted the rcb/fix-type-check-tsbuildinfo-race branch October 22, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants