@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.
@@ -4,7 +4,16 @@
4
4
  * Generates JavaScript code that creates DOM elements from template nodes
5
5
  */
6
6
 
7
- import type { TemplateNode, ElementNode, TextNode, ExpressionNode, ExpressionIR } from '../ir/types'
7
+ import type {
8
+ TemplateNode,
9
+ ElementNode,
10
+ TextNode,
11
+ ExpressionNode,
12
+ ExpressionIR,
13
+ ConditionalFragmentNode,
14
+ OptionalFragmentNode,
15
+ LoopFragmentNode
16
+ } from '../ir/types'
8
17
 
9
18
  /**
10
19
  * Generate DOM creation code from a template node
@@ -95,6 +104,98 @@ ${indent}}\n`
95
104
 
96
105
  return { code, varName }
97
106
  }
107
+
108
+ case 'component': {
109
+ // Components should be resolved before reaching DOM generation
110
+ // If we get here, it means component resolution failed
111
+ throw new Error(`[Zenith] Unresolved component: ${(node as any).name}. Components must be resolved before DOM generation.`)
112
+ }
113
+
114
+ case 'conditional-fragment': {
115
+ // Conditional fragment: {condition ? <A /> : <B />}
116
+ // Both branches are precompiled, runtime toggles visibility
117
+ const condNode = node as ConditionalFragmentNode
118
+ const containerVar = varName
119
+ const conditionId = `cond_${varCounter.count++}`
120
+
121
+ let code = `${indent}const ${containerVar} = document.createDocumentFragment();\n`
122
+ code += `${indent}const ${conditionId}_result = (function() { with (state) { return ${condNode.condition}; } })();\n`
123
+
124
+ // Generate consequent branch
125
+ code += `${indent}if (${conditionId}_result) {\n`
126
+ for (const child of condNode.consequent) {
127
+ const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
128
+ code += `${childResult.code}\n`
129
+ code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
130
+ }
131
+ code += `${indent}} else {\n`
132
+
133
+ // Generate alternate branch
134
+ for (const child of condNode.alternate) {
135
+ const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
136
+ code += `${childResult.code}\n`
137
+ code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
138
+ }
139
+ code += `${indent}}\n`
140
+
141
+ return { code, varName: containerVar }
142
+ }
143
+
144
+ case 'optional-fragment': {
145
+ // Optional fragment: {condition && <A />}
146
+ // Fragment is precompiled, runtime mounts/unmounts based on condition
147
+ const optNode = node as OptionalFragmentNode
148
+ const containerVar = varName
149
+ const conditionId = `opt_${varCounter.count++}`
150
+
151
+ let code = `${indent}const ${containerVar} = document.createDocumentFragment();\n`
152
+ code += `${indent}const ${conditionId}_result = (function() { with (state) { return ${optNode.condition}; } })();\n`
153
+ code += `${indent}if (${conditionId}_result) {\n`
154
+
155
+ for (const child of optNode.fragment) {
156
+ const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
157
+ code += `${childResult.code}\n`
158
+ code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
159
+ }
160
+
161
+ code += `${indent}}\n`
162
+
163
+ return { code, varName: containerVar }
164
+ }
165
+
166
+ case 'loop-fragment': {
167
+ // Loop fragment: {items.map(item => <li>...</li>)}
168
+ // Body is precompiled once, instantiated per item at runtime
169
+ const loopNode = node as LoopFragmentNode
170
+ const containerVar = varName
171
+ const loopId = `loop_${varCounter.count++}`
172
+
173
+ let code = `${indent}const ${containerVar} = document.createDocumentFragment();\n`
174
+ code += `${indent}const ${loopId}_items = (function() { with (state) { return ${loopNode.source}; } })() || [];\n`
175
+
176
+ // Loop parameters
177
+ const itemVar = loopNode.itemVar
178
+ const indexVar = loopNode.indexVar || `${loopId}_idx`
179
+
180
+ code += `${indent}${loopId}_items.forEach(function(${itemVar}, ${indexVar}) {\n`
181
+
182
+ // Generate loop body with loop context variables in scope
183
+ for (const child of loopNode.body) {
184
+ const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
185
+ // Inject loop variables into the child code
186
+ let childCode = childResult.code
187
+ code += `${childCode}\n`
188
+ code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
189
+ }
190
+
191
+ code += `${indent}});\n`
192
+
193
+ return { code, varName: containerVar }
194
+ }
195
+
196
+ default: {
197
+ throw new Error(`[Zenith] Unknown node type: ${(node as any).type}`)
198
+ }
98
199
  }
99
200
  }
100
201
 
@@ -197,8 +197,8 @@ if (typeof window !== 'undefined') {
197
197
  if (document.readyState === 'loading') {
198
198
  document.addEventListener('DOMContentLoaded', autoHydrate);
199
199
  } else {
200
- // DOM already loaded, run on next tick to ensure all scripts are executed
201
- setTimeout(autoHydrate, 0);
200
+ // DOM already loaded, hydrate immediately
201
+ autoHydrate();
202
202
  }
203
203
  })();
204
204
  `
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Expression Classification
3
+ *
4
+ * Analyzes expression code to determine output type for structural lowering.
5
+ *
6
+ * JSX expressions are allowed if — and only if — the compiler can statically
7
+ * enumerate all possible DOM shapes and lower them at compile time.
8
+ */
9
+
10
+ /**
11
+ * Expression output types
12
+ *
13
+ * - primitive: string, number, boolean → text binding
14
+ * - conditional: cond ? <A /> : <B /> → ConditionalFragmentNode
15
+ * - optional: cond && <A /> → OptionalFragmentNode
16
+ * - loop: arr.map(i => <JSX />) → LoopFragmentNode
17
+ * - fragment: <A /> or <><A /><B /></> → inline fragment
18
+ * - unknown: cannot be statically determined → COMPILE ERROR
19
+ */
20
+ export type ExpressionOutputType =
21
+ | 'primitive'
22
+ | 'conditional'
23
+ | 'optional'
24
+ | 'loop'
25
+ | 'fragment'
26
+ | 'unknown'
27
+
28
+ /**
29
+ * Classification result with extracted metadata
30
+ */
31
+ export interface ExpressionClassification {
32
+ type: ExpressionOutputType
33
+ // For conditional expressions
34
+ condition?: string
35
+ consequent?: string
36
+ alternate?: string
37
+ // For optional expressions
38
+ optionalCondition?: string
39
+ optionalFragment?: string
40
+ // For loop expressions
41
+ loopSource?: string
42
+ loopItemVar?: string
43
+ loopIndexVar?: string
44
+ loopBody?: string
45
+ // For inline fragments
46
+ fragmentCode?: string
47
+ }
48
+
49
+ /**
50
+ * Check if code contains JSX-like tags
51
+ */
52
+ function containsJSX(code: string): boolean {
53
+ // Match opening JSX tags: <Tag or <tag
54
+ return /<[a-zA-Z]/.test(code)
55
+ }
56
+
57
+ /**
58
+ * Check if expression starts with a JSX element
59
+ */
60
+ function startsWithJSX(code: string): boolean {
61
+ const trimmed = code.trim()
62
+ return /^<[a-zA-Z]/.test(trimmed) || /^<>/.test(trimmed)
63
+ }
64
+
65
+ /**
66
+ * Classify expression output type
67
+ *
68
+ * @param code - The expression code to classify
69
+ * @returns Classification result with metadata
70
+ */
71
+ export function classifyExpression(code: string): ExpressionClassification {
72
+ const trimmed = code.trim()
73
+
74
+ // Check for .map() expressions with JSX body
75
+ const mapMatch = parseMapExpression(trimmed)
76
+ if (mapMatch) {
77
+ return {
78
+ type: 'loop',
79
+ loopSource: mapMatch.source,
80
+ loopItemVar: mapMatch.itemVar,
81
+ loopIndexVar: mapMatch.indexVar,
82
+ loopBody: mapMatch.body
83
+ }
84
+ }
85
+
86
+ // Check for ternary with JSX branches: condition ? <A /> : <B />
87
+ const ternaryMatch = parseTernaryExpression(trimmed)
88
+ if (ternaryMatch && (containsJSX(ternaryMatch.consequent) || containsJSX(ternaryMatch.alternate))) {
89
+ return {
90
+ type: 'conditional',
91
+ condition: ternaryMatch.condition,
92
+ consequent: ternaryMatch.consequent,
93
+ alternate: ternaryMatch.alternate
94
+ }
95
+ }
96
+
97
+ // Check for logical AND with JSX: condition && <A />
98
+ const logicalAndMatch = parseLogicalAndExpression(trimmed)
99
+ if (logicalAndMatch && containsJSX(logicalAndMatch.fragment)) {
100
+ return {
101
+ type: 'optional',
102
+ optionalCondition: logicalAndMatch.condition,
103
+ optionalFragment: logicalAndMatch.fragment
104
+ }
105
+ }
106
+
107
+ // All other expressions (including inline JSX like {<span>text</span>})
108
+ // are treated as primitive and handled by the existing expression transformer
109
+ // which converts JSX to __zenith.h() calls at runtime
110
+ return { type: 'primitive' }
111
+ }
112
+
113
+ /**
114
+ * Parse .map() expression
115
+ *
116
+ * Matches:
117
+ * - source.map(item => body)
118
+ * - source.map((item, index) => body)
119
+ */
120
+ function parseMapExpression(code: string): {
121
+ source: string
122
+ itemVar: string
123
+ indexVar?: string
124
+ body: string
125
+ } | null {
126
+ // Pattern: source.map(item => body)
127
+ // Pattern: source.map((item) => body)
128
+ // Pattern: source.map((item, index) => body)
129
+
130
+ // Find .map(
131
+ const mapIndex = code.indexOf('.map(')
132
+ if (mapIndex === -1) return null
133
+
134
+ const source = code.slice(0, mapIndex).trim()
135
+ if (!source) return null
136
+
137
+ // Find the arrow function parameters
138
+ let afterMap = code.slice(mapIndex + 5) // after ".map("
139
+
140
+ // Skip whitespace
141
+ afterMap = afterMap.trimStart()
142
+
143
+ // Check for parenthesized params: (item) or (item, index)
144
+ let itemVar: string
145
+ let indexVar: string | undefined
146
+ let bodyStart: number
147
+
148
+ if (afterMap.startsWith('(')) {
149
+ // Find closing paren
150
+ const closeParenIndex = findBalancedParen(afterMap, 0)
151
+ if (closeParenIndex === -1) return null
152
+
153
+ const paramsStr = afterMap.slice(1, closeParenIndex)
154
+ const params = paramsStr.split(',').map(p => p.trim())
155
+
156
+ itemVar = params[0] || ''
157
+ indexVar = params[1]
158
+
159
+ // Find arrow
160
+ const afterParams = afterMap.slice(closeParenIndex + 1).trimStart()
161
+ if (!afterParams.startsWith('=>')) return null
162
+
163
+ bodyStart = mapIndex + 5 + (afterMap.length - afterParams.length) + 2
164
+ } else {
165
+ // Simple param: item => body
166
+ const arrowIndex = afterMap.indexOf('=>')
167
+ if (arrowIndex === -1) return null
168
+
169
+ itemVar = afterMap.slice(0, arrowIndex).trim()
170
+ bodyStart = mapIndex + 5 + arrowIndex + 2
171
+ }
172
+
173
+ if (!itemVar) return null
174
+
175
+ // Extract body (everything after => until the closing paren of .map())
176
+ let body = code.slice(bodyStart).trim()
177
+
178
+ // Remove trailing ) from .map()
179
+ if (body.endsWith(')')) {
180
+ body = body.slice(0, -1).trim()
181
+ }
182
+
183
+ // Check if body contains JSX
184
+ if (!containsJSX(body)) return null
185
+
186
+ return { source, itemVar, indexVar, body }
187
+ }
188
+
189
+ /**
190
+ * Find matching closing parenthesis
191
+ */
192
+ function findBalancedParen(code: string, startIndex: number): number {
193
+ if (code[startIndex] !== '(') return -1
194
+
195
+ let depth = 1
196
+ let i = startIndex + 1
197
+
198
+ while (i < code.length && depth > 0) {
199
+ if (code[i] === '(') depth++
200
+ else if (code[i] === ')') depth--
201
+ i++
202
+ }
203
+
204
+ return depth === 0 ? i - 1 : -1
205
+ }
206
+
207
+ /**
208
+ * Parse ternary expression
209
+ *
210
+ * Matches: condition ? consequent : alternate
211
+ */
212
+ function parseTernaryExpression(code: string): {
213
+ condition: string
214
+ consequent: string
215
+ alternate: string
216
+ } | null {
217
+ // Find the ? that's not inside JSX or strings
218
+ const questionIndex = findTernaryOperator(code)
219
+ if (questionIndex === -1) return null
220
+
221
+ const condition = code.slice(0, questionIndex).trim()
222
+ const afterQuestion = code.slice(questionIndex + 1)
223
+
224
+ // Find the : that matches this ternary
225
+ const colonIndex = findTernaryColon(afterQuestion)
226
+ if (colonIndex === -1) return null
227
+
228
+ const consequent = afterQuestion.slice(0, colonIndex).trim()
229
+ const alternate = afterQuestion.slice(colonIndex + 1).trim()
230
+
231
+ if (!condition || !consequent || !alternate) return null
232
+
233
+ return { condition, consequent, alternate }
234
+ }
235
+
236
+ /**
237
+ * Find ternary ? operator (not inside JSX or nested ternaries)
238
+ */
239
+ function findTernaryOperator(code: string): number {
240
+ let depth = 0
241
+ let inString = false
242
+ let stringChar = ''
243
+ let inTemplate = false
244
+ let jsxDepth = 0
245
+
246
+ for (let i = 0; i < code.length; i++) {
247
+ const char = code[i]
248
+ const prevChar = i > 0 ? code[i - 1] : ''
249
+
250
+ // Handle escape
251
+ if (prevChar === '\\') continue
252
+
253
+ // Handle strings
254
+ if (!inString && !inTemplate && (char === '"' || char === "'")) {
255
+ inString = true
256
+ stringChar = char
257
+ continue
258
+ }
259
+ if (inString && char === stringChar) {
260
+ inString = false
261
+ continue
262
+ }
263
+
264
+ // Handle template literals
265
+ if (!inString && !inTemplate && char === '`') {
266
+ inTemplate = true
267
+ continue
268
+ }
269
+ if (inTemplate && char === '`') {
270
+ inTemplate = false
271
+ continue
272
+ }
273
+
274
+ if (inString || inTemplate) continue
275
+
276
+ // Track JSX depth
277
+ if (char === '<' && /[a-zA-Z>]/.test(code[i + 1] || '')) {
278
+ jsxDepth++
279
+ }
280
+ if (char === '>' && prevChar === '/') {
281
+ jsxDepth = Math.max(0, jsxDepth - 1)
282
+ }
283
+ if (char === '/' && code[i + 1] === '>') {
284
+ // self-closing tag, depth handled when we see >
285
+ }
286
+ if (char === '<' && code[i + 1] === '/') {
287
+ // closing tag coming
288
+ }
289
+ if (char === '>' && jsxDepth > 0 && prevChar !== '/' && code.slice(0, i).includes('</')) {
290
+ jsxDepth = Math.max(0, jsxDepth - 1)
291
+ }
292
+
293
+ // Track parens
294
+ if (char === '(' || char === '{' || char === '[') depth++
295
+ if (char === ')' || char === '}' || char === ']') depth--
296
+
297
+ // Found ternary operator at top level
298
+ if (char === '?' && depth === 0 && jsxDepth === 0) {
299
+ return i
300
+ }
301
+ }
302
+
303
+ return -1
304
+ }
305
+
306
+ /**
307
+ * Find ternary : operator (matching the ?)
308
+ */
309
+ function findTernaryColon(code: string): number {
310
+ let depth = 0
311
+ let ternaryDepth = 0
312
+ let inString = false
313
+ let stringChar = ''
314
+ let inTemplate = false
315
+ let jsxDepth = 0
316
+
317
+ for (let i = 0; i < code.length; i++) {
318
+ const char = code[i]
319
+ const prevChar = i > 0 ? code[i - 1] : ''
320
+
321
+ // Handle escape
322
+ if (prevChar === '\\') continue
323
+
324
+ // Handle strings
325
+ if (!inString && !inTemplate && (char === '"' || char === "'")) {
326
+ inString = true
327
+ stringChar = char
328
+ continue
329
+ }
330
+ if (inString && char === stringChar) {
331
+ inString = false
332
+ continue
333
+ }
334
+
335
+ // Handle template literals
336
+ if (!inString && !inTemplate && char === '`') {
337
+ inTemplate = true
338
+ continue
339
+ }
340
+ if (inTemplate && char === '`') {
341
+ inTemplate = false
342
+ continue
343
+ }
344
+
345
+ if (inString || inTemplate) continue
346
+
347
+ // Track JSX depth (simplified)
348
+ if (char === '<' && /[a-zA-Z>]/.test(code[i + 1] || '')) {
349
+ jsxDepth++
350
+ }
351
+ if (char === '>' && (prevChar === '/' || jsxDepth > 0)) {
352
+ jsxDepth = Math.max(0, jsxDepth - 1)
353
+ }
354
+
355
+ // Track parens
356
+ if (char === '(' || char === '{' || char === '[') depth++
357
+ if (char === ')' || char === '}' || char === ']') depth--
358
+
359
+ // Track nested ternaries
360
+ if (char === '?') ternaryDepth++
361
+ if (char === ':' && ternaryDepth > 0) {
362
+ ternaryDepth--
363
+ continue
364
+ }
365
+
366
+ // Found matching colon at top level
367
+ if (char === ':' && depth === 0 && ternaryDepth === 0 && jsxDepth === 0) {
368
+ return i
369
+ }
370
+ }
371
+
372
+ return -1
373
+ }
374
+
375
+ /**
376
+ * Parse logical AND expression
377
+ *
378
+ * Matches: condition && fragment
379
+ */
380
+ function parseLogicalAndExpression(code: string): {
381
+ condition: string
382
+ fragment: string
383
+ } | null {
384
+ // Find && at top level
385
+ let depth = 0
386
+ let inString = false
387
+ let stringChar = ''
388
+ let inTemplate = false
389
+
390
+ for (let i = 0; i < code.length - 1; i++) {
391
+ const char = code[i]
392
+ const nextChar = code[i + 1]
393
+ const prevChar = i > 0 ? code[i - 1] : ''
394
+
395
+ // Handle escape
396
+ if (prevChar === '\\') continue
397
+
398
+ // Handle strings
399
+ if (!inString && !inTemplate && (char === '"' || char === "'")) {
400
+ inString = true
401
+ stringChar = char
402
+ continue
403
+ }
404
+ if (inString && char === stringChar) {
405
+ inString = false
406
+ continue
407
+ }
408
+
409
+ // Handle template literals
410
+ if (!inString && !inTemplate && char === '`') {
411
+ inTemplate = true
412
+ continue
413
+ }
414
+ if (inTemplate && char === '`') {
415
+ inTemplate = false
416
+ continue
417
+ }
418
+
419
+ if (inString || inTemplate) continue
420
+
421
+ // Track parens
422
+ if (char === '(' || char === '{' || char === '[') depth++
423
+ if (char === ')' || char === '}' || char === ']') depth--
424
+
425
+ // Found && at top level
426
+ if (char === '&' && nextChar === '&' && depth === 0) {
427
+ const condition = code.slice(0, i).trim()
428
+ const fragment = code.slice(i + 2).trim()
429
+
430
+ if (condition && fragment) {
431
+ return { condition, fragment }
432
+ }
433
+ }
434
+ }
435
+
436
+ return null
437
+ }
438
+
439
+ /**
440
+ * Check if an expression type requires structural lowering
441
+ */
442
+ export function requiresStructuralLowering(type: ExpressionOutputType): boolean {
443
+ return type === 'conditional' || type === 'optional' || type === 'loop' || type === 'fragment'
444
+ }