@zenithbuild/runtime 0.5.0-beta.2.5 → 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.
- package/HYDRATION_CONTRACT.md +17 -0
- package/README.md +6 -0
- package/RUNTIME_CONTRACT.md +3 -0
- package/dist/env.js +22 -0
- package/dist/hydrate.js +266 -48
- package/dist/index.js +2 -0
- package/dist/platform.js +102 -0
- package/dist/template.js +93 -6
- package/package.json +3 -2
package/HYDRATION_CONTRACT.md
CHANGED
|
@@ -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.
|
|
@@ -96,3 +99,17 @@ Forbidden in runtime/bundler output:
|
|
|
96
99
|
- `process.env`
|
|
97
100
|
|
|
98
101
|
Compile-time guarantees override runtime flexibility.
|
|
102
|
+
|
|
103
|
+
## 8. Freeze Boundary Contract
|
|
104
|
+
|
|
105
|
+
Runtime payload freezing is allowed only for deterministic internal tables and
|
|
106
|
+
plain JSON-like containers controlled by runtime (`Object` / `Array`).
|
|
107
|
+
|
|
108
|
+
Runtime MUST NOT freeze:
|
|
109
|
+
- `ref()` objects (objects with writable `.current`)
|
|
110
|
+
- function values (handlers/callbacks)
|
|
111
|
+
- host/platform objects (`Node`, `EventTarget`, `URL`, `Request`, `Response`, etc.)
|
|
112
|
+
|
|
113
|
+
Rationale:
|
|
114
|
+
- hydration and `zenMount` must be able to assign `ref.current` without throwing
|
|
115
|
+
- host objects preserve platform mutability semantics
|
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
|
|
package/RUNTIME_CONTRACT.md
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
220
|
+
const signalIndices = _resolveExpressionSignalIndices(expression);
|
|
221
|
+
if (signalIndices.length === 0) {
|
|
188
222
|
continue;
|
|
189
223
|
}
|
|
190
|
-
|
|
191
|
-
|
|
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,13 +622,18 @@ 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];
|
|
484
629
|
if (Array.isArray(c.props)) {
|
|
485
630
|
for (let j = 0; j < c.props.length; j++) {
|
|
486
631
|
const propDesc = c.props[j];
|
|
487
|
-
if (
|
|
632
|
+
if (
|
|
633
|
+
propDesc &&
|
|
634
|
+
typeof propDesc === 'object' &&
|
|
635
|
+
_isHydrationFreezableContainer(propDesc.value)
|
|
636
|
+
) {
|
|
488
637
|
Object.freeze(propDesc.value);
|
|
489
638
|
}
|
|
490
639
|
Object.freeze(propDesc);
|
|
@@ -497,6 +646,7 @@ function _validatePayload(payload) {
|
|
|
497
646
|
Object.freeze(expressions);
|
|
498
647
|
Object.freeze(markers);
|
|
499
648
|
Object.freeze(events);
|
|
649
|
+
Object.freeze(refs);
|
|
500
650
|
Object.freeze(signals);
|
|
501
651
|
Object.freeze(components);
|
|
502
652
|
|
|
@@ -505,6 +655,7 @@ function _validatePayload(payload) {
|
|
|
505
655
|
expressions,
|
|
506
656
|
markers,
|
|
507
657
|
events,
|
|
658
|
+
refs,
|
|
508
659
|
stateValues,
|
|
509
660
|
stateKeys,
|
|
510
661
|
signals,
|
|
@@ -512,7 +663,8 @@ function _validatePayload(payload) {
|
|
|
512
663
|
route,
|
|
513
664
|
params: Object.freeze(params),
|
|
514
665
|
ssrData: Object.freeze(ssrData),
|
|
515
|
-
props: Object.freeze(props)
|
|
666
|
+
props: Object.freeze(props),
|
|
667
|
+
exprFns: Object.freeze(exprFns)
|
|
516
668
|
};
|
|
517
669
|
|
|
518
670
|
return Object.freeze(validatedPayload);
|
|
@@ -576,7 +728,34 @@ function _resolveNodes(root, selector, index, kind) {
|
|
|
576
728
|
return nodes;
|
|
577
729
|
}
|
|
578
730
|
|
|
579
|
-
function
|
|
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
|
+
}
|
|
580
759
|
if (binding.signal_index !== null && binding.signal_index !== undefined) {
|
|
581
760
|
const signalValue = signalMap.get(binding.signal_index);
|
|
582
761
|
if (!signalValue || typeof signalValue.get !== 'function') {
|
|
@@ -647,17 +826,9 @@ function _evaluateExpression(binding, stateValues, stateKeys, signalMap, compone
|
|
|
647
826
|
return props || {};
|
|
648
827
|
}
|
|
649
828
|
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
stateKeys,
|
|
654
|
-
params,
|
|
655
|
-
ssrData,
|
|
656
|
-
mode,
|
|
657
|
-
props
|
|
658
|
-
);
|
|
659
|
-
if (evaluated !== UNRESOLVED_LITERAL) {
|
|
660
|
-
return evaluated;
|
|
829
|
+
const primitiveValue = _resolvePrimitiveLiteral(trimmedLiteral);
|
|
830
|
+
if (primitiveValue !== UNRESOLVED_LITERAL) {
|
|
831
|
+
return primitiveValue;
|
|
661
832
|
}
|
|
662
833
|
if (_isLikelyExpressionLiteral(trimmedLiteral)) {
|
|
663
834
|
throwZenithRuntimeError({
|
|
@@ -780,33 +951,39 @@ function _resolveStrictMemberChainLiteral(
|
|
|
780
951
|
return cursor;
|
|
781
952
|
}
|
|
782
953
|
|
|
783
|
-
function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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;
|
|
789
962
|
|
|
790
|
-
|
|
791
|
-
|
|
963
|
+
if (/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(literal)) {
|
|
964
|
+
return Number(literal);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (literal.length >= 2 && literal.startsWith('"') && literal.endsWith('"')) {
|
|
792
968
|
try {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
`return (${rewritten});`
|
|
796
|
-
);
|
|
797
|
-
} catch (err) {
|
|
798
|
-
if (isZenithRuntimeError(err)) throw err;
|
|
969
|
+
return JSON.parse(literal);
|
|
970
|
+
} catch {
|
|
799
971
|
return UNRESOLVED_LITERAL;
|
|
800
972
|
}
|
|
801
|
-
COMPILED_LITERAL_CACHE.set(cacheKey, evaluator);
|
|
802
973
|
}
|
|
803
974
|
|
|
804
|
-
|
|
805
|
-
return
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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);
|
|
809
984
|
}
|
|
985
|
+
|
|
986
|
+
return UNRESOLVED_LITERAL;
|
|
810
987
|
}
|
|
811
988
|
|
|
812
989
|
function _buildLiteralScope(stateValues, stateKeys, params, ssrData, mode, props) {
|
|
@@ -1580,10 +1757,7 @@ function _applyAttribute(node, attrName, value) {
|
|
|
1580
1757
|
}
|
|
1581
1758
|
|
|
1582
1759
|
function _deepFreezePayload(obj) {
|
|
1583
|
-
if (!obj ||
|
|
1584
|
-
// Skip DOM nodes, signals (objects with get/subscribe), and functions
|
|
1585
|
-
if (typeof obj.nodeType === 'number') return;
|
|
1586
|
-
if (typeof obj.get === 'function' && typeof obj.subscribe === 'function') return;
|
|
1760
|
+
if (!_isHydrationFreezableContainer(obj) || Object.isFrozen(obj)) return;
|
|
1587
1761
|
|
|
1588
1762
|
Object.freeze(obj);
|
|
1589
1763
|
const keys = Object.keys(obj);
|
|
@@ -1594,3 +1768,47 @@ function _deepFreezePayload(obj) {
|
|
|
1594
1768
|
}
|
|
1595
1769
|
}
|
|
1596
1770
|
}
|
|
1771
|
+
|
|
1772
|
+
function _isHydrationRefObject(obj) {
|
|
1773
|
+
if (!obj || typeof obj !== 'object') {
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
if (obj.__zenith_ref === true) {
|
|
1777
|
+
return true;
|
|
1778
|
+
}
|
|
1779
|
+
if (!Object.prototype.hasOwnProperty.call(obj, 'current')) {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
if (typeof obj.get === 'function' && typeof obj.subscribe === 'function') {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
const keys = Object.keys(obj);
|
|
1786
|
+
if (keys.length === 1 && keys[0] === 'current') {
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1789
|
+
if (keys.length === 2 && keys.includes('current') && keys.includes('__zenith_ref')) {
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function _isPlainObject(value) {
|
|
1796
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
const proto = Object.getPrototypeOf(value);
|
|
1800
|
+
return proto === Object.prototype || proto === null;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function _isHydrationFreezableContainer(value) {
|
|
1804
|
+
if (Array.isArray(value)) return true;
|
|
1805
|
+
if (!_isPlainObject(value)) return false;
|
|
1806
|
+
|
|
1807
|
+
if (_isHydrationRefObject(value)) {
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
if (typeof value.get === 'function' && typeof value.subscribe === 'function') {
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
return true;
|
|
1814
|
+
}
|
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';
|
package/dist/platform.js
ADDED
|
@@ -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
|
-
|
|
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
|
|
128
|
-
if (
|
|
185
|
+
const activeLink = findPrimaryStylesheet();
|
|
186
|
+
if (!activeLink) {
|
|
129
187
|
window.location.reload();
|
|
130
188
|
return;
|
|
131
189
|
}
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./dist/index.js"
|
|
7
|
+
".": "./dist/index.js",
|
|
8
|
+
"./template": "./dist/template.js"
|
|
8
9
|
},
|
|
9
10
|
"publishConfig": {
|
|
10
11
|
"access": "public"
|