Core dist cache buildinfo: why the build is non-incremental
Status: decided and built. This note records the RollupError a stale dist/tsconfig.tsbuildinfo produced, the single condition that flips it, the alternatives measured and set aside, and the decision taken: both Rollup compiles of packages/core and apps/cli run non-incremental through a shared tsconfig.rollup.json. An earlier cache-side decision is recorded as superseded. See docs/notes/README.md.
What was measured
A pull request with a packages/core/dist cache hit failed the web suite's pretest build with:
RollupError: rollup.config.ts (43:29): Expected ',', got ':'
-- rollup parsing raw TypeScript because @rollup/plugin-typescript served it no transpiled config. Reproduced byte-for-byte from a fresh clone: build core, archive dist/, delete it, restamp every tracked file to simulate a fresh checkout, extract the archive back, then run the build the suite's pretest runs.
The single difference that flips the result is whether packages/core/.rollup.cache/ is present. packages/core/tsconfig.json sets composite: true, and a build compiling under it maintained two artifacts that had to stay in step: dist/tsconfig.tsbuildinfo, which tells TypeScript "already emitted, skip", and .rollup.cache/, the copy @rollup/plugin-typescript serves instead when TypeScript skips emitting. .rollup.cache/ is gitignored and was never part of the cached path, so a fresh checkout has none; a cache hit restores the buildinfo without it, TypeScript decides rollup.config.ts is unchanged and emits nothing, the plugin's cache fallback has nothing to serve, and rollup reads the raw .ts file. A cache miss runs a full npm run build -w packages/core, which writes both artifacts together, so the later pretest build is served from .rollup.cache/ and succeeds -- the asymmetry the symptom shows only on a hit.
Mechanism
The restore step's dist cache key already hashes packages/core/src, rollup.config.ts, and the tsconfig chain, so a hit binds the artifact to its inputs by content. The buildinfo was the one entry under dist/ that was state rather than product: nothing downstream read it (the apps import the built core.esm.js / core.cjs / index.d.ts, and the freshness guard in docs/TESTING.md compares source-to-dist mtimes, not the buildinfo), and its own include covers packages/core/test/** and vitest.config.ts, both outside the cache key -- so a restored buildinfo could describe a file set the checkout does not have, independent of the .rollup.cache/ gap.
Alternatives considered
- Cache
.rollup.cache/alongsidedist/. Rejected: its filenames embed the build's absolute path, so a runner or a local repro tree at a different path reproduces the sameRollupErroragainst a restored copy of its own cache. It also duplicates every emitted file a second time. - Restamp differently (e.g. stamping
dist/older than sources instead of touching every file to now). Rejected: the failure reproduces with and without the restamp, and with every source-vs-dist mtime ordering tried -- restamping addresses the freshness guard and is orthogonal to this failure. - Exclude the buildinfo from the cached path list alone, without removing it on a hit. Rejected as the sole fix: an entry already saved under its content-derived key is immutable, so an exclusion added going forward does not stop a currently-cached entry from continuing to restore the buildinfo it was saved with.
Decision
Every compile @rollup/plugin-typescript runs is non-incremental, so no build-info file is read or written. Each of packages/core and apps/cli has a tsconfig.rollup.json that extends its tsconfig.json with composite: false, incremental: false, declaration: false and declarationMap: false. Both compiles of a build take it:
rollup.config.tsitself, through thetsconfigoption of--configPluginin thebuildscript (and core'sdevscript);- the sources, through the
tsconfigoption of thetypescript()plugin inrollup.config.ts.
The failure reaches local builds as well as the CI cache: a build-info file with no .rollup.cache/ beside it -- a deleted cache, or a checkout at a new path, since the cache's paths are absolute -- broke the config compile. A non-incremental compile has no build-info file to go stale. Measured against the real build:
incremental: falsealone still fails: withcompositeon, the config-plugin compile skipsrollup.config.ts.- Turning off
compositein the config-plugin compile alone moves the failure to the sources:src/index.tsis parsed as raw TypeScript. - Without the two declaration options,
apps/cli(whose tsconfig setsdeclaration) writes.d.tsfiles intodist/.
With all four, a clean build of either workspace produces the same dist/ file for file, less the build-info file, and no .rollup.cache/ directory. The cost is a full emit on every build. The project-reference typecheck is unaffected: it reads tsconfig.json, which keeps composite.
scripts/rollup-stale-buildinfo.test.mjs copies both workspaces, leaves the build-info file an incremental compile writes and no cache, and runs each npm run build; it runs in the repo-scripts project, which npm run check:all drives, not in the unit suites, because two full builds take about a minute.
Superseded: removing the build-info file on a cache hit
The first fix was confined to CI. A packages/core/dist cache hit removed the restored tsconfig.tsbuildinfo before restamping, and the cached path list excluded it, in .github/actions/setup/action.yml. It left local builds exposed to the same failure, and the non-incremental build replaced it.
What this does not cover
This is scoped to the TypeScript emit of the Rollup builds. It does not change what the packages/core/dist cache key binds, the freshness guard compared in docs/TESTING.md, or the install-tree cache the same action also restores.