@zenithbuild/runtime 0.5.0-beta.2.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,8 @@
1
1
  # Zenith Runtime V0 Hydration Contract
2
2
 
3
+ Canonical public docs: `../zenith-docs/documentation/contracts/hydration-contract.md`
4
+
5
+
3
6
  Status: FROZEN (V0)
4
7
 
5
8
  This contract locks hydration and reactivity boundaries for Zenith V0.
package/README.md CHANGED
@@ -5,6 +5,12 @@
5
5
 
6
6
  The core runtime library for the Zenith framework.
7
7
 
8
+ ## Canonical Docs
9
+
10
+ - Runtime contract: `../zenith-docs/documentation/contracts/runtime-contract.md`
11
+ - Hydration contract: `../zenith-docs/documentation/contracts/hydration-contract.md`
12
+ - Reactive binding model: `../zenith-docs/documentation/reference/reactive-binding-model.md`
13
+
8
14
  ## Overview
9
15
  This package provides the reactivity system, hydration logic, and Virtual DOM primitives used by Zenith applications. It is designed to be lightweight, fast, and tree-shakeable.
10
16
 
@@ -1,5 +1,8 @@
1
1
  # RUNTIME_CONTRACT.md — Sealed Runtime Interface
2
2
 
3
+ Canonical public docs: `../zenith-docs/documentation/contracts/runtime-contract.md`
4
+
5
+
3
6
  > **This document is a legal boundary.**
4
7
  > The runtime is a consumer of bundler output.
5
8
  > It does not reinterpret, normalize, or extend.
