@zenithbuild/runtime 0.7.3 → 0.7.5

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.
Files changed (49) hide show
  1. package/HYDRATION_CONTRACT.md +1 -1
  2. package/LICENSE +21 -0
  3. package/README.md +17 -18
  4. package/RUNTIME_CONTRACT.md +32 -12
  5. package/dist/cleanup.js +6 -3
  6. package/dist/diagnostics.d.ts +7 -0
  7. package/dist/diagnostics.js +7 -0
  8. package/dist/effect-runtime.d.ts +2 -0
  9. package/dist/effect-runtime.js +139 -0
  10. package/dist/effect-scheduler.d.ts +4 -0
  11. package/dist/effect-scheduler.js +88 -0
  12. package/dist/effect-utils.d.ts +19 -0
  13. package/dist/effect-utils.js +160 -0
  14. package/dist/env.d.ts +12 -0
  15. package/dist/env.js +18 -2
  16. package/dist/events.d.ts +1 -8
  17. package/dist/events.js +108 -46
  18. package/dist/expressions.d.ts +14 -0
  19. package/dist/expressions.js +295 -0
  20. package/dist/fragment-patch.d.ts +12 -0
  21. package/dist/fragment-patch.js +118 -0
  22. package/dist/hydrate.d.ts +3 -117
  23. package/dist/hydrate.js +235 -1817
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.js +3 -2
  26. package/dist/markup.d.ts +8 -0
  27. package/dist/markup.js +127 -0
  28. package/dist/mount-runtime.d.ts +1 -0
  29. package/dist/mount-runtime.js +39 -0
  30. package/dist/payload.d.ts +21 -0
  31. package/dist/payload.js +386 -0
  32. package/dist/presence.d.ts +67 -0
  33. package/dist/presence.js +297 -0
  34. package/dist/reactivity-core.d.ts +3 -0
  35. package/dist/reactivity-core.js +22 -0
  36. package/dist/render.d.ts +3 -0
  37. package/dist/render.js +340 -0
  38. package/dist/scanner.d.ts +1 -0
  39. package/dist/scanner.js +61 -0
  40. package/dist/side-effect-scope.d.ts +16 -0
  41. package/dist/side-effect-scope.js +99 -0
  42. package/dist/signal.js +1 -1
  43. package/dist/state.js +1 -1
  44. package/dist/template-parser.d.ts +1 -0
  45. package/dist/template-parser.js +268 -0
  46. package/dist/template.js +10 -11
  47. package/dist/zeneffect.d.ts +12 -14
  48. package/dist/zeneffect.js +25 -519
  49. package/package.json +5 -3
package/dist/hydrate.js CHANGED
@@ -1,51 +1,19 @@
1
- // ---------------------------------------------------------------------------
2
- // hydrate.js Zenith Runtime V0
3
- // ---------------------------------------------------------------------------
4
- // Contract-driven hydration engine.
5
- //
6
- // Runtime discovery is forbidden: this module only resolves selectors from
7
- // bundler-provided tables and executes explicit index-based bindings.
8
- // ---------------------------------------------------------------------------
9
- import { _registerDisposer, _registerListener, cleanup } from './cleanup.js';
10
- import { isZenithRuntimeError, rethrowZenithRuntimeError, throwZenithRuntimeError } from './diagnostics.js';
1
+ import { _resolveExpressionSignalIndices, _evaluateExpression } from './expressions.js';
2
+ import { _validatePayload, _resolveComponentProps, _deepFreezePayload } from './payload.js';
3
+ import { _registerDisposer, cleanup } from './cleanup.js';
4
+ import { rethrowZenithRuntimeError, DOCS_LINKS } from './diagnostics.js';
11
5
  import { signal } from './signal.js';
12
6
  import { state } from './state.js';
