@zenithbuild/runtime 0.1.3 → 0.1.8

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/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
- "name": "@zenithbuild/runtime",
3
- "version": "0.1.3",
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
- }
2
+ "name": "@zenithbuild/runtime",
3
+ "version": "0.1.8",
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
19
  }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Runtime Hydration Layer (Phase 5)
3
+ *
4
+ * Browser-side runtime that hydrates static HTML with dynamic expressions
5
+ *
6
+ * This runtime:
7
+ * - Locates DOM placeholders (data-zen-text, data-zen-attr-*)
8
+ * - Evaluates precompiled expressions against state
9
+ * - Updates DOM textContent, attributes, and properties
10
+ * - Binds event handlers
11
+ * - Handles reactive state updates
12
+ */
13
+
14
+ /**
15
+ * Binding registry - tracks which DOM nodes are bound to which expressions
16
+ */
17
+ interface Binding {
18
+ node: Node
19
+ type: 'text' | 'attribute'
20
+ attributeName?: string
21
+ expressionId: string
22
+ }
23
+
24
+ const bindings: Binding[] = []
25
+
26
+ /**
27
+ * Hydrate static HTML with dynamic expressions (Phase 5 Strategy)
28
+ *
29
+ * @param state - The state object to evaluate expressions against
30
+ * @param container - The container element to hydrate (defaults to document)
31
+ */
32
+ export function hydrateDom(state: any, container: Document | Element = document): void {
33
+ if (!state) {
34
+ console.warn('[Zenith] hydrateDom called without state object')
35
+ return
36
+ }
37
+
38
+ // Store state globally for event handlers
39
+ if (typeof window !== 'undefined') {
40
+ window.__ZENITH_STATE__ = state
41
+ }
42
+
43
+ // Clear existing bindings
44
+ bindings.length = 0
45
+
46
+ // Find all text expression placeholders
47
+ const textPlaceholders = container.querySelectorAll('[data-zen-text]')
48
+ for (let i = 0; i < textPlaceholders.length; i++) {
49
+ const node = textPlaceholders[i]
50
+ if (!node) continue
51
+ const expressionId = node.getAttribute('data-zen-text')
52
+ if (!expressionId) continue
53
+
54
+ bindings.push({
55
+ node,
56
+ type: 'text',
57
+ expressionId
58
+ })
59
+
60
+ updateTextBinding(node, expressionId, state)
61
+ }
62
+
63
+ // Find all attribute expression placeholders
64
+ const attrPlaceholders = container.querySelectorAll('[data-zen-attr-class], [data-zen-attr-style], [data-zen-attr-src], [data-zen-attr-href], [data-zen-attr-disabled], [data-zen-attr-checked]')
65
+
66
+ for (let i = 0; i < attrPlaceholders.length; i++) {
67
+ const node = attrPlaceholders[i]
68
+ if (!(node instanceof Element)) continue
69
+
70
+ // Check each possible attribute
71
+ const attrNames = ['class', 'style', 'src', 'href', 'disabled', 'checked']
72
+ for (const attrName of attrNames) {
73
+ const expressionId = node.getAttribute(`data-zen-attr-${attrName}`)
74
+ if (!expressionId) continue
75
+
76
+ bindings.push({
77
+ node,
78
+ type: 'attribute',
79
+ attributeName: attrName,
80
+ expressionId
81
+ })
82
+
83
+ updateAttributeBinding(node, attrName, expressionId, state)
84
+ }
85
+ }
86
+
87
+ // Bind event handlers
88
+ bindEvents(container)
89
+ }
90
+
91
+ /**
92
+ * Update a text binding
93
+ */
94
+ function updateTextBinding(node: Node, expressionId: string, state: any): void {
95
+ try {
96
+ const expression = window.__ZENITH_EXPRESSIONS__?.get(expressionId)
97
+ if (!expression) {
98
+ console.warn(`[Zenith] Expression ${expressionId} not found in registry`)
99
+ return
100
+ }
101
+
102
+ const result = expression(state)
103
+
104
+ // Handle different result types
105
+ if (result === null || result === undefined || result === false) {
106
+ node.textContent = ''
107
+ } else if (typeof result === 'string' || typeof result === 'number') {
108
+ node.textContent = String(result)
109
+ } else if (result instanceof Node) {
110
+ // Replace node with result node
111
+ if (node.parentNode) {
112
+ node.parentNode.replaceChild(result, node)
113
+ }
114
+ } else if (Array.isArray(result)) {
115
+ // Handle array results (for map expressions)
116
+ if (node.parentNode) {
117
+ const fragment = document.createDocumentFragment()
118
+ for (const item of result) {
119
+ if (item instanceof Node) {
120
+ fragment.appendChild(item)
121
+ } else {
122
+ fragment.appendChild(document.createTextNode(String(item)))
123
+ }
124
+ }
125
+ node.parentNode.replaceChild(fragment, node)
126
+ }
127
+ } else {
128
+ node.textContent = String(result)
129
+ }
130
+ } catch (error: any) {
131
+ console.error(`[Zenith] Error evaluating expression ${expressionId}:`, error)
132
+ console.error('Expression ID:', expressionId, 'State:', state)
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Update an attribute binding
138
+ */
139
+ function updateAttributeBinding(
140
+ element: Element,
141
+ attributeName: string,
142
+ expressionId: string,
143
+ state: any
144
+ ): void {
145
+ try {
146
+ const expression = window.__ZENITH_EXPRESSIONS__?.get(expressionId)
147
+ if (!expression) {
148
+ console.warn(`[Zenith] Expression ${expressionId} not found in registry`)
149
+ return
150
+ }
151
+
152
+ const result = expression(state)
153
+
154
+ // Handle different attribute types
155
+ if (attributeName === 'class' || attributeName === 'className') {
156
+ element.className = String(result ?? '')
157
+ } else if (attributeName === 'style') {
158
+ if (typeof result === 'string') {
159
+ element.setAttribute('style', result)
160
+ } else if (result && typeof result === 'object') {
161
+ // Handle style object
162
+ const styleStr = Object.entries(result)
163
+ .map(([key, value]) => `${key}: ${value}`)
164
+ .join('; ')
165
+ element.setAttribute('style', styleStr)
166
+ }
167
+ } else if (attributeName === 'disabled' || attributeName === 'checked' || attributeName === 'readonly') {
168
+ // Boolean attributes
169
+ if (result) {
170
+ element.setAttribute(attributeName, '')
171
+ } else {
172
+ element.removeAttribute(attributeName)
173
+ }
174
+ } else {
175
+ // Regular attributes
176
+ if (result === null || result === undefined || result === false) {
177
+ element.removeAttribute(attributeName)
178
+ } else {
179
+ element.setAttribute(attributeName, String(result))
180
+ }
181
+ }
182
+ } catch (error: any) {
183
+ console.error(`[Zenith] Error updating attribute ${attributeName} with expression ${expressionId}:`, error)
184
+ console.error('Expression ID:', expressionId, 'State:', state)
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Bind event handlers to DOM elements
190
+ *
191
+ * @param container - The container element to bind events in (defaults to document)
192
+ */
193
+ export function bindEvents(container: Document | Element = document): void {
194
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown']
195
+
196
+ for (const eventType of eventTypes) {
197
+ const elements = container.querySelectorAll(`[data-zen-${eventType}]`)
198
+
199
+ for (let i = 0; i < elements.length; i++) {
200
+ const element = elements[i]
201
+ if (!(element instanceof Element)) continue
202
+
203
+ const handlerName = element.getAttribute(`data-zen-${eventType}`)
204
+ if (!handlerName) continue
205
+
206
+ // Remove existing listener if any (to avoid duplicates)
207
+ const existingHandler = (element as any)[`__zen_${eventType}_handler`]
208
+ if (existingHandler) {
209
+ element.removeEventListener(eventType, existingHandler)
210
+ }
211
+
212
+ // Create new handler
213
+ const handler = (event: Event) => {
214
+ try {
215
+ // Get handler function from window (functions are registered on window)
216
+ const handlerFunc = (window as any)[handlerName]
217
+ if (typeof handlerFunc === 'function') {
218
+ handlerFunc(event, element)
219
+ } else {
220
+ console.warn(`[Zenith] Event handler "${handlerName}" not found for ${eventType} event`)
221
+ }
222
+ } catch (error: any) {
223
+ console.error(`[Zenith] Error executing event handler "${handlerName}":`, error)
224
+ }
225
+ }
226
+
227
+ // Store handler reference to allow cleanup
228
+ ; (element as any)[`__zen_${eventType}_handler`] = handler
229
+
230
+ element.addEventListener(eventType, handler)
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Update all bindings when state changes
237
+ *
238
+ * @param state - The new state object
239
+ */
240
+ export function updateDom(state: any): void {
241
+ if (!state) {
242
+ console.warn('[Zenith] updateDom called without state object')
243
+ return
244
+ }
245
+
246
+ // Update global state
247
+ if (typeof window !== 'undefined') {
248
+ window.__ZENITH_STATE__ = state
249
+ }
250
+
251
+ // Update all tracked bindings
252
+ for (const binding of bindings) {
253
+ if (binding.type === 'text') {
254
+ updateTextBinding(binding.node, binding.expressionId, state)
255
+ } else if (binding.type === 'attribute' && binding.attributeName) {
256
+ if (binding.node instanceof Element) {
257
+ updateAttributeBinding(binding.node, binding.attributeName, binding.expressionId, state)
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Initialize the expression registry
265
+ * Called once when the runtime loads
266
+ *
267
+ * @param expressions - Map of expression IDs to evaluation functions
268
+ */
269
+ export function initExpressions(expressions: Map<string, (state: any) => any>): void {
270
+ if (typeof window !== 'undefined') {
271
+ window.__ZENITH_EXPRESSIONS__ = expressions
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Clear all bindings and event listeners
277
+ * Useful for cleanup when navigating away
278
+ */
279
+ export function cleanupDom(container: Document | Element = document): void {
280
+ // Remove event listeners
281
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown']
282
+ for (const eventType of eventTypes) {
283
+ const elements = container.querySelectorAll(`[data-zen-${eventType}]`)
284
+ for (let i = 0; i < elements.length; i++) {
285
+ const element = elements[i]
286
+ if (!(element instanceof Element)) continue
287
+ const handler = (element as any)[`__zen_${eventType}_handler`]
288
+ if (handler) {
289
+ element.removeEventListener(eventType, handler)
290
+ delete (element as any)[`__zen_${eventType}_handler`]
291
+ }
292
+ }
293
+ }
294
+
295
+ // Clear bindings
296
+ bindings.length = 0
297
+ }
package/src/index.ts CHANGED
@@ -448,3 +448,6 @@ export function fragment(c: any) {
448
448
  items.forEach(i => hC(f, i));
449
449
  return f;
450
450
  }
451
+
452
+ export * from './dom-hydration.js';
453
+ export * from './thin-runtime.js';
@@ -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
+ /**
15
+ * Generate thin declarative runtime code
16
+ *
17
+ * This runtime is minimal and safe - it only:
18
+ * 1. Updates DOM nodes using pre-compiled expression functions
19
+ * 2. Binds event handlers by ID
20
+ * 3. Provides reactive state updates
21
+ *
22
+ * All expressions are pre-compiled at build time.
23
+ */
24
+ export function generateThinRuntime(): string {
25
+ return `
26
+ // Zenith Thin Runtime (Phase 8/9/10)
27
+ // Purely declarative - no template parsing, no eval, no with(window)
28
+
29
+ (function() {
30
+ 'use strict';
31
+
32
+ /**
33
+ * Update a single DOM node with expression result
34
+ * Node is identified by data-zen-text or data-zen-attr-* attribute
35
+ */
36
+ function updateNode(node, expressionId, state, loaderData, props, stores) {
37
+ const expression = window.__ZENITH_EXPRESSIONS__.get(expressionId);
38
+ if (!expression) {
39
+ console.warn('[Zenith] Expression not found:', expressionId);
40
+ return;
41
+ }
42
+
43
+ try {
44
+ const result = expression(state, loaderData, props, stores);
45
+
46
+ // Update node based on attribute type
47
+ if (node.hasAttribute('data-zen-text')) {
48
+ // Text node update
49
+ if (result === null || result === undefined || result === false) {
50
+ node.textContent = '';
51
+ } else {
52
+ node.textContent = String(result);
53
+ }
54
+ } else {
55
+ // Attribute update - determine attribute name from data-zen-attr-*
56
+ const attrMatch = Array.from(node.attributes)
57
+ .find(attr => attr.name.startsWith('data-zen-attr-'));
58
+
59
+ if (attrMatch) {
60
+ const attrName = attrMatch.name.replace('data-zen-attr-', '');
61
+
62
+ if (attrName === 'class' || attrName === 'className') {
63
+ node.className = String(result ?? '');
64
+ } else if (attrName === 'style') {
65
+ if (typeof result === 'string') {
66
+ node.setAttribute('style', result);
67
+ }
68
+ } else if (attrName === 'disabled' || attrName === 'checked') {
69
+ if (result) {
70
+ node.setAttribute(attrName, '');
71
+ } else {
72
+ node.removeAttribute(attrName);
73
+ }
74
+ } else {
75
+ if (result != null && result !== false) {
76
+ node.setAttribute(attrName, String(result));
77
+ } else {
78
+ node.removeAttribute(attrName);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ } catch (error) {
84
+ console.error('[Zenith] Error updating node:', expressionId, error);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Update all hydrated nodes
90
+ * Called when state changes
91
+ */
92
+ function updateAll(state, loaderData, props, stores) {
93
+ // Find all nodes with hydration markers
94
+ const textNodes = document.querySelectorAll('[data-zen-text]');
95
+ const attrNodes = document.querySelectorAll('[data-zen-attr-class], [data-zen-attr-style], [data-zen-attr-src], [data-zen-attr-href]');
96
+
97
+ textNodes.forEach(node => {
98
+ const expressionId = node.getAttribute('data-zen-text');
99
+ if (expressionId) {
100
+ updateNode(node, expressionId, state, loaderData, props, stores);
101
+ }
102
+ });
103
+
104
+ attrNodes.forEach(node => {
105
+ const attrMatch = Array.from(node.attributes)
106
+ .find(attr => attr.name.startsWith('data-zen-attr-'));
107
+ if (attrMatch) {
108
+ const expressionId = attrMatch.value;
109
+ if (expressionId) {
110
+ updateNode(node, expressionId, state, loaderData, props, stores);
111
+ }
112
+ }
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Bind event handlers
118
+ * Handlers are pre-compiled and registered on window
119
+ */
120
+ function bindEvents(container) {
121
+ container = container || document;
122
+
123
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown', 'mouseenter'];
124
+
125
+ eventTypes.forEach(eventType => {
126
+ const elements = container.querySelectorAll('[data-zen-' + eventType + ']');
127
+ elements.forEach(element => {
128
+ const handlerName = element.getAttribute('data-zen-' + eventType);
129
+ if (!handlerName) return;
130
+
131
+ // Remove existing handler
132
+ const handlerKey = '__zen_' + eventType + '_handler';
133
+ const existingHandler = element[handlerKey];
134
+ if (existingHandler) {
135
+ element.removeEventListener(eventType, existingHandler);
136
+ }
137
+
138
+ // Bind new handler (pre-compiled, registered on window)
139
+ const handler = function(event) {
140
+ const handlerFunc = window[handlerName];
141
+ if (typeof handlerFunc === 'function') {
142
+ handlerFunc(event, element);
143
+ }
144
+ };
145
+
146
+ element[handlerKey] = handler;
147
+ element.addEventListener(eventType, handler);
148
+ });
149
+ });
150
+ }
151
+
152
+ // Export to window
153
+ if (typeof window !== 'undefined') {
154
+ window.__zenith_updateAll = updateAll;
155
+ window.__zenith_bindEvents = bindEvents;
156
+ }
157
+ })();
158
+ `
159
+ }