From 688da138721ccda0cf4c2a36f5bb6789df6cf983 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sat, 30 May 2026 18:53:34 +0800 Subject: [PATCH] fix(runtime): keep text-measure helpers internal, matching Ink (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ink keeps its `measure-text` module internal and never re-exports it. vue-tui exported `measureText` / `measureTextNatural` from the public index under the mistaken belief — stated verbatim in commit 0e7d775's own message — that doing so "matched Ink's public API". It does not; Ink keeps that module internal. A later design doc then rationalized the leak post-hoc as an intentional divergence. It was neither intentional nor a divergence — it was a mistake. Align with Ink: - Drop both from the public index. `measureTextNatural` stays as an internal helper (yoga.ts uses it). `measureText` had zero production callers (yoga uses `wrapText` + `measureTextNatural`, never `measureText`) and is removed. - Integration tests that used `measureText(stripAnsi(x), 9999).width` as a line-width helper now use `stringWidth(stripAnsi(x))` directly. - public-api.test.ts gains a regression test asserting neither is exported. - Remove the now-obsolete entry from .agents/docs/ink-divergences.md. Co-authored-by: Claude Opus 4.8 (1M context) --- .agents/docs/ink-divergences.md | 6 ------ .../integration/components/borders.test.tsx | 5 +++-- .../integration/layout/grapheme-clip.test.tsx | 5 +++-- .../integration/layout/overflow.test.tsx | 5 +++-- .../runtime-tests/integration/public-api.test.ts | 10 +++++++++- packages/runtime/src/host/text-measure.ts | 12 ------------ packages/runtime/src/index.ts | 4 +++- 7 files changed, 21 insertions(+), 26 deletions(-) diff --git a/.agents/docs/ink-divergences.md b/.agents/docs/ink-divergences.md index 2a5cbb1..cfb5d0f 100644 --- a/.agents/docs/ink-divergences.md +++ b/.agents/docs/ink-divergences.md @@ -40,12 +40,6 @@ deliberate. Divergences fall into a few kinds: - **Why:** intentionally minimal, single-purpose composables. `waitUntilRenderFlush` is deliberately **not** exposed. -### Exported text-measurement helpers - -- **Ink:** does not export its internal `measure-text` module. -- **vue-tui:** exports `measureText` / `measureTextNatural` from the public index. -- **Why:** a deliberately public utility surface for consumers who need to size text. - ### No named type / prop re-exports - **Ink:** re-exports `BoxProps`, `TextProps`, `StaticProps`, `TransformProps`, diff --git a/packages/runtime-tests/integration/components/borders.test.tsx b/packages/runtime-tests/integration/components/borders.test.tsx index a75ff47..2a9b213 100644 --- a/packages/runtime-tests/integration/components/borders.test.tsx +++ b/packages/runtime-tests/integration/components/borders.test.tsx @@ -1,8 +1,9 @@ import { defineComponent, shallowRef, nextTick } from "vue"; import { test } from "vite-plus/test"; import { render } from "@vue-tui/testing"; -import { Box, Text, measureText } from "@vue-tui/runtime"; +import { Box, Text } from "@vue-tui/runtime"; import stripAnsi from "strip-ansi"; +import stringWidth from "string-width"; // single node — full width box test("single node - full width box", async ({ expect }) => { @@ -977,7 +978,7 @@ test("arrow border on narrow box does not overflow", async ({ expect }) => { ); const frame = lastFrame()!; for (const line of frame.split("\n")) { - expect(measureText(stripAnsi(line), 9999).width).toBeLessThanOrEqual(3); + expect(stringWidth(stripAnsi(line))).toBeLessThanOrEqual(3); } const stripped = stripAnsi(frame); expect(stripped.split("\n")[0]).toContain("↘"); diff --git a/packages/runtime-tests/integration/layout/grapheme-clip.test.tsx b/packages/runtime-tests/integration/layout/grapheme-clip.test.tsx index d61f0c8..38d1754 100644 --- a/packages/runtime-tests/integration/layout/grapheme-clip.test.tsx +++ b/packages/runtime-tests/integration/layout/grapheme-clip.test.tsx @@ -1,11 +1,12 @@ import { defineComponent } from "vue"; import { describe, expect, test } from "vite-plus/test"; import { render } from "@vue-tui/testing"; -import { Box, Text, measureText } from "@vue-tui/runtime"; +import { Box, Text } from "@vue-tui/runtime"; import stripAnsi from "strip-ansi"; +import stringWidth from "string-width"; function lineWidth(text: string): number { - return measureText(stripAnsi(text), 9999).width; + return stringWidth(stripAnsi(text)); } describe("grapheme-aware clipping (issue #21)", () => { diff --git a/packages/runtime-tests/integration/layout/overflow.test.tsx b/packages/runtime-tests/integration/layout/overflow.test.tsx index 70b71e4..73058a9 100644 --- a/packages/runtime-tests/integration/layout/overflow.test.tsx +++ b/packages/runtime-tests/integration/layout/overflow.test.tsx @@ -1,8 +1,9 @@ import { defineComponent } from "vue"; import { describe, expect, test } from "vite-plus/test"; import { render } from "@vue-tui/testing"; -import { Box, Text, Transform, measureText } from "@vue-tui/runtime"; +import { Box, Text, Transform } from "@vue-tui/runtime"; import stripAnsi from "strip-ansi"; +import stringWidth from "string-width"; /** Build a round-border box string like boxen(text, { borderStyle: "round" }) */ function box(text: string): string { @@ -586,7 +587,7 @@ test("out of bounds writes do not crash", async () => { // --- absolute overlay wide glyph clipping (issue #10) --- function lineWidth(text: string): number { - return measureText(stripAnsi(text), 9999).width; + return stringWidth(stripAnsi(text)); } describe("absolute overlay wide glyph clipping", () => { diff --git a/packages/runtime-tests/integration/public-api.test.ts b/packages/runtime-tests/integration/public-api.test.ts index 6143ee0..2435eff 100644 --- a/packages/runtime-tests/integration/public-api.test.ts +++ b/packages/runtime-tests/integration/public-api.test.ts @@ -27,7 +27,6 @@ test("public API exposes documented members", () => { "useAnimation", "useBoxMetrics", "measureElement", - "measureText", "usePaste", // Rendering "renderToString", @@ -43,3 +42,12 @@ test("public API exposes documented members", () => { test("useWindowSize is an alias for useTerminalSize", () => { expect(api.useWindowSize).toBe(api.useTerminalSize); }); + +// Ink keeps its `measure-text` module internal and does not re-export it. vue-tui +// once exported `measureText`/`measureTextNatural` under the (incorrect) belief it +// "matched Ink's public API" — it does not. These stay internal; this guards the +// alignment against re-introduction. See .agents/docs/ink-divergences.md. +test("does not expose internal text-measurement helpers (Ink keeps them internal)", () => { + expect(api).not.toHaveProperty("measureText"); + expect(api).not.toHaveProperty("measureTextNatural"); +}); diff --git a/packages/runtime/src/host/text-measure.ts b/packages/runtime/src/host/text-measure.ts index 53f428d..411a8e7 100644 --- a/packages/runtime/src/host/text-measure.ts +++ b/packages/runtime/src/host/text-measure.ts @@ -124,18 +124,6 @@ export function wrapText(text: string, width: number, mode: WrapMode = "wrap"): return cliTruncate(text, width, { position }).split("\n"); } -export function measureText( - text: string, - width: number, - mode: WrapMode = "wrap", -): { width: number; height: number } { - const wrapped = wrapText(text, width, mode); - return { - width: wrapped.reduce((max, line) => Math.max(max, stringWidth(line)), 0), - height: wrapped.length, - }; -} - /** * Natural (unwrapped) dimensions of `text`, mode-independent. Mirrors Ink's * measure-text.js: width = widest line, height = number of \n-separated lines. diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index c827906..48bcd40 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -38,4 +38,6 @@ export { type KittyKeyboardOptions, type KittyFlagName, } from "./io/kitty-keyboard.ts"; -export { measureText, measureTextNatural } from "./host/text-measure.ts"; +// `measureText` / `measureTextNatural` are deliberately NOT re-exported: Ink keeps +// its `measure-text` module internal, and so do we. They remain internal helpers +// (yoga.ts uses `measureTextNatural`). See .agents/docs/ink-divergences.md.