Files
vue-tui/.agents/docs/api-contract.md
T
Yunfei He 3a029aa684 docs(runtime): record TuiNode-via-TuiApp as accepted incidental exposure
Review (Codex) flagged that `TuiApp extends Omit<App<TuiNode>, "mount">` surfaces
the internal `TuiNode` host-node type in the published .d.ts (it rides out on Vue's
internal `App._container`). Decision: KEEP it / don't fix.

Rationale: `_container` is a Vue-internal field no consumer touches, so the exposure
is purely cosmetic (zero functional impact), and type-only surface isn't held to
strict SemVer, so it imposes no real contract. Hiding it (`App<unknown>` or a
`Pick<App, …>` allowlist) is ceremony for a cosmetic gain on a pre-1.0 lib.

Documented at the TuiApp definition and in api-contract.md so it isn't re-flagged.
No behavior/type change — just a conscious-decision record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:49:08 +08:00

3.5 KiB

Public API contract & surface

What is — and isn't — part of @vue-tui/runtime's public contract, and how the contract is tested. (Behavioral divergences from Ink live in ink-divergences; this file is about the SHAPE of the public surface itself.)

The contract = public exports and their user-consumable types

The public API is everything exported from the main barrel (@vue-tui/runtime): components, composables, entry points — and their TYPES (component prop types, composable return/options types, and named types such as AriaRole, WindowSize, BoxProps, UseXReturn / UseXOptions).

A type is as much a part of the contract as runtime behavior. If user code can name a type and annotate with it, renaming or removing it breaks that code at COMPILE time — exactly as severe as a runtime break. So a type rename/removal is a breaking change, and the type surface is the most important part of the contract to get right.

Because it is contract, it is tested, not merely shipped:

  • public-api.test.ts snapshots the exact public value-export set — adding, removing, or renaming any runtime export fails it, so every surface change must be a deliberate edit there. Specific internal-only members (measureText, the screen-reader linearizer) are additionally tripwired, and internal-only types (e.g. ScreenReaderOptions) are guarded with a compile-time @ts-expect-error. Type-only exports are erased at runtime, so the type surface is guarded individually rather than exhaustively snapshotted.
  • Type-safety behavior is established by RUNNING the type-checker against real usage (tsc for TSX, vue-tsc for templates), never assumed. See accessibility-api for a worked example — which aria spellings the compiler does and does not catch, proven with both tools.

/internal is NOT the contract

@vue-tui/runtime/internal is an explicitly internal / advanced surface — host-node types (TuiNode, …), test-only helpers (renderToStringWithScreenReader), dev/HMR types (DevState, DevErrorInfo), kitty-controller internals, etc. It carries no stability guarantee: its surface is not snapshotted by public-api.test.ts (a member may be tripwired there to prove it is internal, but the surface itself carries no contract), and may change freely between releases.

Placement rule for any export:

  • A user-facing contract → the main barrel (and it is tested).
  • Needed only by tests or advanced integrators → /internal, never the main barrel.

Packaging/build internals (the exports field shape, .mjs paths, dist layout) are likewise not part of the behavioral/type contract and are not aligned to Ink — see the alignment-scope note in ink-divergences.

Accepted incidental exposure: TuiNode via TuiApp

TuiNode is an /internal type, but it is incidentally reachable through the public TuiApp, which extends Omit<App<TuiNode>, "mount"> to inherit Vue's full app surface — Vue's App<HostElement> carries the host type on its internal _container field. This is a conscious non-fix, not a contract: _container is a Vue-internal field no consumer uses, so the exposure is cosmetic (zero functional impact), and type-only surface isn't held to strict SemVer. Narrowing it (App<unknown> / a Pick<App, …> allowlist) was considered and skipped as ceremony for a cosmetic gain on a pre-1.0 library. Treat TuiNode-through-TuiApp as out-of-contract; don't re-flag it. (Decision recorded after review surfaced it.)