@zenithbuild/core 1.2.2 → 1.2.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.
Files changed (83) hide show
  1. package/README.md +20 -19
  2. package/cli/commands/add.ts +2 -2
  3. package/cli/commands/build.ts +2 -3
  4. package/cli/commands/dev.ts +94 -74
  5. package/cli/commands/index.ts +1 -1
  6. package/cli/commands/preview.ts +1 -1
  7. package/cli/commands/remove.ts +2 -2
  8. package/cli/index.ts +1 -1
  9. package/cli/main.ts +1 -1
  10. package/cli/utils/logger.ts +1 -1
  11. package/cli/utils/plugin-manager.ts +1 -1
  12. package/cli/utils/project.ts +4 -4
  13. package/core/components/ErrorPage.zen +218 -0
  14. package/core/components/index.ts +15 -0
  15. package/core/config.ts +1 -0
  16. package/core/index.ts +29 -0
  17. package/dist/compiler-native-frej59m4.node +0 -0
  18. package/dist/core/compiler-native-frej59m4.node +0 -0
  19. package/dist/core/index.js +6293 -0
  20. package/dist/runtime/lifecycle/index.js +1 -0
  21. package/dist/runtime/reactivity/index.js +1 -0
  22. package/dist/zen-build.js +1 -20118
  23. package/dist/zen-dev.js +1 -20118
  24. package/dist/zen-preview.js +1 -20118
  25. package/dist/zenith.js +1 -20118
  26. package/package.json +11 -20
  27. package/compiler/README.md +0 -380
  28. package/compiler/build-analyzer.ts +0 -122
  29. package/compiler/css/index.ts +0 -317
  30. package/compiler/discovery/componentDiscovery.ts +0 -242
  31. package/compiler/discovery/layouts.ts +0 -70
  32. package/compiler/errors/compilerError.ts +0 -56
  33. package/compiler/finalize/finalizeOutput.ts +0 -192
  34. package/compiler/finalize/generateFinalBundle.ts +0 -82
  35. package/compiler/index.ts +0 -83
  36. package/compiler/ir/types.ts +0 -174
  37. package/compiler/output/types.ts +0 -48
  38. package/compiler/parse/detectMapExpressions.ts +0 -102
  39. package/compiler/parse/importTypes.ts +0 -78
  40. package/compiler/parse/parseImports.ts +0 -309
  41. package/compiler/parse/parseScript.ts +0 -46
  42. package/compiler/parse/parseTemplate.ts +0 -628
  43. package/compiler/parse/parseZenFile.ts +0 -66
  44. package/compiler/parse/scriptAnalysis.ts +0 -91
  45. package/compiler/parse/trackLoopContext.ts +0 -82
  46. package/compiler/runtime/dataExposure.ts +0 -332
  47. package/compiler/runtime/generateDOM.ts +0 -255
  48. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  49. package/compiler/runtime/hydration.ts +0 -309
  50. package/compiler/runtime/navigation.ts +0 -432
  51. package/compiler/runtime/thinRuntime.ts +0 -160
  52. package/compiler/runtime/transformIR.ts +0 -406
  53. package/compiler/runtime/wrapExpression.ts +0 -114
  54. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -97
  55. package/compiler/spa-build.ts +0 -917
  56. package/compiler/ssg-build.ts +0 -486
  57. package/compiler/test/component-stacking.test.ts +0 -365
  58. package/compiler/test/map-lowering.test.ts +0 -130
  59. package/compiler/test/validate-test.ts +0 -104
  60. package/compiler/transform/classifyExpression.ts +0 -444
  61. package/compiler/transform/componentResolver.ts +0 -350
  62. package/compiler/transform/componentScriptTransformer.ts +0 -303
  63. package/compiler/transform/expressionTransformer.ts +0 -385
  64. package/compiler/transform/fragmentLowering.ts +0 -819
  65. package/compiler/transform/generateBindings.ts +0 -68
  66. package/compiler/transform/generateHTML.ts +0 -28
  67. package/compiler/transform/layoutProcessor.ts +0 -132
  68. package/compiler/transform/slotResolver.ts +0 -292
  69. package/compiler/transform/transformNode.ts +0 -314
  70. package/compiler/transform/transformTemplate.ts +0 -38
  71. package/compiler/validate/invariants.ts +0 -292
  72. package/compiler/validate/validateExpressions.ts +0 -168
  73. package/core/config/index.ts +0 -18
  74. package/core/config/loader.ts +0 -69
  75. package/core/config/types.ts +0 -119
  76. package/core/plugins/bridge.ts +0 -193
  77. package/core/plugins/index.ts +0 -7
  78. package/core/plugins/registry.ts +0 -126
  79. package/dist/cli.js +0 -11675
  80. package/runtime/build.ts +0 -17
  81. package/runtime/bundle-generator.ts +0 -1266
  82. package/runtime/client-runtime.ts +0 -891
  83. package/runtime/serve.ts +0 -93