13
- import { zeneffect, zenEffect, zenMount, createSideEffectScope, activateSideEffectScope, disposeSideEffectScope } from './zeneffect.js';
14
- const ALIAS_CONFLICT = Symbol('alias_conflict');
15
- const ACTIVE_MARKER_CLASS = 'z-active';
16
- const UNRESOLVED_LITERAL = Symbol('unresolved_literal');
17
- const LEGACY_MARKUP_HELPER = 'html';
18
- const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
19
- const BOOLEAN_ATTRIBUTES = new Set([
20
- 'disabled', 'checked', 'selected', 'readonly', 'multiple',
21
- 'hidden', 'autofocus', 'required', 'open'
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_$]*)*)$/;
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
- });
32
- /**
33
- * Hydrate a pre-rendered DOM tree using explicit payload tables.
34
- *
35
- * @param {{
36
- * ir_version: number,
37
- * root: Document | Element,
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 } }>,
42
- * state_values: Array<*>,
43
- * state_keys?: Array<string>,
44
- * signals: Array<{ id: number, kind: 'signal', state_index: number }>,
45
- * components?: Array<{ instance: string, selector: string, create: Function, source?: { file: string, start?: { line: number, column: number }, end?: { line: number, column: number }, snippet?: string } }>
46
- * }} payload
47
- * @returns {() => void}
48
- */
7
+ import { effect, mount, zeneffect, zenEffect, zenMount } from './zeneffect.js';
8
+ import { createSideEffectScope, activateSideEffectScope, disposeSideEffectScope } from './side-effect-scope.js';
9
+ import { createNodeResolver } from './scanner.js';
10
+ import { bindEventMarkers } from './events.js';
11
+ import { _applyMarkerValue } from './render.js';
12
+ export { _createContextualFragment, _coerceText } from './render.js';
13
+ // Raw HTML boundary enforcement lives in render.js:
14
+ // attrName.toLowerCase() === 'innerhtml'
15
+ // innerHTML bindings are forbidden in Zenith
16
+ // attrName.toLowerCase() === 'unsafehtml'
49
17
  export function hydrate(payload) {
50
18
  cleanup();
51
19
  try {
@@ -53,299 +21,61 @@ export function hydrate(payload) {
53
21
  _deepFreezePayload(payload);
54
22
  const { root, expressions, markers, events, refs, stateValues, stateKeys, signals, components, route, params, ssrData, props, exprFns } = normalized;
55
23
  const componentBindings = Object.create(null);
56
- const signalMap = new Map();
57
- for (let i = 0; i < signals.length; i++) {
58
- const signalDescriptor = signals[i];
59
- const candidate = stateValues[signalDescriptor.state_index];
60
- if (!candidate || typeof candidate !== 'object') {
61
- throw new Error(`[Zenith Runtime] signal id ${signalDescriptor.id} did not resolve to an object`);
62
- }
63
- if (typeof candidate.get !== 'function' || typeof candidate.subscribe !== 'function') {
64
- throw new Error(`[Zenith Runtime] signal id ${signalDescriptor.id} must expose get() and subscribe()`);
65
- }
66
- signalMap.set(i, candidate);
67
- }
68
- const hydratedRefs = [];
69
- for (let i = 0; i < refs.length; i++) {
70
- const refBinding = refs[i];
71
- const targetRef = stateValues[refBinding.state_index];
72
- const nodes = _resolveNodes(root, refBinding.selector, refBinding.index, 'ref', refBinding.source);
73
- targetRef.current = nodes[0] || null;
74
- hydratedRefs.push(targetRef);
75
- }
76
- if (hydratedRefs.length > 0) {
77
- _registerDisposer(() => {
78
- for (let i = 0; i < hydratedRefs.length; i++) {
79
- hydratedRefs[i].current = null;
80
- }
81
- });
82
- }
83
- for (let i = 0; i < components.length; i++) {
84
- const component = components[i];
85
- const resolvedProps = Object.freeze(_resolveComponentProps(component.props || [], signalMap, {
86
- component: component.instance,
87
- route
88
- }));
89
- const hosts = _resolveNodes(root, component.selector, i, 'component', component.source);
90
- for (let j = 0; j < hosts.length; j++) {
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;
122
- }
123
- }
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.
138
- }
139
- }
140
- }
141
- }
142
- const expressionMarkerIndices = new Set();
143
- for (let i = 0; i < expressions.length; i++) {
144
- const expression = expressions[i];
145
- if (expressionMarkerIndices.has(expression.marker_index)) {
146
- throw new Error(`[Zenith Runtime] duplicate expression marker_index ${expression.marker_index}`);
147
- }
148
- expressionMarkerIndices.add(expression.marker_index);
149
- }
150
- const markerByIndex = new Map();
151
- const markerNodesByIndex = new Map();
152
- const markerIndices = new Set();
153
- for (let i = 0; i < markers.length; i++) {
154
- const marker = markers[i];
155
- if (markerIndices.has(marker.index)) {
156
- throw new Error(`[Zenith Runtime] duplicate marker index ${marker.index}`);
157
- }
158
- markerIndices.add(marker.index);
159
- markerByIndex.set(marker.index, marker);
160
- if (marker.kind === 'event') {
161
- continue;
162
- }
163
- const nodes = _resolveNodes(root, marker.selector, marker.index, marker.kind, marker.source);
164
- markerNodesByIndex.set(marker.index, nodes);
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
- }
172
- }
173
- for (let i = 0; i < expressions.length; i++) {
174
- if (!markerIndices.has(i)) {
175
- throw new Error(`[Zenith Runtime] missing marker index ${i}`);
176
- }
177
- }
178
- function renderMarkerByIndex(index) {
179
- const marker = markerByIndex.get(index);
180
- if (!marker || marker.kind === 'event') {
181
- return;
182
- }
183
- const nodes = markerNodesByIndex.get(index) || _resolveNodes(root, marker.selector, marker.index, marker.kind, marker.source);
184
- markerNodesByIndex.set(index, nodes);
185
- const value = _evaluateExpression(expressions[index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns, marker, null);
186
- _applyMarkerValue(nodes, marker, value);
187
- }
188
- const dependentMarkersBySignal = new Map();
189
- for (let i = 0; i < expressions.length; i++) {
190
- const expression = expressions[i];
191
- const signalIndices = _resolveExpressionSignalIndices(expression);
192
- if (signalIndices.length === 0) {
193
- continue;
194
- }
195
- for (let j = 0; j < signalIndices.length; j++) {
196
- const signalIndex = signalIndices[j];
197
- if (!dependentMarkersBySignal.has(signalIndex)) {
198
- dependentMarkersBySignal.set(signalIndex, []);
199
- }
200
- dependentMarkersBySignal.get(signalIndex).push(expression.marker_index);
201
- }
202
- }
203
- for (const [signalId, markerIndexes] of dependentMarkersBySignal.entries()) {
204
- const targetSignal = signalMap.get(signalId);
205
- if (!targetSignal) {
206
- throw new Error(`[Zenith Runtime] expression references unknown signal id ${signalId}`);
207
- }
208
- const unsubscribe = targetSignal.subscribe(() => {
209
- for (let i = 0; i < markerIndexes.length; i++) {
210
- renderMarkerByIndex(markerIndexes[i]);
211
- }
212
- });
213
- if (typeof unsubscribe === 'function') {
214
- _registerDisposer(unsubscribe);
215
- }
216
- }
217
- const dependentMarkersByComponentSignal = new Map();
218
- for (let i = 0; i < expressions.length; i++) {
219
- const expression = expressions[i];
220
- if (!expression || typeof expression !== 'object') {
221
- continue;
222
- }
223
- if (typeof expression.component_instance !== 'string' || typeof expression.component_binding !== 'string') {
224
- continue;
225
- }
226
- const instanceBindings = componentBindings[expression.component_instance];
227
- const candidate = instanceBindings && Object.prototype.hasOwnProperty.call(instanceBindings, expression.component_binding)
228
- ? instanceBindings[expression.component_binding]
229
- : undefined;
230
- if (!candidate || typeof candidate !== 'object') {
231
- continue;
232
- }
233
- if (typeof candidate.get !== 'function' || typeof candidate.subscribe !== 'function') {
234
- continue;
235
- }
236
- if (!dependentMarkersByComponentSignal.has(candidate)) {
237
- dependentMarkersByComponentSignal.set(candidate, []);
238
- }
239
- dependentMarkersByComponentSignal.get(candidate).push(expression.marker_index);
240
- }
241
- for (const [componentSignal, markerIndexes] of dependentMarkersByComponentSignal.entries()) {
242
- const unsubscribe = componentSignal.subscribe(() => {
243
- for (let i = 0; i < markerIndexes.length; i++) {
244
- renderMarkerByIndex(markerIndexes[i]);
245
- }
246
- });
247
- if (typeof unsubscribe === 'function') {
248
- _registerDisposer(unsubscribe);
249
- }
250
- }
251
- const eventIndices = new Set();
252
- const escDispatchEntries = [];
253
- for (let i = 0; i < events.length; i++) {
254
- const eventBinding = events[i];
255
- if (eventIndices.has(eventBinding.index)) {
256
- throw new Error(`[Zenith Runtime] duplicate event index ${eventBinding.index}`);
257
- }
258
- eventIndices.add(eventBinding.index);
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);
263
- if (typeof handler !== 'function') {
264
- const passedExpression = _describeBindingExpression(expressionBinding);
265
- throwZenithRuntimeError({
266
- phase: 'bind',
267
- code: 'BINDING_APPLY_FAILED',
268
- message: `Event binding at index ${eventBinding.index} expected a function reference. You passed: ${passedExpression}`,
269
- marker: { type: `data-zx-on-${eventBinding.event}`, id: eventBinding.index },
270
- path: `event[${eventBinding.index}].${eventBinding.event}`,
271
- hint: 'Use on:*={handler} or ensure the forwarded prop is a function.',
272
- docsLink: DOCS_LINKS.eventBinding,
273
- source: _resolveBindingSource(expressionBinding, marker, eventBinding)
274
- });
275
- }
276
- for (let j = 0; j < nodes.length; j++) {
277
- const node = nodes[j];
278
- const wrappedHandler = function zenithEventHandler(event) {
279
- try {
280
- return handler.call(this, event);
281
- }
282
- catch (error) {
283
- rethrowZenithRuntimeError(error, {
284
- phase: 'event',
285
- code: 'EVENT_HANDLER_FAILED',
286
- message: `Event handler failed for "${eventBinding.event}"`,
287
- marker: { type: `data-zx-on-${eventBinding.event}`, id: eventBinding.index },
288
- path: `event[${eventBinding.index}].${eventBinding.event}`,
289
- hint: 'Inspect the handler body and referenced state.',
290
- docsLink: DOCS_LINKS.eventBinding,
291
- source: _resolveBindingSource(expressionBinding, marker, eventBinding)
292
- });
293
- }
294
- };
295
- if (eventBinding.event === 'esc') {
296
- escDispatchEntries.push({
297
- node,
298
- handler: wrappedHandler
299
- });
300
- continue;
301
- }
302
- node.addEventListener(eventBinding.event, wrappedHandler);
303
- _registerListener(node, eventBinding.event, wrappedHandler);
304
- }
305
- }
306
- if (escDispatchEntries.length > 0) {
307
- const doc = root && root.ownerDocument ? root.ownerDocument : (typeof document !== 'undefined' ? document : null);
308
- if (doc && typeof doc.addEventListener === 'function') {
309
- const escDispatchListener = function zenithEscDispatch(event) {
310
- if (!event || event.key !== 'Escape') {
311
- return;
312
- }
313
- const activeElement = doc.activeElement || null;
314
- let targetEntry = null;
315
- if (activeElement && activeElement !== doc.body && activeElement !== doc.documentElement) {
316
- for (let i = escDispatchEntries.length - 1; i >= 0; i--) {
317
- const entry = escDispatchEntries[i];
318
- if (!entry || !entry.node) {
319
- continue;
320
- }
321
- if (!entry.node.isConnected) {
322
- continue;
323
- }
324
- if (typeof entry.node.contains === 'function' && entry.node.contains(activeElement)) {
325
- targetEntry = entry;
326
- break;
327
- }
328
- }
329
- }
330
- if (!targetEntry && (activeElement === null || activeElement === doc.body || activeElement === doc.documentElement)) {
331
- for (let i = escDispatchEntries.length - 1; i >= 0; i--) {
332
- const entry = escDispatchEntries[i];
333
- if (!entry || !entry.node || !entry.node.isConnected) {
334
- continue;
335
- }
336
- targetEntry = entry;
337
- break;
338
- }
339
- }
340
- if (!targetEntry) {
341
- return;
342
- }
343
- return targetEntry.handler.call(targetEntry.node, event);
344
- };
345
- doc.addEventListener('keydown', escDispatchListener);
346
- _registerListener(doc, 'keydown', escDispatchListener);
347
- }
348
- }
24
+ const resolveNodes = createNodeResolver(root);
25
+ const signalMap = _createSignalMap(signals, stateValues);
26
+ _hydrateRefs(refs, stateValues, resolveNodes);
27
+ _mountComponents({
28
+ components,
29
+ signalMap,
30
+ componentBindings,
31
+ route,
32
+ resolveNodes
33
+ });
34
+ const markerState = _hydrateMarkers({
35
+ expressions,
36
+ markers,
37
+ stateValues,
38
+ stateKeys,
39
+ signalMap,
40
+ componentBindings,
41
+ params,
42
+ ssrData,
43
+ props,
44
+ exprFns,
45
+ resolveNodes
46
+ });
47
+ const renderMarkerByIndex = (index) => _renderMarker({
48
+ index,
49
+ root,
50
+ expressions,
51
+ stateValues,
52
+ stateKeys,
53
+ signalMap,
54
+ componentBindings,
55
+ params,
56
+ ssrData,
57
+ props,
58
+ exprFns,
59
+ resolveNodes,
60
+ ...markerState
61
+ });
62
+ _bindSignalSubscriptions(expressions, signalMap, stateValues, renderMarkerByIndex);
63
+ _bindComponentSignalSubscriptions(expressions, componentBindings, renderMarkerByIndex);
64
+ bindEventMarkers({
65
+ root,
66
+ events,
67
+ expressions,
68
+ markerByIndex: markerState.markerByIndex,
69
+ stateValues,
70
+ stateKeys,
71
+ signalMap,
72
+ componentBindings,
73
+ params,
74
+ ssrData,
75
+ props,
76
+ exprFns,
77
+ resolveNodes
78
+ });
349
79
  return cleanup;
350
80
  }
351
81
  catch (error) {
@@ -357,1535 +87,223 @@ export function hydrate(payload) {
357
87
  });
358
88
  }
359
89
  }
