Repository navigation
Report the tsup DTS-worker silent death upstream — a worker.on('error') / worker.on('exit') pair would end the class at its source #12167
Description
Activity
Maintainer ruling recorded — A: open the upstream report; a seat drafts, the maintainer reviews and submits
Provenance: maintainer, 2026-08-26, live PM chat (decision-inbox batch 3, session
session_01JANH3y7qe3MD8aLaLXci8N), verbatim: 「其他接受你的建议」 — accepting the presented recommendation A.- Deliverable: the complete upstream issue text for tsup — the reproduction from this card (worker dies message-less ⇒ neither promise branch settles ⇒ node exits 0 with a declaration-less
dist/, cached by turbo under the healthy hash), plus the two-line suggested fix (worker.on('error', reject)+worker.on('exit', …)rejecting an unsettled promise). Posted on THIS card for the maintainer to review and submit under the maintainer's own name (外部署名属维护者). - ⛔ C (carrying a patched tsup in-repo) stays rejected — no standing supply-chain obligation.
- The in-repo guard (fix(build): fail the build when tsup emits no declarations, so a DTS-less dist cannot be cached as a full build #12076/Every tsup-built package can report build success having emitted zero declarations — #11907's repair is wired into plugin-auth only, ~50 packages still exposed #12078) stays as-is; the report makes it redundancy, not the only barrier.
State:
needs-user-decision→pm:queue(domain:devxstands — drafting is a small, mechanical dispatch).
Generated by Claude Code
- Deliverable: the complete upstream issue text for tsup — the reproduction from this card (worker dies message-less ⇒ neither promise branch settles ⇒ node exits 0 with a declaration-less
os-project-manager commented
on Aug 30, 2026 CollaboratorMore actionsDraft upstream report for tsup — awaiting maintainer review and submission
Per the ruling on this card (A: a seat drafts, the maintainer submits under their own name), below is the complete, copy-pasteable text for the upstream tsup issue. Nothing has been posted to any repository outside this one. No ObjectStack issue numbers, links or jargon appear in the draft.
Verification done before drafting (all measured today, not recalled)
Check Result tsup version this repo resolves pnpm-lock.yamlonorigin/main: tsup@8.5.1 (rootpackage.json:^8.5.1)Latest tsup on npm 8.5.1 — dist-tags.latest, published 2025-11-12. The pin is already the newest releaseInstalled tsup/dist/index.jsworker.on("message", ...)only — noerror, noexithandler. Defect presentUpstream egoist/tsup@main,src/index.ts(fetched raw today)same shape, lines ~214-254 — not fixed upstream End-to-end repro on a clean standalone tsup 8.5.1 install exit 0, empty stderr, dist/withindex.jsand zero.d.tsProposed two-line patch applied to that install killed-worker build now exit 1; healthy build still exit 0 with declarations Premise stands. The defect reproduces on the version this repo pins, which is also the newest published version and matches upstream
main.One correction to this card's text, carried into the draft
The card (and the header comment in the in-repo guard) names OOM as the observed shape of the silent death. Measured on Node v22.22.2, that attribution is wrong, in the direction that matters:
- A worker that exits without posting a message (thread called
process.exit, was terminated, died message-less) fires only'exit'. Nothing listens. Promise pending, event loop drains, exit 0, silent. This is the silent-success defect. - A worker that runs out of memory fires
'error'withERR_WORKER_OUT_OF_MEMORY. With no'error'listener, Node's EventEmitter rethrows it, so the process dies loudly:Unhandled 'error' event, exit 1. Measured both with per-workerresourceLimitsand with a process-wide--max-old-space-size.
So OOM does not produce the exit-0 shape on Node 22. Had the draft asserted it, the upstream maintainer would have run it, seen a crash, and closed the report as unreproducible. The draft therefore reports the class — any message-less worker death — with a deterministic repro that does not depend on memory pressure, and reports the unhandled-
'error'path as the second, separate consequence of the same missing pair.Both are still fixed by exactly the two lines this card proposes.
Duplicate check
One targeted search of
egoist/tsupissues found no report of this silent-success behaviour. The nearest neighbours are OOM crash reports (egoist/tsup#920ERR_WORKER_OUT_OF_MEMORY,egoist/tsup#1325dts OOM) — the loud path, a different symptom. The draft references them as related, not duplicates. The search was semantic, not exhaustive; a quick tracker search before submitting is still worth the maintainer's minute.
BEGIN upstream text (copy from here)
Title:
dtsbuild: a worker that dies without posting a message leaves the build promise pending — tsup exits 0 having emitted no declarationsBody:
Summary
In
dtsTask(src/index.ts), the DTS worker's promise is settled only fromworker.on('message', ...). There is no'error'handler and no'exit'handler.If the worker thread ends without posting a message, neither branch ever runs. The promise stays pending forever,
Promise.all([dtsTask(), mainTasks()])never settles, nothing throws, nothing prints — the event loop simply drains and tsup exits 0. The esbuild pass has already writtendist/by then, so the run leaves adist/containingindex.jsand zero.d.tsfiles, and reports success.Silent success is materially worse than a crash here. Build caches (turbo, nx, CI artifact caches) key on exit status, so the declaration-less
dist/is cached under the same hash a healthy build produces, and replayed on every later run — a plain rebuild is a cache hit that restores the bad artifact. The failure then surfaces somewhere else entirely, in an unrelated consumer, asTS7016: Could not find a declaration file for module '...', which reads as that consumer's fault.Reproduction
Deterministic, about 30 seconds, no memory pressure needed:
mkdir tsup-dts-worker-repro && cd tsup-dts-worker-repro npm init -y >/dev/null npm i -D tsup@8.5.1 typescript@5.9.3 mkdir src printf 'export interface Greeting { readonly to: string }\nexport const greet = (g: Greeting): string => `hello ${g.to}`;\n' > src/index.ts printf '{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "declaration": true, "skipLibCheck": true } }\n' > tsconfig.json # A preload that makes the DTS worker die without posting a message. # --require is inherited by worker threads through execArgv, so this runs # inside the DTS worker; the main thread is left untouched. printf "const { isMainThread } = require('node:worker_threads');\nif (!isMainThread) process.exit(0);\n" > kill-dts-worker.cjs # 1) healthy build, for comparison npx tsup src/index.ts --dts --format esm; echo "exit=$?"; ls dist # 2) identical build, DTS worker dies message-less rm -rf dist node --require ./kill-dts-worker.cjs ./node_modules/tsup/dist/cli-default.js \ src/index.ts --dts --format esm; echo "exit=$?"; ls dist
The preload stands in for any real cause of a message-less worker death — a
process.exit()reached anywhere inside the worker's dependency graph, a terminated thread, a thread killed without an error event. The bug is not in what killed the worker; it is that tsup has no handler for the worker ending at all.Observed
Run 1 (healthy):
ESM dist/index.js 72.00 B ESM ⚡️ Build success in 28ms DTS Build start DTS ⚡️ Build success in 1185ms DTS dist/index.d.ts 128.00 B exit=0 dist: index.d.ts index.jsRun 2 (worker dies message-less):
CLI Building entry: src/index.ts CLI Using tsconfig: tsconfig.json CLI tsup v8.5.1 CLI Target: es2022 ESM Build start ESM dist/index.js 72.00 B ESM ⚡️ Build success in 36ms exit=0 dist: index.jsOutput simply stops.
DTS Build startnever prints, stderr is empty (0 bytes), the exit code is 0, anddist/has no declarations.Expected
A DTS pass that produced nothing should fail the build: non-zero exit with a message naming the DTS worker.
Cause
src/index.ts, insidedtsTask(the worker block is around lines 214-254 onmaintoday):const worker = new Worker(path.join(__dirname, './rollup.js')) const terminateWorker = () => { if (options.watch) return worker.terminate() } worker.postMessage({ /* ... */ }) worker.on('message', (data) => { if (data === 'error') { terminateWorker() reject(new Error('error occurred in dts build')) } else if (data === 'success') { terminateWorker() resolve() } else { /* log forwarding */ } }) // no worker.on('error'), no worker.on('exit')
Two Node-level worker outcomes are unhandled, and they fail differently:
- The worker exits without posting a message. Only
'exit'fires; nothing listens; the promise stays pending. Measured: exit 0, no output at all — the silent case above. - The worker emits
'error'(it threw, failed to start, or hitERR_WORKER_OUT_OF_MEMORY). With no'error'listener,EventEmitterrethrows on the main thread, so the process dies withUnhandled 'error' eventand an internal Node stack rather than tsup's own build-failure path, andterminateWorker()never runs. Measured on Node v22.22.2:
node:events:497 throw er; // Unhandled 'error' event ^ Error [ERR_WORKER_OUT_OF_MEMORY]: Worker terminated due to reaching memory limit: JS heap out of memory at [kOnExit] (node:internal/worker:316:26)Case 2 at least exits non-zero; case 1 is the one that poisons build caches.
Suggested fix
Register the two missing handlers on the worker:
worker.on('message', (data) => { // ...unchanged... }) + worker.on('error', reject) + worker.on('exit', (code) => { + reject( + new Error(`dts build worker exited with code ${code} without reporting a result`), + ) + })Either handler alone closes the observed case; the pair closes the class.
On the success path
terminateWorker()triggers'exit', but rejecting an already-resolved promise is a no-op, so nosettledflag is strictly required — verified below. A boolean guard works equally well if you prefer it explicit.Verification of the patch
Applied exactly the diff above to the installed
tsup/dist/index.js(8.5.1) and re-ran both builds:Run Before After Worker dies message-less exit 0, silent, no .d.tsexit 1, Error: dts build worker exited with code 0 without reporting a resultHealthy build exit 0, dist/index.d.tsemittedexit 0, dist/index.d.tsemitted (unchanged)Worker OOM exit 1, Unhandled 'error' event, Node internal stackexit 1, rejected through tsup's own path: Worker terminated due to reaching memory limitEnvironment
- tsup 8.5.1 (latest on npm;
dist/index.jsandsrc/index.tsonmainboth carry the shape above) - Node v22.22.2, Linux x64 (Ubuntu 24.04)
- npm, clean standalone install
Related, not duplicates
egoist/tsup#920andegoist/tsup#1325report the DTS worker running out of memory — the loud path (case 2). This report is the silent path (case 1): exit 0, declarations missing, no diagnostic at all. The same two handlers improve case 2's diagnostic as a side effect.I am happy to open a PR with the patch if that is useful.
END upstream text
Generated by Claude Code
- A worker that exits without posting a message (thread called
os-project-manager commented
on Aug 30, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 12167, "status": "done", "branch": "none - drafting task, no code change, no worktree created", "pr": null, "premise_still_valid": true, "summary": "Drafting task per the maintainer's ruling A (2026-08-26). Re-derived the reproduction against the tsup this repo actually pins today and found the premise intact: origin/main resolves tsup@8.5.1, which is also npm's dist-tags.latest (published 2025-11-12), and egoist/tsup@main src/index.ts still registers only worker.on('message') with no error and no exit handler. Built a deterministic end-to-end reproduction on a clean standalone tsup 8.5.1 install (a --require preload that exits the DTS worker thread, since --require is inherited by workers via execArgv): the build exits 0 with an empty stderr and a dist/ holding index.js and zero .d.ts. Applied the proposed two-line patch to that install and confirmed it converts the run to exit 1 while leaving the healthy build unchanged. Posted the complete upstream issue text as a comment on this card for the maintainer to submit under their own name; nothing was posted to any repository outside this one, and option C was not proposed. One correction to the card is carried into the draft and filed separately: OOM is NOT the silent shape - an OOM'd worker emits ERR_WORKER_OUT_OF_MEMORY as an 'error' event which, unhandled, exits 1 loudly. Asserting OOM upstream would have handed the tsup maintainer an unreproducible report.", "tests": "No repo build/test needed - zero files changed in any ObjectStack repo (git status --porcelain on the shared checkout: clean; git worktree list: no new worktree). Verification was of the third-party defect, all measured today on Node v22.22.2 / Linux x64 / Ubuntu 24.04. (1) Version derivation: `git show origin/main:pnpm-lock.yaml | grep tsup@` -> `tsup@8.5.1`; root package.json `\"tsup\": \"^8.5.1\"`; npm registry `dist-tags.latest` = 8.5.1, so the pin is already newest. (2) Defect present in the installed bundle: `worker.on(\"message\"` = 1 hit, `worker.on(\"error\"` / `worker.on(\"exit\"` = 0 hits in tsup/dist/index.js. (3) Not fixed upstream: raw fetch of egoist/tsup@main src/index.ts (http=200) shows the same block, lines ~214-254. (4) Real reproduction, clean standalone install of tsup@8.5.1 + typescript@5.9.3: baseline build exit=0 with dist/index.d.ts emitted; identical build with a preload that runs process.exit(0) when !isMainThread gives exit=0, stdout 177 bytes ending at 'ESM Build success', stderr 0 bytes, dist/ = index.js only, d.ts count 0. (5) Patch verification, mutation confirmed on disk before each run by grep counts (error-handler 1, exit-handler 1, message-handler 1 after the edit; 0/0 after restore, and `cmp -s` byte-identical to the pre-mutation copy): patched + killed worker -> exit=1, `Error: dts build worker exited with code 0 without reporting a result`; patched + healthy -> exit=0 with dist/index.d.ts, confirming the terminate-triggered 'exit' reject is a post-settle no-op. (6) Death-mode table behind the correction: message-less exit from CJS -> exit 0 silent; message-less exit under ESM top-level await -> exit 13 with an unsettled-TLA warning; worker OOM with resourceLimits 16MB -> exit 1 `Unhandled 'error' event` ERR_WORKER_OUT_OF_MEMORY; worker OOM under process-wide --max-old-space-size=64 -> exit 1, same shape; OOM with both handlers installed -> clean rejection, `BUILD FAILED: Worker terminated due to reaching memory limit`. Draft comment read back from the API after posting: 10017 bytes, byte-identical to the local source apart from the platform collapsing one blank line before the footer - nothing eaten by the sanitizer.", "mcp_calls": "6 - issue_read get, issue_read get_comments, search_issues (egoist/tsup dedupe), add_issue_comment (the draft), search_issues (in-repo dedupe before filing), issue_write create, issue_write update. Repo-scoped REST GET works for this seat (comment read-back returned 200) but the search API returns 403, so both dedupe searches went through MCP.", "open_questions": [], "out_of_scope_findings": [ "filed as #13509 (finding): scripts/check-dts-emitted.mjs names OOM as the observed silent-build shape in both its header comment and the failure message it prints, and its printed remediation leads with a --max-old-space-size retry - measurement shows an OOM'd worker is the loud exit-1 path, so the advice targets a mode that cannot produce this failure. Docs/message only; the guard's logic is correct and untouched." ] }Delivery note: the drafted upstream text is the preceding comment, marked BEGIN/END for copy-paste. No upstream issue was opened - the external signature is the maintainer's.
Generated by Claude Code
os-project-manager commented
on Aug 30, 2026 CollaboratorMore actions✅ ACCEPT · R32 ·
domain:devxPM seat — report-only, ⛔ no PR, zero files changedDrafting task per the maintainer's ruling A. The draft is posted above for review and submission under the maintainer's own name; ⛔ nothing was posted outside this repository, and ⛔ option C was not proposed.
⭐⭐⭐ The re-derivation caught a false cause that would have gone upstream
I made "re-derive against the tsup this repo pins today" a precondition because the card is several rounds old. It paid, and not in the way I expected. The premise held — but one of the card's stated facts is wrong:
⛔ OOM is NOT the silent shape. An OOM'd worker emits
ERR_WORKER_OUT_OF_MEMORYas an'error'event, which — unhandled — exits 1 loudly.⇒ "Asserting OOM upstream would have handed the tsup maintainer an unreproducible report."
⭐ Sit with what that avoided. This deliverable goes out under the maintainer's name, to a third-party project. A report whose stated cause cannot produce the described symptom gets closed as invalid, and the cost is not a CI cycle — it is credibility with an upstream maintainer, spent on our behalf, unrecoverably.
The death-mode table that establishes it, all measured today on Node v22.22.2:
mode result message-less exit from CJS exit 0, silent ← the real shape message-less exit under ESM top-level await exit 13, unsettled-TLA warning worker OOM, resourceLimits16MBexit 1, Unhandled 'error' eventERR_WORKER_OUT_OF_MEMORYworker OOM, process-wide --max-old-space-size=64exit 1, same shape OOM with both handlers installed clean rejection The premise itself, verified rather than assumed
origin/mainresolves tsup@8.5.1, which is also npm'sdist-tags.latest(published 2025-11-12) ⇒ ⭐ the pin is already newest, so "upgrade first" is not an available answer.- The defect is present in the installed bundle:
worker.on("message"= 1 hit,worker.on("error"/worker.on("exit"= 0 hits. - Not fixed upstream: a raw fetch of
egoist/tsup@mainsrc/index.ts(http 200) shows the same block at ~214-254.
⭐⭐ The patch was verified before being proposed, not merely described
A clean standalone
tsup@8.5.1+typescript@5.9.3install, with a deterministic reproduction — a--requirepreload that exits the DTS worker thread, exploiting that--requireis inherited by workers viaexecArgv. Baseline emitsdist/index.d.ts; the killed-worker run gives exit 0, stderr 0 bytes,dist/=index.jsonly, d.ts count 0.Then the proposed two-line patch applied to that install: killed worker → exit 1 with
Error: dts build worker exited with code 0 without reporting a result; healthy build → exit 0 withindex.d.tsstill emitted, confirming the terminate-triggered'exit'reject is a post-settle no-op.⭐ That last check is the one an upstream maintainer will actually ask about — "does your fix break the happy path?" — and it is answered before they ask. Mutation confirmed on disk by grep counts before each run, restore proven byte-identical with
cmp -s.Filed
#13509 —
scripts/check-dts-emitted.mjsnames OOM as the observed silent-build shape in both its header comment and its printed failure message, and its remediation leads with a--max-old-space-sizeretry. Measurement says an OOM'd worker is the loud exit-1 path ⇒ ⛔ the advice targets a mode that cannot produce this failure.⚠️ Note what that is: a guard whose logic is correct telling an operator to do something that cannot help, for a failure mode that is not the one occurring. The guard's logic is untouched and correct — only its prose lies. ⛔ Ungraded, unassigned; triage's to route.Hygiene
Zero files changed in any ObjectStack repo — shared checkout
git status --porcelainclean,git worktree listshows no new worktree. Draft read back from the API after posting: 10017 bytes, byte-identical apart from the platform collapsing one blank line before the footer.⛔ This card is not closed by this comment — the deliverable is a draft awaiting the maintainer's review and submission. It closes when that submission happens, or when the maintainer decides not to send it.
Generated by Claude Code
State correction ·
domain:devxPM seat —pm:dispatched⇒pm:awaiting-maintainerThe ACCEPT above (15:35:23Z) ends with an explicit fence:
⛔ This card is not closed by this comment — the deliverable is a draft awaiting the
maintainer's review and submission. It closes when that submission happens, or when the
maintainer decides not to send it.Correct, and unchanged. But the card has been wearing
pm:dispatchedwith an assignee ever since
— telling the state machine a dev is in flight when the drafting work is finished and accepted.
That is this seat's own half-state residue, and it is being cleared on the record rather than
quietly.Why
pm:awaiting-maintainerand not something elseThe state model's distinctions, applied one at a time:
- ⛔ Not
needs-user-decision— the decision is already made. Ruling A was recorded on
2026-08-26 (「其他接受你的建议」): a seat drafts, the maintainer reviews and submits under their
own name. Nothing is awaiting a judgement. - ⛔ Not
pm:on-hold— that is "decided, and the answer is not now". The answer here is yes,
send it. - ⛔ Not closed — the deliverable has not reached its destination.
- ✅
pm:awaiting-maintainer— decision made; the only remaining action is one human action
outside GitHub: posting the drafted issue to the upstreamtsuprepository under the
maintainer's own name. That is precisely what this state is for.
Per the state model,
pm:awaiting-maintaineris mutually exclusive with the otherpm:*states,
sopm:dispatchedcomes off in the same write. The cross-repo ledger and the exit for this
state belong to the 项目总监席 — this seat only enters the state.What the maintainer needs, in one place
- The complete, copy-pasteable upstream issue text is in the comment at 15:31:09Z. It contains no
ObjectStack issue numbers, links or jargon. - ⛔ Nothing has been posted to any repository outside this one.
- Premise re-verified the day it was drafted, not recalled: this repo resolves tsup@8.5.1,
which is also npm'sdist-tags.latest, and upstreamsrc/index.tsstill registers only the
messagehandler — noerror, noexit. - The proposed two-line patch was applied and measured, not merely described: killed worker
goes from exit 0 with a declaration-lessdist/to exit 1 with a real error, and the happy path
still emitsindex.d.ts.
One correction the draft already absorbs
⚠️ OOM is not the silent shape. An OOM'd worker emitsERR_WORKER_OUT_OF_MEMORYand exits
loudly. This matters beyond the draft — it is why #13509 was filed:check-dts-emitted.mjs
names OOM as the observed silent-build shape in both its header and its failure message, and leads
its remediation with a--max-old-space-sizeretry. ⇒ a guard whose logic is correct, telling an
operator to do something that cannot help, for a failure mode that is not the one occurring.
⛔ Ungraded, unassigned — triage's to route.
Generated by Claude Code
- ⛔ Not
✅ SUBMITTED upstream — egoist/tsup#1410. Card closes.
Executed by the director seat (session
local_c32e5a25-0581-4665-952f-60c1be74b250) on the maintainer's in-session delegation, 2026-08-31, verbatim: 「帮我处理 os#12167」 — completing the 2026-08-26 ruling A (seat drafts, maintainer reviews and submits; the submission was performed via the maintainer-operatedhotlongaccount, which the post carries).Pre-submission checks, run fresh:
- Dedup re-run against egoist/tsup (tracker search, 12 nearest results reviewed): no existing report of the silent exit-0 shape — nearest neighbours remain the loud-path OOM reports chore(ci)(deps): bump dorny/paths-filter from 3 to 4 #920/[P3] mobile: responsive layout / mobile experience unverified #1325, referenced as related in the submitted text, exactly as the draft's own dedup found.
- Tracker state: issues enabled, repo not archived.
- Text: the 2026-08-30 corrected draft submitted verbatim (title + body between the BEGIN/END markers) — including the OOM-is-the-loud-path correction, the deterministic message-less-death repro, the two-line fix and its before/after verification matrix. No ObjectStack-internal references in the posted text.
The in-repo guard (
check-dts-emitted.mjs, #12076/#12078) stays in place as the compensating control until the upstream fix ships — as this card itself prescribed (complements, not alternatives).pm:awaiting-maintainerexit: the awaited maintainer action has happened; label off, closing completed.Correction to this card's body, from a measurement taken today — posted by the
domain:devxexecution PM seat (#6023), sessionsession_01Pk26oZ12t5N1hwGW1m1MgC.⛔ This card's body is left as filed. It is the record of what was believed at the time, and rewriting it would falsify that record. This comment is the correction; the body stays.
This card's body says, verbatim:
OOM under memory pressure is the observed shape
⛔ That is not what happens. Measured on Node v22.22.2 / linux x64 against a harness carrying tsup's exact promise shape (a
Workerwith onlymessageregistered — noerror, noexit):worker death mode event delivered process outcome process.exit(), CJS callerexitexit 0, zero output process.exit(), ESM caller (top-level await)exitexit 13 + "unsettled top-level await" warning heap OOM, per-worker resourceLimitserrorexit 1 heap OOM, process-wide --max-old-space-sizeerrorexit 1 ⇒ Node delivers worker heap exhaustion as an
errorevent (ERR_WORKER_OUT_OF_MEMORY), and anerrorevent with no listener is rethrown by EventEmitter. An OOM'd DTS pass exits non-zero and prints a stack — it is the loud sibling, and turbo never caches it. Only the first row is silent, and the caller's module system is load-bearing: the same message-less death exits 13 with a warning under ESM and 0 in silence under CJS. tsup's CLI is CJS, which is why the observed shape was a silent one.⭐ The practical consequence, and the reason this was worth correcting rather than shrugging at: the remedy that followed from the OOM reading — build with more headroom — can never converge, because the exit-0 shape is not a memory problem.
⚠️ What killed the worker in this card's original observation is NOT established by any of the above. All that is shown is that it ended without anerrorevent (because that run exited 0), and therefore that memory pressure is not a demonstrated cause of this shape. ⛔ No cause is assigned here.The guard's header and its printed failure message — which cite this card — are corrected to match in #13898.
Generated by Claude Code
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.