@pyreon/router 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +283 -30
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +285 -29
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index2.d.ts +118 -5
- package/lib/types/index2.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/components.tsx +2 -1
- package/src/index.ts +12 -1
- package/src/match.ts +94 -28
- package/src/router.ts +336 -25
- package/src/tests/router.test.ts +629 -2
- package/src/types.ts +79 -7
package/lib/types/index2.d.ts
CHANGED
|
@@ -5,12 +5,16 @@ import { Computed, Signal } from "@pyreon/reactivity";
|
|
|
5
5
|
//#region src/types.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Extracts typed params from a path string at compile time.
|
|
8
|
+
* Supports optional params via `:param?` — their type is `string | undefined`.
|
|
8
9
|
*
|
|
9
10
|
* @example
|
|
10
11
|
* ExtractParams<'/user/:id/posts/:postId'>
|
|
11
12
|
* // → { id: string; postId: string }
|
|
13
|
+
*
|
|
14
|
+
* ExtractParams<'/user/:id?'>
|
|
15
|
+
* // → { id?: string | undefined }
|
|
12
16
|
*/
|
|
13
|
-
type ExtractParams<T extends string> = T extends `${string}:${infer Param}*/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}*` ? { [K in Param]: string } : T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? { [K in Param]: string } : Record<never, never>;
|
|
17
|
+
type ExtractParams<T extends string> = T extends `${string}:${infer Param}*/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}*` ? { [K in Param]: string } : T extends `${string}:${infer Param}?/${infer Rest}` ? { [K in Param]?: string | undefined } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}?` ? { [K in Param]?: string | undefined } : T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? { [K in Param]: string } : Record<never, never>;
|
|
14
18
|
/**
|
|
15
19
|
* Route metadata interface. Extend it via module augmentation to add custom fields:
|
|
16
20
|
*
|
|
@@ -33,7 +37,7 @@ interface RouteMeta {
|
|
|
33
37
|
/** Scroll behavior for this route */
|
|
34
38
|
scrollBehavior?: "top" | "restore" | "none";
|
|
35
39
|
}
|
|
36
|
-
interface ResolvedRoute<P extends Record<string, string> = Record<string, string>, Q extends Record<string, string> = Record<string, string>> {
|
|
40
|
+
interface ResolvedRoute<P extends Record<string, string | undefined> = Record<string, string>, Q extends Record<string, string> = Record<string, string>> {
|
|
37
41
|
path: string;
|
|
38
42
|
params: P;
|
|
39
43
|
query: Q;
|
|
@@ -63,6 +67,15 @@ type RouteComponent = ComponentFn$1 | LazyComponent;
|
|
|
63
67
|
type NavigationGuardResult = boolean | string | undefined;
|
|
64
68
|
type NavigationGuard = (to: ResolvedRoute, from: ResolvedRoute) => NavigationGuardResult | Promise<NavigationGuardResult>;
|
|
65
69
|
type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Called before each navigation. Return `true` to block, `false` to allow.
|
|
72
|
+
* Async blockers are supported (e.g. to show a confirmation dialog).
|
|
73
|
+
*/
|
|
74
|
+
type BlockerFn = (to: ResolvedRoute, from: ResolvedRoute) => boolean | Promise<boolean>;
|
|
75
|
+
interface Blocker {
|
|
76
|
+
/** Unregister this blocker so future navigations proceed freely. */
|
|
77
|
+
remove(): void;
|
|
78
|
+
}
|
|
66
79
|
interface LoaderContext {
|
|
67
80
|
params: Record<string, string>;
|
|
68
81
|
query: Record<string, string>;
|
|
@@ -88,6 +101,14 @@ interface RouteRecord<TPath extends string = string> {
|
|
|
88
101
|
beforeEnter?: NavigationGuard | NavigationGuard[];
|
|
89
102
|
/** Guard(s) run before leaving this route. Return false to cancel. */
|
|
90
103
|
beforeLeave?: NavigationGuard | NavigationGuard[];
|
|
104
|
+
/**
|
|
105
|
+
* Alternative path(s) for this route. Alias paths render the same component
|
|
106
|
+
* and share guards, loaders, and metadata with the primary path.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* { path: "/user/:id", alias: ["/profile/:id"], component: UserPage }
|
|
110
|
+
*/
|
|
111
|
+
alias?: string | string[];
|
|
91
112
|
/** Child routes rendered inside this route's component via <RouterView /> */
|
|
92
113
|
children?: RouteRecord[];
|
|
93
114
|
/**
|
|
@@ -96,6 +117,12 @@ interface RouteRecord<TPath extends string = string> {
|
|
|
96
117
|
* Receives an AbortSignal that fires if a newer navigation supersedes this one.
|
|
97
118
|
*/
|
|
98
119
|
loader?: RouteLoaderFn;
|
|
120
|
+
/**
|
|
121
|
+
* When true, the router shows cached loader data immediately (stale) and
|
|
122
|
+
* revalidates in the background. The component re-renders once fresh data arrives.
|
|
123
|
+
* Only applies when navigating to a route that already has cached loader data.
|
|
124
|
+
*/
|
|
125
|
+
staleWhileRevalidate?: boolean;
|
|
99
126
|
/** Component rendered when this route's loader throws an error */
|
|
100
127
|
errorComponent?: ComponentFn$1;
|
|
101
128
|
}
|
|
@@ -104,6 +131,13 @@ interface RouterOptions {
|
|
|
104
131
|
routes: RouteRecord[];
|
|
105
132
|
/** "hash" (default) uses location.hash; "history" uses pushState */
|
|
106
133
|
mode?: "hash" | "history";
|
|
134
|
+
/**
|
|
135
|
+
* Base path for the application. Used when deploying to a sub-path
|
|
136
|
+
* (e.g. `"/app"` for `https://example.com/app/`).
|
|
137
|
+
* Only applies in history mode. Must start with `/`.
|
|
138
|
+
* Default: `""` (no base path).
|
|
139
|
+
*/
|
|
140
|
+
base?: string;
|
|
107
141
|
/**
|
|
108
142
|
* Global scroll behavior. Per-route meta.scrollBehavior takes precedence.
|
|
109
143
|
* Default: "top"
|
|
@@ -130,6 +164,13 @@ interface RouterOptions {
|
|
|
130
164
|
* Default: 100.
|
|
131
165
|
*/
|
|
132
166
|
maxCacheSize?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Trailing slash handling:
|
|
169
|
+
* - `"strip"` — removes trailing slashes before matching (default)
|
|
170
|
+
* - `"add"` — ensures paths always end with `/`
|
|
171
|
+
* - `"ignore"` — no normalization
|
|
172
|
+
*/
|
|
173
|
+
trailingSlash?: "strip" | "add" | "ignore";
|
|
133
174
|
}
|
|
134
175
|
interface Router {
|
|
135
176
|
/** Navigate to a path */
|
|
@@ -142,8 +183,18 @@ interface Router {
|
|
|
142
183
|
}): Promise<void>;
|
|
143
184
|
/** Replace current history entry */
|
|
144
185
|
replace(path: string): Promise<void>;
|
|
145
|
-
/**
|
|
186
|
+
/** Replace current history entry using a named route */
|
|
187
|
+
replace(location: {
|
|
188
|
+
name: string;
|
|
189
|
+
params?: Record<string, string>;
|
|
190
|
+
query?: Record<string, string>;
|
|
191
|
+
}): Promise<void>;
|
|
192
|
+
/** Go back one step in history */
|
|
146
193
|
back(): void;
|
|
194
|
+
/** Go forward one step in history */
|
|
195
|
+
forward(): void;
|
|
196
|
+
/** Navigate forward or backward by `delta` steps in the history stack */
|
|
197
|
+
go(delta: number): void;
|
|
147
198
|
/** Register a global before-navigation guard. Returns an unregister function. */
|
|
148
199
|
beforeEach(guard: NavigationGuard): () => void;
|
|
149
200
|
/** Register a global after-navigation hook. Returns an unregister function. */
|
|
@@ -152,12 +203,19 @@ interface Router {
|
|
|
152
203
|
readonly currentRoute: () => ResolvedRoute;
|
|
153
204
|
/** True while a navigation (guards + loaders) is in flight */
|
|
154
205
|
readonly loading: () => boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Promise that resolves once the initial navigation is complete.
|
|
208
|
+
* Useful for SSR and for delaying rendering until the first route is resolved.
|
|
209
|
+
*/
|
|
210
|
+
isReady(): Promise<void>;
|
|
155
211
|
/** Remove all event listeners, clear caches, and abort in-flight navigations. */
|
|
156
212
|
destroy(): void;
|
|
157
213
|
}
|
|
158
214
|
interface RouterInstance extends Router {
|
|
159
215
|
routes: RouteRecord[];
|
|
160
216
|
mode: "hash" | "history";
|
|
217
|
+
/** Normalized base path (e.g. "/app"), empty string if none */
|
|
218
|
+
_base: string;
|
|
161
219
|
_currentPath: Signal<string>;
|
|
162
220
|
_currentRoute: Computed<ResolvedRoute>;
|
|
163
221
|
_componentCache: Map<RouteRecord, ComponentFn$1>;
|
|
@@ -179,6 +237,12 @@ interface RouterInstance extends Router {
|
|
|
179
237
|
_loaderData: Map<RouteRecord, unknown>;
|
|
180
238
|
/** AbortController for the in-flight loader batch — aborted when a newer navigation starts */
|
|
181
239
|
_abortController: AbortController | null;
|
|
240
|
+
/** Registered navigation blockers */
|
|
241
|
+
_blockers: Set<BlockerFn>;
|
|
242
|
+
/** Resolves the isReady() promise after initial navigation completes */
|
|
243
|
+
_readyResolve: (() => void) | null;
|
|
244
|
+
/** The isReady() promise instance */
|
|
245
|
+
_readyPromise: Promise<void>;
|
|
182
246
|
}
|
|
183
247
|
//#endregion
|
|
184
248
|
//#region src/components.d.ts
|
|
@@ -315,8 +379,57 @@ declare function findRouteByName(name: string, routes: RouteRecord[]): RouteReco
|
|
|
315
379
|
//#region src/router.d.ts
|
|
316
380
|
declare const RouterContext: _pyreon_core0.Context<RouterInstance | null>;
|
|
317
381
|
declare function useRouter(): Router;
|
|
318
|
-
declare function useRoute<TPath extends string = string>(): () => ResolvedRoute<ExtractParams<TPath>, Record<string, string>>;
|
|
382
|
+
declare function useRoute<TPath extends string = string>(): () => ResolvedRoute<ExtractParams<TPath> & Record<string, string>, Record<string, string>>;
|
|
383
|
+
/**
|
|
384
|
+
* In-component guard: called before the component's route is left.
|
|
385
|
+
* Return `false` to cancel, a string to redirect, or `undefined`/`true` to proceed.
|
|
386
|
+
* Automatically removed on component unmount.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* onBeforeRouteLeave((to, from) => {
|
|
390
|
+
* if (hasUnsavedChanges()) return false
|
|
391
|
+
* })
|
|
392
|
+
*/
|
|
393
|
+
declare function onBeforeRouteLeave(guard: NavigationGuard): () => void;
|
|
394
|
+
/**
|
|
395
|
+
* In-component guard: called when the route changes but the component is reused
|
|
396
|
+
* (e.g. `/user/1` → `/user/2`). Useful for reacting to param changes.
|
|
397
|
+
* Automatically removed on component unmount.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* onBeforeRouteUpdate((to, from) => {
|
|
401
|
+
* if (!isValidId(to.params.id)) return false
|
|
402
|
+
* })
|
|
403
|
+
*/
|
|
404
|
+
declare function onBeforeRouteUpdate(guard: NavigationGuard): () => void;
|
|
405
|
+
/**
|
|
406
|
+
* Register a navigation blocker. The `fn` callback is called before each
|
|
407
|
+
* navigation — return `true` (or resolve to `true`) to block it.
|
|
408
|
+
*
|
|
409
|
+
* Automatically removed on component unmount if called during component setup.
|
|
410
|
+
* Also installs a `beforeunload` handler so the browser shows a confirmation
|
|
411
|
+
* dialog when the user tries to close the tab while a blocker is active.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* const blocker = useBlocker((to, from) => {
|
|
415
|
+
* return hasUnsavedChanges() && !confirm("Discard changes?")
|
|
416
|
+
* })
|
|
417
|
+
* // later: blocker.remove()
|
|
418
|
+
*/
|
|
419
|
+
declare function useBlocker(fn: BlockerFn): Blocker;
|
|
420
|
+
/**
|
|
421
|
+
* Reactive read/write access to the current route's query parameters.
|
|
422
|
+
*
|
|
423
|
+
* Returns `[get, set]` where `get` is a reactive signal producing the merged
|
|
424
|
+
* query object and `set` navigates to the current path with updated params.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* const [params, setParams] = useSearchParams({ page: "1", sort: "name" })
|
|
428
|
+
* params().page // "1" if not in URL
|
|
429
|
+
* setParams({ page: "2" }) // navigates to ?page=2&sort=name
|
|
430
|
+
*/
|
|
431
|
+
declare function useSearchParams<T extends Record<string, string>>(defaults?: T): [get: () => T, set: (updates: Partial<T>) => Promise<void>];
|
|
319
432
|
declare function createRouter(options: RouterOptions | RouteRecord[]): Router;
|
|
320
433
|
//#endregion
|
|
321
|
-
export { type AfterEachHook, type ExtractParams, type LazyComponent, type LoaderContext, type NavigationGuard, type NavigationGuardResult, type ResolvedRoute, type RouteComponent, type RouteLoaderFn, type RouteMeta, type RouteRecord, type Router, RouterContext, RouterLink, type RouterLinkProps, type RouterOptions, RouterProvider, type RouterProviderProps, RouterView, type RouterViewProps, type ScrollBehaviorFn, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useLoaderData, useRoute, useRouter };
|
|
434
|
+
export { type AfterEachHook, type Blocker, type BlockerFn, type ExtractParams, type LazyComponent, type LoaderContext, type NavigationGuard, type NavigationGuardResult, type ResolvedRoute, type RouteComponent, type RouteLoaderFn, type RouteMeta, type RouteRecord, type Router, RouterContext, RouterLink, type RouterLinkProps, type RouterOptions, RouterProvider, type RouterProviderProps, RouterView, type RouterViewProps, type ScrollBehaviorFn, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useLoaderData, useRoute, useRouter, useSearchParams };
|
|
322
435
|
//# sourceMappingURL=index2.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/types.ts","../../src/components.tsx","../../src/loader.ts","../../src/match.ts","../../src/router.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/types.ts","../../src/components.tsx","../../src/loader.ts","../../src/match.ts","../../src/router.ts"],"mappings":";;;;;;;;AAiBA;;;;;;;;KAAY,aAAA,qBAAkC,CAAA,6DAClC,KAAA,cAAmB,aAAA,KAAkB,IAAA,MAC7C,CAAA,+CACU,KAAA,cACR,CAAA,6DACU,KAAA,2BAAgC,aAAA,KAAkB,IAAA,MAC1D,CAAA,+CACU,KAAA,2BACR,CAAA,4DACU,KAAA,cAAmB,aAAA,KAAkB,IAAA,MAC7C,CAAA,8CACU,KAAA,cACR,MAAA;;;;;;;;;;;;;UAgBG,SAAA;EAzBR;EA2BP,KAAA;EA1BI;EA4BJ,WAAA;EA5BiD;EA8BjD,YAAA;EA7Bc;EA+Bd,cAAA;AAAA;AAAA,UAKe,aAAA,WACL,MAAA,+BAAqC,MAAA,4BACrC,MAAA,mBAAyB,MAAA;EAEnC,IAAA;EACA,MAAA,EAAQ,CAAA;EACR,KAAA,EAAO,CAAA;EACP,IAAA;EAxCqC;EA0CrC,OAAA,EAAS,WAAA;EACT,IAAA,EAAM,SAAA;AAAA;AAAA,cAKK,WAAA;AAAA,UAEI,aAAA;EAAA,UACL,WAAA;EAAA,SACD,MAAA,QAAc,OAAA,CAAQ,aAAA;IAAgB,OAAA,EAAS,aAAA;EAAA;EAhD5C;EAAA,SAkDH,gBAAA,GAAmB,aAAA;EAlDV;EAAA,SAoDT,cAAA,GAAiB,aAAA;AAAA;AAAA,iBAGZ,IAAA,CACd,MAAA,QAAc,OAAA,CAAQ,aAAA;EAAgB,OAAA,EAAS,aAAA;AAAA,IAC/C,OAAA;EAAY,OAAA,GAAU,aAAA;EAAa,KAAA,GAAQ,aAAA;AAAA,IAC1C,aAAA;AAAA,KAaS,cAAA,GAAiB,aAAA,GAAc,aAAA;AAAA,KAI/B,qBAAA;AAAA,KACA,eAAA,IACV,EAAA,EAAI,aAAA,EACJ,IAAA,EAAM,aAAA,KACH,qBAAA,GAAwB,OAAA,CAAQ,qBAAA;AAAA,KAEzB,aAAA,IAAiB,EAAA,EAAI,aAAA,EAAe,IAAA,EAAM,aAAA;;;;;KAQ1C,SAAA,IAAa,EAAA,EAAI,aAAA,EAAe,IAAA,EAAM,aAAA,eAA4B,OAAA;AAAA,UAE7D,OAAA;EArDN;EAuDT,MAAA;AAAA;AAAA,UAKe,aAAA;EACf,MAAA,EAAQ,MAAA;EACR,KAAA,EAAO,MAAA;EAtEwC;EAwE/C,MAAA,EAAQ,WAAA;AAAA;AAAA,KAGE,aAAA,IAAiB,GAAA,EAAK,aAAA,KAAkB,OAAA;AAAA,UAInC,WAAA;EA3Ef;EA6EA,IAAA,EAAM,KAAA;EACN,SAAA,EAAW,cAAA;EA7EJ;EA+EP,IAAA;EA5EA;EA8EA,IAAA,GAAO,SAAA;EA7EP;;;;AAKF;EA8EE,QAAA,cAAsB,EAAA,EAAI,aAAA;;EAE1B,WAAA,GAAc,eAAA,GAAkB,eAAA;EAhFc;EAkF9C,WAAA,GAAc,eAAA,GAAkB,eAAA;EAhFJ;;;;;;;EAwF5B,KAAA;EAlFqC;EAoFrC,QAAA,GAAW,WAAA;EAzFD;;;;;EA+FV,MAAA,GAAS,aAAA;EA5FA;;;;;EAkGT,oBAAA;EA7Fc;EA+Fd,cAAA,GAAiB,aAAA;AAAA;AAAA,KAKP,gBAAA,IACV,EAAA,EAAI,aAAA,EACJ,IAAA,EAAM,aAAA,EACN,aAAA;AAAA,UAGe,aAAA;EACf,MAAA,EAAQ,WAAA;EAzGc;EA2GtB,IAAA;EA1GC;;;;;;EAiHD,IAAA;EAnHA;;;;EAwHA,cAAA,GAAiB,gBAAA;EAvHjB;;;;AAcF;;;;EAkHE,GAAA;EA9GU;;;;;EAoHV,OAAA,IAAW,GAAA,WAAc,KAAA,EAAO,aAAA;EAnHP;;;;;EAyHzB,YAAA;EAtH2B;;;;;;EA6H3B,aAAA;AAAA;AAAA,UAKe,MAAA;EAlIoB;EAoInC,IAAA,CAAK,IAAA,WAAe,OAAA;EApIoC;EAsIxD,IAAA,CAAK,QAAA;IACH,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA,GAAQ,MAAA;EAAA,IACN,OAAA;EAxIuB;EA0I3B,OAAA,CAAQ,IAAA,WAAe,OAAA;EA1IuB;EA4I9C,OAAA,CAAQ,QAAA;IACN,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA,GAAQ,MAAA;EAAA,IACN,OAAA;EAxIuB;EA0I3B,IAAA;EA1I4E;EA4I5E,OAAA;EA5ImF;EA8InF,EAAA,CAAG,KAAA;EA9IoB;EAgJvB,UAAA,CAAW,KAAA,EAAO,eAAA;EAhJwB;EAkJ1C,SAAA,CAAU,IAAA,EAAM,aAAA;EAlJmE;EAAA,SAoJ1E,YAAA,QAAoB,aAAA;EAlJd;EAAA,SAoJN,OAAA;;;;AA7IX;EAkJE,OAAA,IAAW,OAAA;;EAEX,OAAA;AAAA;AAAA,UAOe,cAAA,SAAuB,MAAA;EACtC,MAAA,EAAQ,WAAA;EACR,IAAA;EA5JQ;EA8JR,KAAA;EACA,YAAA,EAAc,MAAA;EACd,aAAA,EAAe,QAAA,CAAS,aAAA;EACxB,eAAA,EAAiB,GAAA,CAAI,WAAA,EAAa,aAAA;EAClC,cAAA,EAAgB,MAAA;EAChB,QAAA,CAAS,OAAA,WAAkB,aAAA;EAC3B,gBAAA,EAAkB,GAAA;EAClB,eAAA,EAAiB,aAAA;EACjB,QAAA,EAAU,aAAA;EACV,aAAA;EAjKgC;;;;;EAuKhC,UAAA;EAnK0B;EAqK1B,cAAA,EAAgB,GAAA,CAAI,WAAA;EAnKd;EAqKN,WAAA,EAAa,GAAA,CAAI,WAAA;EAhKV;EAkKP,gBAAA,EAAkB,eAAA;EA1JJ;EA4Jd,SAAA,EAAW,GAAA,CAAI,SAAA;EA1JD;EA4Jd,aAAA;EAlJW;EAoJX,aAAA,EAAe,OAAA;AAAA;;;UC9SA,mBAAA,SAA4B,KAAA;EAC3C,MAAA,EAAQ,MAAA;EACR,QAAA,GAAW,KAAA,GAAQ,UAAA;AAAA;AAAA,cAGR,cAAA,EAAgB,WAAA,CAAY,mBAAA;AAAA,UAqBxB,eAAA,SAAwB,KAAA;EDpBK;ECsB5C,MAAA,GAAS,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;cA0BE,UAAA,EAAY,WAAA,CAAY,eAAA;AAAA,UA4CpB,eAAA,SAAwB,KAAA;EACvC,EAAA;ED3FE;EC6FF,OAAA;ED5FO;EC8FP,WAAA;ED7FI;EC+FJ,gBAAA;ED/FiD;ECiGjD,KAAA;EDhGc;;;;;;ECuGd,QAAA;EACA,QAAA,GAAW,UAAA;AAAA;AAAA,cAGA,UAAA,EAAY,WAAA,CAAY,eAAA;;;;;;;;;;;;;;;iBC3GrB,aAAA,aAAA,CAAA,GAA8B,CAAA;;;;;;;;;;iBAaxB,kBAAA,CAAmB,MAAA,EAAQ,cAAA,EAAgB,IAAA,WAAe,OAAA;;;;;;;;;;;;iBA6BhE,mBAAA,CAAoB,MAAA,EAAQ,cAAA,GAAiB,MAAA;;;;;;;;;;;;;;iBAqB7C,iBAAA,CACd,MAAA,EAAQ,cAAA,EACR,UAAA,EAAY,MAAA;;;;;;;iBC/EE,UAAA,CAAW,EAAA,WAAa,MAAA;;;;;;;;iBAwBxB,eAAA,CAAgB,EAAA,WAAa,MAAA;AAAA,iBA2B7B,cAAA,CAAe,KAAA,EAAO,MAAA;;;;;iBA4dtB,YAAA,CAAa,OAAA,UAAiB,MAAA,EAAQ,WAAA,KAAgB,aAAA;;iBA0FtD,SAAA,CAAU,OAAA,UAAiB,MAAA,EAAQ,MAAA;;iBAgBnC,eAAA,CAAgB,IAAA,UAAc,MAAA,EAAQ,WAAA,KAAgB,WAAA;;;cClmBzD,aAAA,EAAa,aAAA,CAAA,OAAA,CAAA,cAAA;AAAA,iBAiBV,SAAA,CAAA,GAAa,MAAA;AAAA,iBASb,QAAA,+BAAA,CAAA,SAAiD,aAAA,CAC1B,aAAA,CAAL,KAAA,IAAS,MAAA,kBACzC,MAAA;;;;;;;;;;;iBAoBc,kBAAA,CAAmB,KAAA,EAAO,eAAA;;;;;;;;;;;iBA6B1B,mBAAA,CAAoB,KAAA,EAAO,eAAA;;;;;;;;;;;;;;;iBAgC3B,UAAA,CAAW,EAAA,EAAI,SAAA,GAAY,OAAA;;;;;;;;;;;;iBA0C3B,eAAA,WAA0B,MAAA,iBAAA,CACxC,QAAA,GAAW,CAAA,IACT,GAAA,QAAW,CAAA,EAAG,GAAA,GAAM,OAAA,EAAS,OAAA,CAAQ,CAAA,MAAO,OAAA;AAAA,iBAqBhC,YAAA,CAAa,OAAA,EAAS,aAAA,GAAgB,WAAA,KAAgB,MAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Official router for Pyreon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pyreon/core": "^0.
|
|
43
|
-
"@pyreon/reactivity": "^0.
|
|
44
|
-
"@pyreon/runtime-dom": "^0.
|
|
42
|
+
"@pyreon/core": "^0.5.0",
|
|
43
|
+
"@pyreon/reactivity": "^0.5.0",
|
|
44
|
+
"@pyreon/runtime-dom": "^0.5.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@happy-dom/global-registrator": "^20.8.3",
|
package/src/components.tsx
CHANGED
|
@@ -146,7 +146,8 @@ export const RouterLink: ComponentFn<RouterLinkProps> = (props) => {
|
|
|
146
146
|
prefetchRoute(router as RouterInstance, props.to)
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
const
|
|
149
|
+
const inst = router as RouterInstance | null
|
|
150
|
+
const href = inst?.mode === "history" ? `${inst._base}${props.to}` : `#${props.to}`
|
|
150
151
|
|
|
151
152
|
const activeClass = (): string => {
|
|
152
153
|
if (!router) return ""
|
package/src/index.ts
CHANGED
|
@@ -55,11 +55,22 @@ export {
|
|
|
55
55
|
stringifyQuery,
|
|
56
56
|
} from "./match"
|
|
57
57
|
// Router factory + hooks
|
|
58
|
-
export {
|
|
58
|
+
export {
|
|
59
|
+
createRouter,
|
|
60
|
+
onBeforeRouteLeave,
|
|
61
|
+
onBeforeRouteUpdate,
|
|
62
|
+
RouterContext,
|
|
63
|
+
useBlocker,
|
|
64
|
+
useRoute,
|
|
65
|
+
useRouter,
|
|
66
|
+
useSearchParams,
|
|
67
|
+
} from "./router"
|
|
59
68
|
// Types
|
|
60
69
|
// Data loaders
|
|
61
70
|
export type {
|
|
62
71
|
AfterEachHook,
|
|
72
|
+
Blocker,
|
|
73
|
+
BlockerFn,
|
|
63
74
|
ExtractParams,
|
|
64
75
|
LazyComponent,
|
|
65
76
|
LoaderContext,
|
package/src/match.ts
CHANGED
|
@@ -78,7 +78,9 @@ interface CompiledSegment {
|
|
|
78
78
|
isParam: boolean
|
|
79
79
|
/** true if this segment is a `:param*` splat */
|
|
80
80
|
isSplat: boolean
|
|
81
|
-
/**
|
|
81
|
+
/** true if this segment is a `:param?` optional */
|
|
82
|
+
isOptional: boolean
|
|
83
|
+
/** Param name (without leading `:` and trailing `*`/`?`) — empty for static segments */
|
|
82
84
|
paramName: string
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -122,6 +124,10 @@ interface FlattenedRoute {
|
|
|
122
124
|
hasSplat: boolean
|
|
123
125
|
/** true if this is a wildcard catch-all route (`*` or `(.*)`) */
|
|
124
126
|
isWildcard: boolean
|
|
127
|
+
/** true if any segment is optional (`:param?`) */
|
|
128
|
+
hasOptional: boolean
|
|
129
|
+
/** Minimum number of segments that must be present (excluding trailing optionals) */
|
|
130
|
+
minSegments: number
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
/** WeakMap cache: compile each RouteRecord[] once */
|
|
@@ -129,12 +135,15 @@ const _compiledCache = new WeakMap<RouteRecord[], CompiledRoute[]>()
|
|
|
129
135
|
|
|
130
136
|
function compileSegment(raw: string): CompiledSegment {
|
|
131
137
|
if (raw.endsWith("*") && raw.startsWith(":")) {
|
|
132
|
-
return { raw, isParam: true, isSplat: true, paramName: raw.slice(1, -1) }
|
|
138
|
+
return { raw, isParam: true, isSplat: true, isOptional: false, paramName: raw.slice(1, -1) }
|
|
139
|
+
}
|
|
140
|
+
if (raw.endsWith("?") && raw.startsWith(":")) {
|
|
141
|
+
return { raw, isParam: true, isSplat: false, isOptional: true, paramName: raw.slice(1, -1) }
|
|
133
142
|
}
|
|
134
143
|
if (raw.startsWith(":")) {
|
|
135
|
-
return { raw, isParam: true, isSplat: false, paramName: raw.slice(1) }
|
|
144
|
+
return { raw, isParam: true, isSplat: false, isOptional: false, paramName: raw.slice(1) }
|
|
136
145
|
}
|
|
137
|
-
return { raw, isParam: false, isSplat: false, paramName: "" }
|
|
146
|
+
return { raw, isParam: false, isSplat: false, isOptional: false, paramName: "" }
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
function compileRoute(route: RouteRecord): CompiledRoute {
|
|
@@ -172,17 +181,32 @@ function compileRoute(route: RouteRecord): CompiledRoute {
|
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
/** Expand alias paths into additional compiled entries sharing the original RouteRecord */
|
|
185
|
+
function expandAliases(r: RouteRecord, c: CompiledRoute): CompiledRoute[] {
|
|
186
|
+
if (!r.alias) return []
|
|
187
|
+
const aliases = Array.isArray(r.alias) ? r.alias : [r.alias]
|
|
188
|
+
return aliases.map((aliasPath) => {
|
|
189
|
+
const { alias: _, ...withoutAlias } = r
|
|
190
|
+
const ac = compileRoute({ ...withoutAlias, path: aliasPath })
|
|
191
|
+
ac.children = c.children
|
|
192
|
+
ac.route = r
|
|
193
|
+
return ac
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
175
197
|
function compileRoutes(routes: RouteRecord[]): CompiledRoute[] {
|
|
176
198
|
const cached = _compiledCache.get(routes)
|
|
177
199
|
if (cached) return cached
|
|
178
200
|
|
|
179
|
-
const compiled =
|
|
201
|
+
const compiled: CompiledRoute[] = []
|
|
202
|
+
for (const r of routes) {
|
|
180
203
|
const c = compileRoute(r)
|
|
181
204
|
if (r.children && r.children.length > 0) {
|
|
182
205
|
c.children = compileRoutes(r.children)
|
|
183
206
|
}
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
compiled.push(c)
|
|
208
|
+
compiled.push(...expandAliases(r, c))
|
|
209
|
+
}
|
|
186
210
|
_compiledCache.set(routes, compiled)
|
|
187
211
|
return compiled
|
|
188
212
|
}
|
|
@@ -204,6 +228,12 @@ function makeFlatEntry(
|
|
|
204
228
|
isWildcard: boolean,
|
|
205
229
|
): FlattenedRoute {
|
|
206
230
|
const isStatic = !isWildcard && segments.every((s) => !s.isParam)
|
|
231
|
+
const hasOptional = segments.some((s) => s.isOptional)
|
|
232
|
+
// minSegments: count of segments up to and not including trailing optionals
|
|
233
|
+
let minSegs = segments.length
|
|
234
|
+
if (hasOptional) {
|
|
235
|
+
while (minSegs > 0 && segments[minSegs - 1]?.isOptional) minSegs--
|
|
236
|
+
}
|
|
207
237
|
return {
|
|
208
238
|
segments,
|
|
209
239
|
segmentCount: segments.length,
|
|
@@ -214,6 +244,8 @@ function makeFlatEntry(
|
|
|
214
244
|
firstSegment: getFirstSegment(segments),
|
|
215
245
|
hasSplat: segments.some((s) => s.isSplat),
|
|
216
246
|
isWildcard,
|
|
247
|
+
hasOptional,
|
|
248
|
+
minSegments: minSegs,
|
|
217
249
|
}
|
|
218
250
|
}
|
|
219
251
|
|
|
@@ -372,8 +404,31 @@ function decodeSafe(s: string): string {
|
|
|
372
404
|
* - Param segments: "/user/:id"
|
|
373
405
|
* - Wildcard: "(.*)" matches everything
|
|
374
406
|
*/
|
|
407
|
+
/** Match a single pattern segment against a path segment, extracting params. Returns false on mismatch. */
|
|
408
|
+
function matchPatternSegment(
|
|
409
|
+
pp: string,
|
|
410
|
+
pt: string | undefined,
|
|
411
|
+
params: Record<string, string>,
|
|
412
|
+
pathParts: string[],
|
|
413
|
+
i: number,
|
|
414
|
+
): "splat" | "continue" | "fail" {
|
|
415
|
+
if (pp.endsWith("*") && pp.startsWith(":")) {
|
|
416
|
+
params[pp.slice(1, -1)] = pathParts.slice(i).map(decodeURIComponent).join("/")
|
|
417
|
+
return "splat"
|
|
418
|
+
}
|
|
419
|
+
if (pp.endsWith("?") && pp.startsWith(":")) {
|
|
420
|
+
if (pt !== undefined) params[pp.slice(1, -1)] = decodeURIComponent(pt)
|
|
421
|
+
return "continue"
|
|
422
|
+
}
|
|
423
|
+
if (pt === undefined) return "fail"
|
|
424
|
+
if (pp.startsWith(":")) {
|
|
425
|
+
params[pp.slice(1)] = decodeURIComponent(pt)
|
|
426
|
+
return "continue"
|
|
427
|
+
}
|
|
428
|
+
return pp === pt ? "continue" : "fail"
|
|
429
|
+
}
|
|
430
|
+
|
|
375
431
|
export function matchPath(pattern: string, path: string): Record<string, string> | null {
|
|
376
|
-
// Wildcard pattern
|
|
377
432
|
if (pattern === "(.*)" || pattern === "*") return {}
|
|
378
433
|
|
|
379
434
|
const patternParts = pattern.split("/").filter(Boolean)
|
|
@@ -381,22 +436,18 @@ export function matchPath(pattern: string, path: string): Record<string, string>
|
|
|
381
436
|
|
|
382
437
|
const params: Record<string, string> = {}
|
|
383
438
|
for (let i = 0; i < patternParts.length; i++) {
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
params[pp.slice(1)] = decodeURIComponent(pt)
|
|
394
|
-
} else if (pp !== pt) {
|
|
395
|
-
return null
|
|
396
|
-
}
|
|
439
|
+
const result = matchPatternSegment(
|
|
440
|
+
patternParts[i] as string,
|
|
441
|
+
pathParts[i],
|
|
442
|
+
params,
|
|
443
|
+
pathParts,
|
|
444
|
+
i,
|
|
445
|
+
)
|
|
446
|
+
if (result === "splat") return params
|
|
447
|
+
if (result === "fail") return null
|
|
397
448
|
}
|
|
398
449
|
|
|
399
|
-
if (
|
|
450
|
+
if (pathParts.length > patternParts.length) return null
|
|
400
451
|
return params
|
|
401
452
|
}
|
|
402
453
|
|
|
@@ -414,16 +465,21 @@ function captureSplat(pathParts: string[], from: number, pathLen: number): strin
|
|
|
414
465
|
|
|
415
466
|
// ─── Flattened route matching ─────────────────────────────────────────────────
|
|
416
467
|
|
|
468
|
+
/** Check whether a flattened route's segment count is compatible with the path length */
|
|
469
|
+
function isSegmentCountCompatible(f: FlattenedRoute, pathLen: number): boolean {
|
|
470
|
+
if (f.segmentCount === pathLen) return true
|
|
471
|
+
if (f.hasSplat && pathLen >= f.segmentCount) return true
|
|
472
|
+
if (f.hasOptional && pathLen >= f.minSegments && pathLen <= f.segmentCount) return true
|
|
473
|
+
return false
|
|
474
|
+
}
|
|
475
|
+
|
|
417
476
|
/** Try to match a flattened route against path parts */
|
|
418
477
|
function matchFlattened(
|
|
419
478
|
f: FlattenedRoute,
|
|
420
479
|
pathParts: string[],
|
|
421
480
|
pathLen: number,
|
|
422
481
|
): Record<string, string> | null {
|
|
423
|
-
if (f
|
|
424
|
-
// Could still match if route has a splat
|
|
425
|
-
if (!f.hasSplat || pathLen < f.segmentCount) return null
|
|
426
|
-
}
|
|
482
|
+
if (!isSegmentCountCompatible(f, pathLen)) return null
|
|
427
483
|
|
|
428
484
|
const params: Record<string, string> = {}
|
|
429
485
|
const segments = f.segments
|
|
@@ -431,11 +487,15 @@ function matchFlattened(
|
|
|
431
487
|
for (let i = 0; i < count; i++) {
|
|
432
488
|
const seg = segments[i]
|
|
433
489
|
const pt = pathParts[i]
|
|
434
|
-
if (!seg
|
|
490
|
+
if (!seg) return null
|
|
435
491
|
if (seg.isSplat) {
|
|
436
492
|
params[seg.paramName] = captureSplat(pathParts, i, pathLen)
|
|
437
493
|
return params
|
|
438
494
|
}
|
|
495
|
+
if (pt === undefined) {
|
|
496
|
+
if (!seg.isOptional) return null
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
439
499
|
if (seg.isParam) {
|
|
440
500
|
params[seg.paramName] = decodeSafe(pt)
|
|
441
501
|
} else if (seg.raw !== pt) {
|
|
@@ -564,7 +624,13 @@ function mergeMeta(matched: RouteRecord[]): RouteMeta {
|
|
|
564
624
|
|
|
565
625
|
/** Build a path string from a named route's pattern and params */
|
|
566
626
|
export function buildPath(pattern: string, params: Record<string, string>): string {
|
|
567
|
-
|
|
627
|
+
const built = pattern.replace(/\/:([^/]+)\?/g, (_match, key) => {
|
|
628
|
+
const val = params[key]
|
|
629
|
+
// Optional param — omit the entire segment if no value provided
|
|
630
|
+
if (!val) return ""
|
|
631
|
+
return `/${encodeURIComponent(val)}`
|
|
632
|
+
})
|
|
633
|
+
return built.replace(/:([^/]+)\*?/g, (match, key) => {
|
|
568
634
|
const val = params[key] ?? ""
|
|
569
635
|
// Splat params contain slashes — don't encode them
|
|
570
636
|
if (match.endsWith("*")) return val.split("/").map(encodeURIComponent).join("/")
|