@zenithbuild/compiler 1.0.2
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/build-analyzer.d.ts +44 -0
- package/dist/build-analyzer.js +87 -0
- package/dist/bundler.d.ts +31 -0
- package/dist/bundler.js +86 -0
- package/dist/core/components/index.d.ts +9 -0
- package/dist/core/components/index.js +13 -0
- package/dist/core/config/index.d.ts +11 -0
- package/dist/core/config/index.js +10 -0
- package/dist/core/config/loader.d.ts +17 -0
- package/dist/core/config/loader.js +60 -0
- package/dist/core/config/types.d.ts +98 -0
- package/dist/core/config/types.js +32 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +6 -0
- package/dist/core/lifecycle/index.d.ts +16 -0
- package/dist/core/lifecycle/index.js +19 -0
- package/dist/core/lifecycle/zen-mount.d.ts +66 -0
- package/dist/core/lifecycle/zen-mount.js +151 -0
- package/dist/core/lifecycle/zen-unmount.d.ts +54 -0
- package/dist/core/lifecycle/zen-unmount.js +76 -0
- package/dist/core/plugins/bridge.d.ts +116 -0
- package/dist/core/plugins/bridge.js +121 -0
- package/dist/core/plugins/index.d.ts +6 -0
- package/dist/core/plugins/index.js +6 -0
- package/dist/core/plugins/registry.d.ts +67 -0
- package/dist/core/plugins/registry.js +113 -0
- package/dist/core/reactivity/index.d.ts +30 -0
- package/dist/core/reactivity/index.js +33 -0
- package/dist/core/reactivity/tracking.d.ts +74 -0
- package/dist/core/reactivity/tracking.js +136 -0
- package/dist/core/reactivity/zen-batch.d.ts +45 -0
- package/dist/core/reactivity/zen-batch.js +54 -0
- package/dist/core/reactivity/zen-effect.d.ts +48 -0
- package/dist/core/reactivity/zen-effect.js +98 -0
- package/dist/core/reactivity/zen-memo.d.ts +43 -0
- package/dist/core/reactivity/zen-memo.js +100 -0
- package/dist/core/reactivity/zen-ref.d.ts +44 -0
- package/dist/core/reactivity/zen-ref.js +34 -0
- package/dist/core/reactivity/zen-signal.d.ts +48 -0
- package/dist/core/reactivity/zen-signal.js +84 -0
- package/dist/core/reactivity/zen-state.d.ts +35 -0
- package/dist/core/reactivity/zen-state.js +147 -0
- package/dist/core/reactivity/zen-untrack.d.ts +38 -0
- package/dist/core/reactivity/zen-untrack.js +41 -0
- package/dist/css/index.d.ts +73 -0
- package/dist/css/index.js +246 -0
- package/dist/discovery/componentDiscovery.d.ts +42 -0
- package/dist/discovery/componentDiscovery.js +56 -0
- package/dist/discovery/layouts.d.ts +13 -0
- package/dist/discovery/layouts.js +41 -0
- package/dist/errors/compilerError.d.ts +31 -0
- package/dist/errors/compilerError.js +51 -0
- package/dist/finalize/finalizeOutput.d.ts +32 -0
- package/dist/finalize/finalizeOutput.js +62 -0
- package/dist/finalize/generateFinalBundle.d.ts +24 -0
- package/dist/finalize/generateFinalBundle.js +68 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +51 -0
- package/dist/ir/types.d.ts +181 -0
- package/dist/ir/types.js +8 -0
- package/dist/output/types.d.ts +30 -0
- package/dist/output/types.js +6 -0
- package/dist/parse/detectMapExpressions.d.ts +45 -0
- package/dist/parse/detectMapExpressions.js +77 -0
- package/dist/parse/parseScript.d.ts +8 -0
- package/dist/parse/parseScript.js +36 -0
- package/dist/parse/parseTemplate.d.ts +11 -0
- package/dist/parse/parseTemplate.js +487 -0
- package/dist/parse/parseZenFile.d.ts +11 -0
- package/dist/parse/parseZenFile.js +50 -0
- package/dist/parse/scriptAnalysis.d.ts +25 -0
- package/dist/parse/scriptAnalysis.js +60 -0
- package/dist/parse/trackLoopContext.d.ts +20 -0
- package/dist/parse/trackLoopContext.js +62 -0
- package/dist/parseZenFile.d.ts +10 -0
- package/dist/parseZenFile.js +55 -0
- package/dist/runtime/analyzeAndEmit.d.ts +20 -0
- package/dist/runtime/analyzeAndEmit.js +70 -0
- package/dist/runtime/build.d.ts +6 -0
- package/dist/runtime/build.js +13 -0
- package/dist/runtime/bundle-generator.d.ts +27 -0
- package/dist/runtime/bundle-generator.js +1263 -0
- package/dist/runtime/client-runtime.d.ts +41 -0
- package/dist/runtime/client-runtime.js +397 -0
- package/dist/runtime/dataExposure.d.ts +52 -0
- package/dist/runtime/dataExposure.js +227 -0
- package/dist/runtime/generateDOM.d.ts +21 -0
- package/dist/runtime/generateDOM.js +194 -0
- package/dist/runtime/generateHydrationBundle.d.ts +15 -0
- package/dist/runtime/generateHydrationBundle.js +399 -0
- package/dist/runtime/hydration.d.ts +53 -0
- package/dist/runtime/hydration.js +271 -0
- package/dist/runtime/navigation.d.ts +58 -0
- package/dist/runtime/navigation.js +372 -0
- package/dist/runtime/serve.d.ts +13 -0
- package/dist/runtime/serve.js +76 -0
- package/dist/runtime/thinRuntime.d.ts +23 -0
- package/dist/runtime/thinRuntime.js +158 -0
- package/dist/runtime/transformIR.d.ts +19 -0
- package/dist/runtime/transformIR.js +285 -0
- package/dist/runtime/wrapExpression.d.ts +24 -0
- package/dist/runtime/wrapExpression.js +76 -0
- package/dist/runtime/wrapExpressionWithLoop.d.ts +17 -0
- package/dist/runtime/wrapExpressionWithLoop.js +75 -0
- package/dist/spa-build.d.ts +26 -0
- package/dist/spa-build.js +866 -0
- package/dist/ssg-build.d.ts +32 -0
- package/dist/ssg-build.js +408 -0
- package/dist/test/analyze-emit.test.d.ts +1 -0
- package/dist/test/analyze-emit.test.js +88 -0
- package/dist/test/bundler-contract.test.d.ts +1 -0
- package/dist/test/bundler-contract.test.js +137 -0
- package/dist/test/compiler-authority.test.d.ts +1 -0
- package/dist/test/compiler-authority.test.js +90 -0
- package/dist/test/component-instance-test.d.ts +1 -0
- package/dist/test/component-instance-test.js +115 -0
- package/dist/test/error-native-bridge.test.d.ts +1 -0
- package/dist/test/error-native-bridge.test.js +51 -0
- package/dist/test/error-serialization.test.d.ts +1 -0
- package/dist/test/error-serialization.test.js +38 -0
- package/dist/test/macro-inlining.test.d.ts +1 -0
- package/dist/test/macro-inlining.test.js +178 -0
- package/dist/test/validate-test.d.ts +6 -0
- package/dist/test/validate-test.js +95 -0
- package/dist/transform/classifyExpression.d.ts +46 -0
- package/dist/transform/classifyExpression.js +354 -0
- package/dist/transform/componentResolver.d.ts +15 -0
- package/dist/transform/componentResolver.js +30 -0
- package/dist/transform/expressionTransformer.d.ts +19 -0
- package/dist/transform/expressionTransformer.js +333 -0
- package/dist/transform/fragmentLowering.d.ts +25 -0
- package/dist/transform/fragmentLowering.js +468 -0
- package/dist/transform/layoutProcessor.d.ts +5 -0
- package/dist/transform/layoutProcessor.js +34 -0
- package/dist/transform/transformTemplate.d.ts +11 -0
- package/dist/transform/transformTemplate.js +33 -0
- package/dist/validate/invariants.d.ts +23 -0
- package/dist/validate/invariants.js +55 -0
- package/native/compiler-native/compiler-native.node +0 -0
- package/native/compiler-native/index.d.ts +113 -0
- package/native/compiler-native/index.js +19 -0
- package/native/compiler-native/package.json +19 -0
- package/package.json +49 -0
|
@@ -0,0 +1,468 @@
|
|
|
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
|
+
import { classifyExpression } from './classifyExpression';
|
|
14
|
+
import { InvariantError } from '../errors/compilerError';
|
|
15
|
+
import { INVARIANT } from '../validate/invariants';
|
|
16
|
+
/**
|
|
17
|
+
* Lower JSX-returning expressions into structural fragments
|
|
18
|
+
*
|
|
19
|
+
* Walks the node tree and transforms ExpressionNode instances
|
|
20
|
+
* that return JSX into the appropriate fragment node types.
|
|
21
|
+
*
|
|
22
|
+
* @param nodes - Template nodes to process
|
|
23
|
+
* @param filePath - Source file path for error reporting
|
|
24
|
+
* @param expressions - Expression registry (mutated to add new expressions)
|
|
25
|
+
* @returns Lowered nodes with fragment bindings
|
|
26
|
+
*/
|
|
27
|
+
export function lowerFragments(nodes, filePath, expressions) {
|
|
28
|
+
return nodes.map(node => lowerNode(node, filePath, expressions));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Lower a single node
|
|
32
|
+
*/
|
|
33
|
+
function lowerNode(node, filePath, expressions) {
|
|
34
|
+
switch (node.type) {
|
|
35
|
+
case 'expression':
|
|
36
|
+
return lowerExpressionNode(node, filePath, expressions);
|
|
37
|
+
case 'element':
|
|
38
|
+
return {
|
|
39
|
+
...node,
|
|
40
|
+
children: lowerFragments(node.children, filePath, expressions)
|
|
41
|
+
};
|
|
42
|
+
case 'component':
|
|
43
|
+
return {
|
|
44
|
+
...node,
|
|
45
|
+
children: lowerFragments(node.children, filePath, expressions)
|
|
46
|
+
};
|
|
47
|
+
case 'conditional-fragment':
|
|
48
|
+
return {
|
|
49
|
+
...node,
|
|
50
|
+
consequent: lowerFragments(node.consequent, filePath, expressions),
|
|
51
|
+
alternate: lowerFragments(node.alternate, filePath, expressions)
|
|
52
|
+
};
|
|
53
|
+
case 'optional-fragment':
|
|
54
|
+
return {
|
|
55
|
+
...node,
|
|
56
|
+
fragment: lowerFragments(node.fragment, filePath, expressions)
|
|
57
|
+
};
|
|
58
|
+
case 'loop-fragment':
|
|
59
|
+
return {
|
|
60
|
+
...node,
|
|
61
|
+
body: lowerFragments(node.body, filePath, expressions)
|
|
62
|
+
};
|
|
63
|
+
case 'text':
|
|
64
|
+
default:
|
|
65
|
+
return node;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Lower an expression node to a fragment if it returns JSX
|
|
70
|
+
*/
|
|
71
|
+
function lowerExpressionNode(node, filePath, expressions) {
|
|
72
|
+
const classification = classifyExpression(node.expression);
|
|
73
|
+
// Primitive expressions pass through unchanged
|
|
74
|
+
if (classification.type === 'primitive') {
|
|
75
|
+
return node;
|
|
76
|
+
}
|
|
77
|
+
// Unknown expressions with JSX are compile errors
|
|
78
|
+
if (classification.type === 'unknown') {
|
|
79
|
+
throw new InvariantError(INVARIANT.NON_ENUMERABLE_JSX, `JSX expression output cannot be statically determined: ${node.expression.slice(0, 50)}...`, 'JSX expressions must have statically enumerable output. The compiler must know all possible DOM shapes at compile time.', filePath, node.location.line, node.location.column);
|
|
80
|
+
}
|
|
81
|
+
// Lower based on classification type
|
|
82
|
+
switch (classification.type) {
|
|
83
|
+
case 'conditional':
|
|
84
|
+
return lowerConditionalExpression(node, classification.condition, classification.consequent, classification.alternate, filePath, expressions);
|
|
85
|
+
case 'optional':
|
|
86
|
+
return lowerOptionalExpression(node, classification.optionalCondition, classification.optionalFragment, filePath, expressions);
|
|
87
|
+
case 'loop':
|
|
88
|
+
return lowerLoopExpression(node, classification.loopSource, classification.loopItemVar, classification.loopIndexVar, classification.loopBody, filePath, expressions);
|
|
89
|
+
case 'fragment':
|
|
90
|
+
return lowerInlineFragment(node, classification.fragmentCode, filePath, expressions);
|
|
91
|
+
default:
|
|
92
|
+
// Should not reach here
|
|
93
|
+
return node;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Lower conditional expression: condition ? <A /> : <B />
|
|
98
|
+
*
|
|
99
|
+
* Both branches are parsed and compiled at compile time.
|
|
100
|
+
*/
|
|
101
|
+
function lowerConditionalExpression(node, condition, consequentCode, alternateCode, filePath, expressions) {
|
|
102
|
+
// Parse both branches as JSX fragments
|
|
103
|
+
const consequent = parseJSXToNodes(consequentCode, node.location, filePath, expressions, node.loopContext);
|
|
104
|
+
const alternate = parseJSXToNodes(alternateCode, node.location, filePath, expressions, node.loopContext);
|
|
105
|
+
return {
|
|
106
|
+
type: 'conditional-fragment',
|
|
107
|
+
condition,
|
|
108
|
+
consequent,
|
|
109
|
+
alternate,
|
|
110
|
+
location: node.location,
|
|
111
|
+
loopContext: node.loopContext
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Lower optional expression: condition && <A />
|
|
116
|
+
*
|
|
117
|
+
* Fragment is parsed and compiled at compile time.
|
|
118
|
+
*/
|
|
119
|
+
function lowerOptionalExpression(node, condition, fragmentCode, filePath, expressions) {
|
|
120
|
+
const fragment = parseJSXToNodes(fragmentCode, node.location, filePath, expressions, node.loopContext);
|
|
121
|
+
return {
|
|
122
|
+
type: 'optional-fragment',
|
|
123
|
+
condition,
|
|
124
|
+
fragment,
|
|
125
|
+
location: node.location,
|
|
126
|
+
loopContext: node.loopContext
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Lower loop expression: items.map(item => <li>...</li>)
|
|
131
|
+
*
|
|
132
|
+
* Body is parsed and compiled once, instantiated per item at runtime.
|
|
133
|
+
*/
|
|
134
|
+
function lowerLoopExpression(node, source, itemVar, indexVar, bodyCode, filePath, expressions) {
|
|
135
|
+
// Create loop context for the body
|
|
136
|
+
const loopVariables = [itemVar];
|
|
137
|
+
if (indexVar) {
|
|
138
|
+
loopVariables.push(indexVar);
|
|
139
|
+
}
|
|
140
|
+
const bodyLoopContext = {
|
|
141
|
+
variables: node.loopContext
|
|
142
|
+
? [...node.loopContext.variables, ...loopVariables]
|
|
143
|
+
: loopVariables,
|
|
144
|
+
mapSource: source
|
|
145
|
+
};
|
|
146
|
+
// Parse body with loop context
|
|
147
|
+
const body = parseJSXToNodes(bodyCode, node.location, filePath, expressions, bodyLoopContext);
|
|
148
|
+
return {
|
|
149
|
+
type: 'loop-fragment',
|
|
150
|
+
source,
|
|
151
|
+
itemVar,
|
|
152
|
+
indexVar,
|
|
153
|
+
body,
|
|
154
|
+
location: node.location,
|
|
155
|
+
loopContext: bodyLoopContext
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Lower inline fragment: <A /> or <><A /><B /></>
|
|
160
|
+
*
|
|
161
|
+
* JSX is parsed and inlined directly into the node tree.
|
|
162
|
+
* Returns the original expression node since inline JSX
|
|
163
|
+
* is already handled by the expression transformer.
|
|
164
|
+
*/
|
|
165
|
+
function lowerInlineFragment(node, fragmentCode, filePath, expressions) {
|
|
166
|
+
// For now, inline fragments are handled by the existing expression transformer
|
|
167
|
+
// which converts JSX to __zenith.h() calls
|
|
168
|
+
// In a future iteration, we could parse them to static nodes here
|
|
169
|
+
return node;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Parse JSX code string into TemplateNode[]
|
|
173
|
+
*
|
|
174
|
+
* This is a simplified parser for JSX fragments within expressions.
|
|
175
|
+
* It handles basic JSX structure for lowering purposes.
|
|
176
|
+
*/
|
|
177
|
+
function parseJSXToNodes(code, baseLocation, filePath, expressions, loopContext) {
|
|
178
|
+
const trimmed = code.trim();
|
|
179
|
+
// Handle fragment syntax <>...</>
|
|
180
|
+
if (trimmed.startsWith('<>')) {
|
|
181
|
+
const content = extractFragmentContent(trimmed);
|
|
182
|
+
return parseJSXChildren(content, baseLocation, filePath, expressions, loopContext);
|
|
183
|
+
}
|
|
184
|
+
// Handle single element
|
|
185
|
+
if (trimmed.startsWith('<')) {
|
|
186
|
+
const element = parseJSXElement(trimmed, baseLocation, filePath, expressions, loopContext);
|
|
187
|
+
return element ? [element] : [];
|
|
188
|
+
}
|
|
189
|
+
// Handle parenthesized expression
|
|
190
|
+
if (trimmed.startsWith('(')) {
|
|
191
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
192
|
+
return parseJSXToNodes(inner, baseLocation, filePath, expressions, loopContext);
|
|
193
|
+
}
|
|
194
|
+
// Not JSX - return as expression node
|
|
195
|
+
return [{
|
|
196
|
+
type: 'expression',
|
|
197
|
+
expression: trimmed,
|
|
198
|
+
location: baseLocation,
|
|
199
|
+
loopContext
|
|
200
|
+
}];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Extract content from fragment syntax <>content</>
|
|
204
|
+
*/
|
|
205
|
+
function extractFragmentContent(code) {
|
|
206
|
+
// Remove <> prefix and </> suffix
|
|
207
|
+
const withoutOpen = code.slice(2);
|
|
208
|
+
const closeIndex = withoutOpen.lastIndexOf('</>');
|
|
209
|
+
if (closeIndex === -1) {
|
|
210
|
+
return withoutOpen;
|
|
211
|
+
}
|
|
212
|
+
return withoutOpen.slice(0, closeIndex);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Parse JSX children content
|
|
216
|
+
*/
|
|
217
|
+
function parseJSXChildren(content, baseLocation, filePath, expressions, loopContext) {
|
|
218
|
+
const nodes = [];
|
|
219
|
+
let i = 0;
|
|
220
|
+
let currentText = '';
|
|
221
|
+
while (i < content.length) {
|
|
222
|
+
const char = content[i];
|
|
223
|
+
// Check for JSX element
|
|
224
|
+
if (char === '<' && /[a-zA-Z]/.test(content[i + 1] || '')) {
|
|
225
|
+
// Save accumulated text
|
|
226
|
+
if (currentText.trim()) {
|
|
227
|
+
nodes.push({
|
|
228
|
+
type: 'text',
|
|
229
|
+
value: currentText.trim(),
|
|
230
|
+
location: baseLocation
|
|
231
|
+
});
|
|
232
|
+
currentText = '';
|
|
233
|
+
}
|
|
234
|
+
// Parse element
|
|
235
|
+
const result = parseJSXElementWithEnd(content, i, baseLocation, filePath, expressions, loopContext);
|
|
236
|
+
if (result) {
|
|
237
|
+
nodes.push(result.node);
|
|
238
|
+
i = result.endIndex;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Check for expression {expr}
|
|
243
|
+
if (char === '{') {
|
|
244
|
+
const endBrace = findBalancedBraceEnd(content, i);
|
|
245
|
+
if (endBrace !== -1) {
|
|
246
|
+
// Save accumulated text
|
|
247
|
+
if (currentText.trim()) {
|
|
248
|
+
nodes.push({
|
|
249
|
+
type: 'text',
|
|
250
|
+
value: currentText.trim(),
|
|
251
|
+
location: baseLocation
|
|
252
|
+
});
|
|
253
|
+
currentText = '';
|
|
254
|
+
}
|
|
255
|
+
const exprCode = content.slice(i + 1, endBrace - 1).trim();
|
|
256
|
+
if (exprCode) {
|
|
257
|
+
nodes.push({
|
|
258
|
+
type: 'expression',
|
|
259
|
+
expression: exprCode,
|
|
260
|
+
location: baseLocation,
|
|
261
|
+
loopContext
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
i = endBrace;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
currentText += char;
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
// Add remaining text
|
|
272
|
+
if (currentText.trim()) {
|
|
273
|
+
nodes.push({
|
|
274
|
+
type: 'text',
|
|
275
|
+
value: currentText.trim(),
|
|
276
|
+
location: baseLocation
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return nodes;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Parse a single JSX element
|
|
283
|
+
*/
|
|
284
|
+
function parseJSXElement(code, baseLocation, filePath, expressions, loopContext) {
|
|
285
|
+
const result = parseJSXElementWithEnd(code, 0, baseLocation, filePath, expressions, loopContext);
|
|
286
|
+
return result ? result.node : null;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Parse JSX element and return end index
|
|
290
|
+
*/
|
|
291
|
+
function parseJSXElementWithEnd(code, startIndex, baseLocation, filePath, expressions, loopContext) {
|
|
292
|
+
// Extract tag name
|
|
293
|
+
const tagMatch = code.slice(startIndex).match(/^<([a-zA-Z][a-zA-Z0-9.]*)/);
|
|
294
|
+
if (!tagMatch)
|
|
295
|
+
return null;
|
|
296
|
+
const tagName = tagMatch[1];
|
|
297
|
+
let i = startIndex + tagMatch[0].length;
|
|
298
|
+
// Parse attributes (simplified)
|
|
299
|
+
const attributes = [];
|
|
300
|
+
// Skip whitespace and parse attributes until > or />
|
|
301
|
+
while (i < code.length) {
|
|
302
|
+
// Skip whitespace
|
|
303
|
+
while (i < code.length && /\s/.test(code[i]))
|
|
304
|
+
i++;
|
|
305
|
+
// Check for end of opening tag
|
|
306
|
+
if (code[i] === '>') {
|
|
307
|
+
i++;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
if (code[i] === '/' && code[i + 1] === '>') {
|
|
311
|
+
// Self-closing tag
|
|
312
|
+
const isComponent = tagName[0] === tagName[0].toUpperCase();
|
|
313
|
+
const node = isComponent ? {
|
|
314
|
+
type: 'component',
|
|
315
|
+
name: tagName,
|
|
316
|
+
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
317
|
+
children: [],
|
|
318
|
+
location: baseLocation,
|
|
319
|
+
loopContext
|
|
320
|
+
} : {
|
|
321
|
+
type: 'element',
|
|
322
|
+
tag: tagName.toLowerCase(),
|
|
323
|
+
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
324
|
+
children: [],
|
|
325
|
+
location: baseLocation,
|
|
326
|
+
loopContext
|
|
327
|
+
};
|
|
328
|
+
return { node, endIndex: i + 2 };
|
|
329
|
+
}
|
|
330
|
+
// Parse attribute name
|
|
331
|
+
const attrMatch = code.slice(i).match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
|
332
|
+
if (!attrMatch) {
|
|
333
|
+
i++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const attrName = attrMatch[1];
|
|
337
|
+
i += attrName.length;
|
|
338
|
+
// Skip whitespace
|
|
339
|
+
while (i < code.length && /\s/.test(code[i]))
|
|
340
|
+
i++;
|
|
341
|
+
// Check for value
|
|
342
|
+
if (code[i] !== '=') {
|
|
343
|
+
attributes.push({ name: attrName, value: 'true', location: baseLocation });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
i++; // Skip =
|
|
347
|
+
// Skip whitespace
|
|
348
|
+
while (i < code.length && /\s/.test(code[i]))
|
|
349
|
+
i++;
|
|
350
|
+
// Parse value
|
|
351
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
352
|
+
const quote = code[i];
|
|
353
|
+
let endQuote = i + 1;
|
|
354
|
+
while (endQuote < code.length && code[endQuote] !== quote) {
|
|
355
|
+
if (code[endQuote] === '\\')
|
|
356
|
+
endQuote++;
|
|
357
|
+
endQuote++;
|
|
358
|
+
}
|
|
359
|
+
attributes.push({ name: attrName, value: code.slice(i + 1, endQuote), location: baseLocation });
|
|
360
|
+
i = endQuote + 1;
|
|
361
|
+
}
|
|
362
|
+
else if (code[i] === '{') {
|
|
363
|
+
const endBrace = findBalancedBraceEnd(code, i);
|
|
364
|
+
if (endBrace !== -1) {
|
|
365
|
+
attributes.push({ name: attrName, value: code.slice(i, endBrace), location: baseLocation });
|
|
366
|
+
i = endBrace;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Parse children until closing tag
|
|
371
|
+
const closeTag = `</${tagName}>`;
|
|
372
|
+
const closeIndex = findClosingTag(code, i, tagName);
|
|
373
|
+
let children = [];
|
|
374
|
+
if (closeIndex !== -1 && closeIndex > i) {
|
|
375
|
+
const childContent = code.slice(i, closeIndex);
|
|
376
|
+
children = parseJSXChildren(childContent, baseLocation, filePath, expressions, loopContext);
|
|
377
|
+
i = closeIndex + closeTag.length;
|
|
378
|
+
}
|
|
379
|
+
const isComponent = tagName[0] === tagName[0].toUpperCase();
|
|
380
|
+
const node = isComponent ? {
|
|
381
|
+
type: 'component',
|
|
382
|
+
name: tagName,
|
|
383
|
+
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
384
|
+
children,
|
|
385
|
+
location: baseLocation,
|
|
386
|
+
loopContext
|
|
387
|
+
} : {
|
|
388
|
+
type: 'element',
|
|
389
|
+
tag: tagName.toLowerCase(),
|
|
390
|
+
attributes: attributes.map(a => ({ ...a, value: a.value })),
|
|
391
|
+
children,
|
|
392
|
+
location: baseLocation,
|
|
393
|
+
loopContext
|
|
394
|
+
};
|
|
395
|
+
return { node, endIndex: i };
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Find closing tag for an element
|
|
399
|
+
*/
|
|
400
|
+
function findClosingTag(code, startIndex, tagName) {
|
|
401
|
+
const closeTag = `</${tagName}>`;
|
|
402
|
+
let depth = 1;
|
|
403
|
+
let i = startIndex;
|
|
404
|
+
while (i < code.length && depth > 0) {
|
|
405
|
+
// Check for closing tag
|
|
406
|
+
if (code.slice(i, i + closeTag.length) === closeTag) {
|
|
407
|
+
depth--;
|
|
408
|
+
if (depth === 0)
|
|
409
|
+
return i;
|
|
410
|
+
i += closeTag.length;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
// Check for opening tag (same name, nested)
|
|
414
|
+
const openPattern = new RegExp(`^<${tagName}(?:\\s|>|/>)`);
|
|
415
|
+
const match = code.slice(i).match(openPattern);
|
|
416
|
+
if (match) {
|
|
417
|
+
// Check if self-closing
|
|
418
|
+
const selfClosing = code.slice(i).match(new RegExp(`^<${tagName}[^>]*/>`));
|
|
419
|
+
if (!selfClosing) {
|
|
420
|
+
depth++;
|
|
421
|
+
}
|
|
422
|
+
i += match[0].length;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
i++;
|
|
426
|
+
}
|
|
427
|
+
return -1;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Find balanced brace end
|
|
431
|
+
*/
|
|
432
|
+
function findBalancedBraceEnd(code, startIndex) {
|
|
433
|
+
if (code[startIndex] !== '{')
|
|
434
|
+
return -1;
|
|
435
|
+
let depth = 1;
|
|
436
|
+
let i = startIndex + 1;
|
|
437
|
+
let inString = false;
|
|
438
|
+
let stringChar = '';
|
|
439
|
+
while (i < code.length && depth > 0) {
|
|
440
|
+
const char = code[i];
|
|
441
|
+
const prevChar = code[i - 1];
|
|
442
|
+
// Handle escape
|
|
443
|
+
if (prevChar === '\\') {
|
|
444
|
+
i++;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
// Handle strings
|
|
448
|
+
if (!inString && (char === '"' || char === "'")) {
|
|
449
|
+
inString = true;
|
|
450
|
+
stringChar = char;
|
|
451
|
+
i++;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (inString && char === stringChar) {
|
|
455
|
+
inString = false;
|
|
456
|
+
i++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!inString) {
|
|
460
|
+
if (char === '{')
|
|
461
|
+
depth++;
|
|
462
|
+
else if (char === '}')
|
|
463
|
+
depth--;
|
|
464
|
+
}
|
|
465
|
+
i++;
|
|
466
|
+
}
|
|
467
|
+
return depth === 0 ? i : -1;
|
|
468
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
let native;
|
|
2
|
+
try {
|
|
3
|
+
try {
|
|
4
|
+
native = require('../../native/compiler-native');
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
native = require('../../native/compiler-native/index.js');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
// Bridge load handled elsewhere
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Process a page by inlining a layout
|
|
15
|
+
*/
|
|
16
|
+
export function processLayout(source, layout, props = {}) {
|
|
17
|
+
if (native && native.process_layout_native) {
|
|
18
|
+
try {
|
|
19
|
+
// Convert Map to record for native serialization
|
|
20
|
+
const layoutForNative = {
|
|
21
|
+
name: layout.name,
|
|
22
|
+
html: layout.html,
|
|
23
|
+
scripts: layout.scripts,
|
|
24
|
+
styles: layout.styles
|
|
25
|
+
};
|
|
26
|
+
return native.process_layout_native(source, JSON.stringify(layoutForNative), JSON.stringify(props));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.warn(`[Zenith Native] Layout processing failed: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Fallback: This should ideally not be reached if native is available
|
|
33
|
+
return source;
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Template IR to Compiled Template
|
|
3
|
+
*
|
|
4
|
+
* Phase 2: Transform IR → Static HTML + Runtime Bindings
|
|
5
|
+
*/
|
|
6
|
+
import type { ZenIR } from '../ir/types';
|
|
7
|
+
import type { CompiledTemplate } from '../output/types';
|
|
8
|
+
/**
|
|
9
|
+
* Transform a ZenIR into CompiledTemplate
|
|
10
|
+
*/
|
|
11
|
+
export declare function transformTemplate(ir: ZenIR): CompiledTemplate;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Template IR to Compiled Template
|
|
3
|
+
*
|
|
4
|
+
* Phase 2: Transform IR → Static HTML + Runtime Bindings
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Transform a ZenIR into CompiledTemplate
|
|
8
|
+
*/
|
|
9
|
+
export function transformTemplate(ir) {
|
|
10
|
+
let native;
|
|
11
|
+
try {
|
|
12
|
+
try {
|
|
13
|
+
native = require('../../native/compiler-native');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
native = require('../../native/compiler-native/index.js');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
// Bridge load handled elsewhere
|
|
21
|
+
}
|
|
22
|
+
if (native && native.transformTemplateNative) {
|
|
23
|
+
const { html, bindings } = native.transformTemplateNative(JSON.stringify(ir.template.nodes), JSON.stringify(ir.template.expressions));
|
|
24
|
+
return {
|
|
25
|
+
html,
|
|
26
|
+
bindings,
|
|
27
|
+
scripts: ir.script ? ir.script.raw : null,
|
|
28
|
+
styles: ir.styles.map(s => s.raw)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Fallback to legacy if bridge unavailable (though we aim for full native)
|
|
32
|
+
throw new Error('[Zenith Native] Transformation bridge unavailable');
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant Validation
|
|
3
|
+
*
|
|
4
|
+
* NATIVE BRIDGE: All validation logic exists in Rust (`native/compiler-native/src/validate.rs`).
|
|
5
|
+
* The NAPI function `validateIr(irJson)` is the sole semantic authority.
|
|
6
|
+
*/
|
|
7
|
+
import type { ZenIR } from '../ir/types';
|
|
8
|
+
/**
|
|
9
|
+
* Native bridge for IR validation
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateIr(ir: ZenIR): void;
|
|
12
|
+
export declare const INVARIANT: {
|
|
13
|
+
readonly LOOP_CONTEXT_LOST: "INV001";
|
|
14
|
+
readonly ATTRIBUTE_NOT_FORWARDED: "INV002";
|
|
15
|
+
readonly UNRESOLVED_COMPONENT: "INV003";
|
|
16
|
+
readonly REACTIVE_BOUNDARY: "INV004";
|
|
17
|
+
readonly TEMPLATE_TAG: "INV005";
|
|
18
|
+
readonly SLOT_ATTRIBUTE: "INV006";
|
|
19
|
+
readonly ORPHAN_COMPOUND: "INV007";
|
|
20
|
+
readonly NON_ENUMERABLE_JSX: "INV008";
|
|
21
|
+
readonly UNREGISTERED_EXPRESSION: "INV009";
|
|
22
|
+
readonly COMPONENT_PRECOMPILED: "INV010";
|
|
23
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant Validation
|
|
3
|
+
*
|
|
4
|
+
* NATIVE BRIDGE: All validation logic exists in Rust (`native/compiler-native/src/validate.rs`).
|
|
5
|
+
* The NAPI function `validateIr(irJson)` is the sole semantic authority.
|
|
6
|
+
*/
|
|
7
|
+
import { InvariantError } from '../errors/compilerError';
|
|
8
|
+
let native;
|
|
9
|
+
try {
|
|
10
|
+
try {
|
|
11
|
+
native = require('../../native/compiler-native');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
native = require('../../native/compiler-native/index.js');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
// Bridge load handled elsewhere
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Native bridge for IR validation
|
|
22
|
+
*/
|
|
23
|
+
export function validateIr(ir) {
|
|
24
|
+
if (native && native.validateIr) {
|
|
25
|
+
const validationIR = {
|
|
26
|
+
filePath: ir.filePath,
|
|
27
|
+
template: {
|
|
28
|
+
raw: ir.template.raw,
|
|
29
|
+
nodes: ir.template.nodes,
|
|
30
|
+
expressions: ir.template.expressions,
|
|
31
|
+
},
|
|
32
|
+
styles: ir.styles,
|
|
33
|
+
script: ir.script,
|
|
34
|
+
};
|
|
35
|
+
const error = native.validateIr(JSON.stringify(validationIR));
|
|
36
|
+
if (error) {
|
|
37
|
+
throw new InvariantError(error.code, error.message, error.guarantee, error.file, error.line, error.column, error.context, error.hints);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw new Error('[Zenith Native] Validation bridge unavailable');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const INVARIANT = {
|
|
45
|
+
LOOP_CONTEXT_LOST: 'INV001',
|
|
46
|
+
ATTRIBUTE_NOT_FORWARDED: 'INV002',
|
|
47
|
+
UNRESOLVED_COMPONENT: 'INV003',
|
|
48
|
+
REACTIVE_BOUNDARY: 'INV004',
|
|
49
|
+
TEMPLATE_TAG: 'INV005',
|
|
50
|
+
SLOT_ATTRIBUTE: 'INV006',
|
|
51
|
+
ORPHAN_COMPOUND: 'INV007',
|
|
52
|
+
NON_ENUMERABLE_JSX: 'INV008',
|
|
53
|
+
UNREGISTERED_EXPRESSION: 'INV009',
|
|
54
|
+
COMPONENT_PRECOMPILED: 'INV010',
|
|
55
|
+
};
|
|
Binary file
|