@zenithbuild/core 0.6.2 → 1.0.1

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