waku-navigation 0.0.3 → 0.0.4
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/README.md +61 -83
- package/dist/client.d.ts +49 -11
- package/dist/client.js +147 -122
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A drop-in replacement for `waku/router/client` built on the [Navigation API](htt
|
|
|
4
4
|
|
|
5
5
|
The entire public surface of `waku/router/client` — including every `unstable_*` feature — has a path to the same behavior with `waku-navigation`. This README walks through every feature and shows what the migration looks like.
|
|
6
6
|
|
|
7
|
+
Because the Navigation API intercepts plain `<a>` clicks, **every `<a>` already navigates client-side — no `<Link>` required.** `<Link>` is still here, as an _enhancement_: it adds a type-safe `to`, prefetching, and per-link navigation status. Reach for `<Link>` when you want those; use a plain `<a>` when you don't.
|
|
8
|
+
|
|
7
9
|
> **Browser support**: the Navigation API ships in Chromium 102+ and Safari 26 / Firefox 145 (behind/with caveats on some older versions). Check [caniuse](https://caniuse.com/mdn-api_navigation) for current coverage.
|
|
8
10
|
|
|
9
11
|
## Install
|
|
@@ -39,7 +41,7 @@ Pages and `pages/_slices/*` work exactly as in any Waku app — `waku-navigation
|
|
|
39
41
|
## Examples
|
|
40
42
|
|
|
41
43
|
- `examples/01_minimal` — `useRouter`, `<Slice>`, 404, prefetch, scroll option, events, HMR ([StackBlitz](https://stackblitz.com/github/wakujs/waku-navigation/tree/main/examples/01_minimal))
|
|
42
|
-
- `examples/02_pending` — `useNavigationStatus_UNSTABLE` pending indicators on
|
|
44
|
+
- `examples/02_pending` — `<Link>` with per-link `useNavigationStatus_UNSTABLE` pending indicators, two same-route links staying independent on click, a View Transitions link, and a non-navigation transition that leans on the browser's native spinner
|
|
43
45
|
|
|
44
46
|
---
|
|
45
47
|
|
|
@@ -84,57 +86,62 @@ Notes:
|
|
|
84
86
|
- `scroll: false` is forwarded to the navigate event via the Navigation API's `info` channel, which is not persisted in history. The internal handler then intercepts with `scroll: 'manual'` so the browser skips its default after-transition scroll.
|
|
85
87
|
- `prefetch(to)` calls `unstable_prefetchRsc` and, if the build publishes a `__WAKU_ROUTER_PREFETCH__` helper, preloads the route's JS chunks via `react-dom`'s `preloadModule`.
|
|
86
88
|
|
|
87
|
-
###
|
|
89
|
+
### `<Link>`
|
|
90
|
+
|
|
91
|
+
A plain `<a>` already navigates client-side, so `<Link>` is an _enhancement_, not a requirement. It adds the three things a bare `<a>` can't express:
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
- a **type-safe `to`** — checked against your generated routes, like `waku/router`'s `<Link>`;
|
|
94
|
+
- **prefetching** — `unstable_prefetchOnEnter` / `unstable_prefetchOnView`;
|
|
95
|
+
- **navigation status** — readable by any descendant via `useNavigationStatus_UNSTABLE()`.
|
|
90
96
|
|
|
91
97
|
```tsx
|
|
92
|
-
|
|
93
|
-
import { useNavigationStatus_UNSTABLE } from 'waku-navigation';
|
|
98
|
+
import { Link } from 'waku-navigation';
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
+
<Link to="/slow">
|
|
101
|
+
Slow <NavSpinner />
|
|
102
|
+
</Link>;
|
|
103
|
+
```
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
```ts
|
|
106
|
+
export type LinkProps = {
|
|
107
|
+
to: InferredPaths; // type-safe, from your generated routes
|
|
108
|
+
scroll?: boolean; // false keeps scroll position; otherwise browser default
|
|
109
|
+
unstable_prefetchOnEnter?: boolean; // prefetch on pointer enter
|
|
110
|
+
unstable_prefetchOnView?: boolean; // prefetch when scrolled into view
|
|
111
|
+
unstable_startTransition?: (fn: TransitionFunction) => void; // e.g. View Transitions
|
|
112
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
113
|
+
} & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>;
|
|
106
114
|
```
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
<a href="/slow">Slow <NavSpinner href="/slow" /></a>
|
|
116
|
+
`<Link>` does not intercept the click itself — the browser fires the navigate event and the router correlates it back to this instance. So modifier-clicks, `target`, `download`, and cross-origin `to` all keep their native behavior (use a plain `<a>` for those anyway). The props mirror `waku/router`'s `<Link>`, so migrating across is an import swap.
|
|
110
117
|
|
|
111
|
-
|
|
112
|
-
```
|
|
118
|
+
`unstable_startTransition` overrides how the route-commit transition is started — for example, to integrate the browser View Transitions API. When provided, the per-link pending state is bypassed (it stays `{}`), matching `waku/router`.
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
### `useNavigationStatus_UNSTABLE()`
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
Returns the navigation status of the enclosing `<Link>`, like React's `useFormStatus`. No arguments — it reads the `<Link>` by context.
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
```tsx
|
|
125
|
+
'use client';
|
|
126
|
+
import { useNavigationStatus_UNSTABLE } from 'waku-navigation';
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<a href="/slow" data-nav-key={dataNavKey}>
|
|
127
|
-
Slow <IdSpinner dataNavKey={dataNavKey} />
|
|
128
|
-
</a>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
```
|
|
128
|
+
function NavSpinner() {
|
|
129
|
+
const { pending } = useNavigationStatus_UNSTABLE();
|
|
130
|
+
return pending ? <span>…</span> : null;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
```tsx
|
|
135
|
+
<Link to="/slow">
|
|
136
|
+
Slow <NavSpinner />
|
|
137
|
+
</Link>
|
|
138
|
+
```
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
`pending` is `true` while the link's navigation is in flight and clears in the same commit that reveals the new route — after the destination's client-side `<Suspense>` boundaries settle, and also on abort or error. Two `<Link>`s to the same route stay **independent on click** (the router correlates the clicked one by its own element); programmatic and back/forward navigations have no source element, so every `<Link>` to the destination lights up together. Outside a `<Link>`, the hook returns `{}`.
|
|
136
141
|
|
|
137
|
-
Internally
|
|
142
|
+
Internally each `<Link>` holds a `useOptimistic` state that the router flips inside the navigation transition; React reverts it automatically when the transition settles, so there's no subscription or cleanup to manage.
|
|
143
|
+
|
|
144
|
+
> **You often don't need a custom spinner at all.** React already drives the browser's native spinner for transitions — during a navigation the Navigation API holds the request pending until the new route commits, and for non-navigation transitions React (≥19.2) fires a fake navigation via `onDefaultTransitionIndicator` to spin that same native indicator. Reach for `useNavigationStatus_UNSTABLE` when you want an _in-page_, per-link indicator on top of the browser-level one. `examples/02_pending` includes a transition (the home page's "Load batch" button) that relies on the native spinner alone.
|
|
138
145
|
|
|
139
146
|
### `<Slice>`
|
|
140
147
|
|
|
@@ -160,71 +167,41 @@ import { Slice } from 'waku-navigation';
|
|
|
160
167
|
|
|
161
168
|
`<Router>` takes no props in `waku-navigation` — there is no `initialRoute`, `unstable_fetchRscStore`, or `unstable_routeInterceptor`. The initial route comes from `window.navigation`. If you used `unstable_routeInterceptor` to rewrite a path before refetch, do it in your `useRouter().push` call site instead.
|
|
162
169
|
|
|
163
|
-
### `<Link>`
|
|
170
|
+
### `<Link>` (drop-in) or plain `<a>`
|
|
164
171
|
|
|
165
|
-
|
|
172
|
+
`<Link>` is a drop-in — same import path swap, same props (`to`, `scroll`, `unstable_prefetchOnEnter`, `unstable_prefetchOnView`, `unstable_startTransition`, `ref`, and any `<a>` attributes):
|
|
166
173
|
|
|
167
174
|
```diff
|
|
168
175
|
- import { Link } from 'waku/router/client';
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
+ import { Link } from 'waku-navigation';
|
|
177
|
+
<Link to="/about">About</Link>
|
|
171
178
|
```
|
|
172
179
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
| `<Link>` prop | `waku-navigation` equivalent |
|
|
176
|
-
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
177
|
-
| `to="/x"` | `<a href="/x">` |
|
|
178
|
-
| `scroll={false}` | Click handler that calls `useRouter().push(href, { scroll: false })` |
|
|
179
|
-
| `unstable_pending={node}` | A consumer using `useNavigationStatus_UNSTABLE({ href })` (or `{ dataNavKey }`) to render `node` when pending |
|
|
180
|
-
| `unstable_notPending={node}` | Same, rendering `node` when `!pending` |
|
|
181
|
-
| `unstable_prefetchOnEnter` | `onMouseEnter={() => useRouter().prefetch(href)}` in a client component |
|
|
182
|
-
| `unstable_prefetchOnView` | `IntersectionObserver` + `useRouter().prefetch(href)` |
|
|
183
|
-
| `unstable_startTransition` | Not needed — the router runs every navigation in a transition internally |
|
|
184
|
-
|
|
185
|
-
Example for prefetch-on-hover:
|
|
186
|
-
|
|
187
|
-
```tsx
|
|
188
|
-
'use client';
|
|
189
|
-
import { useRouter } from 'waku-navigation';
|
|
180
|
+
Or drop `<Link>` entirely where you don't need type-safety, prefetching, or status — a plain `<a>` navigates client-side on its own:
|
|
190
181
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}: {
|
|
195
|
-
to: string;
|
|
196
|
-
children: ReactNode;
|
|
197
|
-
}) {
|
|
198
|
-
const { prefetch } = useRouter();
|
|
199
|
-
return (
|
|
200
|
-
<a href={to} onMouseEnter={() => prefetch(to)}>
|
|
201
|
-
{children}
|
|
202
|
-
</a>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
182
|
+
```diff
|
|
183
|
+
- <Link to="/about">About</Link>
|
|
184
|
+
+ <a href="/about">About</a>
|
|
205
185
|
```
|
|
206
186
|
|
|
187
|
+
Cross-origin links, hash-only links, download links, and modifier-keyed clicks all behave correctly with a plain `<a>` — the Navigation API passes them through.
|
|
188
|
+
|
|
207
189
|
### `<Link>…<Consumer/></Link>` (navigation status)
|
|
208
190
|
|
|
209
|
-
|
|
191
|
+
Unchanged — a descendant reads the enclosing `<Link>`'s status via the no-arg hook, exactly as in `waku/router`:
|
|
210
192
|
|
|
211
193
|
```diff
|
|
212
194
|
- import { Link, useNavigationStatus_UNSTABLE } from 'waku/router/client';
|
|
213
|
-
+ import { useNavigationStatus_UNSTABLE } from 'waku-navigation';
|
|
195
|
+
+ import { Link, useNavigationStatus_UNSTABLE } from 'waku-navigation';
|
|
214
196
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
+ function NavSpinner({ href }: { href: string }) {
|
|
218
|
-
+ const { pending } = useNavigationStatus_UNSTABLE({ href });
|
|
197
|
+
function NavSpinner() {
|
|
198
|
+
const { pending } = useNavigationStatus_UNSTABLE();
|
|
219
199
|
return pending ? <span>…</span> : null;
|
|
220
200
|
}
|
|
221
201
|
|
|
222
|
-
|
|
223
|
-
+ <a href="/slow">Slow <NavSpinner href="/slow" /></a>
|
|
202
|
+
<Link to="/slow">Slow <NavSpinner /></Link>
|
|
224
203
|
```
|
|
225
204
|
|
|
226
|
-
Reach for `{ dataNavKey }` + `data-nav-key` on the `<a>` only when you need two same-href anchors to light up independently.
|
|
227
|
-
|
|
228
205
|
### `<Slice>`
|
|
229
206
|
|
|
230
207
|
Same import path change as `useRouter`. All props (`id`, `lazy`, `fallback`, children) are unchanged.
|
|
@@ -297,6 +274,7 @@ These are all handled inside the navigate-event listener so apps usually don't n
|
|
|
297
274
|
- **Form submission guard** — `<form method="POST">` submissions (`event.formData != null`) are passed through to the server.
|
|
298
275
|
- **Hash-only navigations** — not intercepted by default (the browser scrolls to the anchor natively), but state is synced so `useRouter().hash` reflects the new fragment. If `useRouter().push('#x', { scroll: false })` is used, the handler intercepts with `scroll: 'manual'` to honor that.
|
|
299
276
|
- **Abort during transition** — `event.signal` is checked between async steps so a fast-clicked second navigation cleanly cancels the first without committing stale state.
|
|
277
|
+
- **React's default transition indicator** — React (≥19.2) fires a fake same-URL navigation tagged `info: 'react-transition'` for every transition, intercepting it to show the browser's native spinner. The router skips these (they aren't route changes), so an unrelated `useTransition` anywhere in your app never triggers a refetch.
|
|
300
278
|
- **404 on the client** — a refetch that throws with `getErrorInfo(err)?.status === 404` is handled by refetching `/404` and pointing the slot there, mirroring Waku's behavior. The URL still reflects the user's request.
|
|
301
279
|
- **Static route cache** — routes with `getConfig({ render: 'static' })` are added to a `staticPathSet` after their first fetch; revisits skip the refetch entirely (the RSC payload is already in Waku's store).
|
|
302
280
|
- **`X-Waku-Router-Skip` header** — every refetch sends the etags of elements we already have (harvested from the RSC payload's `ETAG:`-prefixed entries) so the server can skip re-rendering shared layouts/slices whose etag still matches.
|
|
@@ -306,7 +284,7 @@ These are all handled inside the navigate-event listener so apps usually don't n
|
|
|
306
284
|
|
|
307
285
|
## Caveats / not yet implemented
|
|
308
286
|
|
|
309
|
-
-
|
|
287
|
+
- `<Link>` is an enhancement over plain `<a>`, not a requirement: a plain `<a>` navigates client-side on its own; `<Link>` adds a type-safe `to`, prefetching, and per-link navigation status via `useNavigationStatus_UNSTABLE()`.
|
|
310
288
|
- `unstable_routeInterceptor` (server-side route rewrite hook) is not supported.
|
|
311
289
|
- `unstable_fetchRscStore` (custom RSC store) is not exposed on `<Router>`.
|
|
312
290
|
- Requires a browser with the Navigation API. There is currently no fallback for older browsers.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type AnchorHTMLAttributes, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { Slice, type Unstable_InferredPaths as InferredPaths } from 'waku/router/client';
|
|
2
3
|
export { Slice };
|
|
3
4
|
type Route = {
|
|
4
5
|
path: string;
|
|
@@ -8,13 +9,43 @@ type Route = {
|
|
|
8
9
|
type NavigationStatus = {
|
|
9
10
|
pending?: boolean;
|
|
10
11
|
};
|
|
11
|
-
type
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
type TransitionFunction = () => void | Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Navigation status of the enclosing {@link Link}, like React's
|
|
15
|
+
* `useFormStatus`. `pending` is `true` while the link's navigation is in
|
|
16
|
+
* flight, until the destination route's async components resolve. Returns `{}`
|
|
17
|
+
* outside a `<Link>`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const useNavigationStatus_UNSTABLE: () => NavigationStatus;
|
|
20
|
+
/** Props for {@link Link}. Mirrors `waku/router`'s `<Link>`. */
|
|
21
|
+
export type LinkProps = {
|
|
22
|
+
/** Destination, type-checked against your app's generated routes. */
|
|
23
|
+
to: InferredPaths;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to scroll on navigation. `false` keeps the current scroll
|
|
27
|
+
* position; otherwise the browser's default after-navigation scroll applies.
|
|
28
|
+
*/
|
|
29
|
+
scroll?: boolean;
|
|
30
|
+
/** Prefetch the route when the pointer enters the link. */
|
|
31
|
+
unstable_prefetchOnEnter?: boolean;
|
|
32
|
+
/** Prefetch the route when the link scrolls into view. */
|
|
33
|
+
unstable_prefetchOnView?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Overrides how the route-commit transition is started, e.g. to integrate
|
|
36
|
+
* the browser View Transitions API. When set, the pending state is bypassed,
|
|
37
|
+
* so {@link useNavigationStatus_UNSTABLE} stays `{}` for this link.
|
|
38
|
+
*/
|
|
39
|
+
unstable_startTransition?: ((fn: TransitionFunction) => void) | undefined;
|
|
40
|
+
ref?: Ref<HTMLAnchorElement> | undefined;
|
|
41
|
+
} & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>;
|
|
42
|
+
/**
|
|
43
|
+
* A type-safe, prefetching, status-aware link. A plain `<a>` already navigates
|
|
44
|
+
* client-side, so `<Link>` is an enhancement: it adds a type-checked `to`,
|
|
45
|
+
* prefetching, and per-link navigation status (read by descendants via
|
|
46
|
+
* {@link useNavigationStatus_UNSTABLE}). Mirrors `waku/router`'s `<Link>`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function Link({ to, children, scroll, unstable_prefetchOnEnter, unstable_prefetchOnView, unstable_startTransition, ref: refProp, ...props }: LinkProps): import("react").JSX.Element;
|
|
18
49
|
type PushReplaceOptions = {
|
|
19
50
|
scroll?: boolean;
|
|
20
51
|
};
|
|
@@ -22,6 +53,11 @@ type RouteChangeEvents = {
|
|
|
22
53
|
on: (name: 'start' | 'complete', handler: (route: Route) => void) => void;
|
|
23
54
|
off: (name: 'start' | 'complete', handler: (route: Route) => void) => void;
|
|
24
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Imperative router handle: the current `path` / `query` / `hash`, plus
|
|
58
|
+
* `push` / `replace` / `reload` / `back` / `forward` / `prefetch` and
|
|
59
|
+
* `unstable_events`. Same shape as `waku/router/client`'s `useRouter`.
|
|
60
|
+
*/
|
|
25
61
|
export declare function useRouter(): {
|
|
26
62
|
path: string;
|
|
27
63
|
query: string;
|
|
@@ -34,6 +70,8 @@ export declare function useRouter(): {
|
|
|
34
70
|
prefetch: (to: string) => void;
|
|
35
71
|
unstable_events: RouteChangeEvents;
|
|
36
72
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The client router. Reads the initial route from `window.navigation`, listens
|
|
75
|
+
* for navigate events, and renders the current page. Takes no props.
|
|
76
|
+
*/
|
|
77
|
+
export declare function Router(): import("react").JSX.Element;
|
package/dist/client.js
CHANGED
|
@@ -4,27 +4,95 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
import { createContext, startTransition, use, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useOptimistic, useRef, useState, } from 'react';
|
|
5
5
|
import { preloadModule } from 'react-dom';
|
|
6
6
|
import { Root, Slot, unstable_prefetchRsc as prefetchRsc, unstable_withEnhanceFetchFn as withEnhanceFetchFn, useElementsPromise_UNSTABLE as useElementsPromise, useRefetch, } from 'waku/minimal/client';
|
|
7
|
-
import { Slice, unstable_encodeRoutePath as encodeRoutePath, unstable_getErrorInfo as getErrorInfo, unstable_parseRoute as parseRoute, unstable_getRouteSlotId as getRouteSlotId, unstable_IS_STATIC_ID as IS_STATIC_ID, unstable_ROUTE_ID as ROUTE_ID, unstable_RouterContext as RouterContext, unstable_SKIP_HEADER as SKIP_HEADER, } from 'waku/router/client';
|
|
8
|
-
// Slice is re-exported from waku/router/client unchanged. It only needs the
|
|
9
|
-
// router context (fetchingSlices + the elements promise) -- both of which our
|
|
10
|
-
// <Router> already provides -- so the component works as-is.
|
|
7
|
+
import { Slice, unstable_addBase as addBase, unstable_encodeRoutePath as encodeRoutePath, unstable_getErrorInfo as getErrorInfo, unstable_parseRoute as parseRoute, unstable_getRouteSlotId as getRouteSlotId, unstable_IS_STATIC_ID as IS_STATIC_ID, unstable_ROUTE_ID as ROUTE_ID, unstable_RouterContext as RouterContext, unstable_SKIP_HEADER as SKIP_HEADER, } from 'waku/router/client';
|
|
11
8
|
export { Slice };
|
|
12
9
|
const NOT_FOUND_PATH = '/404';
|
|
13
|
-
//
|
|
14
|
-
// consumer, the way <label htmlFor> correlates with <input id>. The router
|
|
15
|
-
// reads it off the clicked <a> (or, for programmatic navs, off the matching
|
|
16
|
-
// <a> in the DOM) to know which consumers to mark pending.
|
|
17
|
-
const NAV_KEY_ATTR = 'data-nav-key';
|
|
18
|
-
// Mirrors ETAG_ID_PREFIX in waku's router/common.js, which is not exported
|
|
19
|
-
// from waku/router/client. Elements under this prefix carry the etag for the
|
|
20
|
-
// same-named slot; the X-Waku-Router-Skip header echoes them back so the
|
|
21
|
-
// server can skip re-rendering unchanged slots.
|
|
10
|
+
// Mirrors waku's unexported ETAG_ID_PREFIX (router/common.js).
|
|
22
11
|
const ETAG_ID_PREFIX = 'ETAG:';
|
|
23
12
|
const noopRegister = () => () => { };
|
|
24
13
|
const NavStatusRegistryContext = createContext({
|
|
25
14
|
register: noopRegister,
|
|
26
15
|
});
|
|
16
|
+
const NavigationStatusContext = createContext({});
|
|
17
|
+
/**
|
|
18
|
+
* Navigation status of the enclosing {@link Link}, like React's
|
|
19
|
+
* `useFormStatus`. `pending` is `true` while the link's navigation is in
|
|
20
|
+
* flight, until the destination route's async components resolve. Returns `{}`
|
|
21
|
+
* outside a `<Link>`.
|
|
22
|
+
*/
|
|
23
|
+
export const useNavigationStatus_UNSTABLE = () => useContext(NavigationStatusContext);
|
|
24
|
+
/**
|
|
25
|
+
* A type-safe, prefetching, status-aware link. A plain `<a>` already navigates
|
|
26
|
+
* client-side, so `<Link>` is an enhancement: it adds a type-checked `to`,
|
|
27
|
+
* prefetching, and per-link navigation status (read by descendants via
|
|
28
|
+
* {@link useNavigationStatus_UNSTABLE}). Mirrors `waku/router`'s `<Link>`.
|
|
29
|
+
*/
|
|
30
|
+
export function Link({ to, children, scroll, unstable_prefetchOnEnter, unstable_prefetchOnView, unstable_startTransition, ref: refProp, ...props }) {
|
|
31
|
+
var _a;
|
|
32
|
+
const base = (_a = import.meta.env) === null || _a === void 0 ? void 0 : _a.WAKU_CONFIG_BASE_PATH;
|
|
33
|
+
const resolvedTo = base ? addBase(to, base) : to;
|
|
34
|
+
const ctx = useContext(RouterContext);
|
|
35
|
+
const { register } = useContext(NavStatusRegistryContext);
|
|
36
|
+
const [status, setOptimisticStatus] = useOptimistic({});
|
|
37
|
+
const elementRef = useRef(null);
|
|
38
|
+
const setRef = useCallback((node) => {
|
|
39
|
+
elementRef.current = node;
|
|
40
|
+
if (typeof refProp === 'function')
|
|
41
|
+
refProp(node);
|
|
42
|
+
else if (refProp)
|
|
43
|
+
refProp.current = node;
|
|
44
|
+
}, [refProp]);
|
|
45
|
+
const id = useId();
|
|
46
|
+
useLayoutEffect(() => register(id, {
|
|
47
|
+
getElement: () => elementRef.current,
|
|
48
|
+
href: resolvedTo,
|
|
49
|
+
scroll,
|
|
50
|
+
unstable_startTransition,
|
|
51
|
+
setOptimisticStatus,
|
|
52
|
+
}), [
|
|
53
|
+
id,
|
|
54
|
+
resolvedTo,
|
|
55
|
+
scroll,
|
|
56
|
+
unstable_startTransition,
|
|
57
|
+
register,
|
|
58
|
+
setOptimisticStatus,
|
|
59
|
+
]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!unstable_prefetchOnView || !elementRef.current)
|
|
62
|
+
return;
|
|
63
|
+
const observer = new IntersectionObserver((entries) => {
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (!entry.isIntersecting)
|
|
66
|
+
continue;
|
|
67
|
+
const url = new URL(resolvedTo, window.location.href);
|
|
68
|
+
if (url.href !== window.location.href) {
|
|
69
|
+
ctx === null || ctx === void 0 ? void 0 : ctx.prefetchRoute(parseRoute(url));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, { threshold: 0.1 });
|
|
73
|
+
observer.observe(elementRef.current);
|
|
74
|
+
return () => observer.disconnect();
|
|
75
|
+
}, [unstable_prefetchOnView, resolvedTo, ctx]);
|
|
76
|
+
const onMouseEnter = unstable_prefetchOnEnter
|
|
77
|
+
? (event) => {
|
|
78
|
+
var _a;
|
|
79
|
+
const url = new URL(resolvedTo, window.location.href);
|
|
80
|
+
if (url.href !== window.location.href) {
|
|
81
|
+
ctx === null || ctx === void 0 ? void 0 : ctx.prefetchRoute(parseRoute(url));
|
|
82
|
+
}
|
|
83
|
+
(_a = props.onMouseEnter) === null || _a === void 0 ? void 0 : _a.call(props, event);
|
|
84
|
+
}
|
|
85
|
+
: props.onMouseEnter;
|
|
86
|
+
// No onClick: the browser fires the navigate event for the plain <a>, and
|
|
87
|
+
// InnerRouter's handler correlates it back to this instance.
|
|
88
|
+
return (_jsx(NavigationStatusContext.Provider, { value: status, children: _jsx("a", { ...props, href: resolvedTo, ref: setRef, onMouseEnter: onMouseEnter, children: children }) }));
|
|
89
|
+
}
|
|
27
90
|
const noopEvents = { on: () => { }, off: () => { } };
|
|
91
|
+
/**
|
|
92
|
+
* Imperative router handle: the current `path` / `query` / `hash`, plus
|
|
93
|
+
* `push` / `replace` / `reload` / `back` / `forward` / `prefetch` and
|
|
94
|
+
* `unstable_events`. Same shape as `waku/router/client`'s `useRouter`.
|
|
95
|
+
*/
|
|
28
96
|
export function useRouter() {
|
|
29
97
|
var _a, _b;
|
|
30
98
|
const ctx = useContext(RouterContext);
|
|
@@ -54,35 +122,8 @@ export function useRouter() {
|
|
|
54
122
|
unstable_events: ((_b = ctx === null || ctx === void 0 ? void 0 : ctx.routeChangeEvents) !== null && _b !== void 0 ? _b : noopEvents),
|
|
55
123
|
};
|
|
56
124
|
}
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// { href: '/slow' } -- any navigation whose destination is /slow. Nothing
|
|
61
|
-
// extra on the <a>; matches by destination, so every
|
|
62
|
-
// anchor to /slow shares it (no independence).
|
|
63
|
-
// { dataNavKey: 'x' } -- the navigation from <a data-nav-key="x">. Tells two
|
|
64
|
-
// same-href anchors apart (give them different ids;
|
|
65
|
-
// useId() for list-rendered ones).
|
|
66
|
-
//
|
|
67
|
-
// Pass both to match either. The consumer can live anywhere -- inside the <a>,
|
|
68
|
-
// beside it, or far away. Returns { pending: undefined } until a matching
|
|
69
|
-
// navigation is in flight; pending clears when the new route commits (after
|
|
70
|
-
// client-side Suspense), or on abort/error.
|
|
71
|
-
function useNavigationStatus({ href, dataNavKey, }) {
|
|
72
|
-
const [status, setOptimisticStatus] = useOptimistic({});
|
|
73
|
-
const { register } = useContext(NavStatusRegistryContext);
|
|
74
|
-
const id = useId();
|
|
75
|
-
useLayoutEffect(() => register(id, { href, dataNavKey, setOptimisticStatus }), [id, href, dataNavKey, register, setOptimisticStatus]);
|
|
76
|
-
return status;
|
|
77
|
-
}
|
|
78
|
-
export { useNavigationStatus as useNavigationStatus_UNSTABLE };
|
|
79
|
-
// True when `href` (possibly relative) resolves to the same internal route as
|
|
80
|
-
// `route`. Compared on origin + path + query -- not just pathname -- so
|
|
81
|
-
// /search?q=a doesn't match /search?q=b, and a cross-origin href that happens
|
|
82
|
-
// to share a path doesn't match an internal navigation. The fragment is
|
|
83
|
-
// ignored (hash-only navigations never set pending). Malformed input returns
|
|
84
|
-
// false rather than throwing, so a bad consumer href or odd DOM anchor can't
|
|
85
|
-
// break the navigation handler.
|
|
125
|
+
// Same origin + path + query (not pathname; fragment ignored). Malformed input
|
|
126
|
+
// returns false rather than throwing.
|
|
86
127
|
const routeMatchesHref = (href, route) => {
|
|
87
128
|
let url;
|
|
88
129
|
try {
|
|
@@ -102,15 +143,10 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
102
143
|
const [routeState, setRoute] = useState();
|
|
103
144
|
let route = routeState;
|
|
104
145
|
if (route === undefined) {
|
|
105
|
-
// First render only
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// -- but it must not happen on later renders: suspending InnerRouter
|
|
110
|
-
// inside the navigation transition keeps the navigation from ever
|
|
111
|
-
// committing. The hash starts as '' to match Waku's INTERNAL_ServerRouter
|
|
112
|
-
// SSR output (URL fragments aren't sent to the server) and is upgraded
|
|
113
|
-
// post-hydration in the effect below.
|
|
146
|
+
// First render only. ROUTE_ID records the route the server actually
|
|
147
|
+
// rendered, so an unknown URL served the /404 page resolves to '/404'. This
|
|
148
|
+
// must not suspend on later renders: suspending inside a navigation
|
|
149
|
+
// transition would keep it from ever committing.
|
|
114
150
|
const elements = use(elementsPromise);
|
|
115
151
|
const routeData = elements[ROUTE_ID];
|
|
116
152
|
route =
|
|
@@ -119,26 +155,23 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
119
155
|
: { ...fallbackRoute, hash: '' };
|
|
120
156
|
setRoute(route);
|
|
121
157
|
}
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// The state clears on the next successful navigation.
|
|
158
|
+
// Rethrow during render so the user's <ErrorBoundary> catches non-404
|
|
159
|
+
// failures; cleared by the next successful navigation.
|
|
125
160
|
const [renderError, setRenderError] = useState(null);
|
|
126
161
|
if (renderError)
|
|
127
162
|
throw renderError;
|
|
128
163
|
useEffect(() => {
|
|
164
|
+
// SSR sends no fragment, so the hash starts ''; upgrade it post-hydration.
|
|
129
165
|
if (fallbackRoute.hash) {
|
|
130
166
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
131
167
|
setRoute((r) => ({ ...r, hash: fallbackRoute.hash }));
|
|
132
168
|
}
|
|
133
|
-
// Only on mount.
|
|
134
169
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
170
|
}, []);
|
|
136
171
|
const registryRef = useRef(new Map());
|
|
137
172
|
const staticPathSetRef = useRef(new Set());
|
|
138
173
|
const cachedEtagsRef = useRef({});
|
|
139
|
-
// Stable
|
|
140
|
-
// fetch end) without losing state across re-renders. useMemo with [] keeps
|
|
141
|
-
// the same instance and avoids reading ref.current during render.
|
|
174
|
+
// Stable instance: <Slice> mutates this Set across renders.
|
|
142
175
|
const fetchingSlices = useMemo(() => new Set(), []);
|
|
143
176
|
useEffect(() => {
|
|
144
177
|
elementsPromise.then((elements) => {
|
|
@@ -148,10 +181,10 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
148
181
|
}
|
|
149
182
|
const etags = {};
|
|
150
183
|
for (const [key, value] of Object.entries(elements)) {
|
|
151
|
-
//
|
|
184
|
+
// Skip empty (clear signal) and non-Latin1 (breaks the fetch header).
|
|
152
185
|
if (key.startsWith(ETAG_ID_PREFIX) &&
|
|
153
186
|
typeof value === 'string' &&
|
|
154
|
-
/^[
|
|
187
|
+
/^[ -ÿ]+$/.test(value)) {
|
|
155
188
|
etags[key.slice(ETAG_ID_PREFIX.length)] = value;
|
|
156
189
|
}
|
|
157
190
|
}
|
|
@@ -164,17 +197,15 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
164
197
|
registryRef.current.delete(id);
|
|
165
198
|
};
|
|
166
199
|
}, []);
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// still matches. Shared by navigate + prefetch.
|
|
200
|
+
// Send our cached etags via X-Waku-Router-Skip so the server can skip
|
|
201
|
+
// re-rendering slots whose etag still matches.
|
|
170
202
|
const enhanceFetchWithSkip = useMemo(() => withEnhanceFetchFn((fetchFn) => (input, init) => {
|
|
171
203
|
const headers = new Headers(init === null || init === void 0 ? void 0 : init.headers);
|
|
172
204
|
headers.set(SKIP_HEADER, JSON.stringify(cachedEtagsRef.current));
|
|
173
205
|
return fetchFn(input, { ...init, headers });
|
|
174
206
|
}), []);
|
|
175
|
-
// Waku's prefetch cache keys
|
|
176
|
-
//
|
|
177
|
-
// entry. We memoize by query string so the same params object is reused.
|
|
207
|
+
// Waku's prefetch cache keys params by identity, so reuse one object per
|
|
208
|
+
// query string or prefetch entries get invalidated.
|
|
178
209
|
const rscParamsByQueryRef = useRef(new Map());
|
|
179
210
|
const getRscParams = useCallback((query) => {
|
|
180
211
|
let params = rscParamsByQueryRef.current.get(query);
|
|
@@ -200,21 +231,17 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
200
231
|
routeChangeListeners[name].delete(handler);
|
|
201
232
|
},
|
|
202
233
|
}), [routeChangeListeners]);
|
|
203
|
-
// Eagerly fetch the RSC for a route (used by useRouter().prefetch). Build
|
|
204
|
-
// output may also publish a __WAKU_ROUTER_PREFETCH__ helper that returns the
|
|
205
|
-
// JS chunk ids for a path; if present, we preload them too.
|
|
206
234
|
const prefetchRoute = useCallback((next) => {
|
|
207
235
|
var _a, _b;
|
|
208
236
|
if (staticPathSetRef.current.has(next.path))
|
|
209
237
|
return;
|
|
210
238
|
prefetchRsc(encodeRoutePath(next.path), getRscParams(next.query), enhanceFetchWithSkip);
|
|
239
|
+
// When the build publishes it, __WAKU_ROUTER_PREFETCH__ yields the
|
|
240
|
+
// route's JS chunk ids to preload.
|
|
211
241
|
(_b = (_a = globalThis).__WAKU_ROUTER_PREFETCH__) === null || _b === void 0 ? void 0 : _b.call(_a, next.path, (id) => preloadModule(id, { as: 'script' }));
|
|
212
242
|
}, [enhanceFetchWithSkip, getRscParams]);
|
|
213
|
-
// Vite HMR:
|
|
214
|
-
//
|
|
215
|
-
// our path/id caches (so a "static" route picks up the new content) and
|
|
216
|
-
// refetches the current route. In production import.meta.hot is undefined
|
|
217
|
-
// and the effect body returns early.
|
|
243
|
+
// Vite HMR (dev only): clear caches and refetch the current route when Waku's
|
|
244
|
+
// runtime fires __WAKU_RSC_RELOAD_LISTENERS__.
|
|
218
245
|
useEffect(() => {
|
|
219
246
|
var _a;
|
|
220
247
|
if (!import.meta.hot)
|
|
@@ -234,25 +261,34 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
234
261
|
}, [route, refetch, getRscParams]);
|
|
235
262
|
useEffect(() => {
|
|
236
263
|
const callback = (event) => {
|
|
237
|
-
var _a, _b, _c;
|
|
264
|
+
var _a, _b, _c, _d;
|
|
238
265
|
if (!event.canIntercept)
|
|
239
266
|
return;
|
|
240
267
|
if (event.downloadRequest !== null || event.formData)
|
|
241
268
|
return;
|
|
269
|
+
// React >=19.2's default transition indicator fires fake navigations.
|
|
270
|
+
if (event.info === 'react-transition')
|
|
271
|
+
return;
|
|
242
272
|
const nextRoute = parseRoute(new URL(event.destination.url));
|
|
243
|
-
// useRouter().push/replace forward { scroll } via `info`. The Navigation
|
|
244
|
-
// API itself doesn't persist `info` in history, so it only applies to
|
|
245
|
-
// this single navigation -- exactly what we want.
|
|
246
273
|
const info = event.info;
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
//
|
|
274
|
+
const source = event
|
|
275
|
+
.sourceElement;
|
|
276
|
+
const clickedAnchor = (_b = (_a = source === null || source === void 0 ? void 0 : source.closest) === null || _a === void 0 ? void 0 : _a.call(source, 'a')) !== null && _b !== void 0 ? _b : null;
|
|
277
|
+
// Match the navigating <Link>(s): the clicked one by element identity (so
|
|
278
|
+
// two same-`to` links stay independent), or -- with no source element
|
|
279
|
+
// (programmatic / back-forward) -- every <Link> whose `to` hits the dest.
|
|
280
|
+
const matched = [];
|
|
281
|
+
for (const entry of registryRef.current.values()) {
|
|
282
|
+
const hit = clickedAnchor
|
|
283
|
+
? entry.getElement() === clickedAnchor
|
|
284
|
+
: !source && routeMatchesHref(entry.href, nextRoute);
|
|
285
|
+
if (hit)
|
|
286
|
+
matched.push(entry);
|
|
287
|
+
}
|
|
288
|
+
const resolvedScroll = (_c = info === null || info === void 0 ? void 0 : info.scroll) !== null && _c !== void 0 ? _c : (matched.length ? matched[0].scroll : undefined);
|
|
289
|
+
const suppressScroll = resolvedScroll === false;
|
|
252
290
|
if (event.hashChange) {
|
|
253
|
-
// Hash-only
|
|
254
|
-
// both fire effectively together; emit both so subscribers don't
|
|
255
|
-
// have to special-case them.
|
|
291
|
+
// Hash-only: no refetch; intercept only to suppress the browser scroll.
|
|
256
292
|
emitRouteEvent('start', nextRoute);
|
|
257
293
|
if (suppressScroll) {
|
|
258
294
|
event.intercept({
|
|
@@ -271,35 +307,18 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
271
307
|
}
|
|
272
308
|
emitRouteEvent('start', nextRoute);
|
|
273
309
|
const signal = event.signal;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
let navDataKey = (_c = (_b = (_a = source === null || source === void 0 ? void 0 : source.closest) === null || _a === void 0 ? void 0 : _a.call(source, `a[${NAV_KEY_ATTR}]`)) === null || _b === void 0 ? void 0 : _b.getAttribute(NAV_KEY_ATTR)) !== null && _c !== void 0 ? _c : null;
|
|
283
|
-
if (navDataKey === null && !source) {
|
|
284
|
-
for (const anchor of document.querySelectorAll(`a[${NAV_KEY_ATTR}]`)) {
|
|
285
|
-
const href = anchor.getAttribute('href');
|
|
286
|
-
if (href !== null && routeMatchesHref(href, nextRoute)) {
|
|
287
|
-
navDataKey = anchor.getAttribute(NAV_KEY_ATTR);
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const pendingSetters = [];
|
|
293
|
-
for (const entry of registryRef.current.values()) {
|
|
294
|
-
const byKey = entry.dataNavKey !== undefined && entry.dataNavKey === navDataKey;
|
|
295
|
-
const byHref = entry.href !== undefined && routeMatchesHref(entry.href, nextRoute);
|
|
296
|
-
if (byKey || byHref)
|
|
297
|
-
pendingSetters.push(entry.setOptimisticStatus);
|
|
298
|
-
}
|
|
310
|
+
// A clicked <Link>'s unstable_startTransition overrides the commit
|
|
311
|
+
// transition (View Transitions); its pending is then bypassed.
|
|
312
|
+
const customTransition = clickedAnchor
|
|
313
|
+
? (_d = matched.find((e) => e.unstable_startTransition)) === null || _d === void 0 ? void 0 : _d.unstable_startTransition
|
|
314
|
+
: undefined;
|
|
315
|
+
const pendingSetters = matched
|
|
316
|
+
.filter((e) => !e.unstable_startTransition)
|
|
317
|
+
.map((e) => e.setOptimisticStatus);
|
|
299
318
|
event.intercept({
|
|
300
319
|
...(suppressScroll ? { scroll: 'manual' } : {}),
|
|
301
320
|
handler: () => new Promise((resolve, reject) => {
|
|
302
|
-
//
|
|
321
|
+
// Run in a transition: keeps the previous page visible while the
|
|
303
322
|
// next tree suspends, and scopes the optimistic pending updates so
|
|
304
323
|
// React reverts them on commit/abort/error.
|
|
305
324
|
startTransition(async () => {
|
|
@@ -331,15 +350,18 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
331
350
|
throw err;
|
|
332
351
|
}
|
|
333
352
|
}
|
|
334
|
-
// Updates after the first await lose the
|
|
335
|
-
//
|
|
336
|
-
// so re-wrap the commit
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
startTransition(() => {
|
|
353
|
+
// Updates after the first await lose the transition scope
|
|
354
|
+
// (https://react.dev/reference/react/startTransition#caveats),
|
|
355
|
+
// so re-wrap the commit; a <Link>'s unstable_startTransition
|
|
356
|
+
// takes over here.
|
|
357
|
+
const commitRoute = () => {
|
|
340
358
|
setRenderError(null);
|
|
341
359
|
setRoute(targetRoute);
|
|
342
|
-
}
|
|
360
|
+
};
|
|
361
|
+
if (customTransition)
|
|
362
|
+
customTransition(commitRoute);
|
|
363
|
+
else
|
|
364
|
+
startTransition(commitRoute);
|
|
343
365
|
emitRouteEvent('complete', targetRoute);
|
|
344
366
|
resolve();
|
|
345
367
|
}
|
|
@@ -355,9 +377,8 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
355
377
|
window.navigation.removeEventListener('navigate', callback);
|
|
356
378
|
};
|
|
357
379
|
}, [refetch, enhanceFetchWithSkip, getRscParams, emitRouteEvent]);
|
|
358
|
-
// Mirror
|
|
359
|
-
//
|
|
360
|
-
// value is type-compatible.
|
|
380
|
+
// Mirror waku's INTERNAL_ServerRouter context shape; only route and
|
|
381
|
+
// prefetchRoute are used.
|
|
361
382
|
const notAvailable = (name) => () => {
|
|
362
383
|
throw new Error(`${name} is not available in waku-navigation`);
|
|
363
384
|
};
|
|
@@ -370,6 +391,10 @@ function InnerRouter({ fallbackRoute }) {
|
|
|
370
391
|
}), [route, prefetchRoute, routeChangeEvents, fetchingSlices]);
|
|
371
392
|
return (_jsx(RouterContext.Provider, { value: routerCtxValue, children: _jsx(NavStatusRegistryContext.Provider, { value: { register }, children: _jsx(Slot, { id: "root", children: _jsx(Slot, { id: getRouteSlotId(route.path) }) }) }) }));
|
|
372
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* The client router. Reads the initial route from `window.navigation`, listens
|
|
396
|
+
* for navigate events, and renders the current page. Takes no props.
|
|
397
|
+
*/
|
|
373
398
|
export function Router() {
|
|
374
399
|
const initialRoute = parseRoute(new URL(window.navigation.currentEntry.url));
|
|
375
400
|
return (_jsx(Root, { initialRscPath: encodeRoutePath(initialRoute.path), children: _jsx(InnerRouter, { fallbackRoute: initialRoute }) }));
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { Router, Slice, useNavigationStatus_UNSTABLE, useRouter, } from './client.js';
|
|
1
|
+
export { Link, Router, Slice, useNavigationStatus_UNSTABLE, useRouter, } from './client.js';
|
|
2
|
+
export type { LinkProps } from './client.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Router, Slice, useNavigationStatus_UNSTABLE, useRouter, } from './client.js';
|
|
1
|
+
export { Link, Router, Slice, useNavigationStatus_UNSTABLE, useRouter, } from './client.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waku-navigation",
|
|
3
3
|
"description": "Waku Router implementation with Navigation API",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Daishi Kato",
|
|
7
7
|
"repository": {
|
|
@@ -30,30 +30,30 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@eslint/js": "9.39.4",
|
|
33
|
-
"@playwright/test": "^1.
|
|
33
|
+
"@playwright/test": "^1.61.0",
|
|
34
34
|
"@testing-library/jest-dom": "^6.9.1",
|
|
35
35
|
"@testing-library/react": "^16.3.2",
|
|
36
36
|
"@testing-library/user-event": "^14.6.1",
|
|
37
37
|
"@types/dom-navigation": "^1.0.7",
|
|
38
|
-
"@types/node": "^25.
|
|
39
|
-
"@types/react": "^19.2.
|
|
38
|
+
"@types/node": "^25.9.3",
|
|
39
|
+
"@types/react": "^19.2.17",
|
|
40
40
|
"@types/react-dom": "^19.2.3",
|
|
41
41
|
"eslint": "9.39.4",
|
|
42
|
-
"eslint-import-resolver-typescript": "^4.4.
|
|
42
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
43
43
|
"eslint-plugin-import": "^2.32.0",
|
|
44
44
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
45
45
|
"eslint-plugin-react": "^7.37.5",
|
|
46
46
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
47
|
-
"happy-dom": "^20.
|
|
48
|
-
"prettier": "^3.8.
|
|
49
|
-
"react": "^19.2.
|
|
50
|
-
"react-dom": "^19.2.
|
|
51
|
-
"react-server-dom-webpack": "^19.2.
|
|
47
|
+
"happy-dom": "^20.10.4",
|
|
48
|
+
"prettier": "^3.8.4",
|
|
49
|
+
"react": "^19.2.7",
|
|
50
|
+
"react-dom": "^19.2.7",
|
|
51
|
+
"react-server-dom-webpack": "^19.2.7",
|
|
52
52
|
"ts-expect": "^1.3.0",
|
|
53
53
|
"typescript": "^6.0.3",
|
|
54
|
-
"typescript-eslint": "^8.
|
|
55
|
-
"vite": "^8.0.
|
|
56
|
-
"vitest": "^4.1.
|
|
54
|
+
"typescript-eslint": "^8.61.1",
|
|
55
|
+
"vite": "^8.0.16",
|
|
56
|
+
"vitest": "^4.1.9",
|
|
57
57
|
"waku": "1.0.0-beta.3",
|
|
58
58
|
"waku-navigation": "link:"
|
|
59
59
|
},
|