@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.
- package/README.md +20 -19
- package/cli/commands/add.ts +2 -2
- package/cli/commands/build.ts +2 -3
- package/cli/commands/dev.ts +94 -74
- package/cli/commands/index.ts +1 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/remove.ts +2 -2
- package/cli/index.ts +1 -1
- package/cli/main.ts +1 -1
- package/cli/utils/logger.ts +1 -1
- package/cli/utils/plugin-manager.ts +1 -1
- package/cli/utils/project.ts +4 -4
- package/core/components/ErrorPage.zen +218 -0
- package/core/components/index.ts +15 -0
- package/core/config.ts +1 -0
- package/core/index.ts +29 -0
- package/dist/compiler-native-frej59m4.node +0 -0
- package/dist/core/compiler-native-frej59m4.node +0 -0
- package/dist/core/index.js +6293 -0
- package/dist/runtime/lifecycle/index.js +1 -0
- package/dist/runtime/reactivity/index.js +1 -0
- package/dist/zen-build.js +1 -20118
- package/dist/zen-dev.js +1 -20118
- package/dist/zen-preview.js +1 -20118
- package/dist/zenith.js +1 -20118
- package/package.json +11 -20
- package/compiler/README.md +0 -380
- package/compiler/build-analyzer.ts +0 -122
- package/compiler/css/index.ts +0 -317
- package/compiler/discovery/componentDiscovery.ts +0 -242
- package/compiler/discovery/layouts.ts +0 -70
- package/compiler/errors/compilerError.ts +0 -56
- package/compiler/finalize/finalizeOutput.ts +0 -192
- package/compiler/finalize/generateFinalBundle.ts +0 -82
- package/compiler/index.ts +0 -83
- package/compiler/ir/types.ts +0 -174
- package/compiler/output/types.ts +0 -48
- package/compiler/parse/detectMapExpressions.ts +0 -102
- package/compiler/parse/importTypes.ts +0 -78
- package/compiler/parse/parseImports.ts +0 -309
- package/compiler/parse/parseScript.ts +0 -46
- package/compiler/parse/parseTemplate.ts +0 -628
- package/compiler/parse/parseZenFile.ts +0 -66
- package/compiler/parse/scriptAnalysis.ts +0 -91
- package/compiler/parse/trackLoopContext.ts +0 -82
- package/compiler/runtime/dataExposure.ts +0 -332
- package/compiler/runtime/generateDOM.ts +0 -255
- package/compiler/runtime/generateHydrationBundle.ts +0 -407
- package/compiler/runtime/hydration.ts +0 -309
- package/compiler/runtime/navigation.ts +0 -432
- package/compiler/runtime/thinRuntime.ts +0 -160
- package/compiler/runtime/transformIR.ts +0 -406
- package/compiler/runtime/wrapExpression.ts +0 -114
- package/compiler/runtime/wrapExpressionWithLoop.ts +0 -97
- package/compiler/spa-build.ts +0 -917
- package/compiler/ssg-build.ts +0 -486
- package/compiler/test/component-stacking.test.ts +0 -365
- package/compiler/test/map-lowering.test.ts +0 -130
- package/compiler/test/validate-test.ts +0 -104
- package/compiler/transform/classifyExpression.ts +0 -444
- package/compiler/transform/componentResolver.ts +0 -350
- package/compiler/transform/componentScriptTransformer.ts +0 -303
- package/compiler/transform/expressionTransformer.ts +0 -385
- package/compiler/transform/fragmentLowering.ts +0 -819
- package/compiler/transform/generateBindings.ts +0 -68
- package/compiler/transform/generateHTML.ts +0 -28
- package/compiler/transform/layoutProcessor.ts +0 -132
- package/compiler/transform/slotResolver.ts +0 -292
- package/compiler/transform/transformNode.ts +0 -314
- package/compiler/transform/transformTemplate.ts +0 -38
- package/compiler/validate/invariants.ts +0 -292
- package/compiler/validate/validateExpressions.ts +0 -168
- package/core/config/index.ts +0 -18
- package/core/config/loader.ts +0 -69
- package/core/config/types.ts +0 -119
- package/core/plugins/bridge.ts +0 -193
- package/core/plugins/index.ts +0 -7
- package/core/plugins/registry.ts +0 -126
- package/dist/cli.js +0 -11675
- package/runtime/build.ts +0 -17
- package/runtime/bundle-generator.ts +0 -1266
- package/runtime/client-runtime.ts +0 -891
- 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, '&')
|
|
309
|
-
.replace(/</g, '<')
|
|
310
|
-
.replace(/>/g, '>')
|
|
311
|
-
.replace(/"/g, '"')
|
|
312
|
-
.replace(/'/g, ''')
|
|
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
|
-
}
|