@zenithbuild/runtime 0.6.13 → 0.6.17

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
@@ -21,20 +21,27 @@ const BOOLEAN_ATTRIBUTES = new Set([
21
21
  ]);
22
22
  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
23
  const UNSAFE_MEMBER_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
24
+ const DOCS_LINKS = Object.freeze({
25
+ eventBinding: '/docs/documentation/contracts/runtime-contract.md#event-bindings',
26
+ expressionScope: '/docs/documentation/reference/reactive-binding-model.md#expression-resolution',
27
+ markerTable: '/docs/documentation/reference/markers.md',
28
+ componentBootstrap: '/docs/documentation/contracts/runtime-contract.md#component-bootstrap',
29
+ refs: '/docs/documentation/reference/reactive-binding-model.md#refs-and-mount'
30
+ });
24
31
  /**
25
32
  * Hydrate a pre-rendered DOM tree using explicit payload tables.
26
33
  *
27
34
  * @param {{
28
35
  * ir_version: number,
29
36
  * 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 }>,
37
+ * 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 } }>,
38
+ * 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 } }>,
39
+ * events: Array<{ index: number, event: string, selector: string, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>,
40
+ * 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
41
  * state_values: Array<*>,
35
42
  * state_keys?: Array<string>,
36
43
  * signals: Array<{ id: number, kind: 'signal', state_index: number }>,
37
- * components?: Array<{ instance: string, selector: string, create: Function }>
44
+ * components?: Array<{ instance: string, selector: string, create: Function, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>
38
45
  * }} payload
39
46
  * @returns {() => void}
40
47
  */
@@ -61,7 +68,7 @@ export function hydrate(payload) {
61
68
  for (let i = 0; i < refs.length; i++) {
62
69
  const refBinding = refs[i];
63
70
  const targetRef = stateValues[refBinding.state_index];
64
- const nodes = _resolveNodes(root, refBinding.selector, refBinding.index, 'ref');
71
+ const nodes = _resolveNodes(root, refBinding.selector, refBinding.index, 'ref', refBinding.source);
65
72
  targetRef.current = nodes[0] || null;
66
73
  hydratedRefs.push(targetRef);
67
74
  }
@@ -78,38 +85,56 @@ export function hydrate(payload) {
78
85
  component: component.instance,
79
86
  route
80
87
  }));
81
- const hosts = _resolveNodes(root, component.selector, i, 'component');
88
+ const hosts = _resolveNodes(root, component.selector, i, 'component', component.source);
82
89
  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);
90
+ try {
91
+ const componentScope = createSideEffectScope(`${component.instance}:${j}`);
92
+ const runtimeApi = {
93
+ signal,
94
+ state,
95
+ zeneffect(effect, dependenciesOrOptions) {
96
+ return zeneffect(effect, dependenciesOrOptions, componentScope);
97
+ },
98
+ zenEffect(effect, options) {
99
+ return zenEffect(effect, options, componentScope);
100
+ },
101
+ zenMount(callback) {
102
+ return zenMount(callback, componentScope);
103
+ }
104
+ };
105
+ const instance = component.create(hosts[j], resolvedProps, runtimeApi);
106
+ if (!instance || typeof instance !== 'object') {
107
+ throw new Error(`[Zenith Runtime] component factory for ${component.instance} must return an object`);
108
+ }
109
+ if (typeof instance.mount === 'function') {
110
+ instance.mount();
111
+ }
112
+ activateSideEffectScope(componentScope);
113
+ _registerDisposer(() => {
114
+ disposeSideEffectScope(componentScope);
115
+ if (typeof instance.destroy === 'function') {
116
+ instance.destroy();
117
+ }
118
+ });
119
+ if (instance.bindings && typeof instance.bindings === 'object') {
120
+ componentBindings[component.instance] = instance.bindings;
95
121
  }
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
122
  }
