@pyreon/router 0.3.0 → 0.4.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/src/types.ts CHANGED
@@ -6,20 +6,28 @@ export type { ComponentFn }
6
6
 
7
7
  /**
8
8
  * Extracts typed params from a path string at compile time.
9
+ * Supports optional params via `:param?` — their type is `string | undefined`.
9
10
  *
10
11
  * @example
11
12
  * ExtractParams<'/user/:id/posts/:postId'>
12
13
  * // → { id: string; postId: string }
14
+ *
15
+ * ExtractParams<'/user/:id?'>
16
+ * // → { id?: string | undefined }
13
17
  */
14
18
  export type ExtractParams<T extends string> = T extends `${string}:${infer Param}*/${infer Rest}`
15
19
  ? { [K in Param]: string } & ExtractParams<`/${Rest}`>
16
20
  : T extends `${string}:${infer Param}*`
17
21
  ? { [K in Param]: string }
18
- : T extends `${string}:${infer Param}/${infer Rest}`
19
- ? { [K in Param]: string } & ExtractParams<`/${Rest}`>
20
- : T extends `${string}:${infer Param}`
21
- ? { [K in Param]: string }
22
- : Record<never, never>
22
+ : T extends `${string}:${infer Param}?/${infer Rest}`
23
+ ? { [K in Param]?: string | undefined } & ExtractParams<`/${Rest}`>
24
+ : T extends `${string}:${infer Param}?`
25
+ ? { [K in Param]?: string | undefined }
26
+ : T extends `${string}:${infer Param}/${infer Rest}`
27
+ ? { [K in Param]: string } & ExtractParams<`/${Rest}`>
28
+ : T extends `${string}:${infer Param}`
29
+ ? { [K in Param]: string }
30
+ : Record<never, never>
23
31
 
24
32
  // ─── Route meta ───────────────────────────────────────────────────────────────
25
33
 