@@ -1,314 +0,0 @@
1
- /**
2
- * Transform Template Nodes
3
- *
4
- * Transforms IR nodes into HTML strings and collects bindings
5
- *
6
- * Phase 8: Supports fragment node types (loop-fragment, conditional-fragment, optional-fragment)
7
- */
8
-
9
- import type {
10
- TemplateNode,
11
- ElementNode,
12
- TextNode,
13
- ExpressionNode,
14
- ExpressionIR,
15
- LoopContext,
16
- LoopFragmentNode,
17
- ConditionalFragmentNode,
18
- OptionalFragmentNode,
19
- ComponentNode,
20
- SourceLocation
21
- } from '../ir/types'
22
- import type { Binding } from '../output/types'
23
- import { InvariantError } from '../errors/compilerError'
24
- import { INVARIANT } from '../validate/invariants'
25
-
26
- let loopIdCounter = 0
27
-
28
- function generateLoopId(): string {
29
- return `loop_${loopIdCounter++}`
30
- }
31
-
32
- let bindingIdCounter = 0
33
-
34
- function generateBindingId(): string {
35
- return `expr_${bindingIdCounter++}`
36
- }
37
-
38
- /**
39
- * Transform a template node to HTML and collect bindings
40
- * Phase 7: Supports loop context propagation for map expressions
41
- */
42
- export function transformNode(
43
- node: TemplateNode,
44
- expressions: ExpressionIR[],
45
- parentLoopContext?: LoopContext // Phase 7: Loop context from parent map expressions
46
- ): { html: string; bindings: Binding[] } {
47
- const bindings: Binding[] = []
48
-
49
- function transform(node: TemplateNode, loopContext?: LoopContext): string {
50
- switch (node.type) {
51
- case 'text':
52
- return escapeHtml((node as TextNode).value)
53
-
54
- case 'expression': {
55
- const exprNode = node as ExpressionNode
56
- // Find the expression in the expressions array
57
- const expr = expressions.find(e => e.id === exprNode.expression)
58
- if (!expr) {
59
- throw new Error(`Expression ${exprNode.expression} not found`)
60
- }
61
-
62
- const bindingId = expr.id
63
- // Phase 7: Use loop context from ExpressionNode if available, otherwise use passed context
64
- const activeLoopContext = exprNode.loopContext || loopContext
65
-
66
- bindings.push({
67
- id: bindingId,
68
- type: 'text',
69
- target: 'data-zen-text',
70
- expression: expr.code,
71
- location: expr.location,
72
- loopContext: activeLoopContext // Phase 7: Attach loop context to binding
73
- })
74
-
75
- return `<span data-zen-text="${bindingId}" style="display: contents;"></span>`
76
- }
77
-
78
- case 'element': {
79
- const elNode = node as ElementNode
80
- const tag = elNode.tag
81
-
82
- // Build attributes
83
- const attrs: string[] = []
84
- for (const attr of elNode.attributes) {
85
- if (typeof attr.value === 'string') {
86
- // Static attribute
87
- const value = escapeHtml(attr.value)
88
- attrs.push(`${attr.name}="${value}"`)
89
- } else {
90
- // Expression attribute
91
- const expr = attr.value as ExpressionIR
92
- const bindingId = expr.id
93
- // Phase 7: Use loop context from AttributeIR if available, otherwise use element's loop context
94
- const activeLoopContext = attr.loopContext || loopContext
95
-
96
- bindings.push({
97
- id: bindingId,
98
- type: 'attribute',
99
- target: attr.name, // e.g., "class", "style"
100
- expression: expr.code,
101
- location: expr.location,
102
- loopContext: activeLoopContext // Phase 7: Attach loop context to binding
103
- })
104
-
105
- // Use data-zen-attr-{name} for attribute expressions
106
- attrs.push(`data-zen-attr-${attr.name}="${bindingId}"`)
107
- }
108
- }
109
-
110
- const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : ''
111
-
112
- // Phase 7: Use loop context from ElementNode if available, otherwise use passed context
113
- const activeLoopContext = elNode.loopContext || loopContext
114
-
115
- // Transform children
116
- const childrenHtml = elNode.children.map(child => transform(child, activeLoopContext)).join('')
117
-
118
- // Self-closing tags
119
- const voidElements = new Set([
120
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
121
- 'link', 'meta', 'param', 'source', 'track', 'wbr'
122
- ])
123
-
124
- if (voidElements.has(tag.toLowerCase()) && childrenHtml === '') {
125
- return `<${tag}${attrStr} />`
126
- }
127
-
128
- return `<${tag}${attrStr}>${childrenHtml}</${tag}>`
129
- }
130
-
131
- case 'loop-fragment': {
132
- // Loop fragment: {items.map(item => <li>...</li>)}
133
- // .map() is compile-time sugar, lowered to LoopFragmentNode
134
- // For SSR/SSG, we render one instance of the body as a template
135
- // The runtime will hydrate and expand this for each actual item
136
- const loopNode = node as LoopFragmentNode
137
- const loopId = generateLoopId()
138
- const activeLoopContext = loopNode.loopContext || loopContext
139
-
140
- // Create a binding for the loop expression
141
- bindings.push({
142
- id: loopId,
143
- type: 'loop',
144
- target: 'data-zen-loop',
145
- expression: loopNode.source,
146
- location: loopNode.location,
147
- loopContext: activeLoopContext,
148
- loopMeta: {
149
- itemVar: loopNode.itemVar,
150
- indexVar: loopNode.indexVar,
151
- bodyTemplate: loopNode.body
152
- }
153
- })
154
-
155
- // Generate the loop body template HTML
156
- // For SSR, we render ONE visible instance of the body as a template/placeholder
157
- // The runtime will clone this for each item in the array
158
- const bodyHtml = loopNode.body.map(child => transform(child, activeLoopContext)).join('')
159
-
160
- // Render container with body visible for SSR (not in hidden <template>)
161
- // Runtime will clear and re-render with actual data
162
- return `<div data-zen-loop="${loopId}" data-zen-source="${escapeHtml(loopNode.source)}" data-zen-item="${loopNode.itemVar}"${loopNode.indexVar ? ` data-zen-index="${loopNode.indexVar}"` : ''} style="display: contents;">${bodyHtml}</div>`
163
- }
164
-
165
- case 'conditional-fragment': {
166
- // Conditional fragment: {cond ? <A /> : <B />}
167
- // Both branches are pre-rendered, runtime toggles visibility
168
- const condNode = node as ConditionalFragmentNode
169
- const condId = generateBindingId()
170
- const activeLoopContext = condNode.loopContext || loopContext
171
-
172
- bindings.push({
173
- id: condId,
174
- type: 'conditional',
175
- target: 'data-zen-cond',
176
- expression: condNode.condition,
177
- location: condNode.location,
178
- loopContext: activeLoopContext
179
- })
180
-
181
- // Render both branches
182
- const consequentHtml = condNode.consequent.map(child => transform(child, activeLoopContext)).join('')
183
- const alternateHtml = condNode.alternate.map(child => transform(child, activeLoopContext)).join('')
184
-
185
- return `<div data-zen-cond="${condId}" data-zen-cond-true style="display: contents;">${consequentHtml}</div><div data-zen-cond="${condId}" data-zen-cond-false style="display: none;">${alternateHtml}</div>`
186
- }
187
-
188
- case 'optional-fragment': {
189
- // Optional fragment: {cond && <A />}
190
- // Fragment is pre-rendered, runtime toggles mount/unmount
191
- const optNode = node as OptionalFragmentNode
192
- const optId = generateBindingId()
193
- const activeLoopContext = optNode.loopContext || loopContext
194
-
195
- bindings.push({
196
- id: optId,
197
- type: 'optional',
198
- target: 'data-zen-opt',
199
- expression: optNode.condition,
200
- location: optNode.location,
201
- loopContext: activeLoopContext
202
- })
203
-
204
- const fragmentHtml = optNode.fragment.map(child => transform(child, activeLoopContext)).join('')
205
-
206
- return `<div data-zen-opt="${optId}" style="display: contents;">${fragmentHtml}</div>`
207
- }
208
-
209
- case 'component': {
210
- // Component node - should have been resolved before reaching here
211
- // This is a fallback for unresolved components
212
- const compNode = node as ComponentNode
213
- console.warn(`[Zenith] Unresolved component in transformNode: ${compNode.name}`)
214
-
215
- // Render children as a fragment
216
- const childrenHtml = compNode.children.map(child => transform(child, loopContext)).join('')
217
- return `<!-- unresolved: ${compNode.name} -->${childrenHtml}`
218
- }
219
-
220
- default: {
221
- // Handle any unknown node types
222
- console.warn(`[Zenith] Unknown node type in transformNode: ${(node as any).type}`)
223
- return ''
224
- }
225
- }
226
- }
227
-
228
- const html = transform(node, parentLoopContext)
229
-
230
- // INV-EXPR-REG-001 Enforcement: Before emitting HTML, assert all bindings resolve
231
- checkEveryBindingResolves(bindings, expressions)
232
-
233
- return { html, bindings }
234
- }
235
-
236
- /**
237
- * INV-EXPR-REG-001: Assert every data-zen-* reference resolves to a registered expression
238
- */
239
- function checkEveryBindingResolves(bindings: Binding[], expressions: ExpressionIR[]): void {
240
- const errorPrefix = `Compiler invariant violated: runtime expression used without registration. This is a compiler bug, not a user error.`
241
-
242
- for (const binding of bindings) {
243
- const exprId = binding.id
244
- const resolved = expressions.find(e => e.id === exprId)
245
-
246
- if (!resolved && binding.type !== 'loop') {
247
- // Loop bindings might have a generated loopId that doesn't
248
- // exist in the expressions array, but they have a source expression ID
249
- }
250
-
251
- // Check loop source specifically
252
- if (binding.type === 'loop') {
253
- const sourceExprId = binding.expression
254
- if (!sourceExprId.startsWith('expr_')) {
255
- throw new InvariantError(
256
- 'INV-EXPR-REG-001',
257
- `${errorPrefix}\nLoop fragment references invalid source expression: "${sourceExprId}"`,
258
- 'Loop fragments must reference a valid registered expression ID.',
259
- 'unknown', // transformNode doesn't have filePath, but we can't easily get it here without changing signature
260
- binding.location?.line || 1,
261
- binding.location?.column || 1
262
- )
263
- }
264
-
265
- const sourceResolved = expressions.find(e => e.id === sourceExprId)
266
- if (!sourceResolved) {
267
- throw new InvariantError(
268
- 'INV-EXPR-REG-001',
269
- `${errorPrefix}\nLoop source expression "${sourceExprId}" not found in registry.`,
270
- 'Every loop source must be registered as an ExpressionIR.',
271
- 'unknown',
272
- binding.location?.line || 1,
273
- binding.location?.column || 1
274
- )
275
- }
276
- } else {
277
- // General expression check
278
- if (!exprId.startsWith('expr_')) {
279
- throw new InvariantError(
280
- 'INV-EXPR-REG-001',
281
- `${errorPrefix}\nBinding references invalid expression ID: "${exprId}"`,
282
- 'Every binding must reference a valid registered expression ID.',
283
- 'unknown',
284
- binding.location?.line || 1,
285
- binding.location?.column || 1
286
- )
287
- }
288
-
289
- if (!resolved) {
290
- throw new InvariantError(
291
- 'INV-EXPR-REG-001',
292
- `${errorPrefix}\nExpression ID "${exprId}" not found in registry.`,
293
- 'Every binding must resolve to a registered expression.',
294
- 'unknown',
295
- binding.location?.line || 1,
296
- binding.location?.column || 1
297
- )
298
- }
299
- }
300
- }
301
- }
302
-
303
- /**
304
- * Escape HTML special characters
305
- */
306
- function escapeHtml(text: string): string {
307
- return text
308
- .replace(/&/g, '&amp;')
309
- .replace(/</g, '&lt;')
310
- .replace(/>/g, '&gt;')
311
- .replace(/"/g, '&quot;')
312
- .replace(/'/g, '&#39;')
313
- }
314
-
@@ -1,38 +0,0 @@
1
- /**
2
- * Transform Template IR to Compiled Template
3
- *
4
- * Phase 2: Transform IR → Static HTML + Runtime Bindings
5
- */
6
-
7
- import type { ZenIR } from '../ir/types'
8
- import type { CompiledTemplate } from '../output/types'
9
- import { generateHTML } from './generateHTML'
10
- import { validateBindings, sortBindings } from './generateBindings'
11
-
12
- /**
13
- * Transform a ZenIR into CompiledTemplate
14
- */
15
- export function transformTemplate(ir: ZenIR): CompiledTemplate {
16
- // Generate HTML and collect bindings
17
- const { html, bindings } = generateHTML(ir.template.nodes, ir.template.expressions)
18
-
19
- // Validate bindings
20
- validateBindings(bindings)
21
-
22
- // Sort bindings by location for deterministic output
23
- const sortedBindings = sortBindings(bindings)
24
-
25
- // Extract scripts (raw content, pass through)
26
- const scripts = ir.script ? ir.script.raw : null
27
-
28
- // Extract styles (raw content, pass through)
29
- const styles = ir.styles.map(s => s.raw)
30
-
31
- return {
32
- html,
33
- bindings: sortedBindings,
34
- scripts,
35
- styles
36
- }
37
- }
38
-
@@ -1,292 +0,0 @@
1
- /**
2
- * Invariant Validation
3
- *
4
- * Compile-time checks that enforce Zenith's non-negotiable invariants.
5
- * If any invariant is violated, compilation fails immediately with a clear explanation.
6
- *
7
- * INVARIANTS:
8
- * 1. Components are structural only — no reactive scopes, no state
9
- * 2. Reactivity is scope-owned — flows through components, never into them
10
- * 3. Slot projection preserves identity — loopContext is preserved or merged upward
11
- * 4. Attribute ownership belongs to usage — must be forwarded to semantic root
12
- * 5. All resolution is compile-time — no unresolved components
13
- * 6. Failure is a compiler error — no silent degradation
14
- */
15
-
16
- import type { ZenIR, TemplateNode, ElementNode, ComponentNode, LoopContext } from '../ir/types'
17
- import { InvariantError } from '../errors/compilerError'
18
-
19
- /**
20
- * Invariant Codes
21
- *
22
- * Each invariant has a unique ID for tracking and documentation.
23
- */
24
- export const INVARIANT = {
25
- LOOP_CONTEXT_LOST: 'INV001',
26
- ATTRIBUTE_NOT_FORWARDED: 'INV002',
27
- UNRESOLVED_COMPONENT: 'INV003',
28
- REACTIVE_BOUNDARY: 'INV004',
29
- TEMPLATE_TAG: 'INV005',
30
- SLOT_ATTRIBUTE: 'INV006',
31
- ORPHAN_COMPOUND: 'INV007',
32
- NON_ENUMERABLE_JSX: 'INV008',
33
- } as const
34
-
35
- /**
36
- * Guarantee Messages
37
- *
38
- * Human-readable explanations of what each invariant guarantees.
39
- */
40
- const GUARANTEES: Record<string, string> = {
41
- [INVARIANT.LOOP_CONTEXT_LOST]:
42
- 'Slot content retains its original reactive scope. Expressions and event handlers continue to work after projection.',
43
- [INVARIANT.ATTRIBUTE_NOT_FORWARDED]:
44
- 'Attributes passed to components are forwarded to the semantic root element.',
45
- [INVARIANT.UNRESOLVED_COMPONENT]:
46
- 'All components are resolved at compile time. No runtime component discovery.',
47
- [INVARIANT.REACTIVE_BOUNDARY]:
48
- 'Components are purely structural transforms. They do not create reactive scopes.',
49
- [INVARIANT.TEMPLATE_TAG]:
50
- 'Named slots use compound component pattern (Card.Header), not <template> tags.',
51
- [INVARIANT.SLOT_ATTRIBUTE]:
52
- 'Named slots use compound component pattern (Card.Header), not slot="" attributes.',
53
- [INVARIANT.ORPHAN_COMPOUND]:
54
- 'Compound slot markers (Card.Header) must be direct children of their parent component.',
55
- [INVARIANT.NON_ENUMERABLE_JSX]:
56
- 'JSX expressions must have statically enumerable output. The compiler must know all possible DOM shapes at compile time.',
57
- }
58
-
59
- /**
60
- * Validate all invariants on a compiled IR
61
- *
62
- * Called after component resolution to ensure all invariants hold.
63
- * Throws InvariantError if any invariant is violated.
64
- */
65
- export function validateInvariants(ir: ZenIR, filePath: string): void {
66
- validateNoUnresolvedComponents(ir.template.nodes, filePath)
67
- // Additional invariant checks can be added here
68
- }
69
-
70
- /**
71
- * INV003: Validate that no unresolved components remain
72
- *
73
- * After component resolution, all ComponentNode instances should be
74
- * resolved to ElementNode instances. If any remain, the compiler failed.
75
- */
76
- export function validateNoUnresolvedComponents(
77
- nodes: TemplateNode[],
78
- filePath: string
79
- ): void {
80
- for (const node of nodes) {
81
- checkNodeForUnresolvedComponent(node, filePath)
82
- }
83
- }
84
-
85
- function checkNodeForUnresolvedComponent(node: TemplateNode, filePath: string): void {
86
- if (node.type === 'component') {
87
- throw new InvariantError(
88
- INVARIANT.UNRESOLVED_COMPONENT,
89
- `Unresolved component: <${node.name}>. Component was not found or failed to resolve.`,
90
- GUARANTEES[INVARIANT.UNRESOLVED_COMPONENT]!,
91
- filePath,
92
- node.location.line,
93
- node.location.column
94
- )
95
- }
96
-
97
- if (node.type === 'element') {
98
- for (const child of node.children) {
99
- checkNodeForUnresolvedComponent(child, filePath)
100
- }
101
- }
102
- }
103
-
104
- /**
105
- * INV005: Validate no <template> tags are used
106
- *
107
- * Zenith uses compound component pattern for named slots.
108
- * <template> tags are a different mental model and are forbidden.
109
- */
110
- export function validateNoTemplateTags(
111
- nodes: TemplateNode[],
112
- filePath: string
113
- ): void {
114
- for (const node of nodes) {
115
- checkNodeForTemplateTag(node, filePath)
116
- }
117
- }
118
-
119
- function checkNodeForTemplateTag(node: TemplateNode, filePath: string): void {
120
- if (node.type === 'element' && node.tag === 'template') {
121
- throw new InvariantError(
122
- INVARIANT.TEMPLATE_TAG,
123
- `<template> tags are forbidden in Zenith. Use compound components (e.g., Card.Header) for named slots.`,
124
- GUARANTEES[INVARIANT.TEMPLATE_TAG]!,
125
- filePath,
126
- node.location.line,
127
- node.location.column
128
- )
129
- }
130
-
131
- if (node.type === 'element') {
132
- for (const child of node.children) {
133
- checkNodeForTemplateTag(child, filePath)
134
- }
135
- }
136
- }
137
-
138
- /**
139
- * INV006: Validate no slot="" attributes are used
140
- *
141
- * Zenith uses compound component pattern, not slot props.
142
- */
143
- export function validateNoSlotAttributes(
144
- nodes: TemplateNode[],
145
- filePath: string
146
- ): void {
147
- for (const node of nodes) {
148
- checkNodeForSlotAttribute(node, filePath)
149
- }
150
- }
151
-
152
- function checkNodeForSlotAttribute(node: TemplateNode, filePath: string): void {
153
- if (node.type === 'element') {
154
- const slotAttr = node.attributes.find(attr => attr.name === 'slot')
155
- if (slotAttr) {
156
- throw new InvariantError(
157
- INVARIANT.SLOT_ATTRIBUTE,
158
- `slot="${typeof slotAttr.value === 'string' ? slotAttr.value : '...'}" attribute is forbidden. Use compound components (e.g., Card.Header) for named slots.`,
159
- GUARANTEES[INVARIANT.SLOT_ATTRIBUTE]!,
160
- filePath,
161
- slotAttr.location.line,
162
- slotAttr.location.column
163
- )
164
- }
165
-
166
- for (const child of node.children) {
167
- checkNodeForSlotAttribute(child, filePath)
168
- }
169
- }
170
-
171
- if (node.type === 'component') {
172
- for (const child of node.children) {
173
- checkNodeForSlotAttribute(child, filePath)
174
- }
175
- }
176
- }
177
-
178
- /**
179
- * INV001: Validate loopContext is preserved during slot projection
180
- *
181
- * When slot content is moved from the usage site to the slot target,
182
- * the loopContext must be preserved so reactivity continues to work.
183
- *
184
- * @param originalNodes - Nodes before slot projection
185
- * @param projectedNodes - Nodes after slot projection
186
- */
187
- export function validateLoopContextPreservation(
188
- originalNodes: TemplateNode[],
189
- projectedNodes: TemplateNode[],
190
- filePath: string
191
- ): void {
192
- // Collect all loopContext from original nodes
193
- const originalContexts = collectLoopContexts(originalNodes)
194
-
195
- // Verify all contexts are still present in projected nodes
196
- const projectedContexts = collectLoopContexts(projectedNodes)
197
-
198
- for (const entry of Array.from(originalContexts.entries())) {
199
- const [nodeId, context] = entry
200
- const projected = projectedContexts.get(nodeId)
201
- if (context && !projected) {
202
- // loopContext was lost during projection
203
- // This is a compiler bug, not a user error
204
- throw new InvariantError(
205
- INVARIANT.LOOP_CONTEXT_LOST,
206
- `Reactive scope was lost during slot projection. This is a Zenith compiler bug.`,
207
- GUARANTEES[INVARIANT.LOOP_CONTEXT_LOST]!,
208
- filePath,
209
- 1, // We don't have precise location, use line 1
210
- 1
211
- )
212
- }
213
- }
214
- }
215
-
216
- function collectLoopContexts(nodes: TemplateNode[]): Map<string, LoopContext | undefined> {
217
- const contexts = new Map<string, LoopContext | undefined>()
218
- let nodeId = 0
219
-
220
- function visit(node: TemplateNode): void {
221
- const id = `node_${nodeId++}`
222
-
223
- if (node.type === 'expression') {
224
- contexts.set(id, node.loopContext)
225
- } else if (node.type === 'element') {
226
- contexts.set(id, node.loopContext)
227
- for (const attr of node.attributes) {
228
- if (attr.loopContext) {
229
- contexts.set(`${id}_attr_${attr.name}`, attr.loopContext)
230
- }
231
- }
232
- for (const child of node.children) {
233
- visit(child)
234
- }
235
- } else if (node.type === 'component') {
236
- contexts.set(id, node.loopContext)
237
- for (const child of node.children) {
238
- visit(child)
239
- }
240
- }
241
- }
242
-
243
- for (const node of nodes) {
244
- visit(node)
245
- }
246
-
247
- return contexts
248
- }
249
-
250
- /**
251
- * INV007: Throw error for orphan compound slot markers
252
- *
253
- * Called when a compound component (Card.Header) is found outside
254
- * its parent component context.
255
- */
256
- export function throwOrphanCompoundError(
257
- componentName: string,
258
- parentName: string,
259
- filePath: string,
260
- line: number,
261
- column: number
262
- ): never {
263
- throw new InvariantError(
264
- INVARIANT.ORPHAN_COMPOUND,
265
- `<${componentName}> must be a direct child of <${parentName}>. Compound slot markers cannot be used outside their parent component.`,
266
- GUARANTEES[INVARIANT.ORPHAN_COMPOUND]!,
267
- filePath,
268
- line,
269
- column
270
- )
271
- }
272
-
273
- /**
274
- * INV003: Throw error for unresolved component
275
- *
276
- * Called when a component definition cannot be found.
277
- */
278
- export function throwUnresolvedComponentError(
279
- componentName: string,
280
- filePath: string,
281
- line: number,
282
- column: number
283
- ): never {
284
- throw new InvariantError(
285
- INVARIANT.UNRESOLVED_COMPONENT,
286
- `Component <${componentName}> not found. All components must be defined in the components directory.`,
287
- GUARANTEES[INVARIANT.UNRESOLVED_COMPONENT]!,
288
- filePath,
289
- line,
290
- column
291
- )
292
- }