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