Skip to content

Commit 91adeb5

Browse files
committed
fix(deadcode): run ts-prune under node for the dead-export gate
Bun-hosted ts-prune under-reports unused exports on Linux, so CI treats a still-valid allowlist as stale. Launch the CLI with node.
1 parent e410730 commit 91adeb5

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

‎scripts/check-dead-exports.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
parseAllowlistText,
2121
parseGuardConfig,
2222
parseTsPruneLine,
23+
tsPruneSpawn,
2324
validateAllowlistEntry,
2425
validateAllowlistOwnership,
2526
validateAllowlistText,
@@ -247,6 +248,19 @@ describe("pinned scan invocation", () => {
247248
expect(existsSync(join(repoRoot, config.tsconfig))).toBe(true);
248249
});
249250

251+
test("ts-prune is launched with node, not as a Bun-executed bin", () => {
252+
const spawn = tsPruneSpawn("/repo/node_modules/.bin/ts-prune", [
253+
"-p",
254+
"tsconfig.json",
255+
]);
256+
expect(spawn.command).toBe("node");
257+
expect(spawn.args).toEqual([
258+
"/repo/node_modules/.bin/ts-prune",
259+
"-p",
260+
"tsconfig.json",
261+
]);
262+
});
263+
250264
test("parseGuardConfig rejects an unpinned or empty invocation", () => {
251265
const valid = {
252266
tsconfig: "tsconfig.json",

‎scripts/check-dead-exports.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,18 @@ export function isGuardPassing(outcome: GuardOutcome): boolean {
272272
return outcome.violations.length === 0 && outcome.unused.length === 0;
273273
}
274274

275+
// ts-prune's analyzer (ts-morph) under-reports unused exports when the CLI
276+
// runs on Bun: Linux CI then treats the Darwin/Node allowlist as stale
277+
// (1 consumer-less export vs ~230). The bin shebang is `node`, but Bun's
278+
// spawn of that file still executes it with Bun. Always launch the CLI
279+
// with node so the gate matches `node node_modules/ts-prune/lib/index.js`.
280+
export function tsPruneSpawn(
281+
tsPruneBinPath: string,
282+
tsPruneArgs: readonly string[],
283+
): { readonly command: string; readonly args: string[] } {
284+
return { command: "node", args: [tsPruneBinPath, ...tsPruneArgs] };
285+
}
286+
275287
// Counts the TypeScript files the pinned tsconfig pulls into its program via
276288
// tsc --listFilesOnly: the same project ts-prune analyzes. A narrowed
277289
// tsconfig (or a moved scan root) shrinks this count, and the gate fails
@@ -329,10 +341,16 @@ function main(): void {
329341
);
330342
}
331343
const rules = parseAllowlistText(allowlistText);
332-
const pruned = spawnSync(tsPruneBin, [...config.tsPruneArgs], {
344+
const prune = tsPruneSpawn(tsPruneBin, config.tsPruneArgs);
345+
const pruned = spawnSync(prune.command, prune.args, {
333346
cwd: repoRoot,
334347
encoding: "utf8",
335348
});
349+
if (pruned.error !== undefined) {
350+
fail(
351+
`ts-prune failed to start with ${prune.command}: ${pruned.error.message}`,
352+
);
353+
}
336354
if (pruned.status !== 0) {
337355
fail(`ts-prune failed:\n${pruned.stderr || pruned.stdout}`);
338356
}

0 commit comments

Comments
 (0)