Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
15 kB · 512 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513/* eslint-disable custom-rules/no-top-level-side-effects */
import { appendFileSync } from 'fs'import createReconciler from 'react-reconciler'import { getYogaCounters } from 'src/native-ts/yoga-layout/index.js'import { isEnvTruthy } from '../utils/envUtils.js'import { appendChildNode, clearYogaNodeReferences, createNode, createTextNode, type DOMElement, type DOMNodeAttribute, type ElementNames, insertBeforeNode, markDirty, removeChildNode, setAttribute, setStyle, setTextNodeValue, setTextStyles, type TextNode,} from './dom.js'import { Dispatcher } from './events/dispatcher.js'import { EVENT_HANDLER_PROPS } from './events/event-handlers.js'import { getFocusManager, getRootNode } from './focus.js'import { LayoutDisplay } from './layout/node.js'import applyStyles, { type Styles, type TextStyles } from './styles.js'
// We need to conditionally perform devtools connection to avoid// accidentally breaking other third-party code.// See https://github.com/vadimdemedes/ink/issues/384if (process.env.NODE_ENV === 'development') { try { // eslint-disable-next-line custom-rules/no-top-level-dynamic-import -- dev-only; NODE_ENV check is DCE'd in production void import('./devtools.js') // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { if (error.code === 'ERR_MODULE_NOT_FOUND') { // biome-ignore lint/suspicious/noConsole: intentional warning console.warn( `The environment variable DEV is set to true, so Ink tried to import \`react-devtools-core\`,but this failed as it was not installed. Debugging with React Devtools requires it.
To install use this command:
$ npm install --save-dev react-devtools-core `.trim() + '\n', ) } else { // eslint-disable-next-line @typescript-eslint/only-throw-error throw error } }}
// --
type AnyObject = Record<string, unknown>
const diff = (before: AnyObject, after: AnyObject): AnyObject | undefined => { if (before === after) { return }
if (!before) { return after }
const changed: AnyObject = {} let isChanged = false
for (const key of Object.keys(before)) { const isDeleted = after ? !Object.hasOwn(after, key) : true
if (isDeleted) { changed[key] = undefined isChanged = true } }
if (after) { for (const key of Object.keys(after)) { if (after[key] !== before[key]) { changed[key] = after[key] isChanged = true } } }
return isChanged ? changed : undefined}
const cleanupYogaNode = (node: DOMElement | TextNode): void => { const yogaNode = node.yogaNode if (yogaNode) { yogaNode.unsetMeasureFunc() // Clear all references BEFORE freeing to prevent other code from // accessing freed WASM memory during concurrent operations clearYogaNodeReferences(node) yogaNode.freeRecursive() }}
// --
type Props = Record<string, unknown>
type HostContext = { isInsideText: boolean}
function setEventHandler(node: DOMElement, key: string, value: unknown): void { if (!node._eventHandlers) { node._eventHandlers = {} } node._eventHandlers[key] = value}
function applyProp(node: DOMElement, key: string, value: unknown): void { if (key === 'children') return
if (key === 'style') { setStyle(node, value as Styles) if (node.yogaNode) { applyStyles(node.yogaNode, value as Styles) } return }
if (key === 'textStyles') { node.textStyles = value as TextStyles return }
if (EVENT_HANDLER_PROPS.has(key)) { setEventHandler(node, key, value) return }
setAttribute(node, key, value as DOMNodeAttribute)}
// --
// react-reconciler's Fiber shape — only the fields we walk. The 5th arg to// createInstance is the Fiber (`workInProgress` in react-reconciler.dev.js).// _debugOwner is the component that rendered this element (dev builds only);// return is the parent fiber (always present). We prefer _debugOwner since it// skips past Box/Text wrappers to the actual named component.type FiberLike = { elementType?: { displayName?: string; name?: string } | string | null _debugOwner?: FiberLike | null return?: FiberLike | null}
export function getOwnerChain(fiber: unknown): string[] { const chain: string[] = [] const seen = new Set<unknown>() let cur = fiber as FiberLike | null | undefined for (let i = 0; cur && i < 50; i++) { if (seen.has(cur)) break seen.add(cur) const t = cur.elementType const name = typeof t === 'function' ? (t as { displayName?: string; name?: string }).displayName || (t as { displayName?: string; name?: string }).name : typeof t === 'string' ? undefined // host element (ink-box etc) — skip : t?.displayName || t?.name if (name && name !== chain[chain.length - 1]) chain.push(name) cur = cur._debugOwner ?? cur.return } return chain}
let debugRepaints: boolean | undefinedexport function isDebugRepaintsEnabled(): boolean { if (debugRepaints === undefined) { debugRepaints = isEnvTruthy(process.env.CLAUDE_CODE_DEBUG_REPAINTS) } return debugRepaints}
export const dispatcher = new Dispatcher()
// --- COMMIT INSTRUMENTATION (temp debugging) ---// eslint-disable-next-line custom-rules/no-process-env-top-level -- debug instrumentation, read-once is fineconst COMMIT_LOG = process.env.CLAUDE_CODE_COMMIT_LOGlet _commits = 0let _lastLog = 0let _lastCommitAt = 0let _maxGapMs = 0let _createCount = 0let _prepareAt = 0// --- END ---
// --- SCROLL PROFILING (bench/scroll-e2e.sh reads via getLastYogaMs) ---// Set by onComputeLayout wrapper in ink.tsx; read by onRender for phases.let _lastYogaMs = 0let _lastCommitMs = 0let _commitStart = 0export function recordYogaMs(ms: number): void { _lastYogaMs = ms}export function getLastYogaMs(): number { return _lastYogaMs}export function markCommitStart(): void { _commitStart = performance.now()}export function getLastCommitMs(): number { return _lastCommitMs}export function resetProfileCounters(): void { _lastYogaMs = 0 _lastCommitMs = 0 _commitStart = 0}// --- 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>({ getRootHostContext: () => ({ isInsideText: false }), prepareForCommit: () => { if (COMMIT_LOG) _prepareAt = performance.now() return null }, preparePortalMount: () => null, clearContainer: () => false, resetAfterCommit(rootNode) { _lastCommitMs = _commitStart > 0 ? performance.now() - _commitStart : 0 _commitStart = 0 if (COMMIT_LOG) { const now = performance.now() _commits++ const gap = _lastCommitAt > 0 ? now - _lastCommitAt : 0 if (gap > _maxGapMs) _maxGapMs = gap _lastCommitAt = now const reconcileMs = _prepareAt > 0 ? now - _prepareAt : 0 if (gap > 30 || reconcileMs > 20 || _createCount > 50) { // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation appendFileSync( COMMIT_LOG, `${now.toFixed(1)} gap=${gap.toFixed(1)}ms reconcile=${reconcileMs.toFixed(1)}ms creates=${_createCount}\n`, ) } _createCount = 0 if (now - _lastLog > 1000) { // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation appendFileSync( COMMIT_LOG, `${now.toFixed(1)} commits=${_commits}/s maxGap=${_maxGapMs.toFixed(1)}ms\n`, ) _commits = 0 _maxGapMs = 0 _lastLog = now } } const _t0 = COMMIT_LOG ? performance.now() : 0 if (typeof rootNode.onComputeLayout === 'function') { rootNode.onComputeLayout() } if (COMMIT_LOG) { const layoutMs = performance.now() - _t0 if (layoutMs > 20) { const c = getYogaCounters() // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation appendFileSync( COMMIT_LOG, `${_t0.toFixed(1)} SLOW_YOGA ${layoutMs.toFixed(1)}ms visited=${c.visited} measured=${c.measured} hits=${c.cacheHits} live=${c.live}\n`, ) } }
if (process.env.NODE_ENV === 'test') { if (rootNode.childNodes.length === 0 && rootNode.hasRenderedContent) { return } if (rootNode.childNodes.length > 0) { rootNode.hasRenderedContent = true } rootNode.onImmediateRender?.() return }
const _tr = COMMIT_LOG ? performance.now() : 0 rootNode.onRender?.() if (COMMIT_LOG) { const renderMs = performance.now() - _tr if (renderMs > 10) { // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation appendFileSync( COMMIT_LOG, `${_tr.toFixed(1)} SLOW_PAINT ${renderMs.toFixed(1)}ms\n`, ) } } }, getChildHostContext( parentHostContext: HostContext, type: ElementNames, ): HostContext { const previousIsInsideText = parentHostContext.isInsideText const isInsideText = type === 'ink-text' || type === 'ink-virtual-text' || type === 'ink-link'
if (previousIsInsideText === isInsideText) { return parentHostContext }
return { isInsideText } }, shouldSetTextContent: () => false, createInstance( originalType: ElementNames, newProps: Props, _root: DOMElement, hostContext: HostContext, internalHandle?: unknown, ): DOMElement { if (hostContext.isInsideText && originalType === 'ink-box') { throw new Error(`<Box> can't be nested inside <Text> component`) }
const type = originalType === 'ink-text' && hostContext.isInsideText ? 'ink-virtual-text' : originalType
const node = createNode(type) if (COMMIT_LOG) _createCount++
for (const [key, value] of Object.entries(newProps)) { applyProp(node, key, value) }
if (isDebugRepaintsEnabled()) { node.debugOwnerChain = getOwnerChain(internalHandle) }
return node }, createTextInstance( text: string, _root: DOMElement, hostContext: HostContext, ): TextNode { if (!hostContext.isInsideText) { throw new Error( `Text string "${text}" must be rendered inside <Text> component`, ) }
return createTextNode(text) }, resetTextContent() {}, hideTextInstance(node) { setTextNodeValue(node, '') }, unhideTextInstance(node, text) { setTextNodeValue(node, text) }, getPublicInstance: (instance): DOMElement => instance as DOMElement, hideInstance(node) { node.isHidden = true node.yogaNode?.setDisplay(LayoutDisplay.None) markDirty(node) }, unhideInstance(node) { node.isHidden = false node.yogaNode?.setDisplay(LayoutDisplay.Flex) markDirty(node) }, appendInitialChild: appendChildNode, appendChild: appendChildNode, insertBefore: insertBeforeNode, finalizeInitialChildren( _node: DOMElement, _type: ElementNames, props: Props, ): boolean { return props['autoFocus'] === true }, commitMount(node: DOMElement): void { getFocusManager(node).handleAutoFocus(node) }, isPrimaryRenderer: true, supportsMutation: true, supportsPersistence: false, supportsHydration: false, scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, getCurrentUpdatePriority: () => dispatcher.currentUpdatePriority, beforeActiveInstanceBlur() {}, afterActiveInstanceBlur() {}, detachDeletedInstance() {}, getInstanceFromNode: () => null, prepareScopeUpdate() {}, getInstanceFromScope: () => null, appendChildToContainer: appendChildNode, insertInContainerBefore: insertBeforeNode, removeChildFromContainer(node: DOMElement, removeNode: DOMElement): void { removeChildNode(node, removeNode) cleanupYogaNode(removeNode) getFocusManager(node).handleNodeRemoved(removeNode, node) }, // React 19 commitUpdate receives old and new props directly instead of an updatePayload commitUpdate( node: DOMElement, _type: ElementNames, oldProps: Props, newProps: Props, ): void { const props = diff(oldProps, newProps) const style = diff(oldProps['style'] as Styles, newProps['style'] as Styles)
if (props) { for (const [key, value] of Object.entries(props)) { if (key === 'style') { setStyle(node, value as Styles) continue }
if (key === 'textStyles') { setTextStyles(node, value as TextStyles) continue }
if (EVENT_HANDLER_PROPS.has(key)) { setEventHandler(node, key, value) continue }
setAttribute(node, key, value as DOMNodeAttribute) } }
if (style && node.yogaNode) { applyStyles(node.yogaNode, style, newProps['style'] as Styles) } }, commitTextUpdate(node: TextNode, _oldText: string, newText: string): void { setTextNodeValue(node, newText) }, removeChild(node, removeNode) { removeChildNode(node, removeNode) cleanupYogaNode(removeNode) if (removeNode.nodeName !== '#text') { const root = getRootNode(node) root.focusManager!.handleNodeRemoved(removeNode, root) } }, // React 19 required methods maySuspendCommit(): boolean { return false }, preloadInstance(): boolean { return true }, startSuspendingCommit(): void {}, suspendInstance(): void {}, waitForCommitToBeReady(): null { return null }, NotPendingTransition: null, HostTransitionContext: { $$typeof: Symbol.for('react.context'), _currentValue: null, } as never, setCurrentUpdatePriority(newPriority: number): void { dispatcher.currentUpdatePriority = newPriority }, resolveUpdatePriority(): number { return dispatcher.resolveEventPriority() }, resetFormInstance(): void {}, requestPostPaintCallback(): void {}, shouldAttemptEagerTransition(): boolean { return false }, trackSchedulerEvent(): void {}, resolveEventType(): string | null { return dispatcher.currentEvent?.type ?? null }, resolveEventTimeStamp(): number { return dispatcher.currentEvent?.timeStamp ?? -1.1 },})
// Wire the reconciler's discreteUpdates into the dispatcher.// This breaks the import cycle: dispatcher.ts doesn't import reconciler.ts.dispatcher.discreteUpdates = reconciler.discreteUpdates.bind(reconciler)
export default reconciler