Skip to content

Commit 00f79c9

Browse files
claude[bot]claude
andauthored
docs(check-dts-emitted): name the silent worker-death class, not OOM (#13898)
The guard's header and its printed remedy both named OOM as the shape that leaves a build silently DTS-less. Measured, an OOM'd DTS worker is the LOUD path: Node delivers worker heap exhaustion as an 'error' event (ERR_WORKER_OUT_OF_MEMORY), and an 'error' event with no listener is rethrown by EventEmitter, so the run exits non-zero and prints a stack. The silent shape is a worker that ends without posting a message AND without an 'error' event, from a CJS caller. Reworded both places to lead with that class, to keep OOM present as the loud sibling a reader must be told it is NOT, and to replace the heap-headroom retry with a next action that discriminates between the two causes this guard can fire on. Text only: comments and one console.error string. The predicate, --self-test and rollout are untouched. Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC Co-authored-by: Claude <noreply@anthropic.com>
1 parent dd3ea16 commit 00f79c9

1 file changed

Lines changed: 53 additions & 12 deletions

File tree

scripts/check-dts-emitted.mjs

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,41 @@
2525
// // <- no worker.on('error'), no worker.on('exit')
2626
// });
2727
//
28-
// There is no `error` handler and no `exit` handler. If the worker dies without
29-
// posting a message -- OOM under memory pressure, a hard `process.exit`, a
30-
// terminated thread -- neither branch ever runs, the promise NEVER SETTLES, the
31-
// event loop drains, and **Node exits 0**. tsup's `Promise.all([dtsTask(),
32-
// mainTasks()])` never resolves either, so nothing prints and nothing throws:
33-
// the JS pass has already written `dist/`, so the run leaves a dist with
34-
// `index.js` / `index.mjs` / maps and ZERO `.d.ts` files, and reports success.
28+
// There is no `error` handler and no `exit` handler. What that leaves silent is
29+
// a CLASS, not any one trigger: a worker that ends WITHOUT POSTING A MESSAGE and
30+
// without emitting an `error` event -- a hard `process.exit` inside the worker,
31+
// a terminated thread, any other message-less end -- runs neither branch, so the
32+
// promise NEVER SETTLES, the event loop drains, and **Node exits 0**. tsup's
33+
// `Promise.all([dtsTask(), mainTasks()])` never resolves either, so nothing
34+
// prints and nothing throws: the JS pass has already written `dist/`, so the run
35+
// leaves a dist with `index.js` / `index.mjs` / maps and ZERO `.d.ts` files, and
36+
// reports success.
37+
//
38+
// !! OOM IS THE LOUD SIBLING -- IT IS NOT THIS SHAPE. If you got here after a
39+
// memory-starved build, you are looking at a different failure. Node delivers
40+
// worker heap exhaustion as an `error` event (`ERR_WORKER_OUT_OF_MEMORY`), and
41+
// an `error` event with no listener is rethrown by EventEmitter -- so an OOM'd
42+
// DTS pass exits NON-ZERO and prints a stack. A non-zero build is never cached
43+
// by turbo, which is precisely the property this section used to say OOM lacked.
44+
// Measured on Node v22.22.2 / linux x64, against a harness carrying the promise
45+
// shape above (only `message` registered):
46+
//
47+
// worker death mode event process outcome
48+
// ---------------------------------------------------------------------------
49+
// `process.exit()`, CJS caller exit exit 0, ZERO output
50+
// `process.exit()`, ESM caller (top-level exit exit 13 + "unsettled
51+
// await) top-level await" warning
52+
// heap OOM, per-worker `resourceLimits` error exit 1
53+
// heap OOM, process-wide max-old-space error exit 1
54+
//
55+
// Only the first row is the defect this guard closes. The caller's module system
56+
// is load-bearing: the SAME message-less death exits 13 with a warning under an
57+
// ESM top-level `await` and 0 in silence under CJS. tsup's CLI is CJS, which is
58+
// why the shape that was observed was a silent one.
59+
//
60+
// What actually killed the worker in the original observation is NOT established
61+
// here -- only that it ended without an `error` event, because that run exited 0,
62+
// and therefore that memory pressure is not a demonstrated cause of this shape.
3563
//
3664
// That is a correctness problem for the build CACHE, not just for one build.
3765
// `OS_SKIP_DTS` is declared in turbo.json `globalEnv` and it works: measured on
@@ -165,14 +193,27 @@ function run(dir) {
165193
` '${manifest.name ?? ''}'\" - and it reads as though THEIR change broke this package.\n` +
166194
'\n Most likely cause (#11907): tsup runs DTS generation in a worker thread and\n' +
167195
" settles its promise only on the worker's `message` events - it registers no\n" +
168-
' `error` and no `exit` handler. A worker that dies (OOM under memory pressure\n' +
169-
' is the observed one) posts neither "success" nor "error", so the promise never\n' +
170-
' settles, the event loop drains, and node exits 0 with the JS already written.\n' +
196+
' `error` and no `exit` handler. A worker that ends without posting a message\n' +
197+
' AND without an `error` event - a hard `process.exit`, a terminated thread -\n' +
198+
' settles neither branch, so the promise never settles, the event loop drains,\n' +
199+
' and node exits 0 with the JS pass already written.\n' +
200+
'\n It is NOT an out-of-memory build, and more heap will not help. Node delivers\n' +
201+
' worker heap exhaustion as an `error` event (ERR_WORKER_OUT_OF_MEMORY) which,\n' +
202+
' with no listener registered, is rethrown - so an OOM DTS pass exits NON-ZERO\n' +
203+
' and prints a stack, the build stops at `tsup`, and this guard never runs.\n' +
204+
' Reaching this text is itself evidence the DTS pass exited 0.\n' +
171205
'\n This guard is what stops that exit 0 from becoming a CACHED artifact: turbo\n' +
172206
' caches only successful tasks, and a skip-DTS run already hashes differently,\n' +
173207
' so failing here keeps a DTS-less dist from ever being served as a full build.\n' +
174-
'\n Re-run the build. If it keeps failing here, build with more headroom:\n' +
175-
' NODE_OPTIONS=--max-old-space-size=8192 pnpm --filter <pkg> build\n' +
208+
'\n WHICH SHAPE ARE YOU IN? Count the declarations actually on disk:\n' +
209+
' ls dist/*.d.ts dist/*.d.mts dist/*.d.cts 2>/dev/null | wc -l\n' +
210+
'\n 0 - the DTS pass produced nothing, so read what it printed:\n' +
211+
' pnpm --filter <pkg> build 2>&1 | grep -i dts\n' +
212+
' A "DTS Build start" with no success and no error line after it is the\n' +
213+
' message-less death above.\n' +
214+
'\n 1 or more - nothing died. package.json promises a declaration path that this\n' +
215+
" package's tsup entries never emit, so every rebuild fails here the same way;\n" +
216+
' line the `types` conditions up with the entry points that are really built.\n' +
176217
'\n If a poisoned entry was cached before this guard existed, a plain rebuild is a\n' +
177218
' cache HIT that restores it - clear it with:\n' +
178219
' pnpm exec turbo run build --filter <pkg> --force\n',

0 commit comments

Comments
 (0)