@zenithbuild/runtime 0.2.1 → 0.5.0-beta.2.15

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,309 @@
1
+ const OVERLAY_ID = '__zenith_runtime_error_overlay';
2
+ const MAX_MESSAGE_LENGTH = 120;
3
+ const MAX_HINT_LENGTH = 140;
4
+ const MAX_PATH_LENGTH = 120;
5
+
6
+ const VALID_PHASES = new Set(['hydrate', 'bind', 'render', 'event']);
7
+ const VALID_CODES = new Set([
8
+ 'UNRESOLVED_EXPRESSION',
9
+ 'NON_RENDERABLE_VALUE',
10
+ 'MARKER_MISSING',
11
+ 'FRAGMENT_MOUNT_FAILED',
12
+ 'BINDING_APPLY_FAILED',
13
+ 'EVENT_HANDLER_FAILED'
14
+ ]);
15
+
16
+ function _truncate(input, maxLength) {
17
+ const text = String(input ?? '');
18
+ if (text.length <= maxLength) return text;
19
+ return `${text.slice(0, maxLength - 3)}...`;
20
+ }
21
+
22
+ function _sanitizeAbsolutePaths(value) {
23
+ return String(value ?? '')
24
+ .replace(/[A-Za-z]:\\[^\s"'`]+/g, '<path>')
25
+ .replace(/\/Users\/[^\s"'`]+/g, '<path>')
26
+ .replace(/\/home\/[^\s"'`]+/g, '<path>')
27
+ .replace(/\/private\/[^\s"'`]+/g, '<path>')
28
+ .replace(/\/tmp\/[^\s"'`]+/g, '<path>')
29
+ .replace(/\/var\/folders\/[^\s"'`]+/g, '<path>');
30
+ }
31
+
32
+ function _sanitizeMessage(value) {
33
+ const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
34
+ return _truncate(compact || 'Runtime failure', MAX_MESSAGE_LENGTH);
35
+ }
36
+
37
+ function _sanitizeHint(value) {
38
+ if (value === null || value === undefined || value === false) {
39
+ return undefined;
40
+ }
41
+ const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
42
+ if (!compact) return undefined;
43
+ return _truncate(compact, MAX_HINT_LENGTH);
44
+ }
45
+
46
+ function _sanitizePath(value) {
47
+ if (value === null || value === undefined || value === false) {
48
+ return undefined;
49
+ }
50
+ const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
51
+ if (!compact) return undefined;
52
+ return _truncate(compact, MAX_PATH_LENGTH);
53
+ }
54
+
55
+ function _normalizeMarker(marker) {
56
+ if (!marker || typeof marker !== 'object') return undefined;
57
+ const markerType = _truncate(_sanitizeAbsolutePaths(marker.type || 'data-zx'), 48);
58
+ const markerId = marker.id;
59
+ if (markerId === null || markerId === undefined || markerId === '') return undefined;
60
+ if (typeof markerId === 'number') {
61
+ return { type: markerType, id: markerId };
62
+ }
63
+ return { type: markerType, id: _truncate(_sanitizeAbsolutePaths(markerId), 48) };
64
+ }
65
+
66
+ function _extractErrorMessage(error) {
67
+ if (!error) return '';
68
+ if (typeof error === 'string') return error;
69
+ if (error instanceof Error && typeof error.message === 'string') return error.message;
70
+ if (typeof error.message === 'string') return error.message;
71
+ return String(error);
72
+ }
73
+
74
+ function _safeJson(payload) {
75
+ try {
76
+ return JSON.stringify(payload, null, 2);
77
+ } catch {
78
+ return '{"kind":"ZENITH_RUNTIME_ERROR","message":"Unable to serialize runtime error payload"}';
79
+ }
80
+ }
81
+
82
+ function _isDevDiagnosticsMode() {
83
+ const runtime = typeof globalThis !== 'undefined' ? globalThis : {};
84
+ if (runtime.__ZENITH_RUNTIME_DEV__ === true || runtime.__ZENITH_DEV__ === true) {
85
+ return true;
86
+ }
87
+ if (runtime.__ZENITH_RUNTIME_DEV__ === false || runtime.__ZENITH_DEV__ === false) {
88
+ return false;
89
+ }
90
+ if (runtime.__ZENITH_RUNTIME_PROD__ === true) {
91
+ return false;
92
+ }
93
+ if (typeof location !== 'undefined' && location && typeof location.hostname === 'string') {
94
+ const host = String(location.hostname).toLowerCase();
95
+ if (host === 'localhost' || host === '127.0.0.1' || host === '[::1]') {
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+
102
+ function _renderOverlay(payload) {
103
+ if (!_isDevDiagnosticsMode()) return;
104
+ if (typeof document === 'undefined' || !document.body) return;
105
+
106
+ let overlay = document.getElementById(OVERLAY_ID);
107
+ if (!overlay) {
108
+ overlay = document.createElement('aside');
109
+ overlay.id = OVERLAY_ID;
110
+ overlay.setAttribute('role', 'alert');
111
+ overlay.setAttribute('aria-live', 'assertive');
112
+ overlay.style.position = 'fixed';
113
+ overlay.style.left = '12px';
114
+ overlay.style.right = '12px';
115
+ overlay.style.bottom = '12px';
116
+ overlay.style.maxHeight = '45vh';
117
+ overlay.style.overflow = 'auto';
118
+ overlay.style.zIndex = '2147483647';
119
+ overlay.style.padding = '12px';
120
+ overlay.style.border = '1px solid #ff6b6b';
121
+ overlay.style.borderRadius = '8px';
122
+ overlay.style.background = '#111';
123
+ overlay.style.color = '#ffe5e5';
124
+ overlay.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, monospace';
125
+ overlay.style.fontSize = '12px';
126
+ overlay.style.lineHeight = '1.45';
127
+ overlay.style.boxShadow = '0 12px 40px rgba(0,0,0,0.45)';
128
+
129
+ const copyButton = document.createElement('button');
130
+ copyButton.type = 'button';
131
+ copyButton.setAttribute('data-zx-runtime-copy', 'true');
132
+ copyButton.style.marginTop = '8px';
133
+ copyButton.style.padding = '4px 8px';
134
+ copyButton.style.border = '1px solid #ff9d9d';
135
+ copyButton.style.borderRadius = '4px';
136
+ copyButton.style.background = '#2a2a2a';
137
+ copyButton.style.color = '#ffe5e5';
138
+ copyButton.style.cursor = 'pointer';
139
+ copyButton.textContent = 'Copy JSON';
140
+ overlay.appendChild(copyButton);
141
+
142
+ document.body.appendChild(overlay);
143
+ }
144
+
145
+ const textLines = [
146
+ 'Zenith Runtime Error',
147
+ `phase: ${payload.phase}`,
148
+ `code: ${payload.code}`,
149
+ `message: ${payload.message}`
150
+ ];
151
+
152
+ if (payload.marker) {
153
+ textLines.push(`marker: ${payload.marker.type}#${payload.marker.id}`);
154
+ }
155
+ if (payload.path) {
156
+ textLines.push(`path: ${payload.path}`);
157
+ }
158
+ if (payload.hint) {
159
+ textLines.push(`hint: ${payload.hint}`);
160
+ }
161
+
162
+ const jsonText = _safeJson(payload);
163
+ const panelText = textLines.join('\n');
164
+
165
+ let pre = overlay.querySelector('pre[data-zx-runtime-error]');
166
+ if (!pre) {
167
+ pre = document.createElement('pre');
168
+ pre.setAttribute('data-zx-runtime-error', 'true');
169
+ pre.style.margin = '0';
170
+ pre.style.whiteSpace = 'pre-wrap';
171
+ pre.style.wordBreak = 'break-word';
172
+ overlay.insertBefore(pre, overlay.firstChild);
173
+ }
174
+ pre.textContent = panelText;
175
+
176
+ const copyButton = overlay.querySelector('button[data-zx-runtime-copy="true"]');
177
+ if (copyButton) {
178
+ copyButton.onclick = () => {
179
+ const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : null;
180
+ if (clipboard && typeof clipboard.writeText === 'function') {
181
+ void clipboard.writeText(jsonText);
182
+ }
183
+ };
184
+ }
185
+ }
186
+
187
+ function _mapLegacyError(error, fallback) {
188
+ const rawMessage = _extractErrorMessage(error);
189
+ const safeMessage = _sanitizeMessage(rawMessage);
190
+
191
+ const details = {
192
+ phase: VALID_PHASES.has(fallback.phase) ? fallback.phase : 'hydrate',
193
+ code: VALID_CODES.has(fallback.code) ? fallback.code : 'BINDING_APPLY_FAILED',
194
+ message: _sanitizeMessage(fallback.message || safeMessage),
195
+ marker: _normalizeMarker(fallback.marker),
196
+ path: _sanitizePath(fallback.path),
197
+ hint: _sanitizeHint(fallback.hint)
198
+ };
199
+
200
+ if (/failed to resolve expression literal/i.test(rawMessage)) {
201
+ details.phase = 'bind';
202
+ details.code = 'UNRESOLVED_EXPRESSION';
203
+ details.hint = details.hint || 'Verify expression scope keys and signal aliases.';
204
+ } else if (/non-renderable (object|function)/i.test(rawMessage)) {
205
+ details.phase = 'render';
206
+ details.code = 'NON_RENDERABLE_VALUE';
207
+ const match = rawMessage.match(/at\s+([A-Za-z0-9_\[\].-]+)/);
208
+ if (match && !details.path) {
209
+ details.path = _sanitizePath(match[1]);
210
+ }
211
+ details.hint = details.hint || 'Use map() to render object fields into nodes.';
212
+ } else if (/unresolved .* marker index/i.test(rawMessage)) {
213
+ details.phase = 'bind';
214
+ details.code = 'MARKER_MISSING';
215
+ const markerMatch = rawMessage.match(/unresolved\s+(\w+)\s+marker index\s+(\d+)/i);
216
+ if (markerMatch && !details.marker) {
217
+ details.marker = {
218
+ type: `data-zx-${markerMatch[1]}`,
219
+ id: Number(markerMatch[2])
220
+ };
221
+ }
222
+ details.hint = details.hint || 'Confirm SSR markers and client selector tables match.';
223
+ }
224
+
225
+ return details;
226
+ }
227
+
228
+ export function isZenithRuntimeError(error) {
229
+ return !!(
230
+ error &&
231
+ typeof error === 'object' &&
232
+ error.zenithRuntimeError &&
233
+ error.zenithRuntimeError.kind === 'ZENITH_RUNTIME_ERROR'
234
+ );
235
+ }
236
+
237
+ export function createZenithRuntimeError(details, cause) {
238
+ const phase = VALID_PHASES.has(details?.phase) ? details.phase : 'hydrate';
239
+ const code = VALID_CODES.has(details?.code) ? details.code : 'BINDING_APPLY_FAILED';
240
+ const message = _sanitizeMessage(details?.message || 'Runtime failure');
241
+
242
+ const payload = {
243
+ kind: 'ZENITH_RUNTIME_ERROR',
244
+ phase,
245
+ code,
246
+ message
247
+ };
248
+
249
+ const marker = _normalizeMarker(details?.marker);
250
+ if (marker) payload.marker = marker;
251
+
252
+ const path = _sanitizePath(details?.path);
253
+ if (path) payload.path = path;
254
+
255
+ const hint = _sanitizeHint(details?.hint);
256
+ if (hint) payload.hint = hint;
257
+
258
+ const error = new Error(`[Zenith Runtime] ${code}: ${message}`);
259
+ error.name = 'ZenithRuntimeError';
260
+ error.zenithRuntimeError = payload;
261
+ if (cause !== undefined) {
262
+ error.cause = cause;
263
+ }
264
+ error.toJSON = () => payload;
265
+ return error;
266
+ }
267
+
268
+ function _reportRuntimeError(error) {
269
+ if (!error || error.__zenithRuntimeErrorReported === true) return;
270
+ error.__zenithRuntimeErrorReported = true;
271
+ const payload = error.zenithRuntimeError;
272
+ if (payload && typeof console !== 'undefined' && typeof console.error === 'function') {
273
+ console.error('[Zenith Runtime]', payload);
274
+ }
275
+ _renderOverlay(payload);
276
+ }
277
+
278
+ export function throwZenithRuntimeError(details, cause) {
279
+ const error = createZenithRuntimeError(details, cause);
280
+ _reportRuntimeError(error);
281
+ throw error;
282
+ }
283
+
284
+ export function rethrowZenithRuntimeError(error, fallback = {}) {
285
+ if (isZenithRuntimeError(error)) {
286
+ const payload = error.zenithRuntimeError || {};
287
+ let updatedPayload = payload;
288
+ const marker = !payload.marker ? _normalizeMarker(fallback.marker) : payload.marker;
289
+ const path = !payload.path ? _sanitizePath(fallback.path) : payload.path;
290
+ const hint = !payload.hint ? _sanitizeHint(fallback.hint) : payload.hint;
291
+
292
+ if (marker || path || hint) {
293
+ updatedPayload = {
294
+ ...payload,
295
+ ...(marker ? { marker } : null),
296
+ ...(path ? { path } : null),
297
+ ...(hint ? { hint } : null)
298
+ };
299
+ error.zenithRuntimeError = updatedPayload;
300
+ error.toJSON = () => updatedPayload;
301
+ }
302
+ _reportRuntimeError(error);
303
+ throw error;
304
+ }
305
+ const mapped = _mapLegacyError(error, fallback || {});
306
+ const wrapped = createZenithRuntimeError(mapped, error);
307
+ _reportRuntimeError(wrapped);
308
+ throw wrapped;
309
+ }
package/dist/effect.js ADDED
@@ -0,0 +1,3 @@
1
+ // Deprecated compatibility shim.
2
+ // Runtime V0 contract uses explicit `zeneffect(deps, fn)`.
3
+ export { zeneffect as effect } from './zeneffect.js';
package/dist/events.js ADDED
@@ -0,0 +1,59 @@
1
+ // ---------------------------------------------------------------------------
2
+ // events.js — Zenith Runtime V0
3
+ // ---------------------------------------------------------------------------
4
+ // Event binding system.
5
+ //
6
+ // For: <button data-zx-on-click="1">
7
+ //
8
+ // Algorithm:
9
+ // 1. Extract event name from attribute suffix ("click")
10
+ // 2. Resolve expression at index → must be a function
11
+ // 3. addEventListener directly
12
+ //
13
+ // No synthetic events.
14
+ // No delegation.
15
+ // No wrapping.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ import { _registerListener } from './cleanup.js';
19
+ import { rethrowZenithRuntimeError, throwZenithRuntimeError } from './diagnostics.js';
20
+
21
+ /**
22
+ * Bind an event listener to a DOM element.
23
+ *
24
+ * @param {Element} element - The DOM element
25
+ * @param {string} eventName - The event name (e.g. "click")
26
+ * @param {() => any} exprFn - Pre-bound expression function that must resolve to a handler
27
+ */
28
+ export function bindEvent(element, eventName, exprFn) {
29
+ const resolved = exprFn();
30
+
31
+ if (typeof resolved !== 'function') {
32
+ throwZenithRuntimeError({
33
+ phase: 'bind',
34
+ code: 'BINDING_APPLY_FAILED',
35
+ message: `Event binding did not resolve to a function for "${eventName}"`,
36
+ marker: { type: `data-zx-on-${eventName}`, id: '<unknown>' },
37
+ path: `event:${eventName}`,
38
+ hint: 'Bind events to function references.'
39
+ });
40
+ }
41
+
42
+ const wrapped = function zenithBoundEvent(event) {
43
+ try {
44
+ return resolved.call(this, event);
45
+ } catch (error) {
46
+ rethrowZenithRuntimeError(error, {
47
+ phase: 'event',
48
+ code: 'EVENT_HANDLER_FAILED',
49
+ message: `Event handler failed for "${eventName}"`,
50
+ marker: { type: `data-zx-on-${eventName}`, id: '<unknown>' },
51
+ path: `event:${eventName}:${resolved.name || '<anonymous>'}`,
52
+ hint: 'Inspect handler logic and referenced state.'
53
+ });
54
+ }
55
+ };
56
+
57
+ element.addEventListener(eventName, wrapped);
58
+ _registerListener(element, eventName, wrapped);
59
+ }