feat(core)!: runMain takes the module — one call is the whole main.ts - #39
Merged
Conversation
`runMain(AppModule, { runtime })` now boots `start` itself, carrying the
same phantom needs gate, and the app-taking form is gone. `start` stays
the API for callers that want the `RunningApp` (tests, embedders, a dev
runner booting two applications — none of which may claim
`process.exitCode`).
The nesting it replaces — `runMain(start(module, options))` — made
`start` look complete on its own, and using it alone in an entry point
is the documented footgun: the kernel's uncaught handlers suppress
Node's default exit 1, so a crash exited 0. The front door is now the
one-call shape the docs lead with; the footgun section survives for
embedders, which is who it was always for.
Mechanics:
- The gate is extracted as `RuntimeNeedsGate`, exported, and shared by
`start`, `runMain` and `withApp` — three inline copies of a subtle
conditional tuple become one alias. `runMain` discharges it through
the same cast `withApp` already documents, since the tuple cannot be
spread while `X`/`Needs` are unresolved.
- The exit-code half lives on as `awaitExit(app, exit)`, exported from
`run-main.ts` for its spec but not from `index.ts`. The ten code-table
rows keep their honest stub fixture through it; three specs now drive
the public `runMain` against a real boot (a failing provider is the
cheapest deterministic outcome), and the abandoned-work end-to-end
keeps holding the `RunningApp` — which is exactly the case `start` +
`awaitExit` exist for.
- The three example mains lose the nesting; the temporal one keeps its
`.finally` on `runMain`'s bare Promise, comment intact.
- Both READMEs, the root CLAUDE.md and `docs-examples.test-d.ts` move in
the same commit, per the anti-drift rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR reshapes @btravstack/core’s entry-point API so runMain(module, options, exit?) becomes the single “front door” call in a real main.ts, internally booting start and applying the same compile-time runtime-needs gate. It also centralizes the phantom rest-tuple gate into a shared RuntimeNeedsGate alias and updates docs/tests/examples accordingly.
Changes:
- Change
runMainto accept(module, options, exit?)and introduceawaitExit(app, exit)for exit-code mapping tests. - Export a shared
RuntimeNeedsGatetype and reuse it instart,runMain, andwithApp. - Update READMEs, docs type-tests, example entry points, and add a breaking changeset.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates root docs to show the new one-call runMain(module, options) entry-point shape. |
| packages/core/src/with-app.ts | Switches withApp to use the shared RuntimeNeedsGate type alias. |
| packages/core/src/start.ts | Introduces and exports RuntimeNeedsGate; updates start to use it. |
| packages/core/src/run-main.ts | Reworks runMain to boot start internally; adds awaitExit helper. |
| packages/core/src/run-main.spec.ts | Updates tests to assert the exit-code table via awaitExit and drives real runMain boot paths. |
| packages/core/src/index.ts | Re-exports RuntimeNeedsGate as part of the package’s public types. |
| packages/core/src/docs-examples.test-d.ts | Updates compiled docs samples to the new runMain signature. |
| packages/core/README.md | Mirrors root README updates for the package README. |
| examples/order-temporal-worker/src/main.ts | Updates the Temporal worker example to call runMain directly (no nested start). |
| examples/order-api/src/main.ts | Updates the HTTP API example to call runMain directly. |
| examples/order-amqp-worker/src/main.ts | Updates the AMQP worker example to call runMain directly. |
| CLAUDE.md | Updates the repository guidance to reflect the new runMain(module, options, exit?) contract. |
| .changeset/run-main-front-door.md | Adds a breaking change note describing the new runMain API. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`needs` is required on `HttpOptions`, so the sample as written would not typecheck for a consumer copying it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
runMain(module, options, exit?)bootsstartitself and carries the samephantom needs gate, so a whole
main.tsis one call. The app-taking form isgone — no overload; two ways to call it would be worse DX than the nesting was.
startstays the API for callers that want theRunningApp(tests, embedders,a dev runner booting two applications — none of which may claim
process.exitCode).Why
The nesting made
startlook complete on its own, and using it alone in anentry point is the documented footgun: the kernel's uncaught handlers suppress
Node's default exit 1, so a crash exited
0. When an API needs a "knownfootgun" section for its obvious partial use, the ceremony is optimized for the
rare audience over the common one. The docs now lead with
runMain; the footgunsection survives for embedders, who it was always for. All three example mains
used exactly
runMain(start(...))and none touched the app handle — thecomposition point bought them nothing.
Mechanics
RuntimeNeedsGate— the phantom rest-tuple gate, previously inlined instart,withAppand now needed byrunMain, is one exported alias. Threecopies of a subtle conditional tuple become one.
runMaindischarges itthrough the same cast
withAppalready documents (the tuple cannot be spreadwhile
X/Needsare unresolved).awaitExit(app, exit)— the exit-code half, exported fromrun-main.tsfor its spec but not from
index.ts. The ten code-table rows keep theirhonest stub fixture through it; three specs drive the public
runMainagainst a real boot (a failing provider is the cheapest deterministic
outcome — no clock, no signals), and the abandoned-work end-to-end keeps
holding the
RunningApp, which is exactly the casestart+awaitExitexist for. 12 specs, up from 11.
.finallyonrunMain's barePromise, comment intact.CLAUDE.mdanddocs-examples.test-d.tsmove in the samecommit, per the anti-drift rule. Breaking changeset included (minor, pre-1.0,
nothing published under this shape yet).
Gate
format✓lint✓typecheck✓ (22 tasks, all four needs-gate type testsincluded)
knip✓build✓test21/22 — the one failure is the known localport-9000 clash in
invariants.spec.ts, unrelated;run-main.spec.tsis 12/12.🤖 Generated with Claude Code