Filed out of #12078 (the rollout of scripts/check-dts-emitted.mjs across every tsup-built package), on the dispatching PM's ruling that patching a third-party dependency is a maintainer decision and not an implementer's. Nothing here is a patch; this card asks for a decision to report the defect upstream, and carries the reproduction so the report can be written from it.
The defect, in tsup's own code
tsup 8.5.1 runs DTS generation in a worker_threads.Worker (node_modules/tsup/dist/index.js, the dtsTask inside Promise.all([dtsTask(), mainTasks()])) and settles the build promise only from that worker's message events:
awaitnewPromise((resolve,reject)=>{constworker=newWorker(path.join(__dirname,'./rollup.js'));worker.postMessage({ ... });worker.on('message',(data)=>{if(data==='error'){terminateWorker();reject(...);}elseif(data==='success'){terminateWorker();resolve();}
...
});// no worker.on('error'), no worker.on('exit')});There is no error handler and no exit handler. A worker that dies without posting a message — OOM under memory pressure is the observed shape — settles neither branch: the promise never settles, the event loop drains, and node exits 0 with the esbuild pass having already written dist/. The run leaves a dist/ holding index.js / index.mjs / maps and zero.d.ts, and reports success.
Why it matters to us specifically
turbo caches only successful tasks. Exit 0 on a run with OS_SKIP_DTS unset hashes as — because it is — an ordinary full build, so the declaration-less dist/** is cached under the ordinary hash and replayed on every later run; a plain rebuild is a cache HIT that restores it, and only --force replaces the entry. The symptom lands on someone else's diff, as TS7016: Could not find a declaration file for module '...'.
Reproduced end to end on @objectstack/plugin-auth in #11907 / PR #12076: build exit 0, turbo 26 successful, 26 total, dist/ with zero declarations, cached under the same hash a healthy build produces.
The upstream fix being proposed for report
Register both missing handlers on the worker, so a death that posts no message rejects the build promise instead of leaving it pending:
worker.on('error', reject) — the worker threw or failed to start.worker.on('exit', code) — reject when the promise has not already settled, whatever the code, since a clean exit without a success message is still a DTS pass that produced nothing.
Either one alone closes the observed OOM shape; the pair closes the class.
What we already have, and why this is a complement
PR #12076 landed scripts/check-dts-emitted.mjs (asserts every declaration path the manifest promises is present and non-empty, no-op under OS_SKIP_DTS), and #12078 rolls it into all 67 tsup-built packages' build scripts. That protects this repo now, on our schedule. An upstream fix ends the class for everyone and lets the guard become redundancy rather than the only barrier. They are complements, not alternatives — this card does not propose removing the guard.
Decision asked for
Whether to open the upstream report (tsup issue / PR), and by whom. ⛔ Note what is explicitly not proposed here: carrying a patched tsup in this repo. A patched third-party dependency is a standing supply-chain and maintenance obligation, and that call belongs to the maintainer.
Filed out of #12078 (the rollout of
scripts/check-dts-emitted.mjsacross every tsup-built package), on the dispatching PM's ruling that patching a third-party dependency is a maintainer decision and not an implementer's. Nothing here is a patch; this card asks for a decision to report the defect upstream, and carries the reproduction so the report can be written from it.The defect, in tsup's own code
tsup 8.5.1 runs DTS generation in a
worker_threads.Worker(node_modules/tsup/dist/index.js, thedtsTaskinsidePromise.all([dtsTask(), mainTasks()])) and settles the build promise only from that worker'smessageevents:There is no
errorhandler and noexithandler. A worker that dies without posting a message — OOM under memory pressure is the observed shape — settles neither branch: the promise never settles, the event loop drains, and node exits 0 with the esbuild pass having already writtendist/. The run leaves adist/holdingindex.js/index.mjs/ maps and zero.d.ts, and reports success.Why it matters to us specifically
turbo caches only successful tasks. Exit 0 on a run with
OS_SKIP_DTSunset hashes as — because it is — an ordinary full build, so the declaration-lessdist/**is cached under the ordinary hash and replayed on every later run; a plain rebuild is a cache HIT that restores it, and only--forcereplaces the entry. The symptom lands on someone else's diff, asTS7016: Could not find a declaration file for module '...'.Reproduced end to end on
@objectstack/plugin-authin #11907 / PR #12076: build exit 0, turbo26 successful, 26 total,dist/with zero declarations, cached under the same hash a healthy build produces.The upstream fix being proposed for report
Register both missing handlers on the worker, so a death that posts no message rejects the build promise instead of leaving it pending:
worker.on('error', reject)— the worker threw or failed to start.worker.on('exit', code)— reject when the promise has not already settled, whatever the code, since a clean exit without asuccessmessage is still a DTS pass that produced nothing.Either one alone closes the observed OOM shape; the pair closes the class.
What we already have, and why this is a complement
PR #12076 landed
scripts/check-dts-emitted.mjs(asserts every declaration path the manifest promises is present and non-empty, no-op underOS_SKIP_DTS), and #12078 rolls it into all 67 tsup-built packages' build scripts. That protects this repo now, on our schedule. An upstream fix ends the class for everyone and lets the guard become redundancy rather than the only barrier. They are complements, not alternatives — this card does not propose removing the guard.Decision asked for
Whether to open the upstream report (tsup issue / PR), and by whom. ⛔ Note what is explicitly not proposed here: carrying a patched tsup in this repo. A patched third-party dependency is a standing supply-chain and maintenance obligation, and that call belongs to the maintainer.