ink 6.7.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/build/ansi-tokenizer.d.ts +38 -0
  2. package/build/ansi-tokenizer.js +316 -0
  3. package/build/ansi-tokenizer.js.map +1 -0
  4. package/build/components/App.d.ts +1 -1
  5. package/build/components/App.js +60 -29
  6. package/build/components/App.js.map +1 -1
  7. package/build/components/AppContext.d.ts +5 -1
  8. package/build/components/AppContext.js.map +1 -1
  9. package/build/components/Cursor.d.ts +83 -0
  10. package/build/components/Cursor.js +53 -0
  11. package/build/components/Cursor.js.map +1 -0
  12. package/build/dom.js +5 -4
  13. package/build/dom.js.map +1 -1
  14. package/build/index.d.ts +2 -0
  15. package/build/index.js +1 -0
  16. package/build/index.js.map +1 -1
  17. package/build/ink.d.ts +7 -3
  18. package/build/ink.js +151 -50
  19. package/build/ink.js.map +1 -1
  20. package/build/input-parser.d.ts +7 -0
  21. package/build/input-parser.js +154 -0
  22. package/build/input-parser.js.map +1 -0
  23. package/build/layout.d.ts +7 -0
  24. package/build/layout.js +33 -0
  25. package/build/layout.js.map +1 -0
  26. package/build/output.d.ts +1 -0
  27. package/build/output.js +38 -5
  28. package/build/output.js.map +1 -1
  29. package/build/reconciler.js +12 -2
  30. package/build/reconciler.js.map +1 -1
  31. package/build/render-to-string.d.ts +38 -0
  32. package/build/render-to-string.js +115 -0
  33. package/build/render-to-string.js.map +1 -0
  34. package/build/render.d.ts +12 -1
  35. package/build/render.js.map +1 -1
  36. package/build/sanitize-ansi.d.ts +2 -0
  37. package/build/sanitize-ansi.js +27 -0
  38. package/build/sanitize-ansi.js.map +1 -0
  39. package/build/squash-text-nodes.js +2 -1
  40. package/build/squash-text-nodes.js.map +1 -1
  41. package/build/utils.d.ts +2 -0
  42. package/build/utils.js +4 -0
  43. package/build/utils.js.map +1 -0
  44. package/package.json +10 -7
  45. package/readme.md +125 -13
