@zenithbuild/runtime 0.6.13 → 0.7.0

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.
@@ -2,6 +2,9 @@ const OVERLAY_ID = '__zenith_runtime_error_overlay';
2
2
  const MAX_MESSAGE_LENGTH = 120;
3
3
  const MAX_HINT_LENGTH = 140;
4
4
  const MAX_PATH_LENGTH = 120;
5
+ const MAX_DOCS_LINK_LENGTH = 180;
6
+ const MAX_SNIPPET_LENGTH = 220;
7
+ const MAX_STACK_LENGTH = 420;
5
8
  const VALID_PHASES = new Set(['hydrate', 'bind', 'render', 'event']);
6
9
  const VALID_CODES = new Set([
7
10
  'UNRESOLVED_EXPRESSION',
@@ -9,8 +12,20 @@ const VALID_CODES = new Set([
9
12
  'MARKER_MISSING',
10
13
  'FRAGMENT_MOUNT_FAILED',
11
14
  'BINDING_APPLY_FAILED',
12
- 'EVENT_HANDLER_FAILED'
15
+ 'EVENT_HANDLER_FAILED',
16
+ 'COMPONENT_BOOTSTRAP_FAILED',
17
+ 'UNSAFE_MEMBER_ACCESS'
13
18
  ]);
19
+ const DOCS_LINK_BY_CODE = Object.freeze({
20
+ UNRESOLVED_EXPRESSION: '/docs/documentation/reference/reactive-binding-model.md#expression-resolution',
21
+ NON_RENDERABLE_VALUE: '/docs/documentation/reference/reactive-binding-model.md#renderable-values',
22
+ MARKER_MISSING: '/docs/documentation/reference/markers.md',
23
+ FRAGMENT_MOUNT_FAILED: '/docs/documentation/contracts/runtime-contract.md#fragment-contract',
24
+ BINDING_APPLY_FAILED: '/docs/documentation/contracts/runtime-contract.md#binding-application',
25
+ EVENT_HANDLER_FAILED: '/docs/documentation/contracts/runtime-contract.md#event-bindings',
26
+ COMPONENT_BOOTSTRAP_FAILED: '/docs/documentation/contracts/runtime-contract.md#component-bootstrap',
27
+ UNSAFE_MEMBER_ACCESS: '/docs/documentation/reference/reactive-binding-model.md#expression-resolution'
28
+ });
14
29
  function _truncate(input, maxLength) {
15
30
  const text = String(input ?? '');
16
31
  if (text.length <= maxLength)
@@ -48,6 +63,49 @@ function _sanitizePath(value) {
48
63
  return undefined;
49
64
  return _truncate(compact, MAX_PATH_LENGTH);
50
65
  }
66
+ function _sanitizeDocsLink(value) {
67
+ if (value === null || value === undefined || value === false) {
68
+ return undefined;
69
+ }
70
+ const compact = String(value).replace(/\s+/g, ' ').trim();
71
+ if (!compact)
72
+ return undefined;
73
+ return _truncate(compact, MAX_DOCS_LINK_LENGTH);
74
+ }
75
+ function _sanitizeSourceLocation(value) {
76
+ if (!value || typeof value !== 'object')
77
+ return undefined;
78
+ const line = Number(value.line);
79
+ const column = Number(value.column);
80
+ if (!Number.isInteger(line) || !Number.isInteger(column)) {
81
+ return undefined;
82
+ }
83
+ if (line < 1 || column < 1) {
84
+ return undefined;
85
+ }
86
+ return { line, column };
87
+ }
88
+ function _sanitizeSource(value) {
89
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
90
+ return undefined;
91
+ }
92
+ const fileRaw = value.file;
93
+ const file = typeof fileRaw === 'string' ? _truncate(fileRaw.trim(), 240) : '';
94
+ if (!file) {
95
+ return undefined;
96
+ }
97
+ const start = _sanitizeSourceLocation(value.start);
98
+ const end = _sanitizeSourceLocation(value.end);
99
+ const snippet = typeof value.snippet === 'string'
100
+ ? _truncate(value.snippet.replace(/\s+/g, ' ').trim(), MAX_SNIPPET_LENGTH)
101
+ : undefined;
102
+ return {
103
+ file,
104
+ ...(start ? { start } : null),
105
+ ...(end ? { end } : null),
106
+ ...(snippet ? { snippet } : null)
107
+ };
108
+ }
51
109
  function _normalizeMarker(marker) {
52
110
  if (!marker || typeof marker !== 'object')
53
111
  return undefined;
@@ -170,9 +228,25 @@ function _renderOverlay(payload) {
170
228
  if (payload.path) {
171
229
  textLines.push(`path: ${payload.path}`);
172
230
  }
231
+ if (payload.source && payload.source.file) {
232
+ const line = payload.source.start?.line;
233
+ const column = payload.source.start?.column;
234
+ if (Number.isInteger(line) && Number.isInteger(column)) {
235
+ textLines.push(`source: ${payload.source.file}:${line}:${column}`);
236
+ }
237
+ else {
238
+ textLines.push(`source: ${payload.source.file}`);
239
+ }
240
+ if (payload.source.snippet) {
241
+ textLines.push(`snippet: ${payload.source.snippet}`);
242
+ }
243
+ }
173
244
  if (payload.hint) {
174
245
  textLines.push(`hint: ${payload.hint}`);
175
246
  }
247
+ if (payload.docsLink) {
248
+ textLines.push(`docs: ${payload.docsLink}`);
249
+ }
176
250
  const jsonText = _safeJson(payload);
177
251
  const panelText = textLines.join('\n');
178
252
  let pre = overlay.querySelector('pre[data-zx-runtime-error]');
@@ -204,7 +278,9 @@ function _mapLegacyError(error, fallback) {
204
278
  message: _sanitizeMessage(fallback.message || safeMessage),
205
279
  marker: _normalizeMarker(fallback.marker),
206
280
  path: _sanitizePath(fallback.path),
207
- hint: _sanitizeHint(fallback.hint)
281
+ hint: _sanitizeHint(fallback.hint),
282
+ source: _sanitizeSource(fallback.source),
283
+ docsLink: _sanitizeDocsLink(fallback.docsLink)
208
284
  };
209
285
  if (/failed to resolve expression literal/i.test(rawMessage)) {
210
286
  details.phase = 'bind';
@@ -232,6 +308,9 @@ function _mapLegacyError(error, fallback) {
232
308
  }
233
309
  details.hint = details.hint || 'Confirm SSR markers and client selector tables match.';
234
310
  }
311
+ if (!details.docsLink) {
312
+ details.docsLink = DOCS_LINK_BY_CODE[details.code];
313
+ }
235
314
  return details;
236
315
  }
237
316
  export function isZenithRuntimeError(error) {
@@ -244,11 +323,13 @@ export function createZenithRuntimeError(details, cause) {
244
323
  const phase = VALID_PHASES.has(details?.phase) ? details.phase : 'hydrate';
245
324
  const code = VALID_CODES.has(details?.code) ? details.code : 'BINDING_APPLY_FAILED';
246
325
  const message = _sanitizeMessage(details?.message || 'Runtime failure');
326
+ const docsLink = _sanitizeDocsLink(details?.docsLink || DOCS_LINK_BY_CODE[code]);
247
327
  const payload = {
248
328
  kind: 'ZENITH_RUNTIME_ERROR',
249
329
  phase,
250
330
  code,
251
- message
331
+ message,
332
+ ...(docsLink ? { docsLink } : null)
252
333
  };
253
334
  const marker = _normalizeMarker(details?.marker);
254
335
  if (marker)
@@ -259,6 +340,13 @@ export function createZenithRuntimeError(details, cause) {
259
340
  const hint = _sanitizeHint(details?.hint);
260
341
  if (hint)
261
342
  payload.hint = hint;
343
+ const source = _sanitizeSource(details?.source);
344
+ if (source)
345
+ payload.source = source;
346
+ const stack = _sanitizeHint(details?.stack);
347
+ if (stack) {
348
+ payload.stack = _truncate(stack, MAX_STACK_LENGTH);
349
+ }
262
350
  const error = new Error(`[Zenith Runtime] ${code}: ${message}`);
263
351
  error.name = 'ZenithRuntimeError';
264
352
  error.zenithRuntimeError = payload;
@@ -293,12 +381,18 @@ export function rethrowZenithRuntimeError(error, fallback = {}) {
293
381
  const marker = !payload.marker ? _normalizeMarker(fallback.marker) : payload.marker;
294
382
  const path = !payload.path ? _sanitizePath(fallback.path) : payload.path;
295
383
  const hint = !payload.hint ? _sanitizeHint(fallback.hint) : payload.hint;
296
- if (marker || path || hint) {
384
+ const source = !payload.source ? _sanitizeSource(fallback.source) : payload.source;
385
+ const docsLink = !payload.docsLink
386
+ ? _sanitizeDocsLink(fallback.docsLink || DOCS_LINK_BY_CODE[payload.code])
387
+ : payload.docsLink;
388
+ if (marker || path || hint || source || docsLink) {
297
389
  updatedPayload = {
298
390
  ...payload,
299
391
  ...(marker ? { marker } : null),
300
392
  ...(path ? { path } : null),
301
- ...(hint ? { hint } : null)
393
+ ...(hint ? { hint } : null),
394
+ ...(source ? { source } : null),
395
+ ...(docsLink ? { docsLink } : null)
302
396
  };
303
397
  error.zenithRuntimeError = updatedPayload;
304
398
  error.toJSON = () => updatedPayload;
package/dist/events.js CHANGED
@@ -29,10 +29,11 @@ export function bindEvent(element, eventName, exprFn) {
29
29
  throwZenithRuntimeError({
30
30
  phase: 'bind',
31
31
  code: 'BINDING_APPLY_FAILED',
32
- message: `Event binding did not resolve to a function for "${eventName}"`,
32
+ message: `Event binding expected a function reference for "${eventName}"`,
33
33
  marker: { type: `data-zx-on-${eventName}`, id: '<unknown>' },
34
34
  path: `event:${eventName}`,
35
- hint: 'Bind events to function references.'
35
+ hint: 'Use on:*={handler} and ensure forwarded props are function-valued.',
36
+ docsLink: '/docs/documentation/contracts/runtime-contract.md#event-bindings'
36
37
  });
37
38
  }
38
39
  const wrapped = function zenithBoundEvent(event) {
@@ -46,7 +47,8 @@ export function bindEvent(element, eventName, exprFn) {
46
47
  message: `Event handler failed for "${eventName}"`,
47
48
  marker: { type: `data-zx-on-${eventName}`, id: '<unknown>' },
48
49
  path: `event:${eventName}:${resolved.name || '<anonymous>'}`,
49
- hint: 'Inspect handler logic and referenced state.'
50
+ hint: 'Inspect handler logic and referenced state.',
51
+ docsLink: '/docs/documentation/contracts/runtime-contract.md#event-bindings'
50
52
  });
51
53
  }
52
54
  };
package/dist/hydrate.d.ts CHANGED
@@ -4,14 +4,14 @@
4
4
  * @param {{
5
5
  * ir_version: number,
6
6
  * root: Document | Element,
7
- * expressions: Array<{ marker_index: number, signal_index?: number|null, state_index?: number|null, component_instance?: string|null, component_binding?: string|null, literal?: string|null }>,
8
- * markers: Array<{ index: number, kind: 'text' | 'attr' | 'event', selector: string, attr?: string }>,
9
- * events: Array<{ index: number, event: string, selector: string }>,
10
- * refs?: Array<{ index: number, state_index: number, selector: string }>,
7
+ * expressions: Array<{ marker_index: number, signal_index?: number|null, state_index?: number|null, component_instance?: string|null, component_binding?: string|null, literal?: string|null, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
8
+ * markers: Array<{ index: number, kind: 'text' | 'attr' | 'event', selector: string, attr?: string, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
9
+ * events: Array<{ index: number, event: string, selector: string, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
10
+ * refs?: Array<{ index: number, state_index: number, selector: string, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
11
11
  * state_values: Array<*>,
12
12
  * state_keys?: Array<string>,
13
13
  * signals: Array<{ id: number, kind: 'signal', state_index: number }>,
14
- * components?: Array<{ instance: string, selector: string, create: Function }>
14
+ * components?: Array<{ instance: string, selector: string, create: Function, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>
15
15
  * }} payload
16
16
  * @returns {() => void}
17
17
  */
@@ -25,22 +25,70 @@ export function hydrate(payload: {
25
25
  component_instance?: string | null;
26
26
  component_binding?: string | null;
27
27
  literal?: string | null;
28
+ source?: {
29
+ file: string;
30
+ start?: {
31
+ line: number;
32
+ column: number;
33
+ };
34
+ end?: {
35
+ line: number;
36
+ column: number;
37
+ };
38
+ snippet?: string;
39
+ };
28
40
  }>;
29
41
  markers: Array<{
30
42
  index: number;
31
43
  kind: "text" | "attr" | "event";
32
44
  selector: string;
33
45
  attr?: string;
46
+ source?: {
47
+ file: string;
48
+ start?: {
49
+ line: number;
50
+ column: number;
51
+ };
52
+ end?: {
53
+ line: number;
54
+ column: number;
55
+ };
56
+ snippet?: string;
57
+ };
34
58
  }>;
35
59
  events: Array<{
36
60
  index: number;
37
61
  event: string;
38
62
  selector: string;
63
+ source?: {
64
+ file: string;
65
+ start?: {
66
+ line: number;
67
+ column: number;
68
+ };
69
+ end?: {
70
+ line: number;
71
+ column: number;
72
+ };
73
+ snippet?: string;
74
+ };
39
75
  }>;
40
76
  refs?: Array<{
41
77
  index: number;
42
78
  state_index: number;
43
79
  selector: string;
80
+ source?: {
81
+ file: string;
82
+ start?: {
83
+ line: number;
84
+ column: number;
85
+ };
86
+ end?: {
87
+ line: number;
88
+ column: number;
89
+ };
90
+ snippet?: string;
91
+ };
44
92
  }>;
45
93
  state_values: Array<any>;
46
94
  state_keys?: Array<string>;
@@ -53,5 +101,17 @@ export function hydrate(payload: {
53
101
  instance: string;
54
102
  selector: string;
55
103
  create: Function;
104
+ source?: {
105
+ file: string;
106
+ start?: {
107
+ line: number;
108
+ column: number;
109
+ };
110
+ end?: {
111
+ line: number;
112
+ column: number;
113
+ };
114
+ snippet?: string;
115
+ };
56
116
  }>;
57
117
  }): () => void;
package/dist/hydrate.js CHANGED
@@ -15,26 +15,34 @@ const ALIAS_CONFLICT = Symbol('alias_conflict');
15
15
  const ACTIVE_MARKER_CLASS = 'z-active';
16
16
  const UNRESOLVED_LITERAL = Symbol('unresolved_literal');
17
17
  const LEGACY_MARKUP_HELPER = 'html';
18
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
18
19
  const BOOLEAN_ATTRIBUTES = new Set([
19
20
  'disabled', 'checked', 'selected', 'readonly', 'multiple',
20
21
  'hidden', 'autofocus', 'required', 'open'
21
22
  ]);
22
23
  const STRICT_MEMBER_CHAIN_LITERAL_RE = /^(?:true|false|null|undefined|[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*)$/;
23
24
  const UNSAFE_MEMBER_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
25
+ const DOCS_LINKS = Object.freeze({
26
+ eventBinding: '/docs/documentation/contracts/runtime-contract.md#event-bindings',
27
+ expressionScope: '/docs/documentation/reference/reactive-binding-model.md#expression-resolution',
28
+ markerTable: '/docs/documentation/reference/markers.md',
29
+ componentBootstrap: '/docs/documentation/contracts/runtime-contract.md#component-bootstrap',
30
+ refs: '/docs/documentation/reference/reactive-binding-model.md#refs-and-mount'
31
+ });
24
32
  /**
25
33
  * Hydrate a pre-rendered DOM tree using explicit payload tables.
26
34
  *
27
35
  * @param {{
28
36
  * ir_version: number,
29
37
  * root: Document | Element,
30
- * expressions: Array<{ marker_index: number, signal_index?: number|null, state_index?: number|null, component_instance?: string|null, component_binding?: string|null, literal?: string|null }>,
31
- * markers: Array<{ index: number, kind: 'text' | 'attr' | 'event', selector: string, attr?: string }>,
32
- * events: Array<{ index: number, event: string, selector: string }>,
33
- * refs?: Array<{ index: number, state_index: number, selector: string }>,
38
+ * expressions: Array<{ marker_index: number, signal_index?: number|null, state_index?: number|null, component_instance?: string|null, component_binding?: string|null, literal?: string|null, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
39
+ * markers: Array<{ index: number, kind: 'text' | 'attr' | 'event', selector: string, attr?: string, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
40
+ * events: Array<{ index: number, event: string, selector: string, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
41
+ * refs?: Array<{ index: number, state_index: number, selector: string, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
34
42
  * state_values: Array<*>,
35
43
  * state_keys?: Array<string>,
36
44
  * signals: Array<{ id: number, kind: 'signal', state_index: number }>,
37
- * components?: Array<{ instance: string, selector: string, create: Function }>
45
+ * components?: Array<{ instance: string, selector: string, create: Function, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>
38
46
  * }} payload
39
47
  * @returns {() => void}
40
48
  */
@@ -61,7 +69,7 @@ export function hydrate(payload) {
61
69
  for (let i = 0; i < refs.length; i++) {
62
70
  const refBinding = refs[i];
63
71
  const targetRef = stateValues[refBinding.state_index];
64
- const nodes = _resolveNodes(root, refBinding.selector, refBinding.index, 'ref');
72
+ const nodes = _resolveNodes(root, refBinding.selector, refBinding.index, 'ref', refBinding.source);
65
73
  targetRef.current = nodes[0] || null;
66
74
  hydratedRefs.push(targetRef);
67
75
  }
@@ -78,38 +86,56 @@ export function hydrate(payload) {
78
86
  component: component.instance,
79
87
  route
80
88
  }));
81
- const hosts = _resolveNodes(root, component.selector, i, 'component');
89
+ const hosts = _resolveNodes(root, component.selector, i, 'component', component.source);
82
90
  for (let j = 0; j < hosts.length; j++) {
83
- const componentScope = createSideEffectScope(`${component.instance}:${j}`);
84
- const runtimeApi = {
85
- signal,
86
- state,
87
- zeneffect(effect, dependenciesOrOptions) {
88
- return zeneffect(effect, dependenciesOrOptions, componentScope);
89
- },
90
- zenEffect(effect, options) {
91
- return zenEffect(effect, options, componentScope);
92
- },
93
- zenMount(callback) {
94
- return zenMount(callback, componentScope);
91
+ try {
92
+ const componentScope = createSideEffectScope(`${component.instance}:${j}`);
93
+ const runtimeApi = {
94
+ signal,
95
+ state,
96
+ zeneffect(effect, dependenciesOrOptions) {
97
+ return zeneffect(effect, dependenciesOrOptions, componentScope);
98
+ },
99
+ zenEffect(effect, options) {
100
+ return zenEffect(effect, options, componentScope);
101
+ },
102
+ zenMount(callback) {
103
+ return zenMount(callback, componentScope);
104
+ }
105
+ };
106
+ const instance = component.create(hosts[j], resolvedProps, runtimeApi);
107
+ if (!instance || typeof instance !== 'object') {
108
+ throw new Error(`[Zenith Runtime] component factory for ${component.instance} must return an object`);
109
+ }
110
+ if (typeof instance.mount === 'function') {
111
+ instance.mount();
112
+ }
113
+ activateSideEffectScope(componentScope);
114
+ _registerDisposer(() => {
115
+ disposeSideEffectScope(componentScope);
116
+ if (typeof instance.destroy === 'function') {
117
+ instance.destroy();
118
+ }
119
+ });
120
+ if (instance.bindings && typeof instance.bindings === 'object') {
121
+ componentBindings[component.instance] = instance.bindings;
95
122
  }
96
- };
97
- const instance = component.create(hosts[j], resolvedProps, runtimeApi);
98
- if (!instance || typeof instance !== 'object') {
99
- throw new Error(`[Zenith Runtime] component factory for ${component.instance} must return an object`);
100
- }
101
- if (typeof instance.mount === 'function') {
102
- instance.mount();
103
123
  }
104
- activateSideEffectScope(componentScope);
105
- _registerDisposer(() => {
106
- disposeSideEffectScope(componentScope);
107
- if (typeof instance.destroy === 'function') {
108
- instance.destroy();
124
+ catch (error) {
125
+ try {
126
+ rethrowZenithRuntimeError(error, {
127
+ phase: 'hydrate',
128
+ code: 'COMPONENT_BOOTSTRAP_FAILED',
129
+ message: `Component bootstrap failed for "${component.instance}"`,
130
+ path: `component[${component.instance}]`,
131
+ hint: 'Fix the failing component and refresh; other components continue mounting.',
132
+ docsLink: DOCS_LINKS.componentBootstrap,
133
+ source: component.source
134
+ });
135
+ }
136
+ catch {
137
+ // Fault containment: continue mounting remaining components.
109
138
  }
110
- });
111
- if (instance.bindings && typeof instance.bindings === 'object') {
112
- componentBindings[component.instance] = instance.bindings;
113
139
  }
114
140
  }
115
141
  }
@@ -134,10 +160,15 @@ export function hydrate(payload) {
134
160
  if (marker.kind === 'event') {
135
161
  continue;
136
162
  }
137
- const nodes = _resolveNodes(root, marker.selector, marker.index, marker.kind);
163
+ const nodes = _resolveNodes(root, marker.selector, marker.index, marker.kind, marker.source);
138
164
  markerNodesByIndex.set(marker.index, nodes);
139
- const value = _evaluateExpression(expressions[marker.index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns);
140
- _applyMarkerValue(nodes, marker, value);
165
+ try {
166
+ const value = _evaluateExpression(expressions[marker.index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns, marker, null);
167
+ _applyMarkerValue(nodes, marker, value);
168
+ }
169
+ catch (evalErr) {
170
+ throw evalErr;
171
+ }
141
172
  }
142
173
  for (let i = 0; i < expressions.length; i++) {
143
174
  if (!markerIndices.has(i)) {
@@ -149,9 +180,9 @@ export function hydrate(payload) {
149
180
  if (!marker || marker.kind === 'event') {
150
181
  return;
151
182
  }
152
- const nodes = markerNodesByIndex.get(index) || _resolveNodes(root, marker.selector, marker.index, marker.kind);
183
+ const nodes = markerNodesByIndex.get(index) || _resolveNodes(root, marker.selector, marker.index, marker.kind, marker.source);
153
184
  markerNodesByIndex.set(index, nodes);
154
- const value = _evaluateExpression(expressions[index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns);
185
+ const value = _evaluateExpression(expressions[index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns, marker, null);
155
186
  _applyMarkerValue(nodes, marker, value);
156
187
  }
157
188
  const dependentMarkersBySignal = new Map();
@@ -225,16 +256,21 @@ export function hydrate(payload) {
225
256
  throw new Error(`[Zenith Runtime] duplicate event index ${eventBinding.index}`);
226
257
  }
227
258
  eventIndices.add(eventBinding.index);
228
- const nodes = _resolveNodes(root, eventBinding.selector, eventBinding.index, 'event');
229
- const handler = _evaluateExpression(expressions[eventBinding.index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, 'event', props || {}, exprFns);
259
+ const marker = markerByIndex.get(eventBinding.index) || null;
260
+ const nodes = _resolveNodes(root, eventBinding.selector, eventBinding.index, 'event', eventBinding.source || marker?.source);
261
+ const expressionBinding = expressions[eventBinding.index];
262
+ const handler = _evaluateExpression(expressionBinding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, 'event', props || {}, exprFns, marker, eventBinding);
230
263
  if (typeof handler !== 'function') {
264
+ const passedExpression = _describeBindingExpression(expressionBinding);
231
265
  throwZenithRuntimeError({
232
266
  phase: 'bind',
233
267
  code: 'BINDING_APPLY_FAILED',
234
- message: `Event binding at index ${eventBinding.index} did not resolve to a function`,
268
+ message: `Event binding at index ${eventBinding.index} expected a function reference. You passed: ${passedExpression}`,
235
269
  marker: { type: `data-zx-on-${eventBinding.event}`, id: eventBinding.index },
236
270
  path: `event[${eventBinding.index}].${eventBinding.event}`,
237
- hint: 'Bind events to function references (on:click={handler}).'
271
+ hint: 'Use on:*={handler} or ensure the forwarded prop is a function.',
272
+ docsLink: DOCS_LINKS.eventBinding,
273
+ source: _resolveBindingSource(expressionBinding, marker, eventBinding)
238
274
  });
239
275
  }
240
276
  for (let j = 0; j < nodes.length; j++) {
@@ -250,7 +286,9 @@ export function hydrate(payload) {
250
286
  message: `Event handler failed for "${eventBinding.event}"`,
251
287
  marker: { type: `data-zx-on-${eventBinding.event}`, id: eventBinding.index },
252
288
  path: `event[${eventBinding.index}].${eventBinding.event}`,
253
- hint: 'Inspect the handler body and referenced state.'
289
+ hint: 'Inspect the handler body and referenced state.',
290
+ docsLink: DOCS_LINKS.eventBinding,
291
+ source: _resolveBindingSource(expressionBinding, marker, eventBinding)
254
292
  });
255
293
  }
256
294
  };
@@ -314,7 +352,8 @@ export function hydrate(payload) {
314
352
  rethrowZenithRuntimeError(error, {
315
353
  phase: 'hydrate',
316
354
  code: 'BINDING_APPLY_FAILED',
317
- hint: 'Inspect marker tables, expression bindings, and the runtime overlay diagnostics.'
355
+ hint: 'Inspect marker tables, expression bindings, and the runtime overlay diagnostics.',
356
+ docsLink: DOCS_LINKS.markerTable
318
357
  });
319
358
  }
320
359
  }
@@ -390,6 +429,7 @@ function _validatePayload(payload) {
390
429
  throw new Error(`[Zenith Runtime] expression at position ${i} has invalid fn_index`);
391
430
  }
392
431
  }
432
+ _assertValidSourceSpan(expression.source, `expression[${i}]`);
393
433
  if (expression.signal_indices !== undefined) {
394
434
  if (!Array.isArray(expression.signal_indices)) {
395
435
  throw new Error(`[Zenith Runtime] expression at position ${i} must provide signal_indices[]`);
@@ -421,6 +461,7 @@ function _validatePayload(payload) {
421
461
  if (marker.kind === 'attr' && (typeof marker.attr !== 'string' || marker.attr.length === 0)) {
422
462
  throw new Error(`[Zenith Runtime] attr marker at position ${i} requires attr name`);
423
463
  }
464
+ _assertValidSourceSpan(marker.source, `marker[${i}]`);
424
465
  }
425
466
  for (let i = 0; i < events.length; i++) {
426
467
  const eventBinding = events[i];
@@ -436,6 +477,7 @@ function _validatePayload(payload) {
436
477
  if (typeof eventBinding.selector !== 'string' || eventBinding.selector.length === 0) {
437
478
  throw new Error(`[Zenith Runtime] event binding at position ${i} requires selector`);
438
479
  }
480
+ _assertValidSourceSpan(eventBinding.source, `event[${i}]`);
439
481
  }
440
482
  for (let i = 0; i < refs.length; i++) {
441
483
  const refBinding = refs[i];
@@ -453,6 +495,7 @@ function _validatePayload(payload) {
453
495
  if (typeof refBinding.selector !== 'string' || refBinding.selector.length === 0) {
454
496
  throw new Error(`[Zenith Runtime] ref binding at position ${i} requires selector`);
455
497
  }
498
+ _assertValidSourceSpan(refBinding.source, `ref[${i}]`);
456
499
  const candidate = stateValues[refBinding.state_index];
457
500
  if (!candidate || typeof candidate !== 'object' || !Object.prototype.hasOwnProperty.call(candidate, 'current')) {
458
501
  throw new Error(`[Zenith Runtime] ref binding at position ${i} must resolve to a ref-like object`);
@@ -490,6 +533,7 @@ function _validatePayload(payload) {
490
533
  if (typeof component.create !== 'function') {
491
534
  throw new Error(`[Zenith Runtime] component at position ${i} requires create() function`);
492
535
  }
536
+ _assertValidSourceSpan(component.source, `component[${i}]`);
493
537
  }
494
538
  if (payload.params !== undefined) {
495
539
  if (!payload.params || typeof payload.params !== 'object' || Array.isArray(payload.params)) {
@@ -554,6 +598,36 @@ function _validatePayload(payload) {
554
598
  };
555
599
  return Object.freeze(validatedPayload);
556
600
  }
601
+ function _assertValidSourceSpan(source, contextLabel) {
602
+ if (source === undefined || source === null) {
603
+ return;
604
+ }
605
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
606
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source must be an object`);
607
+ }
608
+ if (typeof source.file !== 'string' || source.file.length === 0) {
609
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.file must be a non-empty string`);
610
+ }
611
+ const points = ['start', 'end'];
612
+ for (let i = 0; i < points.length; i++) {
613
+ const point = source[points[i]];
614
+ if (point === undefined || point === null) {
615
+ continue;
616
+ }
617
+ if (!point || typeof point !== 'object' || Array.isArray(point)) {
618
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.${points[i]} must be an object`);
619
+ }
620
+ if (!Number.isInteger(point.line) || point.line < 1) {
621
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.${points[i]}.line must be >= 1`);
622
+ }
623
+ if (!Number.isInteger(point.column) || point.column < 1) {
624
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.${points[i]}.column must be >= 1`);
625
+ }
626
+ }
627
+ if (source.snippet !== undefined && source.snippet !== null && typeof source.snippet !== 'string') {
628
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.snippet must be a string when provided`);
629
+ }
630
+ }
557
631
  function _resolveComponentProps(propTable, signalMap, context = {}) {
558
632
  if (!Array.isArray(propTable)) {
559
633
  throw new Error('[Zenith Runtime] component props must be an array');
@@ -592,20 +666,44 @@ function _resolveComponentProps(propTable, signalMap, context = {}) {
592
666
  }
593
667
  return resolved;
594
668
  }
595
- function _resolveNodes(root, selector, index, kind) {
596
- const nodes = root.querySelectorAll(selector);
669
+ function _resolveNodes(root, selector, index, kind, source = undefined) {
670
+ const nodes = selector.startsWith('comment:')
671
+ ? _resolveCommentNodes(root, selector.slice('comment:'.length))
672
+ : root.querySelectorAll(selector);
597
673
  if (!nodes || nodes.length === 0) {
674
+ const isRef = kind === 'ref';
598
675
  throwZenithRuntimeError({
599
676
  phase: 'bind',
600
677
  code: 'MARKER_MISSING',
601
678
  message: `Unresolved ${kind} marker index ${index}`,
602
679
  marker: { type: kind, id: index },
603
680
  path: `selector:${selector}`,
604
- hint: 'Confirm SSR marker attributes and runtime selector tables match.'
681
+ hint: isRef
682
+ ? 'Use ref + zenMount and ensure the ref is bound in markup before mount.'
683
+ : 'Confirm SSR marker attributes and runtime selector tables match.',
684
+ docsLink: isRef ? DOCS_LINKS.refs : DOCS_LINKS.markerTable,
685
+ source
605
686
  });
606
687
  }
607
688
  return nodes;
608
689
  }
690
+ function _resolveCommentNodes(root, markerText) {
691
+ const walkerRoot = root && root.nodeType === 9 && root.documentElement ? root.documentElement : root;
692
+ const doc = walkerRoot && walkerRoot.ownerDocument ? walkerRoot.ownerDocument : walkerRoot;
693
+ if (!walkerRoot || !doc || typeof doc.createTreeWalker !== 'function') {
694
+ return [];
695
+ }
696
+ const nodes = [];
697
+ const walker = doc.createTreeWalker(walkerRoot, NodeFilter.SHOW_COMMENT);
698
+ let current = walker.nextNode();
699
+ while (current) {
700
+ if (current.data === markerText) {
701
+ nodes.push(current);
702
+ }
703
+ current = walker.nextNode();
704
+ }
705
+ return nodes;
706
+ }
609
707
  function _resolveExpressionSignalIndices(binding) {
610
708
  if (!binding || typeof binding !== 'object') {
611
709
  return [];
@@ -618,25 +716,30 @@ function _resolveExpressionSignalIndices(binding) {
618
716
  }
619
717
  return [];
620
718
  }
621
- function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props, exprFns) {
719
+ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props, exprFns, markerBinding = null, eventBinding = null) {
622
720
  if (binding.fn_index != null && binding.fn_index !== undefined) {
623
721
  const fns = Array.isArray(exprFns) ? exprFns : [];
624
722
  const fn = fns[binding.fn_index];
625
723
  if (typeof fn === 'function') {
626
- return fn({
627
- signalMap,
628
- params,
629
- ssrData,
630
- props: props || {},
631
- componentBindings,
632
- zenhtml: _zenhtml,
633
- fragment(html) {
634
- return {
635
- __zenith_fragment: true,
636
- html: html === null || html === undefined || html === false ? '' : String(html)
637
- };
638
- }
639
- });
724
+ try {
725
+ return fn({
726
+ signalMap,
727
+ params,
728
+ ssrData,
729
+ props: props || {},
730
+ componentBindings,
731
+ zenhtml: _zenhtml,
732
+ fragment(html) {
733
+ return {
734
+ __zenith_fragment: true,
735
+ html: html === null || html === undefined || html === false ? '' : String(html)
736
+ };
737
+ }
738
+ });
739
+ }
740
+ catch (fnErr) {
741
+ throw fnErr;
742
+ }
640
743
  }
641
744
  }
642
745
  if (binding.signal_index !== null && binding.signal_index !== undefined) {
@@ -678,7 +781,7 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
678
781
  if (binding.literal !== null && binding.literal !== undefined) {
679
782
  if (typeof binding.literal === 'string') {
680
783
  const trimmedLiteral = binding.literal.trim();
681
- const strictMemberValue = _resolveStrictMemberChainLiteral(trimmedLiteral, stateValues, stateKeys, params, ssrData, mode, props, binding.marker_index);
784
+ const strictMemberValue = _resolveStrictMemberChainLiteral(trimmedLiteral, stateValues, stateKeys, params, ssrData, mode, props, binding.marker_index, _resolveBindingSource(binding, markerBinding, eventBinding));
682
785
  if (strictMemberValue !== UNRESOLVED_LITERAL) {
683
786
  return strictMemberValue;
684
787
  }
@@ -696,16 +799,23 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
696
799
  return primitiveValue;
697
800
  }
698
801
  if (_isLikelyExpressionLiteral(trimmedLiteral)) {
802
+ const missingIdentifier = _extractMissingIdentifier(trimmedLiteral);
699
803
  throwZenithRuntimeError({
700
804
  phase: 'bind',
701
805
  code: 'UNRESOLVED_EXPRESSION',
702
- message: `Failed to resolve expression literal: ${_truncateLiteralForError(trimmedLiteral)}`,
806
+ message: missingIdentifier
807
+ ? `Unresolved expression identifier "${missingIdentifier}" in ${_truncateLiteralForError(trimmedLiteral)}`
808
+ : `Failed to resolve expression literal: ${_truncateLiteralForError(trimmedLiteral)}`,
703
809
  marker: {
704
810
  type: _markerTypeForError(mode),
705
811
  id: binding.marker_index
706
812
  },
707
813
  path: `expression[${binding.marker_index}]`,
708
- hint: 'Ensure the expression references declared state keys or params/data bindings.'
814
+ hint: missingIdentifier
815
+ ? `Declare "${missingIdentifier}" in scope or pass it via props.`
816
+ : 'Declare the missing identifier in scope or pass it via props.',
817
+ docsLink: DOCS_LINKS.expressionScope,
818
+ source: _resolveBindingSource(binding, markerBinding, eventBinding)
709
819
  });
710
820
  }
711
821
  }
@@ -713,7 +823,7 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
713
823
  }
714
824
  return '';
715
825
  }
716
- function _throwUnresolvedMemberChainError(literal, markerIndex, mode, pathSuffix, hint) {
826
+ function _throwUnresolvedMemberChainError(literal, markerIndex, mode, pathSuffix, hint, source) {
717
827
  throwZenithRuntimeError({
718
828
  phase: 'bind',
719
829
  code: 'UNRESOLVED_EXPRESSION',
@@ -723,10 +833,12 @@ function _throwUnresolvedMemberChainError(literal, markerIndex, mode, pathSuffix
723
833
  id: markerIndex
724
834
  },
725
835
  path: `marker[${markerIndex}].${pathSuffix}`,
726
- hint
836
+ hint,
837
+ docsLink: DOCS_LINKS.expressionScope,
838
+ source
727
839
  });
728
840
  }
729
- function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, params, ssrData, mode, props, markerIndex) {
841
+ function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, params, ssrData, mode, props, markerIndex, source) {
730
842
  if (typeof literal !== 'string' || !STRICT_MEMBER_CHAIN_LITERAL_RE.test(literal)) {
731
843
  return UNRESOLVED_LITERAL;
732
844
  }
@@ -742,7 +854,7 @@ function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, param
742
854
  const baseIdentifier = segments[0];
743
855
  const scope = _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props);
744
856
  if (!Object.prototype.hasOwnProperty.call(scope, baseIdentifier)) {
745
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${baseIdentifier}`, `Base identifier "${baseIdentifier}" is not bound. Check props/data/params and declared state keys.`);
857
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${baseIdentifier}`, `Base identifier "${baseIdentifier}" is not bound. Check props/data/params and declared state keys.`, source);
746
858
  }
747
859
  let cursor = scope[baseIdentifier];
748
860
  let traversedPath = baseIdentifier;
@@ -754,18 +866,20 @@ function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, param
754
866
  code: 'UNSAFE_MEMBER_ACCESS',
755
867
  message: `Blocked unsafe member access: ${segment} in path "${literal}"`,
756
868
  path: `marker[${markerIndex}].expression.${literal}`,
757
- hint: 'Property access to __proto__, prototype, and constructor is forbidden.'
869
+ hint: 'Property access to __proto__, prototype, and constructor is forbidden.',
870
+ docsLink: DOCS_LINKS.expressionScope,
871
+ source
758
872
  });
759
873
  }
760
874
  if (cursor === null || cursor === undefined) {
761
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it is null or undefined.`);
875
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it is null or undefined.`, source);
762
876
  }
763
877
  const cursorType = typeof cursor;
764
878
  if (cursorType !== 'object' && cursorType !== 'function') {
765
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it resolved to a ${cursorType}.`);
879
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it resolved to a ${cursorType}.`, source);
766
880
  }
767
881
  if (!Object.prototype.hasOwnProperty.call(cursor, segment)) {
768
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Missing member "${segment}" on ${traversedPath}. Check your bindings.`);
882
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Missing member "${segment}" on ${traversedPath}. Check your bindings.`, source);
769
883
  }
770
884
  cursor = cursor[segment];
771
885
  traversedPath = `${traversedPath}.${segment}`;
@@ -898,6 +1012,52 @@ function _isLikelyExpressionLiteral(literal) {
898
1012
  }
899
1013
  return /=>|[()[\]{}<>=?:.+\-*/%|&!]/.test(trimmed);
900
1014
  }
1015
+ function _extractMissingIdentifier(literal) {
1016
+ if (typeof literal !== 'string') {
1017
+ return null;
1018
+ }
1019
+ const match = literal.trim().match(/^([A-Za-z_$][A-Za-z0-9_$]*)/);
1020
+ if (!match) {
1021
+ return null;
1022
+ }
1023
+ const candidate = match[1];
1024
+ if (candidate === 'true' || candidate === 'false' || candidate === 'null' || candidate === 'undefined') {
1025
+ return null;
1026
+ }
1027
+ return candidate;
1028
+ }
1029
+ function _resolveBindingSource(binding, markerBinding, eventBinding) {
1030
+ const candidates = [
1031
+ binding?.source,
1032
+ eventBinding?.source,
1033
+ markerBinding?.source
1034
+ ];
1035
+ for (let i = 0; i < candidates.length; i++) {
1036
+ const candidate = candidates[i];
1037
+ if (candidate && typeof candidate === 'object' && typeof candidate.file === 'string') {
1038
+ return candidate;
1039
+ }
1040
+ }
1041
+ return undefined;
1042
+ }
1043
+ function _describeBindingExpression(binding) {
1044
+ if (!binding || typeof binding !== 'object') {
1045
+ return '<unknown>';
1046
+ }
1047
+ if (typeof binding.literal === 'string' && binding.literal.trim().length > 0) {
1048
+ return _truncateLiteralForError(binding.literal.trim());
1049
+ }
1050
+ if (Number.isInteger(binding.state_index)) {
1051
+ return `state[${binding.state_index}]`;
1052
+ }
1053
+ if (Number.isInteger(binding.signal_index)) {
1054
+ return `signal[${binding.signal_index}]`;
1055
+ }
1056
+ if (typeof binding.component_instance === 'string' && typeof binding.component_binding === 'string') {
1057
+ return `${binding.component_instance}.${binding.component_binding}`;
1058
+ }
1059
+ return '<unknown expression>';
1060
+ }
901
1061
  function _markerTypeForError(kind) {
902
1062
  if (kind === 'text')
903
1063
  return 'data-zx-e';
@@ -1308,6 +1468,10 @@ function _applyMarkerValue(nodes, marker, value) {
1308
1468
  try {
1309
1469
  const node = nodes[i];
1310
1470
  if (marker.kind === 'text') {
1471
+ if (node && node.nodeType === 8) {
1472
+ _applyCommentMarkerValue(node, value, `${markerPath}.text`);
1473
+ continue;
1474
+ }
1311
1475
  if (_isStructuralFragment(value)) {
1312
1476
  _mountStructuralFragment(node, value, `${markerPath}.text`);
1313
1477
  continue;
@@ -1337,11 +1501,40 @@ function _applyMarkerValue(nodes, marker, value) {
1337
1501
  path: marker.kind === 'attr'
1338
1502
  ? `${markerPath}.attr.${marker.attr}`
1339
1503
  : `${markerPath}.${marker.kind}`,
1340
- hint: 'Check the binding value type and marker mapping.'
1504
+ hint: 'Check the binding value type and marker mapping.',
1505
+ docsLink: DOCS_LINKS.markerTable,
1506
+ source: marker.source
1341
1507
  });
1342
1508
  }
1343
1509
  }
1344
1510
  }
1511
+ function _applyCommentMarkerValue(anchor, value, rootPath) {
1512
+ if (_isStructuralFragment(value)) {
1513
+ _mountStructuralFragmentIntoCommentRange(anchor, value, rootPath);
1514
+ return;
1515
+ }
1516
+ const end = _clearCommentPlaceholderContent(anchor);
1517
+ const parent = end.parentNode;
1518
+ if (!parent) {
1519
+ return;
1520
+ }
1521
+ const html = _renderFragmentValue(value, rootPath);
1522
+ if (html !== null) {
1523
+ parent.insertBefore(_createContextualFragment(parent, html), end);
1524
+ return;
1525
+ }
1526
+ const textNode = (parent.ownerDocument || document).createTextNode(_coerceText(value, rootPath));
1527
+ parent.insertBefore(textNode, end);
1528
+ }
1529
+ function _createContextualFragment(parent, html) {
1530
+ const doc = parent.ownerDocument || document;
1531
+ if (!doc || typeof doc.createRange !== 'function') {
1532
+ throw new Error('[Zenith Runtime] comment placeholder HTML rendering requires Range#createContextualFragment');
1533
+ }
1534
+ const range = doc.createRange();
1535
+ range.selectNode(parent);
1536
+ return range.createContextualFragment(html);
1537
+ }
1345
1538
  function _isStructuralFragment(value) {
1346
1539
  if (Array.isArray(value)) {
1347
1540
  for (let i = 0; i < value.length; i++) {
@@ -1352,6 +1545,116 @@ function _isStructuralFragment(value) {
1352
1545
  }
1353
1546
  return value && typeof value === 'object' && value.__zenith_fragment === true && typeof value.mount === 'function';
1354
1547
  }
1548
+ function _ensureCommentPlaceholderEnd(anchor) {
1549
+ let end = anchor.__z_range_end || null;
1550
+ if (end && end.parentNode === anchor.parentNode) {
1551
+ return end;
1552
+ }
1553
+ const parent = anchor.parentNode;
1554
+ if (!parent) {
1555
+ return null;
1556
+ }
1557
+ end = (anchor.ownerDocument || document).createComment(`/ ${anchor.data}`);
1558
+ parent.insertBefore(end, anchor.nextSibling);
1559
+ anchor.__z_range_end = end;
1560
+ return end;
1561
+ }
1562
+ function _clearCommentPlaceholderContent(anchor) {
1563
+ if (anchor.__z_unmounts) {
1564
+ for (let i = 0; i < anchor.__z_unmounts.length; i++) {
1565
+ try {
1566
+ anchor.__z_unmounts[i]();
1567
+ }
1568
+ catch (e) { }
1569
+ }
1570
+ }
1571
+ anchor.__z_unmounts = [];
1572
+ const end = _ensureCommentPlaceholderEnd(anchor);
1573
+ if (!end) {
1574
+ return anchor;
1575
+ }
1576
+ let current = anchor.nextSibling;
1577
+ while (current && current !== end) {
1578
+ const next = current.nextSibling;
1579
+ if (current.parentNode) {
1580
+ current.parentNode.removeChild(current);
1581
+ }
1582
+ current = next;
1583
+ }
1584
+ return end;
1585
+ }
1586
+ function _mountStructuralFragmentIntoCommentRange(anchor, value, rootPath = 'renderable') {
1587
+ const end = _clearCommentPlaceholderContent(anchor);
1588
+ const parent = end.parentNode;
1589
+ if (!parent) {
1590
+ return;
1591
+ }
1592
+ const doc = parent.ownerDocument || document;
1593
+ const newUnmounts = [];
1594
+ function insertHtml(html) {
1595
+ const fragment = _createContextualFragment(parent, html);
1596
+ const nodes = Array.from(fragment.childNodes);
1597
+ parent.insertBefore(fragment, end);
1598
+ for (let i = 0; i < nodes.length; i++) {
1599
+ const inserted = nodes[i];
1600
+ newUnmounts.push(() => {
1601
+ if (inserted.parentNode)
1602
+ inserted.parentNode.removeChild(inserted);
1603
+ });
1604
+ }
1605
+ }
1606
+ function mountItem(item, path) {
1607
+ if (Array.isArray(item)) {
1608
+ for (let i = 0; i < item.length; i++)
1609
+ mountItem(item[i], `${path}[${i}]`);
1610
+ return;
1611
+ }
1612
+ if (item && item.__zenith_fragment === true && typeof item.mount === 'function') {
1613
+ const fragment = doc.createDocumentFragment();
1614
+ item.mount(fragment);
1615
+ const nodes = Array.from(fragment.childNodes);
1616
+ parent.insertBefore(fragment, end);
1617
+ for (let i = 0; i < nodes.length; i++) {
1618
+ const inserted = nodes[i];
1619
+ newUnmounts.push(() => {
1620
+ if (inserted.parentNode)
1621
+ inserted.parentNode.removeChild(inserted);
1622
+ });
1623
+ }
1624
+ if (typeof item.unmount === 'function') {
1625
+ newUnmounts.push(item.unmount.bind(item));
1626
+ }
1627
+ return;
1628
+ }
1629
+ if (item && item.__zenith_fragment === true && typeof item.html === 'string') {
1630
+ insertHtml(item.html);
1631
+ return;
1632
+ }
1633
+ const text = _coerceText(item, path);
1634
+ if (text || text === '') {
1635
+ const textNode = doc.createTextNode(text);
1636
+ parent.insertBefore(textNode, end);
1637
+ newUnmounts.push(() => {
1638
+ if (textNode.parentNode)
1639
+ textNode.parentNode.removeChild(textNode);
1640
+ });
1641
+ }
1642
+ }
1643
+ try {
1644
+ mountItem(value, rootPath);
1645
+ }
1646
+ catch (error) {
1647
+ rethrowZenithRuntimeError(error, {
1648
+ phase: 'render',
1649
+ code: 'FRAGMENT_MOUNT_FAILED',
1650
+ message: 'Fragment mount failed',
1651
+ path: rootPath,
1652
+ hint: 'Verify fragment values and nested renderable arrays.',
1653
+ docsLink: DOCS_LINKS.markerTable
1654
+ });
1655
+ }
1656
+ anchor.__z_unmounts = newUnmounts;
1657
+ }
1355
1658
  function _mountStructuralFragment(container, value, rootPath = 'renderable') {
1356
1659
  if (container.__z_unmounts) {
1357
1660
  for (let i = 0; i < container.__z_unmounts.length; i++) {
@@ -1396,7 +1699,8 @@ function _mountStructuralFragment(container, value, rootPath = 'renderable') {
1396
1699
  code: 'FRAGMENT_MOUNT_FAILED',
1397
1700
  message: 'Fragment mount failed',
1398
1701
  path: rootPath,
1399
- hint: 'Verify fragment values and nested renderable arrays.'
1702
+ hint: 'Verify fragment values and nested renderable arrays.',
1703
+ docsLink: DOCS_LINKS.markerTable
1400
1704
  });
1401
1705
  }
1402
1706
  container.__z_unmounts = newUnmounts;
@@ -1410,7 +1714,8 @@ function _coerceText(value, path = 'renderable') {
1410
1714
  code: 'NON_RENDERABLE_VALUE',
1411
1715
  message: `Zenith Render Error: non-renderable function at ${path}. Use map() to render fields.`,
1412
1716
  path,
1413
- hint: 'Convert functions into explicit event handlers or renderable text.'
1717
+ hint: 'Convert functions into explicit event handlers or renderable text.',
1718
+ docsLink: DOCS_LINKS.expressionScope
1414
1719
  });
1415
1720
  }
1416
1721
  if (value && typeof value === 'object') {
@@ -1419,7 +1724,8 @@ function _coerceText(value, path = 'renderable') {
1419
1724
  code: 'NON_RENDERABLE_VALUE',
1420
1725
  message: `Zenith Render Error: non-renderable object at ${path}. Use map() to render fields.`,
1421
1726
  path,
1422
- hint: 'Use map() to render object fields into nodes.'
1727
+ hint: 'Use map() to render object fields into nodes.',
1728
+ docsLink: DOCS_LINKS.expressionScope
1423
1729
  });
1424
1730
  }
1425
1731
  return String(value);
@@ -1484,7 +1790,12 @@ function _applyAttribute(node, attrName, value) {
1484
1790
  return;
1485
1791
  }
1486
1792
  if (attrName === 'class' || attrName === 'className') {
1487
- node.className = value === null || value === undefined || value === false ? '' : String(value);
1793
+ const classValue = value === null || value === undefined || value === false ? '' : String(value);
1794
+ if (node && node.namespaceURI === SVG_NAMESPACE && typeof node.setAttribute === 'function') {
1795
+ node.setAttribute('class', classValue);
1796
+ return;
1797
+ }
1798
+ node.className = classValue;
1488
1799
  return;
1489
1800
  }
1490
1801
  if (attrName === 'style') {
package/dist/template.js CHANGED
@@ -113,6 +113,8 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
113
113
  }
114
114
 
115
115
  let cssSwapEpoch = 0;
116
+ const CSS_UPDATE_DOCS = '/docs/documentation/contracts/hmr-v1-contract.md#css-updates';
117
+ const BUILD_ERROR_DOCS = '/docs/documentation/contracts/runtime-contract.md#diagnostics';
116
118
 
117
119
  function withCacheBuster(nextHref) {
118
120
  const separator = nextHref.includes('?') ? '&' : '?';
@@ -153,7 +155,7 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
153
155
 
154
156
  function scheduleCssRetry(previousHref, attempt) {
155
157
  if (attempt >= 3) {
156
- window.location.reload();
158
+ reportBuildFailure('CSS update failed (404): server build not ready', CSS_UPDATE_DOCS);
157
159
  return;
158
160
  }
159
161
  const delayMs = (attempt + 1) * 100;
@@ -220,6 +222,19 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
220
222
  shell.style.fontSize = '12px';
221
223
  shell.style.pointerEvents = 'none';
222
224
 
225
+ const buildFailure = document.createElement('div');
226
+ buildFailure.setAttribute('data-zenith-dev-build-error', 'true');
227
+ buildFailure.style.display = 'none';
228
+ buildFailure.style.marginBottom = '8px';
229
+ buildFailure.style.padding = '8px 10px';
230
+ buildFailure.style.maxWidth = '420px';
231
+ buildFailure.style.pointerEvents = 'auto';
232
+ buildFailure.style.border = '1px solid rgba(255,106,106,0.8)';
233
+ buildFailure.style.borderRadius = '8px';
234
+ buildFailure.style.background = 'rgba(90, 16, 16, 0.92)';
235
+ buildFailure.style.color = '#ffe8e8';
236
+ buildFailure.style.whiteSpace = 'pre-wrap';
237
+
223
238
  const pill = document.createElement('button');
224
239
  pill.type = 'button';
225
240
  pill.textContent = 'Zenith Dev';
@@ -243,6 +258,29 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
243
258
  panel.style.borderRadius = '10px';
244
259
  panel.style.padding = '10px';
245
260
  panel.style.boxShadow = '0 14px 30px rgba(0,0,0,0.35)';
261
+ let lastBuildFailureSignature = '';
262
+
263
+ function reportBuildFailure(message, docsLink) {
264
+ const normalizedMessage = typeof message === 'string' && message.trim().length > 0
265
+ ? message.trim()
266
+ : 'Build failed - fix errors to continue.';
267
+ const signature = normalizedMessage + '|' + String(docsLink || '');
268
+ if (signature !== lastBuildFailureSignature) {
269
+ appendLog('[build_failed] ' + normalizedMessage);
270
+ lastBuildFailureSignature = signature;
271
+ }
272
+ const docsText = typeof docsLink === 'string' && docsLink.length > 0
273
+ ? '\\nDocs: ' + docsLink
274
+ : '';
275
+ buildFailure.textContent = 'Build failed - fix errors to continue\\n' + normalizedMessage + docsText;
276
+ buildFailure.style.display = 'block';
277
+ }
278
+
279
+ function clearBuildFailure() {
280
+ lastBuildFailureSignature = '';
281
+ buildFailure.style.display = 'none';
282
+ buildFailure.textContent = '';
283
+ }
246
284
 
247
285
  const status = document.createElement('div');
248
286
  status.textContent = 'status: connecting';
@@ -292,7 +330,7 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
292
330
  logs.textContent = '[zenith-dev] waiting for server events...';
293
331
 
294
332
  panel.append(status, info, controls, logs);
295
- shell.append(pill, panel);
333
+ shell.append(buildFailure, pill, panel);
296
334
 
297
335
  function setOpen(open) {
298
336
  state.overlay.open = open === true;
@@ -313,6 +351,9 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
313
351
  const serverUrl = typeof payload.serverUrl === 'string' ? payload.serverUrl : window.location.origin;
314
352
  const buildId = Number.isInteger(payload.buildId) ? payload.buildId : 'n/a';
315
353
  const buildStatus = typeof payload.status === 'string' ? payload.status : 'unknown';
354
+ const errorMessage = payload && payload.error && typeof payload.error.message === 'string'
355
+ ? payload.error.message
356
+ : (typeof payload.message === 'string' ? payload.message : '');
316
357
  info.textContent =
317
358
  'server: ' + serverUrl + '\\n' +
318
359
  'route: ' + route + '\\n' +
@@ -321,6 +362,11 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
321
362
  'hash: ' + hash + '\\n' +
322
363
  'duration: ' + duration + '\\n' +
323
364
  'changed: ' + changed;
365
+ if (buildStatus === 'error') {
366
+ reportBuildFailure(errorMessage || 'Dev build is in an error state.', BUILD_ERROR_DOCS);
367
+ } else if (buildStatus === 'ok' || buildStatus === 'building') {
368
+ clearBuildFailure();
369
+ }
324
370
  }
325
371
 
326
372
  function allLogsEnabled() {
@@ -388,12 +434,14 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
388
434
  source.addEventListener('build_start', function (event) {
389
435
  const payload = parseEventData(event.data);
390
436
  status.textContent = 'status: rebuilding';
437
+ clearBuildFailure();
391
438
  appendLog('[build_start] ' + (Array.isArray(payload.changedFiles) ? payload.changedFiles.join(', ') : ''));
392
439
  emitDebug('build_start', payload);
393
440
  });
394
441
  source.addEventListener('build_complete', function (event) {
395
442
  const payload = parseEventData(event.data);
396
443
  status.textContent = 'status: ready';
444
+ clearBuildFailure();
397
445
  updateInfo(payload);
398
446
  appendLog('[build_complete] ' + (Number.isFinite(payload.durationMs) ? payload.durationMs + 'ms' : 'done'));
399
447
  emitDebug('build_complete', payload);
@@ -401,6 +449,8 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
401
449
  source.addEventListener('build_error', function (event) {
402
450
  const payload = parseEventData(event.data);
403
451
  status.textContent = 'status: error';
452
+ reportBuildFailure(payload.message || 'Build failed', BUILD_ERROR_DOCS);
453
+ updateInfo(payload);
404
454
  appendLog('[build_error] ' + (payload.message || 'Unknown error'));
405
455
  emitDebug('build_error', payload);
406
456
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/runtime",
3
- "version": "0.6.13",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {