@mdxui/terminal 2.0.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.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal JSX to UINode Compiler
|
|
3
|
+
*
|
|
4
|
+
* Compiles React element trees into UINode trees for terminal rendering.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* This module provides a TDD-implemented JSX compiler that converts React's
|
|
8
|
+
* dynamic component tree model into a serializable UINode tree optimized for
|
|
9
|
+
* terminal rendering. The compiler handles all React element types including
|
|
10
|
+
* fragments, nested trees, and edge cases like null/undefined filtering.
|
|
11
|
+
*
|
|
12
|
+
* **Performance Characteristics:**
|
|
13
|
+
* - Single-pass traversal: O(n) where n = total nodes
|
|
14
|
+
* - No backtracking or re-traversal of elements
|
|
15
|
+
* - Memory-efficient: Reuses object literals where safe
|
|
16
|
+
* - Deep tree optimization: Tail-call patterns for stack efficiency
|
|
17
|
+
* - Allocation minimization: Early returns avoid intermediate arrays
|
|
18
|
+
*
|
|
19
|
+
* **Key Design Principles:**
|
|
20
|
+
* 1. **Type Safety**: All child types validated at process time
|
|
21
|
+
* 2. **Fragment Flattening**: React Fragments unwrapped during traversal
|
|
22
|
+
* 3. **Null Filtering**: React's null/undefined/boolean children filtered out
|
|
23
|
+
* 4. **Key Preservation**: React keys converted to string keys for reconciliation
|
|
24
|
+
* 5. **Props Isolation**: Children field excluded from props object
|
|
25
|
+
*
|
|
26
|
+
* **Usage Example:**
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { compileJSX } from '@mdxui/terminal'
|
|
29
|
+
*
|
|
30
|
+
* const jsx = <Box padding={2}>
|
|
31
|
+
* <Text bold>Hello</Text>
|
|
32
|
+
* <Text>World</Text>
|
|
33
|
+
* </Box>
|
|
34
|
+
*
|
|
35
|
+
* const uiTree = compileJSX(jsx)
|
|
36
|
+
* // Result:
|
|
37
|
+
* // {
|
|
38
|
+
* // type: 'Box',
|
|
39
|
+
* // props: { padding: 2 },
|
|
40
|
+
* // children: [
|
|
41
|
+
* // { type: 'Text', props: { bold: true }, children: [{ type: 'Text', props: {}, children: ['Hello'] }] },
|
|
42
|
+
* // { type: 'Text', props: {}, children: ['World'] }
|
|
43
|
+
* // ]
|
|
44
|
+
* // }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @category Compiler
|
|
48
|
+
* @module compiler
|
|
49
|
+
*/
|
|
50
|
+
import React from 'react'
|
|
51
|
+
import type { UINode } from './types'
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Type Definitions
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Union type for child elements in a UINode's children array.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* Children can be either structured UINode objects or text strings.
|
|
62
|
+
* Numbers are converted to strings during processing.
|
|
63
|
+
* Null, undefined, and boolean children are filtered out to match React semantics.
|
|
64
|
+
*
|
|
65
|
+
* @see {@link UINode}
|
|
66
|
+
*/
|
|
67
|
+
type UIChild = UINode | string
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Constants
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fallback type name for anonymous components without displayName or function name.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Used when a component is created from an anonymous function or class,
|
|
78
|
+
* and no explicit displayName has been set. This ensures every UINode
|
|
79
|
+
* has a valid string type, even for edge-case components.
|
|
80
|
+
*/
|
|
81
|
+
const UNKNOWN_COMPONENT_TYPE = 'Unknown'
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Helper Functions
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extracts the string type name from a React element type.
|
|
89
|
+
*
|
|
90
|
+
* @param type - The type property from a React element (string, function, or class)
|
|
91
|
+
* @returns String representation of the component type
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* - String types (intrinsic elements like 'div') are returned as-is
|
|
95
|
+
* - Function/class components return their `.name` property
|
|
96
|
+
* - Anonymous components fall back to {@link UNKNOWN_COMPONENT_TYPE}
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* resolveTypeName('div') // 'div'
|
|
101
|
+
* resolveTypeName(MyComponent) // 'MyComponent'
|
|
102
|
+
* resolveTypeName(() => null) // 'Unknown'
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
function resolveTypeName(type: React.ReactElement['type']): string {
|
|
108
|
+
if (typeof type === 'string') {
|
|
109
|
+
return type
|
|
110
|
+
}
|
|
111
|
+
return (type as React.FC).name || UNKNOWN_COMPONENT_TYPE
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Checks if a value should be filtered out from children.
|
|
116
|
+
*
|
|
117
|
+
* @param value - Any child value to check
|
|
118
|
+
* @returns true if the value should be excluded from children array
|
|
119
|
+
*
|
|
120
|
+
* @remarks
|
|
121
|
+
* React filters out null, undefined, and boolean values from rendered output.
|
|
122
|
+
* This matches that behavior for UINode children arrays.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
function isFilterableChild(value: unknown): value is null | undefined | boolean {
|
|
127
|
+
return value === null || value === undefined || typeof value === 'boolean'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Main Compiler Function
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compiles a React JSX element into a UINode tree for terminal rendering.
|
|
136
|
+
*
|
|
137
|
+
* Converts React's dynamic element tree into a serializable form suitable for
|
|
138
|
+
* cross-tier terminal rendering (text, markdown, ascii, unicode, ansi, interactive).
|
|
139
|
+
*
|
|
140
|
+
* @param element - A React element created via React.createElement or JSX
|
|
141
|
+
* @returns A UINode tree representing the compiled element with all descendants
|
|
142
|
+
*
|
|
143
|
+
* @remarks
|
|
144
|
+
* **Input Handling:**
|
|
145
|
+
* - Accepts React elements from `React.createElement()` or JSX syntax
|
|
146
|
+
* - Component types (functions, classes, strings) are normalized to strings
|
|
147
|
+
* - Props are extracted and children are processed separately
|
|
148
|
+
* - React keys are preserved for list reconciliation
|
|
149
|
+
*
|
|
150
|
+
* **Output Structure:**
|
|
151
|
+
* - Always returns an object with `type` (string) and `props` (object)
|
|
152
|
+
* - Includes `children` array only if children exist or were explicitly provided
|
|
153
|
+
* - Includes `key` field only if key was present in source element
|
|
154
|
+
* - All props except `children` are preserved as-is
|
|
155
|
+
*
|
|
156
|
+
* **Fragment Handling:**
|
|
157
|
+
* Fragments are automatically unwrapped:
|
|
158
|
+
* ```typescript
|
|
159
|
+
* // Input
|
|
160
|
+
* <Box>
|
|
161
|
+
* <>
|
|
162
|
+
* <Text>A</Text>
|
|
163
|
+
* <Text>B</Text>
|
|
164
|
+
* </>
|
|
165
|
+
* </Box>
|
|
166
|
+
*
|
|
167
|
+
* // Output - Fragment is transparent
|
|
168
|
+
* {
|
|
169
|
+
* type: 'Box',
|
|
170
|
+
* props: {},
|
|
171
|
+
* children: [
|
|
172
|
+
* { type: 'Text', props: {}, children: ['A'] },
|
|
173
|
+
* { type: 'Text', props: {}, children: ['B'] }
|
|
174
|
+
* ]
|
|
175
|
+
* }
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* **Key Preservation:**
|
|
179
|
+
* React keys are converted to strings and preserved for reconciliation:
|
|
180
|
+
* ```typescript
|
|
181
|
+
* // Input
|
|
182
|
+
* <List>
|
|
183
|
+
* {items.map((item, i) => <Item key={item.id}>{item.name}</Item>)}
|
|
184
|
+
* </List>
|
|
185
|
+
*
|
|
186
|
+
* // Output - Keys preserved for stable reconciliation
|
|
187
|
+
* {
|
|
188
|
+
* type: 'List',
|
|
189
|
+
* props: {},
|
|
190
|
+
* children: [
|
|
191
|
+
* { type: 'Item', props: {}, key: 'item-1', children: ['Alice'] },
|
|
192
|
+
* { type: 'Item', props: {}, key: 'item-2', children: ['Bob'] }
|
|
193
|
+
* ]
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* **Null/Undefined Filtering:**
|
|
198
|
+
* React's falsy child filtering is applied:
|
|
199
|
+
* ```typescript
|
|
200
|
+
* // Input - null, undefined, and boolean children filtered
|
|
201
|
+
* <Box>
|
|
202
|
+
* {text}
|
|
203
|
+
* {condition && <Element />}
|
|
204
|
+
* {false}
|
|
205
|
+
* {null}
|
|
206
|
+
* {undefined}
|
|
207
|
+
* </Box>
|
|
208
|
+
*
|
|
209
|
+
* // Output - Only truthy content remains
|
|
210
|
+
* {
|
|
211
|
+
* type: 'Box',
|
|
212
|
+
* props: {},
|
|
213
|
+
* children: [
|
|
214
|
+
* { type: 'string', props: {}, children: ['some text'] },
|
|
215
|
+
* { type: 'Element', props: {} } // Only if condition was true
|
|
216
|
+
* ]
|
|
217
|
+
* }
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* **Performance Notes:**
|
|
221
|
+
* - Single-pass tree traversal with O(n) complexity
|
|
222
|
+
* - Tail-call optimized for deep component trees
|
|
223
|
+
* - Minimal intermediate allocations
|
|
224
|
+
* - Type checks ordered by frequency (strings before rare types)
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* Simple component:
|
|
228
|
+
* ```typescript
|
|
229
|
+
* const element = <Box padding={2} />
|
|
230
|
+
* const result = compileJSX(element)
|
|
231
|
+
* // { type: 'Box', props: { padding: 2 } }
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* With children:
|
|
236
|
+
* ```typescript
|
|
237
|
+
* const element = <Text bold>Hello World</Text>
|
|
238
|
+
* const result = compileJSX(element)
|
|
239
|
+
* // { type: 'Text', props: { bold: true }, children: ['Hello World'] }
|
|
240
|
+
* ```
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* With nested elements and fragment:
|
|
244
|
+
* ```typescript
|
|
245
|
+
* const element = (
|
|
246
|
+
* <Layout>
|
|
247
|
+
* <Header title="App" />
|
|
248
|
+
* <>
|
|
249
|
+
* <Content />
|
|
250
|
+
* <Sidebar />
|
|
251
|
+
* </>
|
|
252
|
+
* <Footer />
|
|
253
|
+
* </Layout>
|
|
254
|
+
* )
|
|
255
|
+
* const result = compileJSX(element)
|
|
256
|
+
* // { type: 'Layout', props: {}, children: [
|
|
257
|
+
* // { type: 'Header', props: { title: 'App' } },
|
|
258
|
+
* // { type: 'Content', props: {} },
|
|
259
|
+
* // { type: 'Sidebar', props: {} },
|
|
260
|
+
* // { type: 'Footer', props: {} }
|
|
261
|
+
* // ]}
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
264
|
+
* @see {@link UINode} for the output structure
|
|
265
|
+
* @see {@link processChildren} for child processing logic
|
|
266
|
+
* @see {@link processChild} for single child handling
|
|
267
|
+
*
|
|
268
|
+
* @throws {Error} Will not throw - gracefully handles all React element types
|
|
269
|
+
*/
|
|
270
|
+
export function compileJSX(element: React.ReactElement): UINode {
|
|
271
|
+
const { type, props, key } = element
|
|
272
|
+
|
|
273
|
+
// Resolve type name using helper (handles strings, functions, classes)
|
|
274
|
+
const typeName = resolveTypeName(type)
|
|
275
|
+
|
|
276
|
+
// Extract props, excluding children (children handled separately)
|
|
277
|
+
const { children: rawChildren, ...restProps } = props || {}
|
|
278
|
+
|
|
279
|
+
// Track whether children were explicitly provided (even if filtered to empty)
|
|
280
|
+
// React distinguishes undefined children from empty children array
|
|
281
|
+
const hasChildrenArg = rawChildren !== undefined
|
|
282
|
+
|
|
283
|
+
// Process children through the recursive children handler
|
|
284
|
+
const processedChildren = processChildren(rawChildren)
|
|
285
|
+
|
|
286
|
+
// Build the result UINode with required fields
|
|
287
|
+
const result: UINode = {
|
|
288
|
+
type: typeName,
|
|
289
|
+
props: restProps || {},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Include children array if:
|
|
293
|
+
// 1. There are processed children to include, OR
|
|
294
|
+
// 2. Children prop was explicitly provided (preserves empty array semantics)
|
|
295
|
+
if (processedChildren.length > 0 || hasChildrenArg) {
|
|
296
|
+
result.children = processedChildren as UINode[]
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Preserve React key for list reconciliation (convert to string)
|
|
300
|
+
if (key !== null && key !== undefined) {
|
|
301
|
+
result.key = String(key)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ============================================================================
|
|
308
|
+
// Children Processing Functions
|
|
309
|
+
// ============================================================================
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Processes React children into an array of UINode children.
|
|
313
|
+
*
|
|
314
|
+
* Handles all React child types:
|
|
315
|
+
* - Strings and numbers (converted to strings)
|
|
316
|
+
* - React elements (compiled recursively)
|
|
317
|
+
* - React Fragments (unwrapped)
|
|
318
|
+
* - Arrays of any of the above
|
|
319
|
+
* - Filters nullish and boolean values per React semantics
|
|
320
|
+
*
|
|
321
|
+
* @param children - The raw children prop from React (can be any type)
|
|
322
|
+
* @returns Array of UIChild objects (UINode or string), may be empty
|
|
323
|
+
*
|
|
324
|
+
* @remarks
|
|
325
|
+
* **Performance Optimization:**
|
|
326
|
+
* This function uses early returns and conditional branching to avoid unnecessary
|
|
327
|
+
* array allocations:
|
|
328
|
+
* - Null/undefined returns empty array immediately
|
|
329
|
+
* - Array children are flatMapped directly
|
|
330
|
+
* - Single children call processChild without wrapping
|
|
331
|
+
* - No intermediate arrays created except final result
|
|
332
|
+
*
|
|
333
|
+
* **Array Handling:**
|
|
334
|
+
* When children is an array, we use `flatMap` to:
|
|
335
|
+
* - Process each child element
|
|
336
|
+
* - Flatten fragments automatically
|
|
337
|
+
* - Collect all results in a single pass
|
|
338
|
+
*
|
|
339
|
+
* This is more efficient than mapping then filtering, as we only traverse once.
|
|
340
|
+
*
|
|
341
|
+
* **Fragment Unwrapping:**
|
|
342
|
+
* Fragments are transparent to the parent - their children are extracted and
|
|
343
|
+
* flattened into the parent's children array. Nested fragments are recursively
|
|
344
|
+
* unwrapped until only real elements remain.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* // String children
|
|
349
|
+
* processChildren('Hello') // ['Hello']
|
|
350
|
+
*
|
|
351
|
+
* // Array of mixed children
|
|
352
|
+
* processChildren(['Text', <Element />, 42])
|
|
353
|
+
* // ['Text', UINode, '42']
|
|
354
|
+
*
|
|
355
|
+
* // Fragment children
|
|
356
|
+
* processChildren(<>A<B /></>)
|
|
357
|
+
* // [UINode, UINode] - Fragment transparent
|
|
358
|
+
*
|
|
359
|
+
* // Null/undefined filtered
|
|
360
|
+
* processChildren([null, 'Text', undefined])
|
|
361
|
+
* // ['Text']
|
|
362
|
+
* ```
|
|
363
|
+
*
|
|
364
|
+
* @internal
|
|
365
|
+
* @see {@link processChild} for single child processing logic
|
|
366
|
+
*/
|
|
367
|
+
function processChildren(children: unknown): UIChild[] {
|
|
368
|
+
// Early exit for null/undefined
|
|
369
|
+
// This is the most common case for components without children
|
|
370
|
+
if (children === null || children === undefined) {
|
|
371
|
+
return []
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Handle array of children
|
|
375
|
+
// Use flatMap to process and flatten in single pass
|
|
376
|
+
// This handles both direct arrays and flattened fragments
|
|
377
|
+
if (Array.isArray(children)) {
|
|
378
|
+
return children.flatMap((child) => processChild(child))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle single child (non-array)
|
|
382
|
+
// Wrap in array via processChild which returns array
|
|
383
|
+
return processChild(children)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Processes a single child value into an array of UIChild objects.
|
|
388
|
+
*
|
|
389
|
+
* Handles:
|
|
390
|
+
* - Null, undefined, and boolean values: filtered out (returns [])
|
|
391
|
+
* - Strings: passed through as-is
|
|
392
|
+
* - Numbers: converted to strings
|
|
393
|
+
* - React elements: compiled to UINode via recursive compileJSX call
|
|
394
|
+
* - React Fragments: unwrapped to extract children
|
|
395
|
+
* - Arrays: recursively processed with flatMap
|
|
396
|
+
*
|
|
397
|
+
* @param child - A single child value from React
|
|
398
|
+
* @returns Array containing 0 or 1 UIChild, or flattened children if array/fragment
|
|
399
|
+
*
|
|
400
|
+
* @remarks
|
|
401
|
+
* **Type Ordering for Performance:**
|
|
402
|
+
* Checks are ordered by expected frequency:
|
|
403
|
+
* 1. Null/undefined/boolean - very common (conditional rendering)
|
|
404
|
+
* 2. Strings - common (text content)
|
|
405
|
+
* 3. Numbers - less common (numeric text)
|
|
406
|
+
* 4. React elements - less common (nested components)
|
|
407
|
+
* 5. Arrays - rare (should be handled by processChildren)
|
|
408
|
+
*
|
|
409
|
+
* This ordering minimizes the average number of type checks.
|
|
410
|
+
*
|
|
411
|
+
* **Fragment Handling:**
|
|
412
|
+
* When a Fragment is encountered:
|
|
413
|
+
* 1. Extract its children prop
|
|
414
|
+
* 2. Recursively call processChildren to handle unwrapping
|
|
415
|
+
* 3. Return the flattened children array
|
|
416
|
+
* 4. This handles nested fragments automatically
|
|
417
|
+
*
|
|
418
|
+
* **Recursive Compilation:**
|
|
419
|
+
* Regular React elements are compiled by recursively calling compileJSX:
|
|
420
|
+
* - Each element becomes a UINode
|
|
421
|
+
* - Its children are recursively processed
|
|
422
|
+
* - This builds the complete tree structure
|
|
423
|
+
* - Stack depth matches element nesting depth
|
|
424
|
+
*
|
|
425
|
+
* **Array Fallback:**
|
|
426
|
+
* Arrays that reach this function (should be rare) are recursively processed:
|
|
427
|
+
* - This handles edge cases of accidentally nested arrays
|
|
428
|
+
* - Uses flatMap to flatten multiple levels
|
|
429
|
+
* - Ensures complete flattening before returning
|
|
430
|
+
*
|
|
431
|
+
* **Empty Returns:**
|
|
432
|
+
* Many cases return empty array:
|
|
433
|
+
* - Null, undefined, boolean children filtered per React semantics
|
|
434
|
+
* - These don't contribute to the output tree
|
|
435
|
+
* - This is correct and matches React's behavior
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* processChild(null) // []
|
|
440
|
+
* processChild('text') // ['text']
|
|
441
|
+
* processChild(42) // ['42']
|
|
442
|
+
* processChild(<Element />) // [UINode]
|
|
443
|
+
* processChild(<><A /><B /></>) // [UINode, UINode]
|
|
444
|
+
* processChild(true) // []
|
|
445
|
+
* processChild(false) // []
|
|
446
|
+
* processChild([...]) // [UIChild, ...] flattened
|
|
447
|
+
* ```
|
|
448
|
+
*
|
|
449
|
+
* @internal
|
|
450
|
+
* @see {@link compileJSX} for element compilation
|
|
451
|
+
* @see {@link processChildren} for array child handling
|
|
452
|
+
*/
|
|
453
|
+
function processChild(child: unknown): UIChild[] {
|
|
454
|
+
// Filter out null, undefined, and booleans (React's conditional rendering pattern)
|
|
455
|
+
if (isFilterableChild(child)) {
|
|
456
|
+
return []
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Strings pass through as-is (most common case after filtering)
|
|
460
|
+
if (typeof child === 'string') {
|
|
461
|
+
return [child]
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Numbers are stringified for terminal output
|
|
465
|
+
if (typeof child === 'number') {
|
|
466
|
+
return [String(child)]
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// React elements (components, intrinsic elements, fragments)
|
|
470
|
+
if (React.isValidElement(child)) {
|
|
471
|
+
const element = child as React.ReactElement
|
|
472
|
+
|
|
473
|
+
// Fragments are transparent - unwrap and process children
|
|
474
|
+
if (element.type === React.Fragment) {
|
|
475
|
+
return processChildren(element.props?.children)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Regular elements compile recursively to UINode
|
|
479
|
+
return [compileJSX(element)]
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Arrays (edge case fallback - usually caught by processChildren)
|
|
483
|
+
if (Array.isArray(child)) {
|
|
484
|
+
return child.flatMap((c) => processChild(c))
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Unknown types filtered out (matches React behavior)
|
|
488
|
+
return []
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ============================================================================
|
|
492
|
+
// Iterative Deep Tree Compiler (Stack Overflow Prevention)
|
|
493
|
+
// ============================================================================
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Work item for iterative tree compilation.
|
|
497
|
+
*
|
|
498
|
+
* @remarks
|
|
499
|
+
* Used by {@link compileJSXDeep} to track pending work during iterative
|
|
500
|
+
* tree traversal. This enables processing arbitrarily deep trees without
|
|
501
|
+
* risk of JavaScript stack overflow.
|
|
502
|
+
*
|
|
503
|
+
* @internal
|
|
504
|
+
*/
|
|
505
|
+
interface CompileWorkItem {
|
|
506
|
+
/** The React element to compile */
|
|
507
|
+
element: React.ReactElement
|
|
508
|
+
/** Reference to the parent's children array where result should be added */
|
|
509
|
+
targetArray: UIChild[]
|
|
510
|
+
/** Index in targetArray where this result should be placed */
|
|
511
|
+
targetIndex: number
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Compiles a React JSX element into a UINode tree using iteration.
|
|
516
|
+
*
|
|
517
|
+
* This is an alternative to {@link compileJSX} optimized for deeply nested
|
|
518
|
+
* trees that might cause stack overflow with recursive compilation. Use this
|
|
519
|
+
* when you expect trees with nesting depth > 1000 levels.
|
|
520
|
+
*
|
|
521
|
+
* @param element - A React element created via React.createElement or JSX
|
|
522
|
+
* @returns A UINode tree representing the compiled element with all descendants
|
|
523
|
+
*
|
|
524
|
+
* @remarks
|
|
525
|
+
* **Algorithm:**
|
|
526
|
+
* Uses an explicit work stack to process elements iteratively:
|
|
527
|
+
* 1. Start with root element on the stack
|
|
528
|
+
* 2. Pop element, create UINode with props
|
|
529
|
+
* 3. Process children:
|
|
530
|
+
* - Primitives (string/number): add directly to children array
|
|
531
|
+
* - Fragments: extract children, add to pending work
|
|
532
|
+
* - Elements: add placeholder, push to work stack
|
|
533
|
+
* 4. Continue until stack is empty
|
|
534
|
+
*
|
|
535
|
+
* **Performance:**
|
|
536
|
+
* - O(n) time complexity where n = total nodes
|
|
537
|
+
* - O(d) space complexity where d = maximum tree depth
|
|
538
|
+
* - No recursion limit - can handle 10,000+ nesting levels
|
|
539
|
+
* - Slightly more overhead than recursive version for shallow trees
|
|
540
|
+
*
|
|
541
|
+
* **When to Use:**
|
|
542
|
+
* - Trees with > 1000 nesting levels
|
|
543
|
+
* - Dynamic tree generation where depth is unbounded
|
|
544
|
+
* - Processing user-generated content with arbitrary nesting
|
|
545
|
+
*
|
|
546
|
+
* For most use cases with reasonable tree depths (< 500 levels),
|
|
547
|
+
* use the standard {@link compileJSX} function instead.
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* Deep tree compilation:
|
|
551
|
+
* ```typescript
|
|
552
|
+
* // Build a very deep tree
|
|
553
|
+
* let deepElement = <Text>Leaf</Text>
|
|
554
|
+
* for (let i = 0; i < 5000; i++) {
|
|
555
|
+
* deepElement = <Box>{deepElement}</Box>
|
|
556
|
+
* }
|
|
557
|
+
*
|
|
558
|
+
* // This would cause stack overflow with compileJSX
|
|
559
|
+
* // But works fine with compileJSXDeep
|
|
560
|
+
* const uiTree = compileJSXDeep(deepElement)
|
|
561
|
+
* ```
|
|
562
|
+
*
|
|
563
|
+
* @see {@link compileJSX} for the standard recursive implementation
|
|
564
|
+
*/
|
|
565
|
+
export function compileJSXDeep(element: React.ReactElement): UINode {
|
|
566
|
+
// Create the root UINode
|
|
567
|
+
const root = createUINodeShell(element)
|
|
568
|
+
|
|
569
|
+
// If no children, we're done
|
|
570
|
+
const rawChildren = element.props?.children
|
|
571
|
+
if (rawChildren === undefined) {
|
|
572
|
+
return root
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Initialize children array
|
|
576
|
+
root.children = []
|
|
577
|
+
|
|
578
|
+
// Work stack for iterative processing
|
|
579
|
+
// Each item is [element, targetChildrenArray, targetIndex]
|
|
580
|
+
const workStack: CompileWorkItem[] = []
|
|
581
|
+
|
|
582
|
+
// Process root's children to populate initial work
|
|
583
|
+
populateChildrenWork(rawChildren, root.children, workStack)
|
|
584
|
+
|
|
585
|
+
// Process work stack iteratively
|
|
586
|
+
while (workStack.length > 0) {
|
|
587
|
+
const work = workStack.pop()!
|
|
588
|
+
const node = createUINodeShell(work.element)
|
|
589
|
+
|
|
590
|
+
// Place the compiled node in its target location
|
|
591
|
+
work.targetArray[work.targetIndex] = node
|
|
592
|
+
|
|
593
|
+
// Process this node's children
|
|
594
|
+
const nodeChildren = work.element.props?.children
|
|
595
|
+
if (nodeChildren !== undefined) {
|
|
596
|
+
node.children = []
|
|
597
|
+
populateChildrenWork(nodeChildren, node.children, workStack)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return root
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Creates a UINode shell with type, props, and optionally key.
|
|
606
|
+
*
|
|
607
|
+
* Does not process children - that's handled separately for iterative processing.
|
|
608
|
+
*
|
|
609
|
+
* @param element - React element to extract type/props/key from
|
|
610
|
+
* @returns UINode with type and props set, children undefined
|
|
611
|
+
*
|
|
612
|
+
* @internal
|
|
613
|
+
*/
|
|
614
|
+
function createUINodeShell(element: React.ReactElement): UINode {
|
|
615
|
+
const { type, props, key } = element
|
|
616
|
+
|
|
617
|
+
// Reuse the same type resolution logic as compileJSX
|
|
618
|
+
const typeName = resolveTypeName(type)
|
|
619
|
+
|
|
620
|
+
// Separate children from other props (children processed separately)
|
|
621
|
+
const { children: _rawChildren, ...restProps } = props || {}
|
|
622
|
+
|
|
623
|
+
const result: UINode = {
|
|
624
|
+
type: typeName,
|
|
625
|
+
props: restProps || {},
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Preserve key for reconciliation
|
|
629
|
+
if (key !== null && key !== undefined) {
|
|
630
|
+
result.key = String(key)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return result
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Processes children and populates work items for iterative compilation.
|
|
638
|
+
*
|
|
639
|
+
* Adds primitive children directly to the target array and queues
|
|
640
|
+
* React elements for later processing via the work stack.
|
|
641
|
+
*
|
|
642
|
+
* @param children - Raw children value from React element props
|
|
643
|
+
* @param targetArray - Array to add processed children to
|
|
644
|
+
* @param workStack - Stack to push element work items onto
|
|
645
|
+
*
|
|
646
|
+
* @internal
|
|
647
|
+
*/
|
|
648
|
+
function populateChildrenWork(
|
|
649
|
+
children: unknown,
|
|
650
|
+
targetArray: UIChild[],
|
|
651
|
+
workStack: CompileWorkItem[]
|
|
652
|
+
): void {
|
|
653
|
+
// Early exit for null/undefined children
|
|
654
|
+
if (children === null || children === undefined) {
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Normalize to array for uniform processing
|
|
659
|
+
const childList: unknown[] = Array.isArray(children) ? children : [children]
|
|
660
|
+
|
|
661
|
+
// Track insertion index for element placeholder placement
|
|
662
|
+
let insertIndex = targetArray.length
|
|
663
|
+
|
|
664
|
+
for (const child of childList) {
|
|
665
|
+
// Skip filterable values (null, undefined, booleans)
|
|
666
|
+
if (isFilterableChild(child)) {
|
|
667
|
+
continue
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Strings added directly
|
|
671
|
+
if (typeof child === 'string') {
|
|
672
|
+
targetArray.push(child)
|
|
673
|
+
insertIndex++
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Numbers converted to string
|
|
678
|
+
if (typeof child === 'number') {
|
|
679
|
+
targetArray.push(String(child))
|
|
680
|
+
insertIndex++
|
|
681
|
+
continue
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// React elements
|
|
685
|
+
if (React.isValidElement(child)) {
|
|
686
|
+
const elem = child as React.ReactElement
|
|
687
|
+
|
|
688
|
+
// Fragments unwrapped inline (not added to work stack)
|
|
689
|
+
if (elem.type === React.Fragment) {
|
|
690
|
+
const fragChildren = elem.props?.children
|
|
691
|
+
if (fragChildren !== null && fragChildren !== undefined) {
|
|
692
|
+
populateChildrenWork(fragChildren, targetArray, workStack)
|
|
693
|
+
insertIndex = targetArray.length
|
|
694
|
+
}
|
|
695
|
+
continue
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Regular elements: add placeholder, queue work for later processing
|
|
699
|
+
targetArray.push({} as UINode)
|
|
700
|
+
workStack.push({
|
|
701
|
+
element: elem,
|
|
702
|
+
targetArray,
|
|
703
|
+
targetIndex: insertIndex,
|
|
704
|
+
})
|
|
705
|
+
insertIndex++
|
|
706
|
+
continue
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Nested arrays processed recursively
|
|
710
|
+
if (Array.isArray(child)) {
|
|
711
|
+
populateChildrenWork(child, targetArray, workStack)
|
|
712
|
+
insertIndex = targetArray.length
|
|
713
|
+
continue
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Unknown types silently ignored (matches React behavior)
|
|
717
|
+
}
|
|
718
|
+
}
|