@@ -0,0 +1,115 @@
1
+ import Yoga from 'yoga-layout';
2
+ import { LegacyRoot } from 'react-reconciler/constants.js';
3
+ import reconciler from './reconciler.js';
4
+ import renderer from './renderer.js';
5
+ import { createNode } from './dom.js';
6
+ /**
7
+ Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.
8
+
9
+ Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.
10
+
11
+ **Notes:**
12
+
13
+ - Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
14
+ - `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
15
+ - `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
16
+ - The `<Static>` component is supported — its output is prepended to the dynamic output.
17
+ - If a component throws during rendering, the error is propagated to the caller after cleanup.
18
+
19
+ @example
20
+ ```
21
+ import {renderToString, Text, Box} from 'ink';
22
+
23
+ const output = renderToString(
24
+ <Box padding={1}>
25
+ <Text color="green">Hello World</Text>
26
+ </Box>,
27
+ {columns: 40}
28
+ );
29
+
30
+ console.log(output);
31
+ ```
32
+ */
33
+ const renderToString = (node, options) => {
34
+ const columns = options?.columns ?? 80;
35
+ // Create a standalone root node — no stdout, stdin, or terminal bindings
36
+ const rootNode = createNode('ink-root');
37
+ // Capture static output from intermediate renders.
38
+ // The <Static> component uses useLayoutEffect to clear its children after
39
+ // the first commit. The reconciler's resetAfterCommit calls onImmediateRender
40
+ // when static content is dirty (and returns early, skipping the normal
41
+ // onRender callback), giving us a chance to capture it before it's cleared
42
+ // by the subsequent re-render.
43
+ let capturedStaticOutput = '';
44
+ rootNode.onComputeLayout = () => {
45
+ rootNode.yogaNode.setWidth(columns);
46
+ rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
47
+ };
48
+ rootNode.onImmediateRender = () => {
49
+ const { staticOutput } = renderer(rootNode, false);
50
+ if (staticOutput && staticOutput !== '\n') {
51
+ capturedStaticOutput += staticOutput;
52
+ }
53
+ };
54
+ // Capture the first uncaught error so we can re-throw it after cleanup.
55
+ // React's reconciler catches component errors internally and reports them
56
+ // via onUncaughtError rather than letting them propagate. For a synchronous
57
+ // utility like renderToString, callers expect errors to throw.
58
+ let uncaughtError;
59
+ // Create a reconciler container in legacy (synchronous) mode.
60
+ // The four trailing callbacks are: onUncaughtError, onCaughtError,
61
+ // onRecoverableError, and onHostTransitionComplete.
62
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
63
+ const container = reconciler.createContainer(rootNode, LegacyRoot, null, false, null, 'render-to-string', (error) => {
64
+ uncaughtError ??= error;
65
+ }, () => { }, () => { }, () => { });
66
+ let teardownSucceeded = false;
67
+ try {
68
+ // Synchronously render the React tree into the container
69
+ reconciler.updateContainerSync(node, container, null, () => { });
70
+ reconciler.flushSyncWork();
71
+ // Yoga layout has already been calculated by onComputeLayout during commit.
72
+ // Render the DOM tree to a string — this captures the dynamic (non-static) output.
73
+ const { output } = renderer(rootNode, false);
74
+ // Tear down: unmount the tree so the reconciler cleans up child nodes
75
+ // and runs effect cleanup functions. Child Yoga nodes are freed by the
76
+ // reconciler's removeChildFromContainer → cleanupYogaNode → freeRecursive.
77
+ reconciler.updateContainerSync(null, container, null, () => { });
78
+ reconciler.flushSyncWork();
79
+ teardownSucceeded = true;
80
+ // Free the root yoga node itself (children already freed by reconciler)
81
+ rootNode.yogaNode.free();
82
+ // Re-throw after full cleanup so callers see the original error.
83
+ if (uncaughtError !== undefined) {
84
+ throw uncaughtError instanceof Error
85
+ ? uncaughtError
86
+ : new Error(String(uncaughtError));
87
+ }
88
+ // The renderer appends a trailing newline to static output for terminal
89
+ // rendering (so dynamic output starts on a fresh line). Strip it here
90
+ // so renderToString returns clean output.
91
+ const normalizedStaticOutput = capturedStaticOutput.endsWith('\n')
92
+ ? capturedStaticOutput.slice(0, -1)
93
+ : capturedStaticOutput;
94
+ if (normalizedStaticOutput && output) {
95
+ return normalizedStaticOutput + '\n' + output;
96
+ }
97
+ return normalizedStaticOutput || output;
98
+ }
99
+ finally {
100
+ // Ensure native Yoga memory is freed even if rendering or teardown threw.
101
+ // Yoga nodes are WASM-backed and not garbage collected.
102
+ if (!teardownSucceeded && rootNode.yogaNode) {
103
+ try {
104
+ // If reconciler teardown failed, some child nodes may not have been
105
+ // freed. Use freeRecursive to clean up the entire tree as best-effort.
106
+ rootNode.yogaNode.freeRecursive();
107
+ }
108
+ catch {
109
+ // Best-effort: node may already be partially freed
110
+ }
111
+ }
112
+ }
113
+ };
114
+ export default renderToString;
115
+ //# sourceMappingURL=render-to-string.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-to-string.js","sourceRoot":"","sources":["../src/render-to-string.ts"],"names":[],"mappings":"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"}
package/build/render.d.ts CHANGED
@@ -94,7 +94,18 @@ export type Instance = {
94
94
  */
95
95
  unmount: Ink['unmount'];
96
96
  /**
97
- Returns a promise that resolves when the app is unmounted.
97
+ Returns a promise that settles when the app is unmounted.
98
+
99
+ It resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.
100
+
101
+ @example
102
+ ```jsx
103
+ const {unmount, waitUntilExit} = render(<MyApp />);
104
+
105
+ setTimeout(unmount, 1000);
106
+
107
+ await waitUntilExit(); // resolves after `unmount()` is called
108
+ ```
98
109
  */
99
110
  waitUntilExit: Ink['waitUntilExit'];
100
111
  cleanup: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,GAAqD,MAAM,UAAU,CAAC;AAC7E,OAAO,SAAS,MAAM,gBAAgB,CAAC;AA0HvC;;EAEE;AACF,MAAM,MAAM,GAAG,CACd,IAAe,EACf,OAA4C,EACjC,EAAE;IACb,MAAM,UAAU,GAAe;QAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,KAAK;QACZ,WAAW,EAAE,IAAI;QACjB,YAAY,EAAE,IAAI;QAClB,MAAM,EAAE,EAAE;QACV,oBAAoB,EAAE,KAAK;QAC3B,UAAU,EAAE,KAAK;QACjB,GAAG,UAAU,CAAC,OAAO,CAAC;KACtB,CAAC;IAEF,MAAM,QAAQ,GAAQ,WAAW,CAChC,UAAU,CAAC,MAAM,EACjB,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,UAAU,IAAI,KAAK,CAC9B,CAAC;IAEF,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEtB,OAAO;QACN,QAAQ,EAAE,QAAQ,CAAC,MAAM;QACzB,OAAO;YACN,QAAQ,CAAC,OAAO,EAAE,CAAC;QACpB,CAAC;QACD,aAAa,EAAE,QAAQ,CAAC,aAAa;QACrC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;QAClD,KAAK,EAAE,QAAQ,CAAC,KAAK;KACrB,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,MAAM,UAAU,GAAG,CAClB,SAAyD,EAAE,EAC3C,EAAE;IAClB,IAAI,MAAM,YAAY,MAAM,EAAE,CAAC;QAC9B,OAAO;YACN,MAAM;YACN,KAAK,EAAE,OAAO,CAAC,KAAK;SACpB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CACnB,MAA0B,EAC1B,cAAyB,EACzB,UAAmB,EACb,EAAE;IACR,IAAI,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAErC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,QAAQ,GAAG,cAAc,EAAE,CAAC;QAC5B,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACjC,CAAC;SAAM,IAAI,QAAQ,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QACjD,OAAO,CAAC,IAAI,CACX,iDAAiD,UAAU,gEAAgE,QAAQ,CAAC,YAAY,IAAI;YACnJ,6HAA6H,CAC9H,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC"}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,GAAqD,MAAM,UAAU,CAAC;AAC7E,OAAO,SAAS,MAAM,gBAAgB,CAAC;AAqIvC;;EAEE;AACF,MAAM,MAAM,GAAG,CACd,IAAe,EACf,OAA4C,EACjC,EAAE;IACb,MAAM,UAAU,GAAe;QAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,KAAK;QACZ,WAAW,EAAE,IAAI;QACjB,YAAY,EAAE,IAAI;QAClB,MAAM,EAAE,EAAE;QACV,oBAAoB,EAAE,KAAK;QAC3B,UAAU,EAAE,KAAK;QACjB,GAAG,UAAU,CAAC,OAAO,CAAC;KACtB,CAAC;IAEF,MAAM,QAAQ,GAAQ,WAAW,CAChC,UAAU,CAAC,MAAM,EACjB,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,UAAU,IAAI,KAAK,CAC9B,CAAC;IAEF,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEtB,OAAO;QACN,QAAQ,EAAE,QAAQ,CAAC,MAAM;QACzB,OAAO;YACN,QAAQ,CAAC,OAAO,EAAE,CAAC;QACpB,CAAC;QACD,aAAa,EAAE,QAAQ,CAAC,aAAa;QACrC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;QAClD,KAAK,EAAE,QAAQ,CAAC,KAAK;KACrB,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,MAAM,UAAU,GAAG,CAClB,SAAyD,EAAE,EAC3C,EAAE;IAClB,IAAI,MAAM,YAAY,MAAM,EAAE,CAAC;QAC9B,OAAO;YACN,MAAM;YACN,KAAK,EAAE,OAAO,CAAC,KAAK;SACpB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CACnB,MAA0B,EAC1B,cAAyB,EACzB,UAAmB,EACb,EAAE;IACR,IAAI,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAErC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,QAAQ,GAAG,cAAc,EAAE,CAAC;QAC5B,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACjC,CAAC;SAAM,IAAI,QAAQ,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QACjD,OAAO,CAAC,IAAI,CACX,iDAAiD,UAAU,gEAAgE,QAAQ,CAAC,YAAY,IAAI;YACnJ,6HAA6H,CAC9H,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ declare const sanitizeAnsi: (text: string) => string;
2
+ export default sanitizeAnsi;
@@ -0,0 +1,27 @@
1
+ import { hasAnsiControlCharacters, tokenizeAnsi } from './ansi-tokenizer.js';
2
+ const sgrParametersRegex = /^[\d:;]*$/;
3
+ // Strip ANSI escape sequences that would conflict with Ink's layout.
4
+ // Preserved: SGR sequences (colors, bold, etc. - end with 'm') and
5
+ // OSC sequences (hyperlinks, etc. - ESC ] or C1 OSC).
6
+ // Stripped: cursor movement, screen clearing, and other control sequences.
7
+ const sanitizeAnsi = (text) => {
8
+ if (!hasAnsiControlCharacters(text)) {
9
+ return text;
10
+ }
11
+ let output = '';
12
+ for (const token of tokenizeAnsi(text)) {
13
+ if (token.type === 'text' || token.type === 'osc') {
14
+ output += token.value;
15
+ continue;
16
+ }
17
+ if (token.type === 'csi' &&
18
+ token.finalCharacter === 'm' &&
19
+ token.intermediateString === '' &&
20
+ sgrParametersRegex.test(token.parameterString)) {
21
+ output += token.value;
22
+ }
23
+ }
24
+ return output;
25
+ };
26
+ export default sanitizeAnsi;
27
+ //# sourceMappingURL=sanitize-ansi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-ansi.js","sourceRoot":"","sources":["../src/sanitize-ansi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAE,YAAY,EAAC,MAAM,qBAAqB,CAAC;AAE3E,MAAM,kBAAkB,GAAG,WAAW,CAAC;AAEvC,qEAAqE;AACrE,mEAAmE;AACnE,sDAAsD;AACtD,2EAA2E;AAC3E,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE;IAC7C,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;YACtB,SAAS;QACV,CAAC;QAED,IACC,KAAK,CAAC,IAAI,KAAK,KAAK;YACpB,KAAK,CAAC,cAAc,KAAK,GAAG;YAC5B,KAAK,CAAC,kBAAkB,KAAK,EAAE;YAC/B,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,EAC7C,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;QACvB,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF,eAAe,YAAY,CAAC"}
@@ -1,3 +1,4 @@
1
+ import sanitizeAnsi from './sanitize-ansi.js';
1
2
  // Squashing text nodes allows to combine multiple text nodes into one and write
2
3
  // to `Output` instance only once. For example, <Text>hello{' '}world</Text>
3
4
  // is actually 3 text nodes, which would result 3 writes to `Output`.
@@ -29,7 +30,7 @@ const squashTextNodes = (node) => {
29
30
  }
30
31
  text += nodeText;
31
32
  }
