From 60a02a8787b67bedc45f20d511e1a2160a3013a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 00:01:33 +0000 Subject: [PATCH] fix(devx): pin tsBuildInfoFile outside the published dist in every composite package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `composite` implies `incremental`, so every composite project writes a `*.tsbuildinfo`. Where it lands is DERIVED, not declared: with an `outDir` and no `rootDir`, TypeScript puts the record at `outDir/.tsbuildinfo` — inside `dist`, which `files: ["dist", …]` publishes whole. `check:published-dist` refuses a record there (objectui#7003), so its verdict came to depend on WHEN it ran: green before anything invoked `tsc`, red after, on a byte-identical tree. Measured at c64975e9f with TypeScript's own `getTsBuildInfoEmitOutputFilePath`: 18 of this repository's 32 incremental projects derived their record into `dist`, not the 2 the report named. The discriminator is `rootDir`, not the build tool — the 12 correct ones carry `rootDir: "src"`, which rebases the derived path back out of `outDir`, and `packages/components/tsconfig.json` records in its own comment that `rootDir` was removed there for an unrelated reason. Every composite package now pins the record to its own package root, so the location is a decision rather than a side effect of an unrelated option. A shared setting in the base config was measured and rejected: TypeScript resolves a relative `tsBuildInfoFile` against the config that DECLARES it, so one line in `tsconfig.json` collapses all 30 packages onto a single repo-root record. `clean` in `data-objectstack` and `plugin-designer` now removes the relocated record too, matching the five packages that already spell it that way — without it, `rm -rf dist` would leave a record describing files that no longer exist (objectui#6703). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr --- .../tsbuildinfo-outside-published-dist.md | 10 ++ packages/app-shell/tsconfig.json | 7 ++ packages/auth/tsconfig.json | 7 ++ packages/collaboration/tsconfig.json | 7 ++ packages/components/tsconfig.json | 7 ++ packages/core/tsconfig.json | 7 ++ packages/data-objectstack/package.json | 2 +- packages/data-objectstack/tsconfig.json | 7 ++ packages/i18n/tsconfig.json | 7 ++ packages/mobile/tsconfig.json | 7 ++ packages/permissions/tsconfig.json | 7 ++ packages/plugin-calendar/tsconfig.json | 7 ++ packages/plugin-charts/tsconfig.json | 7 ++ packages/plugin-chatbot/tsconfig.json | 7 ++ packages/plugin-dashboard/tsconfig.json | 7 ++ packages/plugin-designer/package.json | 2 +- packages/plugin-designer/tsconfig.json | 7 ++ packages/plugin-detail/tsconfig.json | 7 ++ packages/plugin-editor/tsconfig.json | 7 ++ packages/plugin-form/tsconfig.json | 7 ++ packages/plugin-gantt/tsconfig.json | 7 ++ packages/plugin-grid/tsconfig.json | 7 ++ packages/plugin-kanban/tsconfig.json | 7 ++ packages/plugin-list/tsconfig.json | 7 ++ packages/plugin-map/tsconfig.json | 7 ++ packages/plugin-markdown/tsconfig.json | 7 ++ packages/plugin-timeline/tsconfig.json | 7 ++ packages/plugin-tree/tsconfig.json | 7 ++ packages/providers/tsconfig.json | 7 ++ packages/react-runtime/tsconfig.json | 14 ++- packages/react/tsconfig.json | 7 ++ packages/sdui-parser/tsconfig.json | 7 ++ packages/types/tsconfig.json | 7 ++ ...ldinfo-outside-published-dist-9189.test.ts | 118 ++++++++++++++++++ 34 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 .changeset/tsbuildinfo-outside-published-dist.md create mode 100644 scripts/__tests__/tsbuildinfo-outside-published-dist-9189.test.ts diff --git a/.changeset/tsbuildinfo-outside-published-dist.md b/.changeset/tsbuildinfo-outside-published-dist.md new file mode 100644 index 0000000000..1d5ec8bf8f --- /dev/null +++ b/.changeset/tsbuildinfo-outside-published-dist.md @@ -0,0 +1,10 @@ +--- +--- + +Internal only — no package release. + +Every composite project now pins `tsBuildInfoFile` to its own package root, so the +incremental build record is never derived into `dist`. Nothing under any package's +`files` list changes, and no published source or publish-contract field moves: +the diff is 30 `tsconfig.json` files, two `clean` scripts and one pin test +(objectui#9189). diff --git a/packages/app-shell/tsconfig.json b/packages/app-shell/tsconfig.json index 379321e699..836084c2bc 100644 --- a/packages/app-shell/tsconfig.json +++ b/packages/app-shell/tsconfig.json @@ -8,6 +8,13 @@ "types": ["node", "vite/client"], "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, // See `packages/react/tsconfig.json` for the full argument: under diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json index 6bacbb312a..a9abd5dbd9 100644 --- a/packages/auth/tsconfig.json +++ b/packages/auth/tsconfig.json @@ -29,6 +29,13 @@ "moduleResolution": "nodenext", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/collaboration/tsconfig.json b/packages/collaboration/tsconfig.json index 6bacbb312a..a9abd5dbd9 100644 --- a/packages/collaboration/tsconfig.json +++ b/packages/collaboration/tsconfig.json @@ -29,6 +29,13 @@ "moduleResolution": "nodenext", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index a871ab9ccc..1c8d1935b8 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -10,6 +10,13 @@ // Removed rootDir to prevent file not under rootDir errors when importing from .. "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "skipLibCheck": true }, diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d26fafee41..fe60ec2b74 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -5,6 +5,13 @@ "rootDir": "src", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true }, "include": ["src"], diff --git a/packages/data-objectstack/package.json b/packages/data-objectstack/package.json index ddbfd882c7..2e397b5073 100644 --- a/packages/data-objectstack/package.json +++ b/packages/data-objectstack/package.json @@ -23,7 +23,7 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "clean": "rm -rf dist", + "clean": "rm -rf dist tsconfig.tsbuildinfo", "type-check": "tsc --noEmit", "test": "vitest run --root ../.. packages/data-objectstack/", "lint": "eslint ." diff --git a/packages/data-objectstack/tsconfig.json b/packages/data-objectstack/tsconfig.json index 6b9c554eb7..93af094fea 100644 --- a/packages/data-objectstack/tsconfig.json +++ b/packages/data-objectstack/tsconfig.json @@ -2,6 +2,13 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declaration": true, "noEmit": false, diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json index fbf207e8bb..53a9e1f704 100644 --- a/packages/i18n/tsconfig.json +++ b/packages/i18n/tsconfig.json @@ -5,6 +5,13 @@ "rootDir": "src", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "jsx": "react-jsx", "lib": ["ES2020", "DOM"] diff --git a/packages/mobile/tsconfig.json b/packages/mobile/tsconfig.json index 6bacbb312a..a9abd5dbd9 100644 --- a/packages/mobile/tsconfig.json +++ b/packages/mobile/tsconfig.json @@ -29,6 +29,13 @@ "moduleResolution": "nodenext", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/permissions/tsconfig.json b/packages/permissions/tsconfig.json index 6bacbb312a..a9abd5dbd9 100644 --- a/packages/permissions/tsconfig.json +++ b/packages/permissions/tsconfig.json @@ -29,6 +29,13 @@ "moduleResolution": "nodenext", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-calendar/tsconfig.json b/packages/plugin-calendar/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-calendar/tsconfig.json +++ b/packages/plugin-calendar/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-charts/tsconfig.json b/packages/plugin-charts/tsconfig.json index f4e8ad59b4..205dae0180 100644 --- a/packages/plugin-charts/tsconfig.json +++ b/packages/plugin-charts/tsconfig.json @@ -10,6 +10,13 @@ "noImplicitAny": true, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "skipLibCheck": true }, diff --git a/packages/plugin-chatbot/tsconfig.json b/packages/plugin-chatbot/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-chatbot/tsconfig.json +++ b/packages/plugin-chatbot/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-dashboard/tsconfig.json b/packages/plugin-dashboard/tsconfig.json index 6afb0ae62c..7ee44f928c 100644 --- a/packages/plugin-dashboard/tsconfig.json +++ b/packages/plugin-dashboard/tsconfig.json @@ -10,6 +10,13 @@ // Removed explicit rootDir to prevent file not under rootDir errors when importing from workspace dependencies "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index c92439fe2a..9c99d541ef 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -22,7 +22,7 @@ ], "scripts": { "build": "vite build", - "clean": "rm -rf dist", + "clean": "rm -rf dist tsconfig.tsbuildinfo", "test": "vitest run --root ../.. packages/plugin-designer/", "type-check": "tsc --noEmit && tsc -p tsconfig.test.json", "lint": "eslint ." diff --git a/packages/plugin-designer/tsconfig.json b/packages/plugin-designer/tsconfig.json index 948179a1e0..9edfffb69f 100644 --- a/packages/plugin-designer/tsconfig.json +++ b/packages/plugin-designer/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-detail/tsconfig.json b/packages/plugin-detail/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-detail/tsconfig.json +++ b/packages/plugin-detail/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-editor/tsconfig.json b/packages/plugin-editor/tsconfig.json index 2741a57bec..5a4c3bbde1 100644 --- a/packages/plugin-editor/tsconfig.json +++ b/packages/plugin-editor/tsconfig.json @@ -10,6 +10,13 @@ "noImplicitAny": true, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "skipLibCheck": true }, diff --git a/packages/plugin-form/tsconfig.json b/packages/plugin-form/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-form/tsconfig.json +++ b/packages/plugin-form/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-gantt/tsconfig.json b/packages/plugin-gantt/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-gantt/tsconfig.json +++ b/packages/plugin-gantt/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-grid/tsconfig.json b/packages/plugin-grid/tsconfig.json index bdbbc4ad8f..8021ab593b 100644 --- a/packages/plugin-grid/tsconfig.json +++ b/packages/plugin-grid/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-kanban/tsconfig.json b/packages/plugin-kanban/tsconfig.json index e95fb2975a..833cfa5065 100644 --- a/packages/plugin-kanban/tsconfig.json +++ b/packages/plugin-kanban/tsconfig.json @@ -10,6 +10,13 @@ // Removed rootDir to prevent file not under rootDir errors when importing from workspace dependencies "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-list/tsconfig.json b/packages/plugin-list/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-list/tsconfig.json +++ b/packages/plugin-list/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-map/tsconfig.json b/packages/plugin-map/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-map/tsconfig.json +++ b/packages/plugin-map/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-markdown/tsconfig.json b/packages/plugin-markdown/tsconfig.json index e95fb2975a..833cfa5065 100644 --- a/packages/plugin-markdown/tsconfig.json +++ b/packages/plugin-markdown/tsconfig.json @@ -10,6 +10,13 @@ // Removed rootDir to prevent file not under rootDir errors when importing from workspace dependencies "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-timeline/tsconfig.json b/packages/plugin-timeline/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-timeline/tsconfig.json +++ b/packages/plugin-timeline/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/plugin-tree/tsconfig.json b/packages/plugin-tree/tsconfig.json index dd77a93991..7bfdd4e272 100644 --- a/packages/plugin-tree/tsconfig.json +++ b/packages/plugin-tree/tsconfig.json @@ -9,6 +9,13 @@ }, "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "declarationMap": true, "skipLibCheck": true diff --git a/packages/providers/tsconfig.json b/packages/providers/tsconfig.json index f970667bf4..c67c00868f 100644 --- a/packages/providers/tsconfig.json +++ b/packages/providers/tsconfig.json @@ -26,6 +26,13 @@ "moduleResolution": "nodenext", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true }, "include": ["src"], diff --git a/packages/react-runtime/tsconfig.json b/packages/react-runtime/tsconfig.json index 29f8fe82bd..f61ca4e00d 100644 --- a/packages/react-runtime/tsconfig.json +++ b/packages/react-runtime/tsconfig.json @@ -1,6 +1,18 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { "outDir": "dist", "rootDir": "src", "noEmit": false, "declaration": true, "composite": true, "jsx": "react-jsx", "lib": ["ES2020", "DOM"] }, + // `tsBuildInfoFile` pins the incremental build RECORD outside the published + // build output in every composite package here; see the longer note in + // packages/components/tsconfig.json for what happens without it (objectui#9189). + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "noEmit": false, + "declaration": true, + "composite": true, + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", + "jsx": "react-jsx", + "lib": ["ES2020", "DOM"] + }, "include": ["src"], // Tooling is excluded by DIRECTORY, not just by file NAME. A helper under // `__tests__/` that is not itself called `*.test.ts` is otherwise a program diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 6d8c329590..a26191a3c4 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -6,6 +6,13 @@ "jsx": "react-jsx", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, // Node's ESM resolver does NOT extension-search relative specifiers, so an diff --git a/packages/sdui-parser/tsconfig.json b/packages/sdui-parser/tsconfig.json index f8da00652e..27f58e966b 100644 --- a/packages/sdui-parser/tsconfig.json +++ b/packages/sdui-parser/tsconfig.json @@ -5,6 +5,13 @@ "rootDir": "src", "noEmit": false, "declaration": true, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "lib": ["ES2020"] }, diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index 79222d4366..2f60485311 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -5,6 +5,13 @@ "rootDir": "./src", "declaration": true, "declarationMap": false, + // The incremental build RECORD, pinned OUTSIDE the published build output. + // Left unpinned, TypeScript derives it from `outDir`/`rootDir`: a package with + // no `rootDir` gets `dist/tsconfig.tsbuildinfo` -- inside what `files: ["dist", + // ...]` publishes -- and `pnpm check:published-dist` then reds on a byte-identical + // tree as soon as anything runs `tsc` (objectui#9189). Pinned in every composite + // package so the location is a decision, not a side effect of an unrelated option. + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "composite": true, "noEmit": false, "lib": ["ES2020", "DOM"], diff --git a/scripts/__tests__/tsbuildinfo-outside-published-dist-9189.test.ts b/scripts/__tests__/tsbuildinfo-outside-published-dist-9189.test.ts new file mode 100644 index 0000000000..0b8e84e601 --- /dev/null +++ b/scripts/__tests__/tsbuildinfo-outside-published-dist-9189.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +import { BUILD_OUTPUT_DIRS } from '../check-published-dist-tooling.mjs'; + +/** + * objectui#9189 — no incremental build RECORD may be derived into a published + * build output directory. + * + * ## The defect this pins shut + * + * `composite` implies `incremental`, so every composite project writes a + * `*.tsbuildinfo`. Where it writes it is DERIVED, not declared: with an + * `outDir` and no `rootDir`, TypeScript puts the record at + * `outDir/.tsbuildinfo` — i.e. inside `dist`, which + * `files: ["dist", …]` publishes whole. `pnpm check:published-dist` refuses a + * record there (objectui#7003), so the gate's verdict came to depend on WHEN it + * ran: green before anything invoked `tsc`, red after, on a byte-identical + * tree. A reader whose diff touched no tsconfig then reads "I broke the + * published surface" and goes looking in the diff for something that is not + * there. Measured on c64975e9f: 18 of the repository's 32 incremental projects + * derived their record into `dist`. + * + * ## Why the assertion is DERIVED and not a grep for the option + * + * The hazard is not a missing line; it is a location that nobody chose. Before + * this card 12 projects were already correct — but only as a side effect of + * carrying `rootDir: "src"`, which rebases the derived path back out of + * `outDir`. `packages/components/tsconfig.json` says in its own comment that + * `rootDir` was REMOVED there for an unrelated reason, and that removal is what + * moved its record into the published directory. So a test that checked for the + * presence of `tsBuildInfoFile` would pass on a project whose value still + * pointed into `dist`, and would red on a correct project that got there + * another way. Asking TypeScript where the record goes is the only question + * whose answer cannot drift from the compiler's. + * + * `BUILD_OUTPUT_DIRS` is imported from the gate rather than re-spelled, so a + * fourth output directory added over there is covered here in the same commit. + */ + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +/** + * Below this many incremental projects the enumeration is broken, not empty. + * + * The repository has 32 today. A collapse to a handful means `git ls-files` or + * the config parse stopped answering, and an empty comparison would pass while + * reading nothing — the vacuous shape `check-published-dist-tooling.mjs` calls + * out in its own header. + */ +const MIN_INCREMENTAL_PROJECTS = 25; + +/** Every tracked tsconfig, enumerated from git rather than from a hand-written list. */ +function trackedConfigs(): string[] { + return execFileSync('git', ['ls-files', '*tsconfig*.json'], { cwd: repoRoot, encoding: 'utf8' }) + .split('\n') + .filter(Boolean); +} + +/** The record path TypeScript itself would write for `configFile`, or null if it writes none. */ +function derivedRecordPath(configFile: string): string | null { + const absolute = path.join(repoRoot, configFile); + const read = ts.readConfigFile(absolute, ts.sys.readFile); + if (read.error) throw new Error(`cannot read ${configFile}`); + const parsed = ts.parseJsonConfigFileContent( + read.config, + ts.sys, + path.dirname(absolute), + undefined, + absolute, + ); + const derived = ts.getTsBuildInfoEmitOutputFilePath?.(parsed.options); + return derived ? path.relative(repoRoot, derived) : null; +} + +/** Whether a repo-relative path sits inside one of the gate's build output directories. */ +function insideBuildOutput(relativePath: string): boolean { + return relativePath.split('/').some((segment) => BUILD_OUTPUT_DIRS.includes(segment)); +} + +describe('incremental build records stay outside published build output (objectui#9189)', () => { + const projects = trackedConfigs() + .map((file) => ({ file, record: derivedRecordPath(file) })) + .filter((entry): entry is { file: string; record: string } => entry.record !== null); + + it('enumerates the incremental projects it claims to judge', () => { + expect(projects.length).toBeGreaterThanOrEqual(MIN_INCREMENTAL_PROJECTS); + }); + + it('derives no record into a build output directory', () => { + const offenders = projects + .filter((entry) => insideBuildOutput(entry.record)) + .map((entry) => `${entry.file} -> ${entry.record}`); + expect(offenders).toEqual([]); + }); + + it('can fail: dropping the pin from a real project puts its record back in dist', () => { + // The control. Without it this suite would also pass on a tree where the + // derivation silently stopped answering — the same "confident absence" the + // enumeration assertion above guards from the other side. + const absolute = path.join(repoRoot, 'packages/components/tsconfig.json'); + const read = ts.readConfigFile(absolute, ts.sys.readFile); + const parsed = ts.parseJsonConfigFileContent( + read.config, + ts.sys, + path.dirname(absolute), + undefined, + absolute, + ); + const withoutPin = { ...parsed.options, tsBuildInfoFile: undefined }; + const derived = ts.getTsBuildInfoEmitOutputFilePath?.(withoutPin); + expect(derived).toBeTruthy(); + expect(insideBuildOutput(path.relative(repoRoot, derived as string))).toBe(true); + }); +});