prev-cli 0.24.18 → 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 (36) hide show
  1. package/package.json +5 -2
  2. package/src/jsx/adapters/html.test.ts +191 -0
  3. package/src/jsx/adapters/html.ts +404 -0
  4. package/src/jsx/adapters/react.test.ts +172 -0
  5. package/src/jsx/adapters/react.tsx +346 -0
  6. package/src/jsx/define-component.ts +129 -0
  7. package/src/jsx/index.ts +47 -0
  8. package/src/jsx/jsx-runtime.test.ts +63 -0
  9. package/src/jsx/jsx-runtime.ts +117 -0
  10. package/src/jsx/migrate.test.ts +72 -0
  11. package/src/jsx/migrate.ts +451 -0
  12. package/src/jsx/schemas/index.ts +4 -0
  13. package/src/jsx/schemas/primitives.ts +107 -0
  14. package/src/jsx/schemas/tokens.ts +60 -0
  15. package/src/jsx/validation.ts +77 -0
  16. package/src/jsx/vnode.ts +159 -0
  17. package/src/primitives/index.ts +8 -0
  18. package/src/primitives/migrate.test.ts +317 -0
  19. package/src/primitives/migrate.ts +364 -0
  20. package/src/primitives/parser.test.ts +265 -0
  21. package/src/primitives/parser.ts +442 -0
  22. package/src/primitives/template-parser.test.ts +297 -0
  23. package/src/primitives/template-parser.ts +374 -0
  24. package/src/primitives/template-renderer.test.ts +359 -0
  25. package/src/primitives/template-renderer.ts +497 -0
  26. package/src/primitives/tokens.css +82 -0
  27. package/src/primitives/types.ts +248 -0
  28. package/src/tokens/defaults.test.ts +137 -0
  29. package/src/tokens/defaults.ts +77 -0
  30. package/src/tokens/defaults.yaml +76 -0
  31. package/src/tokens/resolver.test.ts +229 -0
  32. package/src/tokens/resolver.ts +173 -0
  33. package/src/tokens/utils.test.ts +172 -0
  34. package/src/tokens/utils.ts +104 -0
  35. package/src/tokens/validation.test.ts +118 -0
  36. package/src/tokens/validation.ts +226 -0
