@olton/router
Version:
Router for SPA.
10 lines (7 loc) • 11.4 kB
JavaScript
/*!
* Router v0.4.1 (Router ror SPA)
* Build: 18.05.2025, 14:39:57
* Copyright 2025 by Serhii Pimenov
* Licensed under MIT
*/
var r=class s{static DEBUG_LEVELS={NONE:0,ERROR:1,WARN:2,INFO:3,DEBUG:4,TRACE:5};static DEBUG_LEVEL=s.DEBUG_LEVELS.NONE;static log(e,t,i){if(e>s.DEBUG_LEVEL)return;let o={error:"color: #ff5555; font-weight: bold",warn:"color: #ffaa00; font-weight: bold",info:"color: #0080fe; font-weight: bold",debug:"color: #00aa00; font-weight: bold",trace:"color: #888888",data:"color: #555; font-style: italic"},a,n;switch(e){case s.DEBUG_LEVELS.ERROR:a="error",n=console.error;break;case s.DEBUG_LEVELS.WARN:a="warn",n=console.warn;break;case s.DEBUG_LEVELS.INFO:a="info",n=console.info;break;case s.DEBUG_LEVELS.DEBUG:a="debug",n=console.debug;break;case s.DEBUG_LEVELS.TRACE:a="trace",n=console.log;break;default:return}console.group(`%c Model: ${t}`,o[a]),i!==void 0&&console.log("%c Data:",o.data,i),console.groupEnd()}static error(e,t){s.log(s.DEBUG_LEVELS.ERROR,e,t)}static warn(e,t){s.log(s.DEBUG_LEVELS.WARN,e,t)}static info(e,t){s.log(s.DEBUG_LEVELS.INFO,e,t)}static debug(e,t){s.log(s.DEBUG_LEVELS.DEBUG,e,t)}static trace(e,t){s.log(s.DEBUG_LEVELS.TRACE,e,t)}};var E="0.4.1",w="18.05.2025, 14:39:57",c=class{constructor(e={}){if(r.DEBUG_LEVEL=e.debug?4:0,r.debug("[Router] Init Router"),this.routes={},this.fallbackRoute=e.fallback||"/",this.maxRedirects=e.maxRedirects||5,this.redirectCount=0,this.basePath=e.base||"",this.middleware=[],this.beforeEachHooks=[],this.afterEachHooks=[],this.cache=new Map,this.cacheLimit=e.cacheLimit??50,this.touchStartX=0,this.enableSwipeNavigation=e.enableSwipeNavigation||!1,this.current=null,this.redirects={},this.useHash=e.useHash||!1,this.events={beforeNavigate:[],afterNavigate:[],routeNotFound:[],error:[]},this.plugins=[],this.enableSwipeNavigation&&this.initSwipeNavigation(),e.routes){r.debug("[Router] Registering routes");for(let[t,i]of Object.entries(e.routes))this.addRoute(t,i)}e.plugins&&Array.isArray(e.plugins)&&(r.debug("[Router] Registering plugins"),e.plugins.forEach(t=>{Array.isArray(t)?this.usePlugin(t[0],t[1]||{}):this.usePlugin(t)})),r.debug("[Router] Subscribing to unhandledrejection event"),window.addEventListener("unhandledrejection",this.handleError.bind(this)),r.debug("[Router] Router Initialized")}on(e,t){return r.debug(`[Router] Subscribing to event ${e}`),this.events[e]&&this.events[e].push(t),this}emit(e,...t){if(r.debug(`[Router] Emitting event ${e}`),this.events[e])for(let i of this.events[e]){let o=i(...t);if(e==="beforeNavigate"&&o===!1)return!1}return!0}initSwipeNavigation(){r.debug("[Router] Initializing swipe navigation"),r.debug("[Router] Adding touchstart event listener"),document.addEventListener("touchstart",e=>{this.touchStartX=e.touches[0].clientX},{passive:!0}),r.debug("[Router] Adding touchend event listener"),document.addEventListener("touchend",e=>{let t=e.changedTouches[0].clientX,i=this.touchStartX-t;Math.abs(i)>100&&(i>0?(r.debug("[Router] Swiping right (forward)"),window.history.forward()):(r.debug("[Router] Swiping left (backward)"),window.history.back()))},{passive:!0})}handleError(e){r.error("[Router] Error during navigation:",e),this.emit("error",e),this.routes["/error"]?this.navigateTo("/error",!0):this.navigateTo(this.fallbackRoute,!0)}sanitizePath(e){r.debug("[Router] Sanitizing path:",e);try{if(!e)return"/";let t=new URL(e,window.location.origin),i=decodeURIComponent(t.pathname);return i=i.replace(/[<>'"`;(){}]/g,"").replace(/[\u0000-\u001F\u007F-\u009F]/g,"").replace(/([^/])\/{2,}/g,"$1/").replace(/\/+/g,"/").replace(/(.+)\/$/,"$1").replace(/\.+/g,".").split("/").filter(o=>o!==".."&&o!==".").join("/"),i.startsWith("/")||(i="/"+i),this.isBlockedPath(i)?"/":(r.debug("[Router] Sanitized path:",i),i)}catch(t){return r.error("[Router] Error sanitizing path:",t),"/"}}isBlockedPath(e){return r.debug("[Router] Checking if path is blocked:",e),[/^\/(api|admin|wp-admin|wp-content|wp-includes)/i,/\.(php|asp|aspx|jsp|cgi|config|env|git|sql|htaccess)$/i,/\/(.+\/)*\.{2,}\//,/javascript:/i,/data:/i,/vbscript:/i,/file:/i].some(i=>i.test(e))}test(e){r.debug("[Router] Testing path:",e);let t=e,i=this.sanitizePath(e),o={original:t,sanitized:i,isBlocked:this.isBlockedPath(i),isModified:t!==i};return r.debug(`[Router] test result for path ${e}:`,o),o}beforeEach(e){return r.debug("[Router] Adding beforeEach hook"),this.beforeEachHooks.push(e),this}afterEach(e){return r.debug("[Router] Adding afterEach hook"),this.afterEachHooks.push(e),this}use(e){return r.debug("[Router] Adding middleware"),this.middleware.push(e),this}addRedirect(e,t){return r.debug("[Router] Adding redirect from",e,"to",t),this.redirects[e]?!1:(this.redirects[e]=t,this)}addRoute(e,t){return r.debug("[Router] Adding route",e),this.routes[e]=t,this}addNestedRoute(e,t,i){r.debug(`[Router] Adding nested route ${t} to ${e}`);let o=`${e}${t}`.replace(/\/\//g,"/");return this.addRoute(o,i),this}addLazyRoute(e,t){return r.debug("[Router] Adding lazy-loaded route",e),this.addRoute(e,async i=>{try{let o=await t();return(o.default||o)(i)}catch(o){throw o}}),this}addLazyNestedRoute(e,t,i){r.debug("[Router] Adding lazy-loaded nested route",t,"to",e);let o=`${e}${t}`.replace(/\/\//g,"/");return this.addLazyRoute(o,i),this}addFallbackRoute(e){return r.debug("[Router] Adding fallback route",e),this.fallbackRoute=e,this}add404Route(e){return r.debug("[Router] Adding 404 route",e),this.routes["/404"]=e,this}addErrorRoute(e){return r.debug("[Router] Adding error route",e),this.routes["/error"]=e,this}addProtectedRoute(e,t,i,o="/login"){return r.debug("[Router] Adding protected route",e),this.addRoute(e,async a=>{if(await i(a))return r.debug("[Router] Guard function passed, executing callback"),t(a);r.debug("[Router] Guard function failed, redirecting to",o),await this.navigateTo(o,!0)}),this}removeRoute(e){return r.debug("[Router] Removing route",e),this.routes[e]&&delete this.routes[e],this}updateRoute(e,t){return r.debug("[Router] Updating route",e),this.routes[e]&&(this.routes[e]=t),this}getRoutes(){return r.debug("[Router] Getting registered routes"),this.routes}async navigate(e){if(r.debug("[Router] Navigating to",e),this.redirectCount>this.maxRedirects){r.error("[Router] Maximum redirect limit reached, redirecting to",this.fallbackRoute),console.error("Maximum redirect limit reached"),this.redirectCount=0,this.emit("error",new Error("Maximum redirect limit reached")),await this.navigateTo("/error",!0);return}this.redirectCount++;let t=this.matchRoute(this.sanitizePath(e));if(t)try{if(r.debug("[Router] Route matched:",t),this.emit("beforeNavigate",t)===!1){r.debug("[Router] Navigation cancelled by beforeNavigate hook");return}if(this.redirects[t.path]){r.debug("[Router] Redirecting to",this.redirects[t.path]),await this.navigateTo(this.redirects[t.path],!0);return}this.redirectCount++;for(let o of this.middleware)r.debug("[Router] Executing middleware"),await o(t);for(let o of this.beforeEachHooks)r.debug("[Router] Executing beforeEach hook"),await o(t);r.debug("[Router] Executing route callback"),await t.callback(t.params);for(let o of this.afterEachHooks)r.debug("[Router] Executing afterEach hook"),await o(t);this.current=t,this.emit("afterNavigate",t),r.debug("[Router] Navigation completed")}catch(i){r.error("[Router] Error during navigation:",i),console.error("Navigation error:",i),this.emit("error",i),this.routes["/error"]&&this.routes["/error"](i)}else r.warn("[Router] Route not found:",e),this.redirectCount=0,this.emit("routeNotFound",e),this.routes["/404"]&&this.routes["/404"]()}async navigateTo(e,t=!1){r.debug(`[Router] Navigating to ${e} ${t?"with replace state":""}`),this.redirectCount=0;let i=new URL(e,window.location.origin);t?(r.debug("[Router] Replacing state with",i),window.history.replaceState({},"",i)):(r.debug("[Router] Pushing state with",i),window.history.pushState({},"",i)),r.debug("[Router] Navigating to",i),await this.navigate(i.pathname)}matchRoute(e){if(r.debug("[Router] Matching route for",e),this.cache.has(e))return this.cache.get(e);r.debug("[Router] Route not found in cache, performing match");let t=this._performMatch(e);if(this.cache.size>=this.cacheLimit){r.debug("[Router] Cache limit reached, removing oldest entry");let i=this.cache.keys().next().value;this.cache.delete(i)}return r.debug("[Router] Caching result for",e),this.cache.set(e,t),t}_performMatch(e){let[t,i]=e.split("?"),o=new URLSearchParams(i),a=Object.fromEntries(o);for(let n in this.routes){let d=[],h=n.replace(/:([^\/]+)/g,(l,u)=>(d.push(u),"([^/]+)")),g=e.match(new RegExp(`^${h}$`));if(g){let l=g.slice(1).reduce((u,b,R)=>(u[d[R]]=b,u),{});return{path:e,pattern:h,callback:this.routes[n],params:l,query:a}}}return null}clearCache(){r.debug("[Router] Clearing cache"),this.cache.clear()}resetRedirectCount(){r.debug("[Router] Resetting redirect count"),this.redirectCount=0}getFullPath(e){return r.debug("[Router] Getting full path for",e),this.useHash?`#${e}`:e}getPathFromLocation(){return r.debug("[Router] Getting path from location"),this.useHash?window.location.hash.slice(1)||"/":window.location.pathname}listen(){return r.debug("[Router] Starting to listen for navigation events"),this._handleNavigation=()=>{this.redirectCount=0,this.navigate(this.getPathFromLocation()).then(e=>{})},this._handleLinkClick=e=>{if(e.target.tagName==="A"){let t=e.target.getAttribute("href");if(t&&(this.useHash&&t.startsWith("#")||!this.useHash&&e.target.href.startsWith(window.location.origin))){e.preventDefault();let i=this.useHash?t.slice(1):e.target.pathname;this.redirectCount=0,this.navigateTo(i).then(o=>{})}}},r.debug("[Router] Listening for navigation events"),window.addEventListener(this.useHash?"hashchange":"popstate",this._handleNavigation),r.debug("[Router] Listening for link clicks"),document.addEventListener("click",this._handleLinkClick),this._initPlugins(),this.redirectCount=0,this.navigate(this.getPathFromLocation()).then(e=>{}),this}_initPlugins(){r.debug("[Router] Init plugins"),this.plugins.forEach(({plugin:e,options:t})=>{typeof e.onInit=="function"&&e.onInit(this,t)})}usePlugin(e,t={}){if(!e)return this;if(r.debug("[Router] Using plugin",e),typeof e=="object"&&typeof e.install=="function")r.debug("[Router] Installing plugin",e),e.install(this,t);else if(typeof e=="function")r.debug("[Router] Executing plugin",e),e(this,t);else return r.warn("Invalid plugin format. Plugin must be an object with install method or a function."),this;return r.debug("[Router] Add plugin to store",e),this.plugins.push({plugin:e,options:t}),r.debug("[Router] Plugin initialized"),this}destroy(){r.debug("[Router] Destroying router"),this.plugins.forEach(({plugin:e,options:t})=>{r.debug("[Router] Destroying plugin",e),typeof e.onDestroy=="function"&&e.onDestroy(this,t)}),r.debug("[Router] Removing event listeners"),window.removeEventListener(this.useHash?"hashchange":"popstate",this._handleNavigation),document.removeEventListener("click",this._handleLinkClick),window.removeEventListener("unhandledrejection",this.handleError),this._handleNavigation=null,this._handleLinkClick=null,this.routes={},this.plugins=[],this.cache.clear(),r.debug("[Router] Router destroyed")}};c.info=()=>{console.info(`%c Router %c v${E} %c ${w} `,"color: #ffffff; font-weight: bold; background: #ed1cab","color: white; background: darkgreen","color: white; background: #0080fe;")};var f=c;var p=f;export{p as default};