prev-cli 0.24.16 → 0.24.19

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 (37) hide show
  1. package/dist/cli.js +2 -1
  2. package/package.json +5 -2
  3. package/src/jsx/adapters/html.test.ts +191 -0
  4. package/src/jsx/adapters/html.ts +404 -0
  5. package/src/jsx/adapters/react.test.ts +172 -0
  6. package/src/jsx/adapters/react.tsx +346 -0
  7. package/src/jsx/define-component.ts +129 -0
  8. package/src/jsx/index.ts +47 -0
  9. package/src/jsx/jsx-runtime.test.ts +63 -0
  10. package/src/jsx/jsx-runtime.ts +117 -0
  11. package/src/jsx/migrate.test.ts +72 -0
  12. package/src/jsx/migrate.ts +451 -0
  13. package/src/jsx/schemas/index.ts +4 -0
  14. package/src/jsx/schemas/primitives.ts +107 -0
  15. package/src/jsx/schemas/tokens.ts +60 -0
  16. package/src/jsx/validation.ts +77 -0
  17. package/src/jsx/vnode.ts +159 -0
  18. package/src/primitives/index.ts +8 -0
  19. package/src/primitives/migrate.test.ts +317 -0
  20. package/src/primitives/migrate.ts +364 -0
  21. package/src/primitives/parser.test.ts +265 -0
  22. package/src/primitives/parser.ts +442 -0
  23. package/src/primitives/template-parser.test.ts +297 -0
  24. package/src/primitives/template-parser.ts +374 -0
  25. package/src/primitives/template-renderer.test.ts +359 -0
  26. package/src/primitives/template-renderer.ts +497 -0
  27. package/src/primitives/tokens.css +82 -0
  28. package/src/primitives/types.ts +248 -0
  29. package/src/tokens/defaults.test.ts +137 -0
  30. package/src/tokens/defaults.ts +77 -0
  31. package/src/tokens/defaults.yaml +76 -0
  32. package/src/tokens/resolver.test.ts +229 -0
  33. package/src/tokens/resolver.ts +173 -0
  34. package/src/tokens/utils.test.ts +172 -0
  35. package/src/tokens/utils.ts +104 -0
  36. package/src/tokens/validation.test.ts +118 -0
  37. package/src/tokens/validation.ts +226 -0
