From 3c1bbbcb5fda7e4eb393f7323f1b347726ccc440 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 11 Aug 2026 23:15:35 -0400 Subject: [PATCH] fix(tui): correct ink-form's misspelled incomplete-form hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hint under a form with unfilled required fields reads "you have not competed yet". The string is upstream's, hardcoded in `ink-form/lib/SubmitButton.js` with no prop to override it, and `ink-form` was last published in 2024 — but it renders in the Inspector's tool, prompt, and resource test forms, so it is ours to fix in practice. Bundling `ink-form` (previous commit) is what makes it patchable: an esbuild `onLoad` hook rewrites the label as the file enters the bundle. The hook throws when the string is absent rather than passing the source through, so an `ink-form` upgrade cannot silently retire the patch or leave it aimed at a string that no longer exists — the tests drive it against the real dependency source for the same reason. Reported upstream as lukasbach/ink-form#14. Closes #1952 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WLYmVPVLdeYpE6aMTUsCks --- clients/tui/README.md | 16 +++++++ clients/tui/__tests__/tsupConfig.test.ts | 39 ++++++++++++++++- clients/tui/tsup.config.ts | 54 ++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/clients/tui/README.md b/clients/tui/README.md index bcf102c3..58e8be40 100644 --- a/clients/tui/README.md +++ b/clients/tui/README.md @@ -138,3 +138,19 @@ keeps npm from hoisting it next to a React the Inspector couldn't also use. `__tests__/tsupConfig.test.ts` enforces this: every dependency declaring a `react` peer must be in `noExternal` unless it is listed as external by design. Add a React-rendering dependency, and that test tells you to bundle it. + +### The `ink-form` label patch + +Bundling `ink-form` also makes it patchable, which one label needs: the hint +under an incomplete form reads "you have not **competed** yet". It is upstream's +string, hardcoded in `ink-form/lib/SubmitButton.js` with no prop to override, +and `ink-form` was last published in 2024 — so `tsup.config.ts` corrects it with +an esbuild `onLoad` hook as the file enters the bundle. It is reported upstream +as [lukasbach/ink-form#14](https://github.com/lukasbach/ink-form/issues/14); if +a release ever carries the fix, drop the patch. + +The hook **throws** when the string isn't found rather than passing the source +through. A silent no-op would let an `ink-form` upgrade retire the patch without +anyone noticing it had stopped applying — or leave a patch aimed at a string +that no longer exists. If the build fails there, check whether upstream fixed +the typo and delete the patch instead of re-targeting it. diff --git a/clients/tui/__tests__/tsupConfig.test.ts b/clients/tui/__tests__/tsupConfig.test.ts index b66cf13b..1ad3490c 100644 --- a/clients/tui/__tests__/tsupConfig.test.ts +++ b/clients/tui/__tests__/tsupConfig.test.ts @@ -25,7 +25,10 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import type { Options } from "tsup"; -import tsupConfig from "../tsup.config.js"; +import tsupConfig, { + INK_FORM_INCOMPLETE_HINT, + fixInkFormIncompleteHint, +} from "../tsup.config.js"; const clientDir = path.resolve( path.dirname(fileURLToPath(import.meta.url)), @@ -120,6 +123,40 @@ describe("tui tsup config", () => { } }); + it("corrects ink-form's misspelled incomplete-form hint", () => { + // Read the real dependency, so an `ink-form` upgrade that fixes or rewords + // the label fails here — the patch must then be removed, not left silently + // matching nothing. + const submitButton = readFileSync( + path.join( + clientDir, + "node_modules", + "ink-form", + "lib", + "SubmitButton.js", + ), + "utf8", + ); + + const patched = fixInkFormIncompleteHint(submitButton, "SubmitButton.js"); + expect(patched).toContain(INK_FORM_INCOMPLETE_HINT.fixed); + expect(patched).not.toContain(INK_FORM_INCOMPLETE_HINT.typo); + }); + + it("fails loudly rather than silently skipping a label it cannot find", () => { + expect(() => + fixInkFormIncompleteHint("no such label here", "SubmitButton.js"), + ).toThrow(/no longer contains the ink-form label/); + }); + + it("only rewrites the misspelling", () => { + // The two strings must differ by exactly the fix, or the patch is silently + // changing copy nobody reviewed. + expect(INK_FORM_INCOMPLETE_HINT.fixed).toBe( + INK_FORM_INCOMPLETE_HINT.typo.replace("competed", "completed"), + ); + }); + it("verifies the deps it exempts are still declared", () => { // If `ink` ever leaves the dependency list, the exemption above is stale // and would silently excuse a future package that took its name. diff --git a/clients/tui/tsup.config.ts b/clients/tui/tsup.config.ts index 1a90e87f..3a4c66f7 100644 --- a/clients/tui/tsup.config.ts +++ b/clients/tui/tsup.config.ts @@ -1,10 +1,63 @@ import { defineConfig } from "tsup"; +import type { Plugin } from "esbuild"; +import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(dirname, "../.."); +/** + * `ink-form` hardcodes a misspelled hint under an incomplete form — "you have + * not competed yet" — with no prop to override it. It is upstream's string + * (`ink-form/lib/SubmitButton.js`), last published in 2024, but it renders in + * the Inspector's tool/prompt/resource test forms, so we correct it on the way + * into the bundle. Reported upstream as lukasbach/ink-form#14; drop this patch + * if a release ever carries the fix. + * + * This is only possible because `ink-form` is inlined (see `noExternal` below). + */ +export const INK_FORM_INCOMPLETE_HINT = { + typo: "There are still required inputs you have not competed yet.", + fixed: "There are still required inputs you have not completed yet.", +}; + +/** + * Applies the correction above, throwing if the string is no longer there. + * + * Failing loudly is the point: a silent no-op would let an `ink-form` upgrade + * (or a fixed upstream, or a reworded label) quietly retire this patch with + * nobody noticing it had stopped applying — or, worse, leave a patch here for a + * string that no longer exists. If this throws, check whether upstream fixed + * the typo; if so, delete this patch rather than re-targeting it. + */ +export function fixInkFormIncompleteHint(source: string, file: string): string { + if (!source.includes(INK_FORM_INCOMPLETE_HINT.typo)) { + throw new Error( + `${file} no longer contains the ink-form label this build patches ` + + `(${JSON.stringify(INK_FORM_INCOMPLETE_HINT.typo)}). If upstream fixed ` + + `the typo, remove fixInkFormIncompleteHint from tsup.config.ts.`, + ); + } + return source.replaceAll( + INK_FORM_INCOMPLETE_HINT.typo, + INK_FORM_INCOMPLETE_HINT.fixed, + ); +} + +const inkFormLabelPatch: Plugin = { + name: "ink-form-label-patch", + setup(build) { + build.onLoad( + { filter: /ink-form[\\/]lib[\\/]SubmitButton\.js$/ }, + async ({ path: file }) => ({ + contents: fixInkFormIncompleteHint(await readFile(file, "utf8"), file), + loader: "js", + }), + ); + }, +}; + export default defineConfig({ entry: ["index.ts"], format: ["esm"], @@ -44,6 +97,7 @@ export default defineConfig({ "@modelcontextprotocol/core", "@napi-rs/keyring", ], + esbuildPlugins: [inkFormLabelPatch], esbuildOptions(options) { options.alias = { "@inspector/core": path.join(repoRoot, "core"),