@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,819 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fragment Lowering
|
|
3
|
-
*
|
|
4
|
-
* Transforms JSX-returning expressions into structural fragment nodes.
|
|
5
|
-
*
|
|
6
|
-
* This phase runs AFTER parsing, BEFORE component resolution.
|
|
7
|
-
* Transforms ExpressionNode → ConditionalFragmentNode | OptionalFragmentNode | LoopFragmentNode
|
|
8
|
-
*
|
|
9
|
-
* IMPORTANT: JSX in Zenith is compile-time sugar only.
|
|
10
|
-
* The compiler enumerates all possible DOM shapes and lowers them at compile time.
|
|
11
|
-
* Runtime never creates DOM — it only toggles visibility and binds values.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type {
|
|
15
|
-
TemplateNode,
|
|
16
|
-
ExpressionNode,
|
|
17
|
-
ConditionalFragmentNode,
|
|
18
|
-
OptionalFragmentNode,
|
|
19
|
-
LoopFragmentNode,
|
|
20
|
-
LoopContext,
|
|
21
|
-
AttributeIR,
|
|
22
|
-
SourceLocation,
|
|
23
|
-
ExpressionIR
|
|
24
|
-
} from '../ir/types'
|
|
25
|
-
import { classifyExpression, requiresStructuralLowering } from './classifyExpression'
|
|
26
|
-
import { InvariantError } from '../errors/compilerError'
|
|
27
|
-
import { INVARIANT } from '../validate/invariants'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Lower JSX-returning expressions into structural fragments
|
|
31
|
-
*
|
|
32
|
-
* Walks the node tree and transforms ExpressionNode instances
|
|
33
|
-
* that return JSX into the appropriate fragment node types.
|
|
34
|
-
*
|
|
35
|
-
* @param nodes - Template nodes to process
|
|
36
|
-
* @param filePath - Source file path for error reporting
|
|
37
|
-
* @param expressions - Expression registry (mutated to add new expressions)
|
|
38
|
-
* @returns Lowered nodes with fragment bindings
|
|
39
|
-
*/
|
|
40
|
-
export function lowerFragments(
|
|
41
|
-
nodes: TemplateNode[],
|
|
42
|
-
filePath: string,
|
|
43
|
-
expressions: ExpressionIR[]
|
|
44
|
-
): TemplateNode[] {
|
|
45
|
-
const lowered = nodes.map(node => lowerNode(node, filePath, expressions))
|
|
46
|
-
|
|
47
|
-
// INV-EXPR-REG-001 Enforcement
|
|
48
|
-
lowered.forEach(node => assertNoRawExpressions(node, filePath))
|
|
49
|
-
|
|
50
|
-
return lowered
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Lower a single node
|
|
55
|
-
*/
|
|
56
|
-
function lowerNode(
|
|
57
|
-
node: TemplateNode,
|
|
58
|
-
filePath: string,
|
|
59
|
-
expressions: ExpressionIR[]
|
|
60
|
-
): TemplateNode {
|
|
61
|
-
switch (node.type) {
|
|
62
|
-
case 'expression':
|
|
63
|
-
return lowerExpressionNode(node, filePath, expressions)
|
|
64
|
-
|
|
65
|
-
case 'element': {
|
|
66
|
-
// Check if this is an <html-content> element directive
|
|
67
|
-
if (node.tag === 'html-content') {
|
|
68
|
-
return lowerHtmlContentElement(node, filePath, expressions)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
...node,
|
|
73
|
-
children: lowerFragments(node.children, filePath, expressions)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
case 'component':
|
|
78
|
-
return {
|
|
79
|
-
...node,
|
|
80
|
-
children: lowerFragments(node.children, filePath, expressions)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
case 'conditional-fragment':
|
|
84
|
-
return {
|
|
85
|
-
...node,
|
|
86
|
-
consequent: lowerFragments(node.consequent, filePath, expressions),
|
|
87
|
-
alternate: lowerFragments(node.alternate, filePath, expressions)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
case 'optional-fragment':
|
|
91
|
-
return {
|
|
92
|
-
...node,
|
|
93
|
-
fragment: lowerFragments(node.fragment, filePath, expressions)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
case 'loop-fragment':
|
|
97
|
-
return {
|
|
98
|
-
...node,
|
|
99
|
-
body: lowerFragments(node.body, filePath, expressions)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
case 'text':
|
|
103
|
-
default:
|
|
104
|
-
return node
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Lower an expression node to a fragment if it returns JSX
|
|
110
|
-
*/
|
|
111
|
-
function lowerExpressionNode(
|
|
112
|
-
node: ExpressionNode,
|
|
113
|
-
filePath: string,
|
|
114
|
-
expressions: ExpressionIR[]
|
|
115
|
-
): TemplateNode {
|
|
116
|
-
// Resolve the expression ID to its original code for classification
|
|
117
|
-
const exprIR = expressions.find(e => e.id === node.expression)
|
|
118
|
-
if (!exprIR) {
|
|
119
|
-
// If not found in registry, it might already be an ID or an error
|
|
120
|
-
// But for classification we need the code.
|
|
121
|
-
return node
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const classification = classifyExpression(exprIR.code)
|
|
125
|
-
|
|
126
|
-
// Primitive expressions pass through unchanged
|
|
127
|
-
if (classification.type === 'primitive') {
|
|
128
|
-
return node
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Unknown expressions with JSX are compile errors
|
|
132
|
-
if (classification.type === 'unknown') {
|
|
133
|
-
throw new InvariantError(
|
|
134
|
-
INVARIANT.NON_ENUMERABLE_JSX,
|
|
135
|
-
`JSX expression output cannot be statically determined: ${node.expression.slice(0, 50)}...`,
|
|
136
|
-
'JSX expressions must have statically enumerable output. The compiler must know all possible DOM shapes at compile time.',
|
|
137
|
-
filePath,
|
|
138
|
-
node.location.line,
|
|
139
|
-
node.location.column
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Lower based on classification type
|
|
144
|
-
switch (classification.type) {
|
|
145
|
-
case 'conditional':
|
|
146
|
-
return lowerConditionalExpression(
|
|
147
|
-
node,
|
|
148
|
-
classification.condition!,
|
|
149
|
-
classification.consequent!,
|
|
150
|
-
classification.alternate!,
|
|
151
|
-
filePath,
|
|
152
|
-
expressions
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
case 'optional':
|
|
156
|
-
return lowerOptionalExpression(
|
|
157
|
-
node,
|
|
158
|
-
classification.optionalCondition!,
|
|
159
|
-
classification.optionalFragment!,
|
|
160
|
-
filePath,
|
|
161
|
-
expressions
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
case 'loop':
|
|
165
|
-
return lowerLoopExpression(
|
|
166
|
-
node,
|
|
167
|
-
classification.loopSource!,
|
|
168
|
-
classification.loopItemVar!,
|
|
169
|
-
classification.loopIndexVar,
|
|
170
|
-
classification.loopBody!,
|
|
171
|
-
filePath,
|
|
172
|
-
expressions
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
case 'fragment':
|
|
176
|
-
return lowerInlineFragment(
|
|
177
|
-
node,
|
|
178
|
-
classification.fragmentCode!,
|
|
179
|
-
filePath,
|
|
180
|
-
expressions
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
default:
|
|
184
|
-
// Should not reach here
|
|
185
|
-
return node
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Lower conditional expression: condition ? <A /> : <B />
|
|
191
|
-
*
|
|
192
|
-
* Both branches are parsed and compiled at compile time.
|
|
193
|
-
*/
|
|
194
|
-
function lowerConditionalExpression(
|
|
195
|
-
node: ExpressionNode,
|
|
196
|
-
condition: string,
|
|
197
|
-
consequentCode: string,
|
|
198
|
-
alternateCode: string,
|
|
199
|
-
filePath: string,
|
|
200
|
-
expressions: ExpressionIR[]
|
|
201
|
-
): ConditionalFragmentNode {
|
|
202
|
-
// Register condition
|
|
203
|
-
const conditionId = registerExpression(condition, node.location, expressions)
|
|
204
|
-
|
|
205
|
-
// Parse both branches as JSX fragments
|
|
206
|
-
const consequent = parseJSXToNodes(consequentCode, node.location, filePath, expressions, node.loopContext)
|
|
207
|
-
const alternate = parseJSXToNodes(alternateCode, node.location, filePath, expressions, node.loopContext)
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
type: 'conditional-fragment',
|
|
211
|
-
condition: conditionId,
|
|
212
|
-
consequent,
|
|
213
|
-
alternate,
|
|
214
|
-
location: node.location,
|
|
215
|
-
loopContext: node.loopContext
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Lower optional expression: condition && <A />
|
|
221
|
-
*
|
|
222
|
-
* Fragment is parsed and compiled at compile time.
|
|
223
|
-
*/
|
|
224
|
-
function lowerOptionalExpression(
|
|
225
|
-
node: ExpressionNode,
|
|
226
|
-
condition: string,
|
|
227
|
-
fragmentCode: string,
|
|
228
|
-
filePath: string,
|
|
229
|
-
expressions: ExpressionIR[]
|
|
230
|
-
): OptionalFragmentNode {
|
|
231
|
-
// Register condition
|
|
232
|
-
const conditionId = registerExpression(condition, node.location, expressions)
|
|
233
|
-
|
|
234
|
-
const fragment = parseJSXToNodes(fragmentCode, node.location, filePath, expressions, node.loopContext)
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
type: 'optional-fragment',
|
|
238
|
-
condition: conditionId,
|
|
239
|
-
fragment,
|
|
240
|
-
location: node.location,
|
|
241
|
-
loopContext: node.loopContext
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Lower loop expression: items.map(item => <li>...</li>)
|
|
247
|
-
*
|
|
248
|
-
* Body is parsed and compiled once, instantiated per item at runtime.
|
|
249
|
-
*/
|
|
250
|
-
function lowerLoopExpression(
|
|
251
|
-
node: ExpressionNode,
|
|
252
|
-
source: string,
|
|
253
|
-
itemVar: string,
|
|
254
|
-
indexVar: string | undefined,
|
|
255
|
-
bodyCode: string,
|
|
256
|
-
filePath: string,
|
|
257
|
-
expressions: ExpressionIR[]
|
|
258
|
-
): LoopFragmentNode {
|
|
259
|
-
// Register loop source as an expression ID
|
|
260
|
-
const sourceId = registerExpression(source, node.location, expressions)
|
|
261
|
-
|
|
262
|
-
// Create loop context for the body
|
|
263
|
-
const loopVariables = [itemVar]
|
|
264
|
-
if (indexVar) {
|
|
265
|
-
loopVariables.push(indexVar)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const bodyLoopContext: LoopContext = {
|
|
269
|
-
variables: node.loopContext
|
|
270
|
-
? [...node.loopContext.variables, ...loopVariables]
|
|
271
|
-
: loopVariables,
|
|
272
|
-
mapSource: sourceId // Use expression ID here
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Parse body with loop context
|
|
276
|
-
const body = parseJSXToNodes(bodyCode, node.location, filePath, expressions, bodyLoopContext)
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
type: 'loop-fragment',
|
|
280
|
-
source: sourceId, // Use expression ID here
|
|
281
|
-
itemVar,
|
|
282
|
-
indexVar,
|
|
283
|
-
body,
|
|
284
|
-
location: node.location,
|
|
285
|
-
loopContext: bodyLoopContext
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Lower inline fragment: <A /> or <><A /><B /></>
|
|
291
|
-
*
|
|
292
|
-
* JSX is parsed and inlined directly into the node tree.
|
|
293
|
-
* Returns the original expression node since inline JSX
|
|
294
|
-
* is already handled by the expression transformer.
|
|
295
|
-
*/
|
|
296
|
-
function lowerInlineFragment(
|
|
297
|
-
node: ExpressionNode,
|
|
298
|
-
fragmentCode: string,
|
|
299
|
-
filePath: string,
|
|
300
|
-
expressions: ExpressionIR[]
|
|
301
|
-
): TemplateNode {
|
|
302
|
-
// For now, inline fragments are handled by the existing expression transformer
|
|
303
|
-
// which converts JSX to __zenith.h() calls
|
|
304
|
-
// In a future iteration, we could parse them to static nodes here
|
|
305
|
-
return node
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Lower <html-content> element directive
|
|
310
|
-
*
|
|
311
|
-
* Syntax: <html-content content="expr" />
|
|
312
|
-
*
|
|
313
|
-
* This renders the expression value as raw HTML (innerHTML).
|
|
314
|
-
* Use with caution - only for trusted content like icons defined in code.
|
|
315
|
-
*/
|
|
316
|
-
function lowerHtmlContentElement(
|
|
317
|
-
node: import('../ir/types').ElementNode,
|
|
318
|
-
filePath: string,
|
|
319
|
-
expressions: ExpressionIR[]
|
|
320
|
-
): TemplateNode {
|
|
321
|
-
// Extract 'content' attribute
|
|
322
|
-
const contentAttr = node.attributes.find(a => a.name === 'content')
|
|
323
|
-
|
|
324
|
-
if (!contentAttr || typeof contentAttr.value !== 'string') {
|
|
325
|
-
throw new InvariantError(
|
|
326
|
-
'ZEN001',
|
|
327
|
-
`<html-content> element requires a 'content' attribute specifying the expression`,
|
|
328
|
-
'Usage: <html-content content="item.html" />',
|
|
329
|
-
filePath,
|
|
330
|
-
node.location.line,
|
|
331
|
-
node.location.column
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const exprCode = contentAttr.value.trim()
|
|
336
|
-
|
|
337
|
-
// Generate expression ID and register the expression
|
|
338
|
-
const exprId = `expr_${expressions.length}`
|
|
339
|
-
const exprIR: ExpressionIR = {
|
|
340
|
-
id: exprId,
|
|
341
|
-
code: exprCode,
|
|
342
|
-
location: node.location
|
|
343
|
-
}
|
|
344
|
-
expressions.push(exprIR)
|
|
345
|
-
|
|
346
|
-
// Create a span element with data-zen-html attribute for raw HTML binding
|
|
347
|
-
return {
|
|
348
|
-
type: 'element',
|
|
349
|
-
tag: 'span',
|
|
350
|
-
attributes: [
|
|
351
|
-
{ name: 'data-zen-html', value: exprIR, location: node.location, loopContext: node.loopContext },
|
|
352
|
-
{ name: 'style', value: 'display: contents;', location: node.location }
|
|
353
|
-
],
|
|
354
|
-
children: [],
|
|
355
|
-
location: node.location,
|
|
356
|
-
loopContext: node.loopContext
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Register a raw expression code string and return its ID
|
|
362
|
-
*/
|
|
363
|
-
function registerExpression(
|
|
364
|
-
code: string,
|
|
365
|
-
location: SourceLocation,
|
|
366
|
-
expressions: ExpressionIR[]
|
|
367
|
-
): string {
|
|
368
|
-
const id = `expr_${expressions.length}`
|
|
369
|
-
expressions.push({ id, code, location })
|
|
370
|
-
return id
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* INV-EXPR-REG-001: Assert no raw expressions exist beyond parse phase
|
|
375
|
-
*/
|
|
376
|
-
function assertNoRawExpressions(node: TemplateNode, filePath: string): void {
|
|
377
|
-
const errorPrefix = `Compiler invariant violated: runtime expression used without registration. This is a compiler bug, not a user error.`
|
|
378
|
-
|
|
379
|
-
switch (node.type) {
|
|
380
|
-
case 'expression':
|
|
381
|
-
if (!node.expression.startsWith('expr_')) {
|
|
382
|
-
throw new InvariantError(
|
|
383
|
-
'INV-EXPR-REG-001',
|
|
384
|
-
`${errorPrefix}\nRaw expression found: "${node.expression}"`,
|
|
385
|
-
'Every expression must be registered and referenced by ID.',
|
|
386
|
-
filePath,
|
|
387
|
-
node.location.line,
|
|
388
|
-
node.location.column
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
break
|
|
392
|
-
|
|
393
|
-
case 'loop-fragment':
|
|
394
|
-
if (!node.source.startsWith('expr_')) {
|
|
395
|
-
throw new InvariantError(
|
|
396
|
-
'INV-EXPR-REG-001',
|
|
397
|
-
`${errorPrefix}\nRaw loop source found: "${node.source}"`,
|
|
398
|
-
'Loop sources must be registered and referenced by ID.',
|
|
399
|
-
filePath,
|
|
400
|
-
node.location.line,
|
|
401
|
-
node.location.column
|
|
402
|
-
)
|
|
403
|
-
}
|
|
404
|
-
node.body.forEach(child => assertNoRawExpressions(child, filePath))
|
|
405
|
-
break
|
|
406
|
-
|
|
407
|
-
case 'conditional-fragment':
|
|
408
|
-
if (!node.condition.startsWith('expr_')) {
|
|
409
|
-
throw new InvariantError(
|
|
410
|
-
'INV-EXPR-REG-001',
|
|
411
|
-
`${errorPrefix}\nRaw condition found: "${node.condition}"`,
|
|
412
|
-
'Conditional expressions must be registered and referenced by ID.',
|
|
413
|
-
filePath,
|
|
414
|
-
node.location.line,
|
|
415
|
-
node.location.column
|
|
416
|
-
)
|
|
417
|
-
}
|
|
418
|
-
node.consequent.forEach(child => assertNoRawExpressions(child, filePath))
|
|
419
|
-
node.alternate.forEach(child => assertNoRawExpressions(child, filePath))
|
|
420
|
-
break
|
|
421
|
-
|
|
422
|
-
case 'optional-fragment':
|
|
423
|
-
if (!node.condition.startsWith('expr_')) {
|
|
424
|
-
throw new InvariantError(
|
|
425
|
-
'INV-EXPR-REG-001',
|
|
426
|
-
`${errorPrefix}\nRaw condition found: "${node.condition}"`,
|
|
427
|
-
'Optional expressions must be registered and referenced by ID.',
|
|
428
|
-
filePath,
|
|
429
|
-
node.location.line,
|
|
430
|
-
node.location.column
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
node.fragment.forEach(child => assertNoRawExpressions(child, filePath))
|
|
434
|
-
break
|
|
435
|
-
|
|
436
|
-
case 'element':
|
|
437
|
-
case 'component':
|
|
438
|
-
node.attributes.forEach(attr => {
|
|
439
|
-
if (typeof attr.value !== 'string' && !attr.value.id.startsWith('expr_')) {
|
|
440
|
-
throw new InvariantError(
|
|
441
|
-
'INV-EXPR-REG-001',
|
|
442
|
-
`${errorPrefix}\nRaw attribute expression found: "${(attr.value as any).code}"`,
|
|
443
|
-
'Attribute expressions must be registered and referenced by ID.',
|
|
444
|
-
filePath,
|
|
445
|
-
attr.location.line,
|
|
446
|
-
attr.location.column
|
|
447
|
-
)
|
|
448
|
-
}
|
|
449
|
-
})
|
|
450
|
-
node.children.forEach(child => assertNoRawExpressions(child, filePath))
|
|
451
|
-
break
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Parse JSX code string into TemplateNode[]
|
|
457
|
-
*
|
|
458
|
-
* This is a simplified parser for JSX fragments within expressions.
|
|
459
|
-
* It handles basic JSX structure for lowering purposes.
|
|
460
|
-
*/
|
|
461
|
-
function parseJSXToNodes(
|
|
462
|
-
code: string,
|
|
463
|
-
baseLocation: SourceLocation,
|
|
464
|
-
filePath: string,
|
|
465
|
-
expressions: ExpressionIR[],
|
|
466
|
-
loopContext?: LoopContext
|
|
467
|
-
): TemplateNode[] {
|
|
468
|
-
const trimmed = code.trim()
|
|
469
|
-
|
|
470
|
-
// Handle fragment syntax <>...</>
|
|
471
|
-
if (trimmed.startsWith('<>')) {
|
|
472
|
-
const content = extractFragmentContent(trimmed)
|
|
473
|
-
return parseJSXChildren(content, baseLocation, filePath, expressions, loopContext)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Handle single element
|
|
477
|
-
if (trimmed.startsWith('<')) {
|
|
478
|
-
const element = parseJSXElement(trimmed, baseLocation, filePath, expressions, loopContext)
|
|
479
|
-
return element ? [element] : []
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Handle parenthesized expression
|
|
483
|
-
if (trimmed.startsWith('(')) {
|
|
484
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
485
|
-
return parseJSXToNodes(inner, baseLocation, filePath, expressions, loopContext)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Not JSX - return as expression node
|
|
489
|
-
return [{
|
|
490
|
-
type: 'expression',
|
|
491
|
-
expression: trimmed,
|
|
492
|
-
location: baseLocation,
|
|
493
|
-
loopContext
|
|
494
|
-
}]
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Extract content from fragment syntax <>content</>
|
|
499
|
-
*/
|
|
500
|
-
function extractFragmentContent(code: string): string {
|
|
501
|
-
// Remove <> prefix and </> suffix
|
|
502
|
-
const withoutOpen = code.slice(2)
|
|
503
|
-
const closeIndex = withoutOpen.lastIndexOf('</>')
|
|
504
|
-
if (closeIndex === -1) {
|
|
505
|
-
return withoutOpen
|
|
506
|
-
}
|
|
507
|
-
return withoutOpen.slice(0, closeIndex)
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Parse JSX children content
|
|
512
|
-
*/
|
|
513
|
-
function parseJSXChildren(
|
|
514
|
-
content: string,
|
|
515
|
-
baseLocation: SourceLocation,
|
|
516
|
-
filePath: string,
|
|
517
|
-
expressions: ExpressionIR[],
|
|
518
|
-
loopContext?: LoopContext
|
|
519
|
-
): TemplateNode[] {
|
|
520
|
-
const nodes: TemplateNode[] = []
|
|
521
|
-
let i = 0
|
|
522
|
-
let currentText = ''
|
|
523
|
-
|
|
524
|
-
while (i < content.length) {
|
|
525
|
-
const char = content[i]
|
|
526
|
-
|
|
527
|
-
// Check for JSX element
|
|
528
|
-
if (char === '<' && /[a-zA-Z]/.test(content[i + 1] || '')) {
|
|
529
|
-
// Save accumulated text
|
|
530
|
-
if (currentText.trim()) {
|
|
531
|
-
nodes.push({
|
|
532
|
-
type: 'text',
|
|
533
|
-
value: currentText.trim(),
|
|
534
|
-
location: baseLocation
|
|
535
|
-
})
|
|
536
|
-
currentText = ''
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Parse element
|
|
540
|
-
const result = parseJSXElementWithEnd(content, i, baseLocation, filePath, expressions, loopContext)
|
|
541
|
-
if (result) {
|
|
542
|
-
nodes.push(result.node)
|
|
543
|
-
i = result.endIndex
|
|
544
|
-
continue
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Check for expression {expr}
|
|
549
|
-
if (char === '{') {
|
|
550
|
-
const endBrace = findBalancedBraceEnd(content, i)
|
|
551
|
-
if (endBrace !== -1) {
|
|
552
|
-
// Save accumulated text
|
|
553
|
-
if (currentText.trim()) {
|
|
554
|
-
nodes.push({
|
|
555
|
-
type: 'text',
|
|
556
|
-
value: currentText.trim(),
|
|
557
|
-
location: baseLocation
|
|
558
|
-
})
|
|
559
|
-
currentText = ''
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const exprCode = content.slice(i + 1, endBrace - 1).trim()
|
|
563
|
-
if (exprCode) {
|
|
564
|
-
// Register inner expression
|
|
565
|
-
const exprId = registerExpression(exprCode, baseLocation, expressions)
|
|
566
|
-
nodes.push({
|
|
567
|
-
type: 'expression',
|
|
568
|
-
expression: exprId,
|
|
569
|
-
location: baseLocation,
|
|
570
|
-
loopContext
|
|
571
|
-
})
|
|
572
|
-
}
|
|
573
|
-
i = endBrace
|
|
574
|
-
continue
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
currentText += char
|
|
579
|
-
i++
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Add remaining text
|
|
583
|
-
if (currentText.trim()) {
|
|
584
|
-
nodes.push({
|
|
585
|
-
type: 'text',
|
|
586
|
-
value: currentText.trim(),
|
|
587
|
-
location: baseLocation
|
|
588
|
-
})
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return nodes
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Parse a single JSX element
|
|
596
|
-
*/
|
|
597
|
-
function parseJSXElement(
|
|
598
|
-
code: string,
|
|
599
|
-
baseLocation: SourceLocation,
|
|
600
|
-
filePath: string,
|
|
601
|
-
expressions: ExpressionIR[],
|
|
602
|
-
loopContext?: LoopContext
|
|
603
|
-
): TemplateNode | null {
|
|
604
|
-
const result = parseJSXElementWithEnd(code, 0, baseLocation, filePath, expressions, loopContext)
|
|
605
|
-
return result ? result.node : null
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Parse JSX element and return end index
|
|
610
|
-
*/
|
|
611
|
-
function parseJSXElementWithEnd(
|
|
612
|
-
code: string,
|
|
613
|
-
startIndex: number,
|
|
614
|
-
baseLocation: SourceLocation,
|
|
615
|
-
filePath: string,
|
|
616
|
-
expressions: ExpressionIR[],
|
|
617
|
-
loopContext?: LoopContext
|
|
618
|
-
): { node: TemplateNode; endIndex: number } | null {
|
|
619
|
-
// Extract tag name
|
|
620
|
-
const tagMatch = code.slice(startIndex).match(/^<([a-zA-Z][a-zA-Z0-9.]*)/)
|
|
621
|
-
if (!tagMatch) return null
|
|
622
|
-
|
|
623
|
-
const tagName = tagMatch[1]!
|
|
624
|
-
let i = startIndex + tagMatch[0].length
|
|
625
|
-
|
|
626
|
-
// Parse attributes (simplified)
|
|
627
|
-
const attributes: AttributeIR[] = []
|
|
628
|
-
|
|
629
|
-
// Skip whitespace and parse attributes until > or />
|
|
630
|
-
while (i < code.length) {
|
|
631
|
-
// Skip whitespace
|
|
632
|
-
while (i < code.length && /\s/.test(code[i]!)) i++
|
|
633
|
-
|
|
634
|
-
// Check for end of opening tag
|
|
635
|
-
if (code[i] === '>') {
|
|
636
|
-
i++
|
|
637
|
-
break
|
|
638
|
-
}
|
|
639
|
-
if (code[i] === '/' && code[i + 1] === '>') {
|
|
640
|
-
// Self-closing tag
|
|
641
|
-
const isComponent = tagName[0] === tagName[0]!.toUpperCase()
|
|
642
|
-
const node: TemplateNode = isComponent ? {
|
|
643
|
-
type: 'component',
|
|
644
|
-
name: tagName,
|
|
645
|
-
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
646
|
-
children: [],
|
|
647
|
-
location: baseLocation,
|
|
648
|
-
loopContext
|
|
649
|
-
} : {
|
|
650
|
-
type: 'element',
|
|
651
|
-
tag: tagName.toLowerCase(),
|
|
652
|
-
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
653
|
-
children: [],
|
|
654
|
-
location: baseLocation,
|
|
655
|
-
loopContext
|
|
656
|
-
}
|
|
657
|
-
return { node, endIndex: i + 2 }
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Parse attribute name
|
|
661
|
-
const attrMatch = code.slice(i).match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/)
|
|
662
|
-
if (!attrMatch) {
|
|
663
|
-
i++
|
|
664
|
-
continue
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const attrName = attrMatch[1]!
|
|
668
|
-
i += attrName.length
|
|
669
|
-
|
|
670
|
-
// Skip whitespace
|
|
671
|
-
while (i < code.length && /\s/.test(code[i]!)) i++
|
|
672
|
-
|
|
673
|
-
// Check for value
|
|
674
|
-
if (code[i] !== '=') {
|
|
675
|
-
attributes.push({ name: attrName, value: 'true', location: baseLocation })
|
|
676
|
-
continue
|
|
677
|
-
}
|
|
678
|
-
i++ // Skip =
|
|
679
|
-
|
|
680
|
-
// Skip whitespace
|
|
681
|
-
while (i < code.length && /\s/.test(code[i]!)) i++
|
|
682
|
-
|
|
683
|
-
// Parse value
|
|
684
|
-
if (code[i] === '"' || code[i] === "'") {
|
|
685
|
-
const quote = code[i]
|
|
686
|
-
let endQuote = i + 1
|
|
687
|
-
while (endQuote < code.length && code[endQuote] !== quote) {
|
|
688
|
-
if (code[endQuote] === '\\') endQuote++
|
|
689
|
-
endQuote++
|
|
690
|
-
}
|
|
691
|
-
attributes.push({ name: attrName, value: code.slice(i + 1, endQuote), location: baseLocation })
|
|
692
|
-
i = endQuote + 1
|
|
693
|
-
} else if (code[i] === '{') {
|
|
694
|
-
const endBrace = findBalancedBraceEnd(code, i)
|
|
695
|
-
if (endBrace !== -1) {
|
|
696
|
-
const exprCode = code.slice(i + 1, endBrace - 1).trim()
|
|
697
|
-
const exprId = registerExpression(exprCode, baseLocation, expressions)
|
|
698
|
-
|
|
699
|
-
attributes.push({
|
|
700
|
-
name: attrName,
|
|
701
|
-
value: { id: exprId, code: exprCode, location: baseLocation },
|
|
702
|
-
location: baseLocation
|
|
703
|
-
})
|
|
704
|
-
i = endBrace
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Parse children until closing tag
|
|
710
|
-
const closeTag = `</${tagName}>`
|
|
711
|
-
const closeIndex = findClosingTag(code, i, tagName)
|
|
712
|
-
|
|
713
|
-
let children: TemplateNode[] = []
|
|
714
|
-
if (closeIndex !== -1 && closeIndex > i) {
|
|
715
|
-
const childContent = code.slice(i, closeIndex)
|
|
716
|
-
children = parseJSXChildren(childContent, baseLocation, filePath, expressions, loopContext)
|
|
717
|
-
i = closeIndex + closeTag.length
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const isComponent = tagName[0] === tagName[0]!.toUpperCase()
|
|
721
|
-
const node: TemplateNode = isComponent ? {
|
|
722
|
-
type: 'component',
|
|
723
|
-
name: tagName,
|
|
724
|
-
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
725
|
-
children,
|
|
726
|
-
location: baseLocation,
|
|
727
|
-
loopContext
|
|
728
|
-
} : {
|
|
729
|
-
type: 'element',
|
|
730
|
-
tag: tagName.toLowerCase(),
|
|
731
|
-
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
732
|
-
children,
|
|
733
|
-
location: baseLocation,
|
|
734
|
-
loopContext
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
return { node, endIndex: i }
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Find closing tag for an element
|
|
742
|
-
*/
|
|
743
|
-
function findClosingTag(code: string, startIndex: number, tagName: string): number {
|
|
744
|
-
const closeTag = `</${tagName}>`
|
|
745
|
-
let depth = 1
|
|
746
|
-
let i = startIndex
|
|
747
|
-
|
|
748
|
-
while (i < code.length && depth > 0) {
|
|
749
|
-
// Check for closing tag
|
|
750
|
-
if (code.slice(i, i + closeTag.length) === closeTag) {
|
|
751
|
-
depth--
|
|
752
|
-
if (depth === 0) return i
|
|
753
|
-
i += closeTag.length
|
|
754
|
-
continue
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Check for opening tag (same name, nested)
|
|
758
|
-
const openPattern = new RegExp(`^<${tagName}(?:\\s|>|/>)`)
|
|
759
|
-
const match = code.slice(i).match(openPattern)
|
|
760
|
-
if (match) {
|
|
761
|
-
// Check if self-closing
|
|
762
|
-
const selfClosing = code.slice(i).match(new RegExp(`^<${tagName}[^>]*/>`))
|
|
763
|
-
if (!selfClosing) {
|
|
764
|
-
depth++
|
|
765
|
-
}
|
|
766
|
-
i += match[0].length
|
|
767
|
-
continue
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
i++
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
return -1
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Find balanced brace end
|
|
778
|
-
*/
|
|
779
|
-
function findBalancedBraceEnd(code: string, startIndex: number): number {
|
|
780
|
-
if (code[startIndex] !== '{') return -1
|
|
781
|
-
|
|
782
|
-
let depth = 1
|
|
783
|
-
let i = startIndex + 1
|
|
784
|
-
let inString = false
|
|
785
|
-
let stringChar = ''
|
|
786
|
-
|
|
787
|
-
while (i < code.length && depth > 0) {
|
|
788
|
-
const char = code[i]
|
|
789
|
-
const prevChar = code[i - 1]
|
|
790
|
-
|
|
791
|
-
// Handle escape
|
|
792
|
-
if (prevChar === '\\') {
|
|
793
|
-
i++
|
|
794
|
-
continue
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Handle strings
|
|
798
|
-
if (!inString && (char === '"' || char === "'")) {
|
|
799
|
-
inString = true
|
|
800
|
-
stringChar = char
|
|
801
|
-
i++
|
|
802
|
-
continue
|
|
803
|
-
}
|
|
804
|
-
if (inString && char === stringChar) {
|
|
805
|
-
inString = false
|
|
806
|
-
i++
|
|
807
|
-
continue
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (!inString) {
|
|
811
|
-
if (char === '{') depth++
|
|
812
|
-
else if (char === '}') depth--
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
i++
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
return depth === 0 ? i : -1
|
|
819
|
-
}
|