32
- return text;
33
+ return sanitizeAnsi(text);
33
34
  };
34
35
  export default squashTextNodes;
35
36
  //# sourceMappingURL=squash-text-nodes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"squash-text-nodes.js","sourceRoot":"","sources":["../src/squash-text-nodes.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,4EAA4E;AAC5E,qEAAqE;AACrE,EAAE;AACF,kGAAkG;AAClG,wFAAwF;AACxF,MAAM,eAAe,GAAG,CAAC,IAAgB,EAAU,EAAE;IACpD,IAAI,IAAI,GAAG,EAAE,CAAC;IAEd,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAEzC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC7B,SAAS;QACV,CAAC;QAED,IAAI,QAAQ,GAAG,EAAE,CAAC;QAElB,IAAI,SAAS,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACpC,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC;QAChC,CAAC;aAAM,CAAC;YACP,IACC,SAAS,CAAC,QAAQ,KAAK,UAAU;gBACjC,SAAS,CAAC,QAAQ,KAAK,kBAAkB,EACxC,CAAC;gBACF,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;YACvC,CAAC;YAED,oFAAoF;YACpF,iFAAiF;YACjF,IACC,QAAQ,CAAC,MAAM,GAAG,CAAC;gBACnB,OAAO,SAAS,CAAC,kBAAkB,KAAK,UAAU,EACjD,CAAC;gBACF,QAAQ,GAAG,SAAS,CAAC,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC1D,CAAC;QACF,CAAC;QAED,IAAI,IAAI,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"squash-text-nodes.js","sourceRoot":"","sources":["../src/squash-text-nodes.ts"],"names":[],"mappings":"AACA,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAE9C,gFAAgF;AAChF,4EAA4E;AAC5E,qEAAqE;AACrE,EAAE;AACF,kGAAkG;AAClG,wFAAwF;AACxF,MAAM,eAAe,GAAG,CAAC,IAAgB,EAAU,EAAE;IACpD,IAAI,IAAI,GAAG,EAAE,CAAC;IAEd,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAEzC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC7B,SAAS;QACV,CAAC;QAED,IAAI,QAAQ,GAAG,EAAE,CAAC;QAElB,IAAI,SAAS,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACpC,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC;QAChC,CAAC;aAAM,CAAC;YACP,IACC,SAAS,CAAC,QAAQ,KAAK,UAAU;gBACjC,SAAS,CAAC,QAAQ,KAAK,kBAAkB,EACxC,CAAC;gBACF,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;YACvC,CAAC;YAED,oFAAoF;YACpF,iFAAiF;YACjF,IACC,QAAQ,CAAC,MAAM,GAAG,CAAC;gBACnB,OAAO,SAAS,CAAC,kBAAkB,KAAK,UAAU,EACjD,CAAC;gBACF,QAAQ,GAAG,SAAS,CAAC,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC1D,CAAC;QACF,CAAC;QAED,IAAI,IAAI,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ declare const isDev: () => boolean;
