From 78cbf5dfbfc799bc3aaaf62fdfbdca5725486f72 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 18:29:20 +0000 Subject: [PATCH 1/2] build: patch tsup's bundled rollup-plugin-dts so one ts.Program covers every DTS entry tsup 8.5.1 bundles rollup-plugin-dts 6.1.1. Its createPrograms keys each entry by a directory, and on the tsconfig-override path tsup always takes, a config-cache hit kept the entry's own directory. So every DTS entry got its own ts.Program. That affects 12 packages here; spec's 18 entries built 18 programs. The patch keys every entry by the tsconfig's directory. Written with pnpm patch-commit; patchedDependencies lives in pnpm-workspace.yaml; the lockfile was regenerated by pnpm install. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx --- patches/tsup@8.5.1.patch | 16 ++++++++++++++++ pnpm-lock.yaml | 27 ++++++++++++++++----------- pnpm-workspace.yaml | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 patches/tsup@8.5.1.patch diff --git a/patches/tsup@8.5.1.patch b/patches/tsup@8.5.1.patch new file mode 100644 index 00000000000..d33719e72db --- /dev/null +++ b/patches/tsup@8.5.1.patch @@ -0,0 +1,16 @@ +diff --git a/dist/rollup.js b/dist/rollup.js +index e128b61b9558318b9f86dc11d72c31f09a8eb7db..35f85e39c2af6c645fb2d3da115742df445d2343 100644 +--- a/dist/rollup.js ++++ b/dist/rollup.js +@@ -4915,6 +4915,11 @@ var require_rollup_plugin_dts = _chunkTWFEYLU4js.__commonJS.call(void 0, { + } else { + logCache("HIT", cacheKey); + } ++ if (overrideConfigPath) { ++ // objectstack patch: a cache HIT kept dirname(input), so each entry ++ // got its own ts.Program. One tsconfig for all entries => one key. ++ dirName = path__namespace.dirname(path__namespace.resolve(process.cwd(), overrideConfigPath)); ++ } + const { fileNames, options, errors } = configByPath.get(cacheKey); + dtsFiles = fileNames.filter((name) => DTS_EXTENSIONS.test(name)); + if (errors.length) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf69c2837b1..9e73b448d03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,11 @@ overrides: qs@>=6.0.0 <7.0.0: ^6.16.0 devalue@<6.0.0: ^5.9.2 +patchedDependencies: + tsup@8.5.1: + hash: d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4 + path: patches/tsup@8.5.1.patch + importers: .: @@ -67,7 +72,7 @@ importers: version: 5.56.9(@typescript-eslint/types@8.67.0) tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) tsx: specifier: ^4.23.12 version: 4.23.12 @@ -384,7 +389,7 @@ importers: version: 26.2.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -406,7 +411,7 @@ importers: version: 26.2.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -428,7 +433,7 @@ importers: version: 26.2.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -918,7 +923,7 @@ importers: version: 26.2.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -1268,7 +1273,7 @@ importers: version: 4.9.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -1290,7 +1295,7 @@ importers: version: 26.2.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -1339,7 +1344,7 @@ importers: version: 8.22.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -1429,7 +1434,7 @@ importers: version: 26.2.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -2547,7 +2552,7 @@ importers: version: 26.2.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -15644,7 +15649,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0): + tsup@8.5.1(patch_hash=d1c8d6bd9db5a776a639cd37b802f8b962f7846a6e3758af4478b0e0bde082e4)(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@6.0.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9e104215b2d..b96f61f907d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,6 +18,24 @@ onlyBuiltDependencies: - msw - sharp +# Patched dependencies. Written with `pnpm patch` / `pnpm patch-commit`, never +# by hand. `patch-commit` writes this key into package.json's `pnpm` field; +# move it here, the file that holds every other pnpm setting, then run +# `pnpm install`. +# - tsup@8.5.1: its bundled rollup-plugin-dts gave every DTS entry its own +# ts.Program. tsup always passes the tsconfig path, and on that path a +# config-cache hit keyed each later entry by its own directory. The patch +# keys every entry by the tsconfig's directory, so all entries share one +# program. The same code is in rollup-plugin-dts 6.5.1. Measured on +# @objectstack/spec's 18 entries: live heap 4997 → 1405 MB. The emitted +# declarations are the same types in every affected package. +# The key names the exact version. A tsup that resolves to anything else +# leaves the patch unused, and `pnpm install` then fails +# (ERR_PNPM_UNUSED_PATCH), so an upgrade cannot drop the patch silently. +# Re-derive the patch, or retire it, on every tsup bump. +patchedDependencies: + tsup@8.5.1: patches/tsup@8.5.1.patch + # Transitive-dependency version pins. pnpm v10 reads `overrides` from THIS file # — the `pnpm.overrides` block in package.json is silently ignored — so all # pins must live here (previously orphaned in package.json: minimatch, tar). From 892f8230c0283e41159d9525b40c4e961aeec72c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 18:56:35 +0000 Subject: [PATCH 2/2] docs(spec): record the one-program DTS pass and today's measurements The spec DTS-pass docblock now describes the patched grouping, the tsup-bump duty, and today's table. Measured at the 6144 ceiling in an 8192 MB cgroup: 18 -> 1 programs, live heap 4997 -> 882 MB, 153s -> 41s. It also records the wider normalisation under which the patched and unpatched trees are equal. The 6144 ceiling is unchanged. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx --- packages/spec/tsup.config.ts | 68 ++++++++++++++++++++---------------- pnpm-workspace.yaml | 17 ++++----- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/spec/tsup.config.ts b/packages/spec/tsup.config.ts index 74106ac0ce2..0092a0e7b82 100644 --- a/packages/spec/tsup.config.ts +++ b/packages/spec/tsup.config.ts @@ -180,17 +180,23 @@ const swapServerOnlyGrammarArm: Plugin = { * it, and the pass's non-heap overhead measures ~250 MB, so the bound is * ~6.4 GB inside an 8 GB container. * - * WHY THE PASS IS THIS HEAVY: ONE `ts.Program` PER ENTRY. tsup 8.5.1 runs the - * pass through its bundled rollup-plugin-dts 6.1.1, whose `createPrograms` - * groups entries by a directory key. tsup always passes the tsconfig path, and - * on that path every entry after the first is keyed by its OWN directory (the - * same code is in rollup-plugin-dts 6.5.1). So each entry above gets its own - * program, which parses, binds and emits its whole reachable graph again: the - * peak grows with entries × graph, not with the graph. ⇒ Every entry added to - * `entries` adds a program to this pass. + * ONE `ts.Program` FOR ALL ENTRIES — held by a patch, not by tsup. tsup 8.5.1 + * runs this pass through its bundled rollup-plugin-dts 6.1.1, whose + * `createPrograms` groups entries by a directory key. tsup always passes the + * tsconfig path, and on that path every entry after the first was keyed by its + * OWN directory (the same code is in rollup-plugin-dts 6.5.1). So each entry + * above got its own program, which parsed, bound and emitted its whole + * reachable graph again: the peak grew with entries × graph, not with the + * graph. `patches/tsup@8.5.1.patch` (wired in `pnpm-workspace.yaml`'s + * `patchedDependencies`) keys every entry by the tsconfig's directory, so the + * 18 entries share one program. The patch names the exact tsup version, and + * `pnpm install` refuses a patch that matches no installed package, so a tsup + * bump cannot drop it silently. ⇒ On a tsup bump, re-derive the patch or + * retire it, then re-measure this table. If this pass ever shows 18 programs + * again, the entries are back to one program each. * - * `noCheck`: rollup-plugin-dts forces `noEmitOnError`, which made every one of - * those programs also semantically CHECK each file it emitted — a type check + * `noCheck`: rollup-plugin-dts forces `noEmitOnError`, which makes the program + * also semantically CHECK each file it emits — a type check * the `typecheck` script (`tsc --noEmit` over this same tsconfig, run by the * required `TypeScript Type Check` job) already performs once. `noCheck` drops * only that duplicate. Syntactic, option, global and declaration diagnostics @@ -198,37 +204,39 @@ const swapServerOnlyGrammarArm: Plugin = { * build; a plain type error in `src/` is `typecheck`'s to report, not this * pass's. * - * Measured on 8cdbe0c6e5's source, DTS pass alone at the 6144 ceiling, inside a + * Measured on 8113763026's source, DTS pass alone at the 6144 ceiling, inside a * cgroup capped at 8192 MB. Live heap is the largest heap V8 kept after a * mark-compact (`--trace-gc`); peak RSS is the peak anonymous RSS of the whole - * process tree: + * process tree; wall times are shared-box readings: * - * pass live heap peak RSS wall - * duplicate check on (before) 5658 MB 6177 MB 181-194s - * noCheck (this config) 5083 MB 5889 MB 134s - * one program, grouping patched 1379 MB 3749 MB 53s - * `tsc --noEmit`, whole package 1103 MB 1149 MB 18s + * pass (both with noCheck) programs live heap peak RSS wall + * one program per entry (unpatched) 18 4997 MB 5694 MB 153s + * one program (patched, this tree) 1 882 MB 3537 MB 41s * - * The third row was measured with the grouping patched in a copy of tsup - * outside this tree. It shows what cutting the program count is worth. + * The patched run did only 4 mark-compacts, so its live-heap figure is a + * lower bound; its largest heap before a mark-compact was 1677 MB. The whole + * `build` script went 189s → 87s. For scale, `tsc --noEmit` over the whole + * package peaked at 1103 MB live on 8cdbe0c6e5. * * NOT BYTE-STABLE: this pass does not emit the same bytes twice. TypeScript * prints union members, and the members of object types derived from them, in - * type-creation order, and that order follows emit order. Three runs of the - * same commit gave three different trees, and rollup's content-hashed chunk - * names moved with them. Once union and property-signature order are - * normalised, all three runs and the `noCheck` run are the same tree (128 - * files, 30389539 bytes). ⇒ Compare two declaration trees in such an - * order-insensitive form, never by a byte digest. + * type-creation order, and that order follows emit order. Three runs of one + * commit gave three different trees, and rollup's content-hashed chunk names + * moved with them. The one-program pass also reorders top-level statements + * and `import`/`export` specifiers, and names one shared chunk after a + * different module (`data-engine` → `analytics.zod`). Normalise all of those + * (sort union and property-only type-literal members, statements and + * specifiers; strip chunk hashes; pair chunks by content), and the unpatched + * and patched trees are the same tree: 130 files, 0 differing. ⇒ Compare two + * declaration trees in that order-insensitive form, never by a byte digest. * * If this pass starts failing with `ERR_WORKER_OUT_OF_MEMORY`, the live type * graph has outgrown 6144 — that is a loud, actionable failure and the point of * the ceiling. ⛔ Do not "fix" it by raising the number past what the build - * container has; that trades this error back for the silent exit 137. Cut the - * program count, shrink the graph, or split the pass across entries. Cutting - * the count moves statement order and one chunk name beyond the noise above. A - * split redraws the shared chunks. Either one changes what publishes, so it - * needs that reviewed first. + * container has; that trades this error back for the silent exit 137. First + * check that the pass still builds ONE program (above). Then shrink the graph, + * or split the pass across entries. A split redraws the shared chunks and + * changes what publishes, so it needs that reviewed first. */ const isDts = process.env.BUILD_DTS === 'true'; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b96f61f907d..66397a353e1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,19 +20,20 @@ onlyBuiltDependencies: # Patched dependencies. Written with `pnpm patch` / `pnpm patch-commit`, never # by hand. `patch-commit` writes this key into package.json's `pnpm` field; -# move it here, the file that holds every other pnpm setting, then run +# move it here, beside `overrides` and `onlyBuiltDependencies`, then run # `pnpm install`. # - tsup@8.5.1: its bundled rollup-plugin-dts gave every DTS entry its own # ts.Program. tsup always passes the tsconfig path, and on that path a # config-cache hit keyed each later entry by its own directory. The patch # keys every entry by the tsconfig's directory, so all entries share one -# program. The same code is in rollup-plugin-dts 6.5.1. Measured on -# @objectstack/spec's 18 entries: live heap 4997 → 1405 MB. The emitted -# declarations are the same types in every affected package. -# The key names the exact version. A tsup that resolves to anything else -# leaves the patch unused, and `pnpm install` then fails -# (ERR_PNPM_UNUSED_PATCH), so an upgrade cannot drop the patch silently. -# Re-derive the patch, or retire it, on every tsup bump. +# program. The same code is in rollup-plugin-dts 6.5.1. Twelve packages +# here have more than one DTS entry; @objectstack/spec had 18 programs +# and now has one, and its DTS pass went from 4997 MB to 882 MB of live +# heap. Every one of the twelve emits the same types as before. +# The key names the exact version. When the installed tsup is any other +# version, the patch goes unused and `pnpm install` fails +# (ERR_PNPM_UNUSED_PATCH), so a tsup upgrade cannot drop the patch +# silently. Re-derive the patch, or retire it, on every tsup bump. patchedDependencies: tsup@8.5.1: patches/tsup@8.5.1.patch