@pyreon/router 0.12.14 → 0.13.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 +29 -7
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +0 -14
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/components.tsx +1 -0
- package/src/loader.ts +4 -1
- package/src/manifest.ts +336 -0
- package/src/router.ts +36 -15
- package/src/scroll.ts +13 -0
- package/src/tests/loader.test.ts +18 -0
- package/src/tests/manifest-snapshot.test.ts +97 -0
- package/src/tests/scroll.test.ts +31 -0
package/lib/types/index.d.ts
CHANGED
|
@@ -449,20 +449,6 @@ declare function onBeforeRouteLeave(guard: NavigationGuard): () => void;
|
|
|
449
449
|
* })
|
|
450
450
|
*/
|
|
451
451
|
declare function onBeforeRouteUpdate(guard: NavigationGuard): () => void;
|
|
452
|
-
/**
|
|
453
|
-
* Register a navigation blocker. The `fn` callback is called before each
|
|
454
|
-
* navigation — return `true` (or resolve to `true`) to block it.
|
|
455
|
-
*
|
|
456
|
-
* Automatically removed on component unmount if called during component setup.
|
|
457
|
-
* Also installs a `beforeunload` handler so the browser shows a confirmation
|
|
458
|
-
* dialog when the user tries to close the tab while a blocker is active.
|
|
459
|
-
*
|
|
460
|
-
* @example
|
|
461
|
-
* const blocker = useBlocker((to, from) => {
|
|
462
|
-
* return hasUnsavedChanges() && !confirm("Discard changes?")
|
|
463
|
-
* })
|
|
464
|
-
* // later: blocker.remove()
|
|
465
|
-
*/
|
|
466
452
|
declare function useBlocker(fn: BlockerFn): Blocker;
|
|
467
453
|
/**
|
|
468
454
|
* Reactive read/write access to the current route's query parameters.
|
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":"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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.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Official router for Pyreon",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/router#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -43,13 +43,14 @@
|
|
|
43
43
|
"prepublishOnly": "bun run build"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@pyreon/core": "^0.
|
|
47
|
-
"@pyreon/reactivity": "^0.
|
|
48
|
-
"@pyreon/runtime-dom": "^0.
|
|
46
|
+
"@pyreon/core": "^0.13.0",
|
|
47
|
+
"@pyreon/reactivity": "^0.13.0",
|
|
48
|
+
"@pyreon/runtime-dom": "^0.13.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@happy-dom/global-registrator": "^20.8.9",
|
|
52
|
-
"@pyreon/
|
|
52
|
+
"@pyreon/manifest": "0.13.0",
|
|
53
|
+
"@pyreon/test-utils": "^0.13.0",
|
|
53
54
|
"@vitest/browser-playwright": "^4.1.4",
|
|
54
55
|
"happy-dom": "^20.8.3"
|
|
55
56
|
}
|
package/src/components.tsx
CHANGED
|
@@ -151,6 +151,7 @@ export const RouterLink: ComponentFn<RouterLinkProps> = (props) => {
|
|
|
151
151
|
if (!router) return ''
|
|
152
152
|
const current = router.currentRoute().path
|
|
153
153
|
const target = props.to
|
|
154
|
+
if (typeof target !== 'string') return ''
|
|
154
155
|
const isExact = current === target
|
|
155
156
|
const isActive = isExact || (!props.exact && isSegmentPrefix(current, target))
|
|
156
157
|
|
package/src/loader.ts
CHANGED
|
@@ -35,8 +35,11 @@ export function useLoaderData<T = unknown>(): T {
|
|
|
35
35
|
*/
|
|
36
36
|
export async function prefetchLoaderData(router: RouterInstance, path: string): Promise<void> {
|
|
37
37
|
const route = router._resolve(path)
|
|
38
|
+
// Use a local AbortController — prefetch is best-effort and must NOT
|
|
39
|
+
// clobber `router._abortController`, which belongs to the active
|
|
40
|
+
// navigation. Previously, hovering a link during a navigation replaced
|
|
41
|
+
// the nav's controller, destroying its abort capability.
|
|
38
42
|
const ac = new AbortController()
|
|
39
|
-
router._abortController = ac
|
|
40
43
|
await Promise.all(
|
|
41
44
|
route.matched
|
|
42
45
|
.filter((r) => r.loader)
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { defineManifest } from '@pyreon/manifest'
|
|
2
|
+
|
|
3
|
+
export default defineManifest({
|
|
4
|
+
name: '@pyreon/router',
|
|
5
|
+
title: 'Router',
|
|
6
|
+
tagline:
|
|
7
|
+
'hash+history+SSR, context-based, prefetching, guards, loaders, useIsActive, View Transitions, middleware, typed search params',
|
|
8
|
+
description:
|
|
9
|
+
'Type-safe client-side router for Pyreon with nested routes, per-route and global navigation guards, data loaders, middleware chain, View Transitions API integration, and typed search params. Context-based (`RouterContext`) with hash and history mode support. Route params are inferred from path strings (`"/user/:id"` yields `{ id: string }`). Named routes enable typed programmatic navigation. SSR-compatible with server-side route resolution. Hash mode uses `history.pushState` (not `window.location.hash`) to avoid double-update. `await router.push()` resolves after the View Transition `updateCallbackDone` (DOM commit), not after animation completion.',
|
|
10
|
+
category: 'browser',
|
|
11
|
+
longExample: `import { createRouter, RouterProvider, RouterView, RouterLink, useRouter, useRoute, useIsActive, useTypedSearchParams, useTransition, useLoaderData, useMiddlewareData } from "@pyreon/router"
|
|
12
|
+
import { mount } from "@pyreon/runtime-dom"
|
|
13
|
+
|
|
14
|
+
// Define routes with typed params, guards, loaders, and middleware
|
|
15
|
+
const router = createRouter({
|
|
16
|
+
routes: [
|
|
17
|
+
{ path: "/", component: Home, name: "home" },
|
|
18
|
+
{ path: "/user/:id", component: User, name: "user",
|
|
19
|
+
loader: ({ params }) => fetchUser(params.id),
|
|
20
|
+
meta: { title: "User Profile" } },
|
|
21
|
+
{ path: "/admin", component: AdminLayout,
|
|
22
|
+
beforeEnter: (to, from) => isAdmin() || "/login",
|
|
23
|
+
children: [
|
|
24
|
+
{ path: "users", component: AdminUsers },
|
|
25
|
+
{ path: "settings", component: AdminSettings },
|
|
26
|
+
] },
|
|
27
|
+
{ path: "/settings", redirect: "/admin/settings" },
|
|
28
|
+
{ path: "(.*)", component: NotFound },
|
|
29
|
+
],
|
|
30
|
+
middleware: [authMiddleware, loggerMiddleware],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Mount with RouterProvider
|
|
34
|
+
mount(
|
|
35
|
+
<RouterProvider router={router}>
|
|
36
|
+
<nav>
|
|
37
|
+
<RouterLink to="/" activeClass="nav-active">Home</RouterLink>
|
|
38
|
+
<RouterLink to={{ name: "user", params: { id: "42" } }}>Profile</RouterLink>
|
|
39
|
+
</nav>
|
|
40
|
+
<RouterView />
|
|
41
|
+
</RouterProvider>,
|
|
42
|
+
document.getElementById("app")!
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Inside a component — hooks
|
|
46
|
+
const User = () => {
|
|
47
|
+
const route = useRoute<"/user/:id">()
|
|
48
|
+
const data = useLoaderData<UserData>()
|
|
49
|
+
const router = useRouter()
|
|
50
|
+
const isAdmin = useIsActive("/admin")
|
|
51
|
+
const { isTransitioning } = useTransition()
|
|
52
|
+
const params = useTypedSearchParams({ tab: "string", page: "number" })
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<h1>{data.name} (ID: {route().params.id})</h1>
|
|
57
|
+
<Show when={isTransitioning()}>
|
|
58
|
+
<ProgressBar />
|
|
59
|
+
</Show>
|
|
60
|
+
<button onClick={() => router.push("/")}>Go Home</button>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}`,
|
|
64
|
+
features: [
|
|
65
|
+
'createRouter() — factory with routes, guards, middleware, loaders, hash/history mode',
|
|
66
|
+
'RouterProvider / RouterView / RouterLink — context-based rendering components',
|
|
67
|
+
'useRouter / useRoute — programmatic navigation and typed route access',
|
|
68
|
+
'useIsActive — reactive boolean for path matching (segment-aware prefix)',
|
|
69
|
+
'useTypedSearchParams — typed search params with auto-coercion',
|
|
70
|
+
'useTransition — reactive signal for route transition state',
|
|
71
|
+
'useMiddlewareData — read data set by route middleware chain',
|
|
72
|
+
'useLoaderData — access route loader results',
|
|
73
|
+
'View Transitions API — auto-enabled, awaits updateCallbackDone',
|
|
74
|
+
'Named routes — typed navigation via { name, params }',
|
|
75
|
+
'Nested routes — recursive matching with child RouterView',
|
|
76
|
+
'Navigation guards — per-route and global beforeEnter/afterEach hooks',
|
|
77
|
+
],
|
|
78
|
+
api: [
|
|
79
|
+
{
|
|
80
|
+
name: 'createRouter',
|
|
81
|
+
kind: 'function',
|
|
82
|
+
signature: 'createRouter(options: RouterOptions | RouteRecord[]): Router',
|
|
83
|
+
summary:
|
|
84
|
+
'Create a router instance with route records, guards, middleware, and mode configuration. Accepts either an array of route records (shorthand) or a full `RouterOptions` object with `routes`, `mode` (`"history"` | `"hash"`), `scrollBehavior`, `beforeEach`, `afterEach`, and `middleware`. The returned `Router` is generic over route names for typed programmatic navigation.',
|
|
85
|
+
example: `const router = createRouter([
|
|
86
|
+
{ path: "/", component: Home },
|
|
87
|
+
{ path: "/user/:id", component: User, loader: ({ params }) => fetchUser(params.id) },
|
|
88
|
+
{ path: "/admin", component: Admin, beforeEnter: requireAuth, children: [
|
|
89
|
+
{ path: "settings", component: Settings },
|
|
90
|
+
]},
|
|
91
|
+
])`,
|
|
92
|
+
mistakes: [
|
|
93
|
+
'`createRouter({ routes: [...], mode: "hash" })` and using `window.location.hash` elsewhere — hash mode uses `history.pushState`, not `location.hash`. Reading `location.hash` directly will not reflect router state',
|
|
94
|
+
'Defining route paths without leading `/` in root routes — all root-level paths must start with `/`',
|
|
95
|
+
'Using `redirect: "/target"` with a guard on the same route — redirects bypass guards. Use `beforeEnter` to conditionally redirect instead',
|
|
96
|
+
'Forgetting the catch-all route — `{ path: "(.*)", component: NotFound }` should be the last route to handle 404s',
|
|
97
|
+
],
|
|
98
|
+
seeAlso: ['RouterProvider', 'useRouter', 'useRoute'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'RouterProvider',
|
|
102
|
+
kind: 'component',
|
|
103
|
+
signature: '<RouterProvider router={router}>{children}</RouterProvider>',
|
|
104
|
+
summary:
|
|
105
|
+
'Provide the router instance to the component tree via `RouterContext`. Must wrap the entire app (or the routed section). Sets up the context stack so `useRouter()`, `useRoute()`, and other hooks can access the router.',
|
|
106
|
+
example: `const App = () => (
|
|
107
|
+
<RouterProvider router={router}>
|
|
108
|
+
<nav><RouterLink to="/">Home</RouterLink></nav>
|
|
109
|
+
<RouterView />
|
|
110
|
+
</RouterProvider>
|
|
111
|
+
)`,
|
|
112
|
+
seeAlso: ['createRouter', 'RouterView', 'RouterLink'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'RouterView',
|
|
116
|
+
kind: 'component',
|
|
117
|
+
signature: '<RouterView />',
|
|
118
|
+
summary:
|
|
119
|
+
'Render the matched route\'s component. For nested routes, the parent route component includes a `<RouterView />` that renders the matched child. Each `<RouterView>` renders one level of the route tree.',
|
|
120
|
+
example: `// Renders the matched route's component
|
|
121
|
+
<RouterView />
|
|
122
|
+
|
|
123
|
+
// Nested routes: parent component includes <RouterView /> for children
|
|
124
|
+
const Admin = () => (
|
|
125
|
+
<div>
|
|
126
|
+
<h1>Admin</h1>
|
|
127
|
+
<RouterView /> {/* renders Settings, Users, etc. */}
|
|
128
|
+
</div>
|
|
129
|
+
)`,
|
|
130
|
+
seeAlso: ['RouterProvider', 'createRouter'],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'RouterLink',
|
|
134
|
+
kind: 'component',
|
|
135
|
+
signature: '<RouterLink to={path} activeClass={cls} exactActiveClass={cls}>{children}</RouterLink>',
|
|
136
|
+
summary:
|
|
137
|
+
'Declarative navigation link that renders an `<a>` element. Supports string paths or named route objects (`{ name, params }`). Applies `activeClass` when the current route matches the link path (prefix), and `exactActiveClass` for exact matches. Click handler calls `router.push()` and prevents default.',
|
|
138
|
+
example: `<RouterLink to="/" activeClass="nav-active">Home</RouterLink>
|
|
139
|
+
<RouterLink to={{ name: "user", params: { id: "42" } }}>Profile</RouterLink>`,
|
|
140
|
+
mistakes: [
|
|
141
|
+
'`<a href="/about" onClick={() => router.push("/about")}>` — use `<RouterLink to="/about">` instead; it handles the anchor element, active class, and click interception',
|
|
142
|
+
'`<RouterLink to="/about" target="_blank">` — external navigation bypasses the router; use a plain `<a>` for external links',
|
|
143
|
+
'`<RouterLink to={dynamicPath}>` without calling the signal — must call: `<RouterLink to={dynamicPath()}>` (or let the compiler handle it via `_rp()`)',
|
|
144
|
+
],
|
|
145
|
+
seeAlso: ['useRouter', 'useIsActive'],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'useRouter',
|
|
149
|
+
kind: 'hook',
|
|
150
|
+
signature: 'useRouter(): Router',
|
|
151
|
+
summary:
|
|
152
|
+
'Access the router instance for programmatic navigation. Returns the `Router` object with `push()`, `replace()`, `back()`, `forward()`, `go()`. `await router.push()` resolves after the View Transition `updateCallbackDone` (DOM commit is complete, new route state is live), NOT after the animation finishes.',
|
|
153
|
+
example: `const router = useRouter()
|
|
154
|
+
|
|
155
|
+
router.push("/settings")
|
|
156
|
+
router.push({ name: "user", params: { id: "42" } })
|
|
157
|
+
router.replace("/login")
|
|
158
|
+
router.back()
|
|
159
|
+
router.forward()
|
|
160
|
+
router.go(-2)`,
|
|
161
|
+
mistakes: [
|
|
162
|
+
'`router.push("/path")` at the top level of a component body — this is synchronous imperative navigation during render, causing an infinite loop. Wrap in `onMount`, event handler, or `effect`',
|
|
163
|
+
'`await router.push("/path")` expecting animation completion — `push` resolves after DOM commit (`updateCallbackDone`), not after View Transition animation finishes. Use the returned transition object\'s `.finished` if you need to wait for animation',
|
|
164
|
+
'Calling `useRouter()` outside a `<RouterProvider>` — throws because no router context exists',
|
|
165
|
+
],
|
|
166
|
+
seeAlso: ['useRoute', 'RouterLink', 'createRouter'],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'useRoute',
|
|
170
|
+
kind: 'hook',
|
|
171
|
+
signature: 'useRoute<TPath extends string>(): () => ResolvedRoute<ExtractParams<TPath>>',
|
|
172
|
+
summary:
|
|
173
|
+
'Access the current resolved route as a reactive accessor. Generic over the path string for typed params — `useRoute<"/user/:id">()` yields `route().params.id: string`. Returns a function (accessor) that must be called to read the current route — reads inside reactive scopes track route changes.',
|
|
174
|
+
example: `// Type-safe params:
|
|
175
|
+
const route = useRoute<"/user/:id">()
|
|
176
|
+
const userId = route().params.id // string
|
|
177
|
+
|
|
178
|
+
// Access query, meta, etc:
|
|
179
|
+
route().query
|
|
180
|
+
route().meta`,
|
|
181
|
+
seeAlso: ['useRouter', 'useSearchParams', 'useLoaderData'],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'useIsActive',
|
|
185
|
+
kind: 'hook',
|
|
186
|
+
signature: 'useIsActive(path: string, exact?: boolean): () => boolean',
|
|
187
|
+
summary:
|
|
188
|
+
'Returns a reactive boolean for whether a path matches the current route. Segment-aware prefix matching: `/admin` matches `/admin/users` but NOT `/admin-panel`. Pass `exact=true` for exact-only matching. Updates reactively when the route changes.',
|
|
189
|
+
example: `const isHome = useIsActive("/")
|
|
190
|
+
const isAdmin = useIsActive("/admin") // prefix match
|
|
191
|
+
const isExactAdmin = useIsActive("/admin", true) // exact only
|
|
192
|
+
|
|
193
|
+
// Reactive — updates when route changes:
|
|
194
|
+
<a class={{ active: isAdmin() }} href="/admin">Admin</a>`,
|
|
195
|
+
mistakes: [
|
|
196
|
+
'`useIsActive("/admin")` matching `/admin-panel` — this does NOT happen. Matching is segment-aware: `/admin` only matches paths starting with `/admin/` or exactly `/admin`',
|
|
197
|
+
'`if (useIsActive("/settings")())` at component top level — the outer call returns an accessor; make sure to read it inside a reactive scope for updates',
|
|
198
|
+
'Using `useIsActive` for complex route matching — it only does path prefix/exact matching. For query-param-aware or meta-aware checks, use `useRoute()` directly',
|
|
199
|
+
],
|
|
200
|
+
seeAlso: ['useRoute', 'RouterLink'],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'useTypedSearchParams',
|
|
204
|
+
kind: 'hook',
|
|
205
|
+
signature: 'useTypedSearchParams<T>(schema: T): TypedSearchParams<T>',
|
|
206
|
+
summary:
|
|
207
|
+
'Type-safe search params with auto-coercion from URL strings. Schema keys define parameter names, values define types (`"string"`, `"number"`, `"boolean"`). Returns an object where each key is a reactive accessor and `.set()` updates the URL.',
|
|
208
|
+
example: `const params = useTypedSearchParams({ page: "number", q: "string", active: "boolean" })
|
|
209
|
+
params.page() // number (auto-coerced)
|
|
210
|
+
params.q() // string
|
|
211
|
+
params.set({ page: 2 }) // updates URL`,
|
|
212
|
+
seeAlso: ['useSearchParams', 'useRoute'],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'useTransition',
|
|
216
|
+
kind: 'hook',
|
|
217
|
+
signature: 'useTransition(): { isTransitioning: () => boolean }',
|
|
218
|
+
summary:
|
|
219
|
+
'Reactive signal for route transition state. `isTransitioning()` is true during navigation (while guards run + loaders resolve), false when the new route is mounted. Useful for progress bars and global loading indicators.',
|
|
220
|
+
example: `const { isTransitioning } = useTransition()
|
|
221
|
+
|
|
222
|
+
<Show when={isTransitioning()}>
|
|
223
|
+
<ProgressBar />
|
|
224
|
+
</Show>`,
|
|
225
|
+
seeAlso: ['useRouter', 'useRoute'],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'useMiddlewareData',
|
|
229
|
+
kind: 'hook',
|
|
230
|
+
signature: 'useMiddlewareData<T>(): T',
|
|
231
|
+
summary:
|
|
232
|
+
'Read data set by `RouteMiddleware` in the middleware chain. Middleware functions receive `ctx` with a mutable `ctx.data` object — properties set there are available to all downstream components via this hook.',
|
|
233
|
+
example: `// Middleware:
|
|
234
|
+
const authMiddleware: RouteMiddleware = async (ctx) => {
|
|
235
|
+
ctx.data.user = await getUser(ctx.to)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Component:
|
|
239
|
+
const data = useMiddlewareData<{ user: User }>()
|
|
240
|
+
// data.user is available`,
|
|
241
|
+
seeAlso: ['createRouter', 'useLoaderData'],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'useLoaderData',
|
|
245
|
+
kind: 'hook',
|
|
246
|
+
signature: 'useLoaderData<T>(): T',
|
|
247
|
+
summary:
|
|
248
|
+
'Access the data returned by the current route\'s `loader` function. The loader runs before the route component mounts; its return value is cached and available synchronously via this hook. Generic over the loader return type.',
|
|
249
|
+
example: `// Route: { path: "/user/:id", component: User, loader: ({ params }) => fetchUser(params.id) }
|
|
250
|
+
|
|
251
|
+
const User = () => {
|
|
252
|
+
const data = useLoaderData<UserData>()
|
|
253
|
+
return <div>{data.name}</div>
|
|
254
|
+
}`,
|
|
255
|
+
seeAlso: ['useMiddlewareData', 'useRoute'],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'useSearchParams',
|
|
259
|
+
kind: 'hook',
|
|
260
|
+
signature:
|
|
261
|
+
'useSearchParams<T>(defaults?: T): [get: () => T, set: (updates: Partial<T>) => Promise<void>]',
|
|
262
|
+
summary:
|
|
263
|
+
'Access and update URL search params as a reactive tuple. Returns `[get, set]` where `get()` reads the current params and `set()` updates them via `replaceState`. For typed params with auto-coercion, prefer `useTypedSearchParams`.',
|
|
264
|
+
example: `const [search, setSearch] = useSearchParams({ page: "1", sort: "name" })
|
|
265
|
+
|
|
266
|
+
// Read:
|
|
267
|
+
search().page // "1"
|
|
268
|
+
|
|
269
|
+
// Write:
|
|
270
|
+
setSearch({ page: "2" })`,
|
|
271
|
+
seeAlso: ['useTypedSearchParams', 'useRoute'],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'useBlocker',
|
|
275
|
+
kind: 'hook',
|
|
276
|
+
signature: 'useBlocker(shouldBlock: () => boolean): Blocker',
|
|
277
|
+
summary:
|
|
278
|
+
'Block navigation when a condition is true (e.g., unsaved form changes). Returns a `Blocker` object with `proceed()` and `reset()` methods. Also hooks into the browser\'s `beforeunload` event to warn on tab close. Uses a shared ref-counted listener for `beforeunload` — N blockers share one event handler.',
|
|
279
|
+
example: `const blocker = useBlocker(() => form.isDirty())
|
|
280
|
+
|
|
281
|
+
<Show when={blocker.isBlocked()}>
|
|
282
|
+
<Dialog>
|
|
283
|
+
<p>Unsaved changes. Leave anyway?</p>
|
|
284
|
+
<button onClick={blocker.proceed}>Leave</button>
|
|
285
|
+
<button onClick={blocker.reset}>Stay</button>
|
|
286
|
+
</Dialog>
|
|
287
|
+
</Show>`,
|
|
288
|
+
seeAlso: ['useRouter'],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: 'onBeforeRouteLeave',
|
|
292
|
+
kind: 'function',
|
|
293
|
+
signature: 'onBeforeRouteLeave(guard: NavigationGuard): void',
|
|
294
|
+
summary:
|
|
295
|
+
'Register a per-component navigation guard that fires when leaving the current route. Return `false` to cancel, a string path to redirect, or `undefined` to allow. Must be called during component setup.',
|
|
296
|
+
example: `onBeforeRouteLeave((to, from) => {
|
|
297
|
+
if (hasUnsavedChanges()) return false // cancel navigation
|
|
298
|
+
})`,
|
|
299
|
+
seeAlso: ['onBeforeRouteUpdate', 'useBlocker'],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'onBeforeRouteUpdate',
|
|
303
|
+
kind: 'function',
|
|
304
|
+
signature: 'onBeforeRouteUpdate(guard: NavigationGuard): void',
|
|
305
|
+
summary:
|
|
306
|
+
'Register a per-component navigation guard that fires when the route updates but the same component stays mounted (e.g., param change `/user/1` to `/user/2`). Same return semantics as `onBeforeRouteLeave`.',
|
|
307
|
+
example: `onBeforeRouteUpdate((to, from) => {
|
|
308
|
+
if (to.params.id === from.params.id) return // no change
|
|
309
|
+
// reload data for new ID...
|
|
310
|
+
})`,
|
|
311
|
+
seeAlso: ['onBeforeRouteLeave', 'useRoute'],
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
gotchas: [
|
|
315
|
+
{
|
|
316
|
+
label: 'View Transitions — what push() awaits',
|
|
317
|
+
note: '`await router.push()` resolves after `updateCallbackDone` (DOM commit), NOT after animation finishes. It does NOT wait for `.finished` (~200-300ms). `.ready` and `.finished` get empty `.catch()` handlers so `AbortError: Transition was skipped` rejections (from interrupted transitions) do not leak as unhandled promise rejections.',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
label: 'Hash mode uses pushState',
|
|
321
|
+
note: 'Hash mode uses `history.pushState` — NOT `window.location.hash` assignment — to avoid double-update from the hashchange event. Reading `location.hash` directly will not reflect router state; use `useRoute()` instead.',
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
label: 'Imperative navigation in render body',
|
|
325
|
+
note: '`router.push()` or `navigate()` called synchronously in the component function body causes an infinite render loop. Wrap in `onMount`, event handlers, `effect`, or any deferred execution context. The `pyreon/no-imperative-navigate-in-render` lint rule catches this.',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
label: 'Hook ordering with View Transitions',
|
|
329
|
+
note: '`afterEach` hooks and scroll restoration fire AFTER the View Transition callback completes — not before. This means hooks see the NEW route state, which is the correct per-spec behavior but a subtle change from pre-VT versions.',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
label: 'For uses by, not key',
|
|
333
|
+
note: '`<For>` in route lists uses `by` not `key`. `<For each={routes()} key={r => r.path}>` silently passes the key to VNode reconciliation instead of the list reconciler. Use `by={r => r.path}`.',
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
})
|
package/src/router.ts
CHANGED
|
@@ -144,6 +144,31 @@ export function onBeforeRouteUpdate(guard: NavigationGuard): () => void {
|
|
|
144
144
|
* })
|
|
145
145
|
* // later: blocker.remove()
|
|
146
146
|
*/
|
|
147
|
+
// Shared beforeunload handler — single listener for all active blockers.
|
|
148
|
+
// Attached when the first blocker registers, detached when the last one is
|
|
149
|
+
// removed. Avoids listener accumulation from multiple useBlocker() calls.
|
|
150
|
+
let _beforeUnloadRefCount = 0
|
|
151
|
+
const _beforeUnloadHandler = (e: BeforeUnloadEvent) => {
|
|
152
|
+
e.preventDefault()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function retainBeforeUnload(): void {
|
|
156
|
+
if (!_isBrowser) return
|
|
157
|
+
if (_beforeUnloadRefCount === 0) {
|
|
158
|
+
window.addEventListener('beforeunload', _beforeUnloadHandler)
|
|
159
|
+
}
|
|
160
|
+
_beforeUnloadRefCount++
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function releaseBeforeUnload(): void {
|
|
164
|
+
if (!_isBrowser) return
|
|
165
|
+
_beforeUnloadRefCount--
|
|
166
|
+
if (_beforeUnloadRefCount <= 0) {
|
|
167
|
+
_beforeUnloadRefCount = 0
|
|
168
|
+
window.removeEventListener('beforeunload', _beforeUnloadHandler)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
147
172
|
export function useBlocker(fn: BlockerFn): Blocker {
|
|
148
173
|
const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
|
|
149
174
|
if (!router)
|
|
@@ -151,22 +176,11 @@ export function useBlocker(fn: BlockerFn): Blocker {
|
|
|
151
176
|
'[Pyreon] No router installed. Wrap your app in <RouterProvider router={router}>.',
|
|
152
177
|
)
|
|
153
178
|
router._blockers.add(fn)
|
|
154
|
-
|
|
155
|
-
// Warn before tab/window close while this blocker is registered
|
|
156
|
-
const beforeUnloadHandler = _isBrowser
|
|
157
|
-
? (e: BeforeUnloadEvent) => {
|
|
158
|
-
e.preventDefault()
|
|
159
|
-
}
|
|
160
|
-
: null
|
|
161
|
-
if (beforeUnloadHandler) {
|
|
162
|
-
window.addEventListener('beforeunload', beforeUnloadHandler)
|
|
163
|
-
}
|
|
179
|
+
retainBeforeUnload()
|
|
164
180
|
|
|
165
181
|
const remove = () => {
|
|
166
182
|
router._blockers.delete(fn)
|
|
167
|
-
|
|
168
|
-
window.removeEventListener('beforeunload', beforeUnloadHandler)
|
|
169
|
-
}
|
|
183
|
+
releaseBeforeUnload()
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
// Auto-remove when the component that called useBlocker unmounts
|
|
@@ -924,9 +938,12 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
|
|
|
924
938
|
)
|
|
925
939
|
// Run loaders for the matched path — uses the same code path SSR
|
|
926
940
|
// already relied on, so loader data ends up in `_loaderData` under the
|
|
927
|
-
// matched route records.
|
|
941
|
+
// matched route records. Uses a LOCAL AbortController: `preload` is
|
|
942
|
+
// a prefetch operation and must NOT clobber `router._abortController`,
|
|
943
|
+
// which belongs to the active navigation. Without this, calling
|
|
944
|
+
// `router.preload(...)` during a navigation destroyed the nav's
|
|
945
|
+
// abort capability.
|
|
928
946
|
const ac = new AbortController()
|
|
929
|
-
router._abortController = ac
|
|
930
947
|
await Promise.all(
|
|
931
948
|
resolved.matched
|
|
932
949
|
.filter((r) => r.loader)
|
|
@@ -946,11 +963,15 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
|
|
|
946
963
|
if (_hashchangeHandler) window.removeEventListener('hashchange', _hashchangeHandler)
|
|
947
964
|
guards.length = 0
|
|
948
965
|
afterHooks.length = 0
|
|
966
|
+
// Release beforeunload for any remaining blockers
|
|
967
|
+
for (let i = router._blockers.size; i > 0; i--) releaseBeforeUnload()
|
|
949
968
|
router._blockers.clear()
|
|
950
969
|
componentCache.clear()
|
|
951
970
|
router._loaderData.clear()
|
|
952
971
|
router._abortController?.abort()
|
|
953
972
|
router._abortController = null
|
|
973
|
+
// Clear global ref so stale router doesn't survive in SSR or re-creation
|
|
974
|
+
if (_activeRouter === router) _activeRouter = null
|
|
954
975
|
},
|
|
955
976
|
|
|
956
977
|
_resolve: (rawPath: string) => resolveRoute(rawPath, routes),
|
package/src/scroll.ts
CHANGED
|
@@ -6,6 +6,12 @@ import type { ResolvedRoute, RouterOptions } from './types'
|
|
|
6
6
|
* Saves scroll position before each navigation and restores it when
|
|
7
7
|
* navigating back to a previously visited path.
|
|
8
8
|
*/
|
|
9
|
+
// LRU cap — in SPAs with unbounded URL space (`/user/:id`, query-string
|
|
10
|
+
// variations, etc.) the `_positions` Map would grow per unique path
|
|
11
|
+
// forever. 100 entries covers typical back-navigation depth; beyond that,
|
|
12
|
+
// scroll restoration is a nice-to-have not a correctness requirement.
|
|
13
|
+
const MAX_SCROLL_POSITIONS = 100
|
|
14
|
+
|
|
9
15
|
export class ScrollManager {
|
|
10
16
|
private readonly _positions = new Map<string, number>()
|
|
11
17
|
private readonly _behavior: RouterOptions['scrollBehavior']
|
|
@@ -21,7 +27,14 @@ export class ScrollManager {
|
|
|
21
27
|
// callsite (the `no-window-in-ssr` lint rule can't AST-trace indirect
|
|
22
28
|
// calls from router setup).
|
|
23
29
|
if (typeof window === 'undefined') return
|
|
30
|
+
// LRU: re-insert moves the entry to newest. Evict oldest when over cap.
|
|
31
|
+
if (this._positions.has(fromPath)) this._positions.delete(fromPath)
|
|
24
32
|
this._positions.set(fromPath, window.scrollY)
|
|
33
|
+
while (this._positions.size > MAX_SCROLL_POSITIONS) {
|
|
34
|
+
const oldest = this._positions.keys().next().value
|
|
35
|
+
if (oldest === undefined) break
|
|
36
|
+
this._positions.delete(oldest)
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
/** Call after navigation is committed — applies scroll behavior */
|
package/src/tests/loader.test.ts
CHANGED
|
@@ -78,6 +78,24 @@ describe('loader data serialization — edge cases', () => {
|
|
|
78
78
|
expect(values[0]).toEqual(complexData)
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
+
test('prefetchLoaderData does NOT clobber router._abortController', async () => {
|
|
82
|
+
// Regression: `prefetchLoaderData` used to overwrite
|
|
83
|
+
// `router._abortController` with its own fresh controller. Hovering
|
|
84
|
+
// a <Link> during an in-flight navigation destroyed the nav's
|
|
85
|
+
// abort capability — subsequent navigations couldn't cancel the
|
|
86
|
+
// first one. Fix: prefetch uses a LOCAL controller.
|
|
87
|
+
const routes: RouteRecord[] = [
|
|
88
|
+
{ path: '/data', component: Home, loader: async () => 'ok' },
|
|
89
|
+
]
|
|
90
|
+
const router = createRouter({ routes, url: '/' }) as RouterInstance
|
|
91
|
+
const navController = new AbortController()
|
|
92
|
+
router._abortController = navController
|
|
93
|
+
await prefetchLoaderData(router, '/data')
|
|
94
|
+
// Prefetch finished; nav's controller must be untouched.
|
|
95
|
+
expect(router._abortController).toBe(navController)
|
|
96
|
+
expect(navController.signal.aborted).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
81
99
|
test('prefetchLoaderData passes AbortSignal to loaders', async () => {
|
|
82
100
|
let receivedSignal: AbortSignal | undefined
|
|
83
101
|
const routes: RouteRecord[] = [
|