@@ -0,0 +1,297 @@
1
+ // src/primitives/template-parser.test.ts
2
+ import { test, expect, describe } from 'bun:test'
3
+ import {
4
+ validateTemplate,
5
+ validateSlots,
6
+ extractRefs,
7
+ extractSlotNames,
8
+ } from './template-parser'
9
+
10
+ describe('validateTemplate', () => {
11
+ test('validates simple template with string root', () => {
12
+ const result = validateTemplate({
13
+ root: 'components/header',
14
+ })
15
+ expect(result.valid).toBe(true)
16
+ expect(result.errors).toHaveLength(0)
17
+ })
18
+
19
+ test('validates template with primitive root', () => {
20
+ const result = validateTemplate({
21
+ root: '$col(gap:lg)',
22
+ })
23
+ expect(result.valid).toBe(true)
24
+ })
25
+
26
+ test('validates template with container and children', () => {
27
+ const result = validateTemplate({
28
+ root: {
29
+ type: '$col(gap:lg)',
30
+ children: {
31
+ header: 'components/header',
32
+ main: '$slot(main)',
33
+ footer: 'components/footer',
34
+ },
35
+ },
36
+ })
37
+ expect(result.valid).toBe(true)
38
+ expect(result.errors).toHaveLength(0)
39
+ })
40
+
41
+ test('validates nested containers', () => {
42
+ const result = validateTemplate({
43
+ root: {
44
+ type: '$col',
45
+ children: {
46
+ card: {
47
+ type: '$box(padding:lg)',
48
+ children: {
49
+ content: 'components/content',
50
+ },
51
+ },
52
+ },
53
+ },
54
+ })
55
+ expect(result.valid).toBe(true)
56
+ })
57
+
58
+ test('rejects template without root', () => {
59
+ const result = validateTemplate({})
60
+ expect(result.valid).toBe(false)
61
+ expect(result.errors[0].message).toContain('root')
62
+ })
63
+
64
+ test('rejects container without type', () => {
65
+ const result = validateTemplate({
66
+ root: {
67
+ children: {
68
+ header: 'components/header',
69
+ },
70
+ },
71
+ })
72
+ expect(result.valid).toBe(false)
73
+ expect(result.errors[0].message).toContain('type')
74
+ })
75
+
76
+ test('rejects invalid primitive syntax', () => {
77
+ const result = validateTemplate({
78
+ root: '$col(gap:invalid)',
79
+ })
80
+ expect(result.valid).toBe(false)
81
+ })
82
+
83
+ test('rejects non-primitive type in container', () => {
84
+ const result = validateTemplate({
85
+ root: {
86
+ type: 'NotAPrimitive',
87
+ children: {},
88
+ },
89
+ })
90
+ expect(result.valid).toBe(false)
91
+ })
92
+
93
+ test('rejects invalid ref format', () => {
94
+ const result = validateTemplate({
95
+ root: 'invalid/path/format',
96
+ })
97
+ expect(result.valid).toBe(false)
98
+ })
99
+
100
+ test('rejects non-layout primitive with children', () => {
101
+ const result = validateTemplate({
102
+ root: {
103
+ type: '$text("hello")',
104
+ children: {
105
+ child: 'components/button',
106
+ },
107
+ },
108
+ })
109
+ expect(result.valid).toBe(false)
110
+ expect(result.errors[0].message).toContain('cannot have children')
111
+ })
112
+
113
+ test('rejects $spacer with children', () => {
114
+ const result = validateTemplate({
115
+ root: {
116
+ type: '$spacer',
117
+ children: {
118
+ child: 'components/button',
119
+ },
120
+ },
121
+ })
122
+ expect(result.valid).toBe(false)
123
+ expect(result.errors[0].message).toContain('cannot have children')
124
+ })
125
+
126
+ test('rejects $image with children', () => {
127
+ const result = validateTemplate({
128
+ root: {
129
+ type: '$image(src:url)',
130
+ children: {
131
+ child: 'components/button',
132
+ },
133
+ },
134
+ })
135
+ expect(result.valid).toBe(false)
136
+ })
137
+ })
138
+
139
+ describe('validateSlots', () => {
140
+ test('validates simple slots', () => {
141
+ const result = validateSlots({
142
+ main: {
143
+ default: 'components/home',
144
+ loading: 'components/spinner',
145
+ },
146
+ })
147
+ expect(result.valid).toBe(true)
148
+ })
149
+
150
+ test('validates slots with defined states', () => {
151
+ const result = validateSlots(
152
+ {
153
+ form: {
154
+ default: 'components/login-form',
155
+ error: 'components/error-form',
156
+ },
157
+ },
158
+ ['default', 'error', 'success']
159
+ )
160
+ expect(result.valid).toBe(true)
161
+ })
162
+
163
+ test('rejects slots referencing undefined states', () => {
164
+ const result = validateSlots(
165
+ {
166
+ form: {
167
+ default: 'components/form',
168
+ nonexistent: 'components/other',
169
+ },
170
+ },
171
+ ['default', 'error']
172
+ )
173
+ expect(result.valid).toBe(false)
174
+ expect(result.errors[0].message).toContain('nonexistent')
175
+ })
176
+
177
+ test('rejects non-string slot content', () => {
178
+ const result = validateSlots({
179
+ main: {
180
+ default: { invalid: 'object' },
181
+ },
182
+ })
183
+ expect(result.valid).toBe(false)
184
+ })
185
+
186
+ test('accepts primitive as slot content', () => {
187
+ const result = validateSlots({
188
+ main: {
189
+ loading: '$text("Loading...")',
190
+ },
191
+ })
192
+ expect(result.valid).toBe(true)
193
+ })
194
+
195
+ test('rejects URL as slot content', () => {
196
+ const result = validateSlots({
197
+ main: {
198
+ default: 'https://example.com/image.jpg',
199
+ },
200
+ })
201
+ expect(result.valid).toBe(false)
202
+ expect(result.errors[0].message).toContain('Invalid ref format')
203
+ })
204
+
205
+ test('rejects invalid ref pattern in slot', () => {
206
+ const result = validateSlots({
207
+ main: {
208
+ default: 'random/path/with/many/parts',
209
+ },
210
+ })
211
+ expect(result.valid).toBe(false)
212
+ })
213
+ })
214
+
215
+ describe('extractRefs', () => {
216
+ test('extracts refs from template', () => {
217
+ const refs = extractRefs({
218
+ root: {
219
+ type: '$col',
220
+ children: {
221
+ header: 'components/header',
222
+ main: 'screens/main',
223
+ footer: 'components/footer',
224
+ },
225
+ },
226
+ })
227
+ expect(refs).toContain('components/header')
228
+ expect(refs).toContain('screens/main')
229
+ expect(refs).toContain('components/footer')
230
+ })
231
+
232
+ test('ignores primitives', () => {
233
+ const refs = extractRefs({
234
+ root: {
235
+ type: '$col',
236
+ children: {
237
+ spacer: '$spacer(xl)',
238
+ slot: '$slot(main)',
239
+ },
240
+ },
241
+ })
242
+ expect(refs).toHaveLength(0)
243
+ })
244
+
245
+ test('deduplicates refs', () => {
246
+ const refs = extractRefs({
247
+ root: {
248
+ type: '$col',
249
+ children: {
250
+ header1: 'components/header',
251
+ header2: 'components/header',
252
+ },
253
+ },
254
+ })
255
+ expect(refs).toHaveLength(1)
256
+ })
257
+ })
258
+
259
+ describe('extractSlotNames', () => {
260
+ test('extracts slot names from template', () => {
261
+ const slots = extractSlotNames({
262
+ root: {
263
+ type: '$col',
264
+ children: {
265
+ header: 'components/header',
266
+ main: '$slot(main)',
267
+ sidebar: '$slot(sidebar)',
268
+ },
269
+ },
270
+ })
271
+ expect(slots).toContain('main')
272
+ expect(slots).toContain('sidebar')
273
+ expect(slots).toHaveLength(2)
274
+ })
275
+
276
+ test('extracts slot from container type', () => {
277
+ const slots = extractSlotNames({
278
+ root: {
279
+ type: '$slot(content)',
280
+ },
281
+ })
282
+ expect(slots).toContain('content')
283
+ })
284
+
285
+ test('deduplicates slot names', () => {
286
+ const slots = extractSlotNames({
287
+ root: {
288
+ type: '$col',
289
+ children: {
290
+ slot1: '$slot(main)',
291
+ slot2: '$slot(main)',
292
+ },
293
+ },
294
+ })
295
+ expect(slots).toHaveLength(1)
296
+ })
297
+ })
@@ -0,0 +1,374 @@
1
+ // src/primitives/template-parser.ts
2
+ // Parser for map-based template structures
3
+
4
+ import {
5
+ isPrimitive,
6
+ isRef,
7
+ isContainerNode,
8
+ type TemplateNode,
9
+ type Template,
10
+ } from './types'
11
+ import { parsePrimitive, getPrimitiveType } from './parser'
12
+
13
+ // Valid container primitive types that can have children
14
+ const CONTAINER_TYPES = new Set(['$col', '$row', '$box'])
15
+
16
+ // Strict ref pattern: type/id format
17
+ const REF_PATTERN = /^(screens|components|flows|atlas)\/[a-z0-9-]+$/
18
+
19
+ export interface TemplateParseError {
20
+ path: string
21
+ message: string
22
+ }
23
+
24
+ export interface TemplateParseResult {
25
+ valid: boolean
26
+ errors: TemplateParseError[]
27
+ warnings: TemplateParseError[]
28
+ }
29
+
30
+ /**
31
+ * Validate a template structure
32
+ */
33
+ export function validateTemplate(template: unknown): TemplateParseResult {
34
+ const errors: TemplateParseError[] = []
35
+ const warnings: TemplateParseError[] = []
36
+
37
+ if (!template || typeof template !== 'object') {
38
+ errors.push({
39
+ path: '/template',
40
+ message: 'Template must be an object',
41
+ })
42
+ return { valid: false, errors, warnings }
43
+ }
44
+
45
+ const templateObj = template as Record<string, unknown>
46
+
47
+ if (!('root' in templateObj)) {
48
+ errors.push({
49
+ path: '/template',
50
+ message: 'Template must have a root node',
51
+ })
52
+ return { valid: false, errors, warnings }
53
+ }
54
+
55
+ // Validate the root node recursively
56
+ validateNode(templateObj.root, '/template/root', errors, warnings)
57
+
58
+ return {
59
+ valid: errors.length === 0,
60
+ errors,
61
+ warnings,
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Validate a single template node (recursive)
67
+ */
68
+ function validateNode(
69
+ node: unknown,
70
+ path: string,
71
+ errors: TemplateParseError[],
72
+ warnings: TemplateParseError[]
73
+ ): void {
74
+ // String leaf node: primitive or ref
75
+ if (typeof node === 'string') {
76
+ validateLeafValue(node, path, errors, warnings)
77
+ return
78
+ }
79
+
80
+ // Container node: { type: string, children: Record<string, node> }
81
+ if (typeof node === 'object' && node !== null) {
82
+ const nodeObj = node as Record<string, unknown>
83
+
84
+ if (!('type' in nodeObj)) {
85
+ errors.push({
86
+ path,
87
+ message: 'Container node must have a "type" field',
88
+ })
89
+ return
90
+ }
91
+
92
+ if (typeof nodeObj.type !== 'string') {
93
+ errors.push({
94
+ path: `${path}/type`,
95
+ message: 'Node type must be a string',
96
+ })
97
+ return
98
+ }
99
+
100
+ // Validate the type is a valid primitive
101
+ if (!isPrimitive(nodeObj.type)) {
102
+ errors.push({
103
+ path: `${path}/type`,
104
+ message: `Node type must be a primitive (start with $): ${nodeObj.type}`,
105
+ })
106
+ } else {
107
+ const parseResult = parsePrimitive(nodeObj.type)
108
+ if (!parseResult.success) {
109
+ errors.push({
110
+ path: `${path}/type`,
111
+ message: parseResult.error.message,
112
+ })
113
+ }
114
+ }
115
+
116
+ // Validate children if present
117
+ if ('children' in nodeObj) {
118
+ // First check if this primitive type can have children
119
+ const primitiveType = getPrimitiveType(nodeObj.type)
120
+ if (primitiveType && !CONTAINER_TYPES.has(primitiveType)) {
121
+ errors.push({
122
+ path: `${path}/children`,
123
+ message: `Primitive ${primitiveType} cannot have children. Only $col, $row, $box support children.`,
124
+ })
125
+ }
126
+
127
+ if (typeof nodeObj.children !== 'object' || nodeObj.children === null) {
128
+ errors.push({
129
+ path: `${path}/children`,
130
+ message: 'Children must be an object',
131
+ })
132
+ } else {
133
+ const children = nodeObj.children as Record<string, unknown>
134
+ for (const [childId, childNode] of Object.entries(children)) {
135
+ validateNode(childNode, `${path}/children/${childId}`, errors, warnings)
136
+ }
137
+ }
138
+ }
139
+
140
+ return
141
+ }
142
+
143
+ errors.push({
144
+ path,
145
+ message: `Invalid node type: expected string or object, got ${typeof node}`,
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Validate a leaf value (primitive string or component ref)
151
+ */
152
+ function validateLeafValue(
153
+ value: string,
154
+ path: string,
155
+ errors: TemplateParseError[],
156
+ warnings: TemplateParseError[]
157
+ ): void {
158
+ if (isPrimitive(value)) {
159
+ // Validate primitive syntax
160
+ const parseResult = parsePrimitive(value)
161
+ if (!parseResult.success) {
162
+ errors.push({
163
+ path,
164
+ message: parseResult.error.message,
165
+ })
166
+ }
167
+ } else if (isRef(value)) {
168
+ // Validate ref format
169
+ if (!REF_PATTERN.test(value)) {
170
+ errors.push({
171
+ path,
172
+ message: `Invalid ref format: ${value}. Expected format: type/id (e.g., components/button)`,
173
+ })
174
+ }
175
+ } else {
176
+ // Unknown format - could be a prop reference or literal
177
+ // Warn if it doesn't look like either
178
+ if (!value.match(/^[a-z][a-zA-Z0-9]*$/) && !value.startsWith('"') && !value.startsWith("'")) {
179
+ warnings.push({
180
+ path,
181
+ message: `Ambiguous value: ${value}. Use $primitive, type/ref, or a valid prop name.`,
182
+ })
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Validate slots structure
189
+ */
190
+ export function validateSlots(
191
+ slots: unknown,
192
+ definedStates?: string[]
193
+ ): TemplateParseResult {
194
+ const errors: TemplateParseError[] = []
195
+ const warnings: TemplateParseError[] = []
196
+
197
+ if (!slots || typeof slots !== 'object') {
198
+ errors.push({
199
+ path: '/slots',
200
+ message: 'Slots must be an object',
201
+ })
202
+ return { valid: false, errors, warnings }
203
+ }
204
+
205
+ const slotsObj = slots as Record<string, unknown>
206
+
207
+ for (const [slotName, stateMapping] of Object.entries(slotsObj)) {
208
+ if (typeof stateMapping !== 'object' || stateMapping === null) {
209
+ errors.push({
210
+ path: `/slots/${slotName}`,
211
+ message: 'Slot state mapping must be an object',
212
+ })
213
+ continue
214
+ }
215
+
216
+ const mapping = stateMapping as Record<string, unknown>
217
+
218
+ for (const [stateName, content] of Object.entries(mapping)) {
219
+ // Validate state name exists if states are defined
220
+ if (definedStates && definedStates.length > 0 && !definedStates.includes(stateName)) {
221
+ errors.push({
222
+ path: `/slots/${slotName}/${stateName}`,
223
+ message: `Unknown state "${stateName}". Defined states: ${definedStates.join(', ')}`,
224
+ })
225
+ }
226
+
227
+ // Validate content is a string ref or primitive
228
+ if (typeof content !== 'string') {
229
+ errors.push({
230
+ path: `/slots/${slotName}/${stateName}`,
231
+ message: 'Slot content must be a string (component ref)',
232
+ })
233
+ } else if (isPrimitive(content)) {
234
+ // Valid primitive - check syntax
235
+ const parseResult = parsePrimitive(content)
236
+ if (!parseResult.success) {
237
+ errors.push({
238
+ path: `/slots/${slotName}/${stateName}`,
239
+ message: parseResult.error.message,
240
+ })
241
+ }
242
+ } else if (isRef(content)) {
243
+ // Must match strict ref pattern
244
+ if (!REF_PATTERN.test(content)) {
245
+ errors.push({
246
+ path: `/slots/${slotName}/${stateName}`,
247
+ message: `Invalid ref format: ${content}. Expected type/id (e.g., components/button)`,
248
+ })
249
+ }
250
+ } else {
251
+ errors.push({
252
+ path: `/slots/${slotName}/${stateName}`,
253
+ message: `Invalid slot content: ${content}. Expected component ref or primitive.`,
254
+ })
255
+ }
256
+ }
257
+ }
258
+
259
+ return {
260
+ valid: errors.length === 0,
261
+ errors,
262
+ warnings,
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Extract all component refs from a template
268
+ */
269
+ export function extractRefs(template: unknown): string[] {
270
+ const refs: string[] = []
271
+ extractRefsFromNode(template, refs)
272
+ return [...new Set(refs)] // Deduplicate
273
+ }
274
+
275
+ function extractRefsFromNode(node: unknown, refs: string[]): void {
276
+ if (typeof node === 'string') {
277
+ if (isRef(node)) {
278
+ refs.push(node)
279
+ }
280
+ return
281
+ }
282
+
283
+ if (typeof node === 'object' && node !== null) {
284
+ const nodeObj = node as Record<string, unknown>
285
+
286
+ // Check root
287
+ if ('root' in nodeObj) {
288
+ extractRefsFromNode(nodeObj.root, refs)
289
+ }
290
+
291
+ // Check children
292
+ if ('children' in nodeObj && typeof nodeObj.children === 'object' && nodeObj.children !== null) {
293
+ const children = nodeObj.children as Record<string, unknown>
294
+ for (const childNode of Object.values(children)) {
295
+ extractRefsFromNode(childNode, refs)
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Extract all slot names from a template
303
+ */
304
+ export function extractSlotNames(template: unknown): string[] {
305
+ const slots: string[] = []
306
+ extractSlotsFromNode(template, slots)
307
+ return [...new Set(slots)]
308
+ }
309
+
310
+ function extractSlotsFromNode(node: unknown, slots: string[]): void {
311
+ if (typeof node === 'string') {
312
+ if (isPrimitive(node)) {
313
+ const primitiveType = getPrimitiveType(node)
314
+ if (primitiveType === '$slot') {
315
+ const parseResult = parsePrimitive(node)
316
+ if (parseResult.success && parseResult.primitive.type === '$slot') {
317
+ slots.push(parseResult.primitive.name)
318
+ }
319
+ }
320
+ }
321
+ return
322
+ }
323
+
324
+ if (typeof node === 'object' && node !== null) {
325
+ const nodeObj = node as Record<string, unknown>
326
+
327
+ // Check type
328
+ if ('type' in nodeObj && typeof nodeObj.type === 'string' && isPrimitive(nodeObj.type)) {
329
+ const primitiveType = getPrimitiveType(nodeObj.type)
330
+ if (primitiveType === '$slot') {
331
+ const parseResult = parsePrimitive(nodeObj.type)
332
+ if (parseResult.success && parseResult.primitive.type === '$slot') {
333
+ slots.push(parseResult.primitive.name)
334
+ }
335
+ }
336
+ }
337
+
338
+ // Check root
339
+ if ('root' in nodeObj) {
340
+ extractSlotsFromNode(nodeObj.root, slots)
341
+ }
342
+
343
+ // Check children
344
+ if ('children' in nodeObj && typeof nodeObj.children === 'object' && nodeObj.children !== null) {
345
+ const children = nodeObj.children as Record<string, unknown>
346
+ for (const childNode of Object.values(children)) {
347
+ extractSlotsFromNode(childNode, slots)
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Flatten a template into a list of nodes with paths
355
+ */
356
+ export function flattenTemplate(template: Template): Array<{ path: string; node: TemplateNode }> {
357
+ const result: Array<{ path: string; node: TemplateNode }> = []
358
+ flattenNode(template.root, '/root', result)
359
+ return result
360
+ }
361
+
362
+ function flattenNode(
363
+ node: TemplateNode,
364
+ path: string,
365
+ result: Array<{ path: string; node: TemplateNode }>
366
+ ): void {
367
+ result.push({ path, node })
368
+
369
+ if (isContainerNode(node) && node.children) {
370
+ for (const [childId, childNode] of Object.entries(node.children)) {
371
+ flattenNode(childNode, `${path}/${childId}`, result)
372
+ }
373
+ }
374
+ }