@zenithbuild/runtime 0.1.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.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @zenith/runtime
2
+
3
+ The core runtime library for the Zenith framework.
4
+
5
+ ## Overview
6
+ This package provides the reactivity system, hydration logic, and Virtual DOM primitives used by Zenith applications. It is designed to be lightweight, fast, and tree-shakeable.
7
+
8
+ ## Features
9
+ - **Fine-Grained Reactivity**: Signals, Effects, Derived State (Memos).
10
+ - **Hydration**: Efficient client-side hydration of server-rendered HTML.
11
+ - **VDOM primitives**: `h` and `fragment` for lightweight view rendering.
12
+ - **Lifecycle Hooks**: `onMount`, `onUnmount`.
13
+
14
+ ## Usage
15
+ This package is typically installed automatically by the Zenith CLI.
16
+ ```typescript
17
+ import { signal, effect } from "@zenith/runtime";
18
+
19
+ const count = signal(0);
20
+ effect(() => console.log(count()));
21
+ ```
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@zenithbuild/runtime",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5.3.3"
18
+ }
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,450 @@
1
+ // Zenith Runtime
2
+ // Ported from hydration_runtime.js
3
+
4
+ // Global types for internal use
5
+ declare global {
6
+ interface Window {
7
+ __ZENITH_RUNTIME__: any;
8
+ __ZENITH_STATE__: any;
9
+ __ZENITH_SCOPES__: any;
10
+ __ZENITH_EXPRESSIONS__: any;
11
+ __zenith: any;
12
+ zenSignal: any;
13
+ zenState: any;
14
+ zenEffect: any;
15
+ zenMemo: any;
16
+ zenBatch: any;
17
+ zenUntrack: any;
18
+ zenRef: any;
19
+ zenOnMount: any;
20
+ zenOnUnmount: any;
21
+ zenithHydrate: any;
22
+ zenithNotify: any;
23
+ zenithSubscribe: any;
24
+ zenOrder: any;
25
+ zenFixSVGNamespace: any;
26
+ }
27
+ }
28
+
29
+ // Internal reactivity state
30
+ let cE: any = null;
31
+ const cS: any[] = [];
32
+ let bD = 0;
33
+ const pE = new Set<any>();
34
+ if (typeof window !== 'undefined') {
35
+ window.__ZENITH_EXPRESSIONS__ = new Map();
36
+ window.__ZENITH_SCOPES__ = {};
37
+ }
38
+ let isFlushing = false;
39
+ let flushScheduled = false;
40
+
41
+ // Phase A3: Post-Mount Execution Hook
42
+ const mountedScopes = new Set<string>();
43
+
44
+ export function mountComponent(scopeId: string) {
45
+ if (mountedScopes.has(scopeId)) return;
46
+ mountedScopes.add(scopeId);
47
+
48
+ const scope = window.__ZENITH_SCOPES__[scopeId];
49
+ if (!scope) return;
50
+
51
+ if (typeof scope.__run === 'function') {
52
+ scope.__run();
53
+ }
54
+ }
55
+
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); } }
59
+
60
+ export function zenRoute() {
61
+ if (typeof window === 'undefined') return { path: '/', slugs: [] };
62
+ const path = window.location.pathname;
63
+ return {
64
+ path: path,
65
+ slugs: path.split('/').filter(Boolean)
66
+ };
67
+ }
68
+
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
+ function scheduleFlush() {
80
+ if (flushScheduled) return;
81
+ flushScheduled = true;
82
+ queueMicrotask(() => {
83
+ flushScheduled = false;
84
+ flushEffects();
85
+ });
86
+ }
87
+
88
+ function flushEffects() {
89
+ if (isFlushing || bD > 0) return;
90
+ isFlushing = true;
91
+ try {
92
+ while (pE.size > 0) {
93
+ const efs = Array.from(pE);
94
+ pE.clear();
95
+ for (const e of efs) {
96
+ if (!e.isRunning) e.run();
97
+ }
98
+ }
99
+ } finally {
100
+ isFlushing = false;
101
+ }
102
+ }
103
+
104
+ function cEf(e: any) { for (const d of e.dependencies) d.delete(e); e.dependencies.clear(); }
105
+
106
+ export const signal = function (v: any) {
107
+ const s = new Set<any>();
108
+ function sig(nV?: any) {
109
+ if (arguments.length === 0) { tD(s); return v; }
110
+ if (nV !== v) { v = nV; nS(s); scheduleFlush(); }
111
+ return v;
112
+ }
113
+ sig._isSignal = true; sig.toString = () => String(v); sig.valueOf = () => v;
114
+ return sig;
115
+ };
116
+
117
+ export const state = function (o: any) {
118
+ const subs = new Map<string, Set<any>>();
119
+ function gS(p: string) { if (!subs.has(p)) subs.set(p, new Set()); return subs.get(p); }
120
+ function notify(p: string) { nS(gS(p)); scheduleFlush(); }
121
+ function subscribe(p: string, ef: any) { gS(p)!.add(ef); ef.dependencies.add(gS(p)); }
122
+ function cP(obj: any, pPath = ''): any {
123
+ if (obj === null || typeof obj !== 'object' || obj._isSignal) return obj;
124
+ return new Proxy(obj, {
125
+ get(t, p) {
126
+ if (p === Symbol.for('zenith_notify')) return notify;
127
+ if (p === Symbol.for('zenith_subscribe')) return subscribe;
128
+ if (typeof p === 'symbol') return t[p];
129
+ const path = pPath ? `${pPath}.${String(p)}` : String(p);
130
+ tD(gS(path)!);
131
+ const v = t[p];
132
+ if (v !== null && typeof v === 'object' && !v._isSignal) return cP(v, path);
133
+ return v;
134
+ },
135
+ set(t, p, nV) {
136
+ if (typeof p === 'symbol') { t[p] = nV; return true; }
137
+ const path = pPath ? `${pPath}.${String(p)}` : String(p);
138
+ const oV = t[p];
139
+ if (oV && typeof oV === 'function' && oV._isSignal) oV(nV);
140
+ else if (oV !== nV) {
141
+ t[p] = nV;
142
+ nS(gS(path));
143
+ const pts = path.split('.');
144
+ for (let i = pts.length - 1; i >= 0; i--) {
145
+ const pp = pts.slice(0, i).join('.');
146
+ if (pp) nS(gS(pp));
147
+ }
148
+ scheduleFlush();
149
+ }
150
+ return true;
151
+ }
152
+ });
153
+ }
154
+ return cP(o);
155
+ };
156
+
157
+ export const effect = function (fn: () => any, opts: any = {}) {
158
+ let cl: any, tm: any;
159
+ const ef: any = {
160
+ dependencies: new Set(),
161
+ isRunning: false,
162
+ run: () => {
163
+ if (ef.isRunning) return;
164
+ const schedule = opts.scheduler || ((f: any) => f());
165
+ if (opts.debounce) {
166
+ if (tm) clearTimeout(tm);
167
+ tm = setTimeout(() => schedule(ex), opts.debounce);
168
+ } else schedule(ex);
169
+ }
170
+ };
171
+ function ex() {
172
+ if (ef.isRunning) return;
173
+ ef.isRunning = true;
174
+ cEf(ef);
175
+ pC(ef);
176
+ try { if (cl) cl(); cl = fn(); }
177
+ finally {
178
+ oC();
179
+ ef.isRunning = false;
180
+ }
181
+ }
182
+ if (!opts.defer) ex();
183
+ return () => { cEf(ef); if (cl) cl(); };
184
+ };
185
+
186
+ export const memo = function (fn: () => any) {
187
+ const sig = signal(undefined);
188
+ effect(() => sig(fn()));
189
+ const m = () => sig(); m._isSignal = true; return m;
190
+ };
191
+
192
+ export const batch = function (fn: () => void) {
193
+ bD++;
194
+ try { fn(); } finally {
195
+ bD--;
196
+ if (bD === 0) flushEffects();
197
+ }
198
+ };
199
+
200
+ export const untrack = function (fn: () => any) {
201
+ pC(null);
202
+ try { return fn(); } finally { oC(); }
203
+ };
204
+
205
+ 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 */ };
208
+
209
+ // DOM Helper (hC)
210
+ function hC(parent: Node, child: any) {
211
+ if (child == null || child === false) return;
212
+
213
+ let fn = child;
214
+ let id: string | null = null;
215
+ if (typeof child === 'object' && child.fn) {
216
+ fn = child.fn;
217
+ id = child.id;
218
+ }
219
+
220
+ const isTitle = parent && (parent as any).tagName && (parent as any).tagName.toLowerCase() === 'title';
221
+
222
+ if (typeof fn === 'function') {
223
+ if (isTitle) {
224
+ const val = fn();
225
+ if (val != null && val !== false) {
226
+ const text = String(val);
227
+ parent.appendChild(document.createTextNode(text));
228
+ document.title = text;
229
+ }
230
+ effect(() => {
231
+ const newVal = fn();
232
+ if (newVal != null && newVal !== false) {
233
+ const newText = String(newVal);
234
+ if (document.title !== newText) {
235
+ parent.textContent = newText;
236
+ document.title = newText;
237
+ }
238
+ }
239
+ }, { id: id ? `title-${id}` : 'title-sync' });
240
+ } else {
241
+ const ph = document.createComment('expr' + (id ? ':' + id : ''));
242
+ parent.appendChild(ph);
243
+ let curNodes: Node[] = [];
244
+ effect(() => {
245
+ const r = fn();
246
+ curNodes.forEach(n => { if (n.parentNode) n.parentNode.removeChild(n); });
247
+ curNodes = [];
248
+ if (r == null || r === false) return;
249
+ const items = Array.isArray(r) ? r.flat(Infinity) : [r];
250
+ items.forEach(item => {
251
+ if (item == null || item === false) return;
252
+ const node = item instanceof Node ? item : document.createTextNode(String(item));
253
+ if (ph.parentNode) {
254
+ ph.parentNode.insertBefore(node, ph);
255
+ curNodes.push(node);
256
+ }
257
+ });
258
+ }, { id });
259
+ }
260
+ } else if (Array.isArray(child)) {
261
+ child.flat(Infinity).forEach(c => hC(parent, c));
262
+ } else {
263
+ parent.appendChild(child instanceof Node ? child : document.createTextNode(String(child)));
264
+ }
265
+ }
266
+
267
+ // Global Hydration
268
+ export function hydrate(state: any, container: Element | Document = document, locals: any = {}) {
269
+ const ir = (window as any).canonicalIR; if (!ir) return;
270
+ window.__ZENITH_STATE__ = state;
271
+ const rootScope = { state, props: {}, locals: locals };
272
+ const nodes = ir(rootScope);
273
+
274
+ function findTag(items: any, tag: string): any {
275
+ const list = Array.isArray(items) ? items : [items];
276
+ for (const item of list) {
277
+ if (item instanceof Element && item.tagName.toLowerCase() === tag) return item;
278
+ if (item instanceof DocumentFragment) {
279
+ const found = findTag(Array.from(item.childNodes), tag);
280
+ if (found) return found;
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+
286
+ const headNode = findTag(nodes, 'head');
287
+ const bodyNode = findTag(nodes, 'body');
288
+
289
+ if (headNode) {
290
+ const headMount = document.head;
291
+ const newTitle = headNode.querySelector('title');
292
+ if (newTitle) {
293
+ let oldTitle = headMount.querySelector('title');
294
+ if (!oldTitle) {
295
+ oldTitle = document.createElement('title');
296
+ headMount.appendChild(oldTitle);
297
+ }
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
+ const titleContent = newTitle.childNodes.length > 0
317
+ ? Array.from(newTitle.childNodes).map((n: any) => n.textContent).join('')
318
+ : '';
319
+ oldTitle.textContent = titleContent;
320
+ document.title = titleContent;
321
+ effect(() => {
322
+ const text = oldTitle!.textContent?.trim();
323
+ if (text && document.title !== text) {
324
+ document.title = text;
325
+ }
326
+ }, { id: 'title-sync' });
327
+ }
328
+ headNode.querySelectorAll('meta').forEach((newMeta: Element) => {
329
+ const name = newMeta.getAttribute('name');
330
+ if (name) {
331
+ const oldMeta = headMount.querySelector(`meta[name="${name}"]`);
332
+ if (oldMeta) oldMeta.setAttribute('content', newMeta.getAttribute('content')!);
333
+ else headMount.appendChild(newMeta.cloneNode(true));
334
+ }
335
+ });
336
+ headNode.childNodes.forEach((n: Node) => {
337
+ if ((n as Element).tagName === 'TITLE' || (n as Element).tagName === 'META') return;
338
+ headMount.appendChild(n.cloneNode(true));
339
+ });
340
+ }
341
+
342
+ const bodyMount = container === document ? document.body : (container as Element);
343
+ if (bodyNode) {
344
+ bodyMount.innerHTML = '';
345
+ Array.from(bodyNode.childNodes).forEach(n => hC(bodyMount, n));
346
+ } else {
347
+ bodyMount.innerHTML = '';
348
+ const items = Array.isArray(nodes) ? nodes : [nodes];
349
+ items.forEach(n => hC(bodyMount, n));
350
+ }
351
+
352
+ for (const scopeId in window.__ZENITH_SCOPES__) {
353
+ mountComponent(scopeId);
354
+ }
355
+
356
+ queueMicrotask(() => {
357
+ flushEffects();
358
+ const titleEl = document.querySelector('title');
359
+ if (titleEl && titleEl.textContent) {
360
+ const text = titleEl.textContent.trim();
361
+ if (text && document.title !== text) {
362
+ document.title = text;
363
+ }
364
+ }
365
+ });
366
+ }
367
+
368
+ // Ordered Effects
369
+ export function order(fn: () => void) {
370
+ if (typeof fn === 'function') fn();
371
+ }
372
+
373
+ // Hyperscript
374
+ let currentNamespace: string | null = null;
375
+ export function h(tag: string, props: any, children: any) {
376
+ const SVG_NS = 'http://www.w3.org/2000/svg';
377
+ const SVG_TAGS = new Set(['svg', 'path', 'circle', 'ellipse', 'line', 'polygon', 'polyline', 'rect', 'g', 'defs', 'clipPath', 'mask', 'use', 'symbol', 'text', 'tspan', 'textPath', 'image', 'foreignObject', 'switch', 'desc', 'title', 'metadata', 'linearGradient', 'radialGradient', 'stop', 'pattern', 'filter', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feFlood', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'feSpecularLighting', 'feTile', 'feTurbulence', 'animate', 'animateMotion', 'animateTransform', 'set', 'marker']);
378
+
379
+ const isSvgTag = SVG_TAGS.has(tag) || SVG_TAGS.has(tag.toLowerCase());
380
+ const useSvgNamespace = isSvgTag || currentNamespace === SVG_NS;
381
+
382
+ const el = useSvgNamespace ? document.createElementNS(SVG_NS, tag) : document.createElement(tag);
383
+
384
+ const previousNamespace = currentNamespace;
385
+ if (tag === 'svg' || tag === 'SVG') {
386
+ currentNamespace = SVG_NS;
387
+ }
388
+
389
+ if (props) {
390
+ const setClass = (element: Element, value: any) => {
391
+ if (useSvgNamespace && 'className' in element && typeof (element as any).className === 'object') {
392
+ (element as any).className.baseVal = String(value || '');
393
+ } else {
394
+ (element as any).className = String(value || '');
395
+ }
396
+ };
397
+
398
+ for (const [k, v] of Object.entries(props)) {
399
+ if (k === 'ref') {
400
+ if (v && typeof v === 'object' && 'current' in v) (v as any).current = el;
401
+ else if (typeof v === 'string') {
402
+ const s = window.__ZENITH_STATE__;
403
+ if (s && s[v] && typeof s[v] === 'object' && 'current' in s[v]) s[v].current = el;
404
+ }
405
+ } else if (k.startsWith('on')) {
406
+ let fn = v;
407
+ if (v && typeof v === 'object' && (v as any).fn) fn = (v as any).fn;
408
+ if (typeof fn === 'function') {
409
+ el.addEventListener(k.slice(2).toLowerCase(), (e) => {
410
+ // Fix: this binding via call(el, e, el)
411
+ const h = (fn as Function).call(el, e, el);
412
+ if (typeof h === 'function') h.call(el, e, el);
413
+ });
414
+ }
415
+ } else {
416
+ let fn = v;
417
+ let id = null;
418
+ if (typeof v === 'object' && (v as any).fn) {
419
+ fn = (v as any).fn;
420
+ id = (v as any).id;
421
+ }
422
+ if (typeof fn === 'function') {
423
+ effect(() => {
424
+ const val = (fn as Function)();
425
+ if (k === 'class' || k === 'className') setClass(el, val);
426
+ else if (val == null || val === false) el.removeAttribute(k);
427
+ else if (el.setAttribute) el.setAttribute(k, String(val));
428
+ }, { id });
429
+ } else {
430
+ if (k === 'class' || k === 'className') setClass(el, v);
431
+ else if (el.setAttribute) el.setAttribute(k, String(v));
432
+ }
433
+ }
434
+ }
435
+ }
436
+ if (children) {
437
+ const items = Array.isArray(children) ? children : [children];
438
+ items.forEach(c => hC(el, c));
439
+ }
440
+
441
+ currentNamespace = previousNamespace;
442
+ return el;
443
+ }
444
+
445
+ export function fragment(c: any) {
446
+ const f = document.createDocumentFragment();
447
+ const items = Array.isArray(c) ? c : [c];
448
+ items.forEach(i => hC(f, i));
449
+ return f;
450
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
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
+ }