@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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +173 -8
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +115 -5
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/components.tsx +21 -4
- package/src/index.ts +5 -0
- package/src/router.ts +187 -13
- package/src/scroll.ts +20 -0
- package/src/tests/router.test.ts +283 -0
- package/src/types.ts +44 -4
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
212
|
+
/** Navigate to a named route */
|
|
179
213
|
push(location: {
|
|
180
|
-
name:
|
|
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:
|
|
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
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -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":";;;;;;;;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;
|
|
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.
|
|
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.
|
|
46
|
-
"@pyreon/reactivity": "^0.12.
|
|
47
|
-
"@pyreon/runtime-dom": "^0.12.
|
|
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",
|
package/src/components.tsx
CHANGED
|
@@ -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
|
-
|
|
254
|
-
|
|
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 = (
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
616
|
+
const doCommit = () => {
|
|
617
|
+
currentPath.set(path)
|
|
618
|
+
syncBrowserUrl(path, replace)
|
|
498
619
|
|
|
499
|
-
|
|
500
|
-
|
|
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 })
|