@pyreon/router 0.12.4 → 0.12.5

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.
@@ -36,6 +36,8 @@ interface RouteMeta {
36
36
  requiresAuth?: boolean;
37
37
  /** Scroll behavior for this route */
38
38
  scrollBehavior?: 'top' | 'restore' | 'none';
39
+ /** Set to false to disable View Transitions API for this route. Default: true */
40
+ viewTransition?: boolean;
39
41
  }
40
42
  interface ResolvedRoute<P extends Record<string, string | undefined> = Record<string, string>, Q extends Record<string, string> = Record<string, string>> {
41
43
  path: string;
@@ -67,6 +69,25 @@ type RouteComponent = ComponentFn$1 | LazyComponent;
67
69
  type NavigationGuardResult = boolean | string | undefined;
68
70
  type NavigationGuard = (to: ResolvedRoute, from: ResolvedRoute) => NavigationGuardResult | Promise<NavigationGuardResult>;
69
71
  type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void;
72
+ /**
73
+ * Context object passed through the middleware chain.
74
+ * Middleware can read/write arbitrary data on `ctx.data`.
75
+ */
76
+ interface RouteMiddlewareContext {
77
+ /** The route being navigated to. */
78
+ to: ResolvedRoute;
79
+ /** The route being navigated from. */
80
+ from: ResolvedRoute;
81
+ /** Shared data — middleware can accumulate state here for downstream middleware/components. */
82
+ data: Record<string, unknown>;
83
+ }
84
+ /**
85
+ * Route middleware function. Called before guards.
86
+ * - Return nothing/undefined to continue
87
+ * - Return `false` to cancel navigation
88
+ * - Return a string to redirect
89
+ */
90
+ type RouteMiddleware = (ctx: RouteMiddlewareContext) => void | false | string | Promise<void | false | string>;
70
91
  /**
71
92
  * Called before each navigation. Return `true` to block, `false` to allow.
72
93
  * Async blockers are supported (e.g. to show a confirmation dialog).
@@ -125,6 +146,8 @@ interface RouteRecord<TPath extends string = string> {
125
146
  staleWhileRevalidate?: boolean;
126
147
  /** Component rendered when this route's loader throws an error */
127
148
  errorComponent?: ComponentFn$1;
149
+ /** Per-route middleware — runs before guards, can accumulate context data. */
150
+ middleware?: RouteMiddleware | RouteMiddleware[];
128
151
  }
129
152
  type ScrollBehaviorFn = (to: ResolvedRoute, from: ResolvedRoute, savedPosition: number | null) => 'top' | 'restore' | 'none' | number;
