Skip to content

Commit 384dd22

Browse files
committed
fix: restore downstream compatibility for linked ink shims
1 parent 1f53742 commit 384dd22

6 files changed

Lines changed: 51 additions & 29 deletions

File tree

packages/ink-compat/src/__tests__/integration/basic.test.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useCursor } from "../../hooks/useCursor.js";
1313
import { useFocus } from "../../hooks/useFocus.js";
1414
import { useInput } from "../../hooks/useInput.js";
1515
import { useIsScreenReaderEnabled } from "../../hooks/useIsScreenReaderEnabled.js";
16+
import type { InkHostNode } from "../../reconciler/types.js";
1617
import { measureElement } from "../../runtime/measureElement.js";
1718
import { render as runtimeRender } from "../../runtime/render.js";
1819
import { render } from "../../testing/index.js";
@@ -457,12 +458,12 @@ test("runtime render populates __inkLayout and resolves percent widths", async (
457458
const stdout = new PassThrough();
458459
const stderr = new PassThrough();
459460

460-
let parentNode: unknown;
461-
let childNode: unknown;
461+
let parentNode: InkHostNode | null = null;
462+
let childNode: InkHostNode | null = null;
462463

463464
function App(): React.ReactElement {
464-
const parentRef = React.useRef<unknown>(null);
465-
const childRef = React.useRef<unknown>(null);
465+
const parentRef = React.useRef<InkHostNode | null>(null);
466+
const childRef = React.useRef<InkHostNode | null>(null);
466467

467468
useEffect(() => {
468469
parentNode = parentRef.current;
@@ -483,8 +484,10 @@ test("runtime render populates __inkLayout and resolves percent widths", async (
483484
const instance = runtimeRender(React.createElement(App), { stdin, stdout, stderr });
484485
try {
485486
await new Promise((resolve) => setTimeout(resolve, 40));
486-
assert.equal(measureElement(parentNode as never).width, 20);
487-
assert.equal(measureElement(childNode as never).width, 20);
487+
assert.ok(parentNode != null, "parent ref should be set");
488+
assert.ok(childNode != null, "child ref should be set");
489+
assert.equal(measureElement(parentNode).width, 20);
490+
assert.equal(measureElement(childNode).width, 20);
488491
} finally {
489492
instance.unmount();
490493
instance.cleanup();
@@ -497,18 +500,18 @@ test("runtime render resolves nested percent sizing from resolved parent layout"
497500
const stdout = new PassThrough();
498501
const stderr = new PassThrough();
499502

500-
let rowParentNode: unknown;
501-
let widthNode: unknown;
502-
let basisNode: unknown;
503-
let columnParentNode: unknown;
504-
let heightNode: unknown;
503+
let rowParentNode: InkHostNode | null = null;
504+
let widthNode: InkHostNode | null = null;
505+
let basisNode: InkHostNode | null = null;
506+
let columnParentNode: InkHostNode | null = null;
507+
let heightNode: InkHostNode | null = null;
505508

506509
function App(): React.ReactElement {
507-
const rowParentRef = React.useRef<unknown>(null);
508-
const widthRef = React.useRef<unknown>(null);
509-
const basisRef = React.useRef<unknown>(null);
510-
const columnParentRef = React.useRef<unknown>(null);
511-
const heightRef = React.useRef<unknown>(null);
510+
const rowParentRef = React.useRef<InkHostNode | null>(null);
511+
const widthRef = React.useRef<InkHostNode | null>(null);
512+
const basisRef = React.useRef<InkHostNode | null>(null);
513+
const columnParentRef = React.useRef<InkHostNode | null>(null);
514+
const heightRef = React.useRef<InkHostNode | null>(null);
512515

513516
useEffect(() => {
514517
rowParentNode = rowParentRef.current;
@@ -555,11 +558,16 @@ test("runtime render resolves nested percent sizing from resolved parent layout"
555558
const instance = runtimeRender(React.createElement(App), { stdin, stdout, stderr });
556559
try {
557560
await new Promise((resolve) => setTimeout(resolve, 60));
558-
assert.equal(measureElement(rowParentNode as never).width, 40);
559-
assert.equal(measureElement(widthNode as never).width, 20);
560-
assert.equal(measureElement(basisNode as never).width, 20);
561-
assert.equal(measureElement(columnParentNode as never).height, 12);
562-
assert.equal(measureElement(heightNode as never).height, 6);
561+
assert.ok(rowParentNode != null, "row parent ref should be set");
562+
assert.ok(widthNode != null, "width ref should be set");
563+
assert.ok(basisNode != null, "basis ref should be set");
564+
assert.ok(columnParentNode != null, "column parent ref should be set");
565+
assert.ok(heightNode != null, "height ref should be set");
566+
assert.equal(measureElement(rowParentNode).width, 40);
567+
assert.equal(measureElement(widthNode).width, 20);
568+
assert.equal(measureElement(basisNode).width, 20);
569+
assert.equal(measureElement(columnParentNode).height, 12);
570+
assert.equal(measureElement(heightNode).height, 6);
563571
} finally {
564572
instance.unmount();
565573
instance.cleanup();

packages/ink-compat/src/components/Box.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import type { InkHostNode } from "../reconciler/types.js";
23

34
type InkBorderStyleName =
45
| "single"
@@ -30,6 +31,8 @@ export interface BoxProps {
3031
width?: number | string;
3132
height?: number | string;
3233
minWidth?: number | string;
34+
/** @jrichman/ink fork: non-upstream extension used by Gemini CLI tab headers */
35+
maxWidth?: number | string;
3336
minHeight?: number | string;
3437
maxHeight?: number | string;
3538

@@ -100,7 +103,7 @@ export interface BoxProps {
100103
stickyChildren?: React.ReactNode;
101104

102105
children?: React.ReactNode;
103-
ref?: React.Ref<unknown>;
106+
ref?: React.Ref<InkHostNode>;
104107
}
105108

106109
export const Box = (props: BoxProps): React.ReactElement => {

packages/ink-compat/src/components/Text.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import type { InkHostNode } from "../reconciler/types.js";
23

34
export interface TextProps {
45
color?: string;
@@ -18,10 +19,20 @@ export interface TextProps {
1819
"aria-label"?: string;
1920
accessibilityLabel?: string;
2021
children?: React.ReactNode;
22+
ref?: React.Ref<InkHostNode>;
2123
}
2224

23-
export const Text = React.forwardRef<unknown, TextProps>((props, ref) => {
24-
return React.createElement("ink-text", { ...props, ref });
25-
});
25+
type TextComponent = ((props: TextProps) => React.ReactElement) & {
26+
displayName?: string;
27+
// Downstream test suites often cast Text to a Vitest Mock after vi.mock("ink").
28+
// Keeping these optional fields on the public type avoids TS2352 assertion failures.
29+
mock?: unknown;
30+
mockClear?: unknown;
31+
mockReset?: unknown;
32+
};
33+
34+
export const Text: TextComponent = (props: TextProps): React.ReactElement => {
35+
return React.createElement("ink-text", { ...props });
36+
};
2637

2738
Text.displayName = "Text";

packages/ink-compat/src/reconciler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface InkHostNode {
1212
parent: InkHostNode | null;
1313
/** Text content for text nodes (string children of <Text>) */
1414
textContent: string | null;
15+
/** Compatibility surface for libraries that expect Ink DOM elements to expose yogaNode. */
16+
yogaNode?: unknown;
1517
}
1618

1719
export interface InkHostContainer {

packages/ink-gradient-shim/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Text } from "ink";
21
/**
32
* ink-gradient shim.
43
* Applies a per-line multiline gradient and emits ANSI truecolor text.
@@ -149,7 +148,7 @@ const Gradient = ({ colors, children }) => {
149148
`render#${gradientTraceRenderCount} colors=${colorsLength} parsedStops=${parsedStops.length} textChars=${Array.from(plainText).length} emittedAnsi=${gradientText.includes("\u001b[38;2;")}`,
150149
);
151150
}, [colorsLength, parsedStops.length, plainText, gradientText]);
152-
return React.createElement(Text, null, gradientText);
151+
return React.createElement("ink-text", null, gradientText);
153152
};
154153

155154
export default Gradient;

packages/ink-spinner-shim/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Text } from "ink";
21
/**
32
* ink-spinner shim — drop-in replacement using ink compat primitives.
43
* Implements the "dots" spinner type used by Gemini CLI.
@@ -22,7 +21,7 @@ const Spinner = ({ type = "dots" }) => {
2221
return () => clearInterval(timer);
2322
}, [spinner]);
2423

25-
return React.createElement(Text, { color: "green" }, spinner.frames[frame]);
24+
return React.createElement("ink-text", { color: "green" }, spinner.frames[frame]);
2625
};
2726

2827
export default Spinner;

0 commit comments

Comments
 (0)