@@ -0,0 +1,63 @@
1
+ // src/jsx/jsx-runtime.test.ts
2
+ import { test, expect, describe, beforeEach, afterEach, spyOn } from 'bun:test'
3
+ import { z } from 'zod'
4
+ import { createVNode, resetIdCounter, createRenderContext, normalizeChildren, vnodeEquals } from './vnode'
5
+ import { Col, Box, Slot, Text, Icon, Fragment, setValidationMode, getValidationMode } from './jsx-runtime'
6
+ import { validateProps, type ValidationMode } from './validation'
7
+ import { defineComponent, defineStatelessComponent } from './define-component'
8
+
9
+ let originalMode: ValidationMode
10
+
11
+ beforeEach(() => {
12
+ resetIdCounter()
13
+ originalMode = getValidationMode()
14
+ setValidationMode('strict')
15
+ })
16
+
17
+ afterEach(() => {
18
+ setValidationMode(originalMode)
19
+ })
20
+
21
+ describe('jsx-runtime', () => {
22
+ test('vnode, primitives, components, validation', () => {
23
+ // vnode internals
24
+ const ctx1 = createRenderContext()
25
+ const ctx2 = createRenderContext()
26
+ ctx1.nextId('col')
27
+ expect(ctx2.nextId('col')).toBe('col-0')
28
+ expect(normalizeChildren(['hello'])[0].props.content).toBe('hello')
29
+ const a = createVNode('col', { gap: 'lg' })
30
+ resetIdCounter()
31
+ expect(vnodeEquals(a, createVNode('col', { gap: 'lg' }))).toBe(true)
32
+
33
+ // primitives
34
+ expect(Col({ gap: 'lg' }).type).toBe('col')
35
+ expect(Fragment({ children: [Text({ children: 'A' })] })).toHaveLength(1)
36
+ expect(Col({ children: [Text({ children: 'X' })] }).children![0].props.content).toBe('X')
37
+ expect(() => Col({ gap: 'invalid' as any })).toThrow()
38
+ expect(() => Slot({} as any)).toThrow()
39
+ expect(() => Icon({} as any)).toThrow()
40
+
41
+ // defineComponent
42
+ const Button = defineComponent({
43
+ name: 'Button',
44
+ props: z.object({ label: z.string() }),
45
+ states: z.enum(['idle', 'loading']),
46
+ defaultState: 'idle',
47
+ render: ({ props, state }) => Box({ bg: state === 'loading' ? 'muted' : 'primary', children: [Text({ children: props.label })] }),
48
+ })
49
+ expect(Button({ label: 'Click' }).componentName).toBe('Button')
50
+ expect(Button({ label: 'X' }, 'loading').children![0].props.bg).toBe('muted')
51
+ expect(() => Button({ label: 123 } as any)).toThrow()
52
+ expect(defineStatelessComponent({ name: 'L', props: z.object({ t: z.string() }), render: (p) => Text({ children: p.t }) })({ t: 'Hi' }).componentName).toBe('L')
53
+
54
+ // validation modes
55
+ setValidationMode('warn')
56
+ const spy = spyOn(console, 'warn').mockImplementation(() => {})
57
+ expect(() => validateProps(z.object({ n: z.string() }), { n: 1 }, 'T')).not.toThrow()
58
+ expect(spy).toHaveBeenCalled()
59
+ spy.mockRestore()
60
+ setValidationMode('off')
61
+ expect(() => validateProps(z.object({ n: z.string() }), { n: 1 }, 'T')).not.toThrow()
62
+ })
63
+ })
@@ -0,0 +1,117 @@
1
+ // src/jsx/jsx-runtime.ts
2
+ // JSX runtime for Vite's automatic JSX transform
3
+ import {
4
+ createVNode,
5
+ normalizeChildren,
6
+ type VNodeType,
7
+ } from './vnode'
8
+ import {
9
+ ColProps,
10
+ RowProps,
11
+ BoxProps,
12
+ SpacerProps,
13
+ SlotProps,
14
+ TextProps,
15
+ IconProps,
16
+ ImageProps,
17
+ } from './schemas/primitives'
18
+ import { validateProps } from './validation'
19
+
20
+ // Re-export validation utilities for consumers
21
+ export { setValidationMode, getValidationMode, type ValidationMode } from './validation'
22
+
23
+ // ============================================================
24
+ // Primitive Components
25
+ // ============================================================
26
+
27
+ export function Col(props: ColProps): VNodeType {
28
+ validateProps(ColProps, props, 'Col')
29
+ const { children, ...rest } = props
30
+ return createVNode('col', rest, normalizeChildren(children))
31
+ }
32
+
33
+ export function Row(props: RowProps): VNodeType {
34
+ validateProps(RowProps, props, 'Row')
35
+ const { children, ...rest } = props
36
+ return createVNode('row', rest, normalizeChildren(children))
37
+ }
38
+
39
+ export function Box(props: BoxProps): VNodeType {
40
+ validateProps(BoxProps, props, 'Box')
41
+ const { children, ...rest } = props
42
+ return createVNode('box', rest, normalizeChildren(children))
43
+ }
44
+
45
+ export function Spacer(props: SpacerProps = {}): VNodeType {
46
+ validateProps(SpacerProps, props, 'Spacer')
47
+ return createVNode('spacer', props)
48
+ }
49
+
50
+ export function Slot(props: SlotProps): VNodeType {
51
+ validateProps(SlotProps, props, 'Slot')
52
+ return createVNode('slot', props)
53
+ }
54
+
55
+ export function Text(props: TextProps): VNodeType {
56
+ validateProps(TextProps, props, 'Text')
57
+ const { children, ...rest } = props
58
+ return createVNode('text', { ...rest, content: children })
59
+ }
60
+
61
+ export function Icon(props: IconProps): VNodeType {
62
+ validateProps(IconProps, props, 'Icon')
63
+ return createVNode('icon', props)
64
+ }
65
+
66
+ export function Image(props: ImageProps): VNodeType {
67
+ validateProps(ImageProps, props, 'Image')
68
+ return createVNode('image', props)
69
+ }
70
+
71
+ // ============================================================
72
+ // JSX Runtime Functions
73
+ // ============================================================
74
+
75
+ type JSXElementType = ((props: Record<string, unknown>, state?: unknown) => VNodeType) & {
76
+ statesSchema?: unknown // Set by defineComponent
77
+ }
78
+
79
+ /**
80
+ * JSX factory function - called for each JSX element
81
+ * Extracts `state` prop for components created with defineComponent
82
+ */
83
+ export function jsx(type: JSXElementType, props: Record<string, unknown>): VNodeType {
84
+ // Check if this is a defineComponent component (has statesSchema)
85
+ if (type.statesSchema && 'state' in props) {
86
+ const { state, ...restProps } = props
87
+ return type(restProps, state)
88
+ }
89
+ return type(props)
90
+ }
91
+
92
+ /**
93
+ * JSX factory for elements with static children
94
+ */
95
+ export const jsxs = jsx
96
+
97
+ /**
98
+ * JSX factory for development mode (with source info)
99
+ */
100
+ export function jsxDEV(
101
+ type: JSXElementType,
102
+ props: Record<string, unknown>,
103
+ _key?: string,
104
+ _isStaticChildren?: boolean,
105
+ _source?: { fileName: string; lineNumber: number; columnNumber: number },
106
+ _self?: unknown
107
+ ): VNodeType {
108
+ return jsx(type, props)
109
+ }
110
+
111
+ // ============================================================
112
+ // Fragment support
113
+ // ============================================================
114
+
115
+ export function Fragment({ children }: { children?: unknown }): VNodeType[] {
116
+ return normalizeChildren(children)
117
+ }
@@ -0,0 +1,72 @@
1
+ // src/jsx/migrate.test.ts
2
+ import { test, expect, describe } from 'bun:test'
3
+ import { migrateYamlToJsx } from './migrate'
4
+
5
+ describe('migrateYamlToJsx', () => {
6
+ test('migrates all primitives, states, slots, props, and handles errors', () => {
7
+ const componentYaml = `
8
+ kind: component
9
+ id: card-layout
10
+ title: Card Layout
11
+ template:
12
+ root:
13
+ type: $col(gap:md padding:lg)
14
+ children:
15
+ row:
16
+ type: $row(gap:sm)
17
+ children:
18
+ icon: $icon("star" size:lg)
19
+ title: $text("Title & <tags>" size:xl)
20
+ spacer: $spacer(xl)
21
+ image: $image(src:"img.png" fit:cover)
22
+ footer: $box(bg:muted)
23
+ `
24
+ const result = migrateYamlToJsx(componentYaml)
25
+ expect(result.success).toBe(true)
26
+ expect(result.jsx).toContain('<Col gap="md"')
27
+ expect(result.jsx).toContain('<Row gap="sm"')
28
+ expect(result.jsx).toContain('<Icon name="star"')
29
+ expect(result.jsx).toContain('&amp;')
30
+ expect(result.jsx).toContain('&lt;')
31
+ expect(result.jsx).toContain('<Spacer size="xl"')
32
+ expect(result.jsx).toContain('<Image src="img.png"')
33
+
34
+ const screenYaml = `
35
+ kind: screen
36
+ id: test
37
+ title: Test
38
+ states:
39
+ on: {}
40
+ off: {}
41
+ template:
42
+ root:
43
+ type: $col
44
+ children:
45
+ slot: $slot(actions)
46
+ slots:
47
+ actions:
48
+ on: $text("On")
49
+ off: $text("Off")
50
+ `
51
+ const screenResult = migrateYamlToJsx(screenYaml)
52
+ expect(screenResult.success).toBe(true)
53
+ expect(screenResult.jsx).toContain("z.enum(['on', 'off'])")
54
+ expect(screenResult.jsx).toContain('<Slot name="actions"')
55
+ expect(screenResult.jsx).toContain('const slots = {')
56
+
57
+ const propsYaml = `
58
+ kind: component
59
+ id: x
60
+ title: X
61
+ props:
62
+ label:
63
+ type: string
64
+ required: true
65
+ template:
66
+ root: $text(label)
67
+ `
68
+ expect(migrateYamlToJsx(propsYaml).jsx).toContain('{props.label}')
69
+ expect(migrateYamlToJsx('invalid: [yaml').success).toBe(false)
70
+ expect(migrateYamlToJsx('kind: x\nid: x\ntitle: X').errors).toContain('No template.root found in config')
71
+ })
72
+ })
@@ -0,0 +1,451 @@
1
+ // src/jsx/migrate.ts
2
+ // Migration tool: converts YAML config to JSX
3
+ import { load as parseYaml } from 'js-yaml'
4
+ import { parsePrimitive, isQuoted } from '../primitives/parser'
5
+ import type { TemplateNode, Slots } from '../primitives/types'
6
+
7
+ export interface MigrationResult {
8
+ success: boolean
9
+ jsx?: string
10
+ errors: string[]
11
+ }
12
+
13
+ export interface YamlConfig {
14
+ kind: string
15
+ id: string
16
+ title: string
17
+ schemaVersion?: string
18
+ states?: Record<string, { description?: string }>
19
+ template?: { root: TemplateNode }
20
+ slots?: Slots
21
+ props?: Record<string, unknown>
22
+ }
23
+
24
+ /**
25
+ * Migrate a YAML config file to JSX/TSX
26
+ */
27
+ export function migrateYamlToJsx(yamlContent: string): MigrationResult {
28
+ const errors: string[] = []
29
+
30
+ let config: YamlConfig
31
+ try {
32
+ config = parseYaml(yamlContent) as YamlConfig
33
+ } catch (e) {
34
+ return { success: false, errors: [`YAML parse error: ${e}`] }
35
+ }
36
+
37
+ if (!config.template?.root) {
38
+ return { success: false, errors: ['No template.root found in config'] }
39
+ }
40
+
41
+ const componentName = pascalCase(config.id || 'Component')
42
+ const hasStates = config.states && Object.keys(config.states).length > 0
43
+ const hasSlots = config.slots && Object.keys(config.slots).length > 0
44
+ const hasProps = config.props && Object.keys(config.props).length > 0
45
+
46
+ // Build imports
47
+ const imports = new Set<string>()
48
+ collectImports(config.template.root, imports)
49
+
50
+ // Generate state type
51
+ const stateNames = hasStates ? Object.keys(config.states!) : ['default']
52
+
53
+ // Generate JSX tree
54
+ const jsxTree = convertNodeToJsx(config.template.root, 2, errors)
55
+
56
+ // Generate slots if present
57
+ let slotsCode = ''
58
+ if (hasSlots) {
59
+ slotsCode = generateSlotsCode(config.slots!, imports, errors)
60
+ }
61
+
62
+ // Build the output
63
+ const output = `// Generated from ${config.id || 'config'}.yaml
64
+ // ${config.title}
65
+ import { z } from 'zod'
66
+ import {
67
+ ${Array.from(imports).sort().join(',\n ')},
68
+ defineComponent,
69
+ } from '@prev-cli/jsx'
70
+
71
+ ${hasProps ? generatePropsSchema(config.props!) : `const PropsSchema = z.object({})`}
72
+
73
+ const StatesSchema = z.enum([${stateNames.map(s => `'${s}'`).join(', ')}])
74
+
75
+ ${slotsCode}
76
+ export const ${componentName} = defineComponent({
77
+ name: '${componentName}',
78
+ props: PropsSchema,
79
+ states: StatesSchema,
80
+ defaultState: '${stateNames[0]}',
81
+ render: ({ props, state }) => (
82
+ ${jsxTree}
83
+ ),
84
+ })
85
+
86
+ export default ${componentName}
87
+ `
88
+
89
+ return {
90
+ success: errors.length === 0,
91
+ jsx: output,
92
+ errors,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Convert a template node to JSX string
98
+ */
99
+ function convertNodeToJsx(node: TemplateNode, indent: number, errors: string[]): string {
100
+ const spaces = ' '.repeat(indent)
101
+
102
+ // String node: primitive or literal
103
+ if (typeof node === 'string') {
104
+ return convertLeafToJsx(node, indent, errors)
105
+ }
106
+
107
+ // Container node with type and children
108
+ if (typeof node === 'object' && node !== null && 'type' in node) {
109
+ const { type, children } = node as { type: string; children?: Record<string, TemplateNode> }
110
+ return convertContainerToJsx(type, children, indent, errors)
111
+ }
112
+
113
+ errors.push(`Unknown node type: ${JSON.stringify(node)}`)
114
+ return `${spaces}{/* unknown node */}`
115
+ }
116
+
117
+ /**
118
+ * Convert a leaf node (primitive string) to JSX
119
+ */
120
+ function convertLeafToJsx(value: string, indent: number, errors: string[]): string {
121
+ const spaces = ' '.repeat(indent)
122
+
123
+ // Check if it's a primitive
124
+ if (value.startsWith('$')) {
125
+ const result = parsePrimitive(value)
126
+ if (!result.success) {
127
+ errors.push(`Invalid primitive: ${value} - ${result.error.message}`)
128
+ return `${spaces}{/* invalid: ${value} */}`
129
+ }
130
+
131
+ const primitive = result.primitive
132
+ switch (primitive.type) {
133
+ case '$text':
134
+ return convertTextPrimitive(primitive, indent)
135
+ case '$icon':
136
+ return convertIconPrimitive(primitive, indent)
137
+ case '$image':
138
+ return convertImagePrimitive(primitive, indent)
139
+ case '$spacer':
140
+ return convertSpacerPrimitive(primitive, indent)
141
+ case '$slot':
142
+ return convertSlotPrimitive(primitive, indent)
143
+ case '$col':
144
+ return `${spaces}<Col${propsToJsx(primitive)} />`
145
+ case '$row':
146
+ return `${spaces}<Row${propsToJsx(primitive)} />`
147
+ case '$box':
148
+ return `${spaces}<Box${propsToJsx(primitive)} />`
149
+ default:
150
+ return `${spaces}{/* ${value} */}`
151
+ }
152
+ }
153
+
154
+ // Component reference
155
+ if (value.includes('/')) {
156
+ return `${spaces}{/* ref: ${value} */}`
157
+ }
158
+
159
+ // Literal text
160
+ return `${spaces}<Text>${escapeJsx(value)}</Text>`
161
+ }
162
+
163
+ /**
164
+ * Convert a container node to JSX
165
+ */
166
+ function convertContainerToJsx(
167
+ type: string,
168
+ children: Record<string, TemplateNode> | undefined,
169
+ indent: number,
170
+ errors: string[]
171
+ ): string {
172
+ const spaces = ' '.repeat(indent)
173
+ const result = parsePrimitive(type)
174
+
175
+ if (!result.success) {
176
+ errors.push(`Invalid primitive: ${type} - ${result.error.message}`)
177
+ return `${spaces}{/* invalid: ${type} */}`
178
+ }
179
+
180
+ const primitive = result.primitive
181
+ const componentName = getComponentName(primitive.type)
182
+ const props = propsToJsx(primitive)
183
+
184
+ if (!children || Object.keys(children).length === 0) {
185
+ return `${spaces}<${componentName}${props} />`
186
+ }
187
+
188
+ const childrenJsx = Object.entries(children)
189
+ .map(([, child]) => convertNodeToJsx(child, indent + 1, errors))
190
+ .join('\n')
191
+
192
+ return `${spaces}<${componentName}${props}>
193
+ ${childrenJsx}
194
+ ${spaces}</${componentName}>`
195
+ }
196
+
197
+ /**
198
+ * Convert primitive type to component name
199
+ */
200
+ function getComponentName(type: string): string {
201
+ const map: Record<string, string> = {
202
+ '$col': 'Col',
203
+ '$row': 'Row',
204
+ '$box': 'Box',
205
+ '$spacer': 'Spacer',
206
+ '$slot': 'Slot',
207
+ '$text': 'Text',
208
+ '$icon': 'Icon',
209
+ '$image': 'Image',
210
+ }
211
+ return map[type] || 'Unknown'
212
+ }
213
+
214
+ /**
215
+ * Convert primitive props to JSX attribute string
216
+ */
217
+ function propsToJsx(primitive: object): string {
218
+ const attrs: string[] = []
219
+
220
+ for (const [key, value] of Object.entries(primitive)) {
221
+ if (key === 'type') continue
222
+ if (value === undefined) continue
223
+
224
+ // Handle invalid identifiers with computed property syntax
225
+ const propName = isValidIdentifier(key) ? key : `["${escapeJsxAttribute(key)}"]`
226
+
227
+ if (typeof value === 'string') {
228
+ attrs.push(`${propName}="${escapeJsxAttribute(value)}"`)
229
+ } else {
230
+ attrs.push(`${propName}={${JSON.stringify(value)}}`)
231
+ }
232
+ }
233
+
234
+ return attrs.length > 0 ? ' ' + attrs.join(' ') : ''
235
+ }
236
+
237
+ /**
238
+ * Convert text primitive to JSX
239
+ */
240
+ function convertTextPrimitive(
241
+ primitive: { content: string; size?: string; weight?: string; color?: string },
242
+ indent: number
243
+ ): string {
244
+ const spaces = ' '.repeat(indent)
245
+ const attrs: string[] = []
246
+
247
+ if (primitive.size) attrs.push(`size="${escapeJsxAttribute(primitive.size)}"`)
248
+ if (primitive.weight) attrs.push(`weight="${escapeJsxAttribute(primitive.weight)}"`)
249
+ if (primitive.color) attrs.push(`color="${escapeJsxAttribute(primitive.color)}"`)
250
+
251
+ const content = isQuoted(primitive.content)
252
+ ? escapeJsx(primitive.content.slice(1, -1))
253
+ : `{props.${primitive.content}}`
254
+
255
+ const propsStr = attrs.length > 0 ? ' ' + attrs.join(' ') : ''
256
+ return `${spaces}<Text${propsStr}>${content}</Text>`
257
+ }
258
+
259
+ /**
260
+ * Convert icon primitive to JSX
261
+ */
262
+ function convertIconPrimitive(
263
+ primitive: { name: string; size?: string; color?: string },
264
+ indent: number
265
+ ): string {
266
+ const spaces = ' '.repeat(indent)
267
+ const attrs: string[] = []
268
+
269
+ const name = isQuoted(primitive.name)
270
+ ? `"${escapeJsxAttribute(primitive.name.slice(1, -1))}"`
271
+ : `{props.${primitive.name}}`
272
+ attrs.push(`name=${name}`)
273
+
274
+ if (primitive.size) attrs.push(`size="${escapeJsxAttribute(primitive.size)}"`)
275
+ if (primitive.color) attrs.push(`color="${escapeJsxAttribute(primitive.color)}"`)
276
+
277
+ return `${spaces}<Icon ${attrs.join(' ')} />`
278
+ }
279
+
280
+ /**
281
+ * Convert image primitive to JSX
282
+ */
283
+ function convertImagePrimitive(
284
+ primitive: { src: string; alt?: string; fit?: string },
285
+ indent: number
286
+ ): string {
287
+ const spaces = ' '.repeat(indent)
288
+ const attrs: string[] = []
289
+
290
+ const src = isQuoted(primitive.src)
291
+ ? `"${escapeJsxAttribute(primitive.src.slice(1, -1))}"`
292
+ : `{props.${primitive.src}}`
293
+ attrs.push(`src=${src}`)
294
+
295
+ if (primitive.alt) attrs.push(`alt="${escapeJsxAttribute(primitive.alt)}"`)
296
+ if (primitive.fit) attrs.push(`fit="${escapeJsxAttribute(primitive.fit)}"`)
297
+
298
+ return `${spaces}<Image ${attrs.join(' ')} />`
299
+ }
300
+
301
+ /**
302
+ * Convert spacer primitive to JSX
303
+ */
304
+ function convertSpacerPrimitive(
305
+ primitive: { size?: string },
306
+ indent: number
307
+ ): string {
308
+ const spaces = ' '.repeat(indent)
309
+ if (primitive.size) {
310
+ return `${spaces}<Spacer size="${escapeJsxAttribute(primitive.size)}" />`
311
+ }
312
+ return `${spaces}<Spacer />`
313
+ }
314
+
315
+ /**
316
+ * Convert slot primitive to JSX
317
+ */
318
+ function convertSlotPrimitive(
319
+ primitive: { name: string },
320
+ indent: number
321
+ ): string {
322
+ const spaces = ' '.repeat(indent)
323
+ return `${spaces}<Slot name="${escapeJsxAttribute(primitive.name)}" />`
324
+ }
325
+
326
+ /**
327
+ * Collect all component imports needed
328
+ */
329
+ function collectImports(node: TemplateNode, imports: Set<string>): void {
330
+ if (typeof node === 'string') {
331
+ if (node.startsWith('$')) {
332
+ const result = parsePrimitive(node)
333
+ if (result.success) {
334
+ imports.add(getComponentName(result.primitive.type))
335
+ }
336
+ }
337
+ return
338
+ }
339
+
340
+ if (typeof node === 'object' && node !== null && 'type' in node) {
341
+ const { type, children } = node as { type: string; children?: Record<string, TemplateNode> }
342
+
343
+ const result = parsePrimitive(type)
344
+ if (result.success) {
345
+ imports.add(getComponentName(result.primitive.type))
346
+ }
347
+
348
+ if (children) {
349
+ for (const child of Object.values(children)) {
350
+ collectImports(child, imports)
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Generate props schema from config
358
+ */
359
+ function generatePropsSchema(props: Record<string, unknown>): string {
360
+ const fields: string[] = []
361
+
362
+ for (const [name, def] of Object.entries(props)) {
363
+ const propDef = def as { type?: string; required?: boolean; default?: unknown }
364
+ let zodType = 'z.string()'
365
+
366
+ if (propDef.type === 'number') zodType = 'z.number()'
367
+ else if (propDef.type === 'boolean') zodType = 'z.boolean()'
368
+
369
+ if (!propDef.required) zodType += '.optional()'
370
+
371
+ fields.push(` ${name}: ${zodType},`)
372
+ }
373
+
374
+ return `const PropsSchema = z.object({
375
+ ${fields.join('\n')}
376
+ })`
377
+ }
378
+
379
+ /**
380
+ * Generate slots code
381
+ */
382
+ function generateSlotsCode(
383
+ slots: Slots,
384
+ imports: Set<string>,
385
+ errors: string[]
386
+ ): string {
387
+ const slotDefs: string[] = []
388
+
389
+ for (const [slotName, states] of Object.entries(slots)) {
390
+ const stateEntries: string[] = []
391
+
392
+ for (const [stateName, content] of Object.entries(states)) {
393
+ if (typeof content === 'string' && content.startsWith('$')) {
394
+ const jsx = convertLeafToJsx(content, 0, errors).trim()
395
+ stateEntries.push(` ${stateName}: ${jsx},`)
396
+ collectImports(content, imports)
397
+ }
398
+ }
399
+
400
+ slotDefs.push(` ${slotName}: {
401
+ ${stateEntries.join('\n')}
402
+ },`)
403
+ }
404
+
405
+ return `const slots = {
406
+ ${slotDefs.join('\n')}
407
+ }
408
+
409
+ `
410
+ }
411
+
412
+ /**
413
+ * Escape JSX special characters in text content
414
+ */
415
+ function escapeJsx(text: string): string {
416
+ return text
417
+ .replace(/&/g, '&amp;')
418
+ .replace(/</g, '&lt;')
419
+ .replace(/>/g, '&gt;')
420
+ .replace(/{/g, '&#123;')
421
+ .replace(/}/g, '&#125;')
422
+ }
423
+
424
+ /**
425
+ * Escape special characters in JSX attribute values
426
+ */
427
+ function escapeJsxAttribute(value: string): string {
428
+ return value
429
+ .replace(/\\/g, '\\\\')
430
+ .replace(/"/g, '\\"')
431
+ .replace(/\n/g, '\\n')
432
+ .replace(/\r/g, '\\r')
433
+ .replace(/\t/g, '\\t')
434
+ }
435
+
436
+ /**
437
+ * Check if a string is a valid JavaScript identifier
438
+ */
439
+ function isValidIdentifier(name: string): boolean {
440
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)
441
+ }
442
+
443
+ /**
444
+ * Convert string to PascalCase
445
+ */
446
+ function pascalCase(str: string): string {
447
+ return str
448
+ .split(/[-_\s]+/)
449
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
450
+ .join('')
451
+ }
@@ -0,0 +1,4 @@
1
+ // src/jsx/schemas/index.ts
2
+ // Re-export all schemas
3
+ export * from './tokens'
4
+ export * from './primitives'