@zenithbuild/runtime 0.1.9 → 0.2.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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Thin Runtime
3
+ *
4
+ * Phase 8/9/10: Declarative runtime for DOM updates and event binding
5
+ *
6
+ * This runtime is purely declarative - it:
7
+ * - Updates DOM nodes by ID
8
+ * - Binds event handlers
9
+ * - Reacts to state changes
10
+ * - Does NOT parse templates or expressions
11
+ * - Does NOT use eval, new Function, or with(window)
12
+ */
13
+ /**
14
+ * Generate thin declarative runtime code
15
+ *
16
+ * This runtime is minimal and safe - it only:
17
+ * 1. Updates DOM nodes using pre-compiled expression functions
18
+ * 2. Binds event handlers by ID
19
+ * 3. Provides reactive state updates
20
+ *
21
+ * All expressions are pre-compiled at build time.
22
+ */
23
+ export function generateThinRuntime() {
24
+ return `
25
+ // Zenith Thin Runtime (Phase 8/9/10)
26
+ // Purely declarative - no template parsing, no eval, no with(window)
27
+
28
+ (function() {
29
+ 'use strict';
30
+
31
+ /**
32
+ * Update a single DOM node with expression result
33
+ * Node is identified by data-zen-text or data-zen-attr-* attribute
34
+ */
35
+ function updateNode(node, expressionId, state, loaderData, props, stores) {
36
+ const expression = window.__ZENITH_EXPRESSIONS__.get(expressionId);
37
+ if (!expression) {
38
+ console.warn('[Zenith] Expression not found:', expressionId);
39
+ return;
40
+ }
41
+
42
+ try {
43
+ const result = expression(state, loaderData, props, stores);
44
+
45
+ // Update node based on attribute type
46
+ if (node.hasAttribute('data-zen-text')) {
47
+ // Text node update
48
+ if (result === null || result === undefined || result === false) {
49
+ node.textContent = '';
50
+ } else {
51
+ node.textContent = String(result);
52
+ }
53
+ } else {
54
+ // Attribute update - determine attribute name from data-zen-attr-*
55
+ const attrMatch = Array.from(node.attributes)
56
+ .find(attr => attr.name.startsWith('data-zen-attr-'));
57
+
58
+ if (attrMatch) {
59
+ const attrName = attrMatch.name.replace('data-zen-attr-', '');
60
+
61
+ if (attrName === 'class' || attrName === 'className') {
62
+ node.className = String(result ?? '');
63
+ } else if (attrName === 'style') {
64
+ if (typeof result === 'string') {
65
+ node.setAttribute('style', result);
66
+ }
67
+ } else if (attrName === 'disabled' || attrName === 'checked') {
68
+ if (result) {
69
+ node.setAttribute(attrName, '');
70
+ } else {
71
+ node.removeAttribute(attrName);
72
+ }
73
+ } else {
74
+ if (result != null && result !== false) {
75
+ node.setAttribute(attrName, String(result));
76
+ } else {
77
+ node.removeAttribute(attrName);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ } catch (error) {
83
+ console.error('[Zenith] Error updating node:', expressionId, error);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Update all hydrated nodes
89
+ * Called when state changes
90
+ */
91
+ function updateAll(state, loaderData, props, stores) {
92
+ // Find all nodes with hydration markers
93
+ const textNodes = document.querySelectorAll('[data-zen-text]');
94
+ const attrNodes = document.querySelectorAll('[data-zen-attr-class], [data-zen-attr-style], [data-zen-attr-src], [data-zen-attr-href]');
95
+
96
+ textNodes.forEach(node => {
97
+ const expressionId = node.getAttribute('data-zen-text');
98
+ if (expressionId) {
99
+ updateNode(node, expressionId, state, loaderData, props, stores);
100
+ }
101
+ });
102
+
103
+ attrNodes.forEach(node => {
104
+ const attrMatch = Array.from(node.attributes)
105
+ .find(attr => attr.name.startsWith('data-zen-attr-'));
106
+ if (attrMatch) {
107
+ const expressionId = attrMatch.value;
108
+ if (expressionId) {
109
+ updateNode(node, expressionId, state, loaderData, props, stores);
110
+ }
111
+ }
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Bind event handlers
117
+ * Handlers are pre-compiled and registered on window
118
+ */
119
+ function bindEvents(container) {
120
+ container = container || document;
121
+
122
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown', 'mouseenter'];
123
+
124
+ eventTypes.forEach(eventType => {
125
+ const elements = container.querySelectorAll('[data-zen-' + eventType + ']');
126
+ elements.forEach(element => {
127
+ const handlerName = element.getAttribute('data-zen-' + eventType);
128
+ if (!handlerName) return;
129
+
130
+ // Remove existing handler
131
+ const handlerKey = '__zen_' + eventType + '_handler';
132
+ const existingHandler = element[handlerKey];
133
+ if (existingHandler) {
134
+ element.removeEventListener(eventType, existingHandler);
135
+ }
136
+
137
+ // Bind new handler (pre-compiled, registered on window)
138
+ const handler = function(event) {
139
+ const handlerFunc = window[handlerName];
140
+ if (typeof handlerFunc === 'function') {
141
+ handlerFunc(event, element);
142
+ }
143
+ };
144
+
145
+ element[handlerKey] = handler;
146
+ element.addEventListener(eventType, handler);
147
+ });
148
+ });
149
+ }
150
+
151
+ // Export to window
152
+ if (typeof window !== 'undefined') {
153
+ window.__zenith_updateAll = updateAll;
154
+ window.__zenith_bindEvents = bindEvents;
155
+ }
156
+ })();
157
+ `;
158
+ }
159
+ //# sourceMappingURL=thin-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-runtime.js","sourceRoot":"","sources":["../src/thin-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqIV,CAAA;AACD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/runtime",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,10 +10,14 @@
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
13
+ "files": [
14
+ "dist",
15
+ "src"
16
+ ],
13
17
  "scripts": {
14
18
  "build": "tsc"
15
19
  },
16
20
  "devDependencies": {
17
21
  "typescript": "^5.3.3"
18
22
  }
19
- }
23
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  // Zenith Runtime
2
2
  // Ported from hydration_runtime.js
3
3
 
4
+ // Exported Types
5
+ export type Signal<T> = ((v?: T) => T) & { _isSignal: true };
6
+ export type Memo<T> = Signal<T>;
7
+ export type Ref<T> = { current: T };
8
+ export type EffectFn = () => (void | (() => void));
9
+ export type DisposeFn = () => void;
10
+ export type Subscriber = { run: () => void; dependencies: Set<Set<any>>; isRunning: boolean };
11
+ export type TrackingContext = { subscriber: Subscriber };
12
+
13
+ export type MountCallback = () => void;
14
+ export type UnmountCallback = () => void;
15
+
4
16
  // Global types for internal use
5
17
  declare global {
6
18
  interface Window {
@@ -28,15 +40,60 @@ declare global {
28
40
 
29
41
  // Internal reactivity state
30
42
  let cE: any = null;
31
- const cS: any[] = [];
43
+ let cS: any[] = [];
32
44
  let bD = 0;
45
+ let isFlushing = false;
46
+ let flushScheduled = false;
33
47
  const pE = new Set<any>();
48
+
49
+ // Lifecycle State
50
+ let isMounted = false;
51
+ const unmountCallbacks = new Set<() => void>();
52
+
53
+ // Public Reactivity Utilities
54
+ export const trackDependency = (s: Set<any>) => { if (cE) { s.add(cE); cE.dependencies.add(s); } };
55
+ export const notifySubscribers = (s: Set<any> | undefined) => {
56
+ if (!s) return;
57
+ const es = Array.from(s);
58
+ for (const e of es) {
59
+ if (e.isRunning) continue;
60
+ if (bD > 0 || isFlushing) pE.add(e);
61
+ else e.run();
62
+ }
63
+ };
64
+ export const getCurrentContext = () => cE;
65
+ export const pushContext = (e: any) => { cS.push(cE); cE = e; };
66
+ export const popContext = () => { cE = cS.pop(); };
67
+ export const cleanupContext = (e: any) => { for (const d of e.dependencies) d.delete(e); e.dependencies.clear(); };
68
+ export const startBatch = () => { bD++; };
69
+ export const endBatch = () => { bD--; if (bD === 0) flushEffects(); };
70
+ export const isBatching = () => bD > 0;
71
+ export const runUntracked = <T>(fn: () => T): T => {
72
+ pushContext(null);
73
+ try { return fn(); } finally { popContext(); }
74
+ };
75
+
76
+ // Public Lifecycle Utilities
77
+ export const triggerMount = () => { isMounted = true; };
78
+ export const triggerUnmount = () => {
79
+ isMounted = false;
80
+ executeUnmountCallbacks();
81
+ };
82
+ export const executeUnmountCallbacks = () => {
83
+ for (const cb of unmountCallbacks) {
84
+ try { cb(); } catch (e) { console.error('Error in unmount callback:', e); }
85
+ }
86
+ unmountCallbacks.clear();
87
+ };
88
+ export const getIsMounted = () => isMounted;
89
+ export const getUnmountCallbackCount = () => unmountCallbacks.size;
90
+ export const resetMountState = () => { isMounted = false; };
91
+ export const resetUnmountState = () => { unmountCallbacks.clear(); };
92
+
34
93
  if (typeof window !== 'undefined') {
35
94
  window.__ZENITH_EXPRESSIONS__ = new Map();
36
95
  window.__ZENITH_SCOPES__ = {};
37
96
  }
38
- let isFlushing = false;
39
- let flushScheduled = false;
40
97
 
41
98
  // Phase A3: Post-Mount Execution Hook
42
99
  const mountedScopes = new Set<string>();
@@ -53,9 +110,12 @@ export function mountComponent(scopeId: string) {
53
110
  }
54
111
  }
55
112
 
56
- function pC(e: any) { cS.push(cE); cE = e; }
57
- function oC() { cE = cS.pop(); }
58
- function tD(s: Set<any>) { if (cE) { s.add(cE); cE.dependencies.add(s); } }
113
+ // Internal shorthand helpers (matching original compiled code requirements if any)
114
+ const pC = pushContext;
115
+ const oC = popContext;
116
+ const tD = trackDependency;
117
+ const nS = notifySubscribers;
118
+ const cEf = cleanupContext;
59
119
 
60
120
  export function zenRoute() {
61
121
  if (typeof window === 'undefined') return { path: '/', slugs: [] };
@@ -66,16 +126,6 @@ export function zenRoute() {
66
126
  };
67
127
  }
68
128
 
69
- function nS(s: Set<any> | undefined) {
70
- if (!s) return;
71
- const es = Array.from(s);
72
- for (const e of es) {
73
- if (e.isRunning) continue;
74
- if (bD > 0 || isFlushing) pE.add(e);
75
- else e.run();
76
- }
77
- }
78
-
79
129
  function scheduleFlush() {
80
130
  if (flushScheduled) return;
81
131
  flushScheduled = true;
@@ -101,8 +151,6 @@ function flushEffects() {
101
151
  }
102
152
  }
103
153
 
104
- function cEf(e: any) { for (const d of e.dependencies) d.delete(e); e.dependencies.clear(); }
105
-
106
154
  export const signal = function (v: any) {
107
155
  const s = new Set<any>();
108
156
  function sig(nV?: any) {
@@ -190,21 +238,27 @@ export const memo = function (fn: () => any) {
190
238
  };
191
239
 
192
240
  export const batch = function (fn: () => void) {
193
- bD++;
194
- try { fn(); } finally {
195
- bD--;
196
- if (bD === 0) flushEffects();
197
- }
241
+ startBatch();
242
+ try { fn(); } finally { endBatch(); }
198
243
  };
199
244
 
200
245
  export const untrack = function (fn: () => any) {
201
- pC(null);
202
- try { return fn(); } finally { oC(); }
246
+ return runUntracked(fn);
203
247
  };
204
248
 
205
249
  export const ref = (i: any) => ({ current: i || null });
206
- export const onMount = (cb: () => void) => { if (window.__zenith && window.__zenith.activeInstance) window.__zenith.activeInstance.mountHooks.push(cb); };
207
- export const onUnmount = (cb: () => void) => { /* TODO */ };
250
+
251
+ export const onMount = (cb: () => void) => {
252
+ if (typeof window !== 'undefined' && window.__zenith && window.__zenith.activeInstance) {
253
+ window.__zenith.activeInstance.mountHooks.push(cb);
254
+ } else {
255
+ // Fallback for non-component context or SSR
256
+ if (isMounted) cb();
257
+ }
258
+ };
259
+ export const onUnmount = (cb: () => void) => {
260
+ unmountCallbacks.add(cb);
261
+ };
208
262
 
209
263
  // DOM Helper (hC)
210
264
  function hC(parent: Node, child: any) {
@@ -295,24 +349,6 @@ export function hydrate(state: any, container: Element | Document = document, lo
295
349
  oldTitle = document.createElement('title');
296
350
  headMount.appendChild(oldTitle);
297
351
  }
298
- const resolveContent = (children: any): string => {
299
- let result = '';
300
- (Array.isArray(children) ? children : [children]).forEach(child => {
301
- if (child == null || child === false) return;
302
- if (typeof child === 'function') {
303
- const val = child();
304
- if (val != null && val !== false) result += String(val);
305
- } else if (typeof child === 'object' && child.fn) {
306
- const val = child.fn();
307
- if (val != null && val !== false) result += String(val);
308
- } else if (child instanceof Node) {
309
- result += child.textContent || '';
310
- } else {
311
- result += String(child);
312
- }
313
- });
314
- return result;
315
- };
316
352
  const titleContent = newTitle.childNodes.length > 0
317
353
  ? Array.from(newTitle.childNodes).map((n: any) => n.textContent).join('')
318
354
  : '';
@@ -407,7 +443,6 @@ export function h(tag: string, props: any, children: any) {
407
443
  if (v && typeof v === 'object' && (v as any).fn) fn = (v as any).fn;
408
444
  if (typeof fn === 'function') {
409
445
  el.addEventListener(k.slice(2).toLowerCase(), (e) => {
410
- // Fix: this binding via call(el, e, el)
411
446
  const h = (fn as Function).call(el, e, el);
412
447
  if (typeof h === 'function') h.call(el, e, el);
413
448
  });
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "declaration": true,
7
- "outDir": "./dist",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true
12
- },
13
- "include": [
14
- "src/**/*"
15
- ]
16
- }