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:
parent
eb5bba62ed
commit
3c1bbbcb5f
3 changed files with 108 additions and 1 deletions
|
|
@ -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
|
`__tests__/tsupConfig.test.ts` enforces this: every dependency declaring a
|
||||||
`react` peer must be in `noExternal` unless it is listed as external by design.
|
`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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ import { readFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import type { Options } from "tsup";
|
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(
|
const clientDir = path.resolve(
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
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", () => {
|
it("verifies the deps it exempts are still declared", () => {
|
||||||
// If `ink` ever leaves the dependency list, the exemption above is stale
|
// If `ink` ever leaves the dependency list, the exemption above is stale
|
||||||
// and would silently excuse a future package that took its name.
|
// and would silently excuse a future package that took its name.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,63 @@
|
||||||
import { defineConfig } from "tsup";
|
import { defineConfig } from "tsup";
|
||||||
|
import type { Plugin } from "esbuild";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const repoRoot = path.resolve(dirname, "../..");
|
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({
|
export default defineConfig({
|
||||||
entry: ["index.ts"],
|
entry: ["index.ts"],
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
|
|
@ -44,6 +97,7 @@ export default defineConfig({
|
||||||
"@modelcontextprotocol/core",
|
"@modelcontextprotocol/core",
|
||||||
"@napi-rs/keyring",
|
"@napi-rs/keyring",
|
||||||
],
|
],
|
||||||
|
esbuildPlugins: [inkFormLabelPatch],
|
||||||
esbuildOptions(options) {
|
esbuildOptions(options) {
|
||||||
options.alias = {
|
options.alias = {
|
||||||
"@inspector/core": path.join(repoRoot, "core"),
|
"@inspector/core": path.join(repoRoot, "core"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue