|
25 | 25 | // // <- no worker.on('error'), no worker.on('exit') |
26 | 26 | // }); |
27 | 27 | // |
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. |
35 | 63 | // |
36 | 64 | // That is a correctness problem for the build CACHE, not just for one build. |
37 | 65 | // `OS_SKIP_DTS` is declared in turbo.json `globalEnv` and it works: measured on |
@@ -165,14 +193,27 @@ function run(dir) { |
165 | 193 | ` '${manifest.name ?? ''}'\" - and it reads as though THEIR change broke this package.\n` + |
166 | 194 | '\n Most likely cause (#11907): tsup runs DTS generation in a worker thread and\n' + |
167 | 195 | " 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' + |
171 | 205 | '\n This guard is what stops that exit 0 from becoming a CACHED artifact: turbo\n' + |
172 | 206 | ' caches only successful tasks, and a skip-DTS run already hashes differently,\n' + |
173 | 207 | ' 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' + |
176 | 217 | '\n If a poisoned entry was cached before this guard existed, a plain rebuild is a\n' + |
177 | 218 | ' cache HIT that restores it - clear it with:\n' + |
178 | 219 | ' pnpm exec turbo run build --filter <pkg> --force\n', |
|
0 commit comments