vue-tui withheld Ink's named prop types (BoxProps, TextProps, StaticProps,
TransformProps, NewlineProps) and the WindowSize/CursorPosition data shapes
under a blanket "avoid React-shaped type names" rule. That rule over-reached:
a <Box> has props in Vue exactly as in React, so those names carry no
React-vs-Vue content — there's no reason to rename them. Re-export them under
Ink's names so a consumer can name a component's props the same way as in Ink.
- Derive each XProps from the component's runtime `props` object via Vue's
`ExtractPublicPropTypes`, so the public type can never drift from the real
props. Pin `required: true as const` on Static.items / Transform.transform:
a standalone `const` widens `true`→`boolean`, which would otherwise drop them
from the required keys — in both the exported type AND the component's own
`setup(props)` typing.
- Add `WindowSize { columns, rows }` and `CursorPosition { x, y }`, anchored to
their real usage in useTerminalSize / useCursor. (The composables still return
reactive refs of these — the data shape matches Ink; the ref wrapper is the
framework difference.)
- Keep the genuinely-divergent names as-is: DOMElement→TuiNode (real
DOM-emulation vs host-node difference), RenderOptions/Instance→MountOptions/
TuiApp (downstream of createApp()), App/Stdin/Stdout/StderrProps = N/A. Rewrite
the ink-divergences doc to record what's now aligned vs still divergent.
- Add a tsc-checked type-level test (public-types.test-d.ts) asserting the
exported shapes; it is excluded from vitest's runtime run by naming.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
7.3 KiB
vue-tui — Intentional Divergences from Ink
vue-tui started as a Vue 3 port of Ink, and it still tracks Ink closely — the aim is behavioral parity except where a difference is deliberate. But it is no longer just a port: it has grown its own design decisions, additive features, and Vue-native choices. This document records the places vue-tui intentionally differs from Ink — by design, not as a gap to fix. A difference that is not listed here is treated as a bug (or simply unverified), not a design choice.
Reference baseline: Ink v7.0.4 (commit
40b3a7578811fd616341ca4e31cc7748aeeff12f). When bumping the target Ink version, re-validate every entry below against the new source.
How to read this
Each entry states what Ink does, what vue-tui does, and why the divergence is deliberate. Divergences fall into a few kinds:
- API surface — public API renamed/reshaped to fit Vue idioms.
- Additive — vue-tui supports something Ink doesn't (a strict superset).
- Framework semantics — a consequence of Vue ≠ React that cannot be papered over.
- N/A — a React-only concept with no Vue equivalent.
Public API surface
Entry point — createApp() instead of render()
- Ink:
render(<App/>). - vue-tui:
createApp(App).mount(options). - Why: mirrors Vue's own
createAppmental model — a Vue developer expects an app object they mount, not a one-shot render call.
App composable — useExit() instead of useApp()
- Ink:
useApp()returns the full AppContext (exit,waitUntilRenderFlush, stdin/stdout/stderr, …). - vue-tui:
useExit()returns only theexitfunction; the rest is reached through dedicated composables (useStdin,useStdout,useStderr, …). - Why: intentionally minimal, single-purpose composables.
waitUntilRenderFlushis deliberately not exposed.
Named type / prop re-exports
- Ink: re-exports its component prop types plus a few data/handle types:
BoxProps,TextProps,StaticProps,TransformProps,NewlineProps,WindowSize,CursorPosition,DOMElement,RenderOptions,Instance,AppProps,StdinProps,StdoutProps,StderrProps. - vue-tui: re-exports the framework-neutral ones under the same names —
BoxProps,TextProps,StaticProps,TransformProps,NewlineProps,WindowSize({ columns, rows }) andCursorPosition({ x, y }). These are not divergences: a<Box>has props in Vue exactly as in React, so the names carry over. They are derived from the runtimepropsobjects via Vue'sExtractPublicPropTypes, so they never drift from the components' real props. Only the remaining few genuinely differ, each for a concrete reason — never merely to "avoid React-shaped names":DOMElement→TuiNode. The one genuinely DOM-shaped type: Ink'sDOMElementmodels a DOM-emulation node (nodeName/attributes/childNodes). vue-tui's host tree is a different representation (TuiContainer | TuiTextLeaf | TuiComment), exported asTuiNodefrom@vue-tui/runtime/internal.RenderOptions/Instance→MountOptions/TuiApp. Downstream of thecreateApp()entry above — vue-tui mounts a Vue app, so the options bag and the returned handle are Vue-shaped, notrender()-shaped.AppProps/StdinProps/StdoutProps/StderrProps→ N/A. These are the props of Ink's internal React context-provider components (<AppContext>,<StdinContext>, …). vue-tui has no such components — that state is reached viacreateAppplus theuseStdin/useStdout/useStderrcomposables — so there is nothing to name.
- Why: the earlier blanket "expose a Vue-native type surface, don't leak
React-shaped names" over-reached — it withheld names like
BoxPropsthat have no React vs Vue content at all. The rule is narrower: mirror Ink's names wherever the underlying type is framework-neutral; reshape only where Vue genuinely has a different thing (a host node, a mounted app) or no thing at all.
Additive features (vue-tui is a strict superset)
Accessibility props — AriaRole / AriaState
- Ink: no equivalent.
- vue-tui:
<Box>/<Text>acceptAriaRole/AriaStateprops that feed the screen-reader linearization. - Why: additive accessibility surface; does not change visual (non-SR) rendering.
Multiple <Static> regions
- Ink: keeps a single
staticNode; only one<Static>is honored. - vue-tui:
findStatics(root)renders every<Static>in the tree. - Why: strictly more capable — a tree with two
<Static>regions both render. Maintainer decision (2026-05-30): KEEP.
Ctrl+C exits under the kitty protocol too
- Ink: wires
exitOnCtrlConly to the legacy\x03path, so a kitty-protocol Ctrl+C (\x1b[99;5u) does not exit. - vue-tui:
useInputexits oninput === 'c' && key.ctrl, gated onexitOnCtrlC(defaulttrue), so Ctrl+C exits under both legacy and kitty protocols. - Why:
exitOnCtrlCreliably exiting under all protocols is the intended behavior; opt out withexitOnCtrlC: false. Maintainer decision (2026-05-30): KEEP.
Framework-semantic divergences (Vue ≠ React)
Removing flexDirection / flexWrap resets to the default
- Ink:
applyFlexStyleshas noundefinedbranch forflexDirection/flexWrap, so an explicitflexDirection={undefined}leaves the yoga node's stale value in place. (For an omitted prop, Ink's<Box>re-applies itsrow/nowrapdefault before the style spread.) - vue-tui: resets
flexDirection/flexWrapto the Box default (row/nowrap) when the prop is removed across renders. - Why: Vue cannot distinguish an omitted prop from an explicit
undefined— both collapse to the prop's default — so vue-tui must pick one behavior. It matches Ink's common case (omitted →row/nowrap) and the Vue-idiomatic expectation (drop the override → get the default). The residual difference (explicit={undefined}→ vue-tui resets, Ink keeps stale) is an unavoidable Vue-vs-React semantic. Maintainer decision (2026-05-30): KEEP.
Other Vue-vs-React semantics (placeholder)
- (maintainer: candidates to document —
v-if/nullrendering as comment host nodes; reactivity-driven re-render timing vs React's render cycle; keyed reconciliation order. Add the ones that are genuinely by-design.)
Not applicable in Vue
React concurrent mode
- Ink: built on React; Suspense /
useTransitionare React features. - vue-tui: no equivalent — N/A, not a gap.
Framework idioms (noted, not behavioral divergences)
Surface conventions, listed so they aren't mistaken for gaps:
- Vue composables (
useFocus,useInput, …) instead of React hooks. <script setup>SFCs /defineComponentinstead of function components.- kebab-case filenames;
.tsover.tsxwhere there's no JSX. shallowRefby default for reactive state.
Maintainer additions
Space for divergences to add or refine. For each, capture: what Ink does, what vue-tui does, and why it's deliberate (the trade-off, not just the what).