this repo has no description
at main 3.4 kB view raw
1import { 2 buildUrl, 3 isValidRoute, 4 parseRouteParams, 5 type Route, 6 type RouteParams, 7 routes, 8 type RoutesWithParams, 9} from "./types/routes.ts"; 10 11const APP_BASE = "/app"; 12 13type Brand<T, B extends string> = T & { readonly __brand: B }; 14type AppPath = Brand<string, "AppPath">; 15 16function asAppPath(path: string): AppPath { 17 const normalized = path.startsWith("/") ? path : "/" + path; 18 return normalized as AppPath; 19} 20 21function getAppPath(): AppPath { 22 const pathname = globalThis.location.pathname; 23 if (pathname.startsWith(APP_BASE)) { 24 const path = pathname.slice(APP_BASE.length) || "/"; 25 return asAppPath(path); 26 } 27 return asAppPath("/"); 28} 29 30function getSearchParams(): URLSearchParams { 31 return new URLSearchParams(globalThis.location.search); 32} 33 34interface RouterState { 35 readonly path: AppPath; 36 readonly searchParams: URLSearchParams; 37} 38 39const state = $state<{ current: RouterState }>({ 40 current: { 41 path: getAppPath(), 42 searchParams: getSearchParams(), 43 }, 44}); 45 46function updateState(): void { 47 state.current = { 48 path: getAppPath(), 49 searchParams: getSearchParams(), 50 }; 51} 52 53globalThis.addEventListener("popstate", updateState); 54 55export function navigate<R extends Route>( 56 route: R, 57 options?: { 58 params?: R extends RoutesWithParams ? RouteParams[R] : never; 59 replace?: boolean; 60 }, 61): void { 62 const url = options?.params ? buildUrl(route, options.params) : route; 63 const fullPath = APP_BASE + (url.startsWith("/") ? url : "/" + url); 64 65 if (options?.replace) { 66 globalThis.history.replaceState(null, "", fullPath); 67 } else { 68 globalThis.history.pushState(null, "", fullPath); 69 } 70 71 updateState(); 72} 73 74export function navigateTo(path: string, replace = false): void { 75 const normalizedPath = path.startsWith("/") ? path : "/" + path; 76 const fullPath = APP_BASE + normalizedPath; 77 78 if (replace) { 79 globalThis.history.replaceState(null, "", fullPath); 80 } else { 81 globalThis.history.pushState(null, "", fullPath); 82 } 83 84 updateState(); 85} 86 87export function getCurrentPath(): AppPath { 88 return state.current.path; 89} 90 91export function getCurrentSearchParams(): URLSearchParams { 92 return state.current.searchParams; 93} 94 95export function getSearchParam(key: string): string | null { 96 return state.current.searchParams.get(key); 97} 98 99export function getFullUrl(path: string): string { 100 return APP_BASE + (path.startsWith("/") ? path : "/" + path); 101} 102 103export function matchRoute(path: AppPath): Route | null { 104 const pathWithoutQuery = path.split("?")[0]; 105 if (isValidRoute(pathWithoutQuery)) { 106 return pathWithoutQuery; 107 } 108 return null; 109} 110 111export function isCurrentRoute(route: Route): boolean { 112 const pathWithoutQuery = state.current.path.split("?")[0]; 113 return pathWithoutQuery === route; 114} 115 116export function getRouteParams<R extends RoutesWithParams>( 117 _route: R, 118): RouteParams[R] { 119 return parseRouteParams(_route); 120} 121 122export type RouteMatch = 123 | { 124 readonly matched: true; 125 readonly route: Route; 126 readonly params: URLSearchParams; 127 } 128 | { readonly matched: false }; 129 130export function match(): RouteMatch { 131 const route = matchRoute(state.current.path); 132 if (route) { 133 return { 134 matched: true, 135 route, 136 params: state.current.searchParams, 137 }; 138 } 139 return { matched: false }; 140} 141 142export { type Route, type RouteParams, routes, type RoutesWithParams };