/** * Enhanced Router * * Provides enhanced routing with guards, middleware, lazy loading, and analytics. */ import { Logger } from '../../core/logger.js'; import { RouteGuard, BuiltInGuards } from './RouteGuard.js'; import { RouteMiddleware, BuiltInMiddleware } from './RouteMiddleware.js'; /** * Router - Enhanced routing system */ export class Router { constructor(config = {}) { this.config = { mode: config.mode || 'history', // 'history' | 'hash' base: config.base || '/', enableAnalytics: config.enableAnalytics ?? true, ...config }; this.routes = new Map(); this.guards = new Map(); this.middleware = []; this.beforeEachHooks = []; this.afterEachHooks = []; this.currentRoute = null; this.analytics = { navigations: [], errors: [] }; // Initialize this.init(); } /** * Create a new Router instance */ static create(config = {}) { return new Router(config); } /** * Initialize router */ init() { // Handle browser navigation window.addEventListener('popstate', (event) => { this.handlePopState(event); }); Logger.info('[Router] Initialized', { mode: this.config.mode, base: this.config.base }); } /** * Register a route */ route(path, config) { const route = { path: this.normalizePath(path), component: config.component, name: config.name || path, title: config.title || null, meta: config.meta || {}, guards: config.guards || [], middleware: config.middleware || [], lazy: config.lazy ?? false, ...config }; this.routes.set(route.path, route); Logger.debug('[Router] Route registered', route); return this; } /** * Register multiple routes */ routes(routesConfig) { routesConfig.forEach(route => { this.route(route.path, route); }); return this; } /** * Register a guard */ guard(name, guardFn) { const guard = RouteGuard.create(name, guardFn); this.guards.set(name, guard); return this; } /** * Register middleware */ use(middleware) { if (typeof middleware === 'function') { this.middleware.push(RouteMiddleware.create('anonymous', middleware)); } else if (middleware instanceof RouteMiddleware) { this.middleware.push(middleware); } else if (typeof middleware === 'string' && BuiltInMiddleware[middleware]) { this.middleware.push(BuiltInMiddleware[middleware]); } return this; } /** * Add beforeEach hook */ beforeEach(hook) { this.beforeEachHooks.push(hook); return this; } /** * Add afterEach hook */ afterEach(hook) { this.afterEachHooks.push(hook); return this; } /** * Navigate to a route */ async navigate(path, options = {}) { const normalizedPath = this.normalizePath(path); const route = this.routes.get(normalizedPath); if (!route) { Logger.warn('[Router] Route not found', normalizedPath); return false; } const from = this.currentRoute; const to = { path: normalizedPath, name: route.name, title: route.title, meta: route.meta, component: route.component }; // Execute beforeEach hooks for (const hook of this.beforeEachHooks) { const result = await hook(to, from); if (result === false || typeof result === 'string') { if (typeof result === 'string') { return await this.navigate(result); } return false; } } // Execute guards for (const guardName of route.guards) { const guard = this.guards.get(guardName) || BuiltInGuards[guardName]; if (!guard) { Logger.warn('[Router] Guard not found', guardName); continue; } const result = await guard.execute(to, from); if (!result.allowed) { if (result.redirect) { return await this.navigate(result.redirect); } Logger.warn('[Router] Navigation blocked by guard', guardName); return false; } } // Execute middleware let middlewareBlocked = false; for (const middleware of [...this.middleware, ...route.middleware]) { await new Promise((resolve) => { middleware.execute(to, from, (allowed) => { if (allowed === false) { middlewareBlocked = true; } resolve(); }); }); if (middlewareBlocked) { return false; } } // Load component if lazy if (route.lazy && typeof route.component === 'function') { try { route.component = await route.component(); } catch (error) { Logger.error('[Router] Failed to load lazy component', error); return false; } } // Update current route this.currentRoute = to; // Update browser history if (this.config.mode === 'history') { history.pushState({ route: to }, route.title || '', normalizedPath); } else { window.location.hash = normalizedPath; } // Update page title if (route.title) { document.title = route.title; } // Track analytics if (this.config.enableAnalytics) { this.trackNavigation(to, from); } // Execute afterEach hooks for (const hook of this.afterEachHooks) { await hook(to, from); } // Render component await this.renderComponent(route, options); Logger.info('[Router] Navigated', { to: normalizedPath }); return true; } /** * Render component */ async renderComponent(route, options = {}) { const container = options.container || document.querySelector('main'); if (!container) { Logger.error('[Router] Container not found'); return; } if (typeof route.component === 'function') { // Component is a function (render function) const content = await route.component(route, options); if (typeof content === 'string') { container.innerHTML = content; } else if (content instanceof HTMLElement) { container.innerHTML = ''; container.appendChild(content); } } else if (typeof route.component === 'string') { // Component is a selector or HTML const element = document.querySelector(route.component); if (element) { container.innerHTML = ''; container.appendChild(element.cloneNode(true)); } else { container.innerHTML = route.component; } } // Re-initialize modules in new content this.reinitializeModules(container); } /** * Re-initialize modules in container */ reinitializeModules(container) { // Trigger module re-initialization const event = new CustomEvent('router:content-updated', { detail: { container }, bubbles: true }); document.dispatchEvent(event); } /** * Handle popstate event */ handlePopState(event) { const path = this.config.mode === 'history' ? window.location.pathname : window.location.hash.slice(1); this.navigate(path, { updateHistory: false }); } /** * Normalize path */ normalizePath(path) { // Remove base if present if (path.startsWith(this.config.base)) { path = path.slice(this.config.base.length); } // Ensure leading slash if (!path.startsWith('/')) { path = '/' + path; } // Remove trailing slash (except root) if (path !== '/' && path.endsWith('/')) { path = path.slice(0, -1); } return path; } /** * Track navigation for analytics */ trackNavigation(to, from) { const navigation = { to: to.path, from: from?.path || null, timestamp: Date.now(), duration: from ? Date.now() - (from.timestamp || Date.now()) : 0 }; this.analytics.navigations.push(navigation); // Limit analytics size if (this.analytics.navigations.length > 100) { this.analytics.navigations.shift(); } // Trigger analytics event const event = new CustomEvent('router:navigation', { detail: navigation, bubbles: true }); window.dispatchEvent(event); } /** * Get current route */ getCurrentRoute() { return this.currentRoute; } /** * Get analytics */ getAnalytics() { return { ...this.analytics, totalNavigations: this.analytics.navigations.length, totalErrors: this.analytics.errors.length }; } /** * Destroy router */ destroy() { this.routes.clear(); this.guards.clear(); this.middleware = []; this.beforeEachHooks = []; this.afterEachHooks = []; this.currentRoute = null; Logger.info('[Router] Destroyed'); } }