104
- activateSideEffectScope(componentScope);
105
- _registerDisposer(() => {
106
- disposeSideEffectScope(componentScope);
107
- if (typeof instance.destroy === 'function') {
108
- instance.destroy();
123
+ catch (error) {
124
+ try {
125
+ rethrowZenithRuntimeError(error, {
126
+ phase: 'hydrate',
127
+ code: 'COMPONENT_BOOTSTRAP_FAILED',
128
+ message: `Component bootstrap failed for "${component.instance}"`,
129
+ path: `component[${component.instance}]`,
130
+ hint: 'Fix the failing component and refresh; other components continue mounting.',
131
+ docsLink: DOCS_LINKS.componentBootstrap,
132
+ source: component.source
133
+ });
134
+ }
135
+ catch {
136
+ // Fault containment: continue mounting remaining components.
109
137
  }
110
- });
111
- if (instance.bindings && typeof instance.bindings === 'object') {
112
- componentBindings[component.instance] = instance.bindings;
113
138
  }
114
139
  }
115
140
  }
@@ -134,9 +159,9 @@ export function hydrate(payload) {
134
159
  if (marker.kind === 'event') {
135
160
  continue;
136
161
  }
137
- const nodes = _resolveNodes(root, marker.selector, marker.index, marker.kind);
162
+ const nodes = _resolveNodes(root, marker.selector, marker.index, marker.kind, marker.source);
138
163
  markerNodesByIndex.set(marker.index, nodes);
139
- const value = _evaluateExpression(expressions[marker.index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns);
164
+ const value = _evaluateExpression(expressions[marker.index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns, marker, null);
140
165
  _applyMarkerValue(nodes, marker, value);
141
166
  }
142
167
  for (let i = 0; i < expressions.length; i++) {
@@ -149,9 +174,9 @@ export function hydrate(payload) {
149
174
  if (!marker || marker.kind === 'event') {
150
175
  return;
151
176
  }
152
- const nodes = markerNodesByIndex.get(index) || _resolveNodes(root, marker.selector, marker.index, marker.kind);
177
+ const nodes = markerNodesByIndex.get(index) || _resolveNodes(root, marker.selector, marker.index, marker.kind, marker.source);
153
178
  markerNodesByIndex.set(index, nodes);
154
- const value = _evaluateExpression(expressions[index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns);
179
+ const value = _evaluateExpression(expressions[index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns, marker, null);
155
180
  _applyMarkerValue(nodes, marker, value);
156
181
  }
157
182
  const dependentMarkersBySignal = new Map();
@@ -225,16 +250,21 @@ export function hydrate(payload) {
225
250
  throw new Error(`[Zenith Runtime] duplicate event index ${eventBinding.index}`);
226
251
  }
227
252
  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);
253
+ const marker = markerByIndex.get(eventBinding.index) || null;
254
+ const nodes = _resolveNodes(root, eventBinding.selector, eventBinding.index, 'event', eventBinding.source || marker?.source);
255
+ const expressionBinding = expressions[eventBinding.index];
256
+ const handler = _evaluateExpression(expressionBinding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, 'event', props || {}, exprFns, marker, eventBinding);
230
257
  if (typeof handler !== 'function') {
258
+ const passedExpression = _describeBindingExpression(expressionBinding);
231
259
  throwZenithRuntimeError({
232
260
  phase: 'bind',
233
261
  code: 'BINDING_APPLY_FAILED',
234
- message: `Event binding at index ${eventBinding.index} did not resolve to a function`,
262
+ message: `Event binding at index ${eventBinding.index} expected a function reference. You passed: ${passedExpression}`,
235
263
  marker: { type: `data-zx-on-${eventBinding.event}`, id: eventBinding.index },
236
264
  path: `event[${eventBinding.index}].${eventBinding.event}`,
237
- hint: 'Bind events to function references (on:click={handler}).'
265
+ hint: 'Use on:*={handler} or ensure the forwarded prop is a function.',
266
+ docsLink: DOCS_LINKS.eventBinding,
267
+ source: _resolveBindingSource(expressionBinding, marker, eventBinding)
238
268
  });
239
269
  }
240
270
  for (let j = 0; j < nodes.length; j++) {
@@ -250,7 +280,9 @@ export function hydrate(payload) {
250
280
  message: `Event handler failed for "${eventBinding.event}"`,
251
281
  marker: { type: `data-zx-on-${eventBinding.event}`, id: eventBinding.index },
252
282
  path: `event[${eventBinding.index}].${eventBinding.event}`,
253
- hint: 'Inspect the handler body and referenced state.'
283
+ hint: 'Inspect the handler body and referenced state.',
284
+ docsLink: DOCS_LINKS.eventBinding,
285
+ source: _resolveBindingSource(expressionBinding, marker, eventBinding)
254
286
  });
255
287
  }
256
288
  };
@@ -314,7 +346,8 @@ export function hydrate(payload) {
314
346
  rethrowZenithRuntimeError(error, {
315
347
  phase: 'hydrate',
316
348
  code: 'BINDING_APPLY_FAILED',
317
- hint: 'Inspect marker tables, expression bindings, and the runtime overlay diagnostics.'
349
+ hint: 'Inspect marker tables, expression bindings, and the runtime overlay diagnostics.',
350
+ docsLink: DOCS_LINKS.markerTable
318
351
  });
319
352
  }
320
353
  }
@@ -390,6 +423,7 @@ function _validatePayload(payload) {
390
423
  throw new Error(`[Zenith Runtime] expression at position ${i} has invalid fn_index`);
391
424
  }
392
425
  }