360
- function _validatePayload(payload) {
361
- if (!payload || typeof payload !== 'object') {
362
- throw new Error('[Zenith Runtime] hydrate(payload) requires an object payload');
363
- }
364
- if (payload.ir_version !== 1) {
365
- throw new Error('[Zenith Runtime] unsupported ir_version (expected 1)');
366
- }
367
- const root = payload.root;
368
- const hasQuery = !!root && typeof root.querySelectorAll === 'function';
369
- if (!hasQuery) {
370
- throw new Error('[Zenith Runtime] hydrate(payload) requires payload.root with querySelectorAll');
371
- }
372
- const expressions = payload.expressions;
373
- if (!Array.isArray(expressions)) {
374
- throw new Error('[Zenith Runtime] hydrate(payload) requires expressions[]');
375
- }
376
- const markers = payload.markers;
377
- if (!Array.isArray(markers)) {
378
- throw new Error('[Zenith Runtime] hydrate(payload) requires markers[]');
379
- }
380
- const events = payload.events;
381
- if (!Array.isArray(events)) {
382
- throw new Error('[Zenith Runtime] hydrate(payload) requires events[]');
383
- }
384
- const refs = Array.isArray(payload.refs) ? payload.refs : [];
385
- const stateValues = payload.state_values;
386
- if (!Array.isArray(stateValues)) {
387
- throw new Error('[Zenith Runtime] hydrate(payload) requires state_values[]');
388
- }
389
- const stateKeys = Array.isArray(payload.state_keys) ? payload.state_keys : [];
390
- if (!Array.isArray(stateKeys)) {
391
- throw new Error('[Zenith Runtime] hydrate(payload) requires state_keys[] when provided');
392
- }
393
- for (let i = 0; i < stateKeys.length; i++) {
394
- if (typeof stateKeys[i] !== 'string') {
395
- throw new Error(`[Zenith Runtime] state_keys[${i}] must be a string`);
396
- }
397
- }
398
- const signals = payload.signals;
399
- if (!Array.isArray(signals)) {
400
- throw new Error('[Zenith Runtime] hydrate(payload) requires signals[]');
401
- }
402
- const components = Array.isArray(payload.components) ? payload.components : [];
403
- const route = typeof payload.route === 'string' && payload.route.length > 0
404
- ? payload.route
405
- : '<unknown>';
406
- const params = payload.params && typeof payload.params === 'object'
407
- ? payload.params
408
- : {};
409
- const ssrData = payload.ssr_data && typeof payload.ssr_data === 'object'
410
- ? payload.ssr_data
411
- : {};
412
- const exprFns = Array.isArray(payload.expr_fns) ? payload.expr_fns : [];
413
- if (markers.length !== expressions.length) {
414
- throw new Error(`[Zenith Runtime] marker/expression mismatch: markers=${markers.length}, expressions=${expressions.length}`);
415
- }
416
- for (let i = 0; i < expressions.length; i++) {
417
- const expression = expressions[i];
418
- if (!expression || typeof expression !== 'object' || Array.isArray(expression)) {
419
- throw new Error(`[Zenith Runtime] expression at position ${i} must be an object`);
420
- }
421
- if (!Number.isInteger(expression.marker_index) || expression.marker_index < 0 || expression.marker_index >= expressions.length) {
422
- throw new Error(`[Zenith Runtime] expression at position ${i} has invalid marker_index`);
423
- }
424
- if (expression.marker_index !== i) {
425
- throw new Error(`[Zenith Runtime] expression table out of order at position ${i}: marker_index=${expression.marker_index}`);
426
- }
427
- if (expression.fn_index !== undefined && expression.fn_index !== null) {
428
- if (!Number.isInteger(expression.fn_index) || expression.fn_index < 0) {
429
- throw new Error(`[Zenith Runtime] expression at position ${i} has invalid fn_index`);
430
- }
431
- }
432
- _assertValidSourceSpan(expression.source, `expression[${i}]`);
433
- if (expression.signal_indices !== undefined) {
434
- if (!Array.isArray(expression.signal_indices)) {
435
- throw new Error(`[Zenith Runtime] expression at position ${i} must provide signal_indices[]`);
436
- }
437
- for (let j = 0; j < expression.signal_indices.length; j++) {
438
- if (!Number.isInteger(expression.signal_indices[j]) || expression.signal_indices[j] < 0) {
439
- throw new Error(`[Zenith Runtime] expression at position ${i} has invalid signal_indices[${j}]`);
440
- }
441
- }
442
- }
443
- }
444
- for (let i = 0; i < markers.length; i++) {
445
- const marker = markers[i];
446
- if (!marker || typeof marker !== 'object' || Array.isArray(marker)) {
447
- throw new Error(`[Zenith Runtime] marker at position ${i} must be an object`);
448
- }
449
- if (!Number.isInteger(marker.index) || marker.index < 0 || marker.index >= expressions.length) {
450
- throw new Error(`[Zenith Runtime] marker at position ${i} has out-of-bounds index`);
451
- }
452
- if (marker.index !== i) {
453
- throw new Error(`[Zenith Runtime] marker table out of order at position ${i}: index=${marker.index}`);
454
- }
455
- if (marker.kind !== 'text' && marker.kind !== 'attr' && marker.kind !== 'event') {
456
- throw new Error(`[Zenith Runtime] marker at position ${i} has invalid kind`);
457
- }
458
- if (typeof marker.selector !== 'string' || marker.selector.length === 0) {
459
- throw new Error(`[Zenith Runtime] marker at position ${i} requires selector`);
460
- }
461
- if (marker.kind === 'attr' && (typeof marker.attr !== 'string' || marker.attr.length === 0)) {
462
- throw new Error(`[Zenith Runtime] attr marker at position ${i} requires attr name`);
463
- }
464
- _assertValidSourceSpan(marker.source, `marker[${i}]`);
465
- }
466
- for (let i = 0; i < events.length; i++) {
467
- const eventBinding = events[i];
468
- if (!eventBinding || typeof eventBinding !== 'object' || Array.isArray(eventBinding)) {
469
- throw new Error(`[Zenith Runtime] event binding at position ${i} must be an object`);
470
- }
471
- if (!Number.isInteger(eventBinding.index) || eventBinding.index < 0 || eventBinding.index >= expressions.length) {
472
- throw new Error(`[Zenith Runtime] event binding at position ${i} has out-of-bounds index`);
473
- }
474
- if (typeof eventBinding.event !== 'string' || eventBinding.event.length === 0) {
475
- throw new Error(`[Zenith Runtime] event binding at position ${i} requires event name`);
476
- }
477
- if (typeof eventBinding.selector !== 'string' || eventBinding.selector.length === 0) {
478
- throw new Error(`[Zenith Runtime] event binding at position ${i} requires selector`);
479
- }
480
- _assertValidSourceSpan(eventBinding.source, `event[${i}]`);
481
- }
482
- for (let i = 0; i < refs.length; i++) {
483
- const refBinding = refs[i];
484
- if (!refBinding || typeof refBinding !== 'object' || Array.isArray(refBinding)) {
485
- throw new Error(`[Zenith Runtime] ref binding at position ${i} must be an object`);
486
- }
487
- if (!Number.isInteger(refBinding.index) || refBinding.index < 0) {
488
- throw new Error(`[Zenith Runtime] ref binding at position ${i} requires non-negative index`);
489
- }
490
- if (!Number.isInteger(refBinding.state_index) ||
491
- refBinding.state_index < 0 ||
492
- refBinding.state_index >= stateValues.length) {
493
- throw new Error(`[Zenith Runtime] ref binding at position ${i} has out-of-bounds state_index`);
494
- }
495
- if (typeof refBinding.selector !== 'string' || refBinding.selector.length === 0) {
496
- throw new Error(`[Zenith Runtime] ref binding at position ${i} requires selector`);
497
- }
498
- _assertValidSourceSpan(refBinding.source, `ref[${i}]`);
499
- const candidate = stateValues[refBinding.state_index];
500
- if (!candidate || typeof candidate !== 'object' || !Object.prototype.hasOwnProperty.call(candidate, 'current')) {
501
- throw new Error(`[Zenith Runtime] ref binding at position ${i} must resolve to a ref-like object`);
502
- }
503
- }
90
+ function _createSignalMap(signals, stateValues) {
91
+ const signalMap = new Map();
504
92
  for (let i = 0; i < signals.length; i++) {
505
93
  const signalDescriptor = signals[i];
506
- if (!signalDescriptor || typeof signalDescriptor !== 'object' || Array.isArray(signalDescriptor)) {
507
- throw new Error(`[Zenith Runtime] signal descriptor at position ${i} must be an object`);
508
- }
509
- if (signalDescriptor.kind !== 'signal') {
510
- throw new Error(`[Zenith Runtime] signal descriptor at position ${i} requires kind="signal"`);
94
+ const candidate = stateValues[signalDescriptor.state_index];
95
+ if (!candidate || typeof candidate !== 'object') {
96
+ throw new Error(`[Zenith Runtime] signal id ${signalDescriptor.id} did not resolve to an object`);
511
97
  }
512
- if (!Number.isInteger(signalDescriptor.id) || signalDescriptor.id < 0) {
513
- throw new Error(`[Zenith Runtime] signal descriptor at position ${i} requires non-negative id`);
514
- }
515
- if (signalDescriptor.id !== i) {
516
- throw new Error(`[Zenith Runtime] signal table out of order at position ${i}: id=${signalDescriptor.id}`);
517
- }
518
- if (!Number.isInteger(signalDescriptor.state_index) || signalDescriptor.state_index < 0 || signalDescriptor.state_index >= stateValues.length) {
519
- throw new Error(`[Zenith Runtime] signal descriptor at position ${i} has out-of-bounds state_index`);
98
+ if (typeof candidate.get !== 'function' || typeof candidate.subscribe !== 'function') {
99
+ throw new Error(`[Zenith Runtime] signal id ${signalDescriptor.id} must expose get() and subscribe()`);
520
100
  }
101
+ signalMap.set(signalDescriptor.id, candidate);
521
102
  }
522
- for (let i = 0; i < components.length; i++) {
523
- const component = components[i];
524
- if (!component || typeof component !== 'object' || Array.isArray(component)) {
525
- throw new Error(`[Zenith Runtime] component at position ${i} must be an object`);
526
- }
527
- if (typeof component.instance !== 'string' || component.instance.length === 0) {
528
- throw new Error(`[Zenith Runtime] component at position ${i} requires instance`);
529
- }
530
- if (typeof component.selector !== 'string' || component.selector.length === 0) {
531
- throw new Error(`[Zenith Runtime] component at position ${i} requires selector`);
532
- }
533
- if (typeof component.create !== 'function') {
534
- throw new Error(`[Zenith Runtime] component at position ${i} requires create() function`);
535
- }
536
- _assertValidSourceSpan(component.source, `component[${i}]`);
537
- }
538
- if (payload.params !== undefined) {
539
- if (!payload.params || typeof payload.params !== 'object' || Array.isArray(payload.params)) {
540
- throw new Error('[Zenith Runtime] hydrate() requires params object');
541
- }
542
- }
543
- if (payload.ssr_data !== undefined) {
544
- if (!payload.ssr_data || typeof payload.ssr_data !== 'object' || Array.isArray(payload.ssr_data)) {
545
- throw new Error('[Zenith Runtime] hydrate() requires ssr_data object');
546
- }
547
- }
548
- const props = payload.props && typeof payload.props === 'object' && !Array.isArray(payload.props)
549
- ? payload.props
550
- : {};
551
- for (let i = 0; i < expressions.length; i++)
552
- Object.freeze(expressions[i]);
553
- for (let i = 0; i < markers.length; i++)
554
- Object.freeze(markers[i]);
555
- for (let i = 0; i < events.length; i++)
556
- Object.freeze(events[i]);
557
- for (let i = 0; i < refs.length; i++)
558
- Object.freeze(refs[i]);
559
- for (let i = 0; i < signals.length; i++)
560
- Object.freeze(signals[i]);
561
- for (let i = 0; i < components.length; i++) {
562
- const c = components[i];
563
- if (Array.isArray(c.props)) {
564
- for (let j = 0; j < c.props.length; j++) {
565
- const propDesc = c.props[j];
566
- if (propDesc &&
567
- typeof propDesc === 'object' &&
568
- _isHydrationFreezableContainer(propDesc.value)) {
569
- Object.freeze(propDesc.value);
570
- }
571
- Object.freeze(propDesc);
572
- }
573
- Object.freeze(c.props);
574
- }
575
- Object.freeze(c);
576
- }
577
- Object.freeze(expressions);
578
- Object.freeze(markers);
579
- Object.freeze(events);
580
- Object.freeze(refs);
581
- Object.freeze(signals);
582
- Object.freeze(components);
583
- const validatedPayload = {
584
- root,
585
- expressions,
586
- markers,
587
- events,
588
- refs,
589
- stateValues,
590
- stateKeys,
591
- signals,
592
- components,
593
- route,
594
- params: Object.freeze(params),
595
- ssrData: Object.freeze(ssrData),
596
- props: Object.freeze(props),
597
- exprFns: Object.freeze(exprFns)
598
- };
599
- return Object.freeze(validatedPayload);
103
+ return signalMap;
600
104
  }
601
- function _assertValidSourceSpan(source, contextLabel) {
602
- if (source === undefined || source === null) {
105
+ function _hydrateRefs(refs, stateValues, resolveNodes) {
106
+ if (refs.length === 0) {
603
107
  return;
604
108
  }
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
- }
631
- function _resolveComponentProps(propTable, signalMap, context = {}) {
632
- if (!Array.isArray(propTable)) {
633
- throw new Error('[Zenith Runtime] component props must be an array');
634
- }
635
- const resolved = Object.create(null);
636
- for (let i = 0; i < propTable.length; i++) {
637
- const descriptor = propTable[i];
638
- if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
639
- throw new Error(`[Zenith Runtime] component prop descriptor at index ${i} must be an object`);
640
- }
641
- if (typeof descriptor.name !== 'string' || descriptor.name.length === 0) {
642
- throw new Error(`[Zenith Runtime] component prop descriptor at index ${i} requires non-empty name`);
643
- }
644
- if (Object.prototype.hasOwnProperty.call(resolved, descriptor.name)) {
645
- throw new Error(`[Zenith Runtime] duplicate component prop "${descriptor.name}"`);
646
- }
647
- if (descriptor.type === 'static') {
648
- if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
649
- throw new Error(`[Zenith Runtime] component prop "${descriptor.name}" static value is missing`);
650
- }
651
- resolved[descriptor.name] = descriptor.value;
652
- continue;
653
- }
654
- if (descriptor.type === 'signal') {
655
- if (!Number.isInteger(descriptor.index)) {
656
- throw new Error(`[Zenith Runtime] component prop "${descriptor.name}" signal index must be an integer`);
657
- }
658
- const signalValue = signalMap.get(descriptor.index);
659
- if (!signalValue || typeof signalValue.get !== 'function') {
660
- throw new Error(`[Zenith Runtime]\nComponent: ${context.component || '<unknown>'}\nRoute: ${context.route || '<unknown>'}\nProp: ${descriptor.name}\nReason: signal index ${descriptor.index} did not resolve`);
661
- }
662
- resolved[descriptor.name] = signalValue;
663
- continue;
664
- }
665
- throw new Error(`[Zenith Runtime] unsupported component prop type "${descriptor.type}" for "${descriptor.name}"`);
666
- }
667
- return resolved;
668
- }
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);
673
- if (!nodes || nodes.length === 0) {
674
- const isRef = kind === 'ref';
675
- throwZenithRuntimeError({
676
- phase: 'bind',
677
- code: 'MARKER_MISSING',
678
- message: `Unresolved ${kind} marker index ${index}`,
679
- marker: { type: kind, id: index },
680
- path: `selector:${selector}`,
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
686
- });
687
- }
688
- return nodes;
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 [];
109
+ const hydratedRefs = [];
110
+ for (let i = 0; i < refs.length; i++) {
111
+ const refBinding = refs[i];
112
+ const targetRef = stateValues[refBinding.state_index];
113
+ const nodes = resolveNodes(refBinding.selector, refBinding.index, 'ref', refBinding.source);
114
+ targetRef.current = nodes[0] || null;
115
+ hydratedRefs.push(targetRef);
695
116
  }
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);
117
+ _registerDisposer(() => {
118
+ for (let i = 0; i < hydratedRefs.length; i++) {
119
+ hydratedRefs[i].current = null;
702
120
  }
703
- current = walker.nextNode();
704
- }
705
- return nodes;
706
- }
707
- function _resolveExpressionSignalIndices(binding) {
708
- if (!binding || typeof binding !== 'object') {
709
- return [];
710
- }
711
- if (Array.isArray(binding.signal_indices) && binding.signal_indices.length > 0) {
712
- return [...new Set(binding.signal_indices.filter((value) => Number.isInteger(value) && value >= 0))];
713
- }
714
- if (Number.isInteger(binding.signal_index) && binding.signal_index >= 0) {
715
- return [binding.signal_index];
716
- }
717
- return [];
121
+ });
718
122
  }
