fix(tui): correct ink-form's misspelled incomplete-form hint

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLYmVPVLdeYpE6aMTUsCks
This commit is contained in:
cliffhall 2026-08-11 23:15:35 -04:00
commit 3c1bbbcb5f
3 changed files with 108 additions and 1 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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"),