fix(runtime): validate invalid foreground color props
This commit is contained in:
@@ -875,6 +875,46 @@ test('foreground color="bold" (a chalk modifier) bolds, does NOT throw', async (
|
||||
expect(lastFrame()).toContain("\x1b[1m");
|
||||
});
|
||||
|
||||
// Foreground colors have their own invalid named-color edge: some chalk keys
|
||||
// exist but are not callable color/modifier methods. Ink calls chalk[color] in
|
||||
// that case and crashes; vue-tui validates during render so the app rejects
|
||||
// through ErrorOverview instead of letting paint wedge the scheduler.
|
||||
test('foreground color="level" throws (Ink parity, recoverable channel)', async ({ expect }) => {
|
||||
await expect(
|
||||
render(
|
||||
defineComponent(() => () => (
|
||||
<Box alignSelf="flex-start">
|
||||
<Text color="level">Hi</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
),
|
||||
).rejects.toThrow(/color/i);
|
||||
});
|
||||
|
||||
for (const borderColorProp of [
|
||||
"borderColor",
|
||||
"borderTopColor",
|
||||
"borderBottomColor",
|
||||
"borderLeftColor",
|
||||
"borderRightColor",
|
||||
] as const) {
|
||||
test(`<Box ${borderColorProp}="level"> throws (Ink parity, recoverable channel)`, async ({
|
||||
expect,
|
||||
}) => {
|
||||
await expect(
|
||||
render(
|
||||
defineComponent(() => () => (
|
||||
<Box borderStyle="single" {...{ [borderColorProp]: "level" }} alignSelf="flex-start">
|
||||
<Text>Hi</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
),
|
||||
).rejects.toThrow(/color/i);
|
||||
});
|
||||
}
|
||||
|
||||
// --- A12 gating: vue must throw WHERE Ink colorizes, and NOT elsewhere ---
|
||||
//
|
||||
// Ink throws on a chalk-modifier-name bg LAZILY — only when it actually
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineComponent, h, inject, type ExtractPublicPropTypes, type PropType } from "vue";
|
||||
import cliBoxes from "cli-boxes";
|
||||
import { AppContextKey } from "../context.ts";
|
||||
import { assertValidBackgroundColor } from "../paint/text-style.ts";
|
||||
import { assertValidBackgroundColor, assertValidForegroundColor } from "../paint/text-style.ts";
|
||||
import type { WithChildren } from "./with-children.ts";
|
||||
|
||||
type Spacing = number;
|
||||
@@ -216,29 +216,46 @@ const BoxImpl = defineComponent({
|
||||
// width=0 with no left/right border) produces an empty string Ink skips.
|
||||
// Matching that needs layout, unavailable at render. Niche + invalid-input-only.
|
||||
if (props.borderStyle) {
|
||||
const stringBg = (value: unknown) => (typeof value === "string" ? value : undefined);
|
||||
const generalBg = stringBg(props.borderBackgroundColor);
|
||||
const stringStyle = (value: unknown) => (typeof value === "string" ? value : undefined);
|
||||
const generalBg = stringStyle(props.borderBackgroundColor);
|
||||
const generalFg = stringStyle(props.borderColor);
|
||||
if (props.borderTop !== false) {
|
||||
assertValidForegroundColor(
|
||||
stringStyle(props.borderTopColor) ?? generalFg,
|
||||
"borderTopColor",
|
||||
);
|
||||
assertValidBackgroundColor(
|
||||
stringBg(props.borderTopBackgroundColor) ?? generalBg,
|
||||
stringStyle(props.borderTopBackgroundColor) ?? generalBg,
|
||||
"borderTopBackgroundColor",
|
||||
);
|
||||
}
|
||||
if (props.borderBottom !== false) {
|
||||
assertValidForegroundColor(
|
||||
stringStyle(props.borderBottomColor) ?? generalFg,
|
||||
"borderBottomColor",
|
||||
);
|
||||
assertValidBackgroundColor(
|
||||
stringBg(props.borderBottomBackgroundColor) ?? generalBg,
|
||||
stringStyle(props.borderBottomBackgroundColor) ?? generalBg,
|
||||
"borderBottomBackgroundColor",
|
||||
);
|
||||
}
|
||||
if (props.borderLeft !== false) {
|
||||
assertValidForegroundColor(
|
||||
stringStyle(props.borderLeftColor) ?? generalFg,
|
||||
"borderLeftColor",
|
||||
);
|
||||
assertValidBackgroundColor(
|
||||
stringBg(props.borderLeftBackgroundColor) ?? generalBg,
|
||||
stringStyle(props.borderLeftBackgroundColor) ?? generalBg,
|
||||
"borderLeftBackgroundColor",
|
||||
);
|
||||
}
|
||||
if (props.borderRight !== false) {
|
||||
assertValidForegroundColor(
|
||||
stringStyle(props.borderRightColor) ?? generalFg,
|
||||
"borderRightColor",
|
||||
);
|
||||
assertValidBackgroundColor(
|
||||
stringBg(props.borderRightBackgroundColor) ?? generalBg,
|
||||
stringStyle(props.borderRightBackgroundColor) ?? generalBg,
|
||||
"borderRightBackgroundColor",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type VNode,
|
||||
} from "vue";
|
||||
import { AppContextKey } from "../context.ts";
|
||||
import { assertValidBackgroundColor } from "../paint/text-style.ts";
|
||||
import { assertValidBackgroundColor, assertValidForegroundColor } from "../paint/text-style.ts";
|
||||
import type { WithChildren } from "./with-children.ts";
|
||||
|
||||
type WrapMode =
|
||||
@@ -73,6 +73,7 @@ const TextImpl = defineComponent({
|
||||
// a group of only empty/inert children) gets past it, so we additionally
|
||||
// skip validation when the content would render empty. (A12)
|
||||
if (wouldRenderNonEmptyText(children)) {
|
||||
assertValidForegroundColor(props.color);
|
||||
assertValidBackgroundColor(props.backgroundColor);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import chalk from "chalk";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { applyChalk, assertValidBackgroundColor, isInvalidBackgroundColor } from "./text-style.ts";
|
||||
import {
|
||||
applyChalk,
|
||||
assertValidBackgroundColor,
|
||||
assertValidForegroundColor,
|
||||
isInvalidBackgroundColor,
|
||||
isInvalidForegroundColor,
|
||||
} from "./text-style.ts";
|
||||
|
||||
test("named color applies chalk method", () => {
|
||||
const prev = chalk.level;
|
||||
@@ -208,6 +214,19 @@ test("assertValidBackgroundColor throws only for a modifier name, with the label
|
||||
expect(() => assertValidBackgroundColor(undefined)).not.toThrow();
|
||||
});
|
||||
|
||||
test("assertValidForegroundColor throws only for chalk keys that are not methods", () => {
|
||||
expect(isInvalidForegroundColor("level")).toBe(true);
|
||||
expect(() => assertValidForegroundColor("level")).toThrow(/color/i);
|
||||
expect(() => assertValidForegroundColor("level", "borderTopColor")).toThrow(/borderTopColor/);
|
||||
|
||||
// Valid foreground forms and unknown strings keep Ink's bare-text fallback.
|
||||
expect(isInvalidForegroundColor("bold")).toBe(false);
|
||||
expect(isInvalidForegroundColor("red")).toBe(false);
|
||||
expect(isInvalidForegroundColor("#abcdef")).toBe(false);
|
||||
expect(isInvalidForegroundColor("not-a-real-color")).toBe(false);
|
||||
expect(isInvalidForegroundColor(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
// Foreground is UNAFFECTED: `color="bold"` resolves `chalk.bold` (a real fn) and
|
||||
// applies the modifier — Ink does NOT throw on a foreground modifier name, and
|
||||
// neither does vue-tui. (Only the bg path has the missing-`bg*`-method problem.)
|
||||
|
||||
@@ -68,6 +68,19 @@ export function isInvalidBackgroundColor(color: unknown): boolean {
|
||||
return typeof bgMethod !== "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a foreground color value that Ink's `colorize` would THROW on.
|
||||
*
|
||||
* Ink's foreground path calls `chalk[color](str)` when `color in chalk`. That
|
||||
* works for real colors and modifiers (`red`, `bold`) but throws for non-method
|
||||
* chalk properties such as `level`.
|
||||
*/
|
||||
export function isInvalidForegroundColor(color: unknown): boolean {
|
||||
if (typeof color !== "string" || color.length === 0) return false;
|
||||
const method = (chalk as unknown as Record<string, unknown>)[color];
|
||||
return color in (chalk as unknown as Record<string, unknown>) && typeof method !== "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw (during component render) if `color` is a chalk-modifier-name
|
||||
* backgroundColor — the exact case Ink's colorize.ts throws on. No-op for every
|
||||
@@ -82,6 +95,18 @@ export function assertValidBackgroundColor(color: unknown, label = "backgroundCo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw during component render for foreground color names that Ink's paint path
|
||||
* would throw on. `label` names the offending prop in the message.
|
||||
*/
|
||||
export function assertValidForegroundColor(color: unknown, label = "color"): void {
|
||||
if (isInvalidForegroundColor(color)) {
|
||||
throw new Error(
|
||||
`Invalid ${label}: ${JSON.stringify(color)} (chalk has this key but it is not a color method)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyChalk(text: string, props: TextProps): string {
|
||||
// Mirror Ink's Text.tsx `transform` (commit 40b3a75): apply each enabled
|
||||
// style as its OWN nested chalk call, in the exact order
|
||||
|
||||
Reference in New Issue
Block a user