eleva 1.0.0-alpha → 1.0.0-rc.10

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.
Files changed (68) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +554 -137
  3. package/dist/eleva-plugins.cjs.js +3397 -0
  4. package/dist/eleva-plugins.cjs.js.map +1 -0
  5. package/dist/eleva-plugins.esm.js +3392 -0
  6. package/dist/eleva-plugins.esm.js.map +1 -0
  7. package/dist/eleva-plugins.umd.js +3403 -0
  8. package/dist/eleva-plugins.umd.js.map +1 -0
  9. package/dist/eleva-plugins.umd.min.js +3 -0
  10. package/dist/eleva-plugins.umd.min.js.map +1 -0
  11. package/dist/eleva.cjs.js +1448 -0
  12. package/dist/eleva.cjs.js.map +1 -0
  13. package/dist/eleva.d.ts +1057 -80
  14. package/dist/eleva.esm.js +1230 -274
  15. package/dist/eleva.esm.js.map +1 -1
  16. package/dist/eleva.umd.js +1230 -274
  17. package/dist/eleva.umd.js.map +1 -1
  18. package/dist/eleva.umd.min.js +3 -0
  19. package/dist/eleva.umd.min.js.map +1 -0
  20. package/dist/plugins/attr.umd.js +231 -0
  21. package/dist/plugins/attr.umd.js.map +1 -0
  22. package/dist/plugins/attr.umd.min.js +3 -0
  23. package/dist/plugins/attr.umd.min.js.map +1 -0
  24. package/dist/plugins/props.umd.js +711 -0
  25. package/dist/plugins/props.umd.js.map +1 -0
  26. package/dist/plugins/props.umd.min.js +3 -0
  27. package/dist/plugins/props.umd.min.js.map +1 -0
  28. package/dist/plugins/router.umd.js +1807 -0
  29. package/dist/plugins/router.umd.js.map +1 -0
  30. package/dist/plugins/router.umd.min.js +3 -0
  31. package/dist/plugins/router.umd.min.js.map +1 -0
  32. package/dist/plugins/store.umd.js +684 -0
  33. package/dist/plugins/store.umd.js.map +1 -0
  34. package/dist/plugins/store.umd.min.js +3 -0
  35. package/dist/plugins/store.umd.min.js.map +1 -0
  36. package/package.json +240 -62
  37. package/src/core/Eleva.js +552 -145
  38. package/src/modules/Emitter.js +154 -18
  39. package/src/modules/Renderer.js +288 -86
  40. package/src/modules/Signal.js +132 -13
  41. package/src/modules/TemplateEngine.js +153 -27
  42. package/src/plugins/Attr.js +252 -0
  43. package/src/plugins/Props.js +590 -0
  44. package/src/plugins/Router.js +1919 -0
  45. package/src/plugins/Store.js +741 -0
  46. package/src/plugins/index.js +40 -0
  47. package/types/core/Eleva.d.ts +482 -48
  48. package/types/core/Eleva.d.ts.map +1 -1
  49. package/types/modules/Emitter.d.ts +151 -20
  50. package/types/modules/Emitter.d.ts.map +1 -1
  51. package/types/modules/Renderer.d.ts +151 -12
  52. package/types/modules/Renderer.d.ts.map +1 -1
  53. package/types/modules/Signal.d.ts +130 -16
  54. package/types/modules/Signal.d.ts.map +1 -1
  55. package/types/modules/TemplateEngine.d.ts +154 -14
  56. package/types/modules/TemplateEngine.d.ts.map +1 -1
  57. package/types/plugins/Attr.d.ts +28 -0
  58. package/types/plugins/Attr.d.ts.map +1 -0
  59. package/types/plugins/Props.d.ts +48 -0
  60. package/types/plugins/Props.d.ts.map +1 -0
  61. package/types/plugins/Router.d.ts +1000 -0
  62. package/types/plugins/Router.d.ts.map +1 -0
  63. package/types/plugins/Store.d.ts +86 -0
  64. package/types/plugins/Store.d.ts.map +1 -0
  65. package/types/plugins/index.d.ts +5 -0
  66. package/types/plugins/index.d.ts.map +1 -0
  67. package/dist/eleva.min.js +0 -2
  68. package/dist/eleva.min.js.map +0 -1
