UNPKG

muspe-cli

Version:

MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo

1,545 lines (1,290 loc) 40.4 kB
/** * MusPE Advanced SPA Router - Enterprise-grade routing with .htaccess support * Supports history mode, hash mode, guards, animations, and mobile-specific features * @version 2.1.0 */ class MusPEAdvancedRouter { constructor(options = {}) { this.routes = new Map(); this.currentRoute = null; this.history = []; this.routeCache = new Map(); this.paramCache = new Map(); this.middlewares = []; // Configuration this.mode = options.mode || 'history'; // 'history', 'hash', or 'abstract' this.basePath = options.basePath || ''; this.fallback = options.fallback || null; this.caseSensitive = options.caseSensitive || false; this.strict = options.strict || false; this.scrollBehavior = options.scrollBehavior || 'top'; this.transitionDuration = options.transitionDuration || 300; this.maxHistorySize = options.maxHistorySize || 100; this.maxCacheSize = options.maxCacheSize || 20; // Guards system this.guards = { beforeEach: [], beforeResolve: [], afterEach: [], beforeLeave: [] }; // Animation settings this.animations = { enter: options.enterAnimation || 'slideInRight', leave: options.leaveAnimation || 'slideOutLeft', duration: options.transitionDuration || 300 }; // Component caching this.cache = new Map(); // Event listeners this.listeners = new Map(); // Route metadata for analytics this.routeMetadata = new Map(); // SPA configuration this.spaConfig = { enablePrefetch: options.enablePrefetch !== false, enableLazyLoading: options.enableLazyLoading !== false, enableAnalytics: options.enableAnalytics !== false, htaccessMode: options.htaccessMode !== false }; this.setupRouting(); } // Setup browser routing with SPA support setupRouting() { // Handle different routing modes if (this.mode === 'history' && this.isHistoryModeSupported()) { this.setupHistoryMode(); } else if (this.mode === 'hash') { this.setupHashMode(); } else { this.setupAbstractMode(); } // Setup automatic link handling for SPA this.setupLinkHandler(); // Setup route prefetching if enabled if (this.spaConfig.enablePrefetch) { this.setupPrefetching(); } // Setup .htaccess fallback detection if (this.spaConfig.htaccessMode) { this.setupHtaccessFallback(); } // Handle initial route const initialPath = this.getCurrentPath(); this.navigate(initialPath, { replace: true, initial: true }); } // Setup history mode routing setupHistoryMode() { window.addEventListener('popstate', (event) => { const path = this.getCurrentPath(); this.navigate(path, { state: event.state, replace: true, fromHistory: true }); }); } // Setup hash mode routing setupHashMode() { window.addEventListener('hashchange', (event) => { const path = this.getHashPath(); this.navigate(path, { replace: true, fromHistory: true }); }); } // Setup abstract mode (for testing/SSR) setupAbstractMode() { // No browser events in abstract mode console.log('Router running in abstract mode'); } // Check if history mode is supported isHistoryModeSupported() { return window.history && window.history.pushState; } // Get current path based on mode getCurrentPath() { switch (this.mode) { case 'hash': return this.getHashPath(); case 'history': return this.getHistoryPath(); default: return '/'; } } // Get path from hash getHashPath() { const hash = window.location.hash; return hash ? hash.slice(1) : '/'; } // Get path from history getHistoryPath() { let path = window.location.pathname; // Remove base path if configured if (this.basePath && path.startsWith(this.basePath)) { path = path.slice(this.basePath.length) || '/'; } return path; } // Setup automatic link handling for SPA setupLinkHandler() { document.addEventListener('click', (event) => { const link = event.target.closest('a'); if (!link || !this.shouldHandleLink(link)) { return; } event.preventDefault(); const href = link.getAttribute('href'); const target = link.getAttribute('target'); if (target === '_blank') { window.open(href, '_blank'); return; } this.navigate(href); }); } // Check if link should be handled by router shouldHandleLink(link) { const href = link.getAttribute('href'); // Skip external links if (!href || href.startsWith('http') || href.startsWith('//')) { return false; } // Skip special links if (href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('sms:')) { return false; } // Skip download links if (link.hasAttribute('download')) { return false; } // Skip links with external attribute if (link.hasAttribute('external') || link.hasAttribute('data-external')) { return false; } return true; } // Setup route prefetching setupPrefetching() { if ('IntersectionObserver' in window) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const link = entry.target; const href = link.getAttribute('href'); if (href && this.shouldHandleLink(link)) { this.prefetchRoute(href); } } }); }, { rootMargin: '50px' }); // Observe links when DOM is ready const observeLinks = () => { document.querySelectorAll('a[href]').forEach(link => { if (this.shouldHandleLink(link)) { observer.observe(link); } }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', observeLinks); } else { observeLinks(); } } } // Setup .htaccess fallback detection setupHtaccessFallback() { // Detect if we're in a fallback scenario (404 -> index.html) const referrer = document.referrer; const currentPath = this.getCurrentPath(); // If we have a path but no referrer and it's not root, it might be a direct access if (currentPath !== '/' && !referrer && this.mode === 'history') { // Check if this path actually exists as a route const route = this.resolveRoute(currentPath); if (!route) { // Redirect to 404 or fallback if (this.fallback) { this.navigate(this.fallback, { replace: true }); } } } } // Register route with advanced SPA options route(path, component, options = {}) { const route = { path, component, name: options.name || path, meta: options.meta || {}, beforeEnter: options.beforeEnter, beforeLeave: options.beforeLeave, children: options.children || [], props: options.props, redirect: options.redirect, alias: options.alias, caseSensitive: options.caseSensitive || this.caseSensitive, cache: options.cache !== false, // Cache by default keepAlive: options.keepAlive || false, lazy: options.lazy || false, preload: options.preload || false, transition: options.transition, middlewares: options.middlewares || [], guards: options.guards || [] }; // Compile route regex for dynamic matching const compiled = this.compileRoute(path); route.regex = compiled.regex; route.keys = compiled.keys; this.routes.set(path, route); // Initialize route metadata for analytics this.routeMetadata.set(path, { accessCount: 0, lastAccessed: null, loadTime: null, errors: [] }); // Register aliases if (route.alias) { const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]; aliases.forEach(alias => { this.routes.set(alias, { ...route, path: alias }); }); } return this; } // Compile route pattern into regex compileRoute(path) { const keys = []; // Escape special regex characters except for route parameters let pattern = path .replace(/\//g, '\\/') .replace(/\./g, '\\.') .replace(/\*/g, '.*'); // Handle route parameters (:param) pattern = pattern.replace(/:([^\/]+)/g, (match, key) => { keys.push({ name: key }); return '([^/]+)'; }); // Handle optional parameters (:param?) pattern = pattern.replace(/\(([^)]+)\)\?/g, '(?:$1)?'); const flags = this.caseSensitive ? '' : 'i'; const regex = new RegExp(`^${pattern}${this.strict ? '' : '\\/?'}$`, flags); return { regex, keys }; } // Group routes with common options group(options, callback) { const originalPrefix = this.routePrefix || ''; const originalGuards = { ...this.routeGuards }; this.routePrefix = originalPrefix + (options.prefix || ''); this.routeGuards = { ...originalGuards, ...(options.guards || {}) }; callback(); this.routePrefix = originalPrefix; this.routeGuards = originalGuards; return this; } // Navigate to route with enhanced SPA support async navigate(path, options = {}) { const { replace = false, state = {}, animation = null, fromHistory = false, query = {}, params = {}, silent = false, initial = false } = options; try { // Normalize and build full path const normalizedPath = this.normalizePath(path); const fullPath = this.buildFullPath(normalizedPath, query); // Find matching route const route = this.resolveRoute(normalizedPath); if (!route) { if (this.fallback) { return this.navigate(this.fallback, { replace: true }); } throw new Error(`Route not found: ${path}`); } // Handle redirects if (route.redirect) { const redirectPath = typeof route.redirect === 'function' ? route.redirect(route) : route.redirect; return this.navigate(redirectPath, { replace: true }); } // Create route context const from = this.currentRoute; const to = { ...route, path: normalizedPath, fullPath, params: { ...params, ...this.extractParams(route, normalizedPath) }, query: { ...query, ...this.parseQuery(window.location.search) }, state, timestamp: Date.now() }; // Run navigation guards const canNavigate = await this.runNavigationGuards(to, from); if (!canNavigate) { return false; } // Load route component if lazy if (route.lazy && !route.loaded) { await this.loadLazyRoute(route); } // Update browser history if (!silent && !fromHistory && !initial) { this.updateBrowserHistory(fullPath, state, replace); } // Perform route transition await this.performTransition(to, from, animation); // Update current route this.currentRoute = to; this.updateRouteHistory(to); // Run after guards await this.runAfterGuards(to, from); // Update metadata this.updateRouteMetadata(route.path); // Emit route change event this.emit('route:changed', { to, from }); this.emit('navigation:complete', to); return true; } catch (error) { console.error('Navigation error:', error); this.emit('route:error', { error, path }); // Try fallback route on error if (this.fallback && path !== this.fallback) { return this.navigate(this.fallback, { replace: true }); } return false; } } // Normalize path normalizePath(path) { // Handle hash mode if (this.mode === 'hash' && !path.startsWith('#')) { path = '#' + path; } // Remove hash for history mode if (this.mode === 'history' && path.startsWith('#')) { path = path.slice(1); } // Remove base path if (this.basePath && path.startsWith(this.basePath)) { path = path.slice(this.basePath.length); } // Ensure leading slash if (!path.startsWith('/')) { path = '/' + path; } // Remove trailing slash (except for root) if (path.length > 1 && path.endsWith('/')) { path = path.slice(0, -1); } return path; } // Build full path with query parameters buildFullPath(path, query = {}) { const queryString = this.buildQueryString(query); return path + (queryString ? '?' + queryString : ''); } // Build query string buildQueryString(query) { return Object.entries(query) .filter(([key, value]) => value != null) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } // Update browser history updateBrowserHistory(path, state, replace) { const url = this.mode === 'hash' ? `#${path}` : (this.basePath + path); if (replace) { window.history.replaceState(state, '', url); } else { window.history.pushState(state, '', url); } } // Update route history updateRouteHistory(route) { this.history.push({ ...route, timestamp: Date.now() }); // Limit history size if (this.history.length > this.maxHistorySize) { this.history = this.history.slice(-this.maxHistorySize); } } // Resolve route from path with enhanced matching resolveRoute(path) { const normalizedPath = this.normalizePath(path); // Check cache first if (this.routeCache.has(normalizedPath)) { return this.routeCache.get(normalizedPath); } // Exact match first if (this.routes.has(normalizedPath)) { const route = this.routes.get(normalizedPath); this.routeCache.set(normalizedPath, route); return route; } // Dynamic route matching using compiled regex for (const [routePath, route] of this.routes) { if (route.regex) { const match = normalizedPath.match(route.regex); if (match) { const matchedRoute = { ...route, matched: match[0] }; this.routeCache.set(normalizedPath, matchedRoute); return matchedRoute; } } } return null; } // Extract parameters from matched route extractParams(route, path) { if (!route.regex || !route.keys) { return {}; } const match = path.match(route.regex); if (!match) { return {}; } const params = {}; route.keys.forEach((key, index) => { params[key.name] = match[index + 1]; }); return params; } // Load lazy route component async loadLazyRoute(route) { const startTime = performance.now(); try { if (typeof route.component === 'function') { // Dynamic import const componentModule = await route.component(); route.component = componentModule.default || componentModule; } route.loaded = true; const loadTime = performance.now() - startTime; this.updateRouteLoadTime(route.path, loadTime); } catch (error) { console.error('Route loading error:', error); this.recordRouteError(route.path, error); throw error; } } // Prefetch route for performance async prefetchRoute(path) { const route = this.resolveRoute(path); if (route && route.lazy && !route.loaded) { try { await this.loadLazyRoute(route); } catch (error) { console.warn('Route prefetch failed:', error); } } } // Match dynamic routes matchRoute(routePath, actualPath) { const routeSegments = routePath.split('/'); const pathSegments = actualPath.split('/'); if (routeSegments.length !== pathSegments.length) { return false; } return routeSegments.every((segment, index) => { if (segment.startsWith(':')) { return true; // Dynamic segment matches anything } return segment === pathSegments[index]; }); } // Extract parameters from path extractParams(routePath, actualPath) { const params = {}; const routeSegments = routePath.split('/'); const pathSegments = actualPath.split('/'); routeSegments.forEach((segment, index) => { if (segment.startsWith(':')) { const paramName = segment.slice(1); params[paramName] = pathSegments[index]; } }); return params; } // Parse query string parseQuery(queryString) { const query = {}; const params = new URLSearchParams(queryString); for (const [key, value] of params) { query[key] = value; } return query; } // Run comprehensive navigation guards async runNavigationGuards(to, from) { // Run global beforeEach guards for (const guard of this.guards.beforeEach) { const result = await this.executeGuard(guard, to, from); if (result === false) return false; if (typeof result === 'string') { this.navigate(result, { replace: true }); return false; } } // Run route-specific beforeEnter guard if (to.beforeEnter) { const result = await this.executeGuard(to.beforeEnter, to, from); if (result === false) return false; if (typeof result === 'string') { this.navigate(result, { replace: true }); return false; } } // Run route middlewares for (const middleware of to.middlewares) { const result = await this.executeGuard(middleware, to, from); if (result === false) return false; } // Run beforeLeave guard on current route if (from && from.beforeLeave) { const result = await this.executeGuard(from.beforeLeave, to, from); if (result === false) return false; } // Run global beforeResolve guards for (const guard of this.guards.beforeResolve) { const result = await this.executeGuard(guard, to, from); if (result === false) return false; } return true; } // Execute a single guard with error handling async executeGuard(guard, to, from) { try { const result = await guard(to, from, (next) => { if (next === false) return false; if (typeof next === 'string') return next; return true; }); return result !== undefined ? result : true; } catch (error) { console.error('Navigation guard error:', error); return false; } } // Run after navigation guards async runAfterGuards(to, from) { for (const guard of this.guards.afterEach) { try { await guard(to, from); } catch (error) { console.error('After guard error:', error); } } } // Perform route transition with enhanced animations async performTransition(to, from, customAnimation) { const container = document.querySelector('#app') || document.body; // Get or create component instance let componentInstance = this.getComponentInstance(to); if (!componentInstance) { componentInstance = await this.createComponentInstance(to); if (to.cache && to.keepAlive) { this.cacheComponent(to.path, componentInstance); } } // Handle page transitions if (from && from.component && MusPE.env.isMobile) { await this.animateTransition(container, componentInstance, customAnimation); } else { // Simple replacement for non-mobile or first load await this.simpleTransition(container, componentInstance); } // Update document metadata this.updateDocumentMetadata(to); // Handle scroll behavior this.handleScrollBehavior(to, from); } // Simple transition without animation async simpleTransition(container, component) { container.innerHTML = ''; if (component && component.mount) { component.mount(container); } else if (typeof component === 'function') { container.innerHTML = component(); } } // Update document metadata (title, meta tags) updateDocumentMetadata(route) { // Update document title if (route.meta && route.meta.title) { document.title = route.meta.title; } // Update meta tags if (route.meta) { Object.entries(route.meta).forEach(([name, content]) => { if (name === 'title') return; let element = document.querySelector(`meta[name="${name}"]`); if (!element) { element = document.createElement('meta'); element.setAttribute('name', name); document.head.appendChild(element); } element.setAttribute('content', content); }); } } // Handle scroll behavior handleScrollBehavior(to, from) { if (this.scrollBehavior === 'top') { window.scrollTo(0, 0); } else if (this.scrollBehavior === 'preserve' && from) { // Preserve scroll position return; } else if (typeof this.scrollBehavior === 'function') { this.scrollBehavior(to, from); } } // Parse query string parseQuery(queryString) { const query = {}; if (queryString) { queryString.replace(/^\?/, '').split('&').forEach(param => { const [key, value] = param.split('='); if (key) { query[decodeURIComponent(key)] = decodeURIComponent(value || ''); } }); } return query; } // Enhanced component caching getComponentInstance(route) { if (route.keepAlive) { return this.cache.get(route.path); } return null; } async createComponentInstance(route) { const ComponentClass = route.component; const props = route.props || {}; if (typeof ComponentClass === 'function') { try { return new ComponentClass(props); } catch (error) { console.error('Component creation error:', error); throw error; } } throw new Error('Invalid component for route'); } cacheComponent(path, instance) { if (this.cache.size >= this.maxCacheSize) { // Remove oldest cached component const firstKey = this.cache.keys().next().value; const component = this.cache.get(firstKey); if (component && component.unmount) { component.unmount(); } this.cache.delete(firstKey); } this.cache.set(path, instance); } // Route metadata management updateRouteMetadata(path) { const metadata = this.routeMetadata.get(path); if (metadata) { metadata.accessCount++; metadata.lastAccessed = Date.now(); } } updateRouteLoadTime(path, loadTime) { const metadata = this.routeMetadata.get(path); if (metadata) { metadata.loadTime = loadTime; } } recordRouteError(path, error) { const metadata = this.routeMetadata.get(path); if (metadata) { metadata.errors.push({ error: error.message, timestamp: Date.now() }); } } // Event system on(event, listener) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(listener); return this; } off(event, listener) { const listeners = this.listeners.get(event); if (listeners) { const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } return this; } emit(event, ...args) { const listeners = this.listeners.get(event); if (listeners) { listeners.forEach(listener => { try { listener(...args); } catch (error) { console.error('Event listener error:', error); } }); } } // Navigation guards registration beforeEach(guard) { this.guards.beforeEach.push(guard); return this; } beforeResolve(guard) { this.guards.beforeResolve.push(guard); return this; } afterEach(guard) { this.guards.afterEach.push(guard); return this; } // Programmatic navigation push(path, state = {}) { return this.navigate(path, { state }); } replace(path, state = {}) { return this.navigate(path, { state, replace: true }); } back() { window.history.back(); } forward() { window.history.forward(); } go(n) { window.history.go(n); } // Route utilities isActive(path) { return this.currentRoute && this.currentRoute.path === path; } isExactActive(path) { return this.isActive(path); } // URL generation url(name, params = {}, query = {}) { const route = Array.from(this.routes.values()).find(r => r.name === name); if (!route) { throw new Error(`Route not found: ${name}`); } let path = route.path; // Replace parameters Object.entries(params).forEach(([key, value]) => { path = path.replace(`:${key}`, value); }); return this.buildFullPath(path, query); } // Middleware registration use(middleware) { this.middlewares.push(middleware); return this; } // Route matching matchRoute(routePath, actualPath) { const routeSegments = routePath.split('/'); const pathSegments = actualPath.split('/'); if (routeSegments.length !== pathSegments.length) { return false; } return routeSegments.every((segment, index) => { if (segment.startsWith(':')) { return true; // Dynamic segment matches anything } return segment === pathSegments[index]; }); } // Cache management clearCache() { this.routeCache.clear(); this.paramCache.clear(); this.cache.forEach(component => { if (component && component.unmount) { component.unmount(); } }); this.cache.clear(); } // Configuration setAnimations(animations) { this.animations = { ...this.animations, ...animations }; } setMode(mode) { if (['history', 'hash', 'abstract'].includes(mode)) { this.mode = mode; this.setupRouting(); } } // Getters getCurrentRoute() { return this.currentRoute; } getHistory() { return [...this.history]; } getRoutes() { return Array.from(this.routes.entries()).map(([path, route]) => ({ path, ...route })); } getAnalytics() { const analytics = {}; for (const [path, metadata] of this.routeMetadata) { analytics[path] = { accessCount: metadata.accessCount, lastAccessed: metadata.lastAccessed, loadTime: metadata.loadTime, errorCount: metadata.errors.length }; } return analytics; } // Route grouping group(options, callback) { const originalPrefix = this.routePrefix || ''; const originalGuards = { ...this.routeGuards }; this.routePrefix = originalPrefix + (options.prefix || ''); this.routeGuards = { ...originalGuards, ...(options.guards || {}) }; callback(this); this.routePrefix = originalPrefix; this.routeGuards = originalGuards; return this; } // Destroy router destroy() { // Remove event listeners window.removeEventListener('popstate', this.handlePopState); window.removeEventListener('hashchange', this.handleHashChange); // Clear caches this.clearCache(); // Clear listeners this.listeners.clear(); // Reset state this.currentRoute = null; this.history = []; } // Enhanced animation system async animateTransition(container, newComponent, customAnimation) { const currentElement = container.firstElementChild; // Create new element const tempContainer = document.createElement('div'); if (newComponent.mount) { newComponent.mount(tempContainer); } const newElement = tempContainer.firstElementChild; if (!currentElement) { container.appendChild(newElement); return; } // Determine animation direction const isForward = this.isForwardNavigation(); const animation = customAnimation || (isForward ? this.animations.enter : this.animations.leave); // Prepare elements for animation newElement.style.position = 'absolute'; newElement.style.top = '0'; newElement.style.left = '0'; newElement.style.width = '100%'; newElement.style.zIndex = '1000'; // Add new element container.appendChild(newElement); // Animate transition await this.runAnimation(currentElement, newElement, animation, isForward); // Cleanup if (currentElement.parentNode) { currentElement.parentNode.removeChild(currentElement); } newElement.style.position = ''; newElement.style.top = ''; newElement.style.left = ''; newElement.style.width = ''; newElement.style.zIndex = ''; } // Run transition animation async runAnimation(oldElement, newElement, animationType, isForward) { const duration = this.animations.duration; switch (animationType) { case 'slideInRight': return this.slideAnimation(oldElement, newElement, isForward ? 'left' : 'right', duration); case 'slideInLeft': return this.slideAnimation(oldElement, newElement, isForward ? 'right' : 'left', duration); case 'fade': return this.fadeAnimation(oldElement, newElement, duration); case 'none': return Promise.resolve(); default: return this.slideAnimation(oldElement, newElement, isForward ? 'left' : 'right', duration); } } // Slide animation async slideAnimation(oldElement, newElement, direction, duration) { const transform = { left: ['translateX(-100%)', 'translateX(0)'], right: ['translateX(100%)', 'translateX(0)'] }; // Initial position for new element newElement.style.transform = transform[direction][0]; // Animate both elements const oldAnimation = oldElement.animate([ { transform: 'translateX(0)' }, { transform: direction === 'left' ? 'translateX(100%)' : 'translateX(-100%)' } ], { duration, easing: 'ease-out', fill: 'forwards' }); const newAnimation = newElement.animate([ { transform: transform[direction][0] }, { transform: transform[direction][1] } ], { duration, easing: 'ease-out', fill: 'forwards' }); await Promise.all([ oldAnimation.finished, newAnimation.finished ]); } // Fade animation async fadeAnimation(oldElement, newElement, duration) { newElement.style.opacity = '0'; const oldAnimation = oldElement.animate([ { opacity: 1 }, { opacity: 0 } ], { duration: duration / 2, easing: 'ease-out', fill: 'forwards' }); await oldAnimation.finished; const newAnimation = newElement.animate([ { opacity: 0 }, { opacity: 1 } ], { duration: duration / 2, easing: 'ease-in', fill: 'forwards' }); await newAnimation.finished; } // Determine if navigation is forward isForwardNavigation() { if (this.history.length < 2) return true; const current = this.history[this.history.length - 1]; const previous = this.history[this.history.length - 2]; return current.timestamp > previous.timestamp; } } // ============ ROUTE MIDDLEWARE HELPERS ============ /** * Common route middleware */ class RouteMiddleware { static auth(to, from, next) { const token = localStorage.getItem('auth_token'); if (!token) { return next('/login'); } next(); } static guest(to, from, next) { const token = localStorage.getItem('auth_token'); if (token) { return next('/dashboard'); } next(); } static admin(to, from, next) { const userRole = localStorage.getItem('user_role'); if (userRole !== 'admin') { return next('/unauthorized'); } next(); } static throttle(delay = 1000) { let lastCall = 0; return (to, from, next) => { const now = Date.now(); if (now - lastCall < delay) { return next(false); } lastCall = now; next(); }; } } // ============ ROUTER COMPONENTS ============ // Create global router instance with enhanced configuration const router = new MusPEAdvancedRouter({ mode: 'history', basePath: '', fallback: '/404', enablePrefetch: true, enableLazyLoading: true, enableAnalytics: true, htaccessMode: true, transitionDuration: 300, scrollBehavior: 'top' }); // Enhanced Router Link component class RouterLink extends MusPEComponent { constructor(props) { super(props); this.state = reactive({ isActive: false }); } mounted() { this.updateActiveState(); router.on('route:changed', () => { this.updateActiveState(); }); } updateActiveState() { const { to, exact = false } = this.props; this.state.isActive = exact ? router.isExactActive(to) : router.isActive(to); } render() { const { to, exact = false, activeClass = 'router-link-active', exactActiveClass = 'router-link-exact-active', tag = 'a', replace = false, append = false, ...otherProps } = this.props; const isActive = this.state.isActive; const isExactActive = exact && isActive; let className = otherProps.class || ''; if (isActive) className += ` ${activeClass}`; if (isExactActive) className += ` ${exactActiveClass}`; className = className.trim(); const handleClick = (e) => { e.preventDefault(); if (replace) { router.replace(to); } else { router.push(to); } }; return h(tag, { ...otherProps, class: className, href: to, onClick: handleClick }, this.props.children || this.props.text || to); } } // Enhanced Router View component with transitions class RouterView extends MusPEComponent { constructor(props) { super(props); this.state = reactive({ currentRoute: null, isTransitioning: false, error: null }); // Listen for route changes router.on('route:changed', ({ to }) => { this.updateRoute(to); }); router.on('route:error', ({ error }) => { this.state.error = error; }); } mounted() { // Render current route const currentRoute = router.getCurrentRoute(); if (currentRoute) { this.updateRoute(currentRoute); } } updateRoute(route) { this.state.currentRoute = route; this.state.error = null; } render() { const { name, class: className } = this.props; if (this.state.error) { return h('div', { class: `router-view router-view-error ${className || ''}`.trim() }, [ h('h1', {}, 'Route Error'), h('p', {}, this.state.error.message) ]); } if (!this.state.currentRoute) { return h('div', { class: `router-view router-view-loading ${className || ''}`.trim() }, 'Loading...'); } const route = this.state.currentRoute; try { if (route.component) { return h('div', { class: `router-view ${className || ''}`.trim(), 'data-route': route.path }, h(route.component, { ...this.props, route, $route: route })); } } catch (error) { console.error('RouterView render error:', error); return h('div', { class: `router-view router-view-error ${className || ''}`.trim() }, 'Component Error'); } return h('div', { class: `router-view ${className || ''}`.trim() }, 'No Component'); } } // ============ HTACCESS GENERATOR ============ /** * Generate .htaccess file for SPA routing */ function generateHtaccess(options = {}) { const { basePath = '', fallbackPath = '/index.html', cacheControl = true, compression = true, security = true } = options; let htaccess = `# MusPE SPA Router Configuration # Generated automatically - do not edit manually RewriteEngine On `; // Handle base path if (basePath) { htaccess += `# Base path configuration RewriteBase ${basePath} `; } // Security headers if (security) { htaccess += `# Security headers Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set X-XSS-Protection "1; mode=block" Header always set Referrer-Policy "strict-origin-when-cross-origin" `; } // Compression if (compression) { htaccess += `# Enable compression <IfModule mod_deflate.c> AddOutputFilterByType DEFLATE text/plain AddOutputFilterByType DEFLATE text/html AddOutputFilterByType DEFLATE text/xml AddOutputFilterByType DEFLATE text/css AddOutputFilterByType DEFLATE application/xml AddOutputFilterByType DEFLATE application/xhtml+xml AddOutputFilterByType DEFLATE application/rss+xml AddOutputFilterByType DEFLATE application/javascript AddOutputFilterByType DEFLATE application/x-javascript </IfModule> `; } // Cache control if (cacheControl) { htaccess += `# Cache control <IfModule mod_expires.c> ExpiresActive on ExpiresByType text/css "access plus 1 month" ExpiresByType application/javascript "access plus 1 month" ExpiresByType image/png "access plus 1 month" ExpiresByType image/jpg "access plus 1 month" ExpiresByType image/jpeg "access plus 1 month" ExpiresByType image/gif "access plus 1 month" ExpiresByType image/ico "access plus 1 month" ExpiresByType image/icon "access plus 1 month" ExpiresByType text/x-icon "access plus 1 month" ExpiresByType image/x-icon "access plus 1 month" ExpiresByType application/pdf "access plus 1 month" ExpiresByType application/javascript "access plus 1 month" ExpiresByType text/x-javascript "access plus 1 month" ExpiresByType application/x-shockwave-flash "access plus 1 month" ExpiresByType image/x-icon "access plus 1 year" ExpiresByType text/css "access plus 1 year" ExpiresByType application/javascript "access plus 1 year" ExpiresByType text/javascript "access plus 1 year" ExpiresByType application/x-javascript "access plus 1 year" ExpiresDefault "access plus 2 days" </IfModule> `; } // SPA routing rules htaccess += `# SPA routing - handle client-side routes # Don't rewrite files or directories that exist RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # Don't rewrite API endpoints RewriteCond %{REQUEST_URI} !^/api/ # Don't rewrite asset files RewriteCond %{REQUEST_URI} !\\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ [NC] # Rewrite everything else to index.html RewriteRule ^.*$ ${fallbackPath} [L] # Handle CORS for API requests <IfModule mod_headers.c> Header add Access-Control-Allow-Origin "*" Header add Access-Control-Allow-Methods "GET, POST, OPTIONS, DELETE, PUT" Header add Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" </IfModule> # Handle OPTIONS requests RewriteCond %{REQUEST_METHOD} OPTIONS RewriteRule ^(.*)$ $1 [R=200,L] # Error pages ErrorDocument 404 ${fallbackPath} ErrorDocument 403 ${fallbackPath} `; return htaccess; } // ============ EXPORTS AND GLOBAL REGISTRATION ============ // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = { MusPEAdvancedRouter, RouteMiddleware, router, RouterLink, RouterView, generateHtaccess }; } // Make available globally if (typeof window !== 'undefined') { window.MusPEAdvancedRouter = MusPEAdvancedRouter; window.RouteMiddleware = RouteMiddleware; window.router = router; window.RouterLink = RouterLink; window.RouterView = RouterView; window.generateHtaccess = generateHtaccess; // Auto-generate .htaccess file for development if (router.spaConfig.htaccessMode && window.location.hostname === 'localhost') { console.log('MusPE Router: SPA mode enabled'); console.log('Generated .htaccess content:'); console.log(generateHtaccess()); } }