719
- function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props, exprFns, markerBinding = null, eventBinding = null) {
720
- if (binding.fn_index != null && binding.fn_index !== undefined) {
721
- const fns = Array.isArray(exprFns) ? exprFns : [];
722
- const fn = fns[binding.fn_index];
723
- if (typeof fn === 'function') {
123
+ function _mountComponents(context) {
124
+ const { components, signalMap, componentBindings, route, resolveNodes } = context;
125
+ for (let i = 0; i < components.length; i++) {
126
+ const component = components[i];
127
+ const resolvedProps = Object.freeze(_resolveComponentProps(component.props || [], signalMap, {
128
+ component: component.instance,
129
+ route
130
+ }));
131
+ const hosts = resolveNodes(component.selector, i, 'component', component.source);
132
+ for (let j = 0; j < hosts.length; j++) {
724
133
  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
- }
743
- }
744
- }
745
- if (binding.signal_index !== null && binding.signal_index !== undefined) {
746
- const signalValue = signalMap.get(binding.signal_index);
747
- if (!signalValue || typeof signalValue.get !== 'function') {
748
- throw new Error('[Zenith Runtime] expression.signal_index did not resolve to a signal');
749
- }
750
- return mode === 'event' ? signalValue : signalValue.get();
751
- }
752
- if (binding.state_index !== null && binding.state_index !== undefined) {
753
- const resolved = stateValues[binding.state_index];
754
- if (mode !== 'event' &&
755
- resolved &&
756
- typeof resolved === 'object' &&
757
- typeof resolved.get === 'function') {
758
- return resolved.get();
759
- }
760
- if (mode !== 'event' && typeof resolved === 'function') {
761
- return resolved();
762
- }
763
- return resolved;
764
- }
765
- if (typeof binding.component_instance === 'string' && typeof binding.component_binding === 'string') {
766
- const instanceBindings = componentBindings[binding.component_instance];
767
- const resolved = instanceBindings && Object.prototype.hasOwnProperty.call(instanceBindings, binding.component_binding)
768
- ? instanceBindings[binding.component_binding]
769
- : undefined;
770
- if (mode !== 'event' &&
771
- resolved &&
772
- typeof resolved === 'object' &&
773
- typeof resolved.get === 'function') {
774
- return resolved.get();
775
- }
776
- if (mode !== 'event' && typeof resolved === 'function') {
777
- return resolved();
778
- }
779
- return resolved;
780
- }
781
- if (binding.literal !== null && binding.literal !== undefined) {
782
- if (typeof binding.literal === 'string') {
783
- const trimmedLiteral = binding.literal.trim();
784
- const strictMemberValue = _resolveStrictMemberChainLiteral(trimmedLiteral, stateValues, stateKeys, params, ssrData, mode, props, binding.marker_index, _resolveBindingSource(binding, markerBinding, eventBinding));
785
- if (strictMemberValue !== UNRESOLVED_LITERAL) {
786
- return strictMemberValue;
787
- }
788
- if (trimmedLiteral === 'data' || trimmedLiteral === 'ssr') {
789
- return ssrData;
790
- }
791
- if (trimmedLiteral === 'params') {
792
- return params;
793
- }
794
- if (trimmedLiteral === 'props') {
795
- return props || {};
796
- }
797
- const primitiveValue = _resolvePrimitiveLiteral(trimmedLiteral);
798
- if (primitiveValue !== UNRESOLVED_LITERAL) {
799
- return primitiveValue;
800
- }
801
- if (_isLikelyExpressionLiteral(trimmedLiteral)) {
802
- const missingIdentifier = _extractMissingIdentifier(trimmedLiteral);
803
- throwZenithRuntimeError({
804
- phase: 'bind',
805
- code: 'UNRESOLVED_EXPRESSION',
806
- message: missingIdentifier
807
- ? `Unresolved expression identifier "${missingIdentifier}" in ${_truncateLiteralForError(trimmedLiteral)}`
808
- : `Failed to resolve expression literal: ${_truncateLiteralForError(trimmedLiteral)}`,
809
- marker: {
810
- type: _markerTypeForError(mode),
811
- id: binding.marker_index
134
+ const componentScope = createSideEffectScope(`${component.instance}:${j}`);
135
+ const runtimeApi = {
136
+ signal,
137
+ state,
138
+ zeneffect(effectOrDeps, optionsOrFn) {
139
+ return zeneffect(effectOrDeps, optionsOrFn, componentScope);
812
140
  },
813
- path: `expression[${binding.marker_index}]`,
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)
819
- });
820
- }
821
- }
822
- return binding.literal;
823
- }
824
- return '';
825
- }
826
- function _throwUnresolvedMemberChainError(literal, markerIndex, mode, pathSuffix, hint, source) {
827
- throwZenithRuntimeError({
828
- phase: 'bind',
829
- code: 'UNRESOLVED_EXPRESSION',
830
- message: `Failed to resolve expression literal: ${_truncateLiteralForError(literal)}`,
831
- marker: {
832
- type: _markerTypeForError(mode),
833
- id: markerIndex
834
- },
835
- path: `marker[${markerIndex}].${pathSuffix}`,
836
- hint,
837
- docsLink: DOCS_LINKS.expressionScope,
838
- source
839
- });
840
- }
841
- function _resolveStrictMemberChainLiteral(literal, stateValues, stateKeys, params, ssrData, mode, props, markerIndex, source) {
842
- if (typeof literal !== 'string' || !STRICT_MEMBER_CHAIN_LITERAL_RE.test(literal)) {
843
- return UNRESOLVED_LITERAL;
844
- }
845
- if (literal === 'true')
846
- return true;
847
- if (literal === 'false')
848
- return false;
849
- if (literal === 'null')
850
- return null;
851
- if (literal === 'undefined')
852
- return undefined;
853
- const segments = literal.split('.');
854
- const baseIdentifier = segments[0];
855
- const scope = _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props);
856
- if (!Object.prototype.hasOwnProperty.call(scope, baseIdentifier)) {
857
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${baseIdentifier}`, `Base identifier "${baseIdentifier}" is not bound. Check props/data/params and declared state keys.`, source);
858
- }
859
- let cursor = scope[baseIdentifier];
860
- let traversedPath = baseIdentifier;
861
- for (let i = 1; i < segments.length; i++) {
862
- const segment = segments[i];
863
- if (UNSAFE_MEMBER_KEYS.has(segment)) {
864
- throwZenithRuntimeError({
865
- phase: 'bind',
866
- code: 'UNSAFE_MEMBER_ACCESS',
867
- message: `Blocked unsafe member access: ${segment} in path "${literal}"`,
868
- path: `marker[${markerIndex}].expression.${literal}`,
869
- hint: 'Property access to __proto__, prototype, and constructor is forbidden.',
870
- docsLink: DOCS_LINKS.expressionScope,
871
- source
872
- });
873
- }
874
- if (cursor === null || cursor === undefined) {
875
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it is null or undefined.`, source);
876
- }
877
- const cursorType = typeof cursor;
878
- if (cursorType !== 'object' && cursorType !== 'function') {
879
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Cannot read "${segment}" from ${traversedPath} because it resolved to a ${cursorType}.`, source);
880
- }
881
- if (!Object.prototype.hasOwnProperty.call(cursor, segment)) {
882
- _throwUnresolvedMemberChainError(literal, markerIndex, mode, `expression.${traversedPath}.${segment}`, `Missing member "${segment}" on ${traversedPath}. Check your bindings.`, source);
883
- }
884
- cursor = cursor[segment];
885
- traversedPath = `${traversedPath}.${segment}`;
886
- }
887
- return cursor;
888
- }
889
- function _resolvePrimitiveLiteral(literal) {
890
- if (typeof literal !== 'string') {
891
- return UNRESOLVED_LITERAL;
892
- }
893
- if (literal === 'true')
894
- return true;
895
- if (literal === 'false')
896
- return false;
897
- if (literal === 'null')
898
- return null;
899
- if (literal === 'undefined')
900
- return undefined;
901
- if (/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(literal)) {
902
- return Number(literal);
903
- }
904
- if (literal.length >= 2 && literal.startsWith('"') && literal.endsWith('"')) {
905
- try {
906
- return JSON.parse(literal);
907
- }
908
- catch {
909
- return UNRESOLVED_LITERAL;
910
- }
911
- }
912
- if (literal.length >= 2 && literal.startsWith('\'') && literal.endsWith('\'')) {
913
- return literal
914
- .slice(1, -1)
915
- .replace(/\\\\/g, '\\')
916
- .replace(/\\'/g, '\'');
917
- }
918
- if (literal.length >= 2 && literal.startsWith('`') && literal.endsWith('`')) {
919
- return literal.slice(1, -1);
920
- }
921
- return UNRESOLVED_LITERAL;
922
- }
923
- function _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props) {
924
- const scope = Object.create(null);
925
- scope.params = params;
926
- scope.data = ssrData;
927
- scope.ssr = ssrData;
928
- scope.props = props || {};
929
- scope.__ZENITH_INTERNAL_ZENHTML = _zenhtml;
930
- scope.__zenith_fragment = function __zenith_fragment(html) {
931
- return {
932
- __zenith_fragment: true,
933
- html: html === null || html === undefined || html === false ? '' : String(html)
934
- };
935
- };
936
- scope[LEGACY_MARKUP_HELPER] = function legacyMarkup(strings, ...values) {
937
- if (!Array.isArray(strings)) {
938
- return scope.__zenith_fragment(strings);
939
- }
940
- let html = '';
941
- for (let i = 0; i < strings.length; i++) {
942
- html += strings[i];
943
- if (i < values.length) {
944
- html += _renderLegacyMarkupInterpolation(values[i]);
945
- }
946
- }
947
- return scope.__zenith_fragment(html);
948
- };
949
- const aliasConflicts = new Set();
950
- if (Array.isArray(stateKeys)) {
951
- for (let i = 0; i < stateKeys.length; i++) {
952
- const key = stateKeys[i];
953
- if (typeof key !== 'string' || key.length === 0) {
954
- continue;
955
- }
956
- if (Object.prototype.hasOwnProperty.call(scope, key)) {
957
- continue;
958
- }
959
- const value = stateValues[i];
960
- scope[key] = value;
961
- const alias = _deriveStateAlias(key);
962
- if (!alias || alias === key || aliasConflicts.has(alias)) {
963
- continue;
964
- }
965
- if (Object.prototype.hasOwnProperty.call(scope, alias)) {
966
- delete scope[alias];
967
- aliasConflicts.add(alias);
968
- continue;
969
- }
970
- scope[alias] = scope[key];
971
- }
972
- }
973
- return scope;
974
- }
975
- function _deriveStateAlias(key) {
976
- if (typeof key !== 'string' || key.length === 0) {
977
- return null;
978
- }
979
- if (!key.startsWith('__')) {
980
- return null;
981
- }
982
- if (key.startsWith('__z_frag_')) {
983
- return null;
984
- }
985
- const segments = key.split('_').filter(Boolean);
986
- for (let i = segments.length - 1; i >= 0; i--) {
987
- const candidate = segments[i];
988
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(candidate)) {
989
- return candidate === key ? null : candidate;
990
- }
991
- }
992
- const match = key.match(/([A-Za-z_$][A-Za-z0-9_$]*)$/);
993
- if (!match) {
994
- return null;
995
- }
996
- const alias = match[1];
997
- return alias === key ? null : alias;
998
- }
999
- function _isLikelyExpressionLiteral(literal) {
1000
- if (typeof literal !== 'string') {
1001
- return false;
1002
- }
1003
- const trimmed = literal.trim();
1004
- if (trimmed.length === 0) {
1005
- return false;
1006
- }
1007
- if (trimmed === 'true' || trimmed === 'false' || trimmed === 'null' || trimmed === 'undefined') {
1008
- return false;
1009
- }
1010
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(trimmed)) {
1011
- return true;
1012
- }
1013
- return /=>|[()[\]{}<>=?:.+\-*/%|&!]/.test(trimmed);
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
- }
1061
- function _markerTypeForError(kind) {
1062
- if (kind === 'text')
1063
- return 'data-zx-e';
1064
- if (kind === 'attr')
1065
- return 'data-zx-attr';
1066
- if (kind === 'event')
1067
- return 'data-zx-on';
1068
- return kind;
1069
- }
1070
- function _truncateLiteralForError(str) {
1071
- if (typeof str !== 'string')
1072
- return String(str);
1073
- const sanitized = str
1074
- .replace(/[A-Za-z]:\\[^\s"'`]+/g, '<path>')
1075
- .replace(/\/Users\/[^\s"'`]+/g, '<path>')
1076
- .replace(/\/home\/[^\s"'`]+/g, '<path>')
1077
- .replace(/\/private\/[^\s"'`]+/g, '<path>')
1078
- .replace(/\/tmp\/[^\s"'`]+/g, '<path>')
1079
- .replace(/\/var\/folders\/[^\s"'`]+/g, '<path>');
1080
- return sanitized.length > 100 ? `${sanitized.substring(0, 97)}...` : sanitized;
1081
- }
1082
- // ──────────────────────────────────────────────────────────────────────────────
1083
- // _zenhtml — LEGACY internal helper for compiler-generated fragment expressions.
1084
- //
1085
- // This function is intentionally NOT exported. It is bound into the evaluator
1086
- // scope under __ZENITH_INTERNAL_ZENHTML so only compiler-emitted literals can
1087
- // reach it. Sanitization happens ONLY inside this helper; existing innerHTML
1088
- // sites used for explicit innerHTML={…} bindings are NOT affected.
1089
- //
1090
- // Scheduled for removal before 1.0 (replaced by full fragment protocol).
1091
- // ──────────────────────────────────────────────────────────────────────────────
1092
- const _ZENHTML_UNSAFE_TAG_RE = /<script[\s>]/i;
1093
- const _ZENHTML_EVENT_ATTR_RE = /\bon[a-z]+\s*=/i;
1094
- const _ZENHTML_JS_URL_RE = /javascript\s*:/i;
1095
- const _ZENHTML_PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
1096
- const _ZENHTML_SCRIPT_CLOSE_RE = /<\/script/gi;
1097
- function _zenhtml(strings, ...values) {
1098
- if (!Array.isArray(strings)) {
1099
- throwZenithRuntimeError({
1100
- phase: 'render',
1101
- code: 'NON_RENDERABLE_VALUE',
1102
- message: '__ZENITH_INTERNAL_ZENHTML must be called as a tagged template literal',
1103
- hint: 'This helper only accepts tagged template syntax.'
1104
- });
1105
- }
1106
- let result = '';
1107
- for (let i = 0; i < strings.length; i++) {
1108
- result += strings[i];
1109
- if (i < values.length) {
1110
- const val = values[i];
1111
- result += _zenhtml_coerce(val, i);
1112
- }
1113
- }
1114
- // Final output sanitization: reject dangerous patterns in assembled HTML
1115
- if (_ZENHTML_UNSAFE_TAG_RE.test(result)) {
1116
- throwZenithRuntimeError({
1117
- phase: 'render',
1118
- code: 'NON_RENDERABLE_VALUE',
1119
- message: 'Embedded markup expression contains forbidden <script> tag',
1120
- hint: 'Script tags are not allowed in embedded markup expressions.'
1121
- });
1122
- }
1123
- if (_ZENHTML_EVENT_ATTR_RE.test(result)) {
1124
- throwZenithRuntimeError({
1125
- phase: 'render',
1126
- code: 'NON_RENDERABLE_VALUE',
1127
- message: 'Embedded markup expression contains inline event handler (on*=)',
1128
- hint: 'Use on:event={handler} bindings instead of inline event attributes.'
1129
- });
1130
- }
1131
- if (_ZENHTML_JS_URL_RE.test(result)) {
1132
- throwZenithRuntimeError({
1133
- phase: 'render',
1134
- code: 'NON_RENDERABLE_VALUE',
1135
- message: 'Embedded markup expression contains javascript: URL',
1136
- hint: 'javascript: URLs are forbidden in embedded markup.'
1137
- });
1138
- }
1139
- // Escape any residual </script sequences
1140
- result = result.replace(_ZENHTML_SCRIPT_CLOSE_RE, '<\\/script');
1141
- return {
1142
- __zenith_fragment: true,
1143
- html: result
1144
- };
1145
- }
1146
- function _zenhtml_coerce(val, interpolationIndex) {
1147
- if (val === null || val === undefined || val === false) {
1148
- return '';
1149
- }
1150
- if (val === true) {
1151
- return '';
1152
- }
1153
- if (typeof val === 'string') {
1154
- return val;
1155
- }
1156
- if (typeof val === 'number') {
1157
- return String(val);
1158
- }
1159
- if (typeof val === 'object' && val.__zenith_fragment === true && typeof val.html === 'string') {
1160
- return val.html;
1161
- }
1162
- if (Array.isArray(val)) {
1163
- let out = '';
1164
- for (let j = 0; j < val.length; j++) {
1165
- out += _zenhtml_coerce(val[j], interpolationIndex);
1166
- }
1167
- return out;
1168
- }
1169
- if (typeof val === 'object') {
1170
- // Check for prototype pollution keys
1171
- const keys = Object.keys(val);
1172
- for (let k = 0; k < keys.length; k++) {
1173
- if (_ZENHTML_PROTO_KEYS.has(keys[k])) {
1174
- throwZenithRuntimeError({
1175
- phase: 'render',
1176
- code: 'NON_RENDERABLE_VALUE',
1177
- message: `Embedded markup interpolation[${interpolationIndex}] contains forbidden key "${keys[k]}"`,
1178
- hint: 'Prototype pollution keys are forbidden in embedded markup expressions.'
1179
- });
1180
- }
1181
- }
1182
- throwZenithRuntimeError({
1183
- phase: 'render',
1184
- code: 'NON_RENDERABLE_VALUE',
1185
- message: `Embedded markup interpolation[${interpolationIndex}] resolved to a non-renderable object`,
1186
- hint: 'Only strings, numbers, booleans, null, undefined, arrays, and __zenith_fragment objects are allowed.'
1187
- });
1188
- }
1189
- // functions and symbols are not renderable
1190
- throwZenithRuntimeError({
1191
- phase: 'render',
1192
- code: 'NON_RENDERABLE_VALUE',
1193
- message: `Embedded markup interpolation[${interpolationIndex}] resolved to type "${typeof val}"`,
1194
- hint: 'Only strings, numbers, booleans, null, undefined, arrays, and __zenith_fragment objects are allowed.'
1195
- });
1196
- }
1197
- function _rewriteMarkupLiterals(expression) {
1198
- let out = '';
1199
- let index = 0;
1200
- let quote = null;
1201
- let escaped = false;
1202
- while (index < expression.length) {
1203
- const ch = expression[index];
1204
- if (quote) {
1205
- out += ch;
1206
- if (escaped) {
1207
- escaped = false;
1208
- index += 1;
1209
- continue;
1210
- }
1211
- if (ch === '\\') {
1212
- escaped = true;
1213
- index += 1;
1214
- continue;
1215
- }
1216
- if (ch === quote) {
1217
- quote = null;
1218
- }
1219
- index += 1;
1220
- continue;
1221
- }
1222
- if (ch === '\'' || ch === '"' || ch === '`') {
1223
- quote = ch;
1224
- out += ch;
1225
- index += 1;
1226
- continue;
1227
- }
1228
- if (ch === '<') {
1229
- const markup = _readMarkupLiteral(expression, index);
1230
- if (markup) {
1231
- out += `__zenith_fragment(${_markupLiteralToTemplate(markup.value)})`;
1232
- index = markup.end;
1233
- continue;
1234
- }
1235
- }
1236
- out += ch;
1237
- index += 1;
1238
- }
1239
- return out;
1240
- }
1241
- function _readMarkupLiteral(source, start) {
1242
- if (source[start] !== '<') {
1243
- return null;
1244
- }
1245
- const firstTag = _readTagToken(source, start);
1246
- if (!firstTag || firstTag.isClosing) {
1247
- return null;
1248
- }
1249
- if (firstTag.selfClosing) {
1250
- return {
1251
- value: source.slice(start, firstTag.end),
1252
- end: firstTag.end
1253
- };
1254
- }
1255
- const stack = [firstTag.name];
1256
- let cursor = firstTag.end;
1257
- while (cursor < source.length) {
1258
- const nextLt = source.indexOf('<', cursor);
1259
- if (nextLt < 0) {
1260
- return null;
1261
- }
1262
- const token = _readTagToken(source, nextLt);
1263
- if (!token) {
1264
- cursor = nextLt + 1;
1265
- continue;
1266
- }
1267
- cursor = token.end;
1268
- if (token.selfClosing) {
1269
- continue;
1270
- }
1271
- if (token.isClosing) {
1272
- const expected = stack[stack.length - 1];
1273
- if (token.name !== expected) {
1274
- return null;
1275
- }
1276
- stack.pop();
1277
- if (stack.length === 0) {
1278
- return {
1279
- value: source.slice(start, token.end),
1280
- end: token.end
141
+ zenEffect(effectFn, options) {
142
+ return zenEffect(effectFn, options, componentScope);
143
+ },
144
+ zenMount(callback) {
145
+ return zenMount(callback, componentScope);
146
+ },
147
+ effect(fnOrDeps, optionsOrFn) {
148
+ return effect(fnOrDeps, optionsOrFn, componentScope);
149
+ },
150
+ mount(callback) {
151
+ return mount(callback, componentScope);
152
+ }
1281
153
  };
154
+ const instance = component.create(hosts[j], resolvedProps, runtimeApi);
155
+ if (!instance || typeof instance !== 'object') {
156
+ throw new Error(`[Zenith Runtime] component factory for ${component.instance} must return an object`);
157
+ }
158
+ if (typeof instance.mount === 'function') {
159
+ instance.mount();
160
+ }
161
+ activateSideEffectScope(componentScope);
162
+ _registerDisposer(() => {
163
+ disposeSideEffectScope(componentScope);
164
+ if (typeof instance.destroy === 'function') {
165
+ instance.destroy();
166
+ }
167
+ });
168
+ if (instance.bindings && typeof instance.bindings === 'object') {
169
+ componentBindings[component.instance] = instance.bindings;
170
+ }
1282
171
  }
1283
- continue;
1284
- }
1285
- stack.push(token.name);
1286
- }
1287
- return null;
1288
- }
1289
- function _readTagToken(source, start) {
1290
- if (source[start] !== '<') {
1291
- return null;
1292
- }
1293
- let index = start + 1;
1294
- let isClosing = false;
1295
- if (index < source.length && source[index] === '/') {
1296
- isClosing = true;
1297
- index += 1;
1298
- }
1299
- if (index >= source.length || !/[A-Za-z_]/.test(source[index])) {
1300
- return null;
1301
- }
1302
- const nameStart = index;
1303
- while (index < source.length && /[A-Za-z0-9:_-]/.test(source[index])) {
1304
- index += 1;
1305
- }
1306
- if (index === nameStart) {
1307
- return null;
1308
- }
1309
- const name = source.slice(nameStart, index);
1310
- let quote = null;
1311
- let escaped = false;
1312
- let braceDepth = 0;
1313
- while (index < source.length) {
1314
- const ch = source[index];
1315
- if (quote) {
1316
- if (escaped) {
1317
- escaped = false;
1318
- index += 1;
1319
- continue;
1320
- }
1321
- if (ch === '\\') {
1322
- escaped = true;
1323
- index += 1;
1324
- continue;
1325
- }
1326
- if (ch === quote) {
1327
- quote = null;
1328
- }
1329
- index += 1;
1330
- continue;
1331
- }
1332
- if (ch === '\'' || ch === '"' || ch === '`') {
1333
- quote = ch;
1334
- index += 1;
1335
- continue;
1336
- }
1337
- if (ch === '{') {
1338
- braceDepth += 1;
1339
- index += 1;
1340
- continue;
1341
- }
1342
- if (ch === '}') {
1343
- braceDepth = Math.max(0, braceDepth - 1);
1344
- index += 1;
1345
- continue;
1346
- }
1347
- if (ch === '>' && braceDepth === 0) {
1348
- const segment = source.slice(start, index + 1);
1349
- const selfClosing = !isClosing && /\/\s*>$/.test(segment);
1350
- return { name, isClosing, selfClosing, end: index + 1 };
1351
- }
1352
- index += 1;
1353
- }
1354
- return null;
1355
- }
1356
- function _markupLiteralToTemplate(markup) {
1357
- let out = '`';
1358
- let index = 0;
1359
- while (index < markup.length) {
1360
- const ch = markup[index];
1361
- if (ch === '{') {
1362
- const segment = _readBalancedBraces(markup, index);
1363
- if (segment) {
1364
- if (_isAttributeExpressionStart(markup, index)) {
1365
- out += '"${' + segment.content + '}"';
172
+ catch (error) {
173
+ try {
174
+ rethrowZenithRuntimeError(error, {
175
+ phase: 'hydrate',
176
+ code: 'COMPONENT_BOOTSTRAP_FAILED',
177
+ message: `Component bootstrap failed for "${component.instance}"`,
178
+ path: `component[${component.instance}]`,
179
+ hint: 'Fix the failing component and refresh; other components continue mounting.',
180
+ docsLink: DOCS_LINKS.componentBootstrap,
181
+ source: component.source
182
+ });
1366
183
  }
1367
- else {
1368
- out += '${' + segment.content + '}';
184
+ catch {
1369
185
  }
1370
- index = segment.end;
1371
- continue;
1372
186
  }
1373
187
  }
1374
- if (ch === '`') {
1375
- out += '\\`';
1376
- index += 1;
1377
- continue;
1378
- }
1379
- if (ch === '\\') {
1380
- out += '\\\\';
1381
- index += 1;
1382
- continue;
1383
- }
1384
- if (ch === '$' && markup[index + 1] === '{') {
1385
- out += '\\${';
1386
- index += 2;
1387
- continue;
1388
- }
1389
- out += ch;
1390
- index += 1;
1391
188
  }
1392
- out += '`';
1393
- return out;
1394
189
  }
1395
- function _isAttributeExpressionStart(markup, start) {
1396
- let cursor = start - 1;
1397
- while (cursor >= 0) {
1398
- const ch = markup[cursor];
1399
- if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
1400
- cursor -= 1;
1401
- continue;
190
+ function _hydrateMarkers(context) {
191
+ const { expressions, markers, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, props, exprFns, resolveNodes } = context;
192
+ const markerByIndex = new Map();
193
+ const markerNodesByIndex = new Map();
194
+ const markerIndices = new Set();
195
+ const expressionMarkerIndices = new Set();
196
+ for (let i = 0; i < expressions.length; i++) {
197
+ const expression = expressions[i];
198
+ if (expressionMarkerIndices.has(expression.marker_index)) {
199
+ throw new Error(`[Zenith Runtime] duplicate expression marker_index ${expression.marker_index}`);
1402
200
  }
1403
- return ch === '=';
1404
- }
1405
- return false;
1406
- }
1407
- function _readBalancedBraces(source, start) {
1408
- if (source[start] !== '{') {
1409
- return null;
201
+ expressionMarkerIndices.add(expression.marker_index);
1410
202
  }
1411
- let depth = 1;
1412
- let index = start + 1;
1413
- let quote = null;
1414
- let escaped = false;
1415
- let content = '';
1416
- while (index < source.length) {
1417
- const ch = source[index];
1418
- if (quote) {
1419
- content += ch;
1420
- if (escaped) {
1421
- escaped = false;
1422
- index += 1;
1423
- continue;
1424
- }
1425
- if (ch === '\\') {
1426
- escaped = true;
1427
- index += 1;
1428
- continue;
1429
- }
1430
- if (ch === quote) {
1431
- quote = null;
1432
- }
1433
- index += 1;
1434
- continue;
1435
- }
1436
- if (ch === '\'' || ch === '"' || ch === '`') {
1437
- quote = ch;
1438
- content += ch;
1439
- index += 1;
1440
- continue;
1441
- }
1442
- if (ch === '{') {
1443
- depth += 1;
1444
- content += ch;
1445
- index += 1;
1446
- continue;
203
+ for (let i = 0; i < markers.length; i++) {
204
+ const marker = markers[i];
205
+ if (markerIndices.has(marker.index)) {
206
+ throw new Error(`[Zenith Runtime] duplicate marker index ${marker.index}`);
1447
207
  }
1448
- if (ch === '}') {
1449
- depth -= 1;
1450
- if (depth === 0) {
1451
- return {
1452
- content,
1453
- end: index + 1
1454
- };
1455
- }
1456
- content += ch;
1457
- index += 1;
208
+ markerIndices.add(marker.index);
209
+ markerByIndex.set(marker.index, marker);
210
+ if (marker.kind === 'event') {
1458
211
  continue;
1459
212
  }
1460
- content += ch;
1461
- index += 1;
213
+ const nodes = resolveNodes(marker.selector, marker.index, marker.kind, marker.source);
214
+ markerNodesByIndex.set(marker.index, nodes);
215
+ const value = _evaluateExpression(expressions[marker.index], stateValues, stateKeys, signalMap, componentBindings, params, ssrData, marker.kind, props, exprFns, marker, null);
216
+ _applyMarkerValue(nodes, marker, value);
1462
217
  }
1463
- return null;
1464
- }
1465
- function _applyMarkerValue(nodes, marker, value) {
1466
- const markerPath = `marker[${marker.index}]`;
1467
- for (let i = 0; i < nodes.length; i++) {
1468
- try {
1469
- const node = nodes[i];
1470
- if (marker.kind === 'text') {
1471
- if (node && node.nodeType === 8) {
1472
- _applyCommentMarkerValue(node, value, `${markerPath}.text`);
1473
- continue;
1474
- }
1475
- if (_isStructuralFragment(value)) {
1476
- _mountStructuralFragment(node, value, `${markerPath}.text`);
1477
- continue;
1478
- }
1479
- const html = _renderFragmentValue(value, `${markerPath}.text`);
1480
- if (html !== null) {
1481
- node.innerHTML = html;
1482
- }
1483
- else {
1484
- node.textContent = _coerceText(value, `${markerPath}.text`);
1485
- }
1486
- continue;
1487
- }
1488
- if (marker.kind === 'attr') {
1489
- _applyAttribute(node, marker.attr, value);
1490
- }
1491
- }
1492
- catch (error) {
1493
- rethrowZenithRuntimeError(error, {
1494
- phase: 'bind',
1495
- code: 'BINDING_APPLY_FAILED',
1496
- message: `Failed to apply ${marker.kind} binding at marker ${marker.index}`,
1497
- marker: {
1498
- type: marker.kind === 'attr' ? `attr:${marker.attr}` : marker.kind,
1499
- id: marker.index
1500
- },
1501
- path: marker.kind === 'attr'
1502
- ? `${markerPath}.attr.${marker.attr}`
1503
- : `${markerPath}.${marker.kind}`,
1504
- hint: 'Check the binding value type and marker mapping.',
1505
- docsLink: DOCS_LINKS.markerTable,
1506
- source: marker.source
1507
- });
218
+ for (let i = 0; i < expressions.length; i++) {
219
+ if (!markerIndices.has(i)) {
220
+ throw new Error(`[Zenith Runtime] missing marker index ${i}`);
1508
221
  }
1509
222
  }
223
+ return { markerByIndex, markerNodesByIndex };
1510
224
  }
1511
- function _applyCommentMarkerValue(anchor, value, rootPath) {
1512
- if (_isStructuralFragment(value)) {
1513
- _mountStructuralFragmentIntoCommentRange(anchor, value, rootPath);
225
+ function _renderMarker(context) {
226
+ const marker = context.markerByIndex.get(context.index);
227
+ if (!marker || marker.kind === 'event') {
1514
228
  return;
1515
229
  }
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
- }
1538
- function _isStructuralFragment(value) {
1539
- if (Array.isArray(value)) {
1540
- for (let i = 0; i < value.length; i++) {
1541
- if (_isStructuralFragment(value[i]))
1542
- return true;
1543
- }
1544
- return false;
1545
- }
1546
- return value && typeof value === 'object' && value.__zenith_fragment === true && typeof value.mount === 'function';
230
+ const nodes = context.markerNodesByIndex.get(context.index)
231
+ || context.resolveNodes(marker.selector, marker.index, marker.kind, marker.source);
232
+ context.markerNodesByIndex.set(context.index, nodes);
233
+ const value = _evaluateExpression(context.expressions[context.index], context.stateValues, context.stateKeys, context.signalMap, context.componentBindings, context.params, context.ssrData, marker.kind, context.props, context.exprFns, marker, null);
234
+ _applyMarkerValue(nodes, marker, value);
1547
235
  }
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;
236
+ function _isSignalLike(candidate) {
237
+ return Boolean(candidate)
238
+ && typeof candidate === 'object'
239
+ && typeof candidate.get === 'function'
240
+ && typeof candidate.subscribe === 'function';
1561
241
  }
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) {
242
+ function _recordSignalMarkerDependency(dependentMarkersBySignal, signalValue, markerIndex) {
243
+ if (!_isSignalLike(signalValue)) {
1590
244
  return;
1591
245
  }
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);
246
+ if (!dependentMarkersBySignal.has(signalValue)) {
247
+ dependentMarkersBySignal.set(signalValue, []);
1645
248
  }
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;
249
+ dependentMarkersBySignal.get(signalValue).push(markerIndex);
1657
250
  }
1658
- function _mountStructuralFragment(container, value, rootPath = 'renderable') {
1659
- if (container.__z_unmounts) {
1660
- for (let i = 0; i < container.__z_unmounts.length; i++) {
1661
- try {
1662
- container.__z_unmounts[i]();
1663
- }
1664
- catch (e) { }
1665
- }
1666
- }
1667
- container.innerHTML = '';
1668
- const newUnmounts = [];
1669
- function mountItem(item, path) {
1670
- if (Array.isArray(item)) {
1671
- for (let i = 0; i < item.length; i++)
1672
- mountItem(item[i], `${path}[${i}]`);
1673
- return;
1674
- }
1675
- if (item && item.__zenith_fragment === true && typeof item.mount === 'function') {
1676
- item.mount(container);
1677
- if (typeof item.unmount === 'function') {
1678
- newUnmounts.push(item.unmount.bind(item));
251
+ function _bindSignalSubscriptions(expressions, signalMap, stateValues, renderMarkerByIndex) {
252
+ const dependentMarkersBySignal = new Map();
253
+ for (let i = 0; i < expressions.length; i++) {
254
+ const expression = expressions[i];
255
+ const signalIndices = _resolveExpressionSignalIndices(expression);
256
+ for (let j = 0; j < signalIndices.length; j++) {
257
+ const signalIndex = signalIndices[j];
258
+ const targetSignal = signalMap.get(signalIndex);
259
+ if (!targetSignal) {
260
+ throw new Error(`[Zenith Runtime] expression references unknown signal id ${signalIndex}`);
1679
261
  }
262
+ _recordSignalMarkerDependency(dependentMarkersBySignal, targetSignal, expression.marker_index);
1680
263
  }
1681
- else {
1682
- const text = _coerceText(item, path);
1683
- if (text || text === '') {
1684
- const textNode = document.createTextNode(text);
1685
- container.appendChild(textNode);
1686
- newUnmounts.push(() => {
1687
- if (textNode.parentNode)
1688
- textNode.parentNode.removeChild(textNode);
1689
- });
1690
- }
264
+ if (Number.isInteger(expression?.state_index)) {
265
+ _recordSignalMarkerDependency(dependentMarkersBySignal, stateValues[expression.state_index], expression.marker_index);
1691
266
  }
1692
267
  }
1693
- try {
1694
- mountItem(value, rootPath);
1695
- }
1696
- catch (error) {
1697
- rethrowZenithRuntimeError(error, {
1698
- phase: 'render',
1699
- code: 'FRAGMENT_MOUNT_FAILED',
1700
- message: 'Fragment mount failed',
1701
- path: rootPath,
1702
- hint: 'Verify fragment values and nested renderable arrays.',
1703
- docsLink: DOCS_LINKS.markerTable
1704
- });
1705
- }
1706
- container.__z_unmounts = newUnmounts;
1707
- }
1708
- function _coerceText(value, path = 'renderable') {
1709
- if (value === null || value === undefined || value === false || value === true)
1710
- return '';
1711
- if (typeof value === 'function') {
1712
- throwZenithRuntimeError({
1713
- phase: 'render',
1714
- code: 'NON_RENDERABLE_VALUE',
1715
- message: `Zenith Render Error: non-renderable function at ${path}. Use map() to render fields.`,
1716
- path,
1717
- hint: 'Convert functions into explicit event handlers or renderable text.',
1718
- docsLink: DOCS_LINKS.expressionScope
1719
- });
1720
- }
1721
- if (value && typeof value === 'object') {
1722
- throwZenithRuntimeError({
1723
- phase: 'render',
1724
- code: 'NON_RENDERABLE_VALUE',
1725
- message: `Zenith Render Error: non-renderable object at ${path}. Use map() to render fields.`,
1726
- path,
1727
- hint: 'Use map() to render object fields into nodes.',
1728
- docsLink: DOCS_LINKS.expressionScope
1729
- });
1730
- }
1731
- return String(value);
1732
- }
1733
- function _renderFragmentValue(value, path = 'renderable') {
1734
- if (value === null || value === undefined || value === false || value === true) {
1735
- return '';
1736
- }
1737
- if (Array.isArray(value)) {
1738
- let out = '';
1739
- for (let i = 0; i < value.length; i++) {
1740
- const itemPath = `${path}[${i}]`;
1741
- const piece = _renderFragmentValue(value[i], itemPath);
1742
- if (piece !== null) {
1743
- out += piece;
1744
- continue;
268
+ for (const [targetSignal, markerIndexes] of dependentMarkersBySignal.entries()) {
269
+ const unsubscribe = targetSignal.subscribe(() => {
270
+ for (let i = 0; i < markerIndexes.length; i++) {
271
+ renderMarkerByIndex(markerIndexes[i]);
1745
272
  }
1746
- out += _escapeHtml(_coerceText(value[i], itemPath));
1747
- }
1748
- return out;
1749
- }
1750
- if (value &&
1751
- typeof value === 'object' &&
1752
- value.__zenith_fragment === true &&
1753
- typeof value.html === 'string') {
1754
- return value.html;
1755
- }
1756
- return null;
1757
- }
1758
- function _escapeHtml(input) {
1759
- return String(input)
1760
- .replace(/&/g, '&amp;')
1761
- .replace(/</g, '&lt;')
1762
- .replace(/>/g, '&gt;')
1763
- .replace(/"/g, '&quot;')
1764
- .replace(/'/g, '&#39;');
1765
- }
1766
- function _renderLegacyMarkupInterpolation(value) {
1767
- if (value === null || value === undefined || value === false || value === true) {
1768
- return '';
1769
- }
1770
- if (Array.isArray(value)) {
1771
- let out = '';
1772
- for (let i = 0; i < value.length; i++) {
1773
- out += _renderLegacyMarkupInterpolation(value[i]);
273
+ });
274
+ if (typeof unsubscribe === 'function') {
275
+ _registerDisposer(unsubscribe);
1774
276
  }
1775
- return out;
1776
- }
1777
- if (value &&
1778
- typeof value === 'object' &&
1779
- value.__zenith_fragment === true &&
1780
- typeof value.html === 'string') {
1781
- return value.html;
1782
277
  }
1783
- return _escapeHtml(_coerceText(value, 'legacy markup interpolation'));
1784
278
  }
1785
- function _applyAttribute(node, attrName, value) {
1786
- if (typeof attrName === 'string' && attrName.toLowerCase() === 'innerhtml') {
1787
- node.innerHTML = value === null || value === undefined || value === false
1788
- ? ''
1789
- : String(value);
1790
- return;
1791
- }
1792
- if (attrName === 'class' || attrName === 'className') {
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;
1799
- return;
1800
- }
1801
- if (attrName === 'style') {
1802
- if (value === null || value === undefined || value === false) {
1803
- node.removeAttribute('style');
1804
- return;
1805
- }
1806
- if (typeof value === 'string') {
1807
- node.setAttribute('style', value);
1808
- return;
279
+ function _bindComponentSignalSubscriptions(expressions, componentBindings, renderMarkerByIndex) {
280
+ const dependentMarkersByComponentSignal = new Map();
281
+ for (let i = 0; i < expressions.length; i++) {
282
+ const expression = expressions[i];
283
+ if (typeof expression?.component_instance !== 'string' || typeof expression.component_binding !== 'string') {
284
+ continue;
1809
285
  }
1810
- if (typeof value === 'object') {
1811
- const entries = Object.entries(value);
1812
- let styleText = '';
1813
- for (let i = 0; i < entries.length; i++) {
1814
- const [key, rawValue] = entries[i];
1815
- styleText += `${key}: ${rawValue};`;
1816
- }
1817
- node.setAttribute('style', styleText);
1818
- return;
286
+ const instanceBindings = componentBindings[expression.component_instance];
287
+ const candidate = instanceBindings?.[expression.component_binding];
288
+ if (!candidate || typeof candidate !== 'object') {
289
+ continue;
1819
290
  }
1820
- node.setAttribute('style', String(value));
1821
- return;
1822
- }
1823
- if (BOOLEAN_ATTRIBUTES.has(attrName)) {
1824
- if (value) {
1825
- node.setAttribute(attrName, '');
291
+ if (typeof candidate.get !== 'function' || typeof candidate.subscribe !== 'function') {
292
+ continue;
1826
293
  }
1827
- else {
1828
- node.removeAttribute(attrName);
294
+ if (!dependentMarkersByComponentSignal.has(candidate)) {
295
+ dependentMarkersByComponentSignal.set(candidate, []);
1829
296
  }
1830
- return;
297
+ dependentMarkersByComponentSignal.get(candidate).push(expression.marker_index);
1831
298
  }
1832
- if (value === null || value === undefined || value === false) {
1833
- node.removeAttribute(attrName);
1834
- return;
1835
- }
1836
- node.setAttribute(attrName, String(value));
1837
- }
1838
- function _deepFreezePayload(obj) {
1839
- if (!_isHydrationFreezableContainer(obj) || Object.isFrozen(obj))
1840
- return;
1841
- Object.freeze(obj);
1842
- const keys = Object.keys(obj);
1843
- for (let i = 0; i < keys.length; i++) {
1844
- const val = obj[keys[i]];
1845
- if (val && typeof val === 'object' && typeof val !== 'function') {
1846
- _deepFreezePayload(val);
299
+ for (const [componentSignal, markerIndexes] of dependentMarkersByComponentSignal.entries()) {
300
+ const unsubscribe = componentSignal.subscribe(() => {
301
+ for (let i = 0; i < markerIndexes.length; i++) {
302
+ renderMarkerByIndex(markerIndexes[i]);
303
+ }
304
+ });
305
+ if (typeof unsubscribe === 'function') {
306
+ _registerDisposer(unsubscribe);
1847
307
  }
1848
308
  }
1849
309
  }
1850
- function _isHydrationRefObject(obj) {
1851
- if (!obj || typeof obj !== 'object') {
1852
- return false;
1853
- }
1854
- if (obj.__zenith_ref === true) {
1855
- return true;
1856
- }
1857
- if (!Object.prototype.hasOwnProperty.call(obj, 'current')) {
1858
- return false;
1859
- }
1860
- if (typeof obj.get === 'function' && typeof obj.subscribe === 'function') {
1861
- return false;
1862
- }
1863
- const keys = Object.keys(obj);
1864
- if (keys.length === 1 && keys[0] === 'current') {
1865
- return true;
1866
- }
1867
- if (keys.length === 2 && keys.includes('current') && keys.includes('__zenith_ref')) {
1868
- return true;
1869
- }
1870
- return false;
1871
- }
1872
- function _isPlainObject(value) {
1873
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
1874
- return false;
1875
- }
1876
- const proto = Object.getPrototypeOf(value);
1877
- return proto === Object.prototype || proto === null;
1878
- }
1879
- function _isHydrationFreezableContainer(value) {
1880
- if (Array.isArray(value))
1881
- return true;
1882
- if (!_isPlainObject(value))
1883
- return false;
1884
- if (_isHydrationRefObject(value)) {
1885
- return false;
1886
- }
1887
- if (typeof value.get === 'function' && typeof value.subscribe === 'function') {
1888
- return false;
1889
- }
1890
- return true;
1891
- }