@@ -0,0 +1,1807 @@
1
+ /*! Eleva Router Plugin v1.0.0-rc.10 | MIT License | https://elevajs.com */
2
+ (function (global, factory) {
3
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
5
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ElevaRouterPlugin = {}));
6
+ })(this, (function (exports) { 'use strict';
7
+
8
+ /**
9
+ * @typedef {import('eleva').Eleva} Eleva
10
+ * @typedef {import('eleva').Signal} Signal
11
+ * @typedef {import('eleva').ComponentDefinition} ComponentDefinition
12
+ * @typedef {import('eleva').Emitter} Emitter
13
+ * @typedef {import('eleva').MountResult} MountResult
14
+ */
15
+
16
+ // ============================================
17
+ // Core Type Definitions
18
+ // ============================================
19
+
20
+ /**
21
+ * @typedef {'hash' | 'history' | 'query'} RouterMode
22
+ * The routing mode determines how the router manages URL state.
23
+ * - `hash`: Uses URL hash (e.g., `/#/path`) - works without server config
24
+ * - `history`: Uses HTML5 History API (e.g., `/path`) - requires server config
25
+ * - `query`: Uses query parameters (e.g., `?view=/path`) - useful for embedded apps
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} RouterOptions
30
+ * @property {RouterMode} [mode='hash'] - The routing mode to use.
31
+ * @property {string} [queryParam='view'] - Query parameter name for 'query' mode.
32
+ * @property {string} [viewSelector='root'] - Selector for the view container element.
33
+ * @property {string} mount - CSS selector for the mount point element.
34
+ * @property {RouteDefinition[]} routes - Array of route definitions.
35
+ * @property {string | ComponentDefinition} [globalLayout] - Default layout for all routes.
36
+ * @property {NavigationGuard} [onBeforeEach] - Global navigation guard.
37
+ * @description Configuration options for the Router plugin.
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} NavigationTarget
42
+ * @property {string} path - The target path (can include params like '/users/:id').
43
+ * @property {Record<string, string>} [params] - Route parameters to inject into the path.
44
+ * @property {Record<string, string>} [query] - Query parameters to append.
45
+ * @property {boolean} [replace=false] - Whether to replace current history entry.
46
+ * @property {Record<string, any>} [state] - State object to pass to history.
47
+ * @description Object describing a navigation target for `router.navigate()`.
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} ScrollPosition
52
+ * @property {number} x - Horizontal scroll position.
53
+ * @property {number} y - Vertical scroll position.
54
+ * @description Represents a saved scroll position.
55
+ */
56
+
57
+ /**
58
+ * @typedef {Object} RouteSegment
59
+ * @property {'static' | 'param'} type - The segment type.
60
+ * @property {string} value - The segment value (for static) or empty string (for param).
61
+ * @property {string} [name] - The parameter name (for param segments).
62
+ * @description Internal representation of a parsed route path segment.
63
+ * @private
64
+ */
65
+
66
+ /**
67
+ * @typedef {Object} RouteMatch
68
+ * @property {RouteDefinition} route - The matched route definition.
69
+ * @property {Record<string, string>} params - The extracted route parameters.
70
+ * @description Result of matching a path against route definitions.
71
+ * @private
72
+ */
73
+
74
+ /**
75
+ * @typedef {Record<string, any>} RouteMeta
76
+ * @description Arbitrary metadata attached to routes for use in guards and components.
77
+ * Common properties include:
78
+ * - `requiresAuth: boolean` - Whether the route requires authentication
79
+ * - `title: string` - Page title for the route
80
+ * - `roles: string[]` - Required user roles
81
+ * @example
82
+ * {
83
+ * path: '/admin',
84
+ * component: AdminPage,
85
+ * meta: { requiresAuth: true, roles: ['admin'], title: 'Admin Dashboard' }
86
+ * }
87
+ */
88
+
89
+ /**
90
+ * @typedef {Object} RouterErrorHandler
91
+ * @property {(error: Error, context: string, details?: Record<string, any>) => void} handle - Throws a formatted error.
92
+ * @property {(message: string, details?: Record<string, any>) => void} warn - Logs a warning.
93
+ * @property {(message: string, error: Error, details?: Record<string, any>) => void} log - Logs an error without throwing.
94
+ * @description Interface for the router's error handling system.
95
+ */
96
+
97
+ // ============================================
98
+ // Event Callback Type Definitions
99
+ // ============================================
100
+
101
+ /**
102
+ * @callback NavigationContextCallback
103
+ * @param {NavigationContext} context - The navigation context (can be modified to block/redirect).
104
+ * @returns {void | Promise<void>}
105
+ * @description Callback for `router:beforeEach` event. Modify context to control navigation.
106
+ */
107
+
108
+ /**
109
+ * @callback ResolveContextCallback
110
+ * @param {ResolveContext} context - The resolve context (can be modified to block/redirect).
111
+ * @returns {void | Promise<void>}
112
+ * @description Callback for `router:beforeResolve` and `router:afterResolve` events.
113
+ */
114
+
115
+ /**
116
+ * @callback RenderContextCallback
117
+ * @param {RenderContext} context - The render context.
118
+ * @returns {void | Promise<void>}
119
+ * @description Callback for `router:beforeRender` and `router:afterRender` events.
120
+ */
121
+
122
+ /**
123
+ * @callback ScrollContextCallback
124
+ * @param {ScrollContext} context - The scroll context with saved position info.
125
+ * @returns {void | Promise<void>}
126
+ * @description Callback for `router:scroll` event. Use to implement scroll behavior.
127
+ */
128
+
129
+ /**
130
+ * @callback RouteChangeCallback
131
+ * @param {RouteLocation} to - The target route location.
132
+ * @param {RouteLocation | null} from - The source route location.
133
+ * @returns {void | Promise<void>}
134
+ * @description Callback for `router:afterEnter`, `router:afterLeave`, `router:afterEach` events.
135
+ */
136
+
137
+ /**
138
+ * @callback RouterErrorCallback
139
+ * @param {Error} error - The error that occurred.
140
+ * @param {RouteLocation} [to] - The target route (if available).
141
+ * @param {RouteLocation | null} [from] - The source route (if available).
142
+ * @returns {void | Promise<void>}
143
+ * @description Callback for `router:onError` event.
144
+ */
145
+
146
+ /**
147
+ * @callback RouterReadyCallback
148
+ * @param {Router} router - The router instance.
149
+ * @returns {void | Promise<void>}
150
+ * @description Callback for `router:ready` event.
151
+ */
152
+
153
+ /**
154
+ * @callback RouteAddedCallback
155
+ * @param {RouteDefinition} route - The added route definition.
156
+ * @returns {void | Promise<void>}
157
+ * @description Callback for `router:routeAdded` event.
158
+ */
159
+
160
+ /**
161
+ * @callback RouteRemovedCallback
162
+ * @param {RouteDefinition} route - The removed route definition.
163
+ * @returns {void | Promise<void>}
164
+ * @description Callback for `router:routeRemoved` event.
165
+ */
166
+
167
+ // ============================================
168
+ // Core Type Definitions (continued)
169
+ // ============================================
170
+
171
+ /**
172
+ * Simple error handler for the core router.
173
+ * Can be overridden by error handling plugins.
174
+ * Provides consistent error formatting and logging for router operations.
175
+ * @private
176
+ */
177
+ const CoreErrorHandler = {
178
+ /**
179
+ * Handles router errors with basic formatting.
180
+ * @param {Error} error - The error to handle.
181
+ * @param {string} context - The context where the error occurred.
182
+ * @param {Object} details - Additional error details.
183
+ * @throws {Error} The formatted error.
184
+ */
185
+ handle(error, context, details = {}) {
186
+ const message = `[ElevaRouter] ${context}: ${error.message}`;
187
+ const formattedError = new Error(message);
188
+
189
+ // Preserve original error details
190
+ formattedError.originalError = error;
191
+ formattedError.context = context;
192
+ formattedError.details = details;
193
+ console.error(message, {
194
+ error,
195
+ context,
196
+ details
197
+ });
198
+ throw formattedError;
199
+ },
200
+ /**
201
+ * Logs a warning without throwing an error.
202
+ * @param {string} message - The warning message.
203
+ * @param {Object} details - Additional warning details.
204
+ */
205
+ warn(message, details = {}) {
206
+ console.warn(`[ElevaRouter] ${message}`, details);
207
+ },
208
+ /**
209
+ * Logs an error without throwing.
210
+ * @param {string} message - The error message.
211
+ * @param {Error} error - The original error.
212
+ * @param {Object} details - Additional error details.
213
+ */
214
+ log(message, error, details = {}) {
215
+ console.error(`[ElevaRouter] ${message}`, {
216
+ error,
217
+ details
218
+ });
219
+ }
220
+ };
221
+
222
+ /**
223
+ * @typedef {Object} RouteLocation
224
+ * @property {string} path - The path of the route (e.g., '/users/123').
225
+ * @property {Record<string, string>} query - Query parameters as key-value pairs.
226
+ * @property {string} fullUrl - The complete URL including hash, path, and query string.
227
+ * @property {Record<string, string>} params - Dynamic route parameters (e.g., `{ id: '123' }`).
228
+ * @property {RouteMeta} meta - Metadata associated with the matched route.
229
+ * @property {string} [name] - The optional name of the matched route.
230
+ * @property {RouteDefinition} matched - The raw route definition object that was matched.
231
+ * @description Represents the current or target location in the router.
232
+ */
233
+
234
+ /**
235
+ * @typedef {boolean | string | NavigationTarget | void} NavigationGuardResult
236
+ * The return value of a navigation guard.
237
+ * - `true` or `undefined/void`: Allow navigation
238
+ * - `false`: Abort navigation
239
+ * - `string`: Redirect to path
240
+ * - `NavigationTarget`: Redirect with options
241
+ */
242
+
243
+ /**
244
+ * @callback NavigationGuard
245
+ * @param {RouteLocation} to - The target route location.
246
+ * @param {RouteLocation | null} from - The source route location (null on initial navigation).
247
+ * @returns {NavigationGuardResult | Promise<NavigationGuardResult>}
248
+ * @description A function that controls navigation flow. Runs before navigation is confirmed.
249
+ * @example
250
+ * // Simple auth guard
251
+ * const authGuard = (to, from) => {
252
+ * if (to.meta.requiresAuth && !isLoggedIn()) {
253
+ * return '/login'; // Redirect
254
+ * }
255
+ * // Allow navigation (implicit return undefined)
256
+ * };
257
+ */
258
+
259
+ /**
260
+ * @callback NavigationHook
261
+ * @param {RouteLocation} to - The target route location.
262
+ * @param {RouteLocation | null} from - The source route location.
263
+ * @returns {void | Promise<void>}
264
+ * @description A lifecycle hook for side effects. Does not affect navigation flow.
265
+ * @example
266
+ * // Analytics hook
267
+ * const analyticsHook = (to, from) => {
268
+ * analytics.trackPageView(to.path);
269
+ * };
270
+ */
271
+
272
+ /**
273
+ * @typedef {Object} RouterPlugin
274
+ * @property {string} name - Unique plugin identifier.
275
+ * @property {string} [version] - Plugin version (recommended to match router version).
276
+ * @property {(router: Router, options?: Record<string, any>) => void} install - Installation function.
277
+ * @property {(router: Router) => void | Promise<void>} [destroy] - Cleanup function called on router.destroy().
278
+ * @description Interface for router plugins. Plugins can extend router functionality.
279
+ * @example
280
+ * const AnalyticsPlugin = {
281
+ * name: 'analytics',
282
+ * version: '1.0.0',
283
+ * install(router, options) {
284
+ * router.emitter.on('router:afterEach', (to, from) => {
285
+ * analytics.track(to.path);
286
+ * });
287
+ * }
288
+ * };
289
+ */
290
+
291
+ /**
292
+ * @typedef {Object} NavigationContext
293
+ * @property {RouteLocation} to - The target route location.
294
+ * @property {RouteLocation | null} from - The source route location.
295
+ * @property {boolean} cancelled - Whether navigation has been cancelled.
296
+ * @property {string | {path: string} | null} redirectTo - Redirect target if navigation should redirect.
297
+ * @description A context object passed to navigation events that plugins can modify to control navigation flow.
298
+ */
299
+
300
+ /**
301
+ * @typedef {Object} ResolveContext
302
+ * @property {RouteLocation} to - The target route location.
303
+ * @property {RouteLocation | null} from - The source route location.
304
+ * @property {RouteDefinition} route - The matched route definition.
305
+ * @property {ComponentDefinition | null} layoutComponent - The resolved layout component (available in afterResolve).
306
+ * @property {ComponentDefinition | null} pageComponent - The resolved page component (available in afterResolve).
307
+ * @property {boolean} cancelled - Whether navigation has been cancelled.
308
+ * @property {string | {path: string} | null} redirectTo - Redirect target if navigation should redirect.
309
+ * @description A context object passed to component resolution events.
310
+ */
311
+
312
+ /**
313
+ * @typedef {Object} RenderContext
314
+ * @property {RouteLocation} to - The target route location.
315
+ * @property {RouteLocation | null} from - The source route location.
316
+ * @property {ComponentDefinition | null} layoutComponent - The layout component being rendered.
317
+ * @property {ComponentDefinition} pageComponent - The page component being rendered.
318
+ * @description A context object passed to render events.
319
+ */
320
+
321
+ /**
322
+ * @typedef {Object} ScrollContext
323
+ * @property {RouteLocation} to - The target route location.
324
+ * @property {RouteLocation | null} from - The source route location.
325
+ * @property {{x: number, y: number} | null} savedPosition - The saved scroll position (if navigating via back/forward).
326
+ * @description A context object passed to scroll events for plugins to handle scroll behavior.
327
+ */
328
+
329
+ /**
330
+ * @typedef {string | ComponentDefinition | (() => Promise<{default: ComponentDefinition}>)} RouteComponent
331
+ * A component that can be rendered for a route.
332
+ * - `string`: Name of a registered component
333
+ * - `ComponentDefinition`: Inline component definition
334
+ * - `() => Promise<{default: ComponentDefinition}>`: Lazy-loaded component (e.g., `() => import('./Page.js')`)
335
+ */
336
+
337
+ /**
338
+ * @typedef {Object} RouteDefinition
339
+ * @property {string} path - URL path pattern. Supports:
340
+ * - Static: `'/about'`
341
+ * - Dynamic params: `'/users/:id'`
342
+ * - Wildcard: `'*'` (catch-all, must be last)
343
+ * @property {RouteComponent} component - The component to render for this route.
344
+ * @property {RouteComponent} [layout] - Optional layout component to wrap the route component.
345
+ * @property {string} [name] - Optional route name for programmatic navigation.
346
+ * @property {RouteMeta} [meta] - Optional metadata (auth flags, titles, etc.).
347
+ * @property {NavigationGuard} [beforeEnter] - Route-specific guard before entering.
348
+ * @property {NavigationHook} [afterEnter] - Hook after entering and component is mounted.
349
+ * @property {NavigationGuard} [beforeLeave] - Guard before leaving this route.
350
+ * @property {NavigationHook} [afterLeave] - Hook after leaving and component is unmounted.
351
+ * @property {RouteSegment[]} [segments] - Internal: parsed path segments (added by router).
352
+ * @description Defines a route in the application.
353
+ * @example
354
+ * // Static route
355
+ * { path: '/about', component: AboutPage }
356
+ *
357
+ * // Dynamic route with params
358
+ * { path: '/users/:id', component: UserPage, meta: { requiresAuth: true } }
359
+ *
360
+ * // Lazy-loaded route with layout
361
+ * {
362
+ * path: '/dashboard',
363
+ * component: () => import('./Dashboard.js'),
364
+ * layout: DashboardLayout,
365
+ * beforeEnter: (to, from) => isLoggedIn() || '/login'
366
+ * }
367
+ *
368
+ * // Catch-all 404 route (must be last)
369
+ * { path: '*', component: NotFoundPage }
370
+ */
371
+
372
+ /**
373
+ * @class Router
374
+ * @classdesc A powerful, reactive, and flexible Router Plugin for Eleva.js.
375
+ * This class manages all routing logic, including state, navigation, and rendering.
376
+ *
377
+ * ## Features
378
+ * - Multiple routing modes (hash, history, query)
379
+ * - Reactive route state via Signals
380
+ * - Navigation guards and lifecycle hooks
381
+ * - Lazy-loaded components
382
+ * - Layout system
383
+ * - Plugin architecture
384
+ * - Scroll position management
385
+ *
386
+ * ## Events Reference
387
+ * | Event | Callback Type | Can Block | Description |
388
+ * |-------|--------------|-----------|-------------|
389
+ * | `router:ready` | {@link RouterReadyCallback} | No | Router initialized |
390
+ * | `router:beforeEach` | {@link NavigationContextCallback} | Yes | Before guards run |
391
+ * | `router:beforeResolve` | {@link ResolveContextCallback} | Yes | Before component loading |
392
+ * | `router:afterResolve` | {@link ResolveContextCallback} | No | After components loaded |
393
+ * | `router:afterLeave` | {@link RouteChangeCallback} | No | After leaving route |
394
+ * | `router:beforeRender` | {@link RenderContextCallback} | No | Before DOM update |
395
+ * | `router:afterRender` | {@link RenderContextCallback} | No | After DOM update |
396
+ * | `router:scroll` | {@link ScrollContextCallback} | No | For scroll behavior |
397
+ * | `router:afterEnter` | {@link RouteChangeCallback} | No | After entering route |
398
+ * | `router:afterEach` | {@link RouteChangeCallback} | No | Navigation complete |
399
+ * | `router:onError` | {@link RouterErrorCallback} | No | Navigation error |
400
+ * | `router:routeAdded` | {@link RouteAddedCallback} | No | Dynamic route added |
401
+ * | `router:routeRemoved` | {@link RouteRemovedCallback} | No | Dynamic route removed |
402
+ *
403
+ * ## Reactive Signals
404
+ * - `currentRoute: Signal<RouteLocation | null>` - Current route info
405
+ * - `previousRoute: Signal<RouteLocation | null>` - Previous route info
406
+ * - `currentParams: Signal<Record<string, string>>` - Current route params
407
+ * - `currentQuery: Signal<Record<string, string>>` - Current query params
408
+ * - `currentLayout: Signal<MountResult | null>` - Mounted layout instance
409
+ * - `currentView: Signal<MountResult | null>` - Mounted view instance
410
+ * - `isReady: Signal<boolean>` - Router readiness state
411
+ *
412
+ * @note Internal API Access Policy:
413
+ * As a core Eleva plugin, the Router may access internal Eleva APIs (prefixed with _)
414
+ * such as `eleva._components`. This is intentional and these internal APIs are
415
+ * considered stable for official plugins. Third-party plugins should avoid
416
+ * accessing internal APIs as they may change without notice.
417
+ *
418
+ * @example
419
+ * // Basic setup
420
+ * const router = new Router(eleva, {
421
+ * mode: 'hash',
422
+ * mount: '#app',
423
+ * routes: [
424
+ * { path: '/', component: HomePage },
425
+ * { path: '/users/:id', component: UserPage },
426
+ * { path: '*', component: NotFoundPage }
427
+ * ]
428
+ * });
429
+ *
430
+ * // Start router
431
+ * await router.start();
432
+ *
433
+ * // Navigate programmatically
434
+ * const success = await router.navigate('/users/123');
435
+ *
436
+ * // Watch for route changes
437
+ * router.currentRoute.watch((route) => {
438
+ * document.title = route?.meta?.title || 'My App';
439
+ * });
440
+ *
441
+ * @private
442
+ */
443
+ class Router {
444
+ /**
445
+ * Creates an instance of the Router.
446
+ * @param {Eleva} eleva - The Eleva framework instance.
447
+ * @param {RouterOptions} options - The configuration options for the router.
448
+ */
449
+ constructor(eleva, options = {}) {
450
+ /** @type {Eleva} The Eleva framework instance. */
451
+ this.eleva = eleva;
452
+
453
+ /** @type {RouterOptions} The merged router options. */
454
+ this.options = {
455
+ mode: "hash",
456
+ queryParam: "view",
457
+ viewSelector: "root",
458
+ ...options
459
+ };
460
+
461
+ /** @private @type {RouteDefinition[]} The processed list of route definitions. */
462
+ this.routes = this._processRoutes(options.routes || []);
463
+
464
+ /** @private @type {import('eleva').Emitter} The shared Eleva event emitter for global hooks. */
465
+ this.emitter = this.eleva.emitter;
466
+
467
+ /** @private @type {boolean} A flag indicating if the router has been started. */
468
+ this.isStarted = false;
469
+
470
+ /** @private @type {boolean} A flag to prevent navigation loops from history events. */
471
+ this._isNavigating = false;
472
+
473
+ /** @private @type {number} Counter for tracking navigation operations to prevent race conditions. */
474
+ this._navigationId = 0;
475
+
476
+ /** @private @type {Array<() => void>} A collection of cleanup functions for event listeners. */
477
+ this.eventListeners = [];
478
+
479
+ /** @type {Signal<RouteLocation | null>} A reactive signal holding the current route's information. */
480
+ this.currentRoute = new this.eleva.signal(null);
481
+
482
+ /** @type {Signal<RouteLocation | null>} A reactive signal holding the previous route's information. */
483
+ this.previousRoute = new this.eleva.signal(null);
484
+
485
+ /** @type {Signal<Object<string, string>>} A reactive signal holding the current route's parameters. */
486
+ this.currentParams = new this.eleva.signal({});
487
+
488
+ /** @type {Signal<Object<string, string>>} A reactive signal holding the current route's query parameters. */
489
+ this.currentQuery = new this.eleva.signal({});
490
+
491
+ /** @type {Signal<import('eleva').MountResult | null>} A reactive signal for the currently mounted layout instance. */
492
+ this.currentLayout = new this.eleva.signal(null);
493
+
494
+ /** @type {Signal<import('eleva').MountResult | null>} A reactive signal for the currently mounted view (page) instance. */
495
+ this.currentView = new this.eleva.signal(null);
496
+
497
+ /** @type {Signal<boolean>} A reactive signal indicating if the router is ready (started and initial navigation complete). */
498
+ this.isReady = new this.eleva.signal(false);
499
+
500
+ /** @private @type {Map<string, RouterPlugin>} Map of registered plugins by name. */
501
+ this.plugins = new Map();
502
+
503
+ /** @private @type {Array<NavigationGuard>} Array of global before-each navigation guards. */
504
+ this._beforeEachGuards = [];
505
+
506
+ // If onBeforeEach was provided in options, add it to the guards array
507
+ if (options.onBeforeEach) {
508
+ this._beforeEachGuards.push(options.onBeforeEach);
509
+ }
510
+
511
+ /** @type {Object} The error handler instance. Can be overridden by plugins. */
512
+ this.errorHandler = CoreErrorHandler;
513
+
514
+ /** @private @type {Map<string, {x: number, y: number}>} Saved scroll positions by route path. */
515
+ this._scrollPositions = new Map();
516
+ this._validateOptions();
517
+ }
518
+
519
+ /**
520
+ * Validates the provided router options.
521
+ * @private
522
+ * @throws {Error} If the routing mode is invalid.
523
+ */
524
+ _validateOptions() {
525
+ if (!["hash", "query", "history"].includes(this.options.mode)) {
526
+ this.errorHandler.handle(new Error(`Invalid routing mode: ${this.options.mode}. Must be "hash", "query", or "history".`), "Configuration validation failed");
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Pre-processes route definitions to parse their path segments for efficient matching.
532
+ * @private
533
+ * @param {RouteDefinition[]} routes - The raw route definitions.
534
+ * @returns {RouteDefinition[]} The processed routes.
535
+ */
536
+ _processRoutes(routes) {
537
+ const processedRoutes = [];
538
+ for (const route of routes) {
539
+ try {
540
+ processedRoutes.push({
541
+ ...route,
542
+ segments: this._parsePathIntoSegments(route.path)
543
+ });
544
+ } catch (error) {
545
+ this.errorHandler.warn(`Invalid path in route definition "${route.path || "undefined"}": ${error.message}`, {
546
+ route,
547
+ error
548
+ });
549
+ }
550
+ }
551
+ return processedRoutes;
552
+ }
553
+
554
+ /**
555
+ * Parses a route path string into an array of static and parameter segments.
556
+ * @private
557
+ * @param {string} path - The path pattern to parse.
558
+ * @returns {Array<{type: 'static' | 'param', value?: string, name?: string}>} An array of segment objects.
559
+ * @throws {Error} If the route path is not a valid string.
560
+ */
561
+ _parsePathIntoSegments(path) {
562
+ if (!path || typeof path !== "string") {
563
+ this.errorHandler.handle(new Error("Route path must be a non-empty string"), "Path parsing failed", {
564
+ path
565
+ });
566
+ }
567
+ const normalizedPath = path.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
568
+ if (normalizedPath === "/") {
569
+ return [];
570
+ }
571
+ return normalizedPath.split("/").filter(Boolean).map(segment => {
572
+ if (segment.startsWith(":")) {
573
+ const paramName = segment.substring(1);
574
+ if (!paramName) {
575
+ this.errorHandler.handle(new Error(`Invalid parameter segment: ${segment}`), "Path parsing failed", {
576
+ segment,
577
+ path
578
+ });
579
+ }
580
+ return {
581
+ type: "param",
582
+ name: paramName
583
+ };
584
+ }
585
+ return {
586
+ type: "static",
587
+ value: segment
588
+ };
589
+ });
590
+ }
591
+
592
+ /**
593
+ * Finds the view element within a container using multiple selector strategies.
594
+ * @private
595
+ * @param {HTMLElement} container - The parent element to search within.
596
+ * @returns {HTMLElement} The found view element or the container itself as a fallback.
597
+ */
598
+ _findViewElement(container) {
599
+ const selector = this.options.viewSelector;
600
+ return container.querySelector(`#${selector}`) || container.querySelector(`.${selector}`) || container.querySelector(`[data-${selector}]`) || container.querySelector(selector) || container;
601
+ }
602
+
603
+ /**
604
+ * Starts the router, initializes event listeners, and performs the initial navigation.
605
+ * @returns {Promise<Router>} The router instance for method chaining.
606
+ *
607
+ * @example
608
+ * // Basic usage
609
+ * await router.start();
610
+ *
611
+ * // Method chaining
612
+ * await router.start().then(r => r.navigate('/home'));
613
+ *
614
+ * // Reactive readiness
615
+ * router.isReady.watch((ready) => {
616
+ * if (ready) console.log('Router is ready!');
617
+ * });
618
+ */
619
+ async start() {
620
+ if (this.isStarted) {
621
+ this.errorHandler.warn("Router is already started");
622
+ return this;
623
+ }
624
+ if (typeof window === "undefined") {
625
+ this.errorHandler.warn("Router start skipped: `window` object not available (SSR environment)");
626
+ return this;
627
+ }
628
+ if (typeof document !== "undefined" && !document.querySelector(this.options.mount)) {
629
+ this.errorHandler.warn(`Mount element "${this.options.mount}" was not found in the DOM. The router will not start.`, {
630
+ mountSelector: this.options.mount
631
+ });
632
+ return this;
633
+ }
634
+ const handler = () => this._handleRouteChange();
635
+ if (this.options.mode === "hash") {
636
+ window.addEventListener("hashchange", handler);
637
+ this.eventListeners.push(() => window.removeEventListener("hashchange", handler));
638
+ } else {
639
+ window.addEventListener("popstate", handler);
640
+ this.eventListeners.push(() => window.removeEventListener("popstate", handler));
641
+ }
642
+ this.isStarted = true;
643
+ // Initial navigation is not a popstate event
644
+ await this._handleRouteChange(false);
645
+ // Set isReady to true after initial navigation completes
646
+ this.isReady.value = true;
647
+ await this.emitter.emit("router:ready", this);
648
+ return this;
649
+ }
650
+
651
+ /**
652
+ * Stops the router and cleans up all event listeners and mounted components.
653
+ * @returns {Promise<void>}
654
+ */
655
+ async destroy() {
656
+ if (!this.isStarted) return;
657
+
658
+ // Clean up plugins
659
+ for (const plugin of this.plugins.values()) {
660
+ if (typeof plugin.destroy === "function") {
661
+ try {
662
+ await plugin.destroy(this);
663
+ } catch (error) {
664
+ this.errorHandler.log(`Plugin ${plugin.name} destroy failed`, error);
665
+ }
666
+ }
667
+ }
668
+ this.eventListeners.forEach(cleanup => cleanup());
669
+ this.eventListeners = [];
670
+ if (this.currentLayout.value) {
671
+ await this.currentLayout.value.unmount();
672
+ }
673
+ this.isStarted = false;
674
+ this.isReady.value = false;
675
+ }
676
+
677
+ /**
678
+ * Alias for destroy(). Stops the router and cleans up all resources.
679
+ * Provided for semantic consistency (start/stop pattern).
680
+ * @returns {Promise<void>}
681
+ *
682
+ * @example
683
+ * await router.start();
684
+ * // ... later
685
+ * await router.stop();
686
+ */
687
+ async stop() {
688
+ return this.destroy();
689
+ }
690
+
691
+ /**
692
+ * Programmatically navigates to a new route.
693
+ * @param {string | NavigationTarget} location - The target location as a path string or navigation target object.
694
+ * @param {Record<string, string>} [params] - Route parameters (only used when location is a string).
695
+ * @returns {Promise<boolean>} True if navigation succeeded, false if blocked by guards or failed.
696
+ *
697
+ * @example
698
+ * // Basic navigation
699
+ * await router.navigate('/users/123');
700
+ *
701
+ * // Check if navigation succeeded
702
+ * const success = await router.navigate('/protected');
703
+ * if (!success) {
704
+ * console.log('Navigation was blocked by a guard');
705
+ * }
706
+ *
707
+ * // Navigate with options
708
+ * await router.navigate({
709
+ * path: '/users/:id',
710
+ * params: { id: '123' },
711
+ * query: { tab: 'profile' },
712
+ * replace: true
713
+ * });
714
+ */
715
+ async navigate(location, params = {}) {
716
+ try {
717
+ const target = typeof location === "string" ? {
718
+ path: location,
719
+ params
720
+ } : location;
721
+ let path = this._buildPath(target.path, target.params || {});
722
+ const query = target.query || {};
723
+ if (Object.keys(query).length > 0) {
724
+ const queryString = new URLSearchParams(query).toString();
725
+ if (queryString) path += `?${queryString}`;
726
+ }
727
+ if (this._isSameRoute(path, target.params, query)) {
728
+ return true; // Already at this route, consider it successful
729
+ }
730
+ const navigationSuccessful = await this._proceedWithNavigation(path);
731
+ if (navigationSuccessful) {
732
+ // Increment navigation ID and capture it for this navigation
733
+ const currentNavId = ++this._navigationId;
734
+ this._isNavigating = true;
735
+ const state = target.state || {};
736
+ const replace = target.replace || false;
737
+ const historyMethod = replace ? "replaceState" : "pushState";
738
+ if (this.options.mode === "hash") {
739
+ if (replace) {
740
+ const newUrl = `${window.location.pathname}${window.location.search}#${path}`;
741
+ window.history.replaceState(state, "", newUrl);
742
+ } else {
743
+ window.location.hash = path;
744
+ }
745
+ } else {
746
+ const url = this.options.mode === "query" ? this._buildQueryUrl(path) : path;
747
+ history[historyMethod](state, "", url);
748
+ }
749
+
750
+ // Only reset the flag if no newer navigation has started
751
+ queueMicrotask(() => {
752
+ if (this._navigationId === currentNavId) {
753
+ this._isNavigating = false;
754
+ }
755
+ });
756
+ }
757
+ return navigationSuccessful;
758
+ } catch (error) {
759
+ this.errorHandler.log("Navigation failed", error);
760
+ await this.emitter.emit("router:onError", error);
761
+ return false;
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Builds a URL for query mode.
767
+ * @private
768
+ * @param {string} path - The path to set as the query parameter.
769
+ * @returns {string} The full URL with the updated query string.
770
+ */
771
+ _buildQueryUrl(path) {
772
+ const urlParams = new URLSearchParams(window.location.search);
773
+ urlParams.set(this.options.queryParam, path.split("?")[0]);
774
+ return `${window.location.pathname}?${urlParams.toString()}`;
775
+ }
776
+
777
+ /**
778
+ * Checks if the target route is identical to the current route.
779
+ * @private
780
+ * @param {string} path - The target path with query string.
781
+ * @param {object} params - The target params.
782
+ * @param {object} query - The target query.
783
+ * @returns {boolean} - True if the routes are the same.
784
+ */
785
+ _isSameRoute(path, params, query) {
786
+ const current = this.currentRoute.value;
787
+ if (!current) return false;
788
+ const [targetPath, queryString] = path.split("?");
789
+ const targetQuery = query || this._parseQuery(queryString || "");
790
+ return current.path === targetPath && JSON.stringify(current.params) === JSON.stringify(params || {}) && JSON.stringify(current.query) === JSON.stringify(targetQuery);
791
+ }
792
+
793
+ /**
794
+ * Injects dynamic parameters into a path string.
795
+ * @private
796
+ */
797
+ _buildPath(path, params) {
798
+ let result = path;
799
+ for (const [key, value] of Object.entries(params)) {
800
+ // Fix: Handle special characters and ensure proper encoding
801
+ const encodedValue = encodeURIComponent(String(value));
802
+ result = result.replace(new RegExp(`:${key}\\b`, "g"), encodedValue);
803
+ }
804
+ return result;
805
+ }
806
+
807
+ /**
808
+ * The handler for browser-initiated route changes (e.g., back/forward buttons).
809
+ * @private
810
+ * @param {boolean} [isPopState=true] - Whether this is a popstate event (back/forward navigation).
811
+ */
812
+ async _handleRouteChange(isPopState = true) {
813
+ if (this._isNavigating) return;
814
+ try {
815
+ const from = this.currentRoute.value;
816
+ const toLocation = this._getCurrentLocation();
817
+ const navigationSuccessful = await this._proceedWithNavigation(toLocation.fullUrl, isPopState);
818
+
819
+ // If navigation was blocked by a guard, revert the URL change
820
+ if (!navigationSuccessful && from) {
821
+ this.navigate({
822
+ path: from.path,
823
+ query: from.query,
824
+ replace: true
825
+ });
826
+ }
827
+ } catch (error) {
828
+ this.errorHandler.log("Route change handling failed", error, {
829
+ currentUrl: typeof window !== "undefined" ? window.location.href : ""
830
+ });
831
+ await this.emitter.emit("router:onError", error);
832
+ }
833
+ }
834
+
835
+ /**
836
+ * Manages the core navigation lifecycle. Runs guards before committing changes.
837
+ * Emits lifecycle events that plugins can hook into:
838
+ * - router:beforeEach - Before guards run (can block/redirect via context)
839
+ * - router:beforeResolve - Before component resolution (can block/redirect)
840
+ * - router:afterResolve - After components are resolved
841
+ * - router:beforeRender - Before DOM rendering
842
+ * - router:afterRender - After DOM rendering
843
+ * - router:scroll - After render, for scroll behavior
844
+ * - router:afterEnter - After entering a route
845
+ * - router:afterLeave - After leaving a route
846
+ * - router:afterEach - After navigation completes
847
+ *
848
+ * @private
849
+ * @param {string} fullPath - The full path (e.g., '/users/123?foo=bar') to navigate to.
850
+ * @param {boolean} [isPopState=false] - Whether this navigation was triggered by popstate (back/forward).
851
+ * @returns {Promise<boolean>} - `true` if navigation succeeded, `false` if aborted.
852
+ */
853
+ async _proceedWithNavigation(fullPath, isPopState = false) {
854
+ const from = this.currentRoute.value;
855
+ const [path, queryString] = (fullPath || "/").split("?");
856
+ const toLocation = {
857
+ path: path.startsWith("/") ? path : `/${path}`,
858
+ query: this._parseQuery(queryString),
859
+ fullUrl: fullPath
860
+ };
861
+ let toMatch = this._matchRoute(toLocation.path);
862
+ if (!toMatch) {
863
+ const notFoundRoute = this.routes.find(route => route.path === "*");
864
+ if (notFoundRoute) {
865
+ toMatch = {
866
+ route: notFoundRoute,
867
+ params: {
868
+ pathMatch: decodeURIComponent(toLocation.path.substring(1))
869
+ }
870
+ };
871
+ } else {
872
+ await this.emitter.emit("router:onError", new Error(`Route not found: ${toLocation.path}`), toLocation, from);
873
+ return false;
874
+ }
875
+ }
876
+ const to = {
877
+ ...toLocation,
878
+ params: toMatch.params,
879
+ meta: toMatch.route.meta || {},
880
+ name: toMatch.route.name,
881
+ matched: toMatch.route
882
+ };
883
+ try {
884
+ // 1. Run all *pre-navigation* guards.
885
+ const canNavigate = await this._runGuards(to, from, toMatch.route);
886
+ if (!canNavigate) return false;
887
+
888
+ // 2. Save current scroll position before navigating away
889
+ if (from && typeof window !== "undefined") {
890
+ this._scrollPositions.set(from.path, {
891
+ x: window.scrollX || window.pageXOffset || 0,
892
+ y: window.scrollY || window.pageYOffset || 0
893
+ });
894
+ }
895
+
896
+ // 3. Emit beforeResolve event - plugins can show loading indicators
897
+ /** @type {ResolveContext} */
898
+ const resolveContext = {
899
+ to,
900
+ from,
901
+ route: toMatch.route,
902
+ layoutComponent: null,
903
+ pageComponent: null,
904
+ cancelled: false,
905
+ redirectTo: null
906
+ };
907
+ await this.emitter.emit("router:beforeResolve", resolveContext);
908
+
909
+ // Check if resolution was cancelled or redirected
910
+ if (resolveContext.cancelled) return false;
911
+ if (resolveContext.redirectTo) {
912
+ this.navigate(resolveContext.redirectTo);
913
+ return false;
914
+ }
915
+
916
+ // 4. Resolve async components *before* touching the DOM.
917
+ const {
918
+ layoutComponent,
919
+ pageComponent
920
+ } = await this._resolveComponents(toMatch.route);
921
+
922
+ // 5. Emit afterResolve event - plugins can hide loading indicators
923
+ resolveContext.layoutComponent = layoutComponent;
924
+ resolveContext.pageComponent = pageComponent;
925
+ await this.emitter.emit("router:afterResolve", resolveContext);
926
+
927
+ // 6. Unmount the previous view/layout.
928
+ if (from) {
929
+ const toLayout = toMatch.route.layout || this.options.globalLayout;
930
+ const fromLayout = from.matched.layout || this.options.globalLayout;
931
+ const tryUnmount = async instance => {
932
+ if (!instance) return;
933
+ try {
934
+ await instance.unmount();
935
+ } catch (error) {
936
+ this.errorHandler.warn("Error during component unmount", {
937
+ error,
938
+ instance
939
+ });
940
+ }
941
+ };
942
+ if (toLayout !== fromLayout) {
943
+ await tryUnmount(this.currentLayout.value);
944
+ this.currentLayout.value = null;
945
+ } else {
946
+ await tryUnmount(this.currentView.value);
947
+ this.currentView.value = null;
948
+ }
949
+
950
+ // Call `afterLeave` hook *after* the old component has been unmounted.
951
+ if (from.matched.afterLeave) {
952
+ await from.matched.afterLeave(to, from);
953
+ }
954
+ await this.emitter.emit("router:afterLeave", to, from);
955
+ }
956
+
957
+ // 7. Update reactive state.
958
+ this.previousRoute.value = from;
959
+ this.currentRoute.value = to;
960
+ this.currentParams.value = to.params || {};
961
+ this.currentQuery.value = to.query || {};
962
+
963
+ // 8. Emit beforeRender event - plugins can add transitions
964
+ /** @type {RenderContext} */
965
+ const renderContext = {
966
+ to,
967
+ from,
968
+ layoutComponent,
969
+ pageComponent
970
+ };
971
+ await this.emitter.emit("router:beforeRender", renderContext);
972
+
973
+ // 9. Render the new components.
974
+ await this._render(layoutComponent, pageComponent, to);
975
+
976
+ // 10. Emit afterRender event - plugins can trigger animations
977
+ await this.emitter.emit("router:afterRender", renderContext);
978
+
979
+ // 11. Emit scroll event - plugins can handle scroll restoration
980
+ /** @type {ScrollContext} */
981
+ const scrollContext = {
982
+ to,
983
+ from,
984
+ savedPosition: isPopState ? this._scrollPositions.get(to.path) || null : null
985
+ };
986
+ await this.emitter.emit("router:scroll", scrollContext);
987
+
988
+ // 12. Run post-navigation hooks.
989
+ if (toMatch.route.afterEnter) {
990
+ await toMatch.route.afterEnter(to, from);
991
+ }
992
+ await this.emitter.emit("router:afterEnter", to, from);
993
+ await this.emitter.emit("router:afterEach", to, from);
994
+ return true;
995
+ } catch (error) {
996
+ this.errorHandler.log("Error during navigation", error, {
997
+ to,
998
+ from
999
+ });
1000
+ await this.emitter.emit("router:onError", error, to, from);
1001
+ return false;
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * Executes all applicable navigation guards for a transition in order.
1007
+ * Guards are executed in the following order:
1008
+ * 1. Global beforeEach event (emitter-based, can block via context)
1009
+ * 2. Global beforeEach guards (registered via onBeforeEach)
1010
+ * 3. Route-specific beforeLeave guard (from the route being left)
1011
+ * 4. Route-specific beforeEnter guard (from the route being entered)
1012
+ *
1013
+ * @private
1014
+ * @param {RouteLocation} to - The target route location.
1015
+ * @param {RouteLocation | null} from - The current route location (null on initial navigation).
1016
+ * @param {RouteDefinition} route - The matched route definition.
1017
+ * @returns {Promise<boolean>} - `false` if navigation should be aborted.
1018
+ */
1019
+ async _runGuards(to, from, route) {
1020
+ // Create navigation context that plugins can modify to block navigation
1021
+ /** @type {NavigationContext} */
1022
+ const navContext = {
1023
+ to,
1024
+ from,
1025
+ cancelled: false,
1026
+ redirectTo: null
1027
+ };
1028
+
1029
+ // Emit beforeEach event with context - plugins can block by modifying context
1030
+ await this.emitter.emit("router:beforeEach", navContext);
1031
+
1032
+ // Check if navigation was cancelled or redirected by event listeners
1033
+ if (navContext.cancelled) return false;
1034
+ if (navContext.redirectTo) {
1035
+ this.navigate(navContext.redirectTo);
1036
+ return false;
1037
+ }
1038
+
1039
+ // Collect all guards in execution order
1040
+ const guards = [...this._beforeEachGuards, ...(from && from.matched.beforeLeave ? [from.matched.beforeLeave] : []), ...(route.beforeEnter ? [route.beforeEnter] : [])];
1041
+ for (const guard of guards) {
1042
+ const result = await guard(to, from);
1043
+ if (result === false) return false;
1044
+ if (typeof result === "string" || typeof result === "object") {
1045
+ this.navigate(result);
1046
+ return false;
1047
+ }
1048
+ }
1049
+ return true;
1050
+ }
1051
+
1052
+ /**
1053
+ * Resolves a string component definition to a component object.
1054
+ * @private
1055
+ * @param {string} def - The component name to resolve.
1056
+ * @returns {ComponentDefinition} The resolved component.
1057
+ * @throws {Error} If the component is not registered.
1058
+ *
1059
+ * @note Core plugins (Router, Attr, Props, Store) may access eleva._components
1060
+ * directly. This is intentional and stable for official Eleva plugins shipped
1061
+ * with the framework. Third-party plugins should use eleva.component() for
1062
+ * registration and avoid direct access to internal APIs.
1063
+ */
1064
+ _resolveStringComponent(def) {
1065
+ const componentDef = this.eleva._components.get(def);
1066
+ if (!componentDef) {
1067
+ this.errorHandler.handle(new Error(`Component "${def}" not registered.`), "Component resolution failed", {
1068
+ componentName: def,
1069
+ availableComponents: Array.from(this.eleva._components.keys())
1070
+ });
1071
+ }
1072
+ return componentDef;
1073
+ }
1074
+
1075
+ /**
1076
+ * Resolves a function component definition to a component object.
1077
+ * @private
1078
+ * @param {Function} def - The function to resolve.
1079
+ * @returns {Promise<ComponentDefinition>} The resolved component.
1080
+ * @throws {Error} If the function fails to load the component.
1081
+ */
1082
+ async _resolveFunctionComponent(def) {
1083
+ try {
1084
+ const funcStr = def.toString();
1085
+ const isAsyncImport = funcStr.includes("import(") || funcStr.startsWith("() =>");
1086
+ const result = await def();
1087
+ return isAsyncImport ? result.default || result : result;
1088
+ } catch (error) {
1089
+ this.errorHandler.handle(new Error(`Failed to load async component: ${error.message}`), "Component resolution failed", {
1090
+ function: def.toString(),
1091
+ error
1092
+ });
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * Validates a component definition object.
1098
+ * @private
1099
+ * @param {any} def - The component definition to validate.
1100
+ * @returns {ComponentDefinition} The validated component.
1101
+ * @throws {Error} If the component definition is invalid.
1102
+ */
1103
+ _validateComponentDefinition(def) {
1104
+ if (!def || typeof def !== "object") {
1105
+ this.errorHandler.handle(new Error(`Invalid component definition: ${typeof def}`), "Component validation failed", {
1106
+ definition: def
1107
+ });
1108
+ }
1109
+ if (typeof def.template !== "function" && typeof def.template !== "string") {
1110
+ this.errorHandler.handle(new Error("Component missing template property"), "Component validation failed", {
1111
+ definition: def
1112
+ });
1113
+ }
1114
+ return def;
1115
+ }
1116
+
1117
+ /**
1118
+ * Resolves a component definition to a component object.
1119
+ * @private
1120
+ * @param {any} def - The component definition to resolve.
1121
+ * @returns {Promise<ComponentDefinition | null>} The resolved component or null.
1122
+ */
1123
+ async _resolveComponent(def) {
1124
+ if (def === null || def === undefined) {
1125
+ return null;
1126
+ }
1127
+ if (typeof def === "string") {
1128
+ return this._resolveStringComponent(def);
1129
+ }
1130
+ if (typeof def === "function") {
1131
+ return await this._resolveFunctionComponent(def);
1132
+ }
1133
+ if (def && typeof def === "object") {
1134
+ return this._validateComponentDefinition(def);
1135
+ }
1136
+ this.errorHandler.handle(new Error(`Invalid component definition: ${typeof def}`), "Component resolution failed", {
1137
+ definition: def
1138
+ });
1139
+ }
1140
+
1141
+ /**
1142
+ * Asynchronously resolves the layout and page components for a route.
1143
+ * @private
1144
+ * @param {RouteDefinition} route - The route to resolve components for.
1145
+ * @returns {Promise<{layoutComponent: ComponentDefinition | null, pageComponent: ComponentDefinition}>}
1146
+ */
1147
+ async _resolveComponents(route) {
1148
+ const effectiveLayout = route.layout || this.options.globalLayout;
1149
+ try {
1150
+ const [layoutComponent, pageComponent] = await Promise.all([this._resolveComponent(effectiveLayout), this._resolveComponent(route.component)]);
1151
+ if (!pageComponent) {
1152
+ this.errorHandler.handle(new Error(`Page component is null or undefined for route: ${route.path}`), "Component resolution failed", {
1153
+ route: route.path
1154
+ });
1155
+ }
1156
+ return {
1157
+ layoutComponent,
1158
+ pageComponent
1159
+ };
1160
+ } catch (error) {
1161
+ this.errorHandler.log(`Error resolving components for route ${route.path}`, error, {
1162
+ route: route.path
1163
+ });
1164
+ throw error;
1165
+ }
1166
+ }
1167
+
1168
+ /**
1169
+ * Renders the components for the current route into the DOM.
1170
+ * @private
1171
+ * @param {ComponentDefinition | null} layoutComponent - The pre-loaded layout component.
1172
+ * @param {ComponentDefinition} pageComponent - The pre-loaded page component.
1173
+ */
1174
+ async _render(layoutComponent, pageComponent) {
1175
+ const mountEl = document.querySelector(this.options.mount);
1176
+ if (!mountEl) {
1177
+ this.errorHandler.handle(new Error(`Mount element "${this.options.mount}" not found.`), {
1178
+ mountSelector: this.options.mount
1179
+ });
1180
+ }
1181
+ if (layoutComponent) {
1182
+ const layoutInstance = await this.eleva.mount(mountEl, this._wrapComponentWithChildren(layoutComponent));
1183
+ this.currentLayout.value = layoutInstance;
1184
+ const viewEl = this._findViewElement(layoutInstance.container);
1185
+ const viewInstance = await this.eleva.mount(viewEl, this._wrapComponentWithChildren(pageComponent));
1186
+ this.currentView.value = viewInstance;
1187
+ } else {
1188
+ const viewInstance = await this.eleva.mount(mountEl, this._wrapComponentWithChildren(pageComponent));
1189
+ this.currentView.value = viewInstance;
1190
+ this.currentLayout.value = null;
1191
+ }
1192
+ }
1193
+
1194
+ /**
1195
+ * Creates a getter function for router context properties.
1196
+ * @private
1197
+ * @param {string} property - The property name to access.
1198
+ * @param {any} defaultValue - The default value if property is undefined.
1199
+ * @returns {Function} A getter function.
1200
+ */
1201
+ _createRouteGetter(property, defaultValue) {
1202
+ return () => this.currentRoute.value?.[property] ?? defaultValue;
1203
+ }
1204
+
1205
+ /**
1206
+ * Wraps a component definition to inject router-specific context into its setup function.
1207
+ * @private
1208
+ * @param {ComponentDefinition} component - The component to wrap.
1209
+ * @returns {ComponentDefinition} The wrapped component definition.
1210
+ */
1211
+ _wrapComponent(component) {
1212
+ const originalSetup = component.setup;
1213
+ const self = this;
1214
+ return {
1215
+ ...component,
1216
+ async setup(ctx) {
1217
+ ctx.router = {
1218
+ navigate: self.navigate.bind(self),
1219
+ current: self.currentRoute,
1220
+ previous: self.previousRoute,
1221
+ // Route property getters
1222
+ get params() {
1223
+ return self._createRouteGetter("params", {})();
1224
+ },
1225
+ get query() {
1226
+ return self._createRouteGetter("query", {})();
1227
+ },
1228
+ get path() {
1229
+ return self._createRouteGetter("path", "/")();
1230
+ },
1231
+ get fullUrl() {
1232
+ return self._createRouteGetter("fullUrl", window.location.href)();
1233
+ },
1234
+ get meta() {
1235
+ return self._createRouteGetter("meta", {})();
1236
+ }
1237
+ };
1238
+ return originalSetup ? await originalSetup(ctx) : {};
1239
+ }
1240
+ };
1241
+ }
1242
+
1243
+ /**
1244
+ * Recursively wraps all child components to ensure they have access to router context.
1245
+ * @private
1246
+ * @param {ComponentDefinition | string} component - The component to wrap (can be a definition object or a registered component name).
1247
+ * @returns {ComponentDefinition | string} The wrapped component definition or the original string reference.
1248
+ */
1249
+ _wrapComponentWithChildren(component) {
1250
+ // If the component is a string (registered component name), return as-is
1251
+ // The router context will be injected when the component is resolved during mounting
1252
+ if (typeof component === "string") {
1253
+ return component;
1254
+ }
1255
+
1256
+ // If not a valid component object, return as-is
1257
+ if (!component || typeof component !== "object") {
1258
+ return component;
1259
+ }
1260
+ const wrappedComponent = this._wrapComponent(component);
1261
+
1262
+ // If the component has children, wrap them too
1263
+ if (wrappedComponent.children && typeof wrappedComponent.children === "object") {
1264
+ const wrappedChildren = {};
1265
+ for (const [selector, childComponent] of Object.entries(wrappedComponent.children)) {
1266
+ wrappedChildren[selector] = this._wrapComponentWithChildren(childComponent);
1267
+ }
1268
+ wrappedComponent.children = wrappedChildren;
1269
+ }
1270
+ return wrappedComponent;
1271
+ }
1272
+
1273
+ /**
1274
+ * Gets the current location information from the browser's window object.
1275
+ * @private
1276
+ * @returns {Omit<RouteLocation, 'params' | 'meta' | 'name' | 'matched'>}
1277
+ */
1278
+ _getCurrentLocation() {
1279
+ if (typeof window === "undefined") return {
1280
+ path: "/",
1281
+ query: {},
1282
+ fullUrl: ""
1283
+ };
1284
+ let path, queryString, fullUrl;
1285
+ switch (this.options.mode) {
1286
+ case "hash":
1287
+ fullUrl = window.location.hash.slice(1) || "/";
1288
+ [path, queryString] = fullUrl.split("?");
1289
+ break;
1290
+ case "query":
1291
+ const urlParams = new URLSearchParams(window.location.search);
1292
+ path = urlParams.get(this.options.queryParam) || "/";
1293
+ queryString = window.location.search.slice(1);
1294
+ fullUrl = path;
1295
+ break;
1296
+ default:
1297
+ // 'history' mode
1298
+ path = window.location.pathname || "/";
1299
+ queryString = window.location.search.slice(1);
1300
+ fullUrl = `${path}${queryString ? "?" + queryString : ""}`;
1301
+ }
1302
+ return {
1303
+ path: path.startsWith("/") ? path : `/${path}`,
1304
+ query: this._parseQuery(queryString),
1305
+ fullUrl
1306
+ };
1307
+ }
1308
+
1309
+ /**
1310
+ * Parses a query string into a key-value object.
1311
+ * @private
1312
+ */
1313
+ _parseQuery(queryString) {
1314
+ const query = {};
1315
+ if (queryString) {
1316
+ new URLSearchParams(queryString).forEach((value, key) => {
1317
+ query[key] = value;
1318
+ });
1319
+ }
1320
+ return query;
1321
+ }
1322
+
1323
+ /**
1324
+ * Matches a given path against the registered routes.
1325
+ * @private
1326
+ * @param {string} path - The path to match.
1327
+ * @returns {{route: RouteDefinition, params: Object<string, string>} | null} The matched route and its params, or null.
1328
+ */
1329
+ _matchRoute(path) {
1330
+ const pathSegments = path.split("/").filter(Boolean);
1331
+ for (const route of this.routes) {
1332
+ // Handle the root path as a special case.
1333
+ if (route.path === "/") {
1334
+ if (pathSegments.length === 0) return {
1335
+ route,
1336
+ params: {}
1337
+ };
1338
+ continue;
1339
+ }
1340
+ if (route.segments.length !== pathSegments.length) continue;
1341
+ const params = {};
1342
+ let isMatch = true;
1343
+ for (let i = 0; i < route.segments.length; i++) {
1344
+ const routeSegment = route.segments[i];
1345
+ const pathSegment = pathSegments[i];
1346
+ if (routeSegment.type === "param") {
1347
+ params[routeSegment.name] = decodeURIComponent(pathSegment);
1348
+ } else if (routeSegment.value !== pathSegment) {
1349
+ isMatch = false;
1350
+ break;
1351
+ }
1352
+ }
1353
+ if (isMatch) return {
1354
+ route,
1355
+ params
1356
+ };
1357
+ }
1358
+ return null;
1359
+ }
1360
+
1361
+ // ============================================
1362
+ // Dynamic Route Management API
1363
+ // ============================================
1364
+
1365
+ /**
1366
+ * Adds a new route dynamically at runtime.
1367
+ * The route will be processed and available for navigation immediately.
1368
+ *
1369
+ * @param {RouteDefinition} route - The route definition to add.
1370
+ * @param {RouteDefinition} [parentRoute] - Optional parent route to add as a child (not yet implemented).
1371
+ * @returns {() => void} A function to remove the added route.
1372
+ *
1373
+ * @example
1374
+ * // Add a route dynamically
1375
+ * const removeRoute = router.addRoute({
1376
+ * path: '/dynamic',
1377
+ * component: DynamicPage,
1378
+ * meta: { title: 'Dynamic Page' }
1379
+ * });
1380
+ *
1381
+ * // Later, remove the route
1382
+ * removeRoute();
1383
+ */
1384
+ addRoute(route, parentRoute = null) {
1385
+ if (!route || !route.path) {
1386
+ this.errorHandler.warn("Invalid route definition: missing path", {
1387
+ route
1388
+ });
1389
+ return () => {};
1390
+ }
1391
+
1392
+ // Check if route already exists
1393
+ if (this.hasRoute(route.path)) {
1394
+ this.errorHandler.warn(`Route "${route.path}" already exists`, {
1395
+ route
1396
+ });
1397
+ return () => {};
1398
+ }
1399
+
1400
+ // Process the route (parse segments)
1401
+ const processedRoute = {
1402
+ ...route,
1403
+ segments: this._parsePathIntoSegments(route.path)
1404
+ };
1405
+
1406
+ // Add to routes array (before wildcard if exists)
1407
+ const wildcardIndex = this.routes.findIndex(r => r.path === "*");
1408
+ if (wildcardIndex !== -1) {
1409
+ this.routes.splice(wildcardIndex, 0, processedRoute);
1410
+ } else {
1411
+ this.routes.push(processedRoute);
1412
+ }
1413
+
1414
+ // Emit event for plugins
1415
+ this.emitter.emit("router:routeAdded", processedRoute);
1416
+
1417
+ // Return removal function
1418
+ return () => this.removeRoute(route.path);
1419
+ }
1420
+
1421
+ /**
1422
+ * Removes a route by its path.
1423
+ *
1424
+ * @param {string} path - The path of the route to remove.
1425
+ * @returns {boolean} True if the route was removed, false if not found.
1426
+ *
1427
+ * @example
1428
+ * router.removeRoute('/dynamic');
1429
+ */
1430
+ removeRoute(path) {
1431
+ const index = this.routes.findIndex(r => r.path === path);
1432
+ if (index === -1) {
1433
+ return false;
1434
+ }
1435
+ const [removedRoute] = this.routes.splice(index, 1);
1436
+
1437
+ // Emit event for plugins
1438
+ this.emitter.emit("router:routeRemoved", removedRoute);
1439
+ return true;
1440
+ }
1441
+
1442
+ /**
1443
+ * Checks if a route with the given path exists.
1444
+ *
1445
+ * @param {string} path - The path to check.
1446
+ * @returns {boolean} True if the route exists.
1447
+ *
1448
+ * @example
1449
+ * if (router.hasRoute('/users/:id')) {
1450
+ * console.log('User route exists');
1451
+ * }
1452
+ */
1453
+ hasRoute(path) {
1454
+ return this.routes.some(r => r.path === path);
1455
+ }
1456
+
1457
+ /**
1458
+ * Gets all registered routes.
1459
+ *
1460
+ * @returns {RouteDefinition[]} A copy of the routes array.
1461
+ *
1462
+ * @example
1463
+ * const routes = router.getRoutes();
1464
+ * console.log('Available routes:', routes.map(r => r.path));
1465
+ */
1466
+ getRoutes() {
1467
+ return [...this.routes];
1468
+ }
1469
+
1470
+ /**
1471
+ * Gets a route by its path.
1472
+ *
1473
+ * @param {string} path - The path of the route to get.
1474
+ * @returns {RouteDefinition | undefined} The route definition or undefined.
1475
+ *
1476
+ * @example
1477
+ * const route = router.getRoute('/users/:id');
1478
+ * if (route) {
1479
+ * console.log('Route meta:', route.meta);
1480
+ * }
1481
+ */
1482
+ getRoute(path) {
1483
+ return this.routes.find(r => r.path === path);
1484
+ }
1485
+
1486
+ // ============================================
1487
+ // Hook Registration Methods
1488
+ // ============================================
1489
+
1490
+ /**
1491
+ * Registers a global pre-navigation guard.
1492
+ * Multiple guards can be registered and will be executed in order.
1493
+ * Guards can also be registered via the emitter using `router:beforeEach` event.
1494
+ *
1495
+ * @param {NavigationGuard} guard - The guard function to register.
1496
+ * @returns {() => void} A function to unregister the guard.
1497
+ *
1498
+ * @example
1499
+ * // Register a guard
1500
+ * const unregister = router.onBeforeEach((to, from) => {
1501
+ * if (to.meta.requiresAuth && !isAuthenticated()) {
1502
+ * return '/login';
1503
+ * }
1504
+ * });
1505
+ *
1506
+ * // Later, unregister the guard
1507
+ * unregister();
1508
+ */
1509
+ onBeforeEach(guard) {
1510
+ this._beforeEachGuards.push(guard);
1511
+ return () => {
1512
+ const index = this._beforeEachGuards.indexOf(guard);
1513
+ if (index > -1) {
1514
+ this._beforeEachGuards.splice(index, 1);
1515
+ }
1516
+ };
1517
+ }
1518
+ /**
1519
+ * Registers a global hook that runs after a new route component has been mounted.
1520
+ * @param {NavigationHook} hook - The hook function to register.
1521
+ * @returns {() => void} A function to unregister the hook.
1522
+ */
1523
+ onAfterEnter(hook) {
1524
+ return this.emitter.on("router:afterEnter", hook);
1525
+ }
1526
+
1527
+ /**
1528
+ * Registers a global hook that runs after a route component has been unmounted.
1529
+ * @param {NavigationHook} hook - The hook function to register.
1530
+ * @returns {() => void} A function to unregister the hook.
1531
+ */
1532
+ onAfterLeave(hook) {
1533
+ return this.emitter.on("router:afterLeave", hook);
1534
+ }
1535
+
1536
+ /**
1537
+ * Registers a global hook that runs after a navigation has been confirmed and all hooks have completed.
1538
+ * @param {NavigationHook} hook - The hook function to register.
1539
+ * @returns {() => void} A function to unregister the hook.
1540
+ */
1541
+ onAfterEach(hook) {
1542
+ return this.emitter.on("router:afterEach", hook);
1543
+ }
1544
+
1545
+ /**
1546
+ * Registers a global error handler for navigation errors.
1547
+ * @param {(error: Error, to?: RouteLocation, from?: RouteLocation) => void} handler - The error handler function.
1548
+ * @returns {() => void} A function to unregister the handler.
1549
+ */
1550
+ onError(handler) {
1551
+ return this.emitter.on("router:onError", handler);
1552
+ }
1553
+
1554
+ /**
1555
+ * Registers a plugin with the router.
1556
+ * @param {RouterPlugin} plugin - The plugin to register.
1557
+ */
1558
+ use(plugin, options = {}) {
1559
+ if (typeof plugin.install !== "function") {
1560
+ this.errorHandler.handle(new Error("Plugin must have an install method"), "Plugin registration failed", {
1561
+ plugin
1562
+ });
1563
+ }
1564
+
1565
+ // Check if plugin is already registered
1566
+ if (this.plugins.has(plugin.name)) {
1567
+ this.errorHandler.warn(`Plugin "${plugin.name}" is already registered`, {
1568
+ existingPlugin: this.plugins.get(plugin.name)
1569
+ });
1570
+ return;
1571
+ }
1572
+ this.plugins.set(plugin.name, plugin);
1573
+ plugin.install(this, options);
1574
+ }
1575
+
1576
+ /**
1577
+ * Gets all registered plugins.
1578
+ * @returns {RouterPlugin[]} Array of registered plugins.
1579
+ */
1580
+ getPlugins() {
1581
+ return Array.from(this.plugins.values());
1582
+ }
1583
+
1584
+ /**
1585
+ * Gets a plugin by name.
1586
+ * @param {string} name - The plugin name.
1587
+ * @returns {RouterPlugin | undefined} The plugin or undefined.
1588
+ */
1589
+ getPlugin(name) {
1590
+ return this.plugins.get(name);
1591
+ }
1592
+
1593
+ /**
1594
+ * Removes a plugin from the router.
1595
+ * @param {string} name - The plugin name.
1596
+ * @returns {boolean} True if the plugin was removed.
1597
+ */
1598
+ removePlugin(name) {
1599
+ const plugin = this.plugins.get(name);
1600
+ if (!plugin) return false;
1601
+
1602
+ // Call destroy if available
1603
+ if (typeof plugin.destroy === "function") {
1604
+ try {
1605
+ plugin.destroy(this);
1606
+ } catch (error) {
1607
+ this.errorHandler.log(`Plugin ${name} destroy failed`, error);
1608
+ }
1609
+ }
1610
+ return this.plugins.delete(name);
1611
+ }
1612
+
1613
+ /**
1614
+ * Sets a custom error handler. Used by error handling plugins.
1615
+ * @param {Object} errorHandler - The error handler object with handle, warn, and log methods.
1616
+ */
1617
+ setErrorHandler(errorHandler) {
1618
+ if (errorHandler && typeof errorHandler.handle === "function" && typeof errorHandler.warn === "function" && typeof errorHandler.log === "function") {
1619
+ this.errorHandler = errorHandler;
1620
+ } else {
1621
+ console.warn("[ElevaRouter] Invalid error handler provided. Must have handle, warn, and log methods.");
1622
+ }
1623
+ }
1624
+ }
1625
+
1626
+ /**
1627
+ * @typedef {Object} RouterOptions
1628
+ * @property {string} mount - A CSS selector for the main element where the app is mounted.
1629
+ * @property {RouteDefinition[]} routes - An array of route definitions.
1630
+ * @property {'hash' | 'query' | 'history'} [mode='hash'] - The routing mode.
1631
+ * @property {string} [queryParam='page'] - The query parameter to use in 'query' mode.
1632
+ * @property {string} [viewSelector='view'] - The selector for the view element within a layout.
1633
+ * @property {boolean} [autoStart=true] - Whether to start the router automatically.
1634
+ * @property {NavigationGuard} [onBeforeEach] - A global guard executed before every navigation.
1635
+ * @property {string | ComponentDefinition | (() => Promise<{default: ComponentDefinition}>)} [globalLayout] - A global layout for all routes. Can be overridden by a route's specific layout.
1636
+ */
1637
+
1638
+ /**
1639
+ * @class 🚀 RouterPlugin
1640
+ * @classdesc A powerful, reactive, and flexible Router Plugin for Eleva.js applications.
1641
+ * This plugin provides comprehensive client-side routing functionality including:
1642
+ * - Multiple routing modes (hash, history, query)
1643
+ * - Navigation guards and lifecycle hooks
1644
+ * - Reactive state management
1645
+ * - Component resolution and lazy loading
1646
+ * - Layout and page component separation
1647
+ * - Plugin system for extensibility
1648
+ * - Advanced error handling
1649
+ *
1650
+ * @example
1651
+ * // Install the plugin
1652
+ * const app = new Eleva("myApp");
1653
+ *
1654
+ * const HomePage = { template: () => `<h1>Home</h1>` };
1655
+ * const AboutPage = { template: () => `<h1>About Us</h1>` };
1656
+ * const UserPage = {
1657
+ * template: (ctx) => `<h1>User: ${ctx.router.params.id}</h1>`
1658
+ * };
1659
+ *
1660
+ * app.use(RouterPlugin, {
1661
+ * mount: '#app',
1662
+ * mode: 'hash',
1663
+ * routes: [
1664
+ * { path: '/', component: HomePage },
1665
+ * { path: '/about', component: AboutPage },
1666
+ * { path: '/users/:id', component: UserPage }
1667
+ * ]
1668
+ * });
1669
+ */
1670
+ const RouterPlugin = {
1671
+ /**
1672
+ * Unique identifier for the plugin
1673
+ * @type {string}
1674
+ */
1675
+ name: "router",
1676
+ /**
1677
+ * Plugin version
1678
+ * @type {string}
1679
+ */
1680
+ version: "1.0.0-rc.10",
1681
+ /**
1682
+ * Plugin description
1683
+ * @type {string}
1684
+ */
1685
+ description: "Client-side routing for Eleva applications",
1686
+ /**
1687
+ * Installs the RouterPlugin into an Eleva instance.
1688
+ *
1689
+ * @param {Eleva} eleva - The Eleva instance
1690
+ * @param {RouterOptions} options - Router configuration options
1691
+ * @param {string} options.mount - A CSS selector for the main element where the app is mounted
1692
+ * @param {RouteDefinition[]} options.routes - An array of route definitions
1693
+ * @param {'hash' | 'query' | 'history'} [options.mode='hash'] - The routing mode
1694
+ * @param {string} [options.queryParam='page'] - The query parameter to use in 'query' mode
1695
+ * @param {string} [options.viewSelector='view'] - The selector for the view element within a layout
1696
+ * @param {boolean} [options.autoStart=true] - Whether to start the router automatically
1697
+ * @param {NavigationGuard} [options.onBeforeEach] - A global guard executed before every navigation
1698
+ * @param {string | ComponentDefinition | (() => Promise<{default: ComponentDefinition}>)} [options.globalLayout] - A global layout for all routes
1699
+ *
1700
+ * @example
1701
+ * // main.js
1702
+ * import Eleva from './eleva.js';
1703
+ * import { RouterPlugin } from './plugins/RouterPlugin.js';
1704
+ *
1705
+ * const app = new Eleva('myApp');
1706
+ *
1707
+ * const HomePage = { template: () => `<h1>Home</h1>` };
1708
+ * const AboutPage = { template: () => `<h1>About Us</h1>` };
1709
+ *
1710
+ * app.use(RouterPlugin, {
1711
+ * mount: '#app',
1712
+ * routes: [
1713
+ * { path: '/', component: HomePage },
1714
+ * { path: '/about', component: AboutPage }
1715
+ * ]
1716
+ * });
1717
+ */
1718
+ install(eleva, options = {}) {
1719
+ if (!options.mount) {
1720
+ throw new Error("[RouterPlugin] 'mount' option is required");
1721
+ }
1722
+ if (!options.routes || !Array.isArray(options.routes)) {
1723
+ throw new Error("[RouterPlugin] 'routes' option must be an array");
1724
+ }
1725
+
1726
+ /**
1727
+ * Registers a component definition with the Eleva instance.
1728
+ * This method handles both inline component objects and pre-registered component names.
1729
+ *
1730
+ * @param {any} def - The component definition to register
1731
+ * @param {string} type - The type of component for naming (e.g., "Route", "Layout")
1732
+ * @returns {string | null} The registered component name or null if no definition provided
1733
+ */
1734
+ const register = (def, type) => {
1735
+ if (!def) return null;
1736
+ if (typeof def === "object" && def !== null && !def.name) {
1737
+ const name = `Eleva${type}Component_${Math.random().toString(36).slice(2, 11)}`;
1738
+ try {
1739
+ eleva.component(name, def);
1740
+ return name;
1741
+ } catch (error) {
1742
+ throw new Error(`[RouterPlugin] Failed to register ${type} component: ${error.message}`);
1743
+ }
1744
+ }
1745
+ return def;
1746
+ };
1747
+ if (options.globalLayout) {
1748
+ options.globalLayout = register(options.globalLayout, "GlobalLayout");
1749
+ }
1750
+ (options.routes || []).forEach(route => {
1751
+ route.component = register(route.component, "Route");
1752
+ if (route.layout) {
1753
+ route.layout = register(route.layout, "RouteLayout");
1754
+ }
1755
+ });
1756
+ const router = new Router(eleva, options);
1757
+ eleva.router = router;
1758
+ if (options.autoStart !== false) {
1759
+ queueMicrotask(() => router.start());
1760
+ }
1761
+
1762
+ // Add plugin metadata to the Eleva instance
1763
+ if (!eleva.plugins) {
1764
+ eleva.plugins = new Map();
1765
+ }
1766
+ eleva.plugins.set(this.name, {
1767
+ name: this.name,
1768
+ version: this.version,
1769
+ description: this.description,
1770
+ options
1771
+ });
1772
+
1773
+ // Add utility methods for manual router access
1774
+ eleva.navigate = router.navigate.bind(router);
1775
+ eleva.getCurrentRoute = () => router.currentRoute.value;
1776
+ eleva.getRouteParams = () => router.currentParams.value;
1777
+ eleva.getRouteQuery = () => router.currentQuery.value;
1778
+ return router;
1779
+ },
1780
+ /**
1781
+ * Uninstalls the plugin from the Eleva instance
1782
+ *
1783
+ * @param {Eleva} eleva - The Eleva instance
1784
+ */
1785
+ async uninstall(eleva) {
1786
+ if (eleva.router) {
1787
+ await eleva.router.destroy();
1788
+ delete eleva.router;
1789
+ }
1790
+
1791
+ // Remove plugin metadata
1792
+ if (eleva.plugins) {
1793
+ eleva.plugins.delete(this.name);
1794
+ }
1795
+
1796
+ // Remove utility methods
1797
+ delete eleva.navigate;
1798
+ delete eleva.getCurrentRoute;
1799
+ delete eleva.getRouteParams;
1800
+ delete eleva.getRouteQuery;
1801
+ }
1802
+ };
1803
+
1804
+ exports.RouterPlugin = RouterPlugin;
1805
+
1806
+ }));
1807
+ //# sourceMappingURL=router.umd.js.map