feat: fix types and add type checking plus lazybundle on launch andddd dev flag

This commit is contained in:
Brooklyn Nicholson 2026-04-11 14:42:28 -05:00
parent 5e5e65f6d5
commit 32302c37dd
34 changed files with 1807 additions and 977 deletions

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { type ReactNode } from 'react'
import { c as _c } from 'react/compiler-runtime'
import Link from './components/Link.js'
@ -6,7 +6,7 @@ import Text from './components/Text.js'
import type { Color } from './styles.js'
import { type NamedColor, Parser, type Color as TermioColor, type TextStyle } from './termio.js'
type Props = {
children: string
children?: ReactNode
/** When true, force all text to be rendered with dim styling */
dimColor?: boolean
}
@ -22,6 +22,11 @@ type SpanProps = {
hyperlink?: string
}
type Span = {
text: string
props: SpanProps
}
/**
* Component that parses ANSI escape codes and renders them using Text components.
*
@ -30,7 +35,7 @@ type SpanProps = {
*
* Memoized to prevent re-renders when parent changes but children string is the same.
*/
export const Ansi = React.memo(function Ansi(t0) {
export const Ansi = React.memo(function Ansi(t0: Props) {
const $ = _c(12)
const { children, dimColor } = t0
@ -78,7 +83,7 @@ export const Ansi = React.memo(function Ansi(t0) {
let t3
if ($[7] !== dimColor) {
t3 = (span, i) => {
t3 = (span: Span, i: number) => {
const hyperlink = span.props.hyperlink
if (dimColor) {
@ -165,10 +170,6 @@ export const Ansi = React.memo(function Ansi(t0) {
return t3
})
type Span = {
text: string
props: SpanProps
}
/**
* Parse an ANSI string into spans using the termio parser.
@ -359,7 +360,7 @@ type BaseTextStyleProps = {
}
// Wrapper component that handles bold/dim mutual exclusivity for Text
function StyledText(t0) {
function StyledText(t0: BaseTextStyleProps & { bold?: boolean; dim?: boolean; children?: ReactNode }) {
const $ = _c(14)
let bold
let children

View file

@ -32,7 +32,7 @@ type Props = PropsWithChildren<{
* from scrolling content) and so signal-exit cleanup can exit the alt
* screen if the component's own unmount doesn't run.
*/
export function AlternateScreen(t0) {
export function AlternateScreen(t0: Props) {
const $ = _c(7)
const { children, mouseTracking: t1 } = t0

View file

@ -1,6 +1,6 @@
import '../global.d.ts'
import React, { type Ref } from 'react'
import React, { type ReactNode, type Ref } from 'react'
import { c as _c } from 'react/compiler-runtime'
import type { Except } from 'type-fest'
@ -11,6 +11,7 @@ import type { KeyboardEvent } from '../events/keyboard-event.js'
import type { Styles } from '../styles.js'
import * as warn from '../warn.js'
export type Props = Except<Styles, 'textWrap'> & {
children?: ReactNode
ref?: Ref<DOMElement>
/**
* Tab order index. Nodes with `tabIndex >= 0` participate in
@ -50,7 +51,7 @@ export type Props = Except<Styles, 'textWrap'> & {
/**
* `<Box>` is an essential Ink component to build your layout. It's like `<div style="display: flex">` in the browser.
*/
function Box(t0) {
function Box(t0: Props) {
const $ = _c(42)
let autoFocus
let children

View file

@ -1,4 +1,4 @@
import React, { createContext, useEffect, useState } from 'react'
import React, { createContext, type ReactNode, useEffect, useState } from 'react'
import { c as _c } from 'react/compiler-runtime'
import { BLURRED_FRAME_INTERVAL_MS, FRAME_INTERVAL_MS } from '../constants.js'
@ -87,7 +87,7 @@ export const ClockContext = createContext<Clock | null>(null)
// Own component so App.tsx doesn't re-render when the clock is created.
// The clock value is stable (created once via useState), so the provider
// never causes consumer re-renders on its own.
export function ClockProvider(t0) {
export function ClockProvider(t0: { readonly children: ReactNode }) {
const $ = _c(7)
const { children } = t0

View file

@ -11,7 +11,7 @@ export type Props = {
readonly fallback?: ReactNode
}
export default function Link(t0) {
export default function Link(t0: Props) {
const $ = _c(5)
const { children, url, fallback } = t0

View file

@ -12,7 +12,7 @@ export type Props = {
/**
* Adds one or more newline (\n) characters. Must be used within <Text> components.
*/
export default function Newline(t0) {
export default function Newline(t0: Props) {
const $ = _c(4)
const { count: t1 } = t0

View file

@ -33,7 +33,7 @@ type Props = Omit<BoxProps, 'noSelect'> & {
* tracking). No-op in the main-screen scrollback render where the
* terminal's native selection is used instead.
*/
export function NoSelect(t0) {
export function NoSelect(t0: Props) {
const $ = _c(8)
let boxProps
let children

View file

@ -25,7 +25,7 @@ type Props = {
* (width × lines.length) and hands the joined string straight to output.write(),
* which already splits on '\n' and parses ANSI into the screen buffer.
*/
export function RawAnsi(t0) {
export function RawAnsi(t0: Props) {
const $ = _c(6)
const { lines, width } = t0

View file

@ -252,7 +252,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
// commit, which is too late for the first frame.
return (
<ink-box
ref={el => {
ref={(el: DOMElement | null) => {
domRef.current = el
if (el) {

View file

@ -1,4 +1,4 @@
import React, { createContext, useSyncExternalStore } from 'react'
import React, { createContext, type ReactNode, useSyncExternalStore } from 'react'
import { c as _c } from 'react/compiler-runtime'
import {
@ -23,7 +23,7 @@ TerminalFocusContext.displayName = 'TerminalFocusContext'
// Separate component so App.tsx doesn't re-render on focus changes.
// Children are a stable prop reference, so they don't re-render either —
// only components that consume the context will re-render.
export function TerminalFocusProvider(t0) {
export function TerminalFocusProvider(t0: { readonly children: ReactNode }) {
const $ = _c(6)
const { children } = t0

View file

@ -116,7 +116,7 @@ const memoizedStylesForWrap: Record<NonNullable<Styles['textWrap']>, Styles> = {
/**
* This component can display text, and change its style to make it colorful, bold, underline, italic or strikethrough.
*/
export default function Text(t0) {
export default function Text(t0: Props) {
const $ = _c(29)
const {

View file

@ -0,0 +1,2 @@
/** Optional react-devtools hook; package may be absent. */
export {}

View file

@ -0,0 +1,10 @@
import { TerminalEvent } from './terminal-event.js'
export class PasteEvent extends TerminalEvent {
readonly text: string
constructor(text: string) {
super('paste', { bubbles: true, cancelable: true })
this.text = text
}
}

View file

@ -0,0 +1,12 @@
import { TerminalEvent } from './terminal-event.js'
export class ResizeEvent extends TerminalEvent {
readonly columns: number
readonly rows: number
constructor(columns: number, rows: number) {
super('resize', { bubbles: true, cancelable: true })
this.columns = columns
this.rows = rows
}
}

View file

@ -339,8 +339,6 @@ export default class Ink {
}
}
// @ts-expect-error @types/react-reconciler@0.32.3 declares 11 args with transitionCallbacks,
// but react-reconciler 0.33.0 source only accepts 10 args (no transitionCallbacks)
this.container = reconciler.createContainer(
this.rootNode,
ConcurrentRoot,
@ -357,7 +355,7 @@ export default class Ink {
noop // onDefaultTransitionIndicator
)
if ('production' === 'development') {
if (process.env.NODE_ENV === 'development') {
reconciler.injectIntoDevTools({
bundleType: 0,
// Reporting React DOM's version, not Ink's
@ -955,7 +953,6 @@ export default class Ink {
}
pause(): void {
// Flush pending React updates and render before pausing.
// @ts-expect-error flushSyncFromReconciler exists in react-reconciler 0.31 but not in @types/react-reconciler
reconciler.flushSyncFromReconciler()
this.onRender()
this.isPaused = true
@ -1783,9 +1780,7 @@ export default class Ink {
</App>
)
// @ts-expect-error updateContainerSync exists in react-reconciler but not in @types/react-reconciler
reconciler.updateContainerSync(tree, this.container, null, noop)
// @ts-expect-error flushSyncWork exists in react-reconciler but not in @types/react-reconciler
reconciler.flushSyncWork()
}
unmount(error?: Error | number | null): void {
@ -1857,9 +1852,7 @@ export default class Ink {
this.drainTimer = null
}
// @ts-expect-error updateContainerSync exists in react-reconciler but not in @types/react-reconciler
reconciler.updateContainerSync(null, this.container, null, noop)
// @ts-expect-error flushSyncWork exists in react-reconciler but not in @types/react-reconciler
reconciler.flushSyncWork()
instances.delete(this.options.stdout)
@ -1966,8 +1959,8 @@ export default class Ink {
const intercept = (
chunk: Uint8Array | string,
encodingOrCb?: BufferEncoding | ((err?: Error) => void),
cb?: (err?: Error) => void
encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
cb?: (err?: Error | null) => void
): boolean => {
const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb

View file

@ -176,27 +176,12 @@ export function resetProfileCounters(): void {
}
// --- END ---
const reconciler = createReconciler<
ElementNames,
Props,
DOMElement,
DOMElement,
TextNode,
DOMElement,
unknown,
unknown,
DOMElement,
HostContext,
null, // UpdatePayload - not used in React 19
NodeJS.Timeout,
-1,
null
>({
const reconciler = createReconciler({
getRootHostContext: () => ({ isInsideText: false }),
prepareForCommit: () => null,
preparePortalMount: () => null,
clearContainer: () => false,
resetAfterCommit(rootNode) {
resetAfterCommit(rootNode: DOMElement) {
_lastCommitMs = _commitStart > 0 ? performance.now() - _commitStart : 0
_commitStart = 0
@ -261,19 +246,19 @@ const reconciler = createReconciler<
return createTextNode(text)
},
resetTextContent() {},
hideTextInstance(node) {
hideTextInstance(node: TextNode) {
setTextNodeValue(node, '')
},
unhideTextInstance(node, text) {
unhideTextInstance(node: TextNode, text: string) {
setTextNodeValue(node, text)
},
getPublicInstance: (instance): DOMElement => instance as DOMElement,
hideInstance(node) {
getPublicInstance: (instance: DOMElement): DOMElement => instance,
hideInstance(node: DOMElement) {
node.isHidden = true
node.yogaNode?.setDisplay(LayoutDisplay.None)
markDirty(node)
},
unhideInstance(node) {
unhideInstance(node: DOMElement) {
node.isHidden = false
node.yogaNode?.setDisplay(LayoutDisplay.Flex)
markDirty(node)
@ -344,7 +329,7 @@ const reconciler = createReconciler<
commitTextUpdate(node: TextNode, _oldText: string, newText: string): void {
setTextNodeValue(node, newText)
},
removeChild(node, removeNode) {
removeChild(node: DOMElement, removeNode: DOMElement | TextNode) {
removeChildNode(node, removeNode)
cleanupYogaNode(removeNode)

View file

@ -63,14 +63,11 @@ export function renderToScreen(el: ReactElement, width: number): { screen: Scree
stylePool = new StylePool()
charPool = new CharPool()
hyperlinkPool = new HyperlinkPool()
// @ts-expect-error react-reconciler 0.33 takes 10 args; @types says 11
container = reconciler.createContainer(root, LegacyRoot, null, false, null, 'search-render', noop, noop, noop, noop)
}
const t0 = performance.now()
// @ts-expect-error updateContainerSync exists but not in @types
reconciler.updateContainerSync(el, container, null, noop)
// @ts-expect-error flushSyncWork exists but not in @types
reconciler.flushSyncWork()
const t1 = performance.now()
@ -105,9 +102,7 @@ export function renderToScreen(el: ReactElement, width: number): { screen: Scree
const t3 = performance.now()
// Unmount so next call gets a fresh tree. Leaves root/container/pools.
// @ts-expect-error updateContainerSync exists but not in @types
reconciler.updateContainerSync(null, container, null, noop)
// @ts-expect-error flushSyncWork exists but not in @types
reconciler.flushSyncWork()
timing.reconcile += t1 - t0