@zenithbuild/core 0.1.0

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.
Files changed (101) hide show
  1. package/.eslintignore +15 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
  4. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
  5. package/.github/pull_request_template.md +15 -0
  6. package/.github/workflows/discord-changelog.yml +141 -0
  7. package/.github/workflows/discord-notify.yml +242 -0
  8. package/.github/workflows/discord-version.yml +195 -0
  9. package/.prettierignore +13 -0
  10. package/.prettierrc +21 -0
  11. package/.zen.d.ts +15 -0
  12. package/LICENSE +21 -0
  13. package/README.md +55 -0
  14. package/app/components/Button.zen +46 -0
  15. package/app/components/Link.zen +11 -0
  16. package/app/favicon.ico +0 -0
  17. package/app/layouts/Main.zen +59 -0
  18. package/app/pages/about.zen +23 -0
  19. package/app/pages/blog/[id].zen +53 -0
  20. package/app/pages/blog/index.zen +32 -0
  21. package/app/pages/dynamic-dx.zen +712 -0
  22. package/app/pages/dynamic-primitives.zen +453 -0
  23. package/app/pages/index.zen +154 -0
  24. package/app/pages/navigation-demo.zen +229 -0
  25. package/app/pages/posts/[...slug].zen +61 -0
  26. package/app/pages/primitives-demo.zen +273 -0
  27. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  28. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  29. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  30. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  31. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
  32. package/assets/logos/README.md +54 -0
  33. package/assets/logos/zen.icns +0 -0
  34. package/bun.lock +39 -0
  35. package/compiler/README.md +380 -0
  36. package/compiler/errors/compilerError.ts +24 -0
  37. package/compiler/finalize/finalizeOutput.ts +163 -0
  38. package/compiler/finalize/generateFinalBundle.ts +82 -0
  39. package/compiler/index.ts +44 -0
  40. package/compiler/ir/types.ts +83 -0
  41. package/compiler/legacy/binding.ts +254 -0
  42. package/compiler/legacy/bindings.ts +338 -0
  43. package/compiler/legacy/component-process.ts +1208 -0
  44. package/compiler/legacy/component.ts +301 -0
  45. package/compiler/legacy/event.ts +50 -0
  46. package/compiler/legacy/expression.ts +1149 -0
  47. package/compiler/legacy/mutation.ts +280 -0
  48. package/compiler/legacy/parse.ts +299 -0
  49. package/compiler/legacy/split.ts +608 -0
  50. package/compiler/legacy/types.ts +32 -0
  51. package/compiler/output/types.ts +34 -0
  52. package/compiler/parse/detectMapExpressions.ts +102 -0
  53. package/compiler/parse/parseScript.ts +22 -0
  54. package/compiler/parse/parseTemplate.ts +425 -0
  55. package/compiler/parse/parseZenFile.ts +66 -0
  56. package/compiler/parse/trackLoopContext.ts +82 -0
  57. package/compiler/runtime/dataExposure.ts +291 -0
  58. package/compiler/runtime/generateDOM.ts +144 -0
  59. package/compiler/runtime/generateHydrationBundle.ts +383 -0
  60. package/compiler/runtime/hydration.ts +309 -0
  61. package/compiler/runtime/navigation.ts +432 -0
  62. package/compiler/runtime/thinRuntime.ts +160 -0
  63. package/compiler/runtime/transformIR.ts +256 -0
  64. package/compiler/runtime/wrapExpression.ts +84 -0
  65. package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
  66. package/compiler/spa-build.ts +1000 -0
  67. package/compiler/test/validate-test.ts +104 -0
  68. package/compiler/transform/generateBindings.ts +47 -0
  69. package/compiler/transform/generateHTML.ts +28 -0
  70. package/compiler/transform/transformNode.ts +126 -0
  71. package/compiler/transform/transformTemplate.ts +38 -0
  72. package/compiler/validate/validateExpressions.ts +168 -0
  73. package/core/index.ts +135 -0
  74. package/core/lifecycle/index.ts +49 -0
  75. package/core/lifecycle/zen-mount.ts +182 -0
  76. package/core/lifecycle/zen-unmount.ts +88 -0
  77. package/core/reactivity/index.ts +54 -0
  78. package/core/reactivity/tracking.ts +167 -0
  79. package/core/reactivity/zen-batch.ts +57 -0
  80. package/core/reactivity/zen-effect.ts +139 -0
  81. package/core/reactivity/zen-memo.ts +146 -0
  82. package/core/reactivity/zen-ref.ts +52 -0
  83. package/core/reactivity/zen-signal.ts +121 -0
  84. package/core/reactivity/zen-state.ts +180 -0
  85. package/core/reactivity/zen-untrack.ts +44 -0
  86. package/docs/COMMENTS.md +111 -0
  87. package/docs/COMMITS.md +36 -0
  88. package/docs/CONTRIBUTING.md +116 -0
  89. package/docs/STYLEGUIDE.md +62 -0
  90. package/package.json +44 -0
  91. package/router/index.ts +76 -0
  92. package/router/manifest.ts +314 -0
  93. package/router/navigation/ZenLink.zen +231 -0
  94. package/router/navigation/index.ts +78 -0
  95. package/router/navigation/zen-link.ts +584 -0
  96. package/router/runtime.ts +458 -0
  97. package/router/types.ts +168 -0
  98. package/runtime/build.ts +17 -0
  99. package/runtime/serve.ts +93 -0
  100. package/scripts/webhook-proxy.ts +213 -0
  101. package/tsconfig.json +28 -0
