@zenithbuild/compiler 1.0.2
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/build-analyzer.d.ts +44 -0
- package/dist/build-analyzer.js +87 -0
- package/dist/bundler.d.ts +31 -0
- package/dist/bundler.js +86 -0
- package/dist/core/components/index.d.ts +9 -0
- package/dist/core/components/index.js +13 -0
- package/dist/core/config/index.d.ts +11 -0
- package/dist/core/config/index.js +10 -0
- package/dist/core/config/loader.d.ts +17 -0
- package/dist/core/config/loader.js +60 -0
- package/dist/core/config/types.d.ts +98 -0
- package/dist/core/config/types.js +32 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +6 -0
- package/dist/core/lifecycle/index.d.ts +16 -0
- package/dist/core/lifecycle/index.js +19 -0
- package/dist/core/lifecycle/zen-mount.d.ts +66 -0
- package/dist/core/lifecycle/zen-mount.js +151 -0
- package/dist/core/lifecycle/zen-unmount.d.ts +54 -0
- package/dist/core/lifecycle/zen-unmount.js +76 -0
- package/dist/core/plugins/bridge.d.ts +116 -0
- package/dist/core/plugins/bridge.js +121 -0
- package/dist/core/plugins/index.d.ts +6 -0
- package/dist/core/plugins/index.js +6 -0
- package/dist/core/plugins/registry.d.ts +67 -0
- package/dist/core/plugins/registry.js +113 -0
- package/dist/core/reactivity/index.d.ts +30 -0
- package/dist/core/reactivity/index.js +33 -0
- package/dist/core/reactivity/tracking.d.ts +74 -0
- package/dist/core/reactivity/tracking.js +136 -0
- package/dist/core/reactivity/zen-batch.d.ts +45 -0
- package/dist/core/reactivity/zen-batch.js +54 -0
- package/dist/core/reactivity/zen-effect.d.ts +48 -0
- package/dist/core/reactivity/zen-effect.js +98 -0
- package/dist/core/reactivity/zen-memo.d.ts +43 -0
- package/dist/core/reactivity/zen-memo.js +100 -0
- package/dist/core/reactivity/zen-ref.d.ts +44 -0
- package/dist/core/reactivity/zen-ref.js +34 -0
- package/dist/core/reactivity/zen-signal.d.ts +48 -0
- package/dist/core/reactivity/zen-signal.js +84 -0
- package/dist/core/reactivity/zen-state.d.ts +35 -0
- package/dist/core/reactivity/zen-state.js +147 -0
- package/dist/core/reactivity/zen-untrack.d.ts +38 -0
- package/dist/core/reactivity/zen-untrack.js +41 -0
- package/dist/css/index.d.ts +73 -0
- package/dist/css/index.js +246 -0
- package/dist/discovery/componentDiscovery.d.ts +42 -0
- package/dist/discovery/componentDiscovery.js +56 -0
- package/dist/discovery/layouts.d.ts +13 -0
- package/dist/discovery/layouts.js +41 -0
- package/dist/errors/compilerError.d.ts +31 -0
- package/dist/errors/compilerError.js +51 -0
- package/dist/finalize/finalizeOutput.d.ts +32 -0
- package/dist/finalize/finalizeOutput.js +62 -0
- package/dist/finalize/generateFinalBundle.d.ts +24 -0
- package/dist/finalize/generateFinalBundle.js +68 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +51 -0
- package/dist/ir/types.d.ts +181 -0
- package/dist/ir/types.js +8 -0
- package/dist/output/types.d.ts +30 -0
- package/dist/output/types.js +6 -0
- package/dist/parse/detectMapExpressions.d.ts +45 -0
- package/dist/parse/detectMapExpressions.js +77 -0
- package/dist/parse/parseScript.d.ts +8 -0
- package/dist/parse/parseScript.js +36 -0
- package/dist/parse/parseTemplate.d.ts +11 -0
- package/dist/parse/parseTemplate.js +487 -0
- package/dist/parse/parseZenFile.d.ts +11 -0
- package/dist/parse/parseZenFile.js +50 -0
- package/dist/parse/scriptAnalysis.d.ts +25 -0
- package/dist/parse/scriptAnalysis.js +60 -0
- package/dist/parse/trackLoopContext.d.ts +20 -0
- package/dist/parse/trackLoopContext.js +62 -0
- package/dist/parseZenFile.d.ts +10 -0
- package/dist/parseZenFile.js +55 -0
- package/dist/runtime/analyzeAndEmit.d.ts +20 -0
- package/dist/runtime/analyzeAndEmit.js +70 -0
- package/dist/runtime/build.d.ts +6 -0
- package/dist/runtime/build.js +13 -0
- package/dist/runtime/bundle-generator.d.ts +27 -0
- package/dist/runtime/bundle-generator.js +1263 -0
- package/dist/runtime/client-runtime.d.ts +41 -0
- package/dist/runtime/client-runtime.js +397 -0
- package/dist/runtime/dataExposure.d.ts +52 -0
- package/dist/runtime/dataExposure.js +227 -0
- package/dist/runtime/generateDOM.d.ts +21 -0
- package/dist/runtime/generateDOM.js +194 -0
- package/dist/runtime/generateHydrationBundle.d.ts +15 -0
- package/dist/runtime/generateHydrationBundle.js +399 -0
- package/dist/runtime/hydration.d.ts +53 -0
- package/dist/runtime/hydration.js +271 -0
- package/dist/runtime/navigation.d.ts +58 -0
- package/dist/runtime/navigation.js +372 -0
- package/dist/runtime/serve.d.ts +13 -0
- package/dist/runtime/serve.js +76 -0
- package/dist/runtime/thinRuntime.d.ts +23 -0
- package/dist/runtime/thinRuntime.js +158 -0
- package/dist/runtime/transformIR.d.ts +19 -0
- package/dist/runtime/transformIR.js +285 -0
- package/dist/runtime/wrapExpression.d.ts +24 -0
- package/dist/runtime/wrapExpression.js +76 -0
- package/dist/runtime/wrapExpressionWithLoop.d.ts +17 -0
- package/dist/runtime/wrapExpressionWithLoop.js +75 -0
- package/dist/spa-build.d.ts +26 -0
- package/dist/spa-build.js +866 -0
- package/dist/ssg-build.d.ts +32 -0
- package/dist/ssg-build.js +408 -0
- package/dist/test/analyze-emit.test.d.ts +1 -0
- package/dist/test/analyze-emit.test.js +88 -0
- package/dist/test/bundler-contract.test.d.ts +1 -0
- package/dist/test/bundler-contract.test.js +137 -0
- package/dist/test/compiler-authority.test.d.ts +1 -0
- package/dist/test/compiler-authority.test.js +90 -0
- package/dist/test/component-instance-test.d.ts +1 -0
- package/dist/test/component-instance-test.js +115 -0
- package/dist/test/error-native-bridge.test.d.ts +1 -0
- package/dist/test/error-native-bridge.test.js +51 -0
- package/dist/test/error-serialization.test.d.ts +1 -0
- package/dist/test/error-serialization.test.js +38 -0
- package/dist/test/macro-inlining.test.d.ts +1 -0
- package/dist/test/macro-inlining.test.js +178 -0
- package/dist/test/validate-test.d.ts +6 -0
- package/dist/test/validate-test.js +95 -0
- package/dist/transform/classifyExpression.d.ts +46 -0
- package/dist/transform/classifyExpression.js +354 -0
- package/dist/transform/componentResolver.d.ts +15 -0
- package/dist/transform/componentResolver.js +30 -0
- package/dist/transform/expressionTransformer.d.ts +19 -0
- package/dist/transform/expressionTransformer.js +333 -0
- package/dist/transform/fragmentLowering.d.ts +25 -0
- package/dist/transform/fragmentLowering.js +468 -0
- package/dist/transform/layoutProcessor.d.ts +5 -0
- package/dist/transform/layoutProcessor.js +34 -0
- package/dist/transform/transformTemplate.d.ts +11 -0
- package/dist/transform/transformTemplate.js +33 -0
- package/dist/validate/invariants.d.ts +23 -0
- package/dist/validate/invariants.js +55 -0
- package/native/compiler-native/compiler-native.node +0 -0
- package/native/compiler-native/index.d.ts +113 -0
- package/native/compiler-native/index.js +19 -0
- package/native/compiler-native/package.json +19 -0
- package/package.json +49 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Hydration Layer
|
|
3
|
+
*
|
|
4
|
+
* Phase 5: Browser-side runtime that hydrates static HTML with dynamic expressions
|
|
5
|
+
*
|
|
6
|
+
* This runtime:
|
|
7
|
+
* - Locates DOM placeholders (data-zen-text, data-zen-attr-*)
|
|
8
|
+
* - Evaluates precompiled expressions against state
|
|
9
|
+
* - Updates DOM textContent, attributes, and properties
|
|
10
|
+
* - Binds event handlers
|
|
11
|
+
* - Handles reactive state updates
|
|
12
|
+
*/
|
|
13
|
+
const bindings = [];
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate static HTML with dynamic expressions
|
|
16
|
+
*
|
|
17
|
+
* @param state - The state object to evaluate expressions against
|
|
18
|
+
* @param container - The container element to hydrate (defaults to document)
|
|
19
|
+
*/
|
|
20
|
+
export function hydrate(state, container = document) {
|
|
21
|
+
if (!state) {
|
|
22
|
+
console.warn('[Zenith] hydrate called without state object');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Store state globally for event handlers
|
|
26
|
+
if (typeof window !== 'undefined') {
|
|
27
|
+
window.__ZENITH_STATE__ = state;
|
|
28
|
+
}
|
|
29
|
+
// Clear existing bindings
|
|
30
|
+
bindings.length = 0;
|
|
31
|
+
// Find all text expression placeholders
|
|
32
|
+
const textPlaceholders = container.querySelectorAll('[data-zen-text]');
|
|
33
|
+
for (let i = 0; i < textPlaceholders.length; i++) {
|
|
34
|
+
const node = textPlaceholders[i];
|
|
35
|
+
if (!node)
|
|
36
|
+
continue;
|
|
37
|
+
const expressionId = node.getAttribute('data-zen-text');
|
|
38
|
+
if (!expressionId)
|
|
39
|
+
continue;
|
|
40
|
+
bindings.push({
|
|
41
|
+
node,
|
|
42
|
+
type: 'text',
|
|
43
|
+
expressionId
|
|
44
|
+
});
|
|
45
|
+
updateTextBinding(node, expressionId, state);
|
|
46
|
+
}
|
|
47
|
+
// Find all attribute expression placeholders
|
|
48
|
+
const attrPlaceholders = container.querySelectorAll('[data-zen-attr-class], [data-zen-attr-style], [data-zen-attr-src], [data-zen-attr-href], [data-zen-attr-disabled], [data-zen-attr-checked]');
|
|
49
|
+
for (let i = 0; i < attrPlaceholders.length; i++) {
|
|
50
|
+
const node = attrPlaceholders[i];
|
|
51
|
+
if (!(node instanceof Element))
|
|
52
|
+
continue;
|
|
53
|
+
// Check each possible attribute
|
|
54
|
+
const attrNames = ['class', 'style', 'src', 'href', 'disabled', 'checked'];
|
|
55
|
+
for (const attrName of attrNames) {
|
|
56
|
+
const expressionId = node.getAttribute(`data-zen-attr-${attrName}`);
|
|
57
|
+
if (!expressionId)
|
|
58
|
+
continue;
|
|
59
|
+
bindings.push({
|
|
60
|
+
node,
|
|
61
|
+
type: 'attribute',
|
|
62
|
+
attributeName: attrName,
|
|
63
|
+
expressionId
|
|
64
|
+
});
|
|
65
|
+
updateAttributeBinding(node, attrName, expressionId, state);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Bind event handlers
|
|
69
|
+
bindEvents(container);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Update a text binding
|
|
73
|
+
*/
|
|
74
|
+
function updateTextBinding(node, expressionId, state) {
|
|
75
|
+
try {
|
|
76
|
+
const expression = window.__ZENITH_EXPRESSIONS__?.get(expressionId);
|
|
77
|
+
if (!expression) {
|
|
78
|
+
console.warn(`[Zenith] Expression ${expressionId} not found in registry`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const result = expression(state);
|
|
82
|
+
// Handle different result types
|
|
83
|
+
if (result === null || result === undefined || result === false) {
|
|
84
|
+
node.textContent = '';
|
|
85
|
+
}
|
|
86
|
+
else if (typeof result === 'string' || typeof result === 'number') {
|
|
87
|
+
node.textContent = String(result);
|
|
88
|
+
}
|
|
89
|
+
else if (result instanceof Node) {
|
|
90
|
+
// Replace node with result node
|
|
91
|
+
if (node.parentNode) {
|
|
92
|
+
node.parentNode.replaceChild(result, node);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (Array.isArray(result)) {
|
|
96
|
+
// Handle array results (for map expressions)
|
|
97
|
+
if (node.parentNode) {
|
|
98
|
+
const fragment = document.createDocumentFragment();
|
|
99
|
+
for (const item of result) {
|
|
100
|
+
if (item instanceof Node) {
|
|
101
|
+
fragment.appendChild(item);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
fragment.appendChild(document.createTextNode(String(item)));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
node.parentNode.replaceChild(fragment, node);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
node.textContent = String(result);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error(`[Zenith] Error evaluating expression ${expressionId}:`, error);
|
|
116
|
+
console.error('Expression ID:', expressionId, 'State:', state);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Update an attribute binding
|
|
121
|
+
*/
|
|
122
|
+
function updateAttributeBinding(element, attributeName, expressionId, state) {
|
|
123
|
+
try {
|
|
124
|
+
const expression = window.__ZENITH_EXPRESSIONS__?.get(expressionId);
|
|
125
|
+
if (!expression) {
|
|
126
|
+
console.warn(`[Zenith] Expression ${expressionId} not found in registry`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const result = expression(state);
|
|
130
|
+
// Handle different attribute types
|
|
131
|
+
if (attributeName === 'class' || attributeName === 'className') {
|
|
132
|
+
element.className = String(result ?? '');
|
|
133
|
+
}
|
|
134
|
+
else if (attributeName === 'style') {
|
|
135
|
+
if (typeof result === 'string') {
|
|
136
|
+
element.setAttribute('style', result);
|
|
137
|
+
}
|
|
138
|
+
else if (result && typeof result === 'object') {
|
|
139
|
+
// Handle style object
|
|
140
|
+
const styleStr = Object.entries(result)
|
|
141
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
142
|
+
.join('; ');
|
|
143
|
+
element.setAttribute('style', styleStr);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (attributeName === 'disabled' || attributeName === 'checked' || attributeName === 'readonly') {
|
|
147
|
+
// Boolean attributes
|
|
148
|
+
if (result) {
|
|
149
|
+
element.setAttribute(attributeName, '');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
element.removeAttribute(attributeName);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Regular attributes
|
|
157
|
+
if (result === null || result === undefined || result === false) {
|
|
158
|
+
element.removeAttribute(attributeName);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
element.setAttribute(attributeName, String(result));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error(`[Zenith] Error updating attribute ${attributeName} with expression ${expressionId}:`, error);
|
|
167
|
+
console.error('Expression ID:', expressionId, 'State:', state);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Bind event handlers to DOM elements
|
|
172
|
+
*
|
|
173
|
+
* @param container - The container element to bind events in (defaults to document)
|
|
174
|
+
*/
|
|
175
|
+
export function bindEvents(container = document) {
|
|
176
|
+
const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
|
|
177
|
+
for (const eventType of eventTypes) {
|
|
178
|
+
const elements = container.querySelectorAll(`[data-zen-${eventType}]`);
|
|
179
|
+
for (let i = 0; i < elements.length; i++) {
|
|
180
|
+
const element = elements[i];
|
|
181
|
+
if (!(element instanceof Element))
|
|
182
|
+
continue;
|
|
183
|
+
const handlerName = element.getAttribute(`data-zen-${eventType}`);
|
|
184
|
+
if (!handlerName)
|
|
185
|
+
continue;
|
|
186
|
+
// Remove existing listener if any (to avoid duplicates)
|
|
187
|
+
const existingHandler = element[`__zen_${eventType}_handler`];
|
|
188
|
+
if (existingHandler) {
|
|
189
|
+
element.removeEventListener(eventType, existingHandler);
|
|
190
|
+
}
|
|
191
|
+
// Create new handler
|
|
192
|
+
const handler = (event) => {
|
|
193
|
+
try {
|
|
194
|
+
// Get handler function from window (functions are registered on window)
|
|
195
|
+
const handlerFunc = window[handlerName];
|
|
196
|
+
if (typeof handlerFunc === 'function') {
|
|
197
|
+
handlerFunc(event, element);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.warn(`[Zenith] Event handler "${handlerName}" not found for ${eventType} event`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error(`[Zenith] Error executing event handler "${handlerName}":`, error);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
element[`__zen_${eventType}_handler`] = handler;
|
|
208
|
+
element.addEventListener(eventType, handler);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Update all bindings when state changes
|
|
214
|
+
*
|
|
215
|
+
* @param state - The new state object
|
|
216
|
+
*/
|
|
217
|
+
export function update(state) {
|
|
218
|
+
if (!state) {
|
|
219
|
+
console.warn('[Zenith] update called without state object');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Update global state
|
|
223
|
+
if (typeof window !== 'undefined') {
|
|
224
|
+
window.__ZENITH_STATE__ = state;
|
|
225
|
+
}
|
|
226
|
+
// Update all tracked bindings
|
|
227
|
+
for (const binding of bindings) {
|
|
228
|
+
if (binding.type === 'text') {
|
|
229
|
+
updateTextBinding(binding.node, binding.expressionId, state);
|
|
230
|
+
}
|
|
231
|
+
else if (binding.type === 'attribute' && binding.attributeName) {
|
|
232
|
+
if (binding.node instanceof Element) {
|
|
233
|
+
updateAttributeBinding(binding.node, binding.attributeName, binding.expressionId, state);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Initialize the expression registry
|
|
240
|
+
* Called once when the runtime loads
|
|
241
|
+
*
|
|
242
|
+
* @param expressions - Map of expression IDs to evaluation functions
|
|
243
|
+
*/
|
|
244
|
+
export function initExpressions(expressions) {
|
|
245
|
+
if (typeof window !== 'undefined') {
|
|
246
|
+
window.__ZENITH_EXPRESSIONS__ = expressions;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Clear all bindings and event listeners
|
|
251
|
+
* Useful for cleanup when navigating away
|
|
252
|
+
*/
|
|
253
|
+
export function cleanup(container = document) {
|
|
254
|
+
// Remove event listeners
|
|
255
|
+
const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
|
|
256
|
+
for (const eventType of eventTypes) {
|
|
257
|
+
const elements = container.querySelectorAll(`[data-zen-${eventType}]`);
|
|
258
|
+
for (let i = 0; i < elements.length; i++) {
|
|
259
|
+
const element = elements[i];
|
|
260
|
+
if (!(element instanceof Element))
|
|
261
|
+
continue;
|
|
262
|
+
const handler = element[`__zen_${eventType}_handler`];
|
|
263
|
+
if (handler) {
|
|
264
|
+
element.removeEventListener(eventType, handler);
|
|
265
|
+
delete element[`__zen_${eventType}_handler`];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Clear bindings
|
|
270
|
+
bindings.length = 0;
|
|
271
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation & Prefetch Runtime
|
|
3
|
+
*
|
|
4
|
+
* Phase 7: Prefetch compiled output, safe SPA navigation, route caching
|
|
5
|
+
*
|
|
6
|
+
* This runtime handles:
|
|
7
|
+
* - Prefetching compiled HTML + JS for routes
|
|
8
|
+
* - Caching prefetched routes
|
|
9
|
+
* - Safe DOM mounting and hydration
|
|
10
|
+
* - Browser history management
|
|
11
|
+
* - Explicit data exposure for navigation
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Route cache entry containing compiled output
|
|
15
|
+
*/
|
|
16
|
+
export interface RouteCacheEntry {
|
|
17
|
+
html: string;
|
|
18
|
+
js: string;
|
|
19
|
+
styles: string[];
|
|
20
|
+
routePath: string;
|
|
21
|
+
compiledAt: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Navigation options with explicit data
|
|
25
|
+
*/
|
|
26
|
+
export interface NavigateOptions {
|
|
27
|
+
loaderData?: any;
|
|
28
|
+
props?: any;
|
|
29
|
+
stores?: any;
|
|
30
|
+
replace?: boolean;
|
|
31
|
+
prefetch?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Prefetch a route's compiled output
|
|
35
|
+
*
|
|
36
|
+
* @param routePath - The route path to prefetch (e.g., "/dashboard")
|
|
37
|
+
* @returns Promise that resolves when prefetch is complete
|
|
38
|
+
*/
|
|
39
|
+
export declare function prefetchRoute(routePath: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Get cached route entry
|
|
42
|
+
*/
|
|
43
|
+
export declare function getCachedRoute(routePath: string): RouteCacheEntry | null;
|
|
44
|
+
/**
|
|
45
|
+
* Navigate to a route with explicit data
|
|
46
|
+
*
|
|
47
|
+
* @param routePath - The route path to navigate to
|
|
48
|
+
* @param options - Navigation options with loaderData, props, stores
|
|
49
|
+
*/
|
|
50
|
+
export declare function navigate(routePath: string, options?: NavigateOptions): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Handle browser back/forward navigation
|
|
53
|
+
*/
|
|
54
|
+
export declare function setupHistoryHandling(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Generate navigation runtime code (to be included in bundle)
|
|
57
|
+
*/
|
|
58
|
+
export declare function generateNavigationRuntime(): string;
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation & Prefetch Runtime
|
|
3
|
+
*
|
|
4
|
+
* Phase 7: Prefetch compiled output, safe SPA navigation, route caching
|
|
5
|
+
*
|
|
6
|
+
* This runtime handles:
|
|
7
|
+
* - Prefetching compiled HTML + JS for routes
|
|
8
|
+
* - Caching prefetched routes
|
|
9
|
+
* - Safe DOM mounting and hydration
|
|
10
|
+
* - Browser history management
|
|
11
|
+
* - Explicit data exposure for navigation
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Route cache - stores prefetched compiled output
|
|
15
|
+
*/
|
|
16
|
+
const routeCache = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Current navigation state
|
|
19
|
+
*/
|
|
20
|
+
let currentRoute = '';
|
|
21
|
+
let navigationInProgress = false;
|
|
22
|
+
/**
|
|
23
|
+
* Prefetch a route's compiled output
|
|
24
|
+
*
|
|
25
|
+
* @param routePath - The route path to prefetch (e.g., "/dashboard")
|
|
26
|
+
* @returns Promise that resolves when prefetch is complete
|
|
27
|
+
*/
|
|
28
|
+
export async function prefetchRoute(routePath) {
|
|
29
|
+
// Normalize route path
|
|
30
|
+
const normalizedPath = routePath === '' ? '/' : routePath;
|
|
31
|
+
// Check if already cached
|
|
32
|
+
if (routeCache.has(normalizedPath)) {
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
// In a real implementation, this would fetch from the build output
|
|
36
|
+
// For Phase 7, we'll generate a placeholder that indicates the route needs to be built
|
|
37
|
+
try {
|
|
38
|
+
// Fetch compiled HTML + JS
|
|
39
|
+
// In production, this would be:
|
|
40
|
+
// const htmlResponse = await fetch(`${normalizedPath}.html`)
|
|
41
|
+
// const jsResponse = await fetch(`${normalizedPath}.js`)
|
|
42
|
+
// For now, return a placeholder that indicates prefetch structure
|
|
43
|
+
const cacheEntry = {
|
|
44
|
+
html: `<!-- Prefetched route: ${normalizedPath} -->`,
|
|
45
|
+
js: `// Prefetched route runtime: ${normalizedPath}`,
|
|
46
|
+
styles: [],
|
|
47
|
+
routePath: normalizedPath,
|
|
48
|
+
compiledAt: Date.now()
|
|
49
|
+
};
|
|
50
|
+
routeCache.set(normalizedPath, cacheEntry);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn(`[Zenith] Failed to prefetch route ${normalizedPath}:`, error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get cached route entry
|
|
59
|
+
*/
|
|
60
|
+
export function getCachedRoute(routePath) {
|
|
61
|
+
const normalizedPath = routePath === '' ? '/' : routePath;
|
|
62
|
+
return routeCache.get(normalizedPath) || null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Navigate to a route with explicit data
|
|
66
|
+
*
|
|
67
|
+
* @param routePath - The route path to navigate to
|
|
68
|
+
* @param options - Navigation options with loaderData, props, stores
|
|
69
|
+
*/
|
|
70
|
+
export async function navigate(routePath, options = {}) {
|
|
71
|
+
if (navigationInProgress) {
|
|
72
|
+
console.warn('[Zenith] Navigation already in progress, skipping');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
navigationInProgress = true;
|
|
76
|
+
try {
|
|
77
|
+
const normalizedPath = routePath === '' ? '/' : routePath;
|
|
78
|
+
// Check if route is cached, otherwise prefetch
|
|
79
|
+
let cacheEntry = getCachedRoute(normalizedPath);
|
|
80
|
+
if (!cacheEntry && options.prefetch !== false) {
|
|
81
|
+
await prefetchRoute(normalizedPath);
|
|
82
|
+
cacheEntry = getCachedRoute(normalizedPath);
|
|
83
|
+
}
|
|
84
|
+
if (!cacheEntry) {
|
|
85
|
+
throw new Error(`Route ${normalizedPath} not found. Ensure the route is compiled.`);
|
|
86
|
+
}
|
|
87
|
+
// Cleanup previous route
|
|
88
|
+
cleanupPreviousRoute();
|
|
89
|
+
// Get router outlet
|
|
90
|
+
const outlet = getRouterOutlet();
|
|
91
|
+
if (!outlet) {
|
|
92
|
+
throw new Error('Router outlet not found. Ensure <div id="zenith-outlet"></div> exists.');
|
|
93
|
+
}
|
|
94
|
+
// Mount compiled HTML
|
|
95
|
+
outlet.innerHTML = cacheEntry.html;
|
|
96
|
+
// Inject styles
|
|
97
|
+
injectStyles(cacheEntry.styles);
|
|
98
|
+
// Execute JS runtime (compiled expressions + hydration)
|
|
99
|
+
await executeRouteRuntime(cacheEntry.js, {
|
|
100
|
+
loaderData: options.loaderData || {},
|
|
101
|
+
props: options.props || {},
|
|
102
|
+
stores: options.stores || {}
|
|
103
|
+
});
|
|
104
|
+
// Update browser history
|
|
105
|
+
if (typeof window !== 'undefined') {
|
|
106
|
+
const url = normalizedPath + (window.location.search || '');
|
|
107
|
+
if (options.replace) {
|
|
108
|
+
window.history.replaceState({ route: normalizedPath }, '', url);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
window.history.pushState({ route: normalizedPath }, '', url);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
currentRoute = normalizedPath;
|
|
115
|
+
// Dispatch navigation event
|
|
116
|
+
dispatchNavigationEvent(normalizedPath, options);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error('[Zenith] Navigation error:', error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
navigationInProgress = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Cleanup previous route
|
|
128
|
+
*/
|
|
129
|
+
function cleanupPreviousRoute() {
|
|
130
|
+
if (typeof window === 'undefined')
|
|
131
|
+
return;
|
|
132
|
+
// Cleanup hydration runtime
|
|
133
|
+
if (window.zenithCleanup) {
|
|
134
|
+
;
|
|
135
|
+
window.zenithCleanup();
|
|
136
|
+
}
|
|
137
|
+
// Remove previous page styles
|
|
138
|
+
document.querySelectorAll('style[data-zen-route-style]').forEach(style => {
|
|
139
|
+
style.remove();
|
|
140
|
+
});
|
|
141
|
+
// Clear window state (if needed)
|
|
142
|
+
// State is managed per-route, so we don't clear it here
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get router outlet element
|
|
146
|
+
*/
|
|
147
|
+
function getRouterOutlet() {
|
|
148
|
+
if (typeof window === 'undefined')
|
|
149
|
+
return null;
|
|
150
|
+
return document.querySelector('#zenith-outlet') || document.querySelector('[data-zen-outlet]');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Inject route styles
|
|
154
|
+
*/
|
|
155
|
+
function injectStyles(styles) {
|
|
156
|
+
if (typeof window === 'undefined')
|
|
157
|
+
return;
|
|
158
|
+
styles.forEach((styleContent, index) => {
|
|
159
|
+
const style = document.createElement('style');
|
|
160
|
+
style.setAttribute('data-zen-route-style', String(index));
|
|
161
|
+
style.textContent = styleContent;
|
|
162
|
+
document.head.appendChild(style);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Execute route runtime JS
|
|
167
|
+
*
|
|
168
|
+
* This executes the compiled JS bundle for the route, which includes:
|
|
169
|
+
* - Expression wrappers
|
|
170
|
+
* - Hydration runtime
|
|
171
|
+
* - Event bindings
|
|
172
|
+
*/
|
|
173
|
+
async function executeRouteRuntime(jsCode, data) {
|
|
174
|
+
if (typeof window === 'undefined')
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
// Execute the compiled JS (which registers expressions and hydration functions)
|
|
178
|
+
// In a real implementation, this would use a script tag or eval (secure context)
|
|
179
|
+
const script = document.createElement('script');
|
|
180
|
+
script.textContent = jsCode;
|
|
181
|
+
document.head.appendChild(script);
|
|
182
|
+
document.head.removeChild(script);
|
|
183
|
+
// After JS executes, call hydrate with explicit data
|
|
184
|
+
if (window.zenithHydrate) {
|
|
185
|
+
const state = window.__ZENITH_STATE__ || {};
|
|
186
|
+
window.zenithHydrate(state, data.loaderData, data.props, data.stores, getRouterOutlet());
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
console.error('[Zenith] Error executing route runtime:', error);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Dispatch navigation event
|
|
196
|
+
*/
|
|
197
|
+
function dispatchNavigationEvent(routePath, options) {
|
|
198
|
+
if (typeof window === 'undefined')
|
|
199
|
+
return;
|
|
200
|
+
const event = new CustomEvent('zenith:navigate', {
|
|
201
|
+
detail: {
|
|
202
|
+
route: routePath,
|
|
203
|
+
loaderData: options.loaderData,
|
|
204
|
+
props: options.props,
|
|
205
|
+
stores: options.stores
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
window.dispatchEvent(event);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Handle browser back/forward navigation
|
|
212
|
+
*/
|
|
213
|
+
export function setupHistoryHandling() {
|
|
214
|
+
if (typeof window === 'undefined')
|
|
215
|
+
return;
|
|
216
|
+
window.addEventListener('popstate', (event) => {
|
|
217
|
+
const state = event.state;
|
|
218
|
+
const routePath = state?.route || window.location.pathname;
|
|
219
|
+
// Navigate without pushing to history (browser already changed it)
|
|
220
|
+
navigate(routePath, { replace: true, prefetch: false }).catch((error) => {
|
|
221
|
+
console.error('[Zenith] History navigation error:', error);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Generate navigation runtime code (to be included in bundle)
|
|
227
|
+
*/
|
|
228
|
+
export function generateNavigationRuntime() {
|
|
229
|
+
return `
|
|
230
|
+
// Zenith Navigation Runtime (Phase 7)
|
|
231
|
+
(function() {
|
|
232
|
+
'use strict';
|
|
233
|
+
|
|
234
|
+
// Route cache
|
|
235
|
+
const __zen_routeCache = new Map();
|
|
236
|
+
|
|
237
|
+
// Current route state
|
|
238
|
+
let __zen_currentRoute = '';
|
|
239
|
+
let __zen_navigationInProgress = false;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Prefetch a route
|
|
243
|
+
*/
|
|
244
|
+
async function prefetchRoute(routePath) {
|
|
245
|
+
const normalizedPath = routePath === '' ? '/' : routePath;
|
|
246
|
+
|
|
247
|
+
if (__zen_routeCache.has(normalizedPath)) {
|
|
248
|
+
return Promise.resolve();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// Fetch compiled HTML + JS
|
|
253
|
+
// This is a placeholder - in production, fetch from build output
|
|
254
|
+
const cacheEntry = {
|
|
255
|
+
html: '<!-- Prefetched: ' + normalizedPath + ' -->',
|
|
256
|
+
js: '// Prefetched runtime: ' + normalizedPath,
|
|
257
|
+
styles: [],
|
|
258
|
+
routePath: normalizedPath,
|
|
259
|
+
compiledAt: Date.now()
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
__zen_routeCache.set(normalizedPath, cacheEntry);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.warn('[Zenith] Prefetch failed:', routePath, error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Navigate to route with explicit data
|
|
271
|
+
*/
|
|
272
|
+
async function navigate(routePath, options) {
|
|
273
|
+
options = options || {};
|
|
274
|
+
|
|
275
|
+
if (__zen_navigationInProgress) {
|
|
276
|
+
console.warn('[Zenith] Navigation in progress');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
__zen_navigationInProgress = true;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const normalizedPath = routePath === '' ? '/' : routePath;
|
|
284
|
+
|
|
285
|
+
// Get cached route or prefetch
|
|
286
|
+
let cacheEntry = __zen_routeCache.get(normalizedPath);
|
|
287
|
+
if (!cacheEntry && options.prefetch !== false) {
|
|
288
|
+
await prefetchRoute(normalizedPath);
|
|
289
|
+
cacheEntry = __zen_routeCache.get(normalizedPath);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!cacheEntry) {
|
|
293
|
+
throw new Error('Route not found: ' + normalizedPath);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Get outlet
|
|
297
|
+
const outlet = document.querySelector('#zenith-outlet') || document.querySelector('[data-zen-outlet]');
|
|
298
|
+
if (!outlet) {
|
|
299
|
+
throw new Error('Router outlet not found');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Mount HTML
|
|
303
|
+
outlet.innerHTML = cacheEntry.html;
|
|
304
|
+
|
|
305
|
+
// Execute runtime JS
|
|
306
|
+
if (cacheEntry.js) {
|
|
307
|
+
const script = document.createElement('script');
|
|
308
|
+
script.textContent = cacheEntry.js;
|
|
309
|
+
document.head.appendChild(script);
|
|
310
|
+
document.head.removeChild(script);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Hydrate with explicit data
|
|
314
|
+
if (window.zenithHydrate) {
|
|
315
|
+
const state = window.__ZENITH_STATE__ || {};
|
|
316
|
+
window.zenithHydrate(
|
|
317
|
+
state,
|
|
318
|
+
options.loaderData || {},
|
|
319
|
+
options.props || {},
|
|
320
|
+
options.stores || {},
|
|
321
|
+
outlet
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Update history
|
|
326
|
+
const url = normalizedPath + (window.location.search || '');
|
|
327
|
+
if (options.replace) {
|
|
328
|
+
window.history.replaceState({ route: normalizedPath }, '', url);
|
|
329
|
+
} else {
|
|
330
|
+
window.history.pushState({ route: normalizedPath }, '', url);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
__zen_currentRoute = normalizedPath;
|
|
334
|
+
|
|
335
|
+
// Dispatch event
|
|
336
|
+
window.dispatchEvent(new CustomEvent('zenith:navigate', {
|
|
337
|
+
detail: { route: normalizedPath, options: options }
|
|
338
|
+
}));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error('[Zenith] Navigation error:', error);
|
|
341
|
+
throw error;
|
|
342
|
+
} finally {
|
|
343
|
+
__zen_navigationInProgress = false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Handle browser history
|
|
349
|
+
*/
|
|
350
|
+
function setupHistoryHandling() {
|
|
351
|
+
window.addEventListener('popstate', function(event) {
|
|
352
|
+
const state = event.state;
|
|
353
|
+
const routePath = state && state.route ? state.route : window.location.pathname;
|
|
354
|
+
|
|
355
|
+
navigate(routePath, { replace: true, prefetch: false }).catch(function(error) {
|
|
356
|
+
console.error('[Zenith] History navigation error:', error);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Initialize history handling
|
|
362
|
+
setupHistoryHandling();
|
|
363
|
+
|
|
364
|
+
// Expose API
|
|
365
|
+
if (typeof window !== 'undefined') {
|
|
366
|
+
window.__zenith_navigate = navigate;
|
|
367
|
+
window.__zenith_prefetch = prefetchRoute;
|
|
368
|
+
window.navigate = navigate; // Global convenience
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
`;
|
|
372
|
+
}
|