package/dist/env.js ADDED
@@ -0,0 +1,22 @@
1
+ // ---------------------------------------------------------------------------
2
+ // env.js — Zenith Runtime canonical environment accessors
3
+ // ---------------------------------------------------------------------------
4
+ // SSR-safe access to window and document. Returns null when not in browser.
5
+ // Use zenWindow() / zenDocument() instead of direct window/document access.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /**
9
+ * SSR-safe window accessor.
10
+ * @returns {Window | null}
11
+ */
12
+ export function zenWindow() {
13
+ return typeof window === 'undefined' ? null : window;
14
+ }
15
+
16
+ /**
17
+ * SSR-safe document accessor.
18
+ * @returns {Document | null}
19
+ */
20
+ export function zenDocument() {
21
+ return typeof document === 'undefined' ? null : document;
22
+ }
package/dist/hydrate.js CHANGED
@@ -32,8 +32,6 @@ const BOOLEAN_ATTRIBUTES = new Set([
32
32
  const STRICT_MEMBER_CHAIN_LITERAL_RE = /^(?:true|false|null|undefined|[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*)$/;
33
33
  const UNSAFE_MEMBER_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
34
34
 
35
- const COMPILED_LITERAL_CACHE = new Map();
36
-
37
35
  /**
38
36
  * Hydrate a pre-rendered DOM tree using explicit payload tables.
39
37
  *
@@ -43,6 +41,7 @@ const COMPILED_LITERAL_CACHE = new Map();
43
41
  * expressions: Array<{ marker_index: number, signal_index?: number|null, state_index?: number|null, component_instance?: string|null, component_binding?: string|null, literal?: string|null }>,
44
42
  * markers: Array<{ index: number, kind: 'text' | 'attr' | 'event', selector: string, attr?: string }>,
45
43
  * events: Array<{ index: number, event: string, selector: string }>,
44
+ * refs?: Array<{ index: number, state_index: number, selector: string }>,
46
45
  * state_values: Array<*>,
47
46
  * state_keys?: Array<string>,
48
47
  * signals: Array<{ id: number, kind: 'signal', state_index: number }>,
@@ -55,7 +54,22 @@ export function hydrate(payload) {
55
54
  try {
56
55
  const normalized = _validatePayload(payload);
57
56
  _deepFreezePayload(payload);
58
- const { root, expressions, markers, events, stateValues, stateKeys, signals, components, route, params, ssrData, props } = normalized;
57
+ const {
58
+ root,
59
+ expressions,
60
+ markers,
61
+ events,
62
+ refs,
63
+ stateValues,
64
+ stateKeys,
65
+ signals,
66
+ components,
67
+ route,
68
+ params,
69
+ ssrData,
70
+ props,
71
+ exprFns
72
+ } = normalized;
59
73
 
60
74
  const componentBindings = Object.create(null);
61
75
 
@@ -72,6 +86,23 @@ export function hydrate(payload) {
72
86
  signalMap.set(i, candidate);
73
87
  }
74
88
 
89
+ const hydratedRefs = [];
90
+ for (let i = 0; i < refs.length; i++) {
91
+ const refBinding = refs[i];
92
+ const targetRef = stateValues[refBinding.state_index];
93
+ const nodes = _resolveNodes(root, refBinding.selector, refBinding.index, 'ref');
94
+ targetRef.current = nodes[0] || null;
95
+ hydratedRefs.push(targetRef);
96
+ }
97
+
98
+ if (hydratedRefs.length > 0) {
99
+ _registerDisposer(() => {
100
+ for (let i = 0; i < hydratedRefs.length; i++) {
101
+ hydratedRefs[i].current = null;
102
+ }
103
+ });
104
+ }
105
+
75
106
  for (let i = 0; i < components.length; i++) {
76
107
  const component = components[i];
77
108
  const resolvedProps = Object.freeze(_resolveComponentProps(component.props || [], signalMap, {
@@ -149,7 +180,8 @@ export function hydrate(payload) {
149
180
  params,
150
181
  ssrData,
151
182
  marker.kind,
152
- props
183
+ props,
184
+ exprFns
153
185
  );
154
186
  _applyMarkerValue(nodes, marker, value);
155
187
  }
@@ -176,7 +208,8 @@ export function hydrate(payload) {
176
208
  params,
177
209
  ssrData,
178
210
  marker.kind,
179
- props
211
+ props,
212
+ exprFns
180
213
  );
181
214
  _applyMarkerValue(nodes, marker, value);
182
215
  }
@@ -184,13 +217,17 @@ export function hydrate(payload) {
184
217
  const dependentMarkersBySignal = new Map();
185
218
  for (let i = 0; i < expressions.length; i++) {
186
219
  const expression = expressions[i];
187
- if (!Number.isInteger(expression.signal_index)) {
220
+ const signalIndices = _resolveExpressionSignalIndices(expression);
221
+ if (signalIndices.length === 0) {
188
222
  continue;
189
223
  }
190
- if (!dependentMarkersBySignal.has(expression.signal_index)) {
191
- dependentMarkersBySignal.set(expression.signal_index, []);
224
+ for (let j = 0; j < signalIndices.length; j++) {
225
+ const signalIndex = signalIndices[j];
226
+ if (!dependentMarkersBySignal.has(signalIndex)) {
227
+ dependentMarkersBySignal.set(signalIndex, []);
228
+ }
229
+ dependentMarkersBySignal.get(signalIndex).push(expression.marker_index);
192
230
  }
193
- dependentMarkersBySignal.get(expression.signal_index).push(expression.marker_index);
194
231
  }
195
232
 
196
233
  for (const [signalId, markerIndexes] of dependentMarkersBySignal.entries()) {
@@ -246,6 +283,7 @@ export function hydrate(payload) {
246
283
  }
247
284
 
248
285
  const eventIndices = new Set();
286
+ const escDispatchEntries = [];
249
287
  for (let i = 0; i < events.length; i++) {
250
288
  const eventBinding = events[i];
251
289
  if (eventIndices.has(eventBinding.index)) {
@@ -262,7 +300,9 @@ export function hydrate(payload) {
262
300
  componentBindings,
263
301
  params,
264
302
  ssrData,
265
- 'event'
303
+ 'event',
304
+ props || {},
305
+ exprFns
266
306
  );
267
307
  if (typeof handler !== 'function') {
268
308
  throwZenithRuntimeError({
@@ -291,11 +331,67 @@ export function hydrate(payload) {
291
331
  });
292
332
  }
293
333
  };
334
+ if (eventBinding.event === 'esc') {
335
+ escDispatchEntries.push({
336
+ node,
337
+ handler: wrappedHandler
338
+ });
339
+ continue;
340
+ }
294
341
  node.addEventListener(eventBinding.event, wrappedHandler);
295
342
  _registerListener(node, eventBinding.event, wrappedHandler);
296
343
  }
297
344
  }
298
345
 
346
+ if (escDispatchEntries.length > 0) {
347
+ const doc = root && root.ownerDocument ? root.ownerDocument : (typeof document !== 'undefined' ? document : null);
348
+ if (doc && typeof doc.addEventListener === 'function') {
349
+ const escDispatchListener = function zenithEscDispatch(event) {
350
+ if (!event || event.key !== 'Escape') {
351
+ return;
352
+ }
353
+
354
+ const activeElement = doc.activeElement || null;
355
+ let targetEntry = null;
356
+
357
+ if (activeElement && activeElement !== doc.body && activeElement !== doc.documentElement) {
358
+ for (let i = escDispatchEntries.length - 1; i >= 0; i--) {
359
+ const entry = escDispatchEntries[i];
360
+ if (!entry || !entry.node) {
361
+ continue;
362
+ }
363
+ if (!entry.node.isConnected) {
364
+ continue;
365
+ }
366
+ if (typeof entry.node.contains === 'function' && entry.node.contains(activeElement)) {
367
+ targetEntry = entry;
368
+ break;
369
+ }
370
+ }
371
+ }
372
+
373
+ if (!targetEntry && (activeElement === null || activeElement === doc.body || activeElement === doc.documentElement)) {
374
+ for (let i = escDispatchEntries.length - 1; i >= 0; i--) {
375
+ const entry = escDispatchEntries[i];
376
+ if (!entry || !entry.node || !entry.node.isConnected) {
377
+ continue;
378
+ }
379
+ targetEntry = entry;
380
+ break;
381
+ }
382
+ }
383
+
384
+ if (!targetEntry) {
385
+ return;
386
+ }
387
+
388
+ return targetEntry.handler.call(targetEntry.node, event);
389
+ };
390
+ doc.addEventListener('keydown', escDispatchListener);
391
+ _registerListener(doc, 'keydown', escDispatchListener);
392
+ }
393
+ }
394
+
299
395
  return cleanup;
300
396
  } catch (error) {
301
397
  rethrowZenithRuntimeError(error, {
@@ -336,6 +432,8 @@ function _validatePayload(payload) {
336
432
  throw new Error('[Zenith Runtime] hydrate(payload) requires events[]');
337
433
  }
338
434
 
435
+ const refs = Array.isArray(payload.refs) ? payload.refs : [];
436
+
339
437
  const stateValues = payload.state_values;
340
438
  if (!Array.isArray(stateValues)) {
341
439
  throw new Error('[Zenith Runtime] hydrate(payload) requires state_values[]');
@@ -365,6 +463,7 @@ function _validatePayload(payload) {
365
463
  const ssrData = payload.ssr_data && typeof payload.ssr_data === 'object'
366
464
  ? payload.ssr_data
367
465
  : {};
466
+ const exprFns = Array.isArray(payload.expr_fns) ? payload.expr_fns : [];
368
467
 
369
468
  if (markers.length !== expressions.length) {
370
469
  throw new Error(
@@ -385,6 +484,23 @@ function _validatePayload(payload) {
385
484
  `[Zenith Runtime] expression table out of order at position ${i}: marker_index=${expression.marker_index}`
386
485
  );
387
486
  }
487
+ if (expression.fn_index !== undefined && expression.fn_index !== null) {
488
+ if (!Number.isInteger(expression.fn_index) || expression.fn_index < 0) {
489
+ throw new Error(`[Zenith Runtime] expression at position ${i} has invalid fn_index`);
490
+ }
491
+ }
492
+ if (expression.signal_indices !== undefined) {
493
+ if (!Array.isArray(expression.signal_indices)) {
494
+ throw new Error(`[Zenith Runtime] expression at position ${i} must provide signal_indices[]`);
495
+ }
496
+ for (let j = 0; j < expression.signal_indices.length; j++) {
497
+ if (!Number.isInteger(expression.signal_indices[j]) || expression.signal_indices[j] < 0) {
498
+ throw new Error(
499
+ `[Zenith Runtime] expression at position ${i} has invalid signal_indices[${j}]`
500
+ );
501
+ }
502
+ }
503
+ }
388
504
  }
389
505
 
390
506
  for (let i = 0; i < markers.length; i++) {
@@ -425,6 +541,34 @@ function _validatePayload(payload) {
425
541
  }
426
542
  }
427
543
 
544
+ for (let i = 0; i < refs.length; i++) {
545
+ const refBinding = refs[i];
546
+ if (!refBinding || typeof refBinding !== 'object' || Array.isArray(refBinding)) {
547
+ throw new Error(`[Zenith Runtime] ref binding at position ${i} must be an object`);
548
+ }
549
+ if (!Number.isInteger(refBinding.index) || refBinding.index < 0) {
550
+ throw new Error(`[Zenith Runtime] ref binding at position ${i} requires non-negative index`);
551
+ }
552
+ if (
553
+ !Number.isInteger(refBinding.state_index) ||
554
+ refBinding.state_index < 0 ||
555
+ refBinding.state_index >= stateValues.length
556
+ ) {
557
+ throw new Error(
558
+ `[Zenith Runtime] ref binding at position ${i} has out-of-bounds state_index`
559
+ );
560
+ }
561
+ if (typeof refBinding.selector !== 'string' || refBinding.selector.length === 0) {
562
+ throw new Error(`[Zenith Runtime] ref binding at position ${i} requires selector`);
563
+ }
564
+ const candidate = stateValues[refBinding.state_index];
565
+ if (!candidate || typeof candidate !== 'object' || !Object.prototype.hasOwnProperty.call(candidate, 'current')) {
566
+ throw new Error(
567
+ `[Zenith Runtime] ref binding at position ${i} must resolve to a ref-like object`
568
+ );
569
+ }
570
+ }
571
+
428
572
  for (let i = 0; i < signals.length; i++) {
429
573
  const signalDescriptor = signals[i];
430
574
  if (!signalDescriptor || typeof signalDescriptor !== 'object' || Array.isArray(signalDescriptor)) {
@@ -478,6 +622,7 @@ function _validatePayload(payload) {
478
622
  for (let i = 0; i < expressions.length; i++) Object.freeze(expressions[i]);
479
623
  for (let i = 0; i < markers.length; i++) Object.freeze(markers[i]);
480
624
  for (let i = 0; i < events.length; i++) Object.freeze(events[i]);
625
+ for (let i = 0; i < refs.length; i++) Object.freeze(refs[i]);
481
626
  for (let i = 0; i < signals.length; i++) Object.freeze(signals[i]);
482
627
  for (let i = 0; i < components.length; i++) {
483
628
  const c = components[i];
@@ -501,6 +646,7 @@ function _validatePayload(payload) {
501
646
  Object.freeze(expressions);
502
647
  Object.freeze(markers);
503
648
  Object.freeze(events);
649
+ Object.freeze(refs);
504
650
  Object.freeze(signals);
505
651
  Object.freeze(components);
506
652
 
@@ -509,6 +655,7 @@ function _validatePayload(payload) {
509
655
  expressions,
510
656
  markers,
511
657
  events,
658
+ refs,
512
659
  stateValues,
513
660
  stateKeys,
514
661
  signals,
@@ -516,7 +663,8 @@ function _validatePayload(payload) {
516
663
  route,
517
664
  params: Object.freeze(params),
518
665
  ssrData: Object.freeze(ssrData),
519
- props: Object.freeze(props)
666
+ props: Object.freeze(props),
667
+ exprFns: Object.freeze(exprFns)
520
668
  };
521
669
 
522
670
  return Object.freeze(validatedPayload);
@@ -580,7 +728,34 @@ function _resolveNodes(root, selector, index, kind) {
580
728
  return nodes;
581
729
  }
582
730
 
583
- function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props) {
731
+ function _resolveExpressionSignalIndices(binding) {
732
+ if (!binding || typeof binding !== 'object') {
733
+ return [];
734
+ }
735
+ if (Array.isArray(binding.signal_indices) && binding.signal_indices.length > 0) {
736
+ return [...new Set(binding.signal_indices.filter((value) => Number.isInteger(value) && value >= 0))];
737
+ }
738
+ if (Number.isInteger(binding.signal_index) && binding.signal_index >= 0) {
739
+ return [binding.signal_index];
740
+ }
741
+ return [];
742
+ }
743
+
744
+ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, componentBindings, params, ssrData, mode, props, exprFns) {
745
+ if (binding.fn_index != null && binding.fn_index !== undefined) {
746
+ const fns = Array.isArray(exprFns) ? exprFns : [];
747
+ const fn = fns[binding.fn_index];
748
+ if (typeof fn === 'function') {
749
+ return fn({
750
+ signalMap,
751
+ params,
752
+ ssrData,
753
+ props: props || {},
754
+ componentBindings,
755
+ zenhtml: _zenhtml
756
+ });
757
+ }
758
+ }
584
759
  if (binding.signal_index !== null && binding.signal_index !== undefined) {
585
760
  const signalValue = signalMap.get(binding.signal_index);
586
761
  if (!signalValue || typeof signalValue.get !== 'function') {
@@ -651,17 +826,9 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
651
826
  return props || {};
652
827
  }
653
828
 
654
- const evaluated = _evaluateLiteralExpression(
655
- trimmedLiteral,
656
- stateValues,
657
- stateKeys,
658
- params,
659
- ssrData,
660
- mode,
661
- props
662
- );
663
- if (evaluated !== UNRESOLVED_LITERAL) {
664
- return evaluated;
829
+ const primitiveValue = _resolvePrimitiveLiteral(trimmedLiteral);
830
+ if (primitiveValue !== UNRESOLVED_LITERAL) {
831
+ return primitiveValue;
665
832
  }
666
833
  if (_isLikelyExpressionLiteral(trimmedLiteral)) {
667
834
  throwZenithRuntimeError({
@@ -784,33 +951,39 @@ function _resolveStrictMemberChainLiteral(
784
951
  return cursor;
785
952
  }
786
953
 
787
- function _evaluateLiteralExpression(expression, stateValues, stateKeys, params, ssrData, mode, props) {
788
- const scope = _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props);
789
- const scopeKeys = Object.keys(scope);
790
- const scopeValues = scopeKeys.map((key) => scope[key]);
791
- const rewritten = _rewriteMarkupLiterals(expression);
792
- const cacheKey = `${scopeKeys.join('|')}::${rewritten}`;
954
+ function _resolvePrimitiveLiteral(literal) {
955
+ if (typeof literal !== 'string') {
956
+ return UNRESOLVED_LITERAL;
957
+ }
958
+ if (literal === 'true') return true;
959
+ if (literal === 'false') return false;
960
+ if (literal === 'null') return null;
961
+ if (literal === 'undefined') return undefined;
962
+
963
+ if (/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(literal)) {
964
+ return Number(literal);
965
+ }
793
966
 
794
- let evaluator = COMPILED_LITERAL_CACHE.get(cacheKey);
795
- if (!evaluator) {
967
+ if (literal.length >= 2 && literal.startsWith('"') && literal.endsWith('"')) {
796
968
  try {
797
- evaluator = Function(
798
- ...scopeKeys,
799
- `return (${rewritten});`
800
- );
801
- } catch (err) {
802
- if (isZenithRuntimeError(err)) throw err;
969
+ return JSON.parse(literal);
970
+ } catch {
803
971
  return UNRESOLVED_LITERAL;
804
972
  }
805
- COMPILED_LITERAL_CACHE.set(cacheKey, evaluator);
806
973
  }
807
974
 
808
- try {
809
- return evaluator(...scopeValues);
810
- } catch (err) {
811
- if (isZenithRuntimeError(err)) throw err;
812
- return UNRESOLVED_LITERAL;
975
+ if (literal.length >= 2 && literal.startsWith('\'') && literal.endsWith('\'')) {
976
+ return literal
977
+ .slice(1, -1)
978
+ .replace(/\\\\/g, '\\')
979
+ .replace(/\\'/g, '\'');
980
+ }
981
+
982
+ if (literal.length >= 2 && literal.startsWith('`') && literal.endsWith('`')) {
983
+ return literal.slice(1, -1);
813
984
  }
985
+
986
+ return UNRESOLVED_LITERAL;
814
987
  }
815
988
 
816
989
  function _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props) {
package/dist/index.js CHANGED
@@ -2,3 +2,5 @@ export { signal } from './signal.js';
2
2
  export { state } from './state.js';
3
3
  export { zeneffect } from './zeneffect.js';
4
4
  export { hydrate } from './hydrate.js';
5
+ export { zenWindow, zenDocument } from './env.js';
6
+ export { zenOn, zenResize, collectRefs } from './platform.js';
@@ -0,0 +1,102 @@
1
+ // ---------------------------------------------------------------------------
2
+ // platform.js — Zenith Runtime canonical DOM/platform helpers
3
+ // ---------------------------------------------------------------------------
4
+ // zenOn: SSR-safe event subscription with disposer
5
+ // zenResize: window resize handler with rAF throttle
6
+ // collectRefs: deterministic null-filtered ref collection
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { zenWindow } from './env.js';
10
+
11
+ /**
12
+ * SSR-safe event subscription. Returns disposer.
13
+ * @param {EventTarget | null} target
14
+ * @param {string} eventName
15
+ * @param {EventListener} handler
16
+ * @param {AddEventListenerOptions | boolean} [options]
17
+ * @returns {() => void}
18
+ */
19
+ export function zenOn(target, eventName, handler, options) {
20
+ if (!target || typeof target.addEventListener !== 'function') {
21
+ return () => {};
22
+ }
23
+ target.addEventListener(eventName, handler, options);
24
+ return () => {
25
+ target.removeEventListener(eventName, handler, options);
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Window resize handler with requestAnimationFrame throttle.
31
+ * Returns disposer.
32
+ * @param {(size: { w: number; h: number }) => void} handler
33
+ * @returns {() => void}
34
+ */
35
+ export function zenResize(handler) {
36
+ const win = zenWindow();
37
+ if (!win || typeof win.addEventListener !== 'function') {
38
+ return () => {};
39
+ }
40
+ const hasRaf =
41
+ typeof win.requestAnimationFrame === 'function'
42
+ && typeof win.cancelAnimationFrame === 'function';
43
+ let scheduledId = null;
44
+ let lastW = Number.NaN;
45
+ let lastH = Number.NaN;
46
+
47
+ const schedule = (callback) => {
48
+ if (hasRaf) {
49
+ return win.requestAnimationFrame(callback);
50
+ }
51
+ return win.setTimeout(callback, 0);
52
+ };
53
+
54
+ const cancel = (id) => {
55
+ if (hasRaf) {
56
+ win.cancelAnimationFrame(id);
57
+ return;
58
+ }
59
+ win.clearTimeout(id);
60
+ };
61
+
62
+ function onResize() {
63
+ if (scheduledId !== null) return;
64
+ scheduledId = schedule(() => {
65
+ scheduledId = null;
66
+ const w = win.innerWidth;
67
+ const h = win.innerHeight;
68
+ if (w !== lastW || h !== lastH) {
69
+ lastW = w;
70
+ lastH = h;
71
+ handler({ w, h });
72
+ }
73
+ });
74
+ }
75
+
76
+ win.addEventListener('resize', onResize);
77
+ onResize();
78
+
79
+ return () => {
80
+ if (scheduledId !== null) {
81
+ cancel(scheduledId);
82
+ scheduledId = null;
83
+ }
84
+ win.removeEventListener('resize', onResize);
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Deterministic null-filtered collection of ref.current values.
90
+ * @param {...{ current?: Element | null }} refs
91
+ * @returns {Element[]}
92
+ */
93
+ export function collectRefs(...refs) {
94
+ const out = [];
95
+ for (let i = 0; i < refs.length; i++) {
96
+ const node = refs[i] && refs[i].current;
97
+ if (node && typeof node === 'object' && typeof node.nodeType === 'number') {
98
+ out.push(node);
99
+ }
100
+ }
101
+ return out;
102
+ }
package/dist/template.js CHANGED
@@ -22,6 +22,8 @@ function buildRuntimeModuleSource() {
22
22
  const segments = [
23
23
  stripImports(readRuntimeSourceFile('zeneffect.js')),
24
24
  stripImports(readRuntimeSourceFile('ref.js')),
25
+ stripImports(readRuntimeSourceFile('env.js')),
26
+ stripImports(readRuntimeSourceFile('platform.js')),
25
27
  stripImports(readRuntimeSourceFile('signal.js')),
26
28
  stripImports(readRuntimeSourceFile('state.js')),
27
29
  stripImports(readRuntimeSourceFile('diagnostics.js')),
@@ -119,18 +121,101 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
119
121
  });
120
122
  }
121
123
 
122
- function swapStylesheet(nextHref) {
124
+ let cssSwapEpoch = 0;
125
+
126
+ function withCacheBuster(nextHref) {
127
+ const separator = nextHref.includes('?') ? '&' : '?';
128
+ return nextHref + separator + '__zenith_dev=' + Date.now();
129
+ }
130
+
131
+ function isSameOriginStylesheet(href) {
132
+ if (typeof href !== 'string' || href.length === 0) {
133
+ return false;
134
+ }
135
+ if (href.startsWith('http://') || href.startsWith('https://')) {
136
+ try {
137
+ return new URL(href, window.location.href).origin === window.location.origin;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+ return true;
143
+ }
144
+
145
+ function findPrimaryStylesheet() {
146
+ const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
147
+ .filter(function (link) {
148
+ return isSameOriginStylesheet(link.getAttribute('href') || '');
149
+ });
150
+ if (links.length === 0) {
151
+ return null;
152
+ }
153
+ const marked = links.find(function (link) {
154
+ return link.getAttribute('data-zenith-dev-primary') === 'true';
155
+ });
156
+ if (marked) {
157
+ return marked;
158
+ }
159
+ links[0].setAttribute('data-zenith-dev-primary', 'true');
160
+ return links[0];
161
+ }
162
+
163
+ function scheduleCssRetry(previousHref, attempt) {
164
+ if (attempt >= 3) {
165
+ window.location.reload();
166
+ return;
167
+ }
168
+ const delayMs = (attempt + 1) * 100;
169
+ setTimeout(function () {
170
+ fetchDevState().then(function (statePayload) {
171
+ const href = statePayload && typeof statePayload.cssHref === 'string' && statePayload.cssHref.length > 0
172
+ ? statePayload.cssHref
173
+ : previousHref;
174
+ swapStylesheet(href, attempt + 1);
175
+ });
176
+ }, delayMs);
177
+ }
178
+
179
+ function swapStylesheet(nextHref, attempt) {
180
+ const tries = Number.isInteger(attempt) ? attempt : 0;
123
181
  if (typeof nextHref !== 'string' || nextHref.length === 0) {
124
182
  window.location.reload();
125
183
  return;
126
184
  }
127
- const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
128
- if (links.length === 0) {
185
+ const activeLink = findPrimaryStylesheet();
186
+ if (!activeLink) {
129
187
  window.location.reload();
130
188
  return;
131
189
  }
132
- const separator = nextHref.includes('?') ? '&' : '?';
133
- links[0].setAttribute('href', nextHref + separator + '__zenith_dev=' + Date.now());
190
+
191
+ const swapId = ++cssSwapEpoch;
192
+ const nextLink = activeLink.cloneNode(true);
193
+ nextLink.setAttribute('href', withCacheBuster(nextHref));
194
+ nextLink.removeAttribute('data-zenith-dev-primary');
195
+ nextLink.setAttribute('data-zenith-dev-pending', 'true');
196
+ activeLink.removeAttribute('data-zenith-dev-primary');
197
+
198
+ nextLink.addEventListener('load', function () {
199
+ if (swapId !== cssSwapEpoch) {
200
+ try { nextLink.remove(); } catch { }
201
+ return;
202
+ }
203
+ nextLink.removeAttribute('data-zenith-dev-pending');
204
+ nextLink.setAttribute('data-zenith-dev-primary', 'true');
205
+ try { activeLink.remove(); } catch { }
206
+ }, { once: true });
207
+
208
+ nextLink.addEventListener('error', function () {
209
+ if (swapId !== cssSwapEpoch) {
210
+ try { nextLink.remove(); } catch { }
211
+ return;
212
+ }
213
+ try { nextLink.remove(); } catch { }
214
+ activeLink.setAttribute('data-zenith-dev-primary', 'true');
215
+ scheduleCssRetry(nextHref, tries);
216
+ }, { once: true });
217
+
218
+ activeLink.insertAdjacentElement('afterend', nextLink);
134
219
  }
135
220
 
136
221
  const state = ensureDevState();
@@ -341,8 +426,10 @@ const RUNTIME_DEV_CLIENT_SOURCE = `(() => {
341
426
  appendLog('[css_update] ' + (payload.href || ''));
342
427
  emitDebug('css_update', payload);
343
428
  fetchDevState().then(function (statePayload) {
344
- if (statePayload && typeof statePayload.cssHref === 'string' && statePayload.cssHref.length > 0) {
429
+ if (statePayload) {
345
430
  updateInfo({ ...payload, ...statePayload });
431
+ }
432
+ if (statePayload && typeof statePayload.cssHref === 'string' && statePayload.cssHref.length > 0) {
346
433
  swapStylesheet(statePayload.cssHref);
347
434
  return;
348
435
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/runtime",
3
- "version": "0.5.0-beta.2.6",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {