Repository navigation
Commit 2168447
fix(build): emit spec .d.ts in program order so two builds of one sha match byte for byte (#30131)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB9ASXAhqQeMSZdJgT2FgPH5)_
Part of #30061 (the seat closes the card on landing; a
`claude/project-thread-*` branch cannot satisfy
`check:closing-target-claim`).
## 维护者速读
- 改了什么:tsup 补丁里的 rollup-plugin-dts 改为在 rollup 读取任何模块之前,按 ts.Program
自身的文件顺序把每个源文件的 .d.ts 一次性生成好;另加一个 pin 测试。spec 源码与 schema 一行未动。
- 为什么改:同一份 main 源码构建两次,spec 的 134 个 .d.ts 中有 18 个字节不同(含 chunk
文件名),拆分大文件的 PR 因此只能两两配对读声明证据。根因是生成顺序跟着 rollup 的异步加载顺序走,每次都不一样。
- 风险与回滚:补丁作用于仓内所有走 tsup DTS 的包;每个文件仍只生成一次,spec 的 DTS 耗时 44s 不变。修复后的产物与
main 已经会产出的两种状态之一逐字节相同。回滚 = revert 本 PR(补丁、锁文件哈希一起回)。
- 先看:`patches/tsup@8.5.1.patch`(新增的 emitInProgramOrder 与 transform
钩子里的读取);`packages/spec/scripts/dts-emit-order.pin.test.ts`;`packages/spec/tsup.config.ts`
中改写的文档块。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run --project local
scripts/dts-emit-order.pin.test.ts`,预期 2 个测试通过,约 5 秒。
- 需要你做的:无,席位落地。
Clause-②: no
Metadata-protocol: none
## What changes
**The cause.** `@objectstack/spec`'s DTS pass runs every entry through
one `ts.Program` (the existing tsup patch). Its checker creates each
type once, on first use, and TypeScript prints union members, and the
object members derived from them, in type-creation order. tsup's bundled
rollup-plugin-dts emitted each file's declarations inside rollup's
`transform` hook, so whichever file rollup transformed first created the
shared literal types and fixed the member order of every later file.
Rollup transforms in the order its async `fs.promises.readFile` loads
finish, which changes from run to run. Measured: in two DTS passes of
one tree the transform order differed at 389 of 445 positions, starting
with the first module.
The parallel-build suspect does not hold. tsup runs the DTS pass as ONE
rollup build in one worker, transforms each module once, and writes the
`esm` and `cjs` outputs from that one module graph. The `.d.ts` and
`.d.mts` twins carry the same member order within a run. The order moves
between runs, not between the formats.
**The fix.** `patches/tsup@8.5.1.patch` (regenerated with `pnpm patch` /
`pnpm patch-commit`; the lockfile's `patch_hash` moves with it) adds
`emitInProgramOrder(program)`: on the first transform, it emits every
non-declaration, non-external source file of the program once, in
`program.getSourceFiles()` order, and stores each file's text and
diagnostics. The transform hook then reads the stored text instead of
emitting. Each file is still emitted exactly once, and the
`noEmitOnError` error path is unchanged. The first hunk, which gives
every entry one program, is unchanged.
**Docs.** `packages/spec/tsup.config.ts`'s "NOT BYTE-STABLE" paragraph
is rewritten: one tree now gives one set of bytes, but the order is the
program's, so comparing two DIFFERENT sources still needs the
order-insensitive reading. `pnpm-workspace.yaml`'s note on the tsup
patch gains the second change.
**The pin.** `packages/spec/scripts/dts-emit-order.pin.test.ts` (local
project, ~4 s) builds a two-entry fixture through the real tsup CLI
twice. A preload delays rollup's read of `a.ts` in one run and `b.ts` in
the other, so the two runs transform the entries in opposite orders, and
the test requires byte-equal output (`.d.ts` and `.d.mts`). Two
controls: the fixture is order-sensitive under this TypeScript (a hand
emit in the two orders gives `"zeta" | "alpha" | "mid"` and `"mid" |
"alpha" | "zeta"`), and each run's read log shows the forced order
reached rollup.
Why a fixture and not two full builds in CI: one spec DTS pass is ~45 s
and ~3.5 GB RSS, and of four unforced builds of `main`, three agreed
byte for byte. Two full builds would double the cost and still miss a
regression most of the time. The forced order fails on every run.
Its radius, measured: a commit changing only `patches/tsup@8.5.1.patch`
and `pnpm-lock.yaml` makes `turbo ls --affected` report 131 packages,
`@objectstack/spec` among them, and the root external-dependency hash is
in turbo's global cache inputs. So a later tsup patch change re-runs
this pin.
**Changeset: `skip-changeset`.** Nothing published changes. The post-fix
spec declaration tree is byte-identical, 134/134, to one of the two
trees `main` already builds (`base1` below). `tsup.config.ts`, the test,
`patches/` and the lockfile are not in any `files[]`.
## Tests
- Pin: 2/2 pass. **Ablation:** with the installed `rollup.js` swapped
for the pre-fix one (one-program hunk only), the byte-equality test
fails (`expected { …(4) } to deeply equal { …(4) }`) and the
order-sensitivity control still passes. Restored, sha256 checked equal
to the fixed file.
- `pnpm --filter @objectstack/spec test`: exit 0, 812/812 files, 24397
passed, 1 todo.
- `NODE_OPTIONS=--max-old-space-size=6144 pnpm --filter
@objectstack/spec typecheck`: exit 0.
- `eslint --no-inline-config` on the two touched TypeScript files: exit
0.
## Acceptance notes
- The patch serves every package that builds declarations with tsup here
(twelve have more than one DTS entry). Their declaration member order
now follows their program order too. Equivalent types, possibly a
different one of their existing run-to-run orders on their next release.
NOT MEASURED per package: reason, outside this card's spec measurement;
no published file of theirs changes in this PR.
- File surface: the claim named the spec d.ts build configuration under
`packages/spec`. The rollup-plugin-dts behaviour lives in the root tsup
patch, so the diff also touches `patches/tsup@8.5.1.patch`,
`pnpm-lock.yaml` (the patch hash) and `pnpm-workspace.yaml` (comment
only).
- A move-only split now reads from ONE base/head pair. Measured on
#29993's split: the tree with `d79eecfe`'s `packages/spec/src` change
reverted and the current tree gave 134/134 byte-identical declarations
under the fix.
- The `--dts` check in `scripts/codemod-split-*.mjs` is untouched
(#30105 is in flight there).
## Verification
<details><summary>Spec builds, before and after (raw sha256 per
declaration file)</summary>
All on the tree of `1f13eb31` (current `main`), `rm -rf dist` then `pnpm
build` per run; per-file sha256 lists in the project files under
`issue-30061/`.
| Pair | Differing of 134 |
| --- | --- |
| base1 vs base2 (before) | 18 |
| two DTS-only passes before the fix (o1, o2) | 0, both equal to base1 |
| head1 vs head2 (after) | 0 |
| head1 vs base1 | 0 |
| head1 vs base2 | 18 |
| three DTS-only passes after the fix (f1, f2, f3) | 0, while the
transform order differed in each |
The 18 differing files before the fix: `ai`, `api`, `automation`,
`contracts`, `kernel`, `marketplace`, `system` (`index.d.ts` and
`.d.mts` each), and the `automation-api.zod-HASH` and
`node-executor.zod-HASH` chunks (both formats, renamed between runs).
Surface reading (`dtsnorm.mjs` from `issue-29987/`, base2 vs head1):
same file set, 116 raw-equal, 18 differ only in union order, surface
digest EQUAL in all 18. The fix moves no published surface; it picks one
of `main`'s existing orders every time.
DTS pass wall time: 44-47 s before, 43-47 s after (shared box).
</details>
<details><summary>Derived gates</summary>
All on head `6d4463b4`. `node scripts/pm/dispatch-gates.mjs --commands`
derived 66 families for this change set (the lockfile pulls in the wide
set); each was run and its exit code recorded before any pipe
(`issue-30061/gates-ran.list` in the project files).
- 63 exit 0, among them `check:cross-package-test-inputs`,
`check:test-source-alias`, `check:nul-bytes`, `check:published-files`,
`check:dts-closure` (38/38 declared declaration files present),
`check:type-check-coverage`, `check:type-check-debt`,
`check:ci-filter-parity`, `check:pm-governed-merges` and spec's
`check:liveness`, `check:llms-txt`, `check:strictness-ledger`,
`check:variant-docs`, `check:empty-state`.
- 3 NOT MEASURED, exit 3 (the gate refused its own prerequisite):
`check-plugin-teardown-shape.mjs --self-test` (shallow clone, wants `git
fetch --unshallow`), `check:dual-build-cjs-loads` and
`check:lean-entry-closure` (both want the whole workspace built; only
`packages/spec` was). CI runs all three.
- `dispatch-gates --ran`: exit 0, 66 derived, 63 run, 3 NOT-MEASURED, 0
UNRUN.
- `check-dev-prereqs` (local-only family): NOT MEASURED, exit 1 because
35 other workspace packages are unbuilt in this container; spec's own
build stamped its prerequisites (`check-dev-prereqs --stamp` inside
`pnpm build`, exit 0).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzeza8Qg2GQUtJQtxRW)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent a9fb14b commit 2168447
5 files changed
Lines changed: 295 additions & 27 deletions
File tree
- packages/spec
- scripts
- patches
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
441 | 441 | | |
442 | 442 | | |
443 | 443 | | |
444 | | - | |
445 | | - | |
446 | | - | |
447 | | - | |
448 | | - | |
449 | | - | |
450 | | - | |
451 | | - | |
452 | | - | |
453 | | - | |
454 | | - | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
455 | 466 | | |
456 | 467 | | |
457 | 468 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
0 commit comments