2
+ export { isDev };
package/build/utils.js ADDED
@@ -0,0 +1,4 @@
1
+ import process from 'node:process';
2
+ const isDev = () => process.env['DEV'] === 'true';
3
+ export { isDev };
4
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC;AAElD,OAAO,EAAC,KAAK,EAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "description": "React for CLI",
5
5
  "license": "MIT",
6
6
  "repository": "vadimdemedes/ink",
@@ -22,8 +22,9 @@
22
22
  "build": "tsc",
23
23
  "prepare": "npm run build",
24
24
  "test": "tsc --noEmit && xo && FORCE_COLOR=true ava",
25
- "example": "NODE_NO_WARNINGS=1 node --loader ts-node/esm",
26
- "benchmark": "NODE_NO_WARNINGS=1 node --loader ts-node/esm"
25
+ "example": "NODE_NO_WARNINGS=1 node --import=tsx",
26
+ "benchmark": "NODE_NO_WARNINGS=1 node --import=tsx",
27
+ "inspect": "react-devtools"
27
28
  },
28
29
  "files": [
29
30
  "build"
@@ -59,7 +60,7 @@
59
60
  "react-reconciler": "^0.33.0",
60
61
  "scheduler": "^0.27.0",
61
62
  "signal-exit": "^3.0.7",
62
- "slice-ansi": "^7.1.0",
63
+ "slice-ansi": "^8.0.0",
63
64
  "stack-utils": "^2.0.6",
64
65
  "string-width": "^8.1.1",
65
66
  "terminal-size": "^4.0.1",
@@ -95,16 +96,18 @@
95
96
  "prettier": "^3.8.1",
96
97
  "react": "^19.2.4",
97
98
  "react-devtools-core": "^7.0.1",
99
+ "react-devtools": "^7.0.1",
100
+ "react-router": "^7.13.0",
98
101
  "sinon": "^21.0.0",
99
102
  "strip-ansi": "^7.1.0",
100
- "ts-node": "^10.9.2",
103
+ "tsx": "^4.21.0",
101
104
  "typescript": "^5.8.3",
102
105
  "xo": "^0.59.3"
103
106
  },
