@zenithbuild/core 1.2.2 → 1.2.4

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 (83) 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 +94 -74
  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 +1 -20118
  23. package/dist/zen-dev.js +1 -20118
  24. package/dist/zen-preview.js +1 -20118
  25. package/dist/zenith.js +1 -20118
  26. package/package.json +11 -20
  27. package/compiler/README.md +0 -380
  28. package/compiler/build-analyzer.ts +0 -122
  29. package/compiler/css/index.ts +0 -317
  30. package/compiler/discovery/componentDiscovery.ts +0 -242
  31. package/compiler/discovery/layouts.ts +0 -70
  32. package/compiler/errors/compilerError.ts +0 -56
  33. package/compiler/finalize/finalizeOutput.ts +0 -192
  34. package/compiler/finalize/generateFinalBundle.ts +0 -82
  35. package/compiler/index.ts +0 -83
  36. package/compiler/ir/types.ts +0 -174
  37. package/compiler/output/types.ts +0 -48
  38. package/compiler/parse/detectMapExpressions.ts +0 -102
  39. package/compiler/parse/importTypes.ts +0 -78
  40. package/compiler/parse/parseImports.ts +0 -309
  41. package/compiler/parse/parseScript.ts +0 -46
  42. package/compiler/parse/parseTemplate.ts +0 -628
  43. package/compiler/parse/parseZenFile.ts +0 -66
  44. package/compiler/parse/scriptAnalysis.ts +0 -91
  45. package/compiler/parse/trackLoopContext.ts +0 -82
  46. package/compiler/runtime/dataExposure.ts +0 -332
  47. package/compiler/runtime/generateDOM.ts +0 -255
  48. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  49. package/compiler/runtime/hydration.ts +0 -309
  50. package/compiler/runtime/navigation.ts +0 -432
  51. package/compiler/runtime/thinRuntime.ts +0 -160
  52. package/compiler/runtime/transformIR.ts +0 -406
  53. package/compiler/runtime/wrapExpression.ts +0 -114
  54. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -97
  55. package/compiler/spa-build.ts +0 -917
  56. package/compiler/ssg-build.ts +0 -486
  57. package/compiler/test/component-stacking.test.ts +0 -365
  58. package/compiler/test/map-lowering.test.ts +0 -130
  59. package/compiler/test/validate-test.ts +0 -104
  60. package/compiler/transform/classifyExpression.ts +0 -444
  61. package/compiler/transform/componentResolver.ts +0 -350
  62. package/compiler/transform/componentScriptTransformer.ts +0 -303
  63. package/compiler/transform/expressionTransformer.ts +0 -385
  64. package/compiler/transform/fragmentLowering.ts +0 -819
  65. package/compiler/transform/generateBindings.ts +0 -68
  66. package/compiler/transform/generateHTML.ts +0 -28
  67. package/compiler/transform/layoutProcessor.ts +0 -132
  68. package/compiler/transform/slotResolver.ts +0 -292
  69. package/compiler/transform/transformNode.ts +0 -314
  70. package/compiler/transform/transformTemplate.ts +0 -38
  71. package/compiler/validate/invariants.ts +0 -292
  72. package/compiler/validate/validateExpressions.ts +0 -168
  73. package/core/config/index.ts +0 -18
  74. package/core/config/loader.ts +0 -69
  75. package/core/config/types.ts +0 -119
  76. package/core/plugins/bridge.ts +0 -193
  77. package/core/plugins/index.ts +0 -7
  78. package/core/plugins/registry.ts +0 -126
  79. package/dist/cli.js +0 -11675
  80. package/runtime/build.ts +0 -17
  81. package/runtime/bundle-generator.ts +0 -1266
  82. package/runtime/client-runtime.ts +0 -891
  83. package/runtime/serve.ts +0 -93
@@ -1,891 +0,0 @@
1
- /**
2
- * Zenith Client Runtime
3
- *
4
- * Shared runtime module served as /runtime.js in dev mode.
5
- * Includes:
6
- * - Reactivity primitives (signal, state, effect, memo)
7
- * - Lifecycle hooks (zenOnMount, zenOnUnmount)
8
- * - Event wiring
9
- * - Hydration functions
10
- *
11
- * This is a standalone module that can be imported/served separately
12
- * from page-specific code.
13
- */
14
-
15
- // ============================================
16
- // Dependency Tracking System
17
- // ============================================
18
-
19
- let currentEffect: any = null;
20
- const effectStack: any[] = [];
21
- let batchDepth = 0;
22
- const pendingEffects = new Set<any>();
23
-
24
- function pushContext(effect: any) {
25
- effectStack.push(currentEffect);
26
- currentEffect = effect;
27
- }
28
-
29
- function popContext() {
30
- currentEffect = effectStack.pop() || null;
31
- }
32
-
33
- function trackDependency(subscribers: Set<any>) {
34
- if (currentEffect) {
35
- subscribers.add(currentEffect);
36
- currentEffect.dependencies.add(subscribers);
37
- }
38
- }
39
-
40
- function notifySubscribers(subscribers: Set<any>) {
41
- const effects = [...subscribers];
42
- for (const effect of effects) {
43
- if (batchDepth > 0) {
44
- pendingEffects.add(effect);
45
- } else {
46
- effect.run();
47
- }
48
- }
49
- }
50
-
51
- function cleanupEffect(effect: any) {
52
- for (const deps of effect.dependencies) {
53
- deps.delete(effect);
54
- }
55
- effect.dependencies.clear();
56
- }
57
-
58
- // ============================================
59
- // zenSignal - Atomic reactive value
60
- // ============================================
61
-
62
- export function zenSignal<T>(initialValue: T): (newValue?: T) => T {
63
- let value = initialValue;
64
- const subscribers = new Set<any>();
65
-
66
- function signal(newValue?: T): T {
67
- if (arguments.length === 0) {
68
- trackDependency(subscribers);
69
- return value;
70
- }
71
- if (newValue !== value) {
72
- value = newValue as T;
73
- notifySubscribers(subscribers);
74
- }
75
- return value;
76
- }
77
-
78
- return signal;
79
- }
80
-
81
- // ============================================
82
- // zenState - Deep reactive object with Proxy
83
- // ============================================
84
-
85
- export function zenState<T extends object>(initialObj: T): T {
86
- const subscribers = new Map<string, Set<any>>();
87
-
88
- function getSubscribers(path: string): Set<any> {
89
- if (!subscribers.has(path)) {
90
- subscribers.set(path, new Set());
91
- }
92
- return subscribers.get(path)!;
93
- }
94
-
95
- function createProxy(obj: any, parentPath: string = ''): any {
96
- if (obj === null || typeof obj !== 'object') {
97
- return obj;
98
- }
99
-
100
- return new Proxy(obj, {
101
- get(target, prop) {
102
- if (typeof prop === 'symbol') return target[prop];
103
-
104
- const path = parentPath ? `${parentPath}.${String(prop)}` : String(prop);
105
- trackDependency(getSubscribers(path));
106
-
107
- const value = target[prop];
108
- if (value !== null && typeof value === 'object') {
109
- return createProxy(value, path);
110
- }
111
- return value;
112
- },
113
-
114
- set(target, prop, newValue) {
115
- if (typeof prop === 'symbol') {
116
- target[prop] = newValue;
117
- return true;
118
- }
119
-
120
- const path = parentPath ? `${parentPath}.${String(prop)}` : String(prop);
121
- const oldValue = target[prop];
122
-
123
- if (oldValue !== newValue) {
124
- target[prop] = newValue;
125
-
126
- // Notify this path
127
- const subs = subscribers.get(path);
128
- if (subs) notifySubscribers(subs);
129
-
130
- // Notify parent paths
131
- const parts = path.split('.');
132
- for (let i = parts.length - 1; i >= 0; i--) {
133
- const parentPath = parts.slice(0, i).join('.');
134
- if (parentPath) {
135
- const parentSubs = subscribers.get(parentPath);
136
- if (parentSubs) notifySubscribers(parentSubs);
137
- }
138
- }
139
- }
140
- return true;
141
- }
142
- });
143
- }
144
-
145
- return createProxy(initialObj);
146
- }
147
-
148
- // ============================================
149
- // zenEffect - Reactive effect
150
- // ============================================
151
-
152
- export function zenEffect(fn: () => void | (() => void)): () => void {
153
- let cleanup: (() => void) | void;
154
-
155
- const effect = {
156
- dependencies: new Set<Set<any>>(),
157
- run() {
158
- cleanupEffect(effect);
159
- pushContext(effect);
160
- try {
161
- if (cleanup) cleanup();
162
- cleanup = fn();
163
- } finally {
164
- popContext();
165
- }
166
- }
167
- };
168
-
169
- effect.run();
170
-
171
- return () => {
172
- cleanupEffect(effect);
173
- if (cleanup) cleanup();
174
- };
175
- }
176
-
177
- // ============================================
178
- // zenMemo - Computed/derived value
179
- // ============================================
180
-
181
- export function zenMemo<T>(fn: () => T): () => T {
182
- let value: T;
183
- let dirty = true;
184
- const subscribers = new Set<any>();
185
-
186
- const effect = {
187
- dependencies: new Set<Set<any>>(),
188
- run() {
189
- cleanupEffect(effect);
190
- pushContext(effect);
191
- try {
192
- value = fn();
193
- dirty = false;
194
- notifySubscribers(subscribers);
195
- } finally {
196
- popContext();
197
- }
198
- }
199
- };
200
-
201
- return () => {
202
- trackDependency(subscribers);
203
- if (dirty) {
204
- effect.run();
205
- }
206
- return value;
207
- };
208
- }
209
-
210
- // ============================================
211
- // zenRef - Non-reactive mutable container
212
- // ============================================
213
-
214
- export function zenRef<T>(initialValue?: T): { current: T | null } {
215
- return { current: initialValue !== undefined ? initialValue : null };
216
- }
217
-
218
- // ============================================
219
- // zenBatch - Batch updates
220
- // ============================================
221
-
222
- export function zenBatch(fn: () => void): void {
223
- batchDepth++;
224
- try {
225
- fn();
226
- } finally {
227
- batchDepth--;
228
- if (batchDepth === 0) {
229
- const effects = [...pendingEffects];
230
- pendingEffects.clear();
231
- for (const effect of effects) {
232
- effect.run();
233
- }
234
- }
235
- }
236
- }
237
-
238
- // ============================================
239
- // zenUntrack - Read without tracking
240
- // ============================================
241
-
242
- export function zenUntrack<T>(fn: () => T): T {
243
- const prevEffect = currentEffect;
244
- currentEffect = null;
245
- try {
246
- return fn();
247
- } finally {
248
- currentEffect = prevEffect;
249
- }
250
- }
251
-
252
- // ============================================
253
- // Lifecycle Hooks
254
- // ============================================
255
-
256
- const mountCallbacks: Array<() => void | (() => void)> = [];
257
- const unmountCallbacks: Array<() => void> = [];
258
- let isMounted = false;
259
-
260
- export function zenOnMount(fn: () => void | (() => void)): void {
261
- if (isMounted) {
262
- const cleanup = fn();
263
- if (typeof cleanup === 'function') {
264
- unmountCallbacks.push(cleanup);
265
- }
266
- } else {
267
- mountCallbacks.push(fn);
268
- }
269
- }
270
-
271
- export function zenOnUnmount(fn: () => void): void {
272
- unmountCallbacks.push(fn);
273
- }
274
-
275
- export function triggerMount(): void {
276
- isMounted = true;
277
- for (const cb of mountCallbacks) {
278
- const cleanup = cb();
279
- if (typeof cleanup === 'function') {
280
- unmountCallbacks.push(cleanup);
281
- }
282
- }
283
- mountCallbacks.length = 0;
284
- }
285
-
286
- export function triggerUnmount(): void {
287
- isMounted = false;
288
- for (const cb of unmountCallbacks) {
289
- try { cb(); } catch (e) { console.error('[Zenith] Unmount error:', e); }
290
- }
291
- unmountCallbacks.length = 0;
292
- }
293
-
294
- // ============================================
295
- // Expression Registry
296
- // ============================================
297
-
298
- const expressionRegistry = new Map<string, (state: any) => any>();
299
-
300
- export function registerExpression(id: string, fn: (state: any) => any): void {
301
- expressionRegistry.set(id, fn);
302
- }
303
-
304
- export function getExpression(id: string): ((state: any) => any) | undefined {
305
- return expressionRegistry.get(id);
306
- }
307
-
308
- // ============================================
309
- // Hydration Functions
310
- // ============================================
311
-
312
- // ============================================
313
- // Hydration & Binding System
314
- // ============================================
315
-
316
- interface BaseBinding {
317
- node: Node;
318
- type: 'text' | 'attribute' | 'html' | 'conditional' | 'optional' | 'loop';
319
- id: string; // Binding/Expression ID
320
- }
321
-
322
- interface TextBinding extends BaseBinding {
323
- type: 'text';
324
- node: Text;
325
- }
326
-
327
- interface AttributeBinding extends BaseBinding {
328
- type: 'attribute';
329
- node: Element;
330
- attrName: string;
331
- }
332
-
333
- interface HtmlBinding extends BaseBinding {
334
- type: 'html';
335
- node: Element;
336
- }
337
-
338
- interface ConditionalBinding extends BaseBinding {
339
- type: 'conditional';
340
- node: Element; // The container div with data-zen-cond
341
- trueBranch?: Node[]; // Cached true branch nodes
342
- falseBranch?: Node[]; // Cached false branch nodes
343
- currentBranch: boolean | null; // true, false, or null (uninitialized)
344
- }
345
-
346
- interface OptionalBinding extends BaseBinding {
347
- type: 'optional';
348
- node: Element; // The container div with data-zen-opt
349
- content?: Node[]; // Cached content nodes
350
- isVisible: boolean | null;
351
- }
352
-
353
- interface LoopBinding extends BaseBinding {
354
- type: 'loop';
355
- node: Element; // The container div with data-zen-loop
356
- template: Node[]; // THe template nodes (cloned from initial content)
357
- items: any[]; // Current items list
358
- itemBindings: Binding[][]; // Bindings for each item
359
- itemVar: string;
360
- indexVar: string | null;
361
- sourceExpr: string;
362
- }
363
-
364
- type Binding = TextBinding | AttributeBinding | HtmlBinding | ConditionalBinding | OptionalBinding | LoopBinding;
365
-
366
- // Root bindings for the application
367
- const rootBindings: Binding[] = [];
368
-
369
- /**
370
- * Recursive Hydration (Tree Walker)
371
- *
372
- * Scans the DOM tree for bindings, respecting scope boundaries.
373
- * Returns a list of bindings found in this subtree (excluding those inside child scopes like loops).
374
- */
375
-
376
- /**
377
- * Manual recursive traversal to better handle skipping subtrees
378
- */
379
- function scanBindings(node: Node, bindings: Binding[]): void {
380
- if (node.nodeType === Node.ELEMENT_NODE) {
381
- const element = node as Element;
382
-
383
- // Check for Loop - Stop traversal into children if found
384
- if (element.hasAttribute('data-zen-loop')) {
385
- const id = element.getAttribute('data-zen-loop')!;
386
- const source = element.getAttribute('data-zen-source')!;
387
- const itemVar = element.getAttribute('data-zen-item')!;
388
- const indexVar = element.getAttribute('data-zen-index');
389
-
390
- // Capture template from initial content (SSR)
391
- // Note: For client-side nav, this might modify `element` immediately?
392
- // For now assume standard hydration of SSR content
393
- const template = Array.from(element.childNodes).map(n => n.cloneNode(true));
394
-
395
- // The loop binding itself manages the children.
396
- // We do NOT scan children here. The loop update() will scan/hydrate instances.
397
-
398
- // Clear initial SSR content so we can re-render fresh?
399
- // Or try to hydrate existing? Hydrating lists is hard (keys etc).
400
- // Simplest safe fix: Clear and re-render.
401
- element.innerHTML = '';
402
-
403
- bindings.push({
404
- type: 'loop',
405
- node: element,
406
- id,
407
- sourceExpr: source,
408
- itemVar,
409
- indexVar,
410
- template,
411
- items: [],
412
- itemBindings: []
413
- } as LoopBinding);
414
-
415
- return; // STOP recursion into loop children
416
- }
417
-
418
- // Check for Conditional - Stop recursion?
419
- // Actually, conditional blocks contain children we WANT to bind if they are currently visible.
420
- // But if we toggle, we need to re-scan.
421
- // So ConditionalBinding should manage its children.
422
- if (element.hasAttribute('data-zen-cond')) {
423
- const id = element.getAttribute('data-zen-cond')!;
424
- // We assume the true/false branches are initially present as separate containers
425
- // OR `data-zen-cond-true` / `data-zen-cond-false` markers?
426
- // The compiler output:
427
- // <div data_zen_cond="id" data_zen_cond_true style="display:contents">...</div>
428
- // <div data_zen_cond="id" data_zen_cond_false style="display:none">...</div>
429
- // These are siblings usually? Or nested?
430
- // Wait, compiler generates TWO divs.
431
- // <div data-zen-cond="id" ...true> and <div data-zen-cond="id" ...false>
432
- // Each is a separate binding effectively?
433
- // If they share the same ID, they are part of the same logic.
434
- // Scanning will find both. We can treat them as independent toggle-able areas?
435
-
436
- // Actually, simpler to treat them as ConditionalBinding.
437
- // But we need to know which branch it is.
438
- const isTrueBranch = element.hasAttribute('data-zen-cond-true');
439
-
440
- // For now, let's treat them as distinct bindings that listen to the same expression
441
- // and toggle visibility.
442
- // We DO want to scan their children because they might be visible.
443
-
444
- // Optimization: If display:none, maybe don't scan yet?
445
- // But we want to be ready to show.
446
-
447
- // Let's implement ConditionalBinding to manage visibility AND recurse.
448
- bindings.push({
449
- type: 'conditional',
450
- node: element,
451
- id,
452
- currentBranch: null, // Force update
453
-
454
- } as any); // Simplification: Just toggle display
455
- // NO, wait. If we hide it, we shouldn't update its children's bindings if they depend on ephemeral state?
456
- // Actually, simplest is just toggle binding.
457
- // Let's rely on standard attribute binding?
458
- // No, standard attribute binding doesn't handle children.
459
-
460
- // Revisiting the compiler:
461
- // It's just a div with an ID. The logic is "If cond is true, show TrueDiv, hide FalseDiv".
462
- // So we can have a Binding that just toggles 'style.display'.
463
- // And we CONTINUE scanning children.
464
- }
465
-
466
- // Optional? Same.
467
-
468
- // Bind attributes
469
- const attrSelectors = ['class', 'style', 'src', 'href', 'disabled', 'checked', 'value', 'placeholder'];
470
- for (const attr of attrSelectors) {
471
- const attrKey = `data-zen-attr-${attr}`;
472
- if (element.hasAttribute(attrKey)) {
473
- bindings.push({
474
- type: 'attribute',
475
- node: element,
476
- id: element.getAttribute(attrKey)!,
477
- attrName: attr
478
- });
479
- }
480
- }
481
-
482
- // Bind HTML
483
- if (element.hasAttribute('data-zen-html')) {
484
- bindings.push({
485
- type: 'html',
486
- node: element,
487
- id: element.getAttribute('data-zen-html')!
488
- });
489
- }
490
-
491
- // Bind Events
492
- bindEventsHelpers(element);
493
- }
494
-
495
- // Check Text Nodes
496
- if (node.nodeType === Node.TEXT_NODE) {
497
- // Text nodes can't have attributes, so we look at parent?
498
- // No, Zenith compiler puts `data-zen-text="id"` on the PARENT element usually?
499
- // Or strictly on the element wrapping the text?
500
- // Compiler: <span data-zen-text="id"></span> or similar.
501
- // Wait, `transformNode` for text binding:
502
- // Returns `<!--binding:id-->`? No.
503
- // It returns a TextNode in IR. `generateBindings` validates "Text binding must have target 'data-zen-text'".
504
- // `transformNode`: if binding exists, it sets `data-zen-text` attribute on the element?
505
- // Wait, text nodes don't have attributes.
506
- // The compiler wraps text in a generic element or expects the parent?
507
- // Usually framework puts it on the parent element.
508
- // Let's check `parseTemplate.ts` or `transformNode.ts`...
509
- // Assuming the element with `data-zen-text` owns the text content.
510
- }
511
-
512
- // Check parent for Text Binding (on Element)
513
- if (node.nodeType === Node.ELEMENT_NODE) {
514
- const el = node as Element;
515
- if (el.hasAttribute('data-zen-text')) {
516
- bindings.push({
517
- type: 'text',
518
- node: el.firstChild as Text || el.appendChild(document.createTextNode('')), // Ensure text node exists
519
- id: el.getAttribute('data-zen-text')!
520
- } as TextBinding);
521
- // We don't skip children here, but usually text binding replaces content.
522
- }
523
- }
524
-
525
- // Recurse to children (unless stopped above)
526
- let child = node.firstChild;
527
- while (child) {
528
- scanBindings(child, bindings);
529
- child = child.nextSibling;
530
- }
531
- }
532
-
533
- function bindEventsHelpers(element: Element) {
534
- const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
535
- for (const eventType of eventTypes) {
536
- const attr = `data-zen-${eventType}`;
537
- if (element.hasAttribute(attr)) {
538
- // We attach handler immediately?
539
- // Handler needs to access runtime state.
540
- // We'll use a closure that calls the global/scoped handler at runtime.
541
- const handlerName = element.getAttribute(attr)!;
542
-
543
- // Remove old
544
- const oldHandler = (element as any)[`__zen_${eventType}`];
545
- if (oldHandler) element.removeEventListener(eventType, oldHandler);
546
-
547
- // Add new
548
- const handler = (e: Event) => dispatchEvent(e, handlerName, element);
549
- (element as any)[`__zen_${eventType}`] = handler;
550
- element.addEventListener(eventType, handler);
551
- }
552
- }
553
- }
554
-
555
- function dispatchEvent(event: Event, handlerName: string, element: Element) {
556
- // We need to find the SCOPE.
557
- // Event handler might be looking for `item` (loop variable).
558
- // This is tricky with standard DOM events.
559
- // We attach scope info to the DOM element?
560
- // When LoopBinding creates nodes, it should attach `_zen_scope` property to the root of the item?
561
-
562
- // Finding scope: traverse up to find an element with `_zen_scope`.
563
- let target = element as any;
564
- let scope = {};
565
-
566
- while (target) {
567
- if (target._zen_scope) {
568
- scope = { ...target._zen_scope, ...scope }; // Merge scopes (inner wins? No, inner is more specific)
569
- // Actually, prototype chain or Object.assign?
570
- // List items usually have unique props.
571
- // Let's accumulate.
572
- // For a simple hierarchy: Global < LoopItem.
573
- // We start matching from Global? No.
574
- // The handler execution needs the collected scope.
575
- break; // Assuming flat scope per loop item for now or merge?
576
- }
577
- target = target.parentNode;
578
- }
579
-
580
- // Global scopes
581
- // We don't easily have access to the *current* global state passed to update() here,
582
- // unless we store it globally.
583
- // `expressionRegistry` handlers take `state`.
584
- // We need to fetch the latest state?
585
- // Zenith signals usually read current value directly.
586
- // But `expression(state)` pattern implies pure function of state.
587
-
588
- // For events: usually they call a function. `increment()`.
589
- // That function access signals/state directly.
590
- // BUT if it uses arguments like `doSomething(item)`, `item` comes from scope.
591
-
592
- // Only expressions registered in registry need `state` passed?
593
- // If `handlerName` is in `__ZENITH_EXPRESSIONS__`, calling it requires state.
594
- // Which state? One with loop vars.
595
-
596
- // HACK: for now, we rely on the fact that most handlers are simple fn calls.
597
- // If they are expressions expecting args, we assume they are bound or don't need args?
598
- // Wait, `<button onclick="remove(item)">`.
599
- // Compiler lower this to an expression: `(state) => remove(state.item)`.
600
- // So we DO need to pass the scope-enriched state to the handler if it's an expression.
601
-
602
- const handlerFunc = (window as any)[handlerName] || (window as any).__ZENITH_EXPRESSIONS__?.get(handlerName);
603
-
604
- if (typeof handlerFunc === 'function') {
605
- // Determine effective state
606
- // We might need a global `getLastState()` or similar if strictly functional.
607
- // Or we pass `scope` (which has item) as the state?
608
- // If generic state is required, we are in trouble without a global reference.
609
- // Let's assume `window.__zenith_last_state` is available or similar?
610
- // Added `window.__zenith_currentState` in update().
611
-
612
- const globalState = (window as any).__zenith_currentState || {};
613
- const effectiveState = { ...globalState, ...scope };
614
-
615
- handlerFunc(event, element, effectiveState); // Pass state as 3rd arg? Or 1st?
616
- // Expression signature: (state) => ...
617
- // Event handler signature: (e, el) => ...
618
- // Conflict.
619
- // Zenith compiler usually generates `(state) => (e) => ...` or similar for events?
620
- // Or the expression IS the handler body?
621
- // If it's `(state) => ...`, we call it to get the result.
622
- // If result is function, call it with event?
623
-
624
- // Let's try calling with state. If it returns function, call that.
625
- try {
626
- const result = handlerFunc(effectiveState);
627
- if (typeof result === 'function') {
628
- result(event, element);
629
- }
630
- } catch (e) {
631
- // It might be a direct event handler (e) => ...
632
- handlerFunc(event, element);
633
- }
634
- }
635
- }
636
-
637
- export function hydrate(state: any, container?: Element | Document): void {
638
- const root = container || document;
639
- // Clear global bindings
640
- rootBindings.length = 0;
641
-
642
- // Store initial state for events
643
- (window as any).__zenith_currentState = state;
644
-
645
- scanBindings(root, rootBindings);
646
-
647
- // Initial update
648
- updateBindings(rootBindings, state);
649
- triggerMount();
650
- }
651
-
652
- /**
653
- * Update Loop Logic
654
- */
655
- function updateLoop(binding: LoopBinding, state: any) {
656
- const { node, id, sourceExpr, itemVar, indexVar, template } = binding;
657
-
658
- // 1. Evaluate Source
659
- const expr = expressionRegistry.get(sourceExpr);
660
- if (!expr) return;
661
-
662
- let list = [];
663
- try {
664
- list = expr(state) || [];
665
- } catch (e) { console.error('Loop source error', e); }
666
-
667
- if (!Array.isArray(list)) list = [];
668
-
669
- // 2. Reconcile (Naive: Clear and Re-render)
670
- // Optimization: Reuse existing items?
671
- // For Patch 1.1.0, let's keep it simple and safe: Full Re-render.
672
- // Ideally we diff.
673
-
674
- // Detect if nothing changed? (Deep equals or ref check)
675
- if (binding.items === list) {
676
- // Same reference, assumes no change?
677
- // In mutable state (Proxy), the array might be same ref but mutated.
678
- // We probably should re-render or at least re-update children.
679
- // Let's fall through.
680
- }
681
- binding.items = list;
682
-
683
- node.innerHTML = '';
684
- binding.itemBindings = []; // Clear child bindings
685
-
686
- list.forEach((item, index) => {
687
- // Create Scope
688
- const scope = { [itemVar]: item };
689
- if (indexVar) scope[indexVar] = index;
690
-
691
- // Clone Template
692
- const fragment = document.createDocumentFragment();
693
- template.forEach(n => fragment.appendChild(n.cloneNode(true)));
694
-
695
- // Mark Scope on Root Elements of Item
696
- // (Used for event delegation)
697
- Array.from(fragment.childNodes).forEach((child: any) => {
698
- if (child.nodeType === 1) child._zen_scope = scope;
699
- });
700
-
701
- // Hydrate this instance (collect bindings)
702
- const instanceBindings: Binding[] = [];
703
- // Scan fragment BEFORE appending? Or after?
704
- // Appending first is easier for traversal, but we want to bind to the specific nodes.
705
- scanBindings(fragment, instanceBindings);
706
- binding.itemBindings.push(instanceBindings);
707
-
708
- node.appendChild(fragment);
709
-
710
- // Initial Update for this instance
711
- updateBindings(instanceBindings, { ...state, ...scope });
712
- });
713
- }
714
-
715
- function updateBindings(bindingsList: Binding[], state: any) {
716
- for (const binding of bindingsList) {
717
- if (binding.type === 'text') {
718
- const expr = expressionRegistry.get(binding.id);
719
- if (expr) {
720
- try { binding.node.textContent = String(expr(state) ?? ''); }
721
- catch (e) { }
722
- }
723
- } else if (binding.type === 'attribute') {
724
- updateAttributeBinding(binding.node, binding.attrName, binding.id, state);
725
- } else if (binding.type === 'html') {
726
- const expr = expressionRegistry.get(binding.id);
727
- if (expr) {
728
- try {
729
- const val = expr(state);
730
- binding.node.innerHTML = String(val ?? '');
731
- } catch (e) { }
732
- }
733
- } else if (binding.type === 'conditional' || binding.type === 'optional') {
734
- // Toggle display based on truthiness
735
- const expr = expressionRegistry.get(binding.id);
736
- if (expr) {
737
- const val = !!expr(state);
738
- // Check if inverted (data-zen-cond-false)
739
- const isInverse = (binding.node as Element).hasAttribute('data-zen-cond-false');
740
- const shouldShow = isInverse ? !val : val;
741
-
742
- (binding.node as HTMLElement).style.display = shouldShow ? 'contents' : 'none';
743
- }
744
- } else if (binding.type === 'loop') {
745
- updateLoop(binding as LoopBinding, state);
746
- }
747
- }
748
- }
749
-
750
- export function update(state: any): void {
751
- (window as any).__zenith_currentState = state;
752
- updateBindings(rootBindings, state);
753
- }
754
-
755
- export function cleanup(container?: Element | Document): void {
756
- rootBindings.length = 0;
757
- rootBindings.length = 0;
758
- triggerUnmount();
759
- }
760
-
761
- // ... existing helper functions (updateAttributeBinding, bindEvents) need slight adjustments or imports?
762
- // We need to keep updateAttributeBinding available.
763
-
764
- // REUSING EXISTING updateAttributeBinding from previous implementation
765
- // (Need to make sure it's defined or moved)
766
-
767
- function updateAttributeBinding(element: Element, attrName: string, expressionId: string, state: any): void {
768
- const expression = expressionRegistry.get(expressionId);
769
- if (!expression) return;
770
-
771
- try {
772
- const result = expression(state);
773
-
774
- if (attrName === 'class' || attrName === 'className') {
775
- (element as HTMLElement).className = String(result ?? '');
776
- } else if (attrName === 'style' && typeof result === 'object') {
777
- const styleStr = Object.entries(result).map(([k, v]) => `${k}: ${v}`).join('; ');
778
- element.setAttribute('style', styleStr);
779
- } else if (['disabled', 'checked', 'readonly'].includes(attrName)) {
780
- if (result) {
781
- element.setAttribute(attrName, '');
782
- } else {
783
- element.removeAttribute(attrName);
784
- }
785
- } else {
786
- if (result === null || result === undefined || result === false) {
787
- element.removeAttribute(attrName);
788
- } else {
789
- element.setAttribute(attrName, String(result));
790
- }
791
- }
792
- } catch (error) {
793
- console.error(`[Zenith] Error updating attribute ${attrName}:`, error);
794
- }
795
- }
796
-
797
- export function bindEvents(container: Element | Document): void {
798
- // Legacy support or external call?
799
- // Our scanBindings() handles events.
800
- // If called manually, we traverse?
801
- if (container.nodeType === Node.ELEMENT_NODE) {
802
- bindEventsHelpers(container as Element);
803
- // And children...
804
- const all = (container as Element).querySelectorAll('*');
805
- all.forEach(el => bindEventsHelpers(el));
806
- }
807
- }
808
-
809
-
810
- // ============================================
811
- // Plugin Runtime Data Access
812
- // ============================================
813
-
814
- /**
815
- * Access plugin data from the neutral envelope
816
- *
817
- * Plugins use this to retrieve their data at runtime.
818
- * The CLI injected this data via window.__ZENITH_PLUGIN_DATA__
819
- *
820
- * @param namespace - Plugin namespace (e.g., 'content', 'router')
821
- * @returns The plugin's data, or undefined if not present
822
- */
823
- export function getPluginRuntimeData(namespace: string): unknown {
824
- if (typeof window === 'undefined') return undefined;
825
- const envelope = (window as any).__ZENITH_PLUGIN_DATA__;
826
- return envelope?.[namespace];
827
- }
828
-
829
- // ============================================
830
- // Browser Globals Setup
831
- // ============================================
832
-
833
- export function setupGlobals(): void {
834
- if (typeof window === 'undefined') return;
835
-
836
- const w = window as any;
837
-
838
- // Zenith namespace
839
- w.__zenith = {
840
- signal: zenSignal,
841
- state: zenState,
842
- effect: zenEffect,
843
- memo: zenMemo,
844
- ref: zenRef,
845
- batch: zenBatch,
846
- untrack: zenUntrack,
847
- onMount: zenOnMount,
848
- onUnmount: zenOnUnmount,
849
- triggerMount,
850
- triggerUnmount,
851
- getPluginData: getPluginRuntimeData // Access plugin data from envelope
852
- };
853
-
854
- // Expression registry
855
- w.__ZENITH_EXPRESSIONS__ = expressionRegistry;
856
-
857
- // Hydration functions
858
- w.__zenith_hydrate = hydrate;
859
- w.__zenith_update = update;
860
- w.__zenith_bindEvents = bindEvents;
861
- w.__zenith_cleanup = cleanup;
862
- w.zenithHydrate = hydrate;
863
- w.zenithUpdate = update;
864
- w.zenithBindEvents = bindEvents;
865
- w.zenithCleanup = cleanup;
866
-
867
- // Direct primitives
868
- w.zenSignal = zenSignal;
869
- w.zenState = zenState;
870
- w.zenEffect = zenEffect;
871
- w.zenMemo = zenMemo;
872
- w.zenRef = zenRef;
873
- w.zenBatch = zenBatch;
874
- w.zenUntrack = zenUntrack;
875
- w.zenOnMount = zenOnMount;
876
- w.zenOnUnmount = zenOnUnmount;
877
-
878
- // Short aliases
879
- w.signal = zenSignal;
880
- w.state = zenState;
881
- w.effect = zenEffect;
882
- w.memo = zenMemo;
883
- w.ref = zenRef;
884
- w.batch = zenBatch;
885
- w.untrack = zenUntrack;
886
- w.onMount = zenOnMount;
887
- w.onUnmount = zenOnUnmount;
888
- }
889
-
890
- // Auto-setup globals on import
891
- setupGlobals();