@zenithbuild/runtime 0.2.0 → 0.5.0-beta.2.3
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 +98 -0
- package/README.md +3 -0
- package/RUNTIME_CONTRACT.md +183 -0
- package/dist/cleanup.js +88 -0
- package/dist/diagnostics.js +309 -0
- package/dist/effect.js +3 -0
- package/dist/events.js +59 -0
- package/dist/hydrate.js +1596 -0
- package/dist/index.js +4 -0
- package/dist/ref.js +23 -0
- package/dist/runtime.js +57 -0
- package/dist/signal.js +58 -0
- package/dist/state.js +79 -0
- package/dist/template.js +360 -0
- package/dist/zeneffect.js +640 -0
- package/package.json +19 -6
- package/src/dom-hydration.ts +0 -297
- package/src/index.ts +0 -488
- package/src/thin-runtime.ts +0 -159
- package/tsconfig.json +0 -28
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Zenith Runtime V0 Hydration Contract
|
|
2
|
+
|
|
3
|
+
Status: FROZEN (V0)
|
|
4
|
+
|
|
5
|
+
This contract locks hydration and reactivity boundaries for Zenith V0.
|
|
6
|
+
|
|
7
|
+
## 1. No Runtime Discovery
|
|
8
|
+
|
|
9
|
+
Runtime must not discover bindings by scanning unknown DOM structure.
|
|
10
|
+
|
|
11
|
+
Allowed:
|
|
12
|
+
- Resolve selectors from bundler-provided marker/event tables.
|
|
13
|
+
|
|
14
|
+
Forbidden:
|
|
15
|
+
- Global `querySelectorAll('*')` discovery passes.
|
|
16
|
+
- Runtime inference of binding intent from HTML shape.
|
|
17
|
+
|
|
18
|
+
## 2. No Implicit Reactivity
|
|
19
|
+
|
|
20
|
+
Runtime primitives are explicit:
|
|
21
|
+
- `signal(initial)`
|
|
22
|
+
- `state(initialObject)`
|
|
23
|
+
- `zeneffect(dependencies, fn)`
|
|
24
|
+
|
|
25
|
+
Forbidden:
|
|
26
|
+
- Proxy-based tracking.
|
|
27
|
+
- Implicit dependency capture contexts.
|
|
28
|
+
- Scheduler/queue abstractions.
|
|
29
|
+
|
|
30
|
+
## 3. Deterministic Ordering
|
|
31
|
+
|
|
32
|
+
Compiler expression ordering is canonical.
|
|
33
|
+
Bundler and runtime must preserve index semantics exactly.
|
|
34
|
+
|
|
35
|
+
Required:
|
|
36
|
+
- Marker indices are sequential `0..N-1`.
|
|
37
|
+
- Marker count equals expression table length.
|
|
38
|
+
- Runtime never remaps indices.
|
|
39
|
+
|
|
40
|
+
## 4. Explicit Bootstrap
|
|
41
|
+
|
|
42
|
+
Hydration is explicit and called by bundler bootstrap only:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
hydrate({
|
|
46
|
+
ir_version: 1,
|
|
47
|
+
root: document,
|
|
48
|
+
expressions: __zenith_expr,
|
|
49
|
+
markers: __zenith_markers,
|
|
50
|
+
events: __zenith_events,
|
|
51
|
+
state_values: __zenith_state_values,
|
|
52
|
+
signals: __zenith_signals,
|
|
53
|
+
components: __zenith_components
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Runtime must export functions only. Runtime must not auto-run.
|
|
58
|
+
|
|
59
|
+
## 5. Component Factory Payload
|
|
60
|
+
|
|
61
|
+
Component scripts are compile-time hoisted and emitted as factory modules.
|
|
62
|
+
Runtime receives only explicit component references in `payload.components`.
|
|
63
|
+
|
|
64
|
+
Required:
|
|
65
|
+
- Component host selectors are provided by bundler.
|
|
66
|
+
- Runtime instantiates factories only from payload.
|
|
67
|
+
- Runtime merges returned `bindings` into hydration state by instance key.
|
|
68
|
+
|
|
69
|
+
Forbidden:
|
|
70
|
+
- Runtime component discovery by scanning unknown DOM shape.
|
|
71
|
+
- Runtime-generated component wrappers or lifecycle registries.
|
|
72
|
+
|
|
73
|
+
## 6. Hard Fail Semantics
|
|
74
|
+
|
|
75
|
+
Runtime must throw for contract drift:
|
|
76
|
+
- Missing or unsupported `ir_version`.
|
|
77
|
+
- Duplicate marker indices.
|
|
78
|
+
- Missing index in sequence.
|
|
79
|
+
- Out-of-order expression table entries.
|
|
80
|
+
- Out-of-order marker table entries.
|
|
81
|
+
- Marker index out of bounds.
|
|
82
|
+
- Marker/expression count mismatch.
|
|
83
|
+
- Event index out of bounds.
|
|
84
|
+
|
|
85
|
+
No fallback behavior is allowed for broken payload contracts.
|
|
86
|
+
|
|
87
|
+
## 7. Determinism and Purity
|
|
88
|
+
|
|
89
|
+
Forbidden in runtime/bundler output:
|
|
90
|
+
- `eval(`
|
|
91
|
+
- `new Function(`
|
|
92
|
+
- `require(`
|
|
93
|
+
- `Date(`
|
|
94
|
+
- `Math.random(`
|
|
95
|
+
- `crypto.randomUUID(`
|
|
96
|
+
- `process.env`
|
|
97
|
+
|
|
98
|
+
Compile-time guarantees override runtime flexibility.
|
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @zenith/runtime
|
|
2
2
|
|
|
3
|
+
> **⚠️ Internal API:** This package is an internal implementation detail of the Zenith framework. It is not intended for public use and its API may break without warning. Please use `@zenithbuild/core` instead.
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
The core runtime library for the Zenith framework.
|
|
4
7
|
|
|
5
8
|
## Overview
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# RUNTIME_CONTRACT.md — Sealed Runtime Interface
|
|
2
|
+
|
|
3
|
+
> **This document is a legal boundary.**
|
|
4
|
+
> The runtime is a consumer of bundler output.
|
|
5
|
+
> It does not reinterpret, normalize, or extend.
|
|
6
|
+
|
|
7
|
+
## Status: FROZEN (V0)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Input Surface
|
|
12
|
+
|
|
13
|
+
The runtime receives exactly one module per page:
|
|
14
|
+
|
|
15
|
+
| Export | Type | Contract |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `__zenith_html` | `string` | Template literal, immutable |
|
|
18
|
+
| `__zenith_expr` | `(() => any)[]` | Pre-bound expression functions, immutable, ordered |
|
|
19
|
+
| `__zenith_page()` | `function` | Returns `{ html, expressions }` |
|
|
20
|
+
|
|
21
|
+
### Expression Functions
|
|
22
|
+
|
|
23
|
+
Each entry in `__zenith_expr` is a **pre-bound zero-argument function** emitted by the bundler. The runtime never resolves expression strings, never performs key lookups, and never interprets JavaScript.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// Bundler emits:
|
|
27
|
+
export const __zenith_expr = [
|
|
28
|
+
() => count(),
|
|
29
|
+
() => count() + 1,
|
|
30
|
+
() => increment()
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export default function __zenith_page() {
|
|
34
|
+
return {
|
|
35
|
+
html: __zenith_html,
|
|
36
|
+
expressions: __zenith_expr
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The runtime simply executes: `expressions[index]()`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Runtime Responsibilities (Allowed)
|
|
46
|
+
|
|
47
|
+
| Action | Method |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Insert HTML into container | `container.innerHTML = html` |
|
|
50
|
+
| Locate expression bindings | `querySelectorAll('[data-zx-e]')` |
|
|
51
|
+
| Locate event bindings | `querySelectorAll('[data-zx-on-*]')` |
|
|
52
|
+
| Bind expressions to DOM | `effect(() => node.textContent = expressions[index]())` |
|
|
53
|
+
| Bind event listeners | `node.addEventListener(event, expressions[index])` |
|
|
54
|
+
| Update DOM on signal change | Via effect re-execution |
|
|
55
|
+
| Clean up on unmount | Remove effects + listeners |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3. Runtime Prohibitions (Forbidden)
|
|
60
|
+
|
|
61
|
+
The runtime **must never**:
|
|
62
|
+
|
|
63
|
+
- Parse JavaScript expressions
|
|
64
|
+
- Resolve expression strings against a scope object
|
|
65
|
+
- Normalize expression strings
|
|
66
|
+
- Modify expression content
|
|
67
|
+
- Introduce component abstractions
|
|
68
|
+
- Perform virtual DOM diffing
|
|
69
|
+
- Re-render full subtrees
|
|
70
|
+
- Implement lifecycle hooks beyond mount/unmount
|
|
71
|
+
- Access or mutate `window` globals
|
|
72
|
+
- Reorder binding indices
|
|
73
|
+
- Interpret import semantics
|
|
74
|
+
- Add framework-level abstractions (routing, stores, context)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 4. Data Attribute Contract
|
|
79
|
+
|
|
80
|
+
Inherited from `BUNDLER_CONTRACT.md`:
|
|
81
|
+
|
|
82
|
+
| Attribute | Format | Runtime Action |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `data-zx-e` | `"<index>"` or `"<i0> <i1> ..."` | Execute `expressions[index]()`, bind result to node |
|
|
85
|
+
| `data-zx-on-<event>` | `"<index>"` | Call `addEventListener(event, expressions[index])` |
|
|
86
|
+
|
|
87
|
+
Index values are 0-based integers matching `__zenith_expr` array positions.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 5. Reactivity Model
|
|
92
|
+
|
|
93
|
+
### Signal Primitive
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const count = signal(0); // Create
|
|
97
|
+
count(); // Read (tracks dependency)
|
|
98
|
+
count.set(1); // Write (notifies subscribers)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Internals:
|
|
102
|
+
- Each signal maintains a `Set<Effect>` of subscribers
|
|
103
|
+
- Reading a signal during effect execution registers the effect as a subscriber
|
|
104
|
+
- Writing a signal notifies all subscribers synchronously
|
|
105
|
+
|
|
106
|
+
### Effect Primitive
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
effect(() => {
|
|
110
|
+
node.textContent = count();
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Internals:
|
|
115
|
+
- On execution, sets itself as the "current tracking context"
|
|
116
|
+
- Any signal read during execution adds this effect to its subscriber set
|
|
117
|
+
- When a dependency signal changes, the effect re-runs
|
|
118
|
+
|
|
119
|
+
### Constraints
|
|
120
|
+
|
|
121
|
+
- No batching
|
|
122
|
+
- No scheduler / microtask queue
|
|
123
|
+
- No async effects
|
|
124
|
+
- No suspense / lazy loading
|
|
125
|
+
- No lifecycle hooks beyond `cleanup()`
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 6. Hydration Algorithm
|
|
130
|
+
|
|
131
|
+
Single-pass, deterministic:
|
|
132
|
+
|
|
133
|
+
1. Call `__zenith_page()` → receive `{ html, expressions }`
|
|
134
|
+
2. Set `container.innerHTML = html`
|
|
135
|
+
3. Walk DOM once: `querySelectorAll('[data-zx-e], [data-zx-on-click], ...')`
|
|
136
|
+
4. For each node with `data-zx-e`:
|
|
137
|
+
- Parse space-separated indices
|
|
138
|
+
- For each index: create `effect(() => node.textContent = expressions[index]())`
|
|
139
|
+
5. For each node with `data-zx-on-*`:
|
|
140
|
+
- Extract event name from attribute suffix
|
|
141
|
+
- `node.addEventListener(eventName, expressions[index])`
|
|
142
|
+
6. Store all effects and listeners in a cleanup registry
|
|
143
|
+
|
|
144
|
+
No recursive diffing. No re-render cycle. No component tree.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 7. Cleanup Contract
|
|
149
|
+
|
|
150
|
+
The runtime exposes `cleanup()`:
|
|
151
|
+
|
|
152
|
+
- Disposes all active effects (clears subscriber sets)
|
|
153
|
+
- Removes all event listeners
|
|
154
|
+
- Clears the binding registry
|
|
155
|
+
- Leaves the DOM intact (caller decides whether to clear container)
|
|
156
|
+
|
|
157
|
+
Cleanup is deterministic — calling it twice is a no-op.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 8. Public API Surface
|
|
162
|
+
|
|
163
|
+
Total exports (exhaustive):
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
export { signal } // Create reactive signal
|
|
167
|
+
export { effect } // Create reactive effect
|
|
168
|
+
export { mount } // Mount page module into container
|
|
169
|
+
export { cleanup } // Tear down all bindings
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Four functions. No more.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 9. Alignment Verification
|
|
177
|
+
|
|
178
|
+
This contract is valid if and only if:
|
|
179
|
+
|
|
180
|
+
- [ ] `data-zx-e` indices match `__zenith_expr` array positions (from `BUNDLER_CONTRACT.md` §2)
|
|
181
|
+
- [ ] Expression functions are pre-bound at bundle time — runtime never resolves strings
|
|
182
|
+
- [ ] HMR footer is ignored by runtime (from `BUNDLER_CONTRACT.md` §7)
|
|
183
|
+
- [ ] No symbol beyond `__zenith_html`, `__zenith_expr`, `__zenith_page` is consumed (from `BUNDLER_CONTRACT.md` §1)
|
package/dist/cleanup.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// cleanup.js — Zenith Runtime V0
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Deterministic teardown system.
|
|
5
|
+
//
|
|
6
|
+
// Tracks:
|
|
7
|
+
// - Active effect disposers
|
|
8
|
+
// - Active event listeners
|
|
9
|
+
//
|
|
10
|
+
// cleanup() removes everything.
|
|
11
|
+
// Calling cleanup() twice is a no-op.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import { resetGlobalSideEffects } from './zeneffect.js';
|
|
15
|
+
|
|
16
|
+
/** @type {function[]} */
|
|
17
|
+
const _disposers = [];
|
|
18
|
+
|
|
19
|
+
/** @type {{ element: Element, event: string, handler: function }[]} */
|
|
20
|
+
const _listeners = [];
|
|
21
|
+
|
|
22
|
+
let _cleaned = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register an effect disposer for later cleanup.
|
|
26
|
+
*
|
|
27
|
+
* @param {function} dispose
|
|
28
|
+
*/
|
|
29
|
+
export function _registerDisposer(dispose) {
|
|
30
|
+
_disposers.push(dispose);
|
|
31
|
+
_cleaned = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register an event listener for later cleanup.
|
|
36
|
+
*
|
|
37
|
+
* @param {Element} element
|
|
38
|
+
* @param {string} event
|
|
39
|
+
* @param {function} handler
|
|
40
|
+
*/
|
|
41
|
+
export function _registerListener(element, event, handler) {
|
|
42
|
+
_listeners.push({ element, event, handler });
|
|
43
|
+
_cleaned = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Tear down all active effects and event listeners.
|
|
48
|
+
*
|
|
49
|
+
* - Disposes all effects (clears subscriber sets)
|
|
50
|
+
* - Removes all event listeners
|
|
51
|
+
* - Clears registries
|
|
52
|
+
* - Idempotent: calling twice is a no-op
|
|
53
|
+
*/
|
|
54
|
+
export function cleanup() {
|
|
55
|
+
// Global zenMount/zenEffect registrations can be created outside hydrate's
|
|
56
|
+
// disposer table (e.g. page-level component bootstraps). Even when cleanup
|
|
57
|
+
// has already run once, we still need to reset that scope deterministically.
|
|
58
|
+
if (_cleaned) {
|
|
59
|
+
resetGlobalSideEffects();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 1. Dispose all effects
|
|
64
|
+
for (let i = 0; i < _disposers.length; i++) {
|
|
65
|
+
_disposers[i]();
|
|
66
|
+
}
|
|
67
|
+
_disposers.length = 0;
|
|
68
|
+
|
|
69
|
+
// 2. Remove all event listeners
|
|
70
|
+
for (let i = 0; i < _listeners.length; i++) {
|
|
71
|
+
const { element, event, handler } = _listeners[i];
|
|
72
|
+
element.removeEventListener(event, handler);
|
|
73
|
+
}
|
|
74
|
+
_listeners.length = 0;
|
|
75
|
+
|
|
76
|
+
_cleaned = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get counts for testing/debugging.
|
|
81
|
+
* @returns {{ effects: number, listeners: number }}
|
|
82
|
+
*/
|
|
83
|
+
export function _getCounts() {
|
|
84
|
+
return {
|
|
85
|
+
effects: _disposers.length,
|
|
86
|
+
listeners: _listeners.length
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const OVERLAY_ID = '__zenith_runtime_error_overlay';
|
|
2
|
+
const MAX_MESSAGE_LENGTH = 120;
|
|
3
|
+
const MAX_HINT_LENGTH = 140;
|
|
4
|
+
const MAX_PATH_LENGTH = 120;
|
|
5
|
+
|
|
6
|
+
const VALID_PHASES = new Set(['hydrate', 'bind', 'render', 'event']);
|
|
7
|
+
const VALID_CODES = new Set([
|
|
8
|
+
'UNRESOLVED_EXPRESSION',
|
|
9
|
+
'NON_RENDERABLE_VALUE',
|
|
10
|
+
'MARKER_MISSING',
|
|
11
|
+
'FRAGMENT_MOUNT_FAILED',
|
|
12
|
+
'BINDING_APPLY_FAILED',
|
|
13
|
+
'EVENT_HANDLER_FAILED'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function _truncate(input, maxLength) {
|
|
17
|
+
const text = String(input ?? '');
|
|
18
|
+
if (text.length <= maxLength) return text;
|
|
19
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function _sanitizeAbsolutePaths(value) {
|
|
23
|
+
return String(value ?? '')
|
|
24
|
+
.replace(/[A-Za-z]:\\[^\s"'`]+/g, '<path>')
|
|
25
|
+
.replace(/\/Users\/[^\s"'`]+/g, '<path>')
|
|
26
|
+
.replace(/\/home\/[^\s"'`]+/g, '<path>')
|
|
27
|
+
.replace(/\/private\/[^\s"'`]+/g, '<path>')
|
|
28
|
+
.replace(/\/tmp\/[^\s"'`]+/g, '<path>')
|
|
29
|
+
.replace(/\/var\/folders\/[^\s"'`]+/g, '<path>');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _sanitizeMessage(value) {
|
|
33
|
+
const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
|
|
34
|
+
return _truncate(compact || 'Runtime failure', MAX_MESSAGE_LENGTH);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _sanitizeHint(value) {
|
|
38
|
+
if (value === null || value === undefined || value === false) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
|
|
42
|
+
if (!compact) return undefined;
|
|
43
|
+
return _truncate(compact, MAX_HINT_LENGTH);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _sanitizePath(value) {
|
|
47
|
+
if (value === null || value === undefined || value === false) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
|
|
51
|
+
if (!compact) return undefined;
|
|
52
|
+
return _truncate(compact, MAX_PATH_LENGTH);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _normalizeMarker(marker) {
|
|
56
|
+
if (!marker || typeof marker !== 'object') return undefined;
|
|
57
|
+
const markerType = _truncate(_sanitizeAbsolutePaths(marker.type || 'data-zx'), 48);
|
|
58
|
+
const markerId = marker.id;
|
|
59
|
+
if (markerId === null || markerId === undefined || markerId === '') return undefined;
|
|
60
|
+
if (typeof markerId === 'number') {
|
|
61
|
+
return { type: markerType, id: markerId };
|
|
62
|
+
}
|
|
63
|
+
return { type: markerType, id: _truncate(_sanitizeAbsolutePaths(markerId), 48) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _extractErrorMessage(error) {
|
|
67
|
+
if (!error) return '';
|
|
68
|
+
if (typeof error === 'string') return error;
|
|
69
|
+
if (error instanceof Error && typeof error.message === 'string') return error.message;
|
|
70
|
+
if (typeof error.message === 'string') return error.message;
|
|
71
|
+
return String(error);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _safeJson(payload) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.stringify(payload, null, 2);
|
|
77
|
+
} catch {
|
|
78
|
+
return '{"kind":"ZENITH_RUNTIME_ERROR","message":"Unable to serialize runtime error payload"}';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _isDevDiagnosticsMode() {
|
|
83
|
+
const runtime = typeof globalThis !== 'undefined' ? globalThis : {};
|
|
84
|
+
if (runtime.__ZENITH_RUNTIME_DEV__ === true || runtime.__ZENITH_DEV__ === true) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (runtime.__ZENITH_RUNTIME_DEV__ === false || runtime.__ZENITH_DEV__ === false) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (runtime.__ZENITH_RUNTIME_PROD__ === true) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (typeof location !== 'undefined' && location && typeof location.hostname === 'string') {
|
|
94
|
+
const host = String(location.hostname).toLowerCase();
|
|
95
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '[::1]') {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _renderOverlay(payload) {
|
|
103
|
+
if (!_isDevDiagnosticsMode()) return;
|
|
104
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
105
|
+
|
|
106
|
+
let overlay = document.getElementById(OVERLAY_ID);
|
|
107
|
+
if (!overlay) {
|
|
108
|
+
overlay = document.createElement('aside');
|
|
109
|
+
overlay.id = OVERLAY_ID;
|
|
110
|
+
overlay.setAttribute('role', 'alert');
|
|
111
|
+
overlay.setAttribute('aria-live', 'assertive');
|
|
112
|
+
overlay.style.position = 'fixed';
|
|
113
|
+
overlay.style.left = '12px';
|
|
114
|
+
overlay.style.right = '12px';
|
|
115
|
+
overlay.style.bottom = '12px';
|
|
116
|
+
overlay.style.maxHeight = '45vh';
|
|
117
|
+
overlay.style.overflow = 'auto';
|
|
118
|
+
overlay.style.zIndex = '2147483647';
|
|
119
|
+
overlay.style.padding = '12px';
|
|
120
|
+
overlay.style.border = '1px solid #ff6b6b';
|
|
121
|
+
overlay.style.borderRadius = '8px';
|
|
122
|
+
overlay.style.background = '#111';
|
|
123
|
+
overlay.style.color = '#ffe5e5';
|
|
124
|
+
overlay.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
125
|
+
overlay.style.fontSize = '12px';
|
|
126
|
+
overlay.style.lineHeight = '1.45';
|
|
127
|
+
overlay.style.boxShadow = '0 12px 40px rgba(0,0,0,0.45)';
|
|
128
|
+
|
|
129
|
+
const copyButton = document.createElement('button');
|
|
130
|
+
copyButton.type = 'button';
|
|
131
|
+
copyButton.setAttribute('data-zx-runtime-copy', 'true');
|
|
132
|
+
copyButton.style.marginTop = '8px';
|
|
133
|
+
copyButton.style.padding = '4px 8px';
|
|
134
|
+
copyButton.style.border = '1px solid #ff9d9d';
|
|
135
|
+
copyButton.style.borderRadius = '4px';
|
|
136
|
+
copyButton.style.background = '#2a2a2a';
|
|
137
|
+
copyButton.style.color = '#ffe5e5';
|
|
138
|
+
copyButton.style.cursor = 'pointer';
|
|
139
|
+
copyButton.textContent = 'Copy JSON';
|
|
140
|
+
overlay.appendChild(copyButton);
|
|
141
|
+
|
|
142
|
+
document.body.appendChild(overlay);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const textLines = [
|
|
146
|
+
'Zenith Runtime Error',
|
|
147
|
+
`phase: ${payload.phase}`,
|
|
148
|
+
`code: ${payload.code}`,
|
|
149
|
+
`message: ${payload.message}`
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (payload.marker) {
|
|
153
|
+
textLines.push(`marker: ${payload.marker.type}#${payload.marker.id}`);
|
|
154
|
+
}
|
|
155
|
+
if (payload.path) {
|
|
156
|
+
textLines.push(`path: ${payload.path}`);
|
|
157
|
+
}
|
|
158
|
+
if (payload.hint) {
|
|
159
|
+
textLines.push(`hint: ${payload.hint}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const jsonText = _safeJson(payload);
|
|
163
|
+
const panelText = textLines.join('\n');
|
|
164
|
+
|
|
165
|
+
let pre = overlay.querySelector('pre[data-zx-runtime-error]');
|
|
166
|
+
if (!pre) {
|
|
167
|
+
pre = document.createElement('pre');
|
|
168
|
+
pre.setAttribute('data-zx-runtime-error', 'true');
|
|
169
|
+
pre.style.margin = '0';
|
|
170
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
171
|
+
pre.style.wordBreak = 'break-word';
|
|
172
|
+
overlay.insertBefore(pre, overlay.firstChild);
|
|
173
|
+
}
|
|
174
|
+
pre.textContent = panelText;
|
|
175
|
+
|
|
176
|
+
const copyButton = overlay.querySelector('button[data-zx-runtime-copy="true"]');
|
|
177
|
+
if (copyButton) {
|
|
178
|
+
copyButton.onclick = () => {
|
|
179
|
+
const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : null;
|
|
180
|
+
if (clipboard && typeof clipboard.writeText === 'function') {
|
|
181
|
+
void clipboard.writeText(jsonText);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _mapLegacyError(error, fallback) {
|
|
188
|
+
const rawMessage = _extractErrorMessage(error);
|
|
189
|
+
const safeMessage = _sanitizeMessage(rawMessage);
|
|
190
|
+
|
|
191
|
+
const details = {
|
|
192
|
+
phase: VALID_PHASES.has(fallback.phase) ? fallback.phase : 'hydrate',
|
|
193
|
+
code: VALID_CODES.has(fallback.code) ? fallback.code : 'BINDING_APPLY_FAILED',
|
|
194
|
+
message: _sanitizeMessage(fallback.message || safeMessage),
|
|
195
|
+
marker: _normalizeMarker(fallback.marker),
|
|
196
|
+
path: _sanitizePath(fallback.path),
|
|
197
|
+
hint: _sanitizeHint(fallback.hint)
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (/failed to resolve expression literal/i.test(rawMessage)) {
|
|
201
|
+
details.phase = 'bind';
|
|
202
|
+
details.code = 'UNRESOLVED_EXPRESSION';
|
|
203
|
+
details.hint = details.hint || 'Verify expression scope keys and signal aliases.';
|
|
204
|
+
} else if (/non-renderable (object|function)/i.test(rawMessage)) {
|
|
205
|
+
details.phase = 'render';
|
|
206
|
+
details.code = 'NON_RENDERABLE_VALUE';
|
|
207
|
+
const match = rawMessage.match(/at\s+([A-Za-z0-9_\[\].-]+)/);
|
|
208
|
+
if (match && !details.path) {
|
|
209
|
+
details.path = _sanitizePath(match[1]);
|
|
210
|
+
}
|
|
211
|
+
details.hint = details.hint || 'Use map() to render object fields into nodes.';
|
|
212
|
+
} else if (/unresolved .* marker index/i.test(rawMessage)) {
|
|
213
|
+
details.phase = 'bind';
|
|
214
|
+
details.code = 'MARKER_MISSING';
|
|
215
|
+
const markerMatch = rawMessage.match(/unresolved\s+(\w+)\s+marker index\s+(\d+)/i);
|
|
216
|
+
if (markerMatch && !details.marker) {
|
|
217
|
+
details.marker = {
|
|
218
|
+
type: `data-zx-${markerMatch[1]}`,
|
|
219
|
+
id: Number(markerMatch[2])
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
details.hint = details.hint || 'Confirm SSR markers and client selector tables match.';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return details;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function isZenithRuntimeError(error) {
|
|
229
|
+
return !!(
|
|
230
|
+
error &&
|
|
231
|
+
typeof error === 'object' &&
|
|
232
|
+
error.zenithRuntimeError &&
|
|
233
|
+
error.zenithRuntimeError.kind === 'ZENITH_RUNTIME_ERROR'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function createZenithRuntimeError(details, cause) {
|
|
238
|
+
const phase = VALID_PHASES.has(details?.phase) ? details.phase : 'hydrate';
|
|
239
|
+
const code = VALID_CODES.has(details?.code) ? details.code : 'BINDING_APPLY_FAILED';
|
|
240
|
+
const message = _sanitizeMessage(details?.message || 'Runtime failure');
|
|
241
|
+
|
|
242
|
+
const payload = {
|
|
243
|
+
kind: 'ZENITH_RUNTIME_ERROR',
|
|
244
|
+
phase,
|
|
245
|
+
code,
|
|
246
|
+
message
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const marker = _normalizeMarker(details?.marker);
|
|
250
|
+
if (marker) payload.marker = marker;
|
|
251
|
+
|
|
252
|
+
const path = _sanitizePath(details?.path);
|
|
253
|
+
if (path) payload.path = path;
|
|
254
|
+
|
|
255
|
+
const hint = _sanitizeHint(details?.hint);
|
|
256
|
+
if (hint) payload.hint = hint;
|
|
257
|
+
|
|
258
|
+
const error = new Error(`[Zenith Runtime] ${code}: ${message}`);
|
|
259
|
+
error.name = 'ZenithRuntimeError';
|
|
260
|
+
error.zenithRuntimeError = payload;
|
|
261
|
+
if (cause !== undefined) {
|
|
262
|
+
error.cause = cause;
|
|
263
|
+
}
|
|
264
|
+
error.toJSON = () => payload;
|
|
265
|
+
return error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function _reportRuntimeError(error) {
|
|
269
|
+
if (!error || error.__zenithRuntimeErrorReported === true) return;
|
|
270
|
+
error.__zenithRuntimeErrorReported = true;
|
|
271
|
+
const payload = error.zenithRuntimeError;
|
|
272
|
+
if (payload && typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
273
|
+
console.error('[Zenith Runtime]', payload);
|
|
274
|
+
}
|
|
275
|
+
_renderOverlay(payload);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function throwZenithRuntimeError(details, cause) {
|
|
279
|
+
const error = createZenithRuntimeError(details, cause);
|
|
280
|
+
_reportRuntimeError(error);
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function rethrowZenithRuntimeError(error, fallback = {}) {
|
|
285
|
+
if (isZenithRuntimeError(error)) {
|
|
286
|
+
const payload = error.zenithRuntimeError || {};
|
|
287
|
+
let updatedPayload = payload;
|
|
288
|
+
const marker = !payload.marker ? _normalizeMarker(fallback.marker) : payload.marker;
|
|
289
|
+
const path = !payload.path ? _sanitizePath(fallback.path) : payload.path;
|
|
290
|
+
const hint = !payload.hint ? _sanitizeHint(fallback.hint) : payload.hint;
|
|
291
|
+
|
|
292
|
+
if (marker || path || hint) {
|
|
293
|
+
updatedPayload = {
|
|
294
|
+
...payload,
|
|
295
|
+
...(marker ? { marker } : null),
|
|
296
|
+
...(path ? { path } : null),
|
|
297
|
+
...(hint ? { hint } : null)
|
|
298
|
+
};
|
|
299
|
+
error.zenithRuntimeError = updatedPayload;
|
|
300
|
+
error.toJSON = () => updatedPayload;
|
|
301
|
+
}
|
|
302
|
+
_reportRuntimeError(error);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
const mapped = _mapLegacyError(error, fallback || {});
|
|
306
|
+
const wrapped = createZenithRuntimeError(mapped, error);
|
|
307
|
+
_reportRuntimeError(wrapped);
|
|
308
|
+
throw wrapped;
|
|
309
|
+
}
|
package/dist/effect.js
ADDED