104
107
  "peerDependencies": {
105
108
  "@types/react": ">=19.0.0",
106
109
  "react": ">=19.0.0",
107
- "react-devtools-core": "^6.1.2"
110
+ "react-devtools-core": ">=6.1.2"
108
111
  },
109
112
  "peerDependenciesMeta": {
110
113
  "@types/react": {
@@ -127,7 +130,7 @@
127
130
  "tsx": "module"
128
131
  },
129
132
  "nodeArguments": [
130
- "--loader=ts-node/esm"
133
+ "--import=tsx"
131
134
  ]
132
135
  },
133
136
  "xo": {
package/readme.md CHANGED
@@ -69,8 +69,6 @@ render(<Counter />);
69
69
 
70
70
  <img src="media/demo.svg" width="600">
71
71
 
72
- Feel free to play around with the code and fork this Repl at [https://repl.it/@vadimdemedes/ink-counter-demo](https://repl.it/@vadimdemedes/ink-counter-demo).
73
-
74
72
  ## Who's Using Ink?
75
73
 
76
74
  - [Claude Code](https://github.com/anthropics/claude-code) - An agentic coding tool made by Anthropic.
@@ -134,6 +132,7 @@ Feel free to play around with the code and fork this Repl at [https://repl.it/@v
134
132
  ## Contents
135
133
 
136
134
  - [Getting Started](#getting-started)
135
+ - [App Lifecycle](#app-lifecycle)
137
136
  - [Components](#components)
138
137
  - [`<Text>`](#text)
139
138
  - [`<Box>`](#box)
@@ -156,7 +155,9 @@ Feel free to play around with the code and fork this Repl at [https://repl.it/@v
156
155
  - [Screen Reader Support](#screen-reader-support)
157
156
  - [Useful Components](#useful-components)
158
157
  - [Useful Hooks](#useful-hooks)
158
+ - [Recipes](#recipes)
159
159
  - [Examples](#examples)
160
+ - [Continuous Integration](#continuous-integration)
160
161
 
161
162
  ## Getting Started
162
163
 
@@ -223,6 +224,22 @@ Think of it as if every `<div>` in the browser had `display: flex`.
223
224
  See [`<Box>`](#box) built-in component below for documentation on how to use Flexbox layouts in Ink.
224
225
  Note that all text must be wrapped in a [`<Text>`](#text) component.
225
226
 
227
+ ## App Lifecycle
228
+
229
+ An Ink app is a Node.js process, so it stays alive only while there is active work in the event loop (timers, pending promises, [`useInput`](#useinputinputhandler-options) listening on `stdin`, etc.). If your component tree has no async work, the app will render once and exit immediately.
230
+
231
+ To exit the app, press **Ctrl+C** (enabled by default via [`exitOnCtrlC`](#exitonctrlc)), call [`exit()`](#exiterrororresult) from [`useApp`](#useapp) inside a component, or call [`unmount()`](#unmount) on the object returned by [`render()`](#rendertree-options).
232
+
233
+ Use [`waitUntilExit()`](#waituntilexit) to run code after the app is unmounted:
234
+
235
+ ```jsx
236
+ const {waitUntilExit} = render(<MyApp />);
237
+
238
+ await waitUntilExit();
239
+
240
+ console.log('App exited');
241
+ ```
242
+
226
243
  ## Components
227
244
 
228
245
  ### `<Text>`
@@ -1420,12 +1437,11 @@ For example, to implement a hanging indent component, you can indent all the lin
1420
1437
  ```jsx
1421
1438
  import {render, Transform} from 'ink';
1422
1439
 
1423
- const HangingIndent = ({content, indent = 4, children, ...props}) => (
1440
+ const HangingIndent = ({indent = 4, children}) => (
1424
1441
  <Transform
1425
1442
  transform={(line, index) =>
1426
1443
  index === 0 ? line : ' '.repeat(indent) + line
1427
1444
  }
1428
- {...props}
1429
1445
  >
1430
1446
  {children}
1431
1447
  </Transform>
@@ -1439,9 +1455,8 @@ const text =
1439
1455
  'of my hands only. I lived there two years and two months. At ' +
1440
1456
  'present I am a sojourner in civilized life again.';
1441
1457
 
1442
- // Other text properties are allowed as well
1443
1458
  render(
1444
- <HangingIndent bold dimColor indent={4}>
1459
+ <HangingIndent indent={4}>
1445
1460
  {text}
1446
1461
  </HangingIndent>
1447
1462
  );
@@ -1585,6 +1600,16 @@ Default: `false`
1585
1600
  If the Page Up or Page Down key was pressed, the corresponding property will be `true`.
1586
1601
  For example, if the user presses Page Down, `key.pageDown` equals `true`.
1587
1602
 
1603
+ ###### key.home
1604
+
1605
+ ###### key.end
1606
+
1607
+ Type: `boolean`\
1608
+ Default: `false`
1609
+
1610
+ If the Home or End key was pressed, the corresponding property will be `true`.
1611
+ For example, if the user presses End, `key.end` equals `true`.
1612
+
1588
1613
  ###### key.meta
1589
1614
 
1590
1615
  Type: `boolean`\
@@ -1643,17 +1668,20 @@ Useful when there are multiple `useInput` hooks used at once to avoid handling t
1643
1668
 
1644
1669
  `useApp` is a React hook that exposes a method to manually exit the app (unmount).
1645
1670
 
1646
- #### exit(error?)
1671
+ #### exit(errorOrResult?)
1647
1672
 
1648
1673
  Type: `Function`
1649
1674
 
1650
1675
  Exit (unmount) the whole Ink app.
1651
1676
 
1652
- ##### error
1677
+ ##### errorOrResult
1653
1678
 
1654
- Type: `Error`
1679
+ Type: `Error | unknown`
1655
1680
 
1656
- Optional error. If passed, [`waitUntilExit`](waituntilexit) will reject with that error.
1681
+ Optional value that controls how [`waitUntilExit`](waituntilexit) settles:
1682
+ - `exit()` resolves with `undefined`.
1683
+ - `exit(error)` rejects when `error` is an `Error`.
1684
+ - `exit(value)` resolves with `value`.
1657
1685
 
1658
1686
  ```js
1659
1687
  import {useApp} from 'ink';
@@ -2079,7 +2107,7 @@ Mount a component and render the output.
2079
2107
 
2080
2108
  ##### tree
2081
2109
 
2082
- Type: `ReactElement`
2110
+ Type: `ReactNode`
2083
2111
 
2084
2112
  ##### options
2085
2113
 
@@ -2132,6 +2160,13 @@ Default: `undefined`
2132
2160
 
2133
2161
  Runs the given callback after each render and re-render with a metrics object.
2134
2162
 
2163
+ ###### isScreenReaderEnabled
2164
+
2165
+ Type: `boolean`\
2166
+ Default: `process.env['INK_SCREEN_READER'] === 'true'`
2167
+
2168
+ Enable screen reader support. See [Screen Reader Support](#screen-reader-support).
2169
+
2135
2170
  ###### debug
2136
2171
 
2137
2172
  Type: `boolean`\
@@ -2234,6 +2269,56 @@ When the kitty keyboard protocol is enabled, input handling changes in several w
2234
2269
  - `Escape` key vs `Ctrl+[` - these are disambiguated.
2235
2270
  - **Event types.** With the `reportEventTypes` flag, key press, repeat, and release events are distinguished via `key.eventType`.
2236
2271
 
2272
+ #### renderToString(tree, options?)
2273
+
2274
+ Returns: `string`
2275
+
2276
+ Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.
2277
+
2278
+ Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.
2279
+
2280
+ ```jsx
2281
+ import {renderToString, Text, Box} from 'ink';
2282
+
2283
+ const output = renderToString(
2284
+ <Box padding={1}>
2285
+ <Text color="green">Hello World</Text>
2286
+ </Box>,
2287
+ );
2288
+
2289
+ console.log(output);
2290
+ ```
2291
+
2292
+ **Notes:**
2293
+
2294
+ - Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
2295
+ - `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
2296
+ - `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
2297
+ - The `<Static>` component is supported — its output is prepended to the dynamic output.
2298
+ - If a component throws during rendering, the error is propagated to the caller after cleanup.
2299
+
2300
+ ##### tree
2301
+
2302
+ Type: `ReactNode`
2303
+
2304
+ ##### options
2305
+
2306
+ Type: `object`
2307
+
2308
+ ###### columns
2309
+
2310
+ Type: `number`\
2311
+ Default: `80`
2312
+
2313
+ Width of the virtual terminal in columns. Controls where text wrapping occurs.
2314
+
2315
+ ```jsx
2316
+ const output = renderToString(<Text>{'A'.repeat(100)}</Text>, {
2317
+ columns: 40,
2318
+ });
2319
+ // Text wraps at 40 columns
2320
+ ```
2321
+
2237
2322
  #### Instance
2238
2323
 
2239
2324
  This is the object that `render()` returns.
@@ -2244,7 +2329,7 @@ Replace the previous root node with a new one or update the props of the current
2244
2329
 
2245
2330
  ###### tree
2246
2331
 
2247
- Type: `ReactElement`
2332
+ Type: `ReactNode`
2248
2333
 
2249
2334
  ```jsx
2250
2335
  // Update props of the root node
@@ -2267,7 +2352,9 @@ unmount();
2267
2352
 
2268
2353
  ##### waitUntilExit()
2269
2354
 
2270
- Returns a promise that resolves when the app is unmounted.
2355
+ Returns a promise that settles when the app is unmounted.
2356
+
2357
+ It resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.
2271
2358
 
2272
2359
  ```jsx
2273
2360
  const {unmount, waitUntilExit} = render(<MyApp />);
@@ -2277,6 +2364,12 @@ setTimeout(unmount, 1000);
2277
2364
  await waitUntilExit(); // resolves after `unmount()` is called
2278
2365
  ```
2279
2366
 
2367
+ ##### cleanup()
2368
+
2369
+ Delete the internal Ink instance associated with the current `stdout`.
2370
+ This is mostly useful for advanced cases (for example, tests) where you need `render()` to create a fresh instance for the same stream.
2371
+ This does not unmount the current app.
2372
+
2280
2373
  ##### clear()
2281
2374
 
2282
2375
  Clear output.
@@ -2491,11 +2584,16 @@ For a practical example of building an accessible component, see the [ARIA examp
2491
2584
  - [ink-scroll-list](https://github.com/ByteLandTechnology/ink-scroll-list) - Scrollable list.
2492
2585
  - [ink-stepper](https://github.com/archcorsair/ink-stepper) - Step-by-step wizard.
2493
2586
  - [ink-virtual-list](https://github.com/archcorsair/ink-virtual-list) - Virtualized list that renders only visible items for performance.
2587
+ - [ink-color-picker](https://github.com/sina-byn/ink-color-picker) - Color picker.
2494
2588
 
2495
2589
  ## Useful Hooks
2496
2590
 
2497
2591
  - [ink-use-stdout-dimensions](https://github.com/cameronhunter/ink-monorepo/tree/master/packages/ink-use-stdout-dimensions) - Subscribe to stdout dimensions.
2498
2592
 
2593
+ ## Recipes
2594
+
2595
+ - [Routing with React Router](recipes/routing.md) - Navigate between routes using `MemoryRouter`.
2596
+
2499
2597
  ## Examples
2500
2598
 
2501
2599
  The [`examples`](/examples) directory contains a set of real examples. You can run them with:
@@ -2517,6 +2615,20 @@ npm run example examples/[example name]
2517
2615
  - [Write to stderr](examples/use-stderr/use-stderr.tsx) - Write to stderr, bypassing main Ink output.
2518
2616
  - [Static](examples/static/static.tsx) - Use the `<Static>` component to render permanent output.
2519
2617
  - [Child process](examples/subprocess-output) - Renders output from a child process.
2618
+ - [Router](examples/router/router.tsx) - Navigate between routes using React Router's `MemoryRouter`.
2619
+
2620
+ ## Continuous Integration
2621
+
2622
+ When running on CI (detected via the `CI` environment variable), Ink adapts its rendering:
2623
+
2624
+ - Only the last frame is rendered on exit, instead of continuously updating the terminal. This is because most CI environments don't support the ANSI escape sequences used to overwrite previous output.
2625
+ - Terminal resize events are not listened to.
2626
+
2627
+ If your CI environment supports full terminal rendering and you want to opt out of this behavior, set `CI=false`:
2628
+
2629
+ ```sh
2630
+ CI=false node my-cli.js
2631
+ ```
2520
2632
 
2521
2633
  ## Maintainers
2522
2634