staticRender.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { c as _c } from "react/compiler-runtime";
  2. import * as React from 'react';
  3. import { useLayoutEffect } from 'react';
  4. import { PassThrough } from 'stream';
  5. import stripAnsi from 'strip-ansi';
  6. import { render, useApp } from '../ink.js';
  7. // This is a workaround for the fact that Ink doesn't support multiple <Static>
  8. // components in the same render tree. Instead of using a <Static> we just render
  9. // the component to a string and then print it to stdout
  10. /**
  11. * Wrapper component that exits after rendering.
  12. * Uses useLayoutEffect to ensure we wait for React's commit phase to complete
  13. * before exiting. This is more robust than process.nextTick() for React 19's
  14. * async render cycle.
  15. */
  16. function RenderOnceAndExit(t0) {
  17. const $ = _c(5);
  18. const {
  19. children
  20. } = t0;
  21. const {
  22. exit
  23. } = useApp();
  24. let t1;
  25. let t2;
  26. if ($[0] !== exit) {
  27. t1 = () => {
  28. const timer = setTimeout(exit, 0);
  29. return () => clearTimeout(timer);
  30. };
  31. t2 = [exit];
  32. $[0] = exit;
  33. $[1] = t1;
  34. $[2] = t2;
  35. } else {
  36. t1 = $[1];
  37. t2 = $[2];
  38. }
  39. useLayoutEffect(t1, t2);
  40. let t3;
  41. if ($[3] !== children) {
  42. t3 = <>{children}</>;
  43. $[3] = children;
  44. $[4] = t3;
  45. } else {
  46. t3 = $[4];
  47. }
  48. return t3;
  49. }
  50. // DEC synchronized update markers used by terminals
  51. const SYNC_START = '\x1B[?2026h';
  52. const SYNC_END = '\x1B[?2026l';
  53. /**
  54. * Extracts content from the first complete frame in Ink's output.
  55. * Ink with non-TTY stdout outputs multiple frames, each wrapped in DEC synchronized
  56. * update sequences ([?2026h ... [?2026l). We only want the first frame's content.
  57. */
  58. function extractFirstFrame(output: string): string {
  59. const startIndex = output.indexOf(SYNC_START);
  60. if (startIndex === -1) return output;
  61. const contentStart = startIndex + SYNC_START.length;
  62. const endIndex = output.indexOf(SYNC_END, contentStart);
  63. if (endIndex === -1) return output;
  64. return output.slice(contentStart, endIndex);
  65. }
  66. /**
  67. * Renders a React node to a string with ANSI escape codes (for terminal output).
  68. */
  69. export function renderToAnsiString(node: React.ReactNode, columns?: number): Promise<string> {
  70. return new Promise(async resolve => {
  71. let output = '';
  72. // Capture all writes. Set .columns so Ink (ink.tsx:~165) picks up a
  73. // chosen width instead of PassThrough's undefined → 80 fallback —
  74. // useful for rendering at terminal width for file dumps that should
  75. // match what the user sees on screen.
  76. const stream = new PassThrough();
  77. if (columns !== undefined) {
  78. ;
  79. (stream as unknown as {
  80. columns: number;
  81. }).columns = columns;
  82. }
  83. stream.on('data', chunk => {
  84. output += chunk.toString();
  85. });
  86. // Render the component wrapped in RenderOnceAndExit
  87. // Non-TTY stdout (PassThrough) gives full-frame output instead of diffs
  88. const instance = await render(<RenderOnceAndExit>{node}</RenderOnceAndExit>, {
  89. stdout: stream as unknown as NodeJS.WriteStream,
  90. patchConsole: false
  91. });
  92. // Wait for the component to exit naturally
  93. await instance.waitUntilExit();
  94. // Extract only the first frame's content to avoid duplication
  95. // (Ink outputs multiple frames in non-TTY mode)
  96. await resolve(extractFirstFrame(output));
  97. });
  98. }
  99. /**
  100. * Renders a React node to a plain text string (ANSI codes stripped).
  101. */
  102. export async function renderToString(node: React.ReactNode, columns?: number): Promise<string> {
  103. const output = await renderToAnsiString(node, columns);
  104. return stripAnsi(output);
  105. }