this repo has no description
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 };