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.
- package/dist/cli.js +2 -1
- package/package.json +5 -2
- package/src/jsx/adapters/html.test.ts +191 -0
- package/src/jsx/adapters/html.ts +404 -0
- package/src/jsx/adapters/react.test.ts +172 -0
- package/src/jsx/adapters/react.tsx +346 -0
- package/src/jsx/define-component.ts +129 -0
- package/src/jsx/index.ts +47 -0
- package/src/jsx/jsx-runtime.test.ts +63 -0
- package/src/jsx/jsx-runtime.ts +117 -0
- package/src/jsx/migrate.test.ts +72 -0
- package/src/jsx/migrate.ts +451 -0
- package/src/jsx/schemas/index.ts +4 -0
- package/src/jsx/schemas/primitives.ts +107 -0
- package/src/jsx/schemas/tokens.ts +60 -0
- package/src/jsx/validation.ts +77 -0
- package/src/jsx/vnode.ts +159 -0
- package/src/primitives/index.ts +8 -0
- package/src/primitives/migrate.test.ts +317 -0
- package/src/primitives/migrate.ts +364 -0
- package/src/primitives/parser.test.ts +265 -0
- package/src/primitives/parser.ts +442 -0
- package/src/primitives/template-parser.test.ts +297 -0
- package/src/primitives/template-parser.ts +374 -0
- package/src/primitives/template-renderer.test.ts +359 -0
- package/src/primitives/template-renderer.ts +497 -0
- package/src/primitives/tokens.css +82 -0
- package/src/primitives/types.ts +248 -0
- package/src/tokens/defaults.test.ts +137 -0
- package/src/tokens/defaults.ts +77 -0
- package/src/tokens/defaults.yaml +76 -0
- package/src/tokens/resolver.test.ts +229 -0
- package/src/tokens/resolver.ts +173 -0
- package/src/tokens/utils.test.ts +172 -0
- package/src/tokens/utils.ts +104 -0
- package/src/tokens/validation.test.ts +118 -0
- 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
|
+
}
|