source dump of claude code
at main 184 lines 4.6 kB view raw
1import type { ReactNode } from 'react' 2import { logForDebugging } from 'src/utils/debug.js' 3import { Stream } from 'stream' 4import type { FrameEvent } from './frame.js' 5import Ink, { type Options as InkOptions } from './ink.js' 6import instances from './instances.js' 7 8export type RenderOptions = { 9 /** 10 * Output stream where app will be rendered. 11 * 12 * @default process.stdout 13 */ 14 stdout?: NodeJS.WriteStream 15 /** 16 * Input stream where app will listen for input. 17 * 18 * @default process.stdin 19 */ 20 stdin?: NodeJS.ReadStream 21 /** 22 * Error stream. 23 * @default process.stderr 24 */ 25 stderr?: NodeJS.WriteStream 26 /** 27 * Configure whether Ink should listen to Ctrl+C keyboard input and exit the app. This is needed in case `process.stdin` is in raw mode, because then Ctrl+C is ignored by default and process is expected to handle it manually. 28 * 29 * @default true 30 */ 31 exitOnCtrlC?: boolean 32 33 /** 34 * Patch console methods to ensure console output doesn't mix with Ink output. 35 * 36 * @default true 37 */ 38 patchConsole?: boolean 39 40 /** 41 * Called after each frame render with timing and flicker information. 42 */ 43 onFrame?: (event: FrameEvent) => void 44} 45 46export type Instance = { 47 /** 48 * Replace previous root node with a new one or update props of the current root node. 49 */ 50 rerender: Ink['render'] 51 /** 52 * Manually unmount the whole Ink app. 53 */ 54 unmount: Ink['unmount'] 55 /** 56 * Returns a promise, which resolves when app is unmounted. 57 */ 58 waitUntilExit: Ink['waitUntilExit'] 59 cleanup: () => void 60} 61 62/** 63 * A managed Ink root, similar to react-dom's createRoot API. 64 * Separates instance creation from rendering so the same root 65 * can be reused for multiple sequential screens. 66 */ 67export type Root = { 68 render: (node: ReactNode) => void 69 unmount: () => void 70 waitUntilExit: () => Promise<void> 71} 72 73/** 74 * Mount a component and render the output. 75 */ 76export const renderSync = ( 77 node: ReactNode, 78 options?: NodeJS.WriteStream | RenderOptions, 79): Instance => { 80 const opts = getOptions(options) 81 const inkOptions: InkOptions = { 82 stdout: process.stdout, 83 stdin: process.stdin, 84 stderr: process.stderr, 85 exitOnCtrlC: true, 86 patchConsole: true, 87 ...opts, 88 } 89 90 const instance: Ink = getInstance( 91 inkOptions.stdout, 92 () => new Ink(inkOptions), 93 ) 94 95 instance.render(node) 96 97 return { 98 rerender: instance.render, 99 unmount() { 100 instance.unmount() 101 }, 102 waitUntilExit: instance.waitUntilExit, 103 cleanup: () => instances.delete(inkOptions.stdout), 104 } 105} 106 107const wrappedRender = async ( 108 node: ReactNode, 109 options?: NodeJS.WriteStream | RenderOptions, 110): Promise<Instance> => { 111 // Preserve the microtask boundary that `await loadYoga()` used to provide. 112 // Without it, the first render fires synchronously before async startup work 113 // (e.g. useReplBridge notification state) settles, and the subsequent Static 114 // write overwrites scrollback instead of appending below the logo. 115 await Promise.resolve() 116 const instance = renderSync(node, options) 117 logForDebugging( 118 `[render] first ink render: ${Math.round(process.uptime() * 1000)}ms since process start`, 119 ) 120 return instance 121} 122 123export default wrappedRender 124 125/** 126 * Create an Ink root without rendering anything yet. 127 * Like react-dom's createRoot — call root.render() to mount a tree. 128 */ 129export async function createRoot({ 130 stdout = process.stdout, 131 stdin = process.stdin, 132 stderr = process.stderr, 133 exitOnCtrlC = true, 134 patchConsole = true, 135 onFrame, 136}: RenderOptions = {}): Promise<Root> { 137 // See wrappedRender — preserve microtask boundary from the old WASM await. 138 await Promise.resolve() 139 const instance = new Ink({ 140 stdout, 141 stdin, 142 stderr, 143 exitOnCtrlC, 144 patchConsole, 145 onFrame, 146 }) 147 148 // Register in the instances map so that code that looks up the Ink 149 // instance by stdout (e.g. external editor pause/resume) can find it. 150 instances.set(stdout, instance) 151 152 return { 153 render: node => instance.render(node), 154 unmount: () => instance.unmount(), 155 waitUntilExit: () => instance.waitUntilExit(), 156 } 157} 158 159const getOptions = ( 160 stdout: NodeJS.WriteStream | RenderOptions | undefined = {}, 161): RenderOptions => { 162 if (stdout instanceof Stream) { 163 return { 164 stdout, 165 stdin: process.stdin, 166 } 167 } 168 169 return stdout 170} 171 172const getInstance = ( 173 stdout: NodeJS.WriteStream, 174 createInstance: () => Ink, 175): Ink => { 176 let instance = instances.get(stdout) 177 178 if (!instance) { 179 instance = createInstance() 180 instances.set(stdout, instance) 181 } 182 183 return instance 184}