@zenithbuild/core 0.4.2 → 0.4.5
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 +59 -15
- package/compiler/css/index.ts +317 -0
- 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/ssg-build.ts +17 -11
- 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
|
@@ -106,17 +106,29 @@ export function finalizeOutput(
|
|
|
106
106
|
* Verify HTML contains no raw {expression} syntax
|
|
107
107
|
*
|
|
108
108
|
* This is a critical check - browser must never see raw expressions
|
|
109
|
+
*
|
|
110
|
+
* Excludes:
|
|
111
|
+
* - Content inside <pre>, <code> tags (display code samples)
|
|
112
|
+
* - Content that looks like HTML tags (from entity decoding)
|
|
113
|
+
* - Comments
|
|
114
|
+
* - Data attributes
|
|
109
115
|
*/
|
|
110
116
|
function verifyNoRawExpressions(html: string, filePath: string): string[] {
|
|
111
117
|
const errors: string[] = []
|
|
112
|
-
|
|
118
|
+
|
|
119
|
+
// Remove content inside <pre> and <code> tags before checking
|
|
120
|
+
// These are code samples that may contain { } legitimately
|
|
121
|
+
let htmlToCheck = html
|
|
122
|
+
.replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, '')
|
|
123
|
+
.replace(/<code[^>]*>[\s\S]*?<\/code>/gi, '')
|
|
124
|
+
|
|
113
125
|
// Check for raw {expression} patterns (not data-zen-* attributes)
|
|
114
126
|
// Allow data-zen-text, data-zen-attr-* but not raw { }
|
|
115
127
|
const rawExpressionPattern = /\{[^}]*\}/g
|
|
116
|
-
const matches =
|
|
117
|
-
|
|
128
|
+
const matches = htmlToCheck.match(rawExpressionPattern)
|
|
129
|
+
|
|
118
130
|
if (matches && matches.length > 0) {
|
|
119
|
-
// Filter out false positives
|
|
131
|
+
// Filter out false positives
|
|
120
132
|
const actualExpressions = matches.filter(match => {
|
|
121
133
|
// Exclude if it's in a comment
|
|
122
134
|
if (html.includes(`<!--${match}`) || html.includes(`${match}-->`)) {
|
|
@@ -126,10 +138,27 @@ function verifyNoRawExpressions(html: string, filePath: string): string[] {
|
|
|
126
138
|
if (match.includes('data-zen-')) {
|
|
127
139
|
return false
|
|
128
140
|
}
|
|
141
|
+
// Exclude if it contains HTML tags (likely from entity decoding in display content)
|
|
142
|
+
// Real expressions don't start with < inside braces
|
|
143
|
+
if (match.match(/^\{[\s]*</)) {
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
// Exclude if it looks like display content containing HTML (spans, divs, etc)
|
|
147
|
+
if (/<[a-zA-Z]/.test(match)) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
// Exclude CSS-like content (common in style attributes)
|
|
151
|
+
if (match.includes(';') && match.includes(':')) {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
// Exclude if it's a single closing tag pattern (from multiline display)
|
|
155
|
+
if (/^\{[\s]*<\//.test(match)) {
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
129
158
|
// This looks like a raw expression
|
|
130
159
|
return true
|
|
131
160
|
})
|
|
132
|
-
|
|
161
|
+
|
|
133
162
|
if (actualExpressions.length > 0) {
|
|
134
163
|
errors.push(
|
|
135
164
|
`HTML contains raw expressions that were not compiled: ${actualExpressions.join(', ')}\n` +
|
|
@@ -138,7 +167,7 @@ function verifyNoRawExpressions(html: string, filePath: string): string[] {
|
|
|
138
167
|
)
|
|
139
168
|
}
|
|
140
169
|
}
|
|
141
|
-
|
|
170
|
+
|
|
142
171
|
return errors
|
|
143
172
|
}
|
|
144
173
|
|
|
@@ -152,12 +181,12 @@ export function finalizeOutputOrThrow(
|
|
|
152
181
|
compiled: CompiledTemplate
|
|
153
182
|
): FinalizedOutput {
|
|
154
183
|
const output = finalizeOutput(ir, compiled)
|
|
155
|
-
|
|
184
|
+
|
|
156
185
|
if (output.hasErrors) {
|
|
157
186
|
const errorMessage = output.errors.join('\n\n')
|
|
158
187
|
throw new Error(`Compilation failed:\n\n${errorMessage}`)
|
|
159
188
|
}
|
|
160
|
-
|
|
189
|
+
|
|
161
190
|
return output
|
|
162
191
|
}
|
|
163
192
|
|
package/compiler/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { parseTemplate } from './parse/parseTemplate'
|
|
|
3
3
|
import { parseScript } from './parse/parseScript'
|
|
4
4
|
import { transformTemplate } from './transform/transformTemplate'
|
|
5
5
|
import { finalizeOutputOrThrow } from './finalize/finalizeOutput'
|
|
6
|
+
import { validateInvariants } from './validate/invariants'
|
|
7
|
+
import { InvariantError } from './errors/compilerError'
|
|
6
8
|
import type { ZenIR, StyleIR } from './ir/types'
|
|
7
9
|
import type { CompiledTemplate } from './output/types'
|
|
8
10
|
import type { FinalizedOutput } from './finalize/finalizeOutput'
|
|
@@ -22,7 +24,13 @@ export function compileZen(filePath: string): {
|
|
|
22
24
|
/**
|
|
23
25
|
* Compile Zen source string into IR and CompiledTemplate
|
|
24
26
|
*/
|
|
25
|
-
export function compileZenSource(
|
|
27
|
+
export function compileZenSource(
|
|
28
|
+
source: string,
|
|
29
|
+
filePath: string,
|
|
30
|
+
options?: {
|
|
31
|
+
componentsDir?: string
|
|
32
|
+
}
|
|
33
|
+
): {
|
|
26
34
|
ir: ZenIR
|
|
27
35
|
compiled: CompiledTemplate
|
|
28
36
|
finalized?: FinalizedOutput
|
|
@@ -34,27 +42,40 @@ export function compileZenSource(source: string, filePath: string): {
|
|
|
34
42
|
const script = parseScript(source)
|
|
35
43
|
|
|
36
44
|
// Parse styles
|
|
37
|
-
const styleRegex =
|
|
45
|
+
const styleRegex = /\u003cstyle[^\u003e]*\u003e([\s\S]*?)\u003c\/style\u003e/gi
|
|
38
46
|
const styles: StyleIR[] = []
|
|
39
47
|
let match
|
|
40
48
|
while ((match = styleRegex.exec(source)) !== null) {
|
|
41
49
|
if (match[1]) styles.push({ raw: match[1].trim() })
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
let ir: ZenIR = {
|
|
45
53
|
filePath,
|
|
46
54
|
template,
|
|
47
55
|
script,
|
|
48
56
|
styles
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
// Resolve components if components directory is provided
|
|
60
|
+
if (options?.componentsDir) {
|
|
61
|
+
const { discoverComponents } = require('./discovery/componentDiscovery')
|
|
62
|
+
const { resolveComponentsInIR } = require('./transform/componentResolver')
|
|
63
|
+
|
|
64
|
+
// Component resolution may throw InvariantError — let it propagate
|
|
65
|
+
const components = discoverComponents(options.componentsDir)
|
|
66
|
+
ir = resolveComponentsInIR(ir, components)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate all compiler invariants after resolution
|
|
70
|
+
// Throws InvariantError if any invariant is violated
|
|
71
|
+
validateInvariants(ir, filePath)
|
|
72
|
+
|
|
51
73
|
const compiled = transformTemplate(ir)
|
|
52
74
|
|
|
53
75
|
try {
|
|
54
76
|
const finalized = finalizeOutputOrThrow(ir, compiled)
|
|
55
77
|
return { ir, compiled, finalized }
|
|
56
78
|
} catch (error: any) {
|
|
57
|
-
throw new Error(`Failed to finalize output for ${filePath}
|
|
79
|
+
throw new Error(`Failed to finalize output for ${filePath}:\\n${error.message}`)
|
|
58
80
|
}
|
|
59
81
|
}
|
|
60
|
-
|
package/compiler/ir/types.ts
CHANGED
|
@@ -23,6 +23,10 @@ export type TemplateNode =
|
|
|
23
23
|
| ElementNode
|
|
24
24
|
| TextNode
|
|
25
25
|
| ExpressionNode
|
|
26
|
+
| ComponentNode
|
|
27
|
+
| ConditionalFragmentNode // JSX ternary: {cond ? <A /> : <B />}
|
|
28
|
+
| OptionalFragmentNode // JSX logical AND: {cond && <A />}
|
|
29
|
+
| LoopFragmentNode // JSX map: {items.map(i => <li>...</li>)}
|
|
26
30
|
|
|
27
31
|
export type ElementNode = {
|
|
28
32
|
type: 'element'
|
|
@@ -33,6 +37,15 @@ export type ElementNode = {
|
|
|
33
37
|
loopContext?: LoopContext // Phase 7: Inherited loop context from parent map expressions
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
export type ComponentNode = {
|
|
41
|
+
type: 'component'
|
|
42
|
+
name: string
|
|
43
|
+
attributes: AttributeIR[]
|
|
44
|
+
children: TemplateNode[]
|
|
45
|
+
location: SourceLocation
|
|
46
|
+
loopContext?: LoopContext
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
export type TextNode = {
|
|
37
50
|
type: 'text'
|
|
38
51
|
value: string
|
|
@@ -46,6 +59,58 @@ export type ExpressionNode = {
|
|
|
46
59
|
loopContext?: LoopContext // Phase 7: Loop context for expressions inside map iterations
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Conditional Fragment Node
|
|
64
|
+
*
|
|
65
|
+
* Represents ternary expressions with JSX branches: {cond ? <A /> : <B />}
|
|
66
|
+
*
|
|
67
|
+
* BOTH branches are compiled at compile time.
|
|
68
|
+
* Runtime toggles visibility — never creates DOM.
|
|
69
|
+
*/
|
|
70
|
+
export type ConditionalFragmentNode = {
|
|
71
|
+
type: 'conditional-fragment'
|
|
72
|
+
condition: string // The condition expression code
|
|
73
|
+
consequent: TemplateNode[] // Precompiled "true" branch
|
|
74
|
+
alternate: TemplateNode[] // Precompiled "false" branch
|
|
75
|
+
location: SourceLocation
|
|
76
|
+
loopContext?: LoopContext
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Optional Fragment Node
|
|
81
|
+
*
|
|
82
|
+
* Represents logical AND expressions with JSX: {cond && <A />}
|
|
83
|
+
*
|
|
84
|
+
* Fragment is compiled at compile time.
|
|
85
|
+
* Runtime toggles mount/unmount based on condition.
|
|
86
|
+
*/
|
|
87
|
+
export type OptionalFragmentNode = {
|
|
88
|
+
type: 'optional-fragment'
|
|
89
|
+
condition: string // The condition expression code
|
|
90
|
+
fragment: TemplateNode[] // Precompiled fragment
|
|
91
|
+
location: SourceLocation
|
|
92
|
+
loopContext?: LoopContext
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Loop Fragment Node
|
|
97
|
+
*
|
|
98
|
+
* Represents .map() expressions with JSX body: {items.map(i => <li>...</li>)}
|
|
99
|
+
*
|
|
100
|
+
* Desugars to @for loop semantics at compile time.
|
|
101
|
+
* Body is compiled once, instantiated per item at runtime.
|
|
102
|
+
* Node identity is compiler-owned via stable keys.
|
|
103
|
+
*/
|
|
104
|
+
export type LoopFragmentNode = {
|
|
105
|
+
type: 'loop-fragment'
|
|
106
|
+
source: string // Array expression (e.g., 'items')
|
|
107
|
+
itemVar: string // Loop variable (e.g., 'item')
|
|
108
|
+
indexVar?: string // Optional index variable
|
|
109
|
+
body: TemplateNode[] // Precompiled loop body template
|
|
110
|
+
location: SourceLocation
|
|
111
|
+
loopContext: LoopContext // Extended with this loop's variables
|
|
112
|
+
}
|
|
113
|
+
|
|
49
114
|
export type AttributeIR = {
|
|
50
115
|
name: string
|
|
51
116
|
value: string | ExpressionIR
|
|
@@ -82,3 +147,4 @@ export type SourceLocation = {
|
|
|
82
147
|
column: number
|
|
83
148
|
}
|
|
84
149
|
|
|
150
|
+
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { parse, parseFragment } from 'parse5'
|
|
9
9
|
import type { TemplateIR, TemplateNode, ElementNode, TextNode, ExpressionNode, AttributeIR, ExpressionIR, SourceLocation, LoopContext } from '../ir/types'
|
|
10
|
-
import { CompilerError } from '../errors/compilerError'
|
|
10
|
+
import { CompilerError, InvariantError } from '../errors/compilerError'
|
|
11
11
|
import { parseScript } from './parseScript'
|
|
12
12
|
import { detectMapExpression, extractLoopVariables, referencesLoopVariable } from './detectMapExpressions'
|
|
13
13
|
import { shouldAttachLoopContext, mergeLoopContext, extractLoopContextFromExpression } from './trackLoopContext'
|
|
14
|
+
import { INVARIANT } from '../validate/invariants'
|
|
15
|
+
import { lowerFragments } from '../transform/fragmentLowering'
|
|
14
16
|
|
|
15
17
|
// Generate stable IDs for expressions
|
|
16
18
|
let expressionIdCounter = 0
|
|
@@ -362,6 +364,29 @@ function parseNode(
|
|
|
362
364
|
const location = getLocation(node, originalHtml)
|
|
363
365
|
const tag = node.tagName?.toLowerCase() || node.nodeName
|
|
364
366
|
|
|
367
|
+
// Extract original tag name from source HTML to preserve casing (parse5 lowercases everything)
|
|
368
|
+
let originalTag = node.tagName || node.nodeName
|
|
369
|
+
if (node.sourceCodeLocation && node.sourceCodeLocation.startOffset !== undefined) {
|
|
370
|
+
const startOffset = node.sourceCodeLocation.startOffset
|
|
371
|
+
// Find the tag name in original HTML (after '<')
|
|
372
|
+
const tagMatch = originalHtml.slice(startOffset).match(/^<([a-zA-Z][a-zA-Z0-9._-]*)/)
|
|
373
|
+
if (tagMatch && tagMatch[1]) {
|
|
374
|
+
originalTag = tagMatch[1]
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// INV005: <template> tags are forbidden — use compound components instead
|
|
379
|
+
if (tag === 'template') {
|
|
380
|
+
throw new InvariantError(
|
|
381
|
+
INVARIANT.TEMPLATE_TAG,
|
|
382
|
+
`<template> tags are forbidden in Zenith. Use compound components (e.g., Card.Header) for named slots.`,
|
|
383
|
+
'Named slots use compound component pattern (Card.Header), not <template> tags.',
|
|
384
|
+
'unknown', // filePath passed to parseTemplate
|
|
385
|
+
location.line,
|
|
386
|
+
location.column
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
365
390
|
// Parse attributes
|
|
366
391
|
const attributes: AttributeIR[] = []
|
|
367
392
|
if (node.attrs) {
|
|
@@ -373,6 +398,18 @@ function parseNode(
|
|
|
373
398
|
}
|
|
374
399
|
: location
|
|
375
400
|
|
|
401
|
+
// INV006: slot="" attributes are forbidden — use compound components instead
|
|
402
|
+
if (attr.name === 'slot') {
|
|
403
|
+
throw new InvariantError(
|
|
404
|
+
INVARIANT.SLOT_ATTRIBUTE,
|
|
405
|
+
`slot="${attr.value || ''}" attribute is forbidden. Use compound components (e.g., Card.Header) for named slots.`,
|
|
406
|
+
'Named slots use compound component pattern (Card.Header), not slot="" attributes.',
|
|
407
|
+
'unknown',
|
|
408
|
+
attrLocation.line,
|
|
409
|
+
attrLocation.column
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
376
413
|
// Handle :attr="expr" syntax (colon-prefixed reactive attributes)
|
|
377
414
|
let attrName = attr.name
|
|
378
415
|
let attrValue = attr.value || ''
|
|
@@ -474,13 +511,29 @@ function parseNode(
|
|
|
474
511
|
}
|
|
475
512
|
}
|
|
476
513
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
514
|
+
// Check if this is a custom component (starts with uppercase)
|
|
515
|
+
const isComponent = originalTag.length > 0 && originalTag[0] === originalTag[0].toUpperCase()
|
|
516
|
+
|
|
517
|
+
if (isComponent) {
|
|
518
|
+
// This is a component node
|
|
519
|
+
return {
|
|
520
|
+
type: 'component',
|
|
521
|
+
name: originalTag,
|
|
522
|
+
attributes,
|
|
523
|
+
children,
|
|
524
|
+
location,
|
|
525
|
+
loopContext: elementLoopContext
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// This is a regular HTML element
|
|
529
|
+
return {
|
|
530
|
+
type: 'element',
|
|
531
|
+
tag,
|
|
532
|
+
attributes,
|
|
533
|
+
children,
|
|
534
|
+
location,
|
|
535
|
+
loopContext: elementLoopContext
|
|
536
|
+
}
|
|
484
537
|
}
|
|
485
538
|
}
|
|
486
539
|
|
|
@@ -517,9 +570,13 @@ export function parseTemplate(html: string, filePath: string): TemplateIR {
|
|
|
517
570
|
}
|
|
518
571
|
}
|
|
519
572
|
|
|
573
|
+
// Phase 8: Lower JSX expressions to structural fragments
|
|
574
|
+
// This transforms expressions like {cond ? <A /> : <B />} into ConditionalFragmentNode
|
|
575
|
+
const loweredNodes = lowerFragments(nodes, filePath, expressions)
|
|
576
|
+
|
|
520
577
|
return {
|
|
521
578
|
raw: templateHtml,
|
|
522
|
-
nodes,
|
|
579
|
+
nodes: loweredNodes,
|
|
523
580
|
expressions
|
|
524
581
|
}
|
|
525
582
|
} catch (error: any) {
|
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
* Generates JavaScript code that creates DOM elements from template nodes
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
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,
|
|
201
|
-
|
|
200
|
+
// DOM already loaded, hydrate immediately
|
|
201
|
+
autoHydrate();
|
|
202
202
|
}
|
|
203
203
|
})();
|
|
204
204
|
`
|
package/compiler/ssg-build.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { discoverPages, generateRouteDefinition } from "../router/manifest"
|
|
|
20
20
|
import { analyzePageSource, getAnalysisSummary, getBuildOutputType, type PageAnalysis } from "./build-analyzer"
|
|
21
21
|
import { generateBundleJS } from "../runtime/bundle-generator"
|
|
22
22
|
import { loadContent } from "../cli/utils/content"
|
|
23
|
+
import { compileCss, resolveGlobalsCss } from "./css"
|
|
23
24
|
|
|
24
25
|
// ============================================
|
|
25
26
|
// Types
|
|
@@ -292,12 +293,23 @@ export function buildSSG(options: SSGBuildOptions): void {
|
|
|
292
293
|
|
|
293
294
|
console.log('')
|
|
294
295
|
|
|
295
|
-
//
|
|
296
|
+
// Compile global styles (Tailwind CSS)
|
|
296
297
|
let globalStyles = ''
|
|
297
|
-
const
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
const globalsCssPath = resolveGlobalsCss(baseDir)
|
|
299
|
+
if (globalsCssPath) {
|
|
300
|
+
console.log('📦 Compiling CSS:', path.relative(baseDir, globalsCssPath))
|
|
301
|
+
const cssOutputPath = path.join(outDir, 'assets', 'styles.css')
|
|
302
|
+
const result = compileCss({
|
|
303
|
+
input: globalsCssPath,
|
|
304
|
+
output: cssOutputPath,
|
|
305
|
+
minify: true
|
|
306
|
+
})
|
|
307
|
+
if (result.success) {
|
|
308
|
+
globalStyles = result.css
|
|
309
|
+
console.log(`📦 Generated assets/styles.css (${result.duration}ms)`)
|
|
310
|
+
} else {
|
|
311
|
+
console.error('❌ CSS compilation failed:', result.error)
|
|
312
|
+
}
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
// Write bundle.js if any pages need hydration
|
|
@@ -307,12 +319,6 @@ export function buildSSG(options: SSGBuildOptions): void {
|
|
|
307
319
|
console.log('📦 Generated assets/bundle.js')
|
|
308
320
|
}
|
|
309
321
|
|
|
310
|
-
// Write global styles
|
|
311
|
-
if (globalStyles) {
|
|
312
|
-
fs.writeFileSync(path.join(outDir, 'assets', 'styles.css'), globalStyles)
|
|
313
|
-
console.log('📦 Generated assets/styles.css')
|
|
314
|
-
}
|
|
315
|
-
|
|
316
322
|
// Write each page
|
|
317
323
|
for (const page of compiledPages) {
|
|
318
324
|
// Create output directory
|