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
JavaScript
/**
* 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());
}
}