@zenithbuild/router 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/dist/match.js ADDED
@@ -0,0 +1,191 @@
1
+ // ---------------------------------------------------------------------------
2
+ // match.js — Zenith Router V0
3
+ // ---------------------------------------------------------------------------
4
+ // Deterministic path matching engine.
5
+ //
6
+ // Algorithm:
7
+ // 1. Split pathname and route path by '/'
8
+ // 2. Walk segments:
9
+ // - ':param' → extract one segment into params object
10
+ // - '*slug' → extract remaining segments (must be terminal, 1+ segments,
11
+ // except root catch-all '/*slug' which allows 0+)
12
+ // - '*slug?' → optional catch-all (must be terminal, 0+ segments)
13
+ // - literal → exact string comparison
14
+ // 3. Deterministic precedence: static > :param > *catchall
15
+ //
16
+ // No regex.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * @typedef {{ path: string, load: Function }} RouteEntry
21
+ * @typedef {{ route: RouteEntry, params: Record<string, string> }} MatchResult
22
+ */
23
+
24
+ /**
25
+ * Match a pathname against a single route definition.
26
+ *
27
+ * @param {string} routePath - The route pattern (e.g. '/users/:id')
28
+ * @param {string} pathname - The actual URL path (e.g. '/users/42')
29
+ * @returns {{ matched: boolean, params: Record<string, string> }}
30
+ */
31
+ export function matchPath(routePath, pathname) {
32
+ const routeSegments = _splitPath(routePath);
33
+ const pathSegments = _splitPath(pathname);
34
+
35
+ const params = {};
36
+ let routeIndex = 0;
37
+ let pathIndex = 0;
38
+
39
+ while (routeIndex < routeSegments.length) {
40
+ const routeSeg = routeSegments[routeIndex];
41
+ if (routeSeg.startsWith('*')) {
42
+ // Catch-all must be terminal.
43
+ const optionalCatchAll = routeSeg.endsWith('?');
44
+ const paramName = optionalCatchAll
45
+ ? routeSeg.slice(1, -1)
46
+ : routeSeg.slice(1);
47
+ if (routeIndex !== routeSegments.length - 1) {
48
+ return { matched: false, params: {} };
49
+ }
50
+ const rest = pathSegments.slice(pathIndex);
51
+ const rootRequiredCatchAll = !optionalCatchAll && routeSegments.length === 1;
52
+ if (rest.length === 0 && !optionalCatchAll && !rootRequiredCatchAll) {
53
+ return { matched: false, params: {} };
54
+ }
55
+ params[paramName] = _normalizeCatchAll(rest);
56
+ pathIndex = pathSegments.length;
57
+ routeIndex = routeSegments.length;
58
+ break;
59
+ }
60
+
61
+ if (pathIndex >= pathSegments.length) {
62
+ return { matched: false, params: {} };
63
+ }
64
+
65
+ const pathSeg = pathSegments[pathIndex];
66
+ if (routeSeg.startsWith(':')) {
67
+ // Dynamic param — extract value as string
68
+ const paramName = routeSeg.slice(1);
69
+ params[paramName] = pathSeg;
70
+ } else if (routeSeg !== pathSeg) {
71
+ // Literal mismatch
72
+ return { matched: false, params: {} };
73
+ }
74
+
75
+ routeIndex += 1;
76
+ pathIndex += 1;
77
+ }
78
+
79
+ if (routeIndex !== routeSegments.length || pathIndex !== pathSegments.length) {
80
+ return { matched: false, params: {} };
81
+ }
82
+
83
+ return { matched: true, params };
84
+ }
85
+
86
+ /**
87
+ * Match a pathname against an ordered array of route definitions.
88
+ * Returns the first match (deterministic, first-match-wins).
89
+ *
90
+ * @param {RouteEntry[]} routes - Ordered route manifest
91
+ * @param {string} pathname - The URL path to match
92
+ * @returns {MatchResult | null}
93
+ */
94
+ export function matchRoute(routes, pathname) {
95
+ const ordered = [...routes].sort((a, b) => _compareRouteSpecificity(a.path, b.path));
96
+ for (let i = 0; i < ordered.length; i++) {
97
+ const route = ordered[i];
98
+ const result = matchPath(route.path, pathname);
99
+
100
+ if (result.matched) {
101
+ return { route, params: result.params };
102
+ }
103
+ }
104
+
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * Split a path string into non-empty segments.
110
+ *
111
+ * @param {string} path
112
+ * @returns {string[]}
113
+ */
114
+ function _splitPath(path) {
115
+ return path.split('/').filter(Boolean);
116
+ }
117
+
118
+ /**
119
+ * Catch-all params are normalized as slash-joined, non-empty path segments.
120
+ * Segments keep raw URL-encoded bytes (no decodeURIComponent).
121
+ *
122
+ * @param {string[]} segments
123
+ * @returns {string}
124
+ */
125
+ function _normalizeCatchAll(segments) {
126
+ return segments.filter(Boolean).join('/');
127
+ }
128
+
129
+ /**
130
+ * @param {string} a
131
+ * @param {string} b
132
+ * @returns {number}
133
+ */
134
+ function _compareRouteSpecificity(a, b) {
135
+ if (a === '/' && b !== '/') return -1;
136
+ if (b === '/' && a !== '/') return 1;
137
+
138
+ const aSegs = _splitPath(a);
139
+ const bSegs = _splitPath(b);
140
+ const aClass = _routeClass(aSegs);
141
+ const bClass = _routeClass(bSegs);
142
+ if (aClass !== bClass) {
143
+ return bClass - aClass;
144
+ }
145
+
146
+ const max = Math.min(aSegs.length, bSegs.length);
147
+
148
+ for (let i = 0; i < max; i++) {
149
+ const aWeight = _segmentWeight(aSegs[i]);
150
+ const bWeight = _segmentWeight(bSegs[i]);
151
+ if (aWeight !== bWeight) {
152
+ return bWeight - aWeight;
153
+ }
154
+ }
155
+
156
+ if (aSegs.length !== bSegs.length) {
157
+ return bSegs.length - aSegs.length;
158
+ }
159
+
160
+ return a.localeCompare(b);
161
+ }
162
+
163
+ /**
164
+ * @param {string[]} segments
165
+ * @returns {number}
166
+ */
167
+ function _routeClass(segments) {
168
+ let hasParam = false;
169
+ let hasCatchAll = false;
170
+ for (const segment of segments) {
171
+ if (segment.startsWith('*')) {
172
+ hasCatchAll = true;
173
+ } else if (segment.startsWith(':')) {
174
+ hasParam = true;
175
+ }
176
+ }
177
+ if (!hasParam && !hasCatchAll) return 3;
178
+ if (hasCatchAll) return 1;
179
+ return 2;
180
+ }
181
+
182
+ /**
183
+ * @param {string | undefined} segment
184
+ * @returns {number}
185
+ */
186
+ function _segmentWeight(segment) {
187
+ if (!segment) return 0;
188
+ if (segment.startsWith('*')) return 1;
189
+ if (segment.startsWith(':')) return 2;
190
+ return 3;
191
+ }
@@ -0,0 +1,67 @@
1
+ // ---------------------------------------------------------------------------
2
+ // navigate.js — Zenith Router V0
3
+ // ---------------------------------------------------------------------------
4
+ // Navigation API.
5
+ //
6
+ // - navigate(path) → push to history, trigger route change
7
+ // - back() → history.back()
8
+ // - forward() → history.forward()
9
+ // - getCurrentPath() → current pathname
10
+ //
11
+ // The navigate function accepts a resolver callback so the router
12
+ // can wire match → mount logic without circular dependencies.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ import { push, current } from './history.js';
16
+ import { _dispatchRouteChange } from './events.js';
17
+
18
+ /** @type {((path: string) => Promise<void>) | null} */
19
+ let _resolveNavigation = null;
20
+
21
+ /**
22
+ * Wire the navigation resolver.
23
+ * Called once by createRouter() to bind match → mount logic.
24
+ *
25
+ * @param {(path: string) => Promise<void>} resolver
26
+ */
27
+ export function _setNavigationResolver(resolver) {
28
+ _resolveNavigation = resolver;
29
+ }
30
+
31
+ /**
32
+ * Navigate to a path.
33
+ * Pushes history, then resolves through the router pipeline.
34
+ *
35
+ * @param {string} path
36
+ * @returns {Promise<void>}
37
+ */
38
+ export async function navigate(path) {
39
+ push(path);
40
+
41
+ if (_resolveNavigation) {
42
+ await _resolveNavigation(path);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Go back in history.
48
+ */
49
+ export function back() {
50
+ history.back();
51
+ }
52
+
53
+ /**
54
+ * Go forward in history.
55
+ */
56
+ export function forward() {
57
+ history.forward();
58
+ }
59
+
60
+ /**
61
+ * Get the current pathname.
62
+ *
63
+ * @returns {string}
64
+ */
65
+ export function getCurrentPath() {
66
+ return current();
67
+ }
@@ -0,0 +1,59 @@
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;
59
+ //# sourceMappingURL=client-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-router.d.ts","sourceRoot":"","sources":["../../src/navigation/client-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B,UAAU,CAAC,EAAE,GAAG,CAAA;IAChB,KAAK,CAAC,EAAE,GAAG,CAAA;IACX,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAaD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BpE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAGxE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,eAAoB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAgEf;AAoGD;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAY3C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAgJlD"}
@@ -0,0 +1,373 @@
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
+ }
373
+ //# sourceMappingURL=client-router.js.map