fix(scripts): pin the tsbuildinfo inside build/, assert on cache hits
Two findings from review round 1, both real. The default incremental cache path derives from `rootDir`, so it landed at `test-servers/tsconfig.tsbuildinfo` — beside the config, OUTSIDE `build/`. That survives the `rm -rf test-servers/build` this project documents as the remedy for a deleted source file, leaving tsc convinced its outputs are up to date and emitting nothing. Pinned to `./build/tsconfig.tsbuildinfo`, and verified that removing the output dir now forces a full re-emit. The build is memoized per repo root; the existence assertion was memoized with it. A later caller naming an entry the first did not would be handed a path tsc never emitted, then fail with exactly the opaque ERR_MODULE_NOT_FOUND the helper exists to replace. The assertion now runs on every call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
e59452ea8c
commit
8d193461e3
3 changed files with 39 additions and 8 deletions
|
|
@ -92,9 +92,10 @@ const built = new Set();
|
||||||
*
|
*
|
||||||
* Unconditional per *process*, not per call: `pack-and-verify.mjs` asks twice
|
* Unconditional per *process*, not per call: `pack-and-verify.mjs` asks twice
|
||||||
* (once for the stdio entry, once for the composable one) and one `tsc` pass
|
* (once for the stdio entry, once for the composable one) and one `tsc` pass
|
||||||
* emits both, so a second call within the same run is a no-op. A fresh process
|
* emits both, so a second call within the same run skips the build. A fresh
|
||||||
* always rebuilds, which is the whole point — that is what makes an edit to
|
* process always rebuilds, which is the whole point — that is what makes an
|
||||||
* `test-servers/src` reach the next smoke.
|
* edit to `test-servers/src` reach the next smoke. The *existence* assertion is
|
||||||
|
* not cached, because a later call can name an entry the first one didn't.
|
||||||
*
|
*
|
||||||
* `log` receives the bare message; the default prefixes it with `label`, which
|
* `log` receives the bare message; the default prefixes it with `label`, which
|
||||||
* is what a caller owning its own prefixed logger (pack-and-verify's `step`)
|
* is what a caller owning its own prefixed logger (pack-and-verify's `step`)
|
||||||
|
|
@ -113,16 +114,29 @@ export function ensureTestServers({
|
||||||
exists = existsSync,
|
exists = existsSync,
|
||||||
}) {
|
}) {
|
||||||
const paths = requires.map((name) => testServerEntryPath(repoRoot, name));
|
const paths = requires.map((name) => testServerEntryPath(repoRoot, name));
|
||||||
if (built.has(repoRoot)) return paths;
|
|
||||||
|
// A cached call can name an entry the first one didn't, so the existence
|
||||||
|
// assertion runs on every call. Skipping it on the cache hit would return a
|
||||||
|
// path to a file tsc never emitted, and the consumer would then fail with
|
||||||
|
// exactly the opaque ERR_MODULE_NOT_FOUND this helper exists to replace.
|
||||||
|
if (built.has(repoRoot)) {
|
||||||
|
assertEmitted(paths, exists, 0);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
log("building test-servers...");
|
log("building test-servers...");
|
||||||
const status = run(repoRoot);
|
const status = run(repoRoot);
|
||||||
|
assertEmitted(paths, exists, status);
|
||||||
|
built.add(repoRoot);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throw the shared actionable message unless tsc succeeded and emitted each path. */
|
||||||
|
function assertEmitted(paths, exists, status) {
|
||||||
const missing = paths.filter((p) => !exists(p));
|
const missing = paths.filter((p) => !exists(p));
|
||||||
if (status !== 0 || missing.length) {
|
if (status !== 0 || missing.length) {
|
||||||
throw new Error(buildFailureMessage(missing));
|
throw new Error(buildFailureMessage(missing));
|
||||||
}
|
}
|
||||||
built.add(repoRoot);
|
|
||||||
return paths;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test-only: forget which roots this process has built. */
|
/** Test-only: forget which roots this process has built. */
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,18 @@ describe("ensureTestServers", () => {
|
||||||
assert.deepEqual(paths, [testServerEntryPath(ROOT, "composable")]);
|
assert.deepEqual(paths, [testServerEntryPath(ROOT, "composable")]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("still asserts existence on a cache hit that names a new entry", () => {
|
||||||
|
// The build is cached; the assertion is not. A second caller asking for an
|
||||||
|
// entry the first didn't must not be handed a path tsc never emitted.
|
||||||
|
const absent = testServerEntryPath(ROOT, "composable");
|
||||||
|
const present = (p) => p !== absent;
|
||||||
|
callEnsure({ requires: ["stdio"], present });
|
||||||
|
assert.throws(
|
||||||
|
() => callEnsure({ requires: ["composable"], present }),
|
||||||
|
(e) => e.message.includes(absent),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("builds again for a different repo root", () => {
|
it("builds again for a different repo root", () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
callEnsure({ calls });
|
callEnsure({ calls });
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,14 @@
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
// The script consumers rebuild this project on every run (#2111), so the
|
// The script consumers rebuild this project on every run (#2111), so the
|
||||||
// up-to-date case has to be cheap. `.tsbuildinfo` lands under `build/`,
|
// up-to-date case has to be cheap.
|
||||||
// which is gitignored.
|
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
// Pinned INSIDE the output dir. The default derives from `rootDir`, landing
|
||||||
|
// it at `test-servers/tsconfig.tsbuildinfo` — beside the config, outside
|
||||||
|
// `build/`. That would survive the `rm -rf test-servers/build` this project
|
||||||
|
// documents as the remedy for a deleted source file, leaving tsc convinced
|
||||||
|
// its outputs are up to date and emitting nothing.
|
||||||
|
"tsBuildInfoFile": "./build/tsconfig.tsbuildinfo",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue