@pyreon/router 0.12.15 → 0.13.1

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.
@@ -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.
@@ -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.15",
3
+ "version": "0.13.1",
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.12.15",
47
- "@pyreon/reactivity": "^0.12.15",
48
- "@pyreon/runtime-dom": "^0.12.15"
46
+ "@pyreon/core": "^0.13.1",
47
+ "@pyreon/reactivity": "^0.13.1",
48
+ "@pyreon/runtime-dom": "^0.13.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@happy-dom/global-registrator": "^20.8.9",
52
- "@pyreon/test-utils": "^0.12.10",
52
+ "@pyreon/manifest": "0.13.1",
53
+ "@pyreon/test-utils": "^0.13.1",
53
54
  "@vitest/browser-playwright": "^4.1.4",
54
55
  "happy-dom": "^20.8.3"
55
56
  }
@@ -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
 
@@ -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
- if (beforeUnloadHandler) {
168
- window.removeEventListener('beforeunload', beforeUnloadHandler)
169
- }
183
+ releaseBeforeUnload()
170
184
  }
171
185
 
172
186
  // Auto-remove when the component that called useBlocker unmounts
@@ -949,11 +963,15 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
949
963
  if (_hashchangeHandler) window.removeEventListener('hashchange', _hashchangeHandler)
950
964
  guards.length = 0
951
965
  afterHooks.length = 0
966
+ // Release beforeunload for any remaining blockers
967
+ for (let i = router._blockers.size; i > 0; i--) releaseBeforeUnload()
952
968
  router._blockers.clear()
953
969
  componentCache.clear()
954
970
  router._loaderData.clear()
955
971
  router._abortController?.abort()
956
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
957
975
  },
958
976
 
959
977
  _resolve: (rawPath: string) => resolveRoute(rawPath, routes),
@@ -0,0 +1,97 @@
1
+ import {
2
+ renderApiReferenceEntries,
3
+ renderLlmsFullSection,
4
+ renderLlmsTxtLine,
5
+ } from '@pyreon/manifest'
6
+ import routerManifest from '../manifest'
7
+
8
+ describe('gen-docs — router snapshot', () => {
9
+ it('renders @pyreon/router to its expected llms.txt bullet', () => {
10
+ expect(renderLlmsTxtLine(routerManifest)).toMatchInlineSnapshot(`"- @pyreon/router — hash+history+SSR, context-based, prefetching, guards, loaders, useIsActive, View Transitions, middleware, typed search params. \`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."`)
11
+ })
12
+
13
+ it('renders @pyreon/router to its expected llms-full.txt section — full body snapshot', () => {
14
+ expect(renderLlmsFullSection(routerManifest)).toMatchInlineSnapshot(`
15
+ "## @pyreon/router — Router
16
+
17
+ 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.
18
+
19
+ \`\`\`typescript
20
+ import { createRouter, RouterProvider, RouterView, RouterLink, useRouter, useRoute, useIsActive, useTypedSearchParams, useTransition, useLoaderData, useMiddlewareData } from "@pyreon/router"
21
+ import { mount } from "@pyreon/runtime-dom"
22
+
23
+ // Define routes with typed params, guards, loaders, and middleware
24
+ const router = createRouter({
25
+ routes: [
26
+ { path: "/", component: Home, name: "home" },
27
+ { path: "/user/:id", component: User, name: "user",
28
+ loader: ({ params }) => fetchUser(params.id),
29
+ meta: { title: "User Profile" } },
30
+ { path: "/admin", component: AdminLayout,
31
+ beforeEnter: (to, from) => isAdmin() || "/login",
32
+ children: [
33
+ { path: "users", component: AdminUsers },
34
+ { path: "settings", component: AdminSettings },
35
+ ] },
36
+ { path: "/settings", redirect: "/admin/settings" },
37
+ { path: "(.*)", component: NotFound },
38
+ ],
39
+ middleware: [authMiddleware, loggerMiddleware],
40
+ })
41
+
42
+ // Mount with RouterProvider
43
+ mount(
44
+ <RouterProvider router={router}>
45
+ <nav>
46
+ <RouterLink to="/" activeClass="nav-active">Home</RouterLink>
47
+ <RouterLink to={{ name: "user", params: { id: "42" } }}>Profile</RouterLink>
48
+ </nav>
49
+ <RouterView />
50
+ </RouterProvider>,
51
+ document.getElementById("app")!
52
+ )
53
+
54
+ // Inside a component — hooks
55
+ const User = () => {
56
+ const route = useRoute<"/user/:id">()
57
+ const data = useLoaderData<UserData>()
58
+ const router = useRouter()
59
+ const isAdmin = useIsActive("/admin")
60
+ const { isTransitioning } = useTransition()
61
+ const params = useTypedSearchParams({ tab: "string", page: "number" })
62
+
63
+ return (
64
+ <div>
65
+ <h1>{data.name} (ID: {route().params.id})</h1>
66
+ <Show when={isTransitioning()}>
67
+ <ProgressBar />
68
+ </Show>
69
+ <button onClick={() => router.push("/")}>Go Home</button>
70
+ </div>
71
+ )
72
+ }
73
+ \`\`\`
74
+
75
+ > **View Transitions — what push() awaits**: \`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.
76
+ >
77
+ > **Hash mode uses pushState**: 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.
78
+ >
79
+ > **Imperative navigation in render body**: \`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.
80
+ >
81
+ > **Hook ordering with View Transitions**: \`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.
82
+ >
83
+ > **For uses by, not key**: \`<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}\`.
84
+ "
85
+ `)
86
+ })
87
+
88
+ it('renders @pyreon/router to MCP api-reference entries — one per api[] item', () => {
89
+ const record = renderApiReferenceEntries(routerManifest)
90
+ expect(Object.keys(record).length).toBe(15)
91
+ expect(Object.keys(record)).toContain('router/createRouter')
92
+ // Spot-check the flagship API — createRouter is the factory
93
+ const createRouter = record['router/createRouter']!
94
+ expect(createRouter.notes).toContain('routes')
95
+ expect(createRouter.mistakes?.split('\n').length).toBeGreaterThan(2)
96
+ })
97
+ })