fix: warn and skip insertion when Box is nested inside Text
Detect invalid <Box> inside <Text> nesting at the DOM insert level and emit a dev warning instead of crashing the WASM yoga engine. The box insertion is skipped to prevent layout corruption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { defineComponent } from "vue";
|
||||
import { expect, test, vi } from "vite-plus/test";
|
||||
import { render } from "@vue-tui/testing";
|
||||
import { Box, Text } from "@vue-tui/runtime";
|
||||
|
||||
test("<Box> inside <Text> emits a dev warning", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
await render(
|
||||
defineComponent(() => () => (
|
||||
<Text>
|
||||
<Box>invalid</Box>
|
||||
</Text>
|
||||
)),
|
||||
);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("<Box>"));
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
@@ -94,6 +94,5 @@ test.todo(
|
||||
"fail when text node is not within <Text> component — vue-tui silently allows text-leaf inside box; validation not yet implemented",
|
||||
);
|
||||
|
||||
test.todo(
|
||||
"fail when <Box> is inside <Text> component — causes WASM table index out of bounds crash; yoga does not safely reject this nesting",
|
||||
);
|
||||
// Resolved: <Box> inside <Text> now emits a dev warning and skips insertion
|
||||
// to prevent WASM crash. See box-in-text-validation.test.tsx.
|
||||
|
||||
@@ -67,6 +67,16 @@ const STYLE_PROPS = new Set([
|
||||
"overflowY",
|
||||
]);
|
||||
|
||||
/** Walk up the DOM tree to check if we're inside a text or virtual-text context. */
|
||||
function isInsideTextContext(node: TuiContainer): boolean {
|
||||
let current: TuiContainer | null = node;
|
||||
while (current) {
|
||||
if (current.type === "text" || current.type === "virtual-text") return true;
|
||||
current = current.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNode, TuiNode> {
|
||||
const { onCommit } = options;
|
||||
|
||||
@@ -132,6 +142,21 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
||||
}
|
||||
const parentC = parent as TuiContainer;
|
||||
|
||||
// Dev warning: <Box> inside <Text> is invalid (matches Ink's validation).
|
||||
// Inserting a box into a text context corrupts the yoga WASM layout engine.
|
||||
if (
|
||||
process.env["NODE_ENV"] !== "production" &&
|
||||
child.type === "box" &&
|
||||
isInsideTextContext(parentC)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"[vue-tui] A <Box> cannot be nested inside a <Text> component. " +
|
||||
"Wrap it in a sibling <Box> instead.",
|
||||
);
|
||||
return; // Skip insertion to prevent WASM crash
|
||||
}
|
||||
|
||||
// Move semantics: if the child is already mounted (Vue's keyed reorder
|
||||
// emits insert(existingChild, parent, newAnchor) without a prior remove),
|
||||
// detach it from its current DOM and yoga positions before re-inserting.
|
||||
|
||||
Reference in New Issue
Block a user