@@ -49,7 +57,7 @@ export interface RouteMeta {
49
57
  // ─── Resolved route ───────────────────────────────────────────────────────────
50
58
 
51
59
  export interface ResolvedRoute<
52
- P extends Record<string, string> = Record<string, string>,
60
+ P extends Record<string, string | undefined> = Record<string, string>,
53
61
  Q extends Record<string, string> = Record<string, string>,
54
62
  > {
55
63
  path: string
@@ -102,6 +110,19 @@ export type NavigationGuard = (
102
110
 
103
111
  export type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void
104
112
 
113
+ // ─── Navigation blockers ──────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Called before each navigation. Return `true` to block, `false` to allow.
117
+ * Async blockers are supported (e.g. to show a confirmation dialog).
118
+ */
119
+ export type BlockerFn = (to: ResolvedRoute, from: ResolvedRoute) => boolean | Promise<boolean>
120
+
121
+ export interface Blocker {
122
+ /** Unregister this blocker so future navigations proceed freely. */
123
+ remove(): void
124
+ }
125
+
105
126
  // ─── Route loaders ────────────────────────────────────────────────────────────
106
127
 
107
128
  export interface LoaderContext {
@@ -133,6 +154,14 @@ export interface RouteRecord<TPath extends string = string> {
133
154
  beforeEnter?: NavigationGuard | NavigationGuard[]
134
155
  /** Guard(s) run before leaving this route. Return false to cancel. */
135
156
  beforeLeave?: NavigationGuard | NavigationGuard[]
157
+ /**
158
+ * Alternative path(s) for this route. Alias paths render the same component
159
+ * and share guards, loaders, and metadata with the primary path.
160
+ *
161
+ * @example
162
+ * { path: "/user/:id", alias: ["/profile/:id"], component: UserPage }
163
+ */
164
+ alias?: string | string[]
136
165
  /** Child routes rendered inside this route's component via <RouterView /> */
137
166
  children?: RouteRecord[]
138
167
  /**
@@ -141,6 +170,12 @@ export interface RouteRecord<TPath extends string = string> {
141
170
  * Receives an AbortSignal that fires if a newer navigation supersedes this one.
142
171
  */
143
172
  loader?: RouteLoaderFn
173
+ /**
174
+ * When true, the router shows cached loader data immediately (stale) and
175
+ * revalidates in the background. The component re-renders once fresh data arrives.
176
+ * Only applies when navigating to a route that already has cached loader data.
177
+ */
178
+ staleWhileRevalidate?: boolean
144
179
  /** Component rendered when this route's loader throws an error */
145
180
  errorComponent?: ComponentFn
146
181
  }
@@ -157,6 +192,13 @@ export interface RouterOptions {
157
192
  routes: RouteRecord[]
158
193
  /** "hash" (default) uses location.hash; "history" uses pushState */
159
194
  mode?: "hash" | "history"
195
+ /**
196
+ * Base path for the application. Used when deploying to a sub-path
197
+ * (e.g. `"/app"` for `https://example.com/app/`).
198
+ * Only applies in history mode. Must start with `/`.
199
+ * Default: `""` (no base path).
200
+ */
201
+ base?: string
160
202
  /**
161
203
  * Global scroll behavior. Per-route meta.scrollBehavior takes precedence.
162
204
  * Default: "top"
@@ -183,6 +225,13 @@ export interface RouterOptions {
183
225
  * Default: 100.
184
226
  */
185
227
  maxCacheSize?: number
228
+ /**
229
+ * Trailing slash handling:
230
+ * - `"strip"` — removes trailing slashes before matching (default)
231
+ * - `"add"` — ensures paths always end with `/`
232
+ * - `"ignore"` — no normalization
233
+ */
234
+ trailingSlash?: "strip" | "add" | "ignore"
186
235
  }
187
236
 
188
237
  // ─── Router interface ─────────────────────────────────────────────────────────
@@ -198,8 +247,18 @@ export interface Router {
198
247
  }): Promise<void>
199
248
  /** Replace current history entry */
200
249
  replace(path: string): Promise<void>
201
- /** Go back */
250
+ /** Replace current history entry using a named route */
251
+ replace(location: {
252
+ name: string
253
+ params?: Record<string, string>
254
+ query?: Record<string, string>
255
+ }): Promise<void>
256
+ /** Go back one step in history */
202
257
  back(): void
258
+ /** Go forward one step in history */
259
+ forward(): void
260
+ /** Navigate forward or backward by `delta` steps in the history stack */
261
+ go(delta: number): void
203
262
  /** Register a global before-navigation guard. Returns an unregister function. */
204
263
  beforeEach(guard: NavigationGuard): () => void
205
264
  /** Register a global after-navigation hook. Returns an unregister function. */
@@ -208,6 +267,11 @@ export interface Router {
208
267
  readonly currentRoute: () => ResolvedRoute
209
268
  /** True while a navigation (guards + loaders) is in flight */
210
269
  readonly loading: () => boolean
270
+ /**
271
+ * Promise that resolves once the initial navigation is complete.
272
+ * Useful for SSR and for delaying rendering until the first route is resolved.
273
+ */
274
+ isReady(): Promise<void>
211
275
  /** Remove all event listeners, clear caches, and abort in-flight navigations. */
212
276
  destroy(): void
213
277
  }
@@ -219,6 +283,8 @@ import type { Computed, Signal } from "@pyreon/reactivity"
219
283
  export interface RouterInstance extends Router {
220
284
  routes: RouteRecord[]
221
285
  mode: "hash" | "history"
286
+ /** Normalized base path (e.g. "/app"), empty string if none */
287
+ _base: string
222
288
  _currentPath: Signal<string>
223
289
  _currentRoute: Computed<ResolvedRoute>
224
290
  _componentCache: Map<RouteRecord, ComponentFn>
@@ -240,4 +306,10 @@ export interface RouterInstance extends Router {
240
306
  _loaderData: Map<RouteRecord, unknown>
241
307
  /** AbortController for the in-flight loader batch — aborted when a newer navigation starts */
242
308
  _abortController: AbortController | null
309
+ /** Registered navigation blockers */
310
+ _blockers: Set<BlockerFn>
311
+ /** Resolves the isReady() promise after initial navigation completes */
312
+ _readyResolve: (() => void) | null
313
+ /** The isReady() promise instance */
314
+ _readyPromise: Promise<void>
243
315
  }