@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,1000 @@
1
+ /**
2
+ * Zenith SPA Build System
3
+ *
4
+ * Builds all pages into a single index.html with:
5
+ * - Route manifest
6
+ * - Compiled page modules (inlined)
7
+ * - Runtime router
8
+ * - Shell HTML with router outlet
9
+ */
10
+
11
+ import fs from "fs"
12
+ import path from "path"
13
+ // Import new compiler
14
+ import { compileZen } from "./index"
15
+ // Legacy imports (fallback for now)
16
+ import { parseZen } from "./legacy/parse"
17
+ import { splitZen } from "./legacy/split"
18
+ import { processComponents } from "./legacy/component-process"
19
+ import {
20
+ discoverPages,
21
+ generateRouteDefinition,
22
+ routePathToRegex
23
+ } from "../router/manifest"
24
+
25
+ interface CompiledPage {
26
+ routePath: string
27
+ filePath: string
28
+ html: string
29
+ scripts: string[]
30
+ styles: string[]
31
+ score: number
32
+ paramNames: string[]
33
+ regex: RegExp
34
+ }
35
+
36
+ interface SPABuildOptions {
37
+ /** Pages directory */
38
+ pagesDir: string
39
+ /** Output directory */
40
+ outDir: string
41
+ /** Base directory for components/layouts */
42
+ baseDir?: string
43
+ }
44
+
45
+ /**
46
+ * Compile a single page file
47
+ * Uses new compiler by default, falls back to legacy if needed
48
+ */
49
+ function compilePage(
50
+ pagePath: string,
51
+ pagesDir: string,
52
+ useNewCompiler: boolean = true // Feature flag: use new compiler
53
+ ): CompiledPage {
54
+ if (useNewCompiler) {
55
+ try {
56
+ // Use new compiler pipeline
57
+ const result = compileZen(pagePath)
58
+
59
+ if (!result.finalized) {
60
+ throw new Error(`Compilation failed: No finalized output`)
61
+ }
62
+
63
+ // Extract compiled output
64
+ const html = result.finalized.html
65
+ const js = result.finalized.js
66
+ const styles = result.finalized.styles
67
+
68
+ // Convert JS bundle to scripts array (for compatibility)
69
+ const scripts = js ? [js] : []
70
+
71
+ // Generate route definition
72
+ const routeDef = generateRouteDefinition(pagePath, pagesDir)
73
+ const regex = routePathToRegex(routeDef.path)
74
+
75
+ return {
76
+ routePath: routeDef.path,
77
+ filePath: pagePath,
78
+ html,
79
+ scripts,
80
+ styles,
81
+ score: routeDef.score,
82
+ paramNames: routeDef.paramNames,
83
+ regex
84
+ }
85
+ } catch (error: any) {
86
+ console.warn(`[Zenith Build] New compiler failed for ${pagePath}, falling back to legacy:`, error.message)
87
+ // Fall through to legacy compiler
88
+ }
89
+ }
90
+
91
+ // Legacy compiler (fallback)
92
+ // Parse the .zen file
93
+ const zen = parseZen(pagePath)
94
+
95
+ // Process components and layouts
96
+ const processedZen = processComponents(zen, pagePath)
97
+
98
+ // Split into html, scripts, styles
99
+ const {
100
+ html,
101
+ styles,
102
+ scripts,
103
+ eventTypes,
104
+ stateBindings,
105
+ stateDeclarations,
106
+ bindings,
107
+ expressionBlocks,
108
+ attrExprBindings
109
+ } = splitZen(processedZen)
110
+
111
+ // Import runtime generators
112
+ const { generateEventBindingRuntime } = require("./legacy/event")
113
+ const { generateBindingRuntime } = require("./legacy/binding")
114
+ const { generateAttributeBindingRuntime } = require("./legacy/bindings")
115
+ const { generateExpressionRuntime, generateExpressionRuntimeHelpers, generateAttributeExpressionRuntime } = require("./legacy/expression")
116
+
117
+ // Generate runtime code
118
+ const eventRuntime = generateEventBindingRuntime(eventTypes)
119
+ const bindingRuntime = generateBindingRuntime(stateBindings, stateDeclarations)
120
+ const attributeBindingRuntime = generateAttributeBindingRuntime(bindings)
121
+
122
+ // Generate expression runtime
123
+ // Always generate helpers if we have :class/:value bindings OR expression blocks
124
+ // (bindings.ts uses window.__zen_eval_expr for :class expressions)
125
+ const hasExpressions = expressionBlocks.length > 0 || attrExprBindings.length > 0 || bindings.length > 0
126
+ const expressionRuntimeHelpers = hasExpressions ? generateExpressionRuntimeHelpers() : ''
127
+ const expressionRuntimes = expressionBlocks.map((block: any) =>
128
+ generateExpressionRuntime(block.expression, block.placeholderId)
129
+ ).join('\n')
130
+ const attrExprRuntime = generateAttributeExpressionRuntime(attrExprBindings)
131
+
132
+ // Combine scripts with runtime
133
+ // If scripts array is empty but we have runtimes to add, create a script entry
134
+ const baseScripts = scripts.length > 0 ? scripts : ['']
135
+
136
+ const scriptsWithRuntime = baseScripts.map((s, index) => {
137
+ let result = ""
138
+
139
+ // Add expression helpers first (only to first script)
140
+ if (expressionRuntimeHelpers && index === 0) {
141
+ result += expressionRuntimeHelpers + "\n\n"
142
+ }
143
+
144
+ if (bindingRuntime) {
145
+ result += bindingRuntime + "\n\n"
146
+ }
147
+ if (attributeBindingRuntime) {
148
+ result += attributeBindingRuntime + "\n\n"
149
+ }
150
+ result += s
151
+
152
+ // Add expression and event runtimes to first script
153
+ if (index === 0) {
154
+ if (expressionRuntimes) {
155
+ result += `\n\n${expressionRuntimes}`
156
+ }
157
+ if (attrExprRuntime) {
158
+ result += `\n\n${attrExprRuntime}`
159
+ }
160
+ if (eventRuntime) {
161
+ result += `\n\n${eventRuntime}`
162
+ }
163
+ }
164
+ return result
165
+ })
166
+
167
+ // Generate route definition
168
+ const routeDef = generateRouteDefinition(pagePath, pagesDir)
169
+ const regex = routePathToRegex(routeDef.path)
170
+
171
+ return {
172
+ routePath: routeDef.path,
173
+ filePath: pagePath,
174
+ html,
175
+ scripts: scriptsWithRuntime,
176
+ styles,
177
+ score: routeDef.score,
178
+ paramNames: routeDef.paramNames,
179
+ regex
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Generate the runtime router code (inlined into the bundle)
185
+ */
186
+ function generateRuntimeRouterCode(): string {
187
+ return `
188
+ // ============================================
189
+ // Zenith Runtime Router
190
+ // ============================================
191
+
192
+ (function() {
193
+ 'use strict';
194
+
195
+ // Current route state
196
+ let currentRoute = {
197
+ path: '/',
198
+ params: {},
199
+ query: {}
200
+ };
201
+
202
+ // Route listeners
203
+ const routeListeners = new Set();
204
+
205
+ // Router outlet element
206
+ let routerOutlet = null;
207
+
208
+ // Page modules registry
209
+ const pageModules = {};
210
+
211
+ // Route manifest
212
+ let routeManifest = [];
213
+
214
+ /**
215
+ * Parse query string
216
+ */
217
+ function parseQueryString(search) {
218
+ const query = {};
219
+ if (!search || search === '?') return query;
220
+ const params = new URLSearchParams(search);
221
+ params.forEach((value, key) => { query[key] = value; });
222
+ return query;
223
+ }
224
+
225
+ /**
226
+ * Resolve route from pathname
227
+ */
228
+ function resolveRoute(pathname) {
229
+ const normalizedPath = pathname === '' ? '/' : pathname;
230
+
231
+ for (const route of routeManifest) {
232
+ const match = route.regex.exec(normalizedPath);
233
+ if (match) {
234
+ const params = {};
235
+ for (let i = 0; i < route.paramNames.length; i++) {
236
+ const paramValue = match[i + 1];
237
+ if (paramValue !== undefined) {
238
+ params[route.paramNames[i]] = decodeURIComponent(paramValue);
239
+ }
240
+ }
241
+ return { record: route, params };
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+
247
+ /**
248
+ * Clean up previous page
249
+ */
250
+ function cleanupPreviousPage() {
251
+ // Trigger unmount lifecycle hooks
252
+ if (window.__zenith && window.__zenith.triggerUnmount) {
253
+ window.__zenith.triggerUnmount();
254
+ }
255
+
256
+ // Remove previous page styles
257
+ document.querySelectorAll('style[data-zen-page-style]').forEach(s => s.remove());
258
+
259
+ // Clean up window properties (state variables, functions)
260
+ // This is important for proper state isolation between pages
261
+ if (window.__zenith_cleanup) {
262
+ window.__zenith_cleanup.forEach(key => {
263
+ try { delete window[key]; } catch(e) {}
264
+ });
265
+ }
266
+ window.__zenith_cleanup = [];
267
+ }
268
+
269
+ /**
270
+ * Inject styles
271
+ */
272
+ function injectStyles(styles) {
273
+ styles.forEach((content, i) => {
274
+ const style = document.createElement('style');
275
+ style.setAttribute('data-zen-page-style', String(i));
276
+ style.textContent = content;
277
+ document.head.appendChild(style);
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Execute scripts
283
+ */
284
+ function executeScripts(scripts) {
285
+ scripts.forEach(content => {
286
+ try {
287
+ const fn = new Function(content);
288
+ fn();
289
+ } catch (e) {
290
+ console.error('[Zenith Router] Script error:', e);
291
+ }
292
+ });
293
+ }
294
+
295
+ /**
296
+ * Render page
297
+ */
298
+ function renderPage(pageModule) {
299
+ if (!routerOutlet) {
300
+ console.warn('[Zenith Router] No router outlet');
301
+ return;
302
+ }
303
+
304
+ cleanupPreviousPage();
305
+ routerOutlet.innerHTML = pageModule.html;
306
+ injectStyles(pageModule.styles);
307
+ executeScripts(pageModule.scripts);
308
+
309
+ // Trigger mount lifecycle hooks after scripts are executed
310
+ if (window.__zenith && window.__zenith.triggerMount) {
311
+ window.__zenith.triggerMount();
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Notify listeners
317
+ */
318
+ function notifyListeners(route, prevRoute) {
319
+ routeListeners.forEach(listener => {
320
+ try { listener(route, prevRoute); } catch(e) {}
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Resolve and render
326
+ */
327
+ function resolveAndRender(path, query, updateHistory, replace) {
328
+ replace = replace || false;
329
+ const prevRoute = { ...currentRoute };
330
+ const resolved = resolveRoute(path);
331
+
332
+ if (resolved) {
333
+ currentRoute = {
334
+ path,
335
+ params: resolved.params,
336
+ query,
337
+ matched: resolved.record
338
+ };
339
+
340
+ const pageModule = pageModules[resolved.record.path];
341
+ if (pageModule) {
342
+ renderPage(pageModule);
343
+ }
344
+ } else {
345
+ currentRoute = { path, params: {}, query, matched: undefined };
346
+ console.warn('[Zenith Router] No route matched:', path);
347
+
348
+ // Render 404 if available, otherwise show message
349
+ if (routerOutlet) {
350
+ routerOutlet.innerHTML = '<div style="padding: 2rem; text-align: center;"><h1>404</h1><p>Page not found</p></div>';
351
+ }
352
+ }
353
+
354
+ if (updateHistory) {
355
+ const url = path + (Object.keys(query).length ? '?' + new URLSearchParams(query) : '');
356
+ if (replace) {
357
+ window.history.replaceState(null, '', url);
358
+ } else {
359
+ window.history.pushState(null, '', url);
360
+ }
361
+ }
362
+
363
+ notifyListeners(currentRoute, prevRoute);
364
+ window.__zenith_route = currentRoute;
365
+ }
366
+
367
+ /**
368
+ * Handle popstate
369
+ */
370
+ function handlePopState() {
371
+ // Don't update history on popstate - browser already changed it
372
+ resolveAndRender(
373
+ window.location.pathname,
374
+ parseQueryString(window.location.search),
375
+ false,
376
+ false
377
+ );
378
+ }
379
+
380
+ /**
381
+ * Navigate (public API)
382
+ */
383
+ function navigate(to, options) {
384
+ options = options || {};
385
+ let path, query = {};
386
+
387
+ if (to.includes('?')) {
388
+ const parts = to.split('?');
389
+ path = parts[0];
390
+ query = parseQueryString('?' + parts[1]);
391
+ } else {
392
+ path = to;
393
+ }
394
+
395
+ if (!path.startsWith('/')) {
396
+ const currentDir = currentRoute.path.split('/').slice(0, -1).join('/');
397
+ path = currentDir + '/' + path;
398
+ }
399
+
400
+ // Normalize path for comparison
401
+ const normalizedPath = path === '' ? '/' : path;
402
+ const currentPath = currentRoute.path === '' ? '/' : currentRoute.path;
403
+
404
+ // Check if we're already on this path
405
+ const isSamePath = normalizedPath === currentPath;
406
+
407
+ // If same path and same query, don't navigate (idempotent)
408
+ if (isSamePath && JSON.stringify(query) === JSON.stringify(currentRoute.query)) {
409
+ return;
410
+ }
411
+
412
+ // Resolve and render with replace option if specified
413
+ resolveAndRender(path, query, true, options.replace || false);
414
+ }
415
+
416
+ /**
417
+ * Get current route
418
+ */
419
+ function getRoute() {
420
+ return { ...currentRoute };
421
+ }
422
+
423
+ /**
424
+ * Subscribe to route changes
425
+ */
426
+ function onRouteChange(listener) {
427
+ routeListeners.add(listener);
428
+ return () => routeListeners.delete(listener);
429
+ }
430
+
431
+ /**
432
+ * Check if path is active
433
+ */
434
+ function isActive(path, exact) {
435
+ if (exact) return currentRoute.path === path;
436
+ return currentRoute.path.startsWith(path);
437
+ }
438
+
439
+ /**
440
+ * Prefetch a route (preload page module)
441
+ */
442
+ const prefetchedRoutes = new Set();
443
+ function prefetch(path) {
444
+ const normalizedPath = path === '' ? '/' : path;
445
+ console.log('[Zenith Router] Prefetch requested for:', normalizedPath);
446
+
447
+ if (prefetchedRoutes.has(normalizedPath)) {
448
+ console.log('[Zenith Router] Route already prefetched:', normalizedPath);
449
+ return Promise.resolve();
450
+ }
451
+ prefetchedRoutes.add(normalizedPath);
452
+
453
+ // Find matching route
454
+ const resolved = resolveRoute(normalizedPath);
455
+ if (!resolved) {
456
+ console.warn('[Zenith Router] Prefetch: No route found for:', normalizedPath);
457
+ return Promise.resolve();
458
+ }
459
+
460
+ console.log('[Zenith Router] Prefetch: Route resolved:', resolved.record.path);
461
+
462
+ // Preload the module if it exists
463
+ if (pageModules[resolved.record.path]) {
464
+ console.log('[Zenith Router] Prefetch: Module already loaded:', resolved.record.path);
465
+ // Module already loaded
466
+ return Promise.resolve();
467
+ }
468
+
469
+ console.log('[Zenith Router] Prefetch: Module not yet loaded (all modules are pre-loaded in SPA build)');
470
+ // In SPA build, all modules are already loaded, so this is a no-op
471
+ // Could prefetch here if we had a way to load modules dynamically
472
+ return Promise.resolve();
473
+ }
474
+
475
+ /**
476
+ * Initialize router
477
+ */
478
+ function initRouter(manifest, modules, outlet) {
479
+ routeManifest = manifest;
480
+ Object.assign(pageModules, modules);
481
+
482
+ if (outlet) {
483
+ routerOutlet = typeof outlet === 'string'
484
+ ? document.querySelector(outlet)
485
+ : outlet;
486
+ }
487
+
488
+ window.addEventListener('popstate', handlePopState);
489
+
490
+ // Initial route resolution
491
+ resolveAndRender(
492
+ window.location.pathname,
493
+ parseQueryString(window.location.search),
494
+ false
495
+ );
496
+ }
497
+
498
+ // Expose router API globally
499
+ window.__zenith_router = {
500
+ navigate,
501
+ getRoute,
502
+ onRouteChange,
503
+ isActive,
504
+ prefetch,
505
+ initRouter
506
+ };
507
+
508
+ // Also expose navigate directly for convenience
509
+ window.navigate = navigate;
510
+
511
+ })();
512
+ `
513
+ }
514
+
515
+ /**
516
+ * Generate the Zen primitives runtime code
517
+ * This makes zen* primitives available globally for auto-imports
518
+ */
519
+ function generateZenPrimitivesRuntime(): string {
520
+ return `
521
+ // ============================================
522
+ // Zenith Reactivity Primitives Runtime
523
+ // ============================================
524
+ // Auto-imported zen* primitives are resolved from window.__zenith
525
+
526
+ (function() {
527
+ 'use strict';
528
+
529
+ // ============================================
530
+ // Dependency Tracking System
531
+ // ============================================
532
+
533
+ let currentEffect = null;
534
+ const effectStack = [];
535
+ let batchDepth = 0;
536
+ const pendingEffects = new Set();
537
+
538
+ function pushContext(effect) {
539
+ effectStack.push(currentEffect);
540
+ currentEffect = effect;
541
+ }
542
+
543
+ function popContext() {
544
+ currentEffect = effectStack.pop() || null;
545
+ }
546
+
547
+ function trackDependency(subscribers) {
548
+ if (currentEffect) {
549
+ subscribers.add(currentEffect);
550
+ currentEffect.dependencies.add(subscribers);
551
+ }
552
+ }
553
+
554
+ function notifySubscribers(subscribers) {
555
+ const effects = [...subscribers];
556
+ for (const effect of effects) {
557
+ if (batchDepth > 0) {
558
+ pendingEffects.add(effect);
559
+ } else {
560
+ effect.run();
561
+ }
562
+ }
563
+ }
564
+
565
+ function cleanupEffect(effect) {
566
+ for (const deps of effect.dependencies) {
567
+ deps.delete(effect);
568
+ }
569
+ effect.dependencies.clear();
570
+ }
571
+
572
+ // ============================================
573
+ // zenSignal - Atomic reactive value
574
+ // ============================================
575
+
576
+ function zenSignal(initialValue) {
577
+ let value = initialValue;
578
+ const subscribers = new Set();
579
+
580
+ function signal(newValue) {
581
+ if (arguments.length === 0) {
582
+ trackDependency(subscribers);
583
+ return value;
584
+ }
585
+ if (newValue !== value) {
586
+ value = newValue;
587
+ notifySubscribers(subscribers);
588
+ }
589
+ return value;
590
+ }
591
+
592
+ return signal;
593
+ }
594
+
595
+ // ============================================
596
+ // zenState - Deep reactive object
597
+ // ============================================
598
+
599
+ function zenState(initialObj) {
600
+ const subscribers = new Map(); // path -> Set of effects
601
+
602
+ function getSubscribers(path) {
603
+ if (!subscribers.has(path)) {
604
+ subscribers.set(path, new Set());
605
+ }
606
+ return subscribers.get(path);
607
+ }
608
+
609
+ function createProxy(obj, path = '') {
610
+ if (typeof obj !== 'object' || obj === null) return obj;
611
+
612
+ return new Proxy(obj, {
613
+ get(target, prop) {
614
+ const propPath = path ? path + '.' + String(prop) : String(prop);
615
+ trackDependency(getSubscribers(propPath));
616
+ const value = target[prop];
617
+ if (typeof value === 'object' && value !== null) {
618
+ return createProxy(value, propPath);
619
+ }
620
+ return value;
621
+ },
622
+ set(target, prop, value) {
623
+ const propPath = path ? path + '.' + String(prop) : String(prop);
624
+ target[prop] = value;
625
+ notifySubscribers(getSubscribers(propPath));
626
+ // Also notify parent path for nested updates
627
+ if (path) {
628
+ notifySubscribers(getSubscribers(path));
629
+ }
630
+ return true;
631
+ }
632
+ });
633
+ }
634
+
635
+ return createProxy(initialObj);
636
+ }
637
+
638
+ // ============================================
639
+ // zenEffect - Auto-tracked side effect
640
+ // ============================================
641
+
642
+ function zenEffect(fn) {
643
+ const effect = {
644
+ fn,
645
+ dependencies: new Set(),
646
+ run() {
647
+ cleanupEffect(this);
648
+ pushContext(this);
649
+ try {
650
+ this.fn();
651
+ } finally {
652
+ popContext();
653
+ }
654
+ },
655
+ dispose() {
656
+ cleanupEffect(this);
657
+ }
658
+ };
659
+
660
+ effect.run();
661
+ return () => effect.dispose();
662
+ }
663
+
664
+ // ============================================
665
+ // zenMemo - Cached computed value
666
+ // ============================================
667
+
668
+ function zenMemo(fn) {
669
+ let cachedValue;
670
+ let dirty = true;
671
+ const subscribers = new Set();
672
+
673
+ const effect = {
674
+ dependencies: new Set(),
675
+ run() {
676
+ dirty = true;
677
+ notifySubscribers(subscribers);
678
+ }
679
+ };
680
+
681
+ function compute() {
682
+ if (dirty) {
683
+ cleanupEffect(effect);
684
+ pushContext(effect);
685
+ try {
686
+ cachedValue = fn();
687
+ dirty = false;
688
+ } finally {
689
+ popContext();
690
+ }
691
+ }
692
+ trackDependency(subscribers);
693
+ return cachedValue;
694
+ }
695
+
696
+ return compute;
697
+ }
698
+
699
+ // ============================================
700
+ // zenRef - Non-reactive mutable container
701
+ // ============================================
702
+
703
+ function zenRef(initialValue) {
704
+ return { current: initialValue !== undefined ? initialValue : null };
705
+ }
706
+
707
+ // ============================================
708
+ // zenBatch - Batch updates
709
+ // ============================================
710
+
711
+ function zenBatch(fn) {
712
+ batchDepth++;
713
+ try {
714
+ fn();
715
+ } finally {
716
+ batchDepth--;
717
+ if (batchDepth === 0) {
718
+ const effects = [...pendingEffects];
719
+ pendingEffects.clear();
720
+ for (const effect of effects) {
721
+ effect.run();
722
+ }
723
+ }
724
+ }
725
+ }
726
+
727
+ // ============================================
728
+ // zenUntrack - Read without tracking
729
+ // ============================================
730
+
731
+ function zenUntrack(fn) {
732
+ const prevEffect = currentEffect;
733
+ currentEffect = null;
734
+ try {
735
+ return fn();
736
+ } finally {
737
+ currentEffect = prevEffect;
738
+ }
739
+ }
740
+
741
+ // ============================================
742
+ // Lifecycle Hooks
743
+ // ============================================
744
+
745
+ const mountCallbacks = [];
746
+ const unmountCallbacks = [];
747
+ let isMounted = false;
748
+
749
+ function zenOnMount(fn) {
750
+ if (isMounted) {
751
+ // Already mounted, run immediately
752
+ const cleanup = fn();
753
+ if (typeof cleanup === 'function') {
754
+ unmountCallbacks.push(cleanup);
755
+ }
756
+ } else {
757
+ mountCallbacks.push(fn);
758
+ }
759
+ }
760
+
761
+ function zenOnUnmount(fn) {
762
+ unmountCallbacks.push(fn);
763
+ }
764
+
765
+ // Called by router when page mounts
766
+ function triggerMount() {
767
+ isMounted = true;
768
+ for (const cb of mountCallbacks) {
769
+ const cleanup = cb();
770
+ if (typeof cleanup === 'function') {
771
+ unmountCallbacks.push(cleanup);
772
+ }
773
+ }
774
+ mountCallbacks.length = 0;
775
+ }
776
+
777
+ // Called by router when page unmounts
778
+ function triggerUnmount() {
779
+ isMounted = false;
780
+ for (const cb of unmountCallbacks) {
781
+ try { cb(); } catch(e) { console.error('[Zenith] Unmount error:', e); }
782
+ }
783
+ unmountCallbacks.length = 0;
784
+ }
785
+
786
+ // ============================================
787
+ // Export to window.__zenith
788
+ // ============================================
789
+
790
+ window.__zenith = {
791
+ // Reactivity primitives
792
+ signal: zenSignal,
793
+ state: zenState,
794
+ effect: zenEffect,
795
+ memo: zenMemo,
796
+ ref: zenRef,
797
+ batch: zenBatch,
798
+ untrack: zenUntrack,
799
+ // Lifecycle
800
+ onMount: zenOnMount,
801
+ onUnmount: zenOnUnmount,
802
+ // Internal hooks for router
803
+ triggerMount,
804
+ triggerUnmount
805
+ };
806
+
807
+ // Also expose with zen* prefix for direct usage
808
+ window.zenSignal = zenSignal;
809
+ window.zenState = zenState;
810
+ window.zenEffect = zenEffect;
811
+ window.zenMemo = zenMemo;
812
+ window.zenRef = zenRef;
813
+ window.zenBatch = zenBatch;
814
+ window.zenUntrack = zenUntrack;
815
+ window.zenOnMount = zenOnMount;
816
+ window.zenOnUnmount = zenOnUnmount;
817
+
818
+ // Clean aliases
819
+ window.signal = zenSignal;
820
+ window.state = zenState;
821
+ window.effect = zenEffect;
822
+ window.memo = zenMemo;
823
+ window.ref = zenRef;
824
+ window.batch = zenBatch;
825
+ window.untrack = zenUntrack;
826
+ window.onMount = zenOnMount;
827
+ window.onUnmount = zenOnUnmount;
828
+
829
+ })();
830
+ `
831
+ }
832
+
833
+ /**
834
+ * Generate the HTML shell
835
+ */
836
+ function generateHTMLShell(
837
+ pages: CompiledPage[],
838
+ layoutStyles: string[]
839
+ ): string {
840
+ // Collect all global styles (from layouts)
841
+ const globalStyles = layoutStyles.join("\n")
842
+
843
+ // Generate route manifest JavaScript
844
+ const manifestJS = pages.map(page => ({
845
+ path: page.routePath,
846
+ regex: page.regex.toString(),
847
+ paramNames: page.paramNames,
848
+ score: page.score,
849
+ filePath: page.filePath
850
+ }))
851
+
852
+ // Generate page modules JavaScript
853
+ const modulesJS = pages.map(page => {
854
+ const escapedHtml = JSON.stringify(page.html)
855
+ const escapedScripts = JSON.stringify(page.scripts)
856
+ const escapedStyles = JSON.stringify(page.styles)
857
+
858
+ return `${JSON.stringify(page.routePath)}: {
859
+ html: ${escapedHtml},
860
+ scripts: ${escapedScripts},
861
+ styles: ${escapedStyles}
862
+ }`
863
+ }).join(",\n ")
864
+
865
+ // Generate manifest with actual RegExp objects
866
+ const manifestCode = `[
867
+ ${pages.map(page => `{
868
+ path: ${JSON.stringify(page.routePath)},
869
+ regex: ${page.regex.toString()},
870
+ paramNames: ${JSON.stringify(page.paramNames)},
871
+ score: ${page.score}
872
+ }`).join(",\n ")}
873
+ ]`
874
+
875
+ return `<!DOCTYPE html>
876
+ <html lang="en">
877
+ <head>
878
+ <meta charset="UTF-8">
879
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
880
+ <title>Zenith App</title>
881
+ <link rel="icon" type="image/x-icon" href="./favicon.ico">
882
+ <style>
883
+ /* Global/Layout Styles */
884
+ ${globalStyles}
885
+ </style>
886
+ </head>
887
+ <body>
888
+ <!-- Router Outlet -->
889
+ <div id="app"></div>
890
+
891
+ <!-- Zenith Primitives Runtime -->
892
+ <script>
893
+ ${generateZenPrimitivesRuntime()}
894
+ </script>
895
+
896
+ <!-- Zenith Runtime Router -->
897
+ <script>
898
+ ${generateRuntimeRouterCode()}
899
+ </script>
900
+
901
+ <!-- Route Manifest & Page Modules -->
902
+ <script>
903
+ (function() {
904
+ // Route manifest (sorted by score, highest first)
905
+ const manifest = ${manifestCode};
906
+
907
+ // Page modules keyed by route path
908
+ const modules = {
909
+ ${modulesJS}
910
+ };
911
+
912
+ // Initialize router when DOM is ready
913
+ if (document.readyState === 'loading') {
914
+ document.addEventListener('DOMContentLoaded', function() {
915
+ window.__zenith_router.initRouter(manifest, modules, '#app');
916
+ });
917
+ } else {
918
+ window.__zenith_router.initRouter(manifest, modules, '#app');
919
+ }
920
+ })();
921
+ </script>
922
+ </body>
923
+ </html>`
924
+ }
925
+
926
+ /**
927
+ * Build SPA from pages directory
928
+ */
929
+ export function buildSPA(options: SPABuildOptions): void {
930
+ const { pagesDir, outDir, baseDir } = options
931
+
932
+ // Clean output directory
933
+ if (fs.existsSync(outDir)) {
934
+ fs.rmSync(outDir, { recursive: true, force: true })
935
+ }
936
+ fs.mkdirSync(outDir, { recursive: true })
937
+
938
+ // Discover all pages
939
+ const pageFiles = discoverPages(pagesDir)
940
+
941
+ if (pageFiles.length === 0) {
942
+ console.warn("[Zenith Build] No pages found in", pagesDir)
943
+ return
944
+ }
945
+
946
+ console.log(`[Zenith Build] Found ${pageFiles.length} page(s)`)
947
+
948
+ // Compile all pages
949
+ const compiledPages: CompiledPage[] = []
950
+ const layoutStyles: string[] = []
951
+
952
+ for (const pageFile of pageFiles) {
953
+ console.log(`[Zenith Build] Compiling: ${path.relative(pagesDir, pageFile)}`)
954
+
955
+ try {
956
+ const compiled = compilePage(pageFile, pagesDir)
957
+ compiledPages.push(compiled)
958
+ } catch (error) {
959
+ console.error(`[Zenith Build] Error compiling ${pageFile}:`, error)
960
+ throw error
961
+ }
962
+ }
963
+
964
+ // Sort pages by score (highest first)
965
+ compiledPages.sort((a, b) => b.score - a.score)
966
+
967
+ // Extract layout styles (they should be global)
968
+ // For now, we'll include any styles from the first page that uses a layout
969
+ // TODO: Better layout handling
970
+
971
+ // Generate HTML shell
972
+ const htmlShell = generateHTMLShell(compiledPages, layoutStyles)
973
+
974
+ // Write index.html
975
+ fs.writeFileSync(path.join(outDir, "index.html"), htmlShell)
976
+
977
+ // Copy favicon if it exists
978
+ const faviconPath = path.join(path.dirname(pagesDir), "favicon.ico")
979
+ if (fs.existsSync(faviconPath)) {
980
+ fs.copyFileSync(faviconPath, path.join(outDir, "favicon.ico"))
981
+ }
982
+
983
+ console.log(`[Zenith Build] Successfully built ${compiledPages.length} page(s)`)
984
+ console.log(`[Zenith Build] Output: ${outDir}/index.html`)
985
+
986
+ // Log route manifest
987
+ console.log("\n[Zenith Build] Route Manifest:")
988
+ for (const page of compiledPages) {
989
+ console.log(` ${page.routePath.padEnd(25)} → ${path.relative(pagesDir, page.filePath)} (score: ${page.score})`)
990
+ }
991
+ }
992
+
993
+ /**
994
+ * Watch mode for development (future)
995
+ */
996
+ export function watchSPA(_options: SPABuildOptions): void {
997
+ // TODO: Implement file watching
998
+ console.log("[Zenith Build] Watch mode not yet implemented")
999
+ }
1000
+