426
+ _assertValidSourceSpan(expression.source, `expression[${i}]`);
393
427
  if (expression.signal_indices !== undefined) {
394
428
  if (!Array.isArray(expression.signal_indices)) {
395
429
  throw new Error(`[Zenith Runtime] expression at position ${i} must provide signal_indices[]`);
@@ -421,6 +455,7 @@ function _validatePayload(payload) {
421
455
  if (marker.kind === 'attr' && (typeof marker.attr !== 'string' || marker.attr.length === 0)) {
422
456
  throw new Error(`[Zenith Runtime] attr marker at position ${i} requires attr name`);
423
457
  }
458
+ _assertValidSourceSpan(marker.source, `marker[${i}]`);
424
459
  }
425
460
  for (let i = 0; i < events.length; i++) {
426
461
  const eventBinding = events[i];
@@ -436,6 +471,7 @@ function _validatePayload(payload) {
436
471
  if (typeof eventBinding.selector !== 'string' || eventBinding.selector.length === 0) {
437
472
  throw new Error(`[Zenith Runtime] event binding at position ${i} requires selector`);
438
473
  }
474
+ _assertValidSourceSpan(eventBinding.source, `event[${i}]`);
439
475
  }
440
476
  for (let i = 0; i < refs.length; i++) {
441
477
  const refBinding = refs[i];
@@ -453,6 +489,7 @@ function _validatePayload(payload) {
453
489
  if (typeof refBinding.selector !== 'string' || refBinding.selector.length === 0) {
454
490
  throw new Error(`[Zenith Runtime] ref binding at position ${i} requires selector`);
455
491
  }
492
+ _assertValidSourceSpan(refBinding.source, `ref[${i}]`);
456
493
  const candidate = stateValues[refBinding.state_index];
457
494
  if (!candidate || typeof candidate !== 'object' || !Object.prototype.hasOwnProperty.call(candidate, 'current')) {
458
495
  throw new Error(`[Zenith Runtime] ref binding at position ${i} must resolve to a ref-like object`);
@@ -490,6 +527,7 @@ function _validatePayload(payload) {
490
527
  if (typeof component.create !== 'function') {
491
528
  throw new Error(`[Zenith Runtime] component at position ${i} requires create() function`);
492
529
  }
530
+ _assertValidSourceSpan(component.source, `component[${i}]`);
493
531
  }
494
532
  if (payload.params !== undefined) {
495
533
  if (!payload.params || typeof payload.params !== 'object' || Array.isArray(payload.params)) {
@@ -554,6 +592,36 @@ function _validatePayload(payload) {
554
592
  };
555
593
  return Object.freeze(validatedPayload);
556
594
  }
595
+ function _assertValidSourceSpan(source, contextLabel) {
596
+ if (source === undefined || source === null) {
597
+ return;
598
+ }
599
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
600
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source must be an object`);
601
+ }
602
+ if (typeof source.file !== 'string' || source.file.length === 0) {
603
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.file must be a non-empty string`);
604
+ }
605
+ const points = ['start', 'end'];
606
+ for (let i = 0; i < points.length; i++) {
607
+ const point = source[points[i]];
608
+ if (point === undefined || point === null) {
609
+ continue;
610
+ }
611
+ if (!point || typeof point !== 'object' || Array.isArray(point)) {
612
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.${points[i]} must be an object`);
613
+ }
614
+ if (!Number.isInteger(point.line) || point.line < 1) {
615
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.${points[i]}.line must be >= 1`);
616
+ }
617
+ if (!Number.isInteger(point.column) || point.column < 1) {
618
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.${points[i]}.column must be >= 1`);
619
+ }
620
+ }
621
+ if (source.snippet !== undefined && source.snippet !== null && typeof source.snippet !== 'string') {
622
+ throw new Error(`[Zenith Runtime] ${contextLabel}.source.snippet must be a string when provided`);
623
+ }
624
+ }
557
625
  function _resolveComponentProps(propTable, signalMap, context = {}) {
558
626
  if (!Array.isArray(propTable)) {
559
627
  throw new Error('[Zenith Runtime] component props must be an array');
@@ -592,16 +660,21 @@ function _resolveComponentProps(propTable, signalMap, context = {}) {
592
660
  }
593
661
  return resolved;
594
662
  }
595
- function _resolveNodes(root, selector, index, kind) {
663
+ function _resolveNodes(root, selector, index, kind, source = undefined) {
596
664
  const nodes = root.querySelectorAll(selector);
597
665
  if (!nodes || nodes.length === 0) {
666
+ const isRef = kind === 'ref';
598
667
  throwZenithRuntimeError({
599
668
  phase: 'bind',
600
669
  code: 'MARKER_MISSING',
601
670
  message: `Unresolved ${kind} marker index ${index}`,
602
671
  marker: { type: kind, id: index },
603
672
  path: `selector:${selector}`,
604
- hint: 'Confirm SSR marker attributes and runtime selector tables match.'
673
+ hint: isRef
674
+ ? 'Use ref + zenMount and ensure the ref is bound in markup before mount.'
675
+ : 'Confirm SSR marker attributes and runtime selector tables match.',
676
+ docsLink: isRef ? DOCS_LINKS.refs : DOCS_LINKS.markerTable,
677
+ source
605
678
  });
606
679
  }
607
680
  return nodes;
@@ -618,7 +691,7 @@ function _resolveExpressionSignalIndices(binding) {
618
691
  }
619
692
  return [];
620
693
  }
621
- function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props, exprFns) {
694
+ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props, exprFns, markerBinding = null, eventBinding = null) {
622
695
  if (binding.fn_index != null && binding.fn_index !== undefined) {
623
696
  const fns = Array.isArray(exprFns) ? exprFns : [];
624
697
  const fn = fns[binding.fn_index];
@@ -678,7 +751,7 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
678
751
  if (binding.literal !== null && binding.literal !== undefined) {
679
752
  if (typeof binding.literal === 'string') {
680
753
  const trimmedLiteral = binding.literal.trim();
681
- const strictMemberValue = _resolveStrictMemberChainLiteral(trimmedLiteral, stateValues, stateKeys, params, ssrData, mode, props, binding.marker_index);
754
+ const strictMemberValue = _resolveStrictMemberChainLiteral(trimmedLiteral, stateValues, stateKeys, params, ssrData, mode, props, binding.marker_index, _resolveBindingSource(binding, markerBinding, eventBinding));
682
755
  if (strictMemberValue !== UNRESOLVED_LITERAL) {
683
756
  return strictMemberValue;
684
757
  }
@@ -696,16 +769,23 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
696
769
  return primitiveValue;
697
770
  }
698
771
  if (_isLikelyExpressionLiteral(trimmedLiteral)) {
772
+ const missingIdentifier = _extractMissingIdentifier(trimmedLiteral);
699
773
  throwZenithRuntimeError({
700
774
  phase: 'bind',
701
775
  code: 'UNRESOLVED_EXPRESSION',
702
- message: `Failed to resolve expression literal: ${_truncateLiteralForError(trimmedLiteral)}`,
776
+ message: missingIdentifier
777
+ ? `Unresolved expression identifier "${missingIdentifier}" in ${_truncateLiteralForError(trimmedLiteral)}`
778
+ : `Failed to resolve expression literal: ${_truncateLiteralForError(trimmedLiteral)}`,
703
779
  marker: {
704
780
  type: _markerTypeForError(mode),
705
781
  id: binding.marker_index
706
782
  },
707
783
  path: `expression[${binding.marker_index}]`,
708
- hint: 'Ensure the expression references declared state keys or params/data bindings.'
784
+ hint: missingIdentifier
785
+ ? `Declare "${missingIdentifier}" in scope or pass it via props.`
786
+ : 'Declare the missing identifier in scope or pass it via props.',
787
+ docsLink: DOCS_LINKS.expressionScope,
788
+ source: _resolveBindingSource(binding, markerBinding, eventBinding)
709
789
  });
710
790
  }
711
791
  }
@@ -713,7 +793,7 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
713
793
  }
714
794
  return '';
715
795
  }
716
- function _throwUnresolvedMemberChainError(literal, markerIndex, mode, pathSuffix, hint) {
796
+ function _throwUnresolvedMemberChainError(literal, markerIndex, mode, pathSuffix, hint, source) {
717
797
  throwZenithRuntimeError({
718
798
  phase: 'bind',
719
799
  code: 'UNRESOLVED_EXPRESSION',
@@ -723,10 +803,12 @@ function _throwUnresolvedMemberChainError(literal, markerIndex, mode, pathSuffix
723
803
  id: markerIndex
724
804
  },
725
805
  path: `marker[${markerIndex}].${pathSuffix}`,
726
- hint
806
+ hint,
807
+ docsLink: DOCS_LINKS.expressionScope,
808
+ source
727
809
  });
728
810
  }
729
- function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, params, ssrData, mode, props, markerIndex) {
811
+ function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, params, ssrData, mode, props, markerIndex, source) {
730
812
  if (typeof literal !== 'string' || !STRICT_MEMBER_CHAIN_LITERAL_RE.test(literal)) {
731
813
  return UNRESOLVED_LITERAL;
732
814
  }
@@ -742,7 +824,7 @@ function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, param
742
824
  const baseIdentifier = segments[0];
743
825
  const scope = _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props);
744
826
  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.`);
827
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${baseIdentifier}`, `Base identifier "${baseIdentifier}" is not bound. Check props/data/params and declared state keys.`, source);
746
828
  }
747
829
  let cursor = scope[baseIdentifier];
748
830
  let traversedPath = baseIdentifier;
@@ -754,18 +836,20 @@ function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, param
754
836
  code: 'UNSAFE_MEMBER_ACCESS',
755
837
  message: `Blocked unsafe member access: ${segment} in path "${literal}"`,
756
838
  path: `marker[${markerIndex}].expression.${literal}`,
757
- hint: 'Property access to __proto__, prototype, and constructor is forbidden.'
839
+ hint: 'Property access to __proto__, prototype, and constructor is forbidden.',
840
+ docsLink: DOCS_LINKS.expressionScope,
841
+ source
758
842
  });
759
843
  }
760
844
  if (cursor === null || cursor === undefined) {
761
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it is null or undefined.`);
845
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it is null or undefined.`, source);
762
846
  }
763
847
  const cursorType = typeof cursor;
764
848
  if (cursorType !== 'object' && cursorType !== 'function') {
765
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it resolved to a ${cursorType}.`);
849
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it resolved to a ${cursorType}.`, source);
766
850
  }
767
851
  if (!Object.prototype.hasOwnProperty.call(cursor, segment)) {
768
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Missing member "${segment}" on ${traversedPath}. Check your bindings.`);
852
+ _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Missing member "${segment}" on ${traversedPath}. Check your bindings.`, source);
769
853
  }
770
854
  cursor = cursor[segment];
771
855
  traversedPath = `${traversedPath}.${segment}`;
@@ -898,6 +982,52 @@ function _isLikelyExpressionLiteral(literal) {
898
982
  }
899
983
  return /=>|[()[\]{}<>=?:.+\-*/%|&!]/.test(trimmed);
900
984
  }
985
+ function _extractMissingIdentifier(literal) {
986
+ if (typeof literal !== 'string') {
987
+ return null;
988
+ }
989
+ const match = literal.trim().match(/^([A-Za-z_$][A-Za-z0-9_$]*)/);
990
+ if (!match) {
991
+ return null;
992
+ }
993
+ const candidate = match[1];
994
+ if (candidate === 'true' || candidate === 'false' || candidate === 'null' || candidate === 'undefined') {
995
+ return null;
996
+ }
997
+ return candidate;
998
+ }
999
+ function _resolveBindingSource(binding, markerBinding, eventBinding) {
1000
+ const candidates = [
1001
+ binding?.source,
1002
+ eventBinding?.source,
1003
+ markerBinding?.source
1004
+ ];
1005
+ for (let i = 0; i < candidates.length; i++) {
1006
+ const candidate = candidates[i];
1007
+ if (candidate && typeof candidate === 'object' && typeof candidate.file === 'string') {
1008
+ return candidate;
1009
+ }
1010
+ }
1011
+ return undefined;
1012
+ }
1013
+ function _describeBindingExpression(binding) {
1014
+ if (!binding || typeof binding !== 'object') {
1015
+ return '<unknown>';
1016
+ }
1017
+ if (typeof binding.literal === 'string' && binding.literal.trim().length > 0) {
1018
+ return _truncateLiteralForError(binding.literal.trim());
1019
+ }
1020
+ if (Number.isInteger(binding.state_index)) {
1021
+ return `state[${binding.state_index}]`;
1022
+ }
1023
+ if (Number.isInteger(binding.signal_index)) {
1024
+ return `signal[${binding.signal_index}]`;
1025
+ }
1026
+ if (typeof binding.component_instance === 'string' && typeof binding.component_binding === 'string') {
1027
+ return `${binding.component_instance}.${binding.component_binding}`;
1028
+ }
1029
+ return '<unknown expression>';
1030
+ }
901
1031
  function _markerTypeForError(kind) {
902
1032
  if (kind === 'text')
903
1033
  return 'data-zx-e';
@@ -1337,7 +1467,9 @@ function _applyMarkerValue(nodes, marker, value) {
1337
1467
  path: marker.kind === 'attr'
1338
1468
  ? `${markerPath}.attr.${marker.attr}`
1339
1469
  : `${markerPath}.${marker.kind}`,
1340
- hint: 'Check the binding value type and marker mapping.'
1470
+ hint: 'Check the binding value type and marker mapping.',
1471
+ docsLink: DOCS_LINKS.markerTable,
1472
+ source: marker.source
1341
1473
  });
1342
1474
  }
1343
1475
  }
@@ -1396,7 +1528,8 @@ function _mountStructuralFragment(container, value, rootPath = 'renderable') {
1396
1528
  code: 'FRAGMENT_MOUNT_FAILED',
1397
1529
  message: 'Fragment mount failed',
1398
1530
  path: rootPath,
1399
- hint: 'Verify fragment values and nested renderable arrays.'
1531
+ hint: 'Verify fragment values and nested renderable arrays.',
1532
+ docsLink: DOCS_LINKS.markerTable
1400
1533
  });
1401
1534
  }
1402
1535
  container.__z_unmounts = newUnmounts;
@@ -1410,7 +1543,8 @@ function _coerceText(value, path = 'renderable') {
1410
1543
  code: 'NON_RENDERABLE_VALUE',
1411
1544
  message: `Zenith Render Error: non-renderable function at ${path}. Use map() to render fields.`,
1412
1545
  path,
1413
- hint: 'Convert functions into explicit event handlers or renderable text.'
1546
+ hint: 'Convert functions into explicit event handlers or renderable text.',
1547
+ docsLink: DOCS_LINKS.expressionScope
1414
1548
  });
1415
1549
  }
1416
1550
  if (value && typeof value === 'object') {
@@ -1419,7 +1553,8 @@ function _coerceText(value, path = 'renderable') {
1419
1553
  code: 'NON_RENDERABLE_VALUE',
1420
1554
  message: `Zenith Render Error: non-renderable object at ${path}. Use map() to render fields.`,
1421
1555
  path,
1422
- hint: 'Use map() to render object fields into nodes.'
1556
+ hint: 'Use map() to render object fields into nodes.',
1557
+ docsLink: DOCS_LINKS.expressionScope
1423
1558
  });
1424
1559
  }
1425
1560
  return String(value);
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.6.17",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {