@zenithbuild/core 0.4.2 → 0.4.4
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/cli/commands/dev.ts +4 -1
- package/compiler/discovery/componentDiscovery.ts +174 -0
- package/compiler/errors/compilerError.ts +32 -0
- package/compiler/finalize/finalizeOutput.ts +37 -8
- package/compiler/index.ts +26 -5
- package/compiler/ir/types.ts +66 -0
- package/compiler/parse/parseTemplate.ts +66 -9
- package/compiler/runtime/generateDOM.ts +102 -1
- package/compiler/runtime/transformIR.ts +2 -2
- package/compiler/transform/classifyExpression.ts +444 -0
- package/compiler/transform/componentResolver.ts +289 -0
- package/compiler/transform/fragmentLowering.ts +634 -0
- package/compiler/transform/slotResolver.ts +292 -0
- package/compiler/validate/invariants.ts +292 -0
- package/package.json +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Resolution
|
|
3
|
+
*
|
|
4
|
+
* Resolves component nodes in IR by inlining component templates with slot substitution.
|
|
5
|
+
* Uses compound component pattern for named slots (Card.Header, Card.Footer).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TemplateNode, ComponentNode, ElementNode, ZenIR, LoopContext } from '../ir/types'
|
|
9
|
+
import type { ComponentMetadata } from '../discovery/componentDiscovery'
|
|
10
|
+
import { extractSlotsFromChildren, resolveSlots } from './slotResolver'
|
|
11
|
+
import { throwOrphanCompoundError, throwUnresolvedComponentError } from '../validate/invariants'
|
|
12
|
+
|
|
13
|
+
// Track which components have been used (for style collection)
|
|
14
|
+
const usedComponents = new Set<string>()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve all component nodes in a template IR
|
|
18
|
+
*
|
|
19
|
+
* Recursively replaces ComponentNode instances with their resolved templates
|
|
20
|
+
* Also collects styles from used components and adds them to the IR
|
|
21
|
+
*/
|
|
22
|
+
export function resolveComponentsInIR(
|
|
23
|
+
ir: ZenIR,
|
|
24
|
+
components: Map<string, ComponentMetadata>
|
|
25
|
+
): ZenIR {
|
|
26
|
+
// Clear used components tracking for this compilation
|
|
27
|
+
usedComponents.clear()
|
|
28
|
+
|
|
29
|
+
// Resolve components in template nodes
|
|
30
|
+
const resolvedNodes = resolveComponentsInNodes(ir.template.nodes, components)
|
|
31
|
+
|
|
32
|
+
// Collect styles from all used components
|
|
33
|
+
const componentStyles = Array.from(usedComponents)
|
|
34
|
+
.map(name => components.get(name))
|
|
35
|
+
.filter((meta): meta is ComponentMetadata => meta !== undefined && meta.styles.length > 0)
|
|
36
|
+
.flatMap(meta => meta.styles.map(raw => ({ raw })))
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
...ir,
|
|
40
|
+
template: {
|
|
41
|
+
...ir.template,
|
|
42
|
+
nodes: resolvedNodes
|
|
43
|
+
},
|
|
44
|
+
// Merge component styles with existing page styles
|
|
45
|
+
styles: [...ir.styles, ...componentStyles]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve component nodes in a list of template nodes
|
|
51
|
+
*/
|
|
52
|
+
function resolveComponentsInNodes(
|
|
53
|
+
nodes: TemplateNode[],
|
|
54
|
+
components: Map<string, ComponentMetadata>,
|
|
55
|
+
depth: number = 0
|
|
56
|
+
): TemplateNode[] {
|
|
57
|
+
const resolved: TemplateNode[] = []
|
|
58
|
+
|
|
59
|
+
for (const node of nodes) {
|
|
60
|
+
const resolvedNode = resolveComponentNode(node, components, depth)
|
|
61
|
+
|
|
62
|
+
if (Array.isArray(resolvedNode)) {
|
|
63
|
+
resolved.push(...resolvedNode)
|
|
64
|
+
} else {
|
|
65
|
+
resolved.push(resolvedNode)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return resolved
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a single component node
|
|
74
|
+
*
|
|
75
|
+
* If the node is a component, look up its definition and inline it with slot resolution.
|
|
76
|
+
* Otherwise, recursively process children.
|
|
77
|
+
*/
|
|
78
|
+
function resolveComponentNode(
|
|
79
|
+
node: TemplateNode,
|
|
80
|
+
components: Map<string, ComponentMetadata>,
|
|
81
|
+
depth: number = 0
|
|
82
|
+
): TemplateNode | TemplateNode[] {
|
|
83
|
+
// Handle component nodes
|
|
84
|
+
if (node.type === 'component') {
|
|
85
|
+
return resolveComponent(node, components, depth)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle element nodes - recursively resolve children
|
|
89
|
+
if (node.type === 'element') {
|
|
90
|
+
const resolvedChildren = resolveComponentsInNodes(node.children, components, depth + 1)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
...node,
|
|
94
|
+
children: resolvedChildren
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Text and expression nodes pass through unchanged
|
|
99
|
+
return node
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get base component name from compound name
|
|
104
|
+
*
|
|
105
|
+
* "Card.Header" -> "Card"
|
|
106
|
+
* "Button" -> "Button"
|
|
107
|
+
*/
|
|
108
|
+
function getBaseComponentName(name: string): string {
|
|
109
|
+
const dotIndex = name.indexOf('.')
|
|
110
|
+
return dotIndex > 0 ? name.slice(0, dotIndex) : name
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a component name is a compound slot marker
|
|
115
|
+
*
|
|
116
|
+
* "Card.Header" -> true (if Card exists)
|
|
117
|
+
* "Card" -> false
|
|
118
|
+
* "Button" -> false
|
|
119
|
+
*/
|
|
120
|
+
function isCompoundSlotMarker(name: string, components: Map<string, ComponentMetadata>): boolean {
|
|
121
|
+
const dotIndex = name.indexOf('.')
|
|
122
|
+
if (dotIndex <= 0) return false
|
|
123
|
+
|
|
124
|
+
const baseName = name.slice(0, dotIndex)
|
|
125
|
+
return components.has(baseName)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resolve a component by inlining its template with slot substitution
|
|
130
|
+
*/
|
|
131
|
+
function resolveComponent(
|
|
132
|
+
componentNode: ComponentNode,
|
|
133
|
+
components: Map<string, ComponentMetadata>,
|
|
134
|
+
depth: number = 0
|
|
135
|
+
): TemplateNode | TemplateNode[] {
|
|
136
|
+
const componentName = componentNode.name
|
|
137
|
+
|
|
138
|
+
// Check if this is a compound slot marker (Card.Header, Card.Footer)
|
|
139
|
+
// These are handled by the parent component, not resolved directly
|
|
140
|
+
// INV007: Orphan compound slot markers are a compile-time error
|
|
141
|
+
if (isCompoundSlotMarker(componentName, components)) {
|
|
142
|
+
throwOrphanCompoundError(
|
|
143
|
+
componentName,
|
|
144
|
+
getBaseComponentName(componentName),
|
|
145
|
+
'component', // filePath not available here, will be caught by caller
|
|
146
|
+
componentNode.location.line,
|
|
147
|
+
componentNode.location.column
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Look up component metadata
|
|
152
|
+
const componentMeta = components.get(componentName)
|
|
153
|
+
|
|
154
|
+
// INV003: Unresolved components are a compile-time error
|
|
155
|
+
if (!componentMeta) {
|
|
156
|
+
throwUnresolvedComponentError(
|
|
157
|
+
componentName,
|
|
158
|
+
'component', // filePath not available here, will be caught by caller
|
|
159
|
+
componentNode.location.line,
|
|
160
|
+
componentNode.location.column
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Track this component as used (for style collection)
|
|
165
|
+
usedComponents.add(componentName)
|
|
166
|
+
|
|
167
|
+
// Extract slots from component children FIRST (before resolving nested components)
|
|
168
|
+
// This preserves compound component structure (Card.Header, Card.Footer)
|
|
169
|
+
// IMPORTANT: Pass parent's loopContext to preserve reactive scope
|
|
170
|
+
// Components are purely structural - they don't create new reactive boundaries
|
|
171
|
+
const slots = extractSlotsFromChildren(
|
|
172
|
+
componentName,
|
|
173
|
+
componentNode.children,
|
|
174
|
+
componentNode.loopContext // Preserve parent's reactive scope
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
// Now resolve nested components within the extracted slot content
|
|
178
|
+
const resolvedSlots = {
|
|
179
|
+
default: resolveComponentsInNodes(slots.default, components, depth + 1),
|
|
180
|
+
named: new Map<string, TemplateNode[]>(),
|
|
181
|
+
parentLoopContext: slots.parentLoopContext // Carry through the parent scope
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const [slotName, slotContent] of slots.named) {
|
|
185
|
+
resolvedSlots.named.set(slotName, resolveComponentsInNodes(slotContent, components, depth + 1))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Deep clone the component template nodes to avoid mutation
|
|
189
|
+
const templateNodes = JSON.parse(JSON.stringify(componentMeta.nodes)) as TemplateNode[]
|
|
190
|
+
|
|
191
|
+
// Resolve slots in component template
|
|
192
|
+
const resolvedTemplate = resolveSlots(templateNodes, resolvedSlots)
|
|
193
|
+
|
|
194
|
+
// Forward attributes from component usage to the root element
|
|
195
|
+
// This ensures onclick, class, and other attributes are preserved
|
|
196
|
+
const forwardedTemplate = forwardAttributesToRoot(
|
|
197
|
+
resolvedTemplate,
|
|
198
|
+
componentNode.attributes,
|
|
199
|
+
componentNode.loopContext
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Recursively resolve any nested components in the resolved template
|
|
203
|
+
const fullyResolved = resolveComponentsInNodes(forwardedTemplate, components, depth + 1)
|
|
204
|
+
|
|
205
|
+
return fullyResolved
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Forward attributes from component usage to the template's root element
|
|
210
|
+
*
|
|
211
|
+
* When using <Button onclick="increment">Text</Button>,
|
|
212
|
+
* the onclick should be applied to the <button> element in Button.zen template.
|
|
213
|
+
*/
|
|
214
|
+
function forwardAttributesToRoot(
|
|
215
|
+
nodes: TemplateNode[],
|
|
216
|
+
attributes: ComponentNode['attributes'],
|
|
217
|
+
loopContext?: LoopContext
|
|
218
|
+
): TemplateNode[] {
|
|
219
|
+
if (attributes.length === 0) {
|
|
220
|
+
return nodes
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Find the first non-text element (the root element)
|
|
224
|
+
const rootIndex = nodes.findIndex(n => n.type === 'element')
|
|
225
|
+
if (rootIndex === -1) {
|
|
226
|
+
return nodes
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const root = nodes[rootIndex] as ElementNode
|
|
230
|
+
|
|
231
|
+
// Merge attributes: component usage attributes override template defaults
|
|
232
|
+
// Also preserve the parent's loopContext on forwarded attributes
|
|
233
|
+
const mergedAttributes = [...root.attributes]
|
|
234
|
+
|
|
235
|
+
for (const attr of attributes) {
|
|
236
|
+
const existingIndex = mergedAttributes.findIndex(a => a.name === attr.name)
|
|
237
|
+
|
|
238
|
+
// Attach parent's loopContext to forwarded attributes to preserve reactivity
|
|
239
|
+
const forwardedAttr = {
|
|
240
|
+
...attr,
|
|
241
|
+
loopContext: attr.loopContext || loopContext
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (existingIndex >= 0) {
|
|
245
|
+
const existingAttr = mergedAttributes[existingIndex]!
|
|
246
|
+
// Special handling for class: merge classes
|
|
247
|
+
if (attr.name === 'class' && typeof attr.value === 'string' && typeof existingAttr.value === 'string') {
|
|
248
|
+
mergedAttributes[existingIndex] = {
|
|
249
|
+
...existingAttr,
|
|
250
|
+
value: `${existingAttr.value} ${attr.value}`
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Override other attributes
|
|
254
|
+
mergedAttributes[existingIndex] = forwardedAttr
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Add new attribute
|
|
258
|
+
mergedAttributes.push(forwardedAttr)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Return updated nodes with root element having merged attributes
|
|
263
|
+
return [
|
|
264
|
+
...nodes.slice(0, rootIndex),
|
|
265
|
+
{
|
|
266
|
+
...root,
|
|
267
|
+
attributes: mergedAttributes,
|
|
268
|
+
loopContext: root.loopContext || loopContext
|
|
269
|
+
},
|
|
270
|
+
...nodes.slice(rootIndex + 1)
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if an IR contains any component nodes
|
|
276
|
+
*/
|
|
277
|
+
export function hasComponents(nodes: TemplateNode[]): boolean {
|
|
278
|
+
function checkNode(node: TemplateNode): boolean {
|
|
279
|
+
if (node.type === 'component') {
|
|
280
|
+
return true
|
|
281
|
+
}
|
|
282
|
+
if (node.type === 'element') {
|
|
283
|
+
return node.children.some(checkNode)
|
|
284
|
+
}
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return nodes.some(checkNode)
|
|
289
|
+
}
|