test(tui): drive the ink-form patch through the plugin, and drop the esbuild type import
Review follow-ups on #1983. The patch's tests called the string helper directly, so nothing exercised the esbuild `onLoad` filter that decides whether the helper runs at all. A filter that stopped matching would leave the build green, the tests green, and the typo shipped — the silent no-op the throwing helper exists to prevent, reintroduced one level up. The plugin is now driven the way esbuild drives it (register, then invoke) against the real `ink-form` module path, and a second test pins the filter's scope: it matches that path and a Windows-separator one, and rejects `Form.js`, `ink-select-input`'s own `SubmitButton.js`, and a first-party `src/SubmitButton.jsx`. The plugin type came from `import type { Plugin } from "esbuild"`, but `esbuild` is tsup's transitive dependency, not one this client declares — it typechecked only while npm happened to hoist it, and would break on a lock change that nested it. Derive it from tsup's own `Options["esbuildPlugins"]` instead of adding a dependency for a type. 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
3c1bbbcb5f
commit
39575ff060
2 changed files with 81 additions and 15 deletions
|
|
@ -27,7 +27,9 @@ import { fileURLToPath } from "node:url";
|
||||||
import type { Options } from "tsup";
|
import type { Options } from "tsup";
|
||||||
import tsupConfig, {
|
import tsupConfig, {
|
||||||
INK_FORM_INCOMPLETE_HINT,
|
INK_FORM_INCOMPLETE_HINT,
|
||||||
|
INK_FORM_SUBMIT_BUTTON,
|
||||||
fixInkFormIncompleteHint,
|
fixInkFormIncompleteHint,
|
||||||
|
inkFormLabelPatch,
|
||||||
} from "../tsup.config.js";
|
} from "../tsup.config.js";
|
||||||
|
|
||||||
const clientDir = path.resolve(
|
const clientDir = path.resolve(
|
||||||
|
|
@ -86,6 +88,15 @@ function reactRenderingDependencies(): string[] {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The real `ink-form` module the label patch targets. */
|
||||||
|
const submitButtonPath = path.join(
|
||||||
|
clientDir,
|
||||||
|
"node_modules",
|
||||||
|
"ink-form",
|
||||||
|
"lib",
|
||||||
|
"SubmitButton.js",
|
||||||
|
);
|
||||||
|
|
||||||
const config = singleConfig(tsupConfig);
|
const config = singleConfig(tsupConfig);
|
||||||
const noExternal = stringEntries(config.noExternal);
|
const noExternal = stringEntries(config.noExternal);
|
||||||
const external = stringEntries(config.external);
|
const external = stringEntries(config.external);
|
||||||
|
|
@ -127,22 +138,64 @@ describe("tui tsup config", () => {
|
||||||
// Read the real dependency, so an `ink-form` upgrade that fixes or rewords
|
// 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
|
// the label fails here — the patch must then be removed, not left silently
|
||||||
// matching nothing.
|
// matching nothing.
|
||||||
const submitButton = readFileSync(
|
const patched = fixInkFormIncompleteHint(
|
||||||
path.join(
|
readFileSync(submitButtonPath, "utf8"),
|
||||||
clientDir,
|
submitButtonPath,
|
||||||
"node_modules",
|
|
||||||
"ink-form",
|
|
||||||
"lib",
|
|
||||||
"SubmitButton.js",
|
|
||||||
),
|
|
||||||
"utf8",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const patched = fixInkFormIncompleteHint(submitButton, "SubmitButton.js");
|
|
||||||
expect(patched).toContain(INK_FORM_INCOMPLETE_HINT.fixed);
|
expect(patched).toContain(INK_FORM_INCOMPLETE_HINT.fixed);
|
||||||
expect(patched).not.toContain(INK_FORM_INCOMPLETE_HINT.typo);
|
expect(patched).not.toContain(INK_FORM_INCOMPLETE_HINT.typo);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes the real module through the plugin, not just the helper", async () => {
|
||||||
|
// The helper is only reached if the plugin's onLoad filter matches. Drive
|
||||||
|
// the plugin as esbuild would — register, then invoke — so a filter that
|
||||||
|
// stops matching the real module's path fails here instead of no-opping
|
||||||
|
// through a green build (the exact silence this patch exists to prevent).
|
||||||
|
type OnLoadCallback = (args: {
|
||||||
|
path: string;
|
||||||
|
}) => Promise<{ contents: string }>;
|
||||||
|
const registered: { filter: RegExp; callback: OnLoadCallback }[] = [];
|
||||||
|
|
||||||
|
const build = {
|
||||||
|
onLoad: (options: { filter: RegExp }, callback: OnLoadCallback) =>
|
||||||
|
registered.push({ filter: options.filter, callback }),
|
||||||
|
};
|
||||||
|
// esbuild's `PluginBuild` carries far more than this patch touches, and the
|
||||||
|
// stub above deliberately implements only the one hook it registers — so
|
||||||
|
// the double cast is bridging a real structural gap, not hiding a mismatch.
|
||||||
|
// A hook the patch called but the stub lacks fails as undefined here rather
|
||||||
|
// than passing silently.
|
||||||
|
inkFormLabelPatch.setup(
|
||||||
|
build as unknown as Parameters<typeof inkFormLabelPatch.setup>[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(registered).toHaveLength(1);
|
||||||
|
const [{ filter, callback }] = registered;
|
||||||
|
expect(filter.test(submitButtonPath)).toBe(true);
|
||||||
|
|
||||||
|
const { contents } = await callback({ path: submitButtonPath });
|
||||||
|
expect(contents).toContain(INK_FORM_INCOMPLETE_HINT.fixed);
|
||||||
|
expect(contents).not.toContain(INK_FORM_INCOMPLETE_HINT.typo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes the patch to ink-form's SubmitButton and nothing else", () => {
|
||||||
|
expect(INK_FORM_SUBMIT_BUTTON.test(submitButtonPath)).toBe(true);
|
||||||
|
// A Windows-style path must match too — the filter runs against whatever
|
||||||
|
// esbuild resolved, and its separator is the platform's.
|
||||||
|
expect(
|
||||||
|
INK_FORM_SUBMIT_BUTTON.test(
|
||||||
|
"C:\\repo\\node_modules\\ink-form\\lib\\SubmitButton.js",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
for (const other of [
|
||||||
|
"/repo/node_modules/ink-form/lib/Form.js",
|
||||||
|
"/repo/node_modules/ink-select-input/build/SubmitButton.js",
|
||||||
|
"/repo/src/SubmitButton.jsx",
|
||||||
|
]) {
|
||||||
|
expect(INK_FORM_SUBMIT_BUTTON.test(other), other).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("fails loudly rather than silently skipping a label it cannot find", () => {
|
it("fails loudly rather than silently skipping a label it cannot find", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
fixInkFormIncompleteHint("no such label here", "SubmitButton.js"),
|
fixInkFormIncompleteHint("no such label here", "SubmitButton.js"),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { defineConfig } from "tsup";
|
import { defineConfig, type Options } from "tsup";
|
||||||
import type { Plugin } from "esbuild";
|
|
||||||
import { readFile } from "node:fs/promises";
|
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";
|
||||||
|
|
@ -45,11 +44,25 @@ export function fixInkFormIncompleteHint(source: string, file: string): string {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inkFormLabelPatch: Plugin = {
|
/**
|
||||||
|
* Which module the patch above is applied to.
|
||||||
|
*
|
||||||
|
* Exported so the tests can check it against the *resolved* path of the real
|
||||||
|
* `ink-form` module — a filter that stops matching is the one way this patch
|
||||||
|
* can silently no-op, since `fixInkFormIncompleteHint` would then never run.
|
||||||
|
*/
|
||||||
|
export const INK_FORM_SUBMIT_BUTTON = /ink-form[\\/]lib[\\/]SubmitButton\.js$/;
|
||||||
|
|
||||||
|
// The plugin type is derived from tsup rather than imported from `esbuild`:
|
||||||
|
// `esbuild` is tsup's transitive dependency, not a declared one of this client,
|
||||||
|
// so a direct import typechecks only while npm happens to hoist it.
|
||||||
|
type EsbuildPlugin = NonNullable<Options["esbuildPlugins"]>[number];
|
||||||
|
|
||||||
|
export const inkFormLabelPatch: EsbuildPlugin = {
|
||||||
name: "ink-form-label-patch",
|
name: "ink-form-label-patch",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
build.onLoad(
|
build.onLoad(
|
||||||
{ filter: /ink-form[\\/]lib[\\/]SubmitButton\.js$/ },
|
{ filter: INK_FORM_SUBMIT_BUTTON },
|
||||||
async ({ path: file }) => ({
|
async ({ path: file }) => ({
|
||||||
contents: fixInkFormIncompleteHint(await readFile(file, "utf8"), file),
|
contents: fixInkFormIncompleteHint(await readFile(file, "utf8"), file),
|
||||||
loader: "js",
|
loader: "js",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue