From 56dda472229771e6e0e62a78510606651ae4f2c0 Mon Sep 17 00:00:00 2001 From: timon0305 Date: Tue, 14 Jul 2026 04:43:21 +0200 Subject: [PATCH 1/2] fix: keep test-only source out of the published build (#217) The tsconfig exclude pointed at a stale path (src/server/tools/test-helpers.ts), so the real file (src/core/server/tools/test-helpers.ts) and the *.compile-test.ts brand guards were compiled into dist/ and shipped in the package. Split the build config from the typecheck config: tsconfig.build.json excludes all test code by pattern (won't rot if files move), while the base tsconfig still type-checks the compile-test guards. build/build:watch now use -p tsconfig.build.json. Adds a regression test that asks tsc which files the build config compiles and fails if any test module survives. --- package.json | 4 +- .../build-excludes-test-code.test.ts | 49 +++++++++++++++++++ tsconfig.build.json | 11 +++++ tsconfig.json | 2 +- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/build-excludes-test-code.test.ts create mode 100644 tsconfig.build.json diff --git a/package.json b/package.json index 09dfb75..d0129de 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ }, "scripts": { "clean": "node -e \"const fs=require('fs'); fs.rmSync('dist',{recursive:true,force:true});\"", - "build": "npm run clean && npx tsc", - "build:watch": "tsc --watch", + "build": "npm run clean && npx tsc -p tsconfig.build.json", + "build:watch": "tsc -p tsconfig.build.json --watch", "dev": "tsx watch src/index.ts", "start": "node dist/index.js", "smoke": "npm run build && node dist/index.js --help", diff --git a/src/__tests__/build-excludes-test-code.test.ts b/src/__tests__/build-excludes-test-code.test.ts new file mode 100644 index 0000000..358c67b --- /dev/null +++ b/src/__tests__/build-excludes-test-code.test.ts @@ -0,0 +1,49 @@ +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +/** + * Guards against test-only source leaking into the published build (#217). + * + * The stale `tsconfig.json` exclude pointed at a path that no longer matched + * `test-helpers.ts`, so it (and the `.compile-test.ts` brand guards) shipped + * into `dist/`. `tsconfig.build.json` now excludes test code by pattern; this + * test asks tsc which files that config would compile and fails if any test + * module is still in the program. + */ + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const tscEntry = path.join(repoRoot, 'node_modules', 'typescript', 'bin', 'tsc'); + +function buildProgramSrcFiles(): string[] { + const out = execFileSync( + process.execPath, + [tscEntry, '-p', 'tsconfig.build.json', '--listFilesOnly'], + { cwd: repoRoot, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 } + ); + return out + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((abs) => path.relative(repoRoot, abs).split(path.sep).join('/')) + .filter((rel) => rel.startsWith('src/')); +} + +const TEST_CODE = [ + /\.test\.ts$/, + /\.compile-test\.ts$/, + /(^|\/)test-helpers\.ts$/, + /(^|\/)__tests__\//, +]; + +describe('build output excludes test code (#217)', () => { + it('the build tsconfig compiles no test-only modules from src', () => { + const files = buildProgramSrcFiles(); + // Guard against a vacuous pass: if the build config ever compiled nothing, + // `leaked` would be empty while the real build is broken. + expect(files).toContain('src/index.ts'); + const leaked = files.filter((f) => TEST_CODE.some((re) => re.test(f))); + expect(leaked).toEqual([]); + }, 60_000); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..d5f57bd --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.compile-test.ts", + "**/test-helpers.ts", + "**/__tests__/**" + ] +} diff --git a/tsconfig.json b/tsconfig.json index eee93ee..7d18e80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,5 +20,5 @@ "typeRoots": ["./node_modules/@types"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts", "src/server/tools/test-helpers.ts"] + "exclude": ["node_modules", "dist", "**/*.test.ts"] } From 7c653f9e0b72eff4a5e57abb2910fbff0cfffcf6 Mon Sep 17 00:00:00 2001 From: timon0305 Date: Wed, 15 Jul 2026 00:06:33 +0200 Subject: [PATCH 2/2] docs: note that the exclude patterns and TEST_CODE regexes must stay in sync Cross-reference tsconfig.build.json's exclude and the TEST_CODE regexes in build-excludes-test-code.test.ts so a new test-only suffix gets added in both. --- src/__tests__/build-excludes-test-code.test.ts | 1 + tsconfig.build.json | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/__tests__/build-excludes-test-code.test.ts b/src/__tests__/build-excludes-test-code.test.ts index 358c67b..c0a5df8 100644 --- a/src/__tests__/build-excludes-test-code.test.ts +++ b/src/__tests__/build-excludes-test-code.test.ts @@ -30,6 +30,7 @@ function buildProgramSrcFiles(): string[] { .filter((rel) => rel.startsWith('src/')); } +// Mirrors tsconfig.build.json's exclude — add any new test-only suffix to both. const TEST_CODE = [ /\.test\.ts$/, /\.compile-test\.ts$/, diff --git a/tsconfig.build.json b/tsconfig.build.json index d5f57bd..8997abd 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,5 +1,7 @@ { "extends": "./tsconfig.json", + // New test-only suffixes must also go in TEST_CODE in + // src/__tests__/build-excludes-test-code.test.ts, which guards this list. "exclude": [ "node_modules", "dist",