130
153
  interface RouterOptions {
@@ -172,12 +195,23 @@ interface RouterOptions {
172
195
  */
173
196
  trailingSlash?: 'strip' | 'add' | 'ignore';
174
197
  }
175
- interface Router {
198
+ /**
199
+ * Router interface. Parameterized by route name union for type-safe named navigation.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * type MyRoutes = 'home' | 'user' | 'settings'
204
+ * const router: Router<MyRoutes> = createRouter({ routes })
205
+ * router.push({ name: 'user', params: { id: '42' } }) // ✓
206
+ * router.push({ name: 'typo' }) // TS error
207
+ * ```
208
+ */
209
+ interface Router<TNames extends string = string> {
176
210
  /** Navigate to a path */
177
211
  push(path: string): Promise<void>;
178
- /** Navigate to a path by name */
212
+ /** Navigate to a named route */
179
213
  push(location: {
180
- name: string;
214
+ name: TNames;
181
215
  params?: Record<string, string>;
182
216
  query?: Record<string, string>;
183
217
  }): Promise<void>;
@@ -185,7 +219,7 @@ interface Router {
185
219
  replace(path: string): Promise<void>;
186
220
  /** Replace current history entry using a named route */
187
221
  replace(location: {
188
- name: string;
222
+ name: TNames;
189
223
  params?: Record<string, string>;
190
224
  query?: Record<string, string>;
191
225
  }): Promise<void>;
@@ -446,8 +480,84 @@ declare function useBlocker(fn: BlockerFn): Blocker;
446
480
  * ```
447
481
  */
448
482
  declare function useIsActive(path: string, exact?: boolean): () => boolean;
483
+ /** Schema entry for typed search params. */
484
+ type SearchParamSchema = {
485
+ [key: string]: 'string' | 'number' | 'boolean';
486
+ };
487
+ /** Infer the typed result from a search param schema. */
488
+ type InferSearchParams<T extends SearchParamSchema> = { [K in keyof T]: T[K] extends 'number' ? number : T[K] extends 'boolean' ? boolean : string };
489
+ /**
490
+ * Read and write URL search params reactively.
491
+ *
492
+ * @example Basic (untyped)
493
+ * ```ts
494
+ * const [params, setParams] = useSearchParams({ page: "1" })
495
+ * params().page // "1"
496
+ * setParams({ page: "2" }) // updates URL
497
+ * ```
498
+ *
499
+ * @example Typed with schema
500
+ * ```ts
501
+ * const [params, setParams] = useSearchParams({
502
+ * page: 'number',
503
+ * sort: 'string',
504
+ * desc: 'boolean',
505
+ * })
506
+ * params().page // number (auto-coerced)
507
+ * params().desc // boolean
508
+ * ```
509
+ */
449
510
  declare function useSearchParams<T extends Record<string, string>>(defaults?: T): [get: () => T, set: (updates: Partial<T>) => Promise<void>];
511
+ /**
512
+ * Typed search params with auto-coercion.
513
+ *
514
+ * Schema values define the type: `'string'`, `'number'`, or `'boolean'`.
515
+ * Query string values are automatically coerced to the declared type.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * const [params, setParams] = useTypedSearchParams({
520
+ * page: 'number',
521
+ * sort: 'string',
522
+ * desc: 'boolean',
523
+ * })
524
+ * params().page // number (coerced from "3" → 3)
525
+ * params().desc // boolean (coerced from "true" → true)
526
+ * setParams({ page: 2 }) // updates URL with ?page=2
527
+ * ```
528
+ */
529
+ declare function useTypedSearchParams<T extends SearchParamSchema>(schema: T): [get: () => InferSearchParams<T>, set: (updates: Partial<InferSearchParams<T>>) => Promise<void>];
530
+ /**
531
+ * Returns true while a navigation is in progress (guards + loaders running).
532
+ * Use this to show loading indicators during route transitions.
533
+ *
534
+ * @example
535
+ * ```tsx
536
+ * const isNavigating = useTransition()
537
+ * <Show when={isNavigating}>
538
+ * <LoadingBar />
539
+ * </Show>
540
+ * ```
541
+ */
542
+ declare function useTransition(): () => boolean;
543
+ /**
544
+ * Read data accumulated by route middleware.
545
+ *
546
+ * @example
547
+ * ```ts
548
+ * // In middleware:
549
+ * const authMiddleware: RouteMiddleware = async (ctx) => {
550
+ * ctx.data.user = await getUser(ctx.to)
551
+ * if (!ctx.data.user) return '/login'
552
+ * }
553
+ *
554
+ * // In component:
555
+ * const data = useMiddlewareData()
556
+ * const user = () => data().user as User
557
+ * ```
558
+ */
559
+ declare function useMiddlewareData(): () => Record<string, unknown>;
450
560
  declare function createRouter(options: RouterOptions | RouteRecord[]): Router;
451
561
  //#endregion
452
- 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, useIsActive, useLoaderData, useRoute, useRouter, useSearchParams };
562
+ 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 RouteMiddleware, type RouteMiddlewareContext, 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, useIsActive, useLoaderData, useMiddlewareData, useRoute, useRouter, useSearchParams, useTransition, useTypedSearchParams };
453
563
  //# 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":"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"}
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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/router",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "Official router for Pyreon",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/router#readme",
6
6
  "bugs": {
@@ -42,9 +42,9 @@
42
42
  "prepublishOnly": "bun run build"
43
43
  },
44
44
  "dependencies": {
45
- "@pyreon/core": "^0.12.4",
46
- "@pyreon/reactivity": "^0.12.4",
47
- "@pyreon/runtime-dom": "^0.12.4"
45
+ "@pyreon/core": "^0.12.5",
46
+ "@pyreon/reactivity": "^0.12.5",
47
+ "@pyreon/runtime-dom": "^0.12.5"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@happy-dom/global-registrator": "^20.8.9",
@@ -1,5 +1,5 @@
1
1
  import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
2
- import { createRef, h, onUnmount, provide, useContext } from '@pyreon/core'
2
+ import { createRef, ErrorBoundary, h, onUnmount, provide, useContext } from '@pyreon/core'
3
3
  import { LoaderDataContext, prefetchLoaderData } from './loader'
4
4
  import { isLazy, RouterContext, setActiveRouter } from './router'
5
5
  import type { LazyComponent, ResolvedRoute, RouteRecord, Router, RouterInstance } from './types'
@@ -250,11 +250,28 @@ function renderWithLoader(
250
250
  route: Pick<ResolvedRoute, 'params' | 'query' | 'meta'>,
251
251
  ): VNodeChild {
252
252
  const routeProps = { params: route.params, query: route.query, meta: route.meta }
253
- if (!record.loader) {
254
- return h(Comp, routeProps)
253
+
254
+ // If route has an error component, wrap rendering in error boundary
255
+ if (record.errorComponent) {
256
+ return h(ErrorBoundary, {
257
+ fallback: (error: Error) => h(record.errorComponent!, { ...routeProps, error }),
258
+ children: record.loader
259
+ ? renderLoaderContent(router, record, Comp, routeProps)
260
+ : h(Comp, routeProps),
261
+ })
255
262
  }
263
+
264
+ if (!record.loader) return h(Comp, routeProps)
265
+ return renderLoaderContent(router, record, Comp, routeProps)
266
+ }
267
+
268
+ function renderLoaderContent(
269
+ router: RouterInstance,
270
+ record: RouteRecord,
271
+ Comp: ComponentFn,
272
+ routeProps: Record<string, unknown>,
273
+ ): VNodeChild {
256
274
  const data = router._loaderData.get(record)
257
- // If loader data is undefined and route has an errorComponent, render it
258
275
  if (data === undefined && record.errorComponent) {
259
276
  return h(record.errorComponent, routeProps)
260
277
  }
package/src/index.ts CHANGED
@@ -64,7 +64,10 @@ export {
64
64
  useIsActive,
65
65
  useRoute,
66
66
  useRouter,
67
+ useMiddlewareData,
67
68
  useSearchParams,
69
+ useTransition,
70
+ useTypedSearchParams,
68
71
  } from './router'
69
72
  // Types
70
73
  // Data loaders
@@ -81,6 +84,8 @@ export type {
81
84
  RouteComponent,
82
85
  RouteLoaderFn,
83
86
  RouteMeta,
87
+ RouteMiddleware,
88
+ RouteMiddlewareContext,
84
89
  RouteRecord,
85
90
  Router,
86
91
  RouterOptions,
package/src/router.ts CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  type NavigationGuard,
13
13
  type NavigationGuardResult,
14
14
  type ResolvedRoute,
15
+ type RouteMiddleware,
16
+ type RouteMiddlewareContext,
15
17
  type RouteRecord,
16
18
  type Router,
17
19
  type RouterInstance,
@@ -227,14 +229,43 @@ function matchSegments(current: string, pattern: string, exact: boolean): boolea
227
229
  return ps.every((seg, i) => seg.startsWith(':') || seg === cs[i])
228
230
  }
229
231
 
232
+ /** Schema entry for typed search params. */
233
+ export type SearchParamSchema = {
234
+ [key: string]: 'string' | 'number' | 'boolean'
235
+ }
236
+
237
+ /** Infer the typed result from a search param schema. */
238
+ type InferSearchParams<T extends SearchParamSchema> = {
239
+ [K in keyof T]: T[K] extends 'number' ? number
240
+ : T[K] extends 'boolean' ? boolean
241
+ : string
242
+ }
243
+
244
+ /**
245
+ * Read and write URL search params reactively.
246
+ *
247
+ * @example Basic (untyped)
248
+ * ```ts
249
+ * const [params, setParams] = useSearchParams({ page: "1" })
250
+ * params().page // "1"
251
+ * setParams({ page: "2" }) // updates URL
252
+ * ```
253
+ *
254
+ * @example Typed with schema
255
+ * ```ts
256
+ * const [params, setParams] = useSearchParams({
257
+ * page: 'number',
258
+ * sort: 'string',
259
+ * desc: 'boolean',
260
+ * })
261
+ * params().page // number (auto-coerced)
262
+ * params().desc // boolean
263
+ * ```
264
+ */
230
265
  export function useSearchParams<T extends Record<string, string>>(
231
266
  defaults?: T,
232
267
  ): [get: () => T, set: (updates: Partial<T>) => Promise<void>] {
233
- const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
234
- if (!router)
235
- throw new Error(
236
- '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
237
- )
268
+ const router = _getRouter()
238
269
  const get = (): T => {
239
270
  const query = router.currentRoute().query
240
271
  if (!defaults) return query as T
@@ -248,6 +279,98 @@ export function useSearchParams<T extends Record<string, string>>(
248
279
  return [get, set]
249
280
  }
250
281
 
282
+ /**
283
+ * Typed search params with auto-coercion.
284
+ *
285
+ * Schema values define the type: `'string'`, `'number'`, or `'boolean'`.
286
+ * Query string values are automatically coerced to the declared type.
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * const [params, setParams] = useTypedSearchParams({
291
+ * page: 'number',
292
+ * sort: 'string',
293
+ * desc: 'boolean',
294
+ * })
295
+ * params().page // number (coerced from "3" → 3)
296
+ * params().desc // boolean (coerced from "true" → true)
297
+ * setParams({ page: 2 }) // updates URL with ?page=2
298
+ * ```
299
+ */
300
+ export function useTypedSearchParams<T extends SearchParamSchema>(
301
+ schema: T,
302
+ ): [get: () => InferSearchParams<T>, set: (updates: Partial<InferSearchParams<T>>) => Promise<void>] {
303
+ const router = _getRouter()
304
+ const get = (): InferSearchParams<T> => {
305
+ const query = router.currentRoute().query
306
+ const result: Record<string, unknown> = {}
307
+ for (const [key, type] of Object.entries(schema)) {
308
+ const raw = query[key]
309
+ if (type === 'number') result[key] = raw !== undefined ? Number(raw) : 0
310
+ else if (type === 'boolean') result[key] = raw === 'true' || raw === '1'
311
+ else result[key] = raw ?? ''
312
+ }
313
+ return result as InferSearchParams<T>
314
+ }
315
+ const set = (updates: Partial<InferSearchParams<T>>): Promise<void> => {
316
+ const current = get()
317
+ const merged: Record<string, string> = {}
318
+ for (const [k, v] of Object.entries({ ...current, ...updates })) {
319
+ merged[k] = String(v)
320
+ }
321
+ const path = router.currentRoute().path + stringifyQuery(merged)
322
+ return router.replace(path)
323
+ }
324
+ return [get, set]
325
+ }
326
+
327
+ function _getRouter(): RouterInstance {
328
+ const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
329
+ if (!router)
330
+ throw new Error(
331
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
332
+ )
333
+ return router
334
+ }
335
+
336
+ /**
337
+ * Returns true while a navigation is in progress (guards + loaders running).
338
+ * Use this to show loading indicators during route transitions.
339
+ *
340
+ * @example
341
+ * ```tsx
342
+ * const isNavigating = useTransition()
343
+ * <Show when={isNavigating}>
344
+ * <LoadingBar />
345
+ * </Show>
346
+ * ```
347
+ */
348
+ export function useTransition(): () => boolean {
349
+ const router = _getRouter()
350
+ return () => router._loadingSignal() > 0
351
+ }
352
+
353
+ /**
354
+ * Read data accumulated by route middleware.
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * // In middleware:
359
+ * const authMiddleware: RouteMiddleware = async (ctx) => {
360
+ * ctx.data.user = await getUser(ctx.to)
361
+ * if (!ctx.data.user) return '/login'
362
+ * }
363
+ *
364
+ * // In component:
365
+ * const data = useMiddlewareData()
366
+ * const user = () => data().user as User
367
+ * ```
368
+ */
369
+ export function useMiddlewareData(): () => Record<string, unknown> {
370
+ const router = _getRouter()
371
+ return () => (router.currentRoute() as any)._middlewareData ?? {}
372
+ }
373
+
251
374
  // ─── Factory ──────────────────────────────────────────────────────────────────
252
375
 
253
376
  export function createRouter(options: RouterOptions | RouteRecord[]): Router {
@@ -489,17 +612,34 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
489
612
  from: ResolvedRoute,
490
613
  ): void {
491
614
  scrollManager.save(from.path)
492
- currentPath.set(path)
493
- syncBrowserUrl(path, replace)
494
615
 
495
- if (_isBrowser && to.meta.title) {
496
- document.title = to.meta.title
497
- }
616
+ const doCommit = () => {
617
+ currentPath.set(path)
618
+ syncBrowserUrl(path, replace)
498
619
 
499
- for (const record of router._loaderData.keys()) {
500
- if (!to.matched.includes(record)) {
501
- router._loaderData.delete(record)
620
+ if (_isBrowser && to.meta.title) {
621
+ document.title = to.meta.title
502
622
  }
623
+
624
+ for (const record of router._loaderData.keys()) {
625
+ if (!to.matched.includes(record)) {
626
+ router._loaderData.delete(record)
627
+ }
628
+ }
629
+ }
630
+
631
+ // Use View Transitions API when available and not explicitly disabled.
632
+ // Route meta can opt out: meta: { viewTransition: false }
633
+ const useVT = _isBrowser
634
+ && to.meta.viewTransition !== false
635
+ && typeof (document as any).startViewTransition === 'function'
636
+
637
+ if (useVT) {
638
+ (document as any).startViewTransition(() => {
639
+ doCommit()
640
+ })
641
+ } else {
642
+ doCommit()
503
643
  }
504
644
 
505
645
  for (const hook of afterHooks) {
@@ -529,6 +669,30 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
529
669
  return 'continue'
530
670
  }
531
671
 
672
+ /** Run per-route middleware chain. Middleware from all matched routes execute in order. */
673
+ async function runMiddleware(
674
+ to: ResolvedRoute,
675
+ from: ResolvedRoute,
676
+ gen: number,
677
+ ): Promise<{ action: 'continue' } | { action: 'cancel' } | { action: 'redirect'; target: string }> {
678
+ const ctx: RouteMiddlewareContext = { to, from, data: {} }
679
+
680
+ for (const record of to.matched) {
681
+ if (!record.middleware) continue
682
+ const mws = Array.isArray(record.middleware) ? record.middleware : [record.middleware]
683
+ for (const mw of mws) {
684
+ if (gen !== _navGen) return { action: 'cancel' }
685
+ const result = await mw(ctx)
686
+ if (result === false) return { action: 'cancel' }
687
+ if (typeof result === 'string') return { action: 'redirect', target: result }
688
+ }
689
+ }
690
+
691
+ // Store middleware data on the resolved route for component access
692
+ ;(to as any)._middlewareData = ctx.data
693
+ return { action: 'continue' }
694
+ }
695
+
532
696
  async function navigate(rawPath: string, replace: boolean, redirectDepth = 0): Promise<void> {
533
697
  if (redirectDepth > 10) {
534
698
  if (__DEV__) {
@@ -560,6 +724,16 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
560
724
  return
561
725
  }
562
726
 
727
+ // Run per-route middleware chain (before guards)
728
+ const mwResult = await runMiddleware(to, from, gen)
729
+ if (mwResult.action !== 'continue') {
730
+ loadingSignal.update((n) => n - 1)
731
+ if (mwResult.action === 'redirect') {
732
+ return navigate(sanitizePath(mwResult.target), replace, redirectDepth + 1)
733
+ }
734
+ return
735
+ }
736
+
563
737
  const guardOutcome = await runAllGuards(to, from, gen)
564
738
  if (guardOutcome.action !== 'continue') {
565
739
  loadingSignal.update((n) => n - 1)
package/src/scroll.ts CHANGED
@@ -35,6 +35,26 @@ export class ScrollManager {
35
35
  }
36
36
 
37
37
  private _applyResult(result: 'top' | 'restore' | 'none' | number, toPath: string): void {
38
+ // Hash scrolling: if the path contains #, scroll to the element
39
+ const hashIdx = toPath.indexOf('#')
40
+ if (hashIdx >= 0) {
41
+ const id = toPath.slice(hashIdx + 1)
42
+ if (id) {
43
+ // Use requestAnimationFrame to ensure DOM is updated before scrolling
44
+ requestAnimationFrame(() => {
45
+ const el = document.getElementById(id)
46
+ if (el) {
47
+ el.scrollIntoView({ behavior: 'smooth' })
48
+ return
49
+ }
50
+ // Fallback: try name attribute (for anchors)
51
+ const namedEl = document.querySelector(`[name="${CSS.escape(id)}"]`)
52
+ if (namedEl) namedEl.scrollIntoView({ behavior: 'smooth' })
53
+ })
54
+ return
55
+ }
56
+ }
57
+
38
58
  if (result === 'none') return
39
59
  if (result === 'top' || result === undefined) {
40
60
  window.scrollTo({ top: 0, behavior: 'instant' as ScrollBehavior })