Skip to content

feat(core)!: runMain takes the module — one call is the whole main.ts - #39

Merged
btravers merged 2 commits into
mainfrom
feat/run-main-front-door
Aug 14, 2026
Merged

feat(core)!: runMain takes the module — one call is the whole main.ts#39
btravers merged 2 commits into
mainfrom
feat/run-main-front-door

Conversation

@btravers

Copy link
Copy Markdown
Contributor
// before                                          // after
await runMain(start(AppModule, { runtime }));      await runMain(AppModule, { runtime });

runMain(module, options, exit?) boots start itself and carries the same
phantom needs gate, so a whole main.ts is one call. The app-taking form is
gone — no overload; two ways to call it would be worse DX than the nesting was.
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).

Why

The nesting 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. When an API needs a "known
footgun" 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 footgun
section survives for embedders, who it was always for. All three example mains
used exactly runMain(start(...)) and none touched the app handle — the
composition point bought them nothing.

Mechanics

  • RuntimeNeedsGate — the phantom rest-tuple gate, previously inlined in
    start, withApp and now needed by runMain, is one exported alias. Three
    copies of a subtle conditional tuple become one. runMain discharges it
    through the same cast withApp already documents (the tuple cannot be spread
    while X/Needs are unresolved).
  • awaitExit(app, exit) — the exit-code half, 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 drive the public runMain
    against 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 case start + awaitExit
    exist for. 12 specs, up from 11.
  • The three example mains lose the nesting; the temporal one keeps its
    .finally on runMain's bare Promise, comment intact.
  • Both READMEs, root CLAUDE.md and docs-examples.test-d.ts move in the same
    commit, per the anti-drift rule. Breaking changeset included (minor, pre-1.0,
    nothing published under this shape yet).

Gate

formatlinttypecheck ✓ (22 tasks, all four needs-gate type tests
included) knipbuildtest 21/22 — the one failure is the known local
port-9000 clash in invariants.spec.ts, unrelated; run-main.spec.ts is 12/12.

🤖 Generated with Claude Code

`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>
Copilot AI lite review requested due to automatic review settings August 14, 2026 22:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 runMain to accept (module, options, exit?) and introduce awaitExit(app, exit) for exit-code mapping tests.
  • Export a shared RuntimeNeedsGate type and reuse it in start, runMain, and withApp.
  • 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.

Comment thread packages/core/src/run-main.ts Outdated
`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>
@btravers
btravers merged commit 1e7ac8c into main Aug 14, 2026
13 checks passed
@btravers
btravers deleted the feat/run-main-front-door branch August 14, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants