@zenithbuild/compiler 1.3.2 → 1.3.9

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 (73) hide show
  1. package/dist/build-analyzer.d.ts +44 -0
  2. package/dist/build-analyzer.js +87 -0
  3. package/dist/bundler.d.ts +31 -0
  4. package/dist/bundler.js +92 -0
  5. package/dist/core/config/index.d.ts +11 -0
  6. package/dist/core/config/index.js +10 -0
  7. package/dist/core/config/loader.d.ts +17 -0
  8. package/dist/core/config/loader.js +60 -0
  9. package/dist/core/config/types.d.ts +98 -0
  10. package/dist/core/config/types.js +32 -0
  11. package/dist/core/index.d.ts +7 -0
  12. package/dist/core/index.js +6 -0
  13. package/dist/core/plugins/bridge.d.ts +116 -0
  14. package/dist/core/plugins/bridge.js +121 -0
  15. package/dist/core/plugins/index.d.ts +6 -0
  16. package/dist/core/plugins/index.js +6 -0
  17. package/dist/core/plugins/registry.d.ts +67 -0
  18. package/dist/core/plugins/registry.js +112 -0
  19. package/dist/css/index.d.ts +73 -0
  20. package/dist/css/index.js +246 -0
  21. package/dist/discovery/componentDiscovery.d.ts +41 -0
  22. package/dist/discovery/componentDiscovery.js +66 -0
  23. package/dist/discovery/layouts.d.ts +14 -0
  24. package/dist/discovery/layouts.js +36 -0
  25. package/dist/errors/compilerError.d.ts +31 -0
  26. package/dist/errors/compilerError.js +51 -0
  27. package/dist/finalize/generateFinalBundle.d.ts +24 -0
  28. package/dist/finalize/generateFinalBundle.js +68 -0
  29. package/dist/index.d.ts +34 -0
  30. package/dist/index.js +44 -0
  31. package/dist/ir/types.d.ts +206 -0
  32. package/dist/ir/types.js +8 -0
  33. package/dist/output/types.d.ts +39 -0
  34. package/dist/output/types.js +6 -0
  35. package/dist/parseZenFile.d.ts +17 -0
  36. package/dist/parseZenFile.js +52 -0
  37. package/dist/runtime/build.d.ts +6 -0
  38. package/dist/runtime/build.js +13 -0
  39. package/dist/runtime/bundle-generator.d.ts +27 -0
  40. package/dist/runtime/bundle-generator.js +1438 -0
  41. package/dist/runtime/client-runtime.d.ts +41 -0
  42. package/dist/runtime/client-runtime.js +397 -0
  43. package/dist/runtime/hydration.d.ts +53 -0
  44. package/dist/runtime/hydration.js +271 -0
  45. package/dist/runtime/navigation.d.ts +58 -0
  46. package/dist/runtime/navigation.js +372 -0
  47. package/dist/runtime/serve.d.ts +13 -0
  48. package/dist/runtime/serve.js +76 -0
  49. package/dist/runtime/thinRuntime.d.ts +23 -0
  50. package/dist/runtime/thinRuntime.js +158 -0
  51. package/dist/spa-build.d.ts +26 -0
  52. package/dist/spa-build.js +849 -0
  53. package/dist/ssg-build.d.ts +31 -0
  54. package/dist/ssg-build.js +429 -0
  55. package/dist/test/bundler-contract.test.d.ts +1 -0
  56. package/dist/test/bundler-contract.test.js +137 -0
  57. package/dist/test/compiler-entry.test.d.ts +1 -0
  58. package/dist/test/compiler-entry.test.js +28 -0
  59. package/dist/test/error-native-bridge.test.d.ts +1 -0
  60. package/dist/test/error-native-bridge.test.js +31 -0
  61. package/dist/test/error-serialization.test.d.ts +1 -0
  62. package/dist/test/error-serialization.test.js +38 -0
  63. package/dist/test/phase5-boundary.test.d.ts +1 -0
  64. package/dist/test/phase5-boundary.test.js +51 -0
  65. package/dist/transform/layoutProcessor.d.ts +26 -0
  66. package/dist/transform/layoutProcessor.js +34 -0
  67. package/dist/validate/invariants.d.ts +23 -0
  68. package/dist/validate/invariants.js +55 -0
  69. package/native/compiler-native/compiler-native.node +0 -0
  70. package/native/compiler-native/index.d.ts +2 -46
  71. package/native/compiler-native/index.js +1 -16
  72. package/native/compiler-native/package.json +1 -1
  73. package/package.json +15 -5
@@ -0,0 +1,1438 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Zenith Bundle Generator
5
+ *
6
+ * Generates the shared client runtime bundle that gets served as:
7
+ * - /assets/bundle.js in production
8
+ * - /runtime.js in development
9
+ *
10
+ * This is a cacheable, versioned file that contains:
11
+ * - Reactivity primitives (zenSignal, zenState, zenEffect, etc.)
12
+ * - Lifecycle hooks (zenOnMount, zenOnUnmount)
13
+ * - Hydration functions (zenithHydrate)
14
+ * - Event binding utilities
15
+ */
16
+ /**
17
+ * Generate the complete client runtime bundle
18
+ * This is served as an external JS file, not inlined
19
+ */
20
+ export function generateBundleJS(pluginData) {
21
+ // Serialize plugin data blindly - CLI never inspects what's inside.
22
+ // We escape </script> sequences just in case this bundle is ever inlined (unlikely but safe).
23
+ const serializedData = pluginData
24
+ ? JSON.stringify(pluginData).replace(/<\/script/g, '<\\/script')
25
+ : '{}';
26
+ // Resolve core runtime paths - assumes sibling directory or relative in node_modules
27
+ const rootDir = process.cwd();
28
+ let coreRuntimePath = path.join(rootDir, '../zenith-core/dist/runtime');
29
+ if (!existsSync(coreRuntimePath)) {
30
+ coreRuntimePath = path.join(rootDir, '../zenith-core/core');
31
+ }
32
+ let reactivityJS = '';
33
+ let lifecycleJS = '';
34
+ try {
35
+ let reactivityFile = path.join(coreRuntimePath, 'reactivity/index.js');
36
+ if (!existsSync(reactivityFile))
37
+ reactivityFile = path.join(coreRuntimePath, 'reactivity/index.ts');
38
+ let lifecycleFile = path.join(coreRuntimePath, 'lifecycle/index.js');
39
+ if (!existsSync(lifecycleFile))
40
+ lifecycleFile = path.join(coreRuntimePath, 'lifecycle/index.ts');
41
+ if (existsSync(reactivityFile) && reactivityFile.endsWith('.js')) {
42
+ reactivityJS = transformExportsToGlobal(readFileSync(reactivityFile, 'utf-8'));
43
+ }
44
+ if (existsSync(lifecycleFile) && lifecycleFile.endsWith('.js')) {
45
+ lifecycleJS = transformExportsToGlobal(readFileSync(lifecycleFile, 'utf-8'));
46
+ }
47
+ }
48
+ catch (e) {
49
+ if (process.env.ZENITH_DEBUG === 'true') {
50
+ console.warn('[Zenith] Could not load runtime from core, falling back to internal', e);
51
+ }
52
+ }
53
+ // Fallback to internal hydration_runtime.js from native compiler source
54
+ // Use the compiler's own location to find the file, not process.cwd()
55
+ if (!reactivityJS || !lifecycleJS) {
56
+ // Resolve relative to this bundle-generator.ts file's location
57
+ // In compiled form, this will be in dist/runtime/, so we go up to find native/
58
+ const compilerRoot = path.resolve(path.dirname(import.meta.url.replace('file://', '')), '../../..');
59
+ const nativeRuntimePath = path.join(compilerRoot, 'native/compiler-native/src/hydration_runtime.js');
60
+ if (existsSync(nativeRuntimePath)) {
61
+ const nativeJS = readFileSync(nativeRuntimePath, 'utf-8');
62
+ // IMPORTANT: Include the FULL IIFE - do NOT strip the wrapper!
63
+ // The runtime has its own bootstrap guard and idempotency check.
64
+ // It will install primitives to window and create window.__ZENITH_RUNTIME__
65
+ reactivityJS = nativeJS;
66
+ lifecycleJS = ' '; // Ensure it doesn't trigger the "not found" message
67
+ }
68
+ }
69
+ return `/*!
70
+ * Zenith Runtime v1.0.1
71
+ * Shared client-side runtime for hydration and reactivity
72
+ */
73
+ (function(global) {
74
+ 'use strict';
75
+
76
+ // Initialize plugin data envelope
77
+ global.__ZENITH_PLUGIN_DATA__ = ${serializedData};
78
+
79
+ ${reactivityJS ? ` // ============================================
80
+ // Core Reactivity (Injected from @zenithbuild/core)
81
+ // ============================================
82
+ ${reactivityJS}` : ` // Fallback: Reactivity not found`}
83
+
84
+ ${lifecycleJS ? ` // ============================================
85
+ // Lifecycle Hooks (Injected from @zenithbuild/core)
86
+ // ============================================
87
+ ${lifecycleJS}` : ` // Fallback: Lifecycle not found`}
88
+
89
+ // ----------------------------------------------------------------------
90
+ // COMPAT: Expose internal exports as globals
91
+ // ----------------------------------------------------------------------
92
+ // The code above was stripped of "export { ... }" but assigned to internal variables.
93
+ // We need to map them back to global scope if they weren't attached by the code itself.
94
+
95
+ // Reactivity primitives map (internal name -> global alias)
96
+ // Based on zenith-core/core/reactivity/index.ts re-exports:
97
+ // export { zenSignal, zenState, ... }
98
+
99
+ // Since we stripped exports, we rely on the fact that the bundled code
100
+ // defines variables like "var zenSignal = ..." or "function zenSignal...".
101
+ // Note: Minified code variables might be renamed (e.g., "var P=...").
102
+ // Ideally, @zenithbuild/core should export an IIFE build for this purpose.
103
+ // For now, we assume the code above already does "global.zenSignal = ..."
104
+ // OR we rely on the Aliases section below to do the mapping if the names match.
105
+
106
+ // ============================================
107
+ // Lifecycle Hooks (Required for hydration)
108
+ // ============================================
109
+ // These functions are required by the runtime - define them if not injected from core
110
+
111
+ const mountCallbacks = [];
112
+ const unmountCallbacks = [];
113
+ let isMounted = false;
114
+
115
+ function zenOnMount(fn) {
116
+ if (isMounted) {
117
+ // Already mounted, run immediately
118
+ const cleanup = fn();
119
+ if (typeof cleanup === 'function') {
120
+ unmountCallbacks.push(cleanup);
121
+ }
122
+ } else {
123
+ mountCallbacks.push(fn);
124
+ }
125
+ }
126
+
127
+ function zenOnUnmount(fn) {
128
+ unmountCallbacks.push(fn);
129
+ }
130
+
131
+ // Called by hydration when page mounts
132
+ function triggerMount() {
133
+ isMounted = true;
134
+ for (let i = 0; i < mountCallbacks.length; i++) {
135
+ try {
136
+ const cleanup = mountCallbacks[i]();
137
+ if (typeof cleanup === 'function') {
138
+ unmountCallbacks.push(cleanup);
139
+ }
140
+ } catch(e) {
141
+ console.error('[Zenith] Mount callback error:', e);
142
+ }
143
+ }
144
+ mountCallbacks.length = 0;
145
+ }
146
+
147
+ // Called by router when page unmounts
148
+ function triggerUnmount() {
149
+ isMounted = false;
150
+ for (let i = 0; i < unmountCallbacks.length; i++) {
151
+ try { unmountCallbacks[i](); } catch(e) { console.error('[Zenith] Unmount error:', e); }
152
+ }
153
+ unmountCallbacks.length = 0;
154
+ }
155
+
156
+ // ============================================
157
+ // Component Instance System
158
+ // ============================================
159
+ // Each component instance gets isolated state, effects, and lifecycles
160
+ // Instances are tied to DOM elements via hydration markers
161
+
162
+ const componentRegistry = {};
163
+
164
+ function createComponentInstance(componentName, rootElement) {
165
+ const instanceMountCallbacks = [];
166
+ const instanceUnmountCallbacks = [];
167
+ const instanceEffects = [];
168
+ let instanceMounted = false;
169
+
170
+ return {
171
+ // DOM reference
172
+ root: rootElement,
173
+
174
+ // Lifecycle hooks (instance-scoped)
175
+ onMount: function(fn) {
176
+ if (instanceMounted) {
177
+ const cleanup = fn();
178
+ if (typeof cleanup === 'function') {
179
+ instanceUnmountCallbacks.push(cleanup);
180
+ }
181
+ } else {
182
+ instanceMountCallbacks.push(fn);
183
+ }
184
+ },
185
+ onUnmount: function(fn) {
186
+ instanceUnmountCallbacks.push(fn);
187
+ },
188
+
189
+ // Reactivity (uses global primitives but tracks for cleanup)
190
+ signal: function(initial) {
191
+ return global.zenSignal(initial);
192
+ },
193
+ state: function(initial) {
194
+ return global.zenState(initial);
195
+ },
196
+ ref: function(initial) {
197
+ return global.zenRef(initial);
198
+ },
199
+ effect: function(fn) {
200
+ const cleanup = global.zenEffect(fn);
201
+ instanceEffects.push(cleanup);
202
+ return cleanup;
203
+ },
204
+ memo: function(fn) {
205
+ return global.zenMemo(fn);
206
+ },
207
+ batch: function(fn) {
208
+ global.zenBatch(fn);
209
+ },
210
+ untrack: function(fn) {
211
+ return global.zenUntrack(fn);
212
+ },
213
+
214
+ // Lifecycle execution
215
+ mount: function() {
216
+ instanceMounted = true;
217
+ for (let i = 0; i < instanceMountCallbacks.length; i++) {
218
+ try {
219
+ const cleanup = instanceMountCallbacks[i]();
220
+ if (typeof cleanup === 'function') {
221
+ instanceUnmountCallbacks.push(cleanup);
222
+ }
223
+ } catch(e) {
224
+ console.error('[Zenith] Component mount error:', componentName, e);
225
+ }
226
+ }
227
+ instanceMountCallbacks.length = 0;
228
+ },
229
+ unmount: function() {
230
+ instanceMounted = false;
231
+ // Cleanup effects
232
+ for (let i = 0; i < instanceEffects.length; i++) {
233
+ try {
234
+ if (typeof instanceEffects[i] === 'function') instanceEffects[i]();
235
+ } catch(e) {
236
+ console.error('[Zenith] Effect cleanup error:', e);
237
+ }
238
+ }
239
+ instanceEffects.length = 0;
240
+ // Run unmount callbacks
241
+ for (let i = 0; i < instanceUnmountCallbacks.length; i++) {
242
+ try { instanceUnmountCallbacks[i](); } catch(e) { console.error('[Zenith] Unmount error:', e); }
243
+ }
244
+ instanceUnmountCallbacks.length = 0;
245
+ }
246
+ };
247
+ }
248
+
249
+ function defineComponent(name, factory) {
250
+ componentRegistry[name] = factory;
251
+ }
252
+
253
+ function instantiateComponent(name, props, rootElement) {
254
+ const factory = componentRegistry[name];
255
+ if (!factory) {
256
+ if (name === 'ErrorPage') {
257
+ // Built-in fallback for ErrorPage if not registered by user
258
+ return fallbackErrorPage(props, rootElement);
259
+ }
260
+ console.warn('[Zenith] Component not found:', name);
261
+ return null;
262
+ }
263
+ return factory(props, rootElement);
264
+ }
265
+
266
+ function renderErrorPage(error, metadata) {
267
+ console.error('[Zenith Runtime Error]', error, metadata);
268
+
269
+ // In production, we might want a simpler page, but for now let's use the high-fidelity one
270
+ // if it's available.
271
+ const container = document.getElementById('app') || document.body;
272
+
273
+ // If we've already rendered an error page, don't do it again to avoid infinite loops
274
+ if (window.__ZENITH_ERROR_RENDERED__) return;
275
+ window.__ZENITH_ERROR_RENDERED__ = true;
276
+
277
+ const errorProps = {
278
+ message: error.message || 'Unknown Error',
279
+ stack: error.stack,
280
+ file: metadata.file || (error.file),
281
+ line: metadata.line || (error.line),
282
+ column: metadata.column || (error.column),
283
+ errorType: metadata.errorType || error.name || 'RuntimeError',
284
+ code: metadata.code || 'ERR500',
285
+ context: metadata.context || (metadata.expressionId ? 'Expression: ' + metadata.expressionId : ''),
286
+ hints: metadata.hints || [],
287
+ isProd: false // Check env here if possible
288
+ };
289
+
290
+ // Try to instantiate the user's ErrorPage
291
+ const instance = instantiateComponent('ErrorPage', errorProps, container);
292
+ if (instance) {
293
+ container.innerHTML = '';
294
+ instance.mount();
295
+ } else {
296
+ // Fallback to basic HTML if ErrorPage component fails or is missing
297
+ container.innerHTML = \`
298
+ <div style="padding: 4rem; font-family: system-ui, sans-serif; background: #000; color: #fff; min-h: 100vh;">
299
+ <h1 style="font-size: 3rem; margin-bottom: 1rem; color: #ef4444;">Zenith Runtime Error</h1>
300
+ <p style="font-size: 1.5rem; opacity: 0.8;">\${errorProps.message}</p>
301
+ <pre style="margin-top: 2rem; padding: 1rem; background: #111; border-radius: 8px; overflow: auto; font-size: 0.8rem; color: #888;">\${errorProps.stack}</pre>
302
+ </div>
303
+ \`;
304
+ }
305
+ }
306
+
307
+ function fallbackErrorPage(props, el) {
308
+ // This could be a more complex fallback, but for now we just return null
309
+ // to trigger the basic HTML fallback in renderErrorPage.
310
+ return null;
311
+ }
312
+
313
+ /**
314
+ * Hydrate components by discovering data-zen-component markers
315
+ * This is the ONLY place component instantiation should happen
316
+ */
317
+ function hydrateComponents(container) {
318
+ try {
319
+ const componentElements = container.querySelectorAll('[data-zen-component]');
320
+
321
+ for (let i = 0; i < componentElements.length; i++) {
322
+ const el = componentElements[i];
323
+ const componentName = el.getAttribute('data-zen-component');
324
+
325
+ // Skip if already hydrated OR if handled by instance script (data-zen-inst)
326
+ if (el.__zenith_instance || el.hasAttribute('data-zen-inst')) continue;
327
+
328
+ // Parse props from data attribute if present
329
+ const propsJson = el.getAttribute('data-zen-props') || '{}';
330
+ let props = {};
331
+ try {
332
+ props = JSON.parse(propsJson);
333
+ } catch(e) {
334
+ console.warn('[Zenith] Invalid props JSON for', componentName);
335
+ }
336
+
337
+ try {
338
+ // Instantiate component and bind to DOM element
339
+ const instance = instantiateComponent(componentName, props, el);
340
+
341
+ if (instance) {
342
+ el.__zenith_instance = instance;
343
+ }
344
+ } catch (e) {
345
+ renderErrorPage(e, { component: componentName, props: props });
346
+ }
347
+ }
348
+ } catch (e) {
349
+ renderErrorPage(e, { activity: 'hydrateComponents' });
350
+ }
351
+ }
352
+
353
+ // ============================================
354
+ // Expression Registry & Hydration
355
+ // ============================================
356
+
357
+ const expressionRegistry = new Map();
358
+
359
+ function registerExpression(id, fn) {
360
+ expressionRegistry.set(id, fn);
361
+ }
362
+
363
+ function getExpression(id) {
364
+ return expressionRegistry.get(id);
365
+ }
366
+
367
+ function updateNode(node, exprId, pageState) {
368
+ const expr = getExpression(exprId);
369
+ if (!expr) return;
370
+
371
+ zenEffect(function() {
372
+ try {
373
+ const result = expr(pageState);
374
+
375
+ if (node.hasAttribute('data-zen-text')) {
376
+ // Handle complex text/children results
377
+ if (result === null || result === undefined || result === false) {
378
+ node.textContent = '';
379
+ } else if (typeof result === 'string') {
380
+ if (result.trim().startsWith('<') && result.trim().endsWith('>')) {
381
+ node.innerHTML = result;
382
+ } else {
383
+ node.textContent = result;
384
+ }
385
+ } else if (result instanceof Node) {
386
+ node.innerHTML = '';
387
+ node.appendChild(result);
388
+ } else if (Array.isArray(result)) {
389
+ node.innerHTML = '';
390
+ const fragment = document.createDocumentFragment();
391
+ result.flat(Infinity).forEach(item => {
392
+ if (item instanceof Node) fragment.appendChild(item);
393
+ else if (item != null && item !== false) fragment.appendChild(document.createTextNode(String(item)));
394
+ });
395
+ node.appendChild(fragment);
396
+ } else {
397
+ node.textContent = String(result);
398
+ }
399
+ } else {
400
+ // Attribute update
401
+ const attrNames = ['class', 'style', 'src', 'href', 'disabled', 'checked'];
402
+ for (const attr of attrNames) {
403
+ if (node.hasAttribute('data-zen-attr-' + attr)) {
404
+ if (attr === 'class' || attr === 'className') {
405
+ node.className = String(result || '');
406
+ } else if (attr === 'disabled' || attr === 'checked') {
407
+ if (result) node.setAttribute(attr, '');
408
+ else node.removeAttribute(attr);
409
+ } else {
410
+ if (result != null && result !== false) node.setAttribute(attr, String(result));
411
+ else node.removeAttribute(attr);
412
+ }
413
+ }
414
+ }
415
+ }
416
+ } catch (e) {
417
+ renderErrorPage(e, { expressionId: exprId, node: node });
418
+ }
419
+ });
420
+ }
421
+
422
+ /**
423
+ * Hydrate a page with reactive bindings
424
+ * Called after page HTML is in DOM
425
+ */
426
+ function updateLoopBinding(template, exprId, pageState) {
427
+ const expr = getExpression(exprId);
428
+ if (!expr) return;
429
+
430
+ const itemVar = template.getAttribute('data-zen-item');
431
+ const indexVar = template.getAttribute('data-zen-index');
432
+
433
+ // Use a marker or a container next to the template to hold instances
434
+ let container = template.__zen_container;
435
+ if (!container) {
436
+ container = document.createElement('div');
437
+ container.style.display = 'contents';
438
+ template.parentNode.insertBefore(container, template.nextSibling);
439
+ template.__zen_container = container;
440
+ }
441
+
442
+ zenEffect(function() {
443
+ try {
444
+ const items = expr(pageState);
445
+ if (!Array.isArray(items)) return;
446
+
447
+ // Simple reconciliation: clear and redraw
448
+ container.innerHTML = '';
449
+
450
+ items.forEach(function(item, index) {
451
+ const fragment = template.content.cloneNode(true);
452
+
453
+ // Create child scope
454
+ const childState = Object.assign({}, pageState);
455
+ if (itemVar) childState[itemVar] = item;
456
+ if (indexVar) childState[indexVar] = index;
457
+
458
+ // Recursive hydration for the fragment
459
+ zenithHydrate(childState, fragment);
460
+
461
+ container.appendChild(fragment);
462
+ });
463
+ } catch (e) {
464
+ renderErrorPage(e, { expressionId: exprId, activity: 'loopReconciliation' });
465
+ }
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Hydrate static HTML with dynamic expressions
471
+ */
472
+ /**
473
+ * Hydrate static HTML with dynamic expressions (Comment-based)
474
+ */
475
+ function zenithHydrate(pageState, container) {
476
+ try {
477
+ container = container || document;
478
+
479
+ // Walker to find comment nodes efficiently
480
+ const walker = document.createTreeWalker(
481
+ container,
482
+ NodeFilter.SHOW_COMMENT,
483
+ null,
484
+ false
485
+ );
486
+
487
+ const exprLocationMap = new Map();
488
+ let node;
489
+
490
+ while(node = walker.nextNode()) {
491
+ const content = node.nodeValue || '';
492
+ if (content.startsWith('zen:expr_')) {
493
+ const exprId = content.replace('zen:expr_', '');
494
+ exprLocationMap.set(node, exprId);
495
+ }
496
+ }
497
+
498
+ // Process expressions
499
+ for (const [commentNode, exprId] of exprLocationMap) {
500
+ updateNode(commentNode, exprId, pageState);
501
+ }
502
+
503
+ // Wire up event handlers (still attribute based, usually safe)
504
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
505
+ eventTypes.forEach(eventType => {
506
+ const elements = container.querySelectorAll('[data-zen-' + eventType + ']');
507
+ elements.forEach(el => {
508
+ const handlerName = el.getAttribute('data-zen-' + eventType);
509
+ // Check global scope (window) or expression registry
510
+ if (handlerName) {
511
+ el.addEventListener(eventType, function(e) {
512
+ // Resolve handler at runtime to allow for late definition
513
+ const handler = global[handlerName] || getExpression(handlerName);
514
+ if (typeof handler === 'function') {
515
+ handler(e, el);
516
+ } else {
517
+ console.warn('[Zenith] Handler not found:', handlerName);
518
+ }
519
+ });
520
+ }
521
+ });
522
+ });
523
+
524
+ // Trigger mount
525
+ if (container === document || container.id === 'app' || container.tagName === 'BODY') {
526
+ triggerMount();
527
+ }
528
+ } catch (e) {
529
+ renderErrorPage(e, { activity: 'zenithHydrate' });
530
+ }
531
+ }
532
+
533
+ // Update logic for comment placeholders
534
+ function updateNode(placeholder, exprId, pageState) {
535
+ const expr = getExpression(exprId);
536
+ if (!expr) return;
537
+
538
+ // Store reference to current nodes for cleanup
539
+ let currentNodes = [];
540
+
541
+ zenEffect(function() {
542
+ try {
543
+ const result = expr(pageState);
544
+
545
+ // Cleanup old nodes
546
+ currentNodes.forEach(n => n.remove());
547
+ currentNodes = [];
548
+
549
+ if (result == null || result === false) {
550
+ // Render nothing
551
+ } else if (result instanceof Node) {
552
+ placeholder.parentNode.insertBefore(result, placeholder);
553
+ currentNodes.push(result);
554
+ } else if (Array.isArray(result)) {
555
+ result.flat(Infinity).forEach(item => {
556
+ const n = item instanceof Node ? item : document.createTextNode(String(item));
557
+ placeholder.parentNode.insertBefore(n, placeholder);
558
+ currentNodes.push(n);
559
+ });
560
+ } else {
561
+ // Primitive
562
+ const n = document.createTextNode(String(result));
563
+ placeholder.parentNode.insertBefore(n, placeholder);
564
+ currentNodes.push(n);
565
+ }
566
+ } catch (e) {
567
+ renderErrorPage(e, { expressionId: exprId });
568
+ }
569
+ });
570
+ }
571
+
572
+ // ============================================
573
+ // zenith:content - Content Engine
574
+ // ============================================
575
+
576
+ const schemaRegistry = new Map();
577
+ const builtInEnhancers = {
578
+ readTime: (item) => {
579
+ const wordsPerMinute = 200;
580
+ const text = item.content || '';
581
+ const wordCount = text.split(/\\s+/).length;
582
+ const minutes = Math.ceil(wordCount / wordsPerMinute);
583
+ return Object.assign({}, item, { readTime: minutes + ' min' });
584
+ },
585
+ wordCount: (item) => {
586
+ const text = item.content || '';
587
+ const wordCount = text.split(/\\s+/).length;
588
+ return Object.assign({}, item, { wordCount: wordCount });
589
+ }
590
+ };
591
+
592
+ async function applyEnhancers(item, enhancers) {
593
+ let enrichedItem = Object.assign({}, item);
594
+ for (const enhancer of enhancers) {
595
+ if (typeof enhancer === 'string') {
596
+ const fn = builtInEnhancers[enhancer];
597
+ if (fn) enrichedItem = await fn(enrichedItem);
598
+ } else if (typeof enhancer === 'function') {
599
+ enrichedItem = await enhancer(enrichedItem);
600
+ }
601
+ }
602
+ return enrichedItem;
603
+ }
604
+
605
+ class ZenCollection {
606
+ constructor(items) {
607
+ this.items = [...items];
608
+ this.filters = [];
609
+ this.sortField = null;
610
+ this.sortOrder = 'desc';
611
+ this.limitCount = null;
612
+ this.selectedFields = null;
613
+ this.enhancers = [];
614
+ this._groupByFolder = false;
615
+ }
616
+ where(fn) { this.filters.push(fn); return this; }
617
+ sortBy(field, order = 'desc') { this.sortField = field; this.sortOrder = order; return this; }
618
+ limit(n) { this.limitCount = n; return this; }
619
+ fields(f) { this.selectedFields = f; return this; }
620
+ enhanceWith(e) { this.enhancers.push(e); return this; }
621
+ groupByFolder() { this._groupByFolder = true; return this; }
622
+ get() {
623
+ let results = [...this.items];
624
+ for (const filter of this.filters) results = results.filter(filter);
625
+ if (this.sortField) {
626
+ results.sort((a, b) => {
627
+ const valA = a[this.sortField];
628
+ const valB = b[this.sortField];
629
+ if (valA < valB) return this.sortOrder === 'asc' ? -1 : 1;
630
+ if (valA > valB) return this.sortOrder === 'asc' ? 1 : -1;
631
+ return 0;
632
+ });
633
+ }
634
+ if (this.limitCount !== null) results = results.slice(0, this.limitCount);
635
+
636
+ // Apply enhancers synchronously if possible
637
+ if (this.enhancers.length > 0) {
638
+ results = results.map(item => {
639
+ let enrichedItem = Object.assign({}, item);
640
+ for (const enhancer of this.enhancers) {
641
+ if (typeof enhancer === 'string') {
642
+ const fn = builtInEnhancers[enhancer];
643
+ if (fn) enrichedItem = fn(enrichedItem);
644
+ } else if (typeof enhancer === 'function') {
645
+ enrichedItem = enhancer(enrichedItem);
646
+ }
647
+ }
648
+ return enrichedItem;
649
+ });
650
+ }
651
+
652
+ if (this.selectedFields) {
653
+ results = results.map(item => {
654
+ const newItem = {};
655
+ this.selectedFields.forEach(f => { newItem[f] = item[f]; });
656
+ return newItem;
657
+ });
658
+ }
659
+
660
+ // Group by folder if requested
661
+ if (this._groupByFolder) {
662
+ const groups = {};
663
+ const groupOrder = [];
664
+ for (const item of results) {
665
+ // Extract folder from slug (e.g., "getting-started/installation" -> "getting-started")
666
+ const slug = item.slug || item.id || '';
667
+ const parts = slug.split('/');
668
+ const folder = parts.length > 1 ? parts[0] : 'root';
669
+
670
+ if (!groups[folder]) {
671
+ groups[folder] = {
672
+ id: folder,
673
+ title: folder.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
674
+ items: []
675
+ };
676
+ groupOrder.push(folder);
677
+ }
678
+ groups[folder].items.push(item);
679
+ }
680
+ return groupOrder.map(f => groups[f]);
681
+ }
682
+
683
+ return results;
684
+ }
685
+ }
686
+
687
+ function defineSchema(name, schema) { schemaRegistry.set(name, schema); }
688
+
689
+ function zenCollection(collectionName) {
690
+ // Access plugin data from the neutral envelope
691
+ // Content plugin stores all items under 'content' namespace
692
+ const pluginData = global.__ZENITH_PLUGIN_DATA__ || {};
693
+ const contentItems = pluginData.content || [];
694
+
695
+ // Filter by collection name (plugin owns data structure, runtime just filters)
696
+ const data = Array.isArray(contentItems)
697
+ ? contentItems.filter(item => item && item.collection === collectionName)
698
+ : [];
699
+
700
+ return new ZenCollection(data);
701
+ }
702
+
703
+ // ============================================
704
+ // useZenOrder - Documentation ordering & navigation
705
+ // ============================================
706
+
707
+ function slugify(text) {
708
+ return String(text || '')
709
+ .toLowerCase()
710
+ .replace(/[^\\w\\s-]/g, '')
711
+ .replace(/\\s+/g, '-')
712
+ .replace(/-+/g, '-')
713
+ .trim();
714
+ }
715
+
716
+ function getDocSlug(doc) {
717
+ const slugOrId = String(doc.slug || doc.id || '');
718
+ const parts = slugOrId.split('/');
719
+ const filename = parts[parts.length - 1];
720
+ return filename ? slugify(filename) : slugify(doc.title || 'untitled');
721
+ }
722
+
723
+ function processRawSections(rawSections) {
724
+ const sections = (rawSections || []).map(function(rawSection) {
725
+ const sectionSlug = slugify(rawSection.title || rawSection.id || 'section');
726
+ const items = (rawSection.items || []).map(function(item) {
727
+ return Object.assign({}, item, {
728
+ slug: getDocSlug(item),
729
+ sectionSlug: sectionSlug,
730
+ isIntro: item.intro === true || (item.tags && item.tags.includes && item.tags.includes('intro'))
731
+ });
732
+ });
733
+
734
+ // Sort items: intro first, then order, then alphabetical
735
+ items.sort(function(a, b) {
736
+ if (a.isIntro && !b.isIntro) return -1;
737
+ if (!a.isIntro && b.isIntro) return 1;
738
+ if (a.order !== undefined && b.order !== undefined) return a.order - b.order;
739
+ if (a.order !== undefined) return -1;
740
+ if (b.order !== undefined) return 1;
741
+ return (a.title || '').localeCompare(b.title || '');
742
+ });
743
+
744
+ return {
745
+ id: rawSection.id || sectionSlug,
746
+ title: rawSection.title || 'Untitled',
747
+ slug: sectionSlug,
748
+ order: rawSection.order !== undefined ? rawSection.order : (rawSection.meta && rawSection.meta.order),
749
+ hasIntro: items.some(function(i) { return i.isIntro; }),
750
+ items: items
751
+ };
752
+ });
753
+
754
+ // Sort sections: order → hasIntro → alphabetical
755
+ sections.sort(function(a, b) {
756
+ if (a.order !== undefined && b.order !== undefined) return a.order - b.order;
757
+ if (a.order !== undefined) return -1;
758
+ if (b.order !== undefined) return 1;
759
+ if (a.hasIntro && !b.hasIntro) return -1;
760
+ if (!a.hasIntro && b.hasIntro) return 1;
761
+ return a.title.localeCompare(b.title);
762
+ });
763
+
764
+ return sections;
765
+ }
766
+
767
+ function createZenOrder(rawSections) {
768
+ const sections = processRawSections(rawSections);
769
+
770
+ return {
771
+ sections: sections,
772
+ selectedSection: sections[0] || null,
773
+ selectedDoc: sections[0] && sections[0].items[0] || null,
774
+
775
+ getSectionBySlug: function(sectionSlug) {
776
+ return sections.find(function(s) { return s.slug === sectionSlug; }) || null;
777
+ },
778
+
779
+ getDocBySlug: function(sectionSlug, docSlug) {
780
+ var section = sections.find(function(s) { return s.slug === sectionSlug; });
781
+ if (!section) return null;
782
+ return section.items.find(function(d) { return d.slug === docSlug; }) || null;
783
+ },
784
+
785
+ getNextDoc: function(currentDoc) {
786
+ if (!currentDoc) return null;
787
+ var currentSection = sections.find(function(s) { return s.slug === currentDoc.sectionSlug; });
788
+ if (!currentSection) return null;
789
+ var idx = currentSection.items.findIndex(function(d) { return d.slug === currentDoc.slug; });
790
+ if (idx < currentSection.items.length - 1) return currentSection.items[idx + 1];
791
+ var secIdx = sections.findIndex(function(s) { return s.slug === currentSection.slug; });
792
+ if (secIdx < sections.length - 1) return sections[secIdx + 1].items[0] || null;
793
+ return null;
794
+ },
795
+
796
+ getPrevDoc: function(currentDoc) {
797
+ if (!currentDoc) return null;
798
+ var currentSection = sections.find(function(s) { return s.slug === currentDoc.sectionSlug; });
799
+ if (!currentSection) return null;
800
+ var idx = currentSection.items.findIndex(function(d) { return d.slug === currentDoc.slug; });
801
+ if (idx > 0) return currentSection.items[idx - 1];
802
+ var secIdx = sections.findIndex(function(s) { return s.slug === currentSection.slug; });
803
+ if (secIdx > 0) {
804
+ var prev = sections[secIdx - 1];
805
+ return prev.items[prev.items.length - 1] || null;
806
+ }
807
+ return null;
808
+ },
809
+
810
+ buildDocUrl: function(sectionSlug, docSlug) {
811
+ if (!docSlug || docSlug === 'index') return '/documentation/' + sectionSlug;
812
+ return '/documentation/' + sectionSlug + '/' + docSlug;
813
+ }
814
+ };
815
+ }
816
+
817
+ // ============================================
818
+ // Virtual DOM Helper for JSX-style expressions
819
+ // ============================================
820
+
821
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
822
+ const SVG_TAGS = new Set([
823
+ 'svg', 'path', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'rect',
824
+ 'g', 'defs', 'clipPath', 'mask', 'use', 'symbol', 'text', 'tspan', 'textPath',
825
+ 'image', 'foreignObject', 'switch', 'desc', 'title', 'metadata', 'linearGradient',
826
+ 'radialGradient', 'stop', 'pattern', 'filter', 'feBlend', 'feColorMatrix',
827
+ 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting',
828
+ 'feDisplacementMap', 'feFlood', 'feGaussianBlur', 'feImage', 'feMerge',
829
+ 'feMergeNode', 'feMorphology', 'feOffset', 'feSpecularLighting', 'feTile',
830
+ 'feTurbulence', 'animate', 'animateMotion', 'animateTransform', 'set', 'marker'
831
+ ]);
832
+
833
+ // Track current namespace context for nested elements
834
+ let currentNamespace = null;
835
+
836
+ function h(tag, props, children) {
837
+ // Determine if this element should be in SVG namespace
838
+ const isSvgTag = SVG_TAGS.has(tag) || SVG_TAGS.has(tag.toLowerCase());
839
+ const useSvgNamespace = isSvgTag || currentNamespace === SVG_NAMESPACE;
840
+
841
+ // Create element with correct namespace
842
+ const el = useSvgNamespace
843
+ ? document.createElementNS(SVG_NAMESPACE, tag)
844
+ : document.createElement(tag);
845
+
846
+ // Track namespace context for children (save previous, set new if svg root)
847
+ const previousNamespace = currentNamespace;
848
+ if (tag === 'svg' || tag === 'SVG') {
849
+ currentNamespace = SVG_NAMESPACE;
850
+ }
851
+
852
+ if (props) {
853
+ // Helper to set class for both HTML and SVG elements
854
+ const setClass = (element, value) => {
855
+ if (useSvgNamespace && 'className' in element && typeof element.className === 'object') {
856
+ element.className.baseVal = String(value || '');
857
+ } else {
858
+ element.className = String(value || '');
859
+ }
860
+ };
861
+
862
+ for (const [key, value] of Object.entries(props)) {
863
+ if (key.startsWith('on') && typeof value === 'function') {
864
+ el.addEventListener(key.slice(2).toLowerCase(), value);
865
+ } else if (key === 'class' || key === 'className') {
866
+ if (typeof value === 'function') {
867
+ // Reactive class binding
868
+ zenEffect(function() {
869
+ setClass(el, value());
870
+ });
871
+ } else {
872
+ setClass(el, value);
873
+ }
874
+ } else if (key === 'style' && typeof value === 'object') {
875
+ Object.assign(el.style, value);
876
+ } else if (typeof value === 'function') {
877
+ // Reactive attribute binding
878
+ zenEffect(function() {
879
+ const v = value();
880
+ if (v != null && v !== false) {
881
+ el.setAttribute(key, String(v));
882
+ } else {
883
+ el.removeAttribute(key);
884
+ }
885
+ });
886
+ } else if (value != null && value !== false) {
887
+ el.setAttribute(key, String(value));
888
+ }
889
+ }
890
+ }
891
+
892
+ if (children != null && children !== false) {
893
+ // Flatten nested arrays (from .map() calls)
894
+ const childrenArray = Array.isArray(children) ? children.flat(Infinity) : [children];
895
+ for (const child of childrenArray) {
896
+ // Skip null, undefined, and false
897
+ if (child == null || child === false) continue;
898
+
899
+ if (typeof child === 'function') {
900
+ // Reactive child - use a placeholder and update reactively
901
+ const placeholder = document.createComment('expr');
902
+ el.appendChild(placeholder);
903
+ let currentNodes = [];
904
+
905
+ zenEffect(function() {
906
+ const result = child();
907
+ // Remove old nodes
908
+ for (let i = 0; i < currentNodes.length; i++) {
909
+ const n = currentNodes[i];
910
+ if (n.parentNode) n.parentNode.removeChild(n);
911
+ }
912
+ currentNodes = [];
913
+
914
+ if (result == null || result === false) {
915
+ // Render nothing
916
+ } else if (result instanceof Node) {
917
+ placeholder.parentNode.insertBefore(result, placeholder);
918
+ currentNodes = [result];
919
+ } else if (Array.isArray(result)) {
920
+ // Array of nodes/strings
921
+ const flat = result.flat(Infinity);
922
+ for (let i = 0; i < flat.length; i++) {
923
+ const item = flat[i];
924
+ if (item == null || item === false) continue;
925
+ const node = item instanceof Node ? item : document.createTextNode(String(item));
926
+ placeholder.parentNode.insertBefore(node, placeholder);
927
+ currentNodes.push(node);
928
+ }
929
+ } else {
930
+ // Primitive (string, number)
931
+ const textNode = document.createTextNode(String(result));
932
+ placeholder.parentNode.insertBefore(textNode, placeholder);
933
+ currentNodes = [textNode];
934
+ }
935
+ });
936
+ } else {
937
+ // Static child
938
+ el.appendChild(child instanceof Node ? child : document.createTextNode(String(child)));
939
+ }
940
+ }
941
+ }
942
+
943
+ // Restore previous namespace context
944
+ currentNamespace = previousNamespace;
945
+
946
+ return el;
947
+ }
948
+
949
+
950
+ // ============================================
951
+ // Export to window.__zenith
952
+ // ============================================
953
+
954
+ global.__zenith = Object.assign(global.__zenith || {}, {
955
+ // Reactivity primitives (from hydration_runtime.js via global)
956
+ signal: global.zenSignal,
957
+ state: global.zenState,
958
+ effect: global.zenEffect,
959
+ memo: global.zenMemo,
960
+ ref: global.zenRef,
961
+ batch: global.zenBatch,
962
+ untrack: global.zenUntrack,
963
+ // zenith:content
964
+ defineSchema: defineSchema,
965
+ zenCollection: zenCollection,
966
+ // useZenOrder hook
967
+ createZenOrder: createZenOrder,
968
+ processRawSections: processRawSections,
969
+ slugify: slugify,
970
+ // Virtual DOM helper for JSX
971
+ h: h,
972
+ // Lifecycle
973
+ onMount: zenOnMount,
974
+ onUnmount: zenOnUnmount,
975
+ // Internal hooks
976
+ triggerMount: triggerMount,
977
+ triggerUnmount: triggerUnmount,
978
+ // Hydration
979
+ hydrate: zenithHydrate,
980
+ hydrateComponents: hydrateComponents, // Marker-driven component instantiation
981
+ registerExpression: registerExpression,
982
+ getExpression: getExpression,
983
+ // Component instance system
984
+ createInstance: createComponentInstance,
985
+ defineComponent: defineComponent,
986
+ instantiate: instantiateComponent
987
+ });
988
+
989
+ // NOTE: zen* primitives (zenSignal, zenState, zenEffect, etc.) are already
990
+ // assigned to global by hydration_runtime.js IIFE - no need to reassign here.
991
+
992
+ // Clean aliases (point to already-assigned globals)
993
+ global.signal = global.zenSignal;
994
+ global.state = global.zenState;
995
+ global.effect = global.zenEffect;
996
+ global.memo = global.zenMemo;
997
+ global.ref = global.zenRef;
998
+ global.batch = global.zenBatch;
999
+ global.untrack = global.zenUntrack;
1000
+ global.onMount = global.zenOnMount;
1001
+ global.onUnmount = global.zenOnUnmount;
1002
+
1003
+ // useZenOrder hook exports
1004
+ global.createZenOrder = createZenOrder;
1005
+ global.processRawSections = processRawSections;
1006
+ global.slugify = slugify;
1007
+
1008
+ // zenith:content exports
1009
+ global.zenCollection = zenCollection;
1010
+ global.ZenCollection = ZenCollection;
1011
+
1012
+ // ============================================
1013
+ // SPA Router Runtime
1014
+ // ============================================
1015
+
1016
+ // Router state
1017
+ // Current route state
1018
+ var currentRoute = {
1019
+ path: '/',
1020
+ params: {},
1021
+ query: {}
1022
+ };
1023
+
1024
+ // Route listeners
1025
+ var routeListeners = new Set();
1026
+
1027
+ // Router outlet element
1028
+ var routerOutlet = null;
1029
+
1030
+ // Page modules registry
1031
+ var pageModules = {};
1032
+
1033
+ // Route manifest
1034
+ var routeManifest = [];
1035
+
1036
+ /**
1037
+ * Parse query string
1038
+ */
1039
+ function parseQueryString(search) {
1040
+ var query = {};
1041
+ if (!search || search === '?') return query;
1042
+ var params = new URLSearchParams(search);
1043
+ params.forEach(function(value, key) { query[key] = value; });
1044
+ return query;
1045
+ }
1046
+
1047
+ // ============================================
1048
+ // Global Error Listeners (Dev Mode)
1049
+ // ============================================
1050
+
1051
+ window.onerror = function(message, source, lineno, colno, error) {
1052
+ renderErrorPage(error || new Error(message), {
1053
+ file: source,
1054
+ line: lineno,
1055
+ column: colno,
1056
+ errorType: 'UncaughtError'
1057
+ });
1058
+ return false;
1059
+ };
1060
+
1061
+ window.onunhandledrejection = function(event) {
1062
+ renderErrorPage(event.reason || new Error('Unhandled Promise Rejection'), {
1063
+ errorType: 'UnhandledRejection'
1064
+ });
1065
+ };
1066
+
1067
+ /**
1068
+ * Resolve route from pathname
1069
+ */
1070
+ function resolveRoute(pathname) {
1071
+ var normalizedPath = pathname === '' ? '/' : pathname;
1072
+
1073
+ for (var i = 0; i < routeManifest.length; i++) {
1074
+ var route = routeManifest[i];
1075
+ var match = route.regex.exec(normalizedPath);
1076
+ if (match) {
1077
+ var params = {};
1078
+ for (var j = 0; j < route.paramNames.length; j++) {
1079
+ var paramValue = match[j + 1];
1080
+ if (paramValue !== undefined) {
1081
+ params[route.paramNames[j]] = decodeURIComponent(paramValue);
1082
+ }
1083
+ }
1084
+ return { record: route, params: params };
1085
+ }
1086
+ }
1087
+ return null;
1088
+ }
1089
+
1090
+ /**
1091
+ * Clean up previous page
1092
+ */
1093
+ function cleanupPreviousPage() {
1094
+ // Trigger unmount lifecycle hooks
1095
+ if (global.__zenith && global.__zenith.triggerUnmount) {
1096
+ global.__zenith.triggerUnmount();
1097
+ }
1098
+
1099
+ // Remove previous page styles
1100
+ var prevStyles = document.querySelectorAll('style[data-zen-page-style]');
1101
+ prevStyles.forEach(function(s) { s.remove(); });
1102
+
1103
+ // Clean up window properties
1104
+ if (global.__zenith_cleanup) {
1105
+ global.__zenith_cleanup.forEach(function(key) {
1106
+ try { delete global[key]; } catch(e) {}
1107
+ });
1108
+ }
1109
+ global.__zenith_cleanup = [];
1110
+ }
1111
+
1112
+ /**
1113
+ * Inject styles
1114
+ */
1115
+ function injectStyles(styles) {
1116
+ styles.forEach(function(content, i) {
1117
+ var style = document.createElement('style');
1118
+ style.setAttribute('data-zen-page-style', String(i));
1119
+ style.textContent = content;
1120
+ document.head.appendChild(style);
1121
+ });
1122
+ }
1123
+
1124
+ /**
1125
+ * Execute scripts
1126
+ */
1127
+ function executeScripts(scripts) {
1128
+ scripts.forEach(function(content) {
1129
+ try {
1130
+ var fn = new Function(content);
1131
+ fn();
1132
+ } catch (e) {
1133
+ console.error('[Zenith Router] Script error:', e);
1134
+ }
1135
+ });
1136
+ }
1137
+
1138
+ /**
1139
+ * Render page
1140
+ */
1141
+ function renderPage(pageModule) {
1142
+ if (!routerOutlet) {
1143
+ console.warn('[Zenith Router] No router outlet');
1144
+ return;
1145
+ }
1146
+
1147
+ cleanupPreviousPage();
1148
+ routerOutlet.innerHTML = pageModule.html;
1149
+ injectStyles(pageModule.styles);
1150
+ executeScripts(pageModule.scripts);
1151
+
1152
+ // Trigger mount lifecycle hooks after scripts are executed
1153
+ if (global.__zenith && global.__zenith.triggerMount) {
1154
+ global.__zenith.triggerMount();
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Notify listeners
1160
+ */
1161
+ function notifyListeners(route, prevRoute) {
1162
+ routeListeners.forEach(function(listener) {
1163
+ try { listener(route, prevRoute); } catch(e) {}
1164
+ });
1165
+ }
1166
+
1167
+ /**
1168
+ * Resolve and render
1169
+ */
1170
+ function resolveAndRender(path, query, updateHistory, replace) {
1171
+ replace = replace || false;
1172
+ var prevRoute = Object.assign({}, currentRoute);
1173
+ var resolved = resolveRoute(path);
1174
+
1175
+ if (resolved) {
1176
+ currentRoute = {
1177
+ path: path,
1178
+ params: resolved.params,
1179
+ query: query,
1180
+ matched: resolved.record
1181
+ };
1182
+
1183
+ var pageModule = pageModules[resolved.record.path];
1184
+ if (pageModule) {
1185
+ renderPage(pageModule);
1186
+ }
1187
+ } else {
1188
+ currentRoute = { path: path, params: {}, query: query, matched: undefined };
1189
+ console.warn('[Zenith Router] No route matched:', path);
1190
+
1191
+ // Render 404 if available
1192
+ if (routerOutlet) {
1193
+ routerOutlet.innerHTML = '<div style="padding: 2rem; text-align: center;"><h1>404</h1><p>Page not found</p></div>';
1194
+ }
1195
+ }
1196
+
1197
+ if (updateHistory) {
1198
+ var url = path + (Object.keys(query).length ? '?' + new URLSearchParams(query) : '');
1199
+ if (replace) {
1200
+ history.replaceState(null, '', url);
1201
+ } else {
1202
+ history.pushState(null, '', url);
1203
+ }
1204
+ }
1205
+
1206
+ notifyListeners(currentRoute, prevRoute);
1207
+ global.__zenith_route = currentRoute;
1208
+ }
1209
+
1210
+ /**
1211
+ * Handle popstate
1212
+ */
1213
+ function handlePopState() {
1214
+ resolveAndRender(
1215
+ location.pathname,
1216
+ parseQueryString(location.search),
1217
+ false,
1218
+ false
1219
+ );
1220
+ }
1221
+
1222
+ /**
1223
+ * Navigate (public API)
1224
+ */
1225
+ function navigate(to, options) {
1226
+ options = options || {};
1227
+ var path, query = {};
1228
+
1229
+ if (to.includes('?')) {
1230
+ var parts = to.split('?');
1231
+ path = parts[0];
1232
+ query = parseQueryString('?' + parts[1]);
1233
+ } else {
1234
+ path = to;
1235
+ }
1236
+
1237
+ if (!path.startsWith('/')) {
1238
+ var currentDir = currentRoute.path.split('/').slice(0, -1).join('/');
1239
+ path = currentDir + '/' + path;
1240
+ }
1241
+
1242
+ var normalizedPath = path === '' ? '/' : path;
1243
+ var currentPath = currentRoute.path === '' ? '/' : currentRoute.path;
1244
+ var isSamePath = normalizedPath === currentPath;
1245
+
1246
+ if (isSamePath && JSON.stringify(query) === JSON.stringify(currentRoute.query)) {
1247
+ return;
1248
+ }
1249
+
1250
+ // Dev mode: If no route manifest is loaded, use browser navigation
1251
+ // This allows ZenLink to work in dev server where pages are served fresh
1252
+ if (routeManifest.length === 0) {
1253
+ var url = normalizedPath + (Object.keys(query).length ? '?' + new URLSearchParams(query) : '');
1254
+ if (options.replace) {
1255
+ location.replace(url);
1256
+ } else {
1257
+ location.href = url;
1258
+ }
1259
+ return;
1260
+ }
1261
+
1262
+ resolveAndRender(path, query, true, options.replace || false);
1263
+ }
1264
+
1265
+ /**
1266
+ * Get current route
1267
+ */
1268
+ function getRoute() {
1269
+ return Object.assign({}, currentRoute);
1270
+ }
1271
+
1272
+ /**
1273
+ * Subscribe to route changes
1274
+ */
1275
+ function onRouteChange(listener) {
1276
+ routeListeners.add(listener);
1277
+ return function() { routeListeners.delete(listener); };
1278
+ }
1279
+
1280
+ /**
1281
+ * Check if path is active
1282
+ */
1283
+ function isActive(path, exact) {
1284
+ if (exact) return currentRoute.path === path;
1285
+ return currentRoute.path.startsWith(path);
1286
+ }
1287
+
1288
+ /**
1289
+ * Prefetch a route
1290
+ */
1291
+ var prefetchedRoutes = new Set();
1292
+ function prefetch(path) {
1293
+ var normalizedPath = path === '' ? '/' : path;
1294
+
1295
+ if (prefetchedRoutes.has(normalizedPath)) {
1296
+ return Promise.resolve();
1297
+ }
1298
+ prefetchedRoutes.add(normalizedPath);
1299
+
1300
+ var resolved = resolveRoute(normalizedPath);
1301
+ if (!resolved) {
1302
+ return Promise.resolve();
1303
+ }
1304
+
1305
+ // In SPA build, all modules are already loaded
1306
+ return Promise.resolve();
1307
+ }
1308
+
1309
+ /**
1310
+ * Initialize router
1311
+ */
1312
+ function initRouter(manifest, modules, outlet) {
1313
+ routeManifest = manifest;
1314
+ Object.assign(pageModules, modules);
1315
+
1316
+ if (outlet) {
1317
+ routerOutlet = typeof outlet === 'string'
1318
+ ? document.querySelector(outlet)
1319
+ : outlet;
1320
+ }
1321
+
1322
+ window.addEventListener('popstate', handlePopState);
1323
+
1324
+ // Initial route resolution
1325
+ resolveAndRender(
1326
+ location.pathname,
1327
+ parseQueryString(location.search),
1328
+ false
1329
+ );
1330
+ }
1331
+
1332
+ // Expose router API globally
1333
+ global.__zenith_router = {
1334
+ navigate: navigate,
1335
+ getRoute: getRoute,
1336
+ onRouteChange: onRouteChange,
1337
+ isActive: isActive,
1338
+ prefetch: prefetch,
1339
+ initRouter: initRouter
1340
+ };
1341
+
1342
+ // Also expose navigate directly for convenience
1343
+ global.navigate = navigate;
1344
+ global.zenCollection = zenCollection;
1345
+
1346
+
1347
+ // ============================================
1348
+ // HMR Client (Development Only)
1349
+ // ============================================
1350
+
1351
+ if (typeof window !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1')) {
1352
+ let socket;
1353
+ function connectHMR() {
1354
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1355
+ socket = new WebSocket(protocol + '//' + location.host + '/hmr');
1356
+
1357
+ socket.onmessage = function(event) {
1358
+ try {
1359
+ const data = JSON.parse(event.data);
1360
+ if (data.type === 'reload') {
1361
+ console.log('[Zenith] HMR: Reloading page...');
1362
+ location.reload();
1363
+ } else if (data.type === 'style-update') {
1364
+ console.log('[Zenith] HMR: Updating style ' + data.url);
1365
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
1366
+ for (let i = 0; i < links.length; i++) {
1367
+ const link = links[i];
1368
+ const url = new URL(link.href);
1369
+ if (url.pathname === data.url) {
1370
+ link.href = data.url + '?t=' + Date.now();
1371
+ break;
1372
+ }
1373
+ }
1374
+ }
1375
+ } catch (e) {
1376
+ console.error('[Zenith] HMR Error:', e);
1377
+ }
1378
+ };
1379
+
1380
+ socket.onclose = function() {
1381
+ console.log('[Zenith] HMR: Connection closed. Retrying in 2s...');
1382
+ setTimeout(connectHMR, 2000);
1383
+ };
1384
+ }
1385
+
1386
+ // Connect unless explicitly disabled
1387
+ if (!window.__ZENITH_NO_HMR__) {
1388
+ connectHMR();
1389
+ }
1390
+ }
1391
+
1392
+ })(typeof window !== 'undefined' ? window : this);
1393
+ `;
1394
+ }
1395
+ /**
1396
+ * Remove ESM export statements from bundled code
1397
+ * e.g. "export { a as b, c };" -> ""
1398
+ */
1399
+ /**
1400
+ * Transform ESM export statements to global assignments
1401
+ * e.g. "export { a as b, c };" -> "global.b = a; global.c = c;"
1402
+ */
1403
+ function transformExportsToGlobal(code) {
1404
+ // Regex to capture "export { ... };" content
1405
+ // Matches "export { a as b, c }"
1406
+ const exportPattern = /export\s*\{\s*([^}]+)\s*\};?/g;
1407
+ return code.replace(exportPattern, (match, content) => {
1408
+ const parts = content.split(',');
1409
+ const assignments = parts.map((part) => {
1410
+ const trimmed = part.trim();
1411
+ if (!trimmed)
1412
+ return '';
1413
+ // Check for "a as b"
1414
+ const asMatch = trimmed.match(/^(\S+)\s+as\s+(\S+)$/);
1415
+ if (asMatch) {
1416
+ return `global.${asMatch[2]} = ${asMatch[1]};`;
1417
+ }
1418
+ // Just "a"
1419
+ return `global.${trimmed} = ${trimmed};`;
1420
+ });
1421
+ return assignments.join('\n');
1422
+ });
1423
+ }
1424
+ /**
1425
+ * Generate a minified version of the bundle
1426
+ * For production builds
1427
+ */
1428
+ export function generateMinifiedBundleJS() {
1429
+ // For now, return non-minified
1430
+ // TODO: Add minification via terser or similar
1431
+ return generateBundleJS();
1432
+ }
1433
+ /**
1434
+ * Get bundle version for cache busting
1435
+ */
1436
+ export function getBundleVersion() {
1437
+ return '0.1.0';
1438
+ }