@@ -0,0 +1,608 @@
1
+ import type { ZenFile, StateBinding, ScriptBlock } from "./types"
2
+ import * as parse5 from "parse5"
3
+ import { extractStateDeclarations, extractStateDeclarationsWithLocation, transformStateDeclarations, type StateDeclarationInfo } from "./parse"
4
+ import { extractEventHandlers, detectStateMutations, validateStateMutations, type InlineHandlerInfo } from "./mutation"
5
+ import {
6
+ extractExpressionBlocks,
7
+ transformExpressionBlocks,
8
+ generateExpressionRuntime,
9
+ generateExpressionRuntimeHelpers,
10
+ extractAttributeExpressions,
11
+ generateAttributeExpressionRuntime,
12
+ type ExpressionBlock,
13
+ type AttributeExpressionBinding
14
+ } from "./expression"
15
+
16
+ // ============================================
17
+ // ES6 Import Transform & Auto-Import System
18
+ // ============================================
19
+
20
+ /**
21
+ * Zen primitives that can be auto-imported
22
+ * Maps the function name to its internal implementation
23
+ */
24
+ const ZEN_AUTO_IMPORTS = {
25
+ // Reactivity primitives
26
+ zenSignal: '__zenith.signal',
27
+ zenState: '__zenith.state',
28
+ zenEffect: '__zenith.effect',
29
+ zenMemo: '__zenith.memo',
30
+ zenRef: '__zenith.ref',
31
+ zenBatch: '__zenith.batch',
32
+ zenUntrack: '__zenith.untrack',
33
+ // Clean aliases
34
+ signal: '__zenith.signal',
35
+ state: '__zenith.state',
36
+ effect: '__zenith.effect',
37
+ memo: '__zenith.memo',
38
+ ref: '__zenith.ref',
39
+ batch: '__zenith.batch',
40
+ untrack: '__zenith.untrack',
41
+ // Lifecycle hooks
42
+ zenOnMount: '__zenith.onMount',
43
+ zenOnUnmount: '__zenith.onUnmount',
44
+ onMount: '__zenith.onMount',
45
+ onUnmount: '__zenith.onUnmount',
46
+ // Navigation (from router)
47
+ navigate: '__zenith_router.navigate',
48
+ getRoute: '__zenith_router.getRoute',
49
+ isActive: '__zenith_router.isActive',
50
+ prefetch: '__zenith_router.prefetch',
51
+ } as const;
52
+
53
+ /**
54
+ * Parse and transform ES6 imports in script content
55
+ * Converts imports to window/global references
56
+ */
57
+ function transformImports(script: string): {
58
+ transformed: string;
59
+ imports: Array<{ from: string; names: string[] }>;
60
+ } {
61
+ const imports: Array<{ from: string; names: string[] }> = [];
62
+
63
+ // Match ES6 import statements
64
+ // import { a, b, c } from 'module'
65
+ // import { a as b } from 'module'
66
+ // import defaultExport from 'module'
67
+ // import * as name from 'module'
68
+ const importRegex = /import\s+(?:(\{[^}]+\})|(\*\s+as\s+\w+)|(\w+))\s+from\s+['"]([^'"]+)['"]\s*;?/g;
69
+
70
+ let transformed = script;
71
+ let match;
72
+
73
+ while ((match = importRegex.exec(script)) !== null) {
74
+ const namedImports = match[1]; // { a, b, c }
75
+ const namespaceImport = match[2]; // * as name
76
+ const defaultImport = match[3]; // defaultExport
77
+ const modulePath = match[4]; // 'module'
78
+
79
+ const importedNames: string[] = [];
80
+
81
+ if (namedImports) {
82
+ // Parse { a, b as c, d }
83
+ const names = namedImports
84
+ .replace(/[{}]/g, '')
85
+ .split(',')
86
+ .map(n => n.trim())
87
+ .filter(n => n);
88
+
89
+ for (const name of names) {
90
+ // Handle 'a as b' syntax
91
+ const asMatch = name.match(/(\w+)\s+as\s+(\w+)/);
92
+ if (asMatch) {
93
+ importedNames.push(asMatch[2]); // Use the alias
94
+ } else {
95
+ importedNames.push(name);
96
+ }
97
+ }
98
+ }
99
+
100
+ if (defaultImport) {
101
+ importedNames.push(defaultImport);
102
+ }
103
+
104
+ if (namespaceImport) {
105
+ const nsMatch = namespaceImport.match(/\*\s+as\s+(\w+)/);
106
+ if (nsMatch) {
107
+ importedNames.push(nsMatch[1]);
108
+ }
109
+ }
110
+
111
+ imports.push({ from: modulePath, names: importedNames });
112
+ }
113
+
114
+ // Remove import statements from the script
115
+ transformed = transformed.replace(importRegex, '');
116
+
117
+ // Clean up any leftover empty lines from removed imports
118
+ transformed = transformed.replace(/^\s*\n{2,}/gm, '\n');
119
+
120
+ return { transformed, imports };
121
+ }
122
+
123
+ /**
124
+ * Detect zen primitives used in script and generate auto-import declarations
125
+ */
126
+ function generateAutoImports(script: string, existingImports: string[]): string {
127
+ const autoImportDeclarations: string[] = [];
128
+
129
+ for (const [name, globalRef] of Object.entries(ZEN_AUTO_IMPORTS)) {
130
+ // Skip if already imported
131
+ if (existingImports.includes(name)) continue;
132
+
133
+ // Check if the primitive is used in the script (as a word boundary)
134
+ const usageRegex = new RegExp(`\\b${name}\\s*\\(`, 'g');
135
+ if (usageRegex.test(script)) {
136
+ autoImportDeclarations.push(`const ${name} = window.${globalRef};`);
137
+ }
138
+ }
139
+
140
+ if (autoImportDeclarations.length > 0) {
141
+ return `// Auto-imported zen primitives\n${autoImportDeclarations.join('\n')}\n\n`;
142
+ }
143
+
144
+ return '';
145
+ }
146
+
147
+ /**
148
+ * Process script content: transform imports and add auto-imports
149
+ */
150
+ export function processScriptImports(script: string): string {
151
+ // First, transform ES6 imports
152
+ const { transformed, imports } = transformImports(script);
153
+
154
+ // Collect all imported names
155
+ const importedNames = imports.flatMap(i => i.names);
156
+
157
+ // Generate auto-imports for zen primitives
158
+ const autoImports = generateAutoImports(transformed, importedNames);
159
+
160
+ // Combine: auto-imports first, then the transformed script
161
+ return autoImports + transformed;
162
+ }
163
+
164
+ // Transform on* attributes to data-zen-* attributes during compilation
165
+ // Also converts inline arrow functions to function names
166
+ // Returns: { transformedHtml, eventTypes, inlineHandlerMap }
167
+ // where inlineHandlerMap maps original arrow function code to generated function names
168
+ function transformEventAttributes(
169
+ html: string,
170
+ inlineHandlerMap: Map<string, string> // maps arrow function code -> generated function name
171
+ ): { transformedHtml: string; eventTypes: Set<string> } {
172
+ const document = parse5.parse(html);
173
+ const eventTypes = new Set<string>();
174
+
175
+ function walk(node: any) {
176
+ // Transform attributes on element nodes
177
+ if (node.attrs && Array.isArray(node.attrs)) {
178
+ node.attrs = node.attrs.map((attr: any) => {
179
+ const attrName = attr.name.toLowerCase();
180
+ // Check if attribute starts with "on" (event handler)
181
+ if (attrName.startsWith('on') && attrName.length > 2) {
182
+ // Convert onclick -> data-zen-click, onchange -> data-zen-change, etc.
183
+ const eventType = attrName.slice(2); // Remove "on" prefix
184
+ eventTypes.add(eventType); // Track which event types are used
185
+
186
+ const handlerValue = attr.value;
187
+ // Check if it's an inline arrow function
188
+ const arrowFunctionMatch = handlerValue.match(/^\s*\([^)]*\)\s*=>\s*(.+)$/);
189
+
190
+ if (arrowFunctionMatch) {
191
+ // It's an inline arrow function - look up the generated function name
192
+ const arrowBody = arrowFunctionMatch[1].trim();
193
+ const generatedName = inlineHandlerMap.get(arrowBody);
194
+ if (generatedName) {
195
+ return {
196
+ name: `data-zen-${eventType}`,
197
+ value: generatedName
198
+ };
199
+ }
200
+ }
201
+
202
+ return {
203
+ name: `data-zen-${eventType}`,
204
+ value: handlerValue
205
+ };
206
+ }
207
+ return attr;
208
+ });
209
+ }
210
+
211
+ // Recursively process child nodes
212
+ if (node.childNodes) {
213
+ node.childNodes.forEach(walk);
214
+ }
215
+ }
216
+
217
+ walk(document);
218
+
219
+ // Serialize back to HTML string
220
+ return {
221
+ transformedHtml: parse5.serialize(document),
222
+ eventTypes
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Transform text bindings { stateName } in text nodes to <span data-zen-bind="stateName"></span>
228
+ * Returns transformed HTML and a map of state bindings
229
+ */
230
+ function transformTextBindings(
231
+ html: string,
232
+ declaredStates: Set<string>
233
+ ): { transformedHtml: string; stateBindings: Map<string, StateBinding> } {
234
+ const document = parse5.parse(html);
235
+ const stateBindings = new Map<string, StateBinding>();
236
+ let bindingIndex = 0;
237
+
238
+ function walk(node: any) {
239
+ // Skip script and style nodes - their content is handled separately
240
+ if (node.nodeName === 'script' || node.nodeName === 'style') {
241
+ return;
242
+ }
243
+
244
+ if (!node.childNodes || !Array.isArray(node.childNodes)) {
245
+ return;
246
+ }
247
+
248
+ const newChildNodes: any[] = [];
249
+
250
+ for (const child of node.childNodes) {
251
+ if (child.nodeName === '#text' && child.value) {
252
+ const text = child.value;
253
+ // Match { identifier } pattern - only simple identifiers, no expressions
254
+ const bindingRegex = /\{\s*(\w+)\s*\}/g;
255
+ const matches = Array.from(text.matchAll(bindingRegex));
256
+
257
+ if (matches.length === 0) {
258
+ // No bindings, keep the text node as-is
259
+ newChildNodes.push(child);
260
+ } else {
261
+ // Validate all bindings reference declared states
262
+ // Skip validation for instance-scoped state names (__zen_comp_*_stateName)
263
+ // These are component states that have already been transformed during inlining
264
+ for (const match of matches) {
265
+ const m = match as RegExpMatchArray;
266
+ const stateName = m[1];
267
+ if (!stateName) continue;
268
+
269
+ // Skip validation for instance-scoped component state names
270
+ // These are generated during component inlining (e.g., __zen_comp_0_clicks)
271
+ if (stateName.startsWith('__zen_')) {
272
+ continue;
273
+ }
274
+
275
+ if (!declaredStates.has(stateName)) {
276
+ throw new Error(
277
+ `Compiler Error: Binding "{ ${stateName} }" references undeclared state. ` +
278
+ `Declared states: ${Array.from(declaredStates).join(', ') || '(none)'}`
279
+ );
280
+ }
281
+ }
282
+
283
+ // Split text by bindings and create nodes
284
+ let lastIndex = 0;
285
+
286
+ for (const match of matches) {
287
+ const m = match as RegExpMatchArray;
288
+ const stateName = m[1];
289
+ if (!stateName) continue; // Skip if state name is missing (shouldn't happen)
290
+
291
+ const matchStart = m.index!;
292
+ const matchEnd = matchStart + m[0].length;
293
+
294
+ // Add text before the binding
295
+ if (matchStart > lastIndex) {
296
+ const beforeText = text.substring(lastIndex, matchStart);
297
+ if (beforeText) {
298
+ newChildNodes.push({
299
+ nodeName: '#text',
300
+ value: beforeText,
301
+ parentNode: node
302
+ });
303
+ }
304
+ }
305
+
306
+ // Create span element for binding
307
+ const bindId = `bind-${bindingIndex++}`;
308
+ const spanNode = {
309
+ nodeName: 'span',
310
+ tagName: 'span',
311
+ attrs: [
312
+ {
313
+ name: 'data-zen-bind',
314
+ value: stateName
315
+ },
316
+ {
317
+ name: 'data-zen-bind-id',
318
+ value: bindId
319
+ }
320
+ ],
321
+ childNodes: [],
322
+ parentNode: node
323
+ };
324
+ newChildNodes.push(spanNode);
325
+
326
+ // Track this binding
327
+ if (!stateBindings.has(stateName)) {
328
+ stateBindings.set(stateName, {
329
+ stateName,
330
+ bindings: []
331
+ });
332
+ }
333
+ const binding = stateBindings.get(stateName)!;
334
+ binding.bindings.push({
335
+ stateName,
336
+ nodeIndex: bindingIndex - 1
337
+ });
338
+
339
+ lastIndex = matchEnd;
340
+ }
341
+
342
+ // Add remaining text after last binding
343
+ if (lastIndex < text.length) {
344
+ const afterText = text.substring(lastIndex);
345
+ if (afterText) {
346
+ newChildNodes.push({
347
+ nodeName: '#text',
348
+ value: afterText,
349
+ parentNode: node
350
+ });
351
+ }
352
+ }
353
+ }
354
+ } else {
355
+ // Not a text node, recurse into it
356
+ walk(child);
357
+ newChildNodes.push(child);
358
+ }
359
+ }
360
+
361
+ node.childNodes = newChildNodes;
362
+ }
363
+
364
+ walk(document);
365
+
366
+ return {
367
+ transformedHtml: parse5.serialize(document),
368
+ stateBindings
369
+ };
370
+ }
371
+
372
+ // Transform :class and :value attributes to data-zen-* attributes
373
+ // Returns: { transformedHtml, bindings } where bindings contains attribute binding info
374
+ function transformAttributeBindings(html: string, scripts: ScriptBlock[]): { transformedHtml: string; bindings: Array<{ type: 'class' | 'value'; expression: string }> } {
375
+ const document = parse5.parse(html);
376
+ const bindings: Array<{ type: 'class' | 'value'; expression: string }> = [];
377
+
378
+ function walk(node: any) {
379
+ // Transform attributes on element nodes
380
+ if (node.attrs && Array.isArray(node.attrs)) {
381
+ node.attrs = node.attrs.map((attr: any) => {
382
+ const attrName = attr.name;
383
+ // Check if attribute is :class or :value (colon-prefixed)
384
+ if (attrName === ':class' || attrName === ':value') {
385
+ const bindingType = attrName.slice(1) as 'class' | 'value'; // Remove ":" prefix
386
+ const expression = attr.value.trim(); // Store the quoted expression
387
+
388
+ // Track this binding
389
+ bindings.push({ type: bindingType, expression });
390
+
391
+ // Transform to data-zen-* attribute
392
+ return {
393
+ name: `data-zen-${bindingType}`,
394
+ value: expression // Store the expression string
395
+ };
396
+ }
397
+ return attr;
398
+ });
399
+ }
400
+
401
+ // Recursively process child nodes
402
+ if (node.childNodes) {
403
+ node.childNodes.forEach(walk);
404
+ }
405
+ }
406
+
407
+ walk(document);
408
+
409
+ // NOTE: Component state transformation is now handled in component-process.ts
410
+ // via transformStateInHtml, which only transforms component INTERNAL state.
411
+ // Props are direct bindings and should NOT be transformed here.
412
+ // This function is kept for backward compatibility but does nothing now.
413
+
414
+ // Serialize back to HTML string
415
+ return {
416
+ transformedHtml: parse5.serialize(document),
417
+ bindings
418
+ };
419
+ }
420
+
421
+ // Strip script and style tags from HTML since they're extracted to separate files
422
+ function stripScriptAndStyleTags(html: string): string {
423
+ // Remove script tags (including content)
424
+ html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
425
+ // Remove style tags (including content)
426
+ html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
427
+ return html;
428
+ }
429
+
430
+ /**
431
+ * Compiler error for state redeclaration
432
+ */
433
+ export class StateRedeclarationError extends Error {
434
+ constructor(
435
+ public stateName: string,
436
+ public firstDeclaration: StateDeclarationInfo,
437
+ public secondDeclaration: StateDeclarationInfo
438
+ ) {
439
+ const firstLoc = `script ${firstDeclaration.scriptIndex + 1}, line ${firstDeclaration.line}, column ${firstDeclaration.column}`;
440
+ const secondLoc = `script ${secondDeclaration.scriptIndex + 1}, line ${secondDeclaration.line}, column ${secondDeclaration.column}`;
441
+ super(
442
+ `Compiler Error: State variable "${stateName}" is declared multiple times.\n` +
443
+ ` First declaration: ${firstLoc}\n` +
444
+ ` Second declaration: ${secondLoc}\n` +
445
+ ` State variables must be declared exactly once.`
446
+ );
447
+ this.name = 'StateRedeclarationError';
448
+ }
449
+ }
450
+
451
+ // this function splits the props into what we are compiling the them down too
452
+ // html styles and scripts
453
+ export function splitZen(file: ZenFile) {
454
+ // Extract state declarations from all scripts with location information
455
+ const allDeclarations: StateDeclarationInfo[] = [];
456
+ for (let i = 0; i < file.scripts.length; i++) {
457
+ const script = file.scripts[i];
458
+ if (script) {
459
+ const declarations = extractStateDeclarationsWithLocation(script.content, i);
460
+ allDeclarations.push(...declarations);
461
+ }
462
+ }
463
+
464
+ // Check for redeclarations and throw compile-time error
465
+ const declaredStates = new Map<string, StateDeclarationInfo>();
466
+ for (const declaration of allDeclarations) {
467
+ const existing = declaredStates.get(declaration.name);
468
+ if (existing) {
469
+ throw new StateRedeclarationError(declaration.name, existing, declaration);
470
+ }
471
+ declaredStates.set(declaration.name, declaration);
472
+ }
473
+
474
+ // Convert to Map<string, string> for backward compatibility
475
+ const stateMap = new Map<string, string>();
476
+ declaredStates.forEach((info, name) => {
477
+ stateMap.set(name, info.value);
478
+ });
479
+
480
+ // Extract event handlers from HTML (including inline arrow functions)
481
+ const { eventHandlers, inlineHandlers } = extractEventHandlers(file.html);
482
+
483
+ // Also add function props to event handlers (functions passed as props can mutate state)
484
+ if (file.functionProps) {
485
+ file.functionProps.forEach(funcName => eventHandlers.add(funcName));
486
+ }
487
+
488
+ // Generate function code for inline handlers and create a map
489
+ // Map: arrow function body -> generated function name
490
+ const inlineHandlerMap = new Map<string, string>(); // arrow body -> function name
491
+ const inlineHandlerFunctions: string[] = []; // generated function code to inject
492
+
493
+ inlineHandlers.forEach((handlerInfo, generatedName) => {
494
+ inlineHandlerMap.set(handlerInfo.body, generatedName);
495
+
496
+ // Replace parameter references in the body (e.g., "e" -> "event")
497
+ let body = handlerInfo.body;
498
+ if (handlerInfo.paramName && handlerInfo.paramName !== 'event' && handlerInfo.paramName !== '') {
499
+ // Replace the parameter name with "event" in the body
500
+ // Use word boundaries to avoid partial matches
501
+ const paramRegex = new RegExp(`\\b${handlerInfo.paramName}\\b`, 'g');
502
+ body = body.replace(paramRegex, 'event');
503
+ }
504
+
505
+ // Generate function code: function name(event, el) { body }
506
+ const functionCode = `function ${generatedName}(event, el) { ${body} }`;
507
+ inlineHandlerFunctions.push(functionCode);
508
+ });
509
+
510
+ // Inject inline handler functions into scripts (before state declaration removal)
511
+ const scriptsWithInlineHandlers = file.scripts.map((script, index) => {
512
+ if (inlineHandlerFunctions.length > 0 && index === 0) {
513
+ // Inject inline handlers at the start of the first script
514
+ return {
515
+ ...script,
516
+ content: inlineHandlerFunctions.join('\n\n') + '\n\n' + script.content
517
+ };
518
+ }
519
+ return script;
520
+ });
521
+
522
+ // Detect and validate state mutations
523
+ for (let i = 0; i < scriptsWithInlineHandlers.length; i++) {
524
+ const script = scriptsWithInlineHandlers[i];
525
+ if (!script) continue;
526
+ const mutations = detectStateMutations(script.content, new Set(stateMap.keys()));
527
+ validateStateMutations(mutations, eventHandlers, i);
528
+ }
529
+
530
+ // Transform event attributes (converting inline arrow functions to function names)
531
+ const { transformedHtml: htmlAfterEvents, eventTypes } = transformEventAttributes(file.html, inlineHandlerMap);
532
+
533
+ // Then transform attribute bindings (:class, :value)
534
+ const { transformedHtml: htmlAfterAttributeBindings, bindings } = transformAttributeBindings(htmlAfterEvents, file.scripts);
535
+
536
+ // Extract and transform attribute expressions (attr={expression})
537
+ const { transformedHtml: htmlAfterAttrExpressions, bindings: attrExprBindings } = extractAttributeExpressions(
538
+ htmlAfterAttributeBindings,
539
+ new Set(stateMap.keys())
540
+ );
541
+
542
+ // Extract and transform dynamic expression blocks ({ condition && <el> }, { a ? b : c }, { arr.map(...) })
543
+ const expressionBlocks = extractExpressionBlocks(htmlAfterAttrExpressions, new Set(stateMap.keys()));
544
+ const htmlAfterExpressions = transformExpressionBlocks(htmlAfterAttrExpressions, expressionBlocks);
545
+
546
+ // Then transform text bindings (this will validate against declared states)
547
+ const { transformedHtml: htmlAfterBindings, stateBindings } = transformTextBindings(
548
+ htmlAfterExpressions,
549
+ new Set(stateMap.keys())
550
+ );
551
+
552
+ // Finally strip script/style tags
553
+ const finalHtml = stripScriptAndStyleTags(htmlAfterBindings);
554
+
555
+ // Transform scripts to:
556
+ // 1. Process ES6 imports (transform to window references)
557
+ // 2. Auto-import zen primitives
558
+ // 3. Remove state declarations (runtime will handle them)
559
+ // 4. Register functions on window for component access
560
+ const transformedScripts = scriptsWithInlineHandlers.map((s, index) => {
561
+ if (!s) return '';
562
+
563
+ // Step 1 & 2: Process imports and auto-imports
564
+ let transformed = processScriptImports(s.content);
565
+
566
+ // Step 3: Transform state declarations
567
+ transformed = transformStateDeclarations(transformed);
568
+
569
+ // Step 4: Extract function declarations and register them on window
570
+ // This allows components to access parent functions via props
571
+ // SKIP if script contains an IIFE with window registration (component scripts are pre-wrapped)
572
+ // Component scripts have pattern: window.__zen_comp_*_funcName = funcName
573
+ const hasComponentRegistration = /window\.__zen_comp_\d+_\w+\s*=/.test(transformed);
574
+
575
+ if (!hasComponentRegistration) {
576
+ const functionRegex = /function\s+(\w+)\s*\([^)]*\)\s*\{/g;
577
+ const functionNames: string[] = [];
578
+ let match;
579
+ while ((match = functionRegex.exec(transformed)) !== null) {
580
+ if (match[1]) {
581
+ functionNames.push(match[1]);
582
+ }
583
+ }
584
+
585
+ // Register functions on window (only if not already registered)
586
+ if (functionNames.length > 0) {
587
+ const registrationCode = functionNames.map(name =>
588
+ ` if (typeof window.${name} === 'undefined') { window.${name} = ${name}; }`
589
+ ).join('\n');
590
+ transformed += `\n\n// Register functions on window for component access\n${registrationCode}`;
591
+ }
592
+ }
593
+
594
+ return transformed;
595
+ });
596
+
597
+ return {
598
+ html: finalHtml,
599
+ scripts: transformedScripts,
600
+ styles: file.styles.map(style => style.content),
601
+ eventTypes: Array.from(eventTypes).sort(), // Return sorted array of event types
602
+ stateBindings: Array.from(stateBindings.values()), // Return array of state bindings
603
+ stateDeclarations: stateMap, // Return map of state declarations (name -> value)
604
+ bindings, // Return attribute bindings for :class and :value
605
+ expressionBlocks, // Return expression blocks for dynamic rendering
606
+ attrExprBindings // Return attribute expression bindings
607
+ }
608
+ }
@@ -0,0 +1,32 @@
1
+ export interface ZenFile {
2
+ html: string;
3
+ scripts: ScriptBlock[];
4
+ styles: StyleBlock[];
5
+ functionProps?: Set<string>; // Function names passed as props to components
6
+ }
7
+
8
+ export interface ScriptBlock {
9
+ content: string;
10
+ index: number;
11
+ }
12
+
13
+ export interface StyleBlock {
14
+ content: string;
15
+ index: number;
16
+ }
17
+
18
+ export interface Binding {
19
+ stateName: string;
20
+ nodeIndex: number; // For tracking multiple bindings to the same state
21
+ }
22
+
23
+ export interface StateBinding {
24
+ stateName: string;
25
+ bindings: Binding[];
26
+ }
27
+
28
+ export interface StateDeclaration {
29
+ name: string;
30
+ initialValue: string; // The expression after the = sign
31
+ }
32
+
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Compiled Template Output Types
3
+ *
4
+ * Phase 2: Transform IR → Static HTML + Runtime Bindings
5
+ */
6
+
7
+ export type CompiledTemplate = {
8
+ html: string
9
+ bindings: Binding[]
10
+ scripts: string | null
11
+ styles: string[]
12
+ }
13
+
14
+ export type Binding = {
15
+ id: string
16
+ type: 'text' | 'attribute'
17
+ target: string // e.g., "data-zen-text" or "class" for attribute bindings
18
+ expression: string // The original expression code
19
+ location?: {
20
+ line: number
21
+ column: number
22
+ }
23
+ loopContext?: LoopContext // Phase 7: Loop context for expressions inside map iterations
24
+ }
25
+
26
+ /**
27
+ * Loop context for expressions inside map iterations
28
+ * Phase 7: Tracks loop variables for runtime setter generation
29
+ */
30
+ export type LoopContext = {
31
+ variables: string[] // e.g., ['todo', 'index']
32
+ mapSource?: string // The array being mapped, e.g., 'todoItems'
33
+ }
34
+