waku-navigation 0.0.2 → 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 +79 -43
- package/dist/client.d.ts +52 -7
- package/dist/client.js +198 -112
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +15 -15
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` — `<
|
|
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
|
|
|
@@ -53,7 +55,7 @@ import { Router } from 'waku-navigation';
|
|
|
53
55
|
<Router />;
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
No props. It reads the initial route from `window.navigation.currentEntry.url
|
|
58
|
+
No props. It reads the initial route from `window.navigation.currentEntry.url` (preferring the route recorded in the RSC payload, so a server-rendered 404 page resolves to `/404`), sets up the navigate-event listener, and renders the page slot. It mirrors the shape Waku's `INTERNAL_ServerRouter` provides during SSR, so server-rendered markup hydrates without a flicker.
|
|
57
59
|
|
|
58
60
|
### `useRouter()`
|
|
59
61
|
|
|
@@ -84,21 +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:
|
|
92
|
+
|
|
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()`.
|
|
88
96
|
|
|
89
97
|
```tsx
|
|
90
|
-
import {
|
|
98
|
+
import { Link } from 'waku-navigation';
|
|
99
|
+
|
|
100
|
+
<Link to="/slow">
|
|
101
|
+
Slow <NavSpinner />
|
|
102
|
+
</Link>;
|
|
103
|
+
```
|
|
104
|
+
|
|
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'>;
|
|
114
|
+
```
|
|
115
|
+
|
|
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.
|
|
117
|
+
|
|
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`.
|
|
119
|
+
|
|
120
|
+
### `useNavigationStatus_UNSTABLE()`
|
|
121
|
+
|
|
122
|
+
Returns the navigation status of the enclosing `<Link>`, like React's `useFormStatus`. No arguments — it reads the `<Link>` by context.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
'use client';
|
|
126
|
+
import { useNavigationStatus_UNSTABLE } from 'waku-navigation';
|
|
91
127
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
128
|
+
function NavSpinner() {
|
|
129
|
+
const { pending } = useNavigationStatus_UNSTABLE();
|
|
130
|
+
return pending ? <span>…</span> : null;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<Link to="/slow">
|
|
136
|
+
Slow <NavSpinner />
|
|
137
|
+
</Link>
|
|
95
138
|
```
|
|
96
139
|
|
|
97
|
-
|
|
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 `{}`.
|
|
98
141
|
|
|
99
|
-
|
|
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.
|
|
100
143
|
|
|
101
|
-
|
|
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.
|
|
102
145
|
|
|
103
146
|
### `<Slice>`
|
|
104
147
|
|
|
@@ -124,46 +167,39 @@ import { Slice } from 'waku-navigation';
|
|
|
124
167
|
|
|
125
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.
|
|
126
169
|
|
|
127
|
-
### `<Link>`
|
|
170
|
+
### `<Link>` (drop-in) or plain `<a>`
|
|
171
|
+
|
|
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):
|
|
128
173
|
|
|
129
174
|
```diff
|
|
130
175
|
- import { Link } from 'waku/router/client';
|
|
176
|
+
+ import { Link } from 'waku-navigation';
|
|
177
|
+
<Link to="/about">About</Link>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Or drop `<Link>` entirely where you don't need type-safety, prefetching, or status — a plain `<a>` navigates client-side on its own:
|
|
181
|
+
|
|
182
|
+
```diff
|
|
131
183
|
- <Link to="/about">About</Link>
|
|
132
184
|
+ <a href="/about">About</a>
|
|
133
185
|
```
|
|
134
186
|
|
|
135
|
-
|
|
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.
|
|
136
188
|
|
|
137
|
-
|
|
138
|
-
| ---------------------------- | -------------------------------------------------------------------------------------- |
|
|
139
|
-
| `to="/x"` | `href="/x"` |
|
|
140
|
-
| `scroll={false}` | Click handler that calls `useRouter().push(href, { scroll: false })` |
|
|
141
|
-
| `unstable_pending={node}` | Wrap the `<a>` in `<Pending fallback={node}>` |
|
|
142
|
-
| `unstable_notPending={node}` | No direct equivalent yet — render conditionally based on `useRouter().unstable_events` |
|
|
143
|
-
| `unstable_prefetchOnEnter` | `onMouseEnter={() => useRouter().prefetch(href)}` in a client component |
|
|
144
|
-
| `unstable_prefetchOnView` | `IntersectionObserver` + `useRouter().prefetch(href)` |
|
|
145
|
-
| `unstable_startTransition` | Not needed — the router uses `useTransition` internally |
|
|
189
|
+
### `<Link>…<Consumer/></Link>` (navigation status)
|
|
146
190
|
|
|
147
|
-
|
|
191
|
+
Unchanged — a descendant reads the enclosing `<Link>`'s status via the no-arg hook, exactly as in `waku/router`:
|
|
148
192
|
|
|
149
|
-
```
|
|
150
|
-
'
|
|
151
|
-
import {
|
|
193
|
+
```diff
|
|
194
|
+
- import { Link, useNavigationStatus_UNSTABLE } from 'waku/router/client';
|
|
195
|
+
+ import { Link, useNavigationStatus_UNSTABLE } from 'waku-navigation';
|
|
152
196
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}) {
|
|
160
|
-
const { prefetch } = useRouter();
|
|
161
|
-
return (
|
|
162
|
-
<a href={to} onMouseEnter={() => prefetch(to)}>
|
|
163
|
-
{children}
|
|
164
|
-
</a>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
197
|
+
function NavSpinner() {
|
|
198
|
+
const { pending } = useNavigationStatus_UNSTABLE();
|
|
199
|
+
return pending ? <span>…</span> : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
<Link to="/slow">Slow <NavSpinner /></Link>
|
|
167
203
|
```
|
|
168
204
|
|
|
169
205
|
### `<Slice>`
|
|
@@ -222,7 +258,6 @@ import {
|
|
|
222
258
|
unstable_removeBase,
|
|
223
259
|
unstable_RouterContext,
|
|
224
260
|
unstable_parseRoute,
|
|
225
|
-
unstable_getHttpStatusFromMeta,
|
|
226
261
|
} from 'waku/router/client';
|
|
227
262
|
```
|
|
228
263
|
|
|
@@ -239,16 +274,17 @@ These are all handled inside the navigate-event listener so apps usually don't n
|
|
|
239
274
|
- **Form submission guard** — `<form method="POST">` submissions (`event.formData != null`) are passed through to the server.
|
|
240
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.
|
|
241
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.
|
|
242
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.
|
|
243
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).
|
|
244
|
-
- **`X-Waku-Router-Skip` header** — every refetch
|
|
245
|
-
- **HMR cache invalidation** — when Waku's dev runtime fires `globalThis.__WAKU_RSC_RELOAD_LISTENERS__` (Vite HMR update), the router clears `staticPathSet` and `
|
|
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.
|
|
281
|
+
- **HMR cache invalidation** — when Waku's dev runtime fires `globalThis.__WAKU_RSC_RELOAD_LISTENERS__` (Vite HMR update), the router clears `staticPathSet` and `cachedEtags` and refetches the current route. Guarded by `import.meta.hot` so it's stripped in production.
|
|
246
282
|
|
|
247
283
|
---
|
|
248
284
|
|
|
249
285
|
## Caveats / not yet implemented
|
|
250
286
|
|
|
251
|
-
- `<Link>` is not
|
|
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()`.
|
|
252
288
|
- `unstable_routeInterceptor` (server-side route rewrite hook) is not supported.
|
|
253
289
|
- `unstable_fetchRscStore` (custom RSC store) is not exposed on `<Router>`.
|
|
254
290
|
- Requires a browser with the Navigation API. There is currently no fallback for older browsers.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import { Slice } from 'waku/router/client';
|
|
1
|
+
import { type AnchorHTMLAttributes, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { Slice, type Unstable_InferredPaths as InferredPaths } from 'waku/router/client';
|
|
3
3
|
export { Slice };
|
|
4
4
|
type Route = {
|
|
5
5
|
path: string;
|
|
6
6
|
query: string;
|
|
7
7
|
hash: string;
|
|
8
8
|
};
|
|
9
|
+
type NavigationStatus = {
|
|
10
|
+
pending?: boolean;
|
|
11
|
+
};
|
|
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;
|
|
9
49
|
type PushReplaceOptions = {
|
|
10
50
|
scroll?: boolean;
|
|
11
51
|
};
|
|
@@ -13,6 +53,11 @@ type RouteChangeEvents = {
|
|
|
13
53
|
on: (name: 'start' | 'complete', handler: (route: Route) => void) => void;
|
|
14
54
|
off: (name: 'start' | 'complete', handler: (route: Route) => void) => void;
|
|
15
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
|
+
*/
|
|
16
61
|
export declare function useRouter(): {
|
|
17
62
|
path: string;
|
|
18
63
|
query: string;
|
|
@@ -25,8 +70,8 @@ export declare function useRouter(): {
|
|
|
25
70
|
prefetch: (to: string) => void;
|
|
26
71
|
unstable_events: RouteChangeEvents;
|
|
27
72
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export declare function Router(): import("react
|
|
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
|
@@ -1,21 +1,98 @@
|
|
|
1
1
|
/// <reference types="dom-navigation" />
|
|
2
2
|
'use client';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
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,
|
|
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
|
-
|
|
10
|
+
// Mirrors waku's unexported ETAG_ID_PREFIX (router/common.js).
|
|
11
|
+
const ETAG_ID_PREFIX = 'ETAG:';
|
|
14
12
|
const noopRegister = () => () => { };
|
|
15
|
-
const
|
|
13
|
+
const NavStatusRegistryContext = createContext({
|
|
16
14
|
register: noopRegister,
|
|
17
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
|
+
}
|
|
18
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
|
+
*/
|
|
19
96
|
export function useRouter() {
|
|
20
97
|
var _a, _b;
|
|
21
98
|
const ctx = useContext(RouterContext);
|
|
@@ -45,71 +122,73 @@ export function useRouter() {
|
|
|
45
122
|
unstable_events: ((_b = ctx === null || ctx === void 0 ? void 0 : ctx.routeChangeEvents) !== null && _b !== void 0 ? _b : noopEvents),
|
|
46
123
|
};
|
|
47
124
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.map((child) => {
|
|
65
|
-
if (isValidElement(child) && child.type === 'a') {
|
|
66
|
-
const { href: h } = child.props;
|
|
67
|
-
return typeof h === 'string' ? h : undefined;
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
})
|
|
71
|
-
.find((h) => h !== undefined);
|
|
72
|
-
useLayoutEffect(() => register(id, { href, startTransition }), [id, href, register, startTransition]);
|
|
73
|
-
return (_jsxs(_Fragment, { children: [stamped, isPending ? fallback : null] }));
|
|
74
|
-
}
|
|
75
|
-
function InnerRouter({ initialRoute, httpStatus, }) {
|
|
125
|
+
// Same origin + path + query (not pathname; fragment ignored). Malformed input
|
|
126
|
+
// returns false rather than throwing.
|
|
127
|
+
const routeMatchesHref = (href, route) => {
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
url = new URL(href, window.location.href);
|
|
131
|
+
}
|
|
132
|
+
catch (_a) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (url.origin !== window.location.origin)
|
|
136
|
+
return false;
|
|
137
|
+
const parsed = parseRoute(url);
|
|
138
|
+
return parsed.path === route.path && parsed.query === route.query;
|
|
139
|
+
};
|
|
140
|
+
function InnerRouter({ fallbackRoute }) {
|
|
76
141
|
const refetch = useRefetch();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
142
|
+
const elementsPromise = useElementsPromise();
|
|
143
|
+
const [routeState, setRoute] = useState();
|
|
144
|
+
let route = routeState;
|
|
145
|
+
if (route === undefined) {
|
|
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.
|
|
150
|
+
const elements = use(elementsPromise);
|
|
151
|
+
const routeData = elements[ROUTE_ID];
|
|
152
|
+
route =
|
|
153
|
+
routeData && routeData[0] !== fallbackRoute.path
|
|
154
|
+
? { path: routeData[0], query: routeData[1], hash: '' }
|
|
155
|
+
: { ...fallbackRoute, hash: '' };
|
|
156
|
+
setRoute(route);
|
|
157
|
+
}
|
|
158
|
+
// Rethrow during render so the user's <ErrorBoundary> catches non-404
|
|
159
|
+
// failures; cleared by the next successful navigation.
|
|
87
160
|
const [renderError, setRenderError] = useState(null);
|
|
88
161
|
if (renderError)
|
|
89
162
|
throw renderError;
|
|
90
163
|
useEffect(() => {
|
|
91
|
-
|
|
164
|
+
// SSR sends no fragment, so the hash starts ''; upgrade it post-hydration.
|
|
165
|
+
if (fallbackRoute.hash) {
|
|
92
166
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
93
|
-
setRoute((r) => ({ ...r, hash:
|
|
167
|
+
setRoute((r) => ({ ...r, hash: fallbackRoute.hash }));
|
|
94
168
|
}
|
|
95
|
-
// Only on mount.
|
|
96
169
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
97
170
|
}, []);
|
|
98
171
|
const registryRef = useRef(new Map());
|
|
99
172
|
const staticPathSetRef = useRef(new Set());
|
|
100
|
-
const
|
|
101
|
-
// Stable
|
|
102
|
-
// fetch end) without losing state across re-renders. useMemo with [] keeps
|
|
103
|
-
// the same instance and avoids reading ref.current during render.
|
|
173
|
+
const cachedEtagsRef = useRef({});
|
|
174
|
+
// Stable instance: <Slice> mutates this Set across renders.
|
|
104
175
|
const fetchingSlices = useMemo(() => new Set(), []);
|
|
105
|
-
const elementsPromise = useElementsPromise();
|
|
106
176
|
useEffect(() => {
|
|
107
177
|
elementsPromise.then((elements) => {
|
|
108
178
|
const routeData = elements[ROUTE_ID];
|
|
109
179
|
if (routeData && elements[IS_STATIC_ID]) {
|
|
110
180
|
staticPathSetRef.current.add(routeData[0]);
|
|
111
181
|
}
|
|
112
|
-
|
|
182
|
+
const etags = {};
|
|
183
|
+
for (const [key, value] of Object.entries(elements)) {
|
|
184
|
+
// Skip empty (clear signal) and non-Latin1 (breaks the fetch header).
|
|
185
|
+
if (key.startsWith(ETAG_ID_PREFIX) &&
|
|
186
|
+
typeof value === 'string' &&
|
|
187
|
+
/^[ -ÿ]+$/.test(value)) {
|
|
188
|
+
etags[key.slice(ETAG_ID_PREFIX.length)] = value;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
cachedEtagsRef.current = etags;
|
|
113
192
|
}, () => { });
|
|
114
193
|
}, [elementsPromise]);
|
|
115
194
|
const register = useCallback((id, entry) => {
|
|
@@ -118,16 +197,15 @@ function InnerRouter({ initialRoute, httpStatus, }) {
|
|
|
118
197
|
registryRef.current.delete(id);
|
|
119
198
|
};
|
|
120
199
|
}, []);
|
|
121
|
-
//
|
|
122
|
-
//
|
|
200
|
+
// Send our cached etags via X-Waku-Router-Skip so the server can skip
|
|
201
|
+
// re-rendering slots whose etag still matches.
|
|
123
202
|
const enhanceFetchWithSkip = useMemo(() => withEnhanceFetchFn((fetchFn) => (input, init) => {
|
|
124
203
|
const headers = new Headers(init === null || init === void 0 ? void 0 : init.headers);
|
|
125
|
-
headers.set(SKIP_HEADER, JSON.stringify(
|
|
204
|
+
headers.set(SKIP_HEADER, JSON.stringify(cachedEtagsRef.current));
|
|
126
205
|
return fetchFn(input, { ...init, headers });
|
|
127
206
|
}), []);
|
|
128
|
-
// Waku's prefetch cache keys
|
|
129
|
-
//
|
|
130
|
-
// 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.
|
|
131
209
|
const rscParamsByQueryRef = useRef(new Map());
|
|
132
210
|
const getRscParams = useCallback((query) => {
|
|
133
211
|
let params = rscParamsByQueryRef.current.get(query);
|
|
@@ -153,28 +231,24 @@ function InnerRouter({ initialRoute, httpStatus, }) {
|
|
|
153
231
|
routeChangeListeners[name].delete(handler);
|
|
154
232
|
},
|
|
155
233
|
}), [routeChangeListeners]);
|
|
156
|
-
// Eagerly fetch the RSC for a route (used by useRouter().prefetch). Build
|
|
157
|
-
// output may also publish a __WAKU_ROUTER_PREFETCH__ helper that returns the
|
|
158
|
-
// JS chunk ids for a path; if present, we preload them too.
|
|
159
234
|
const prefetchRoute = useCallback((next) => {
|
|
160
235
|
var _a, _b;
|
|
161
236
|
if (staticPathSetRef.current.has(next.path))
|
|
162
237
|
return;
|
|
163
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.
|
|
164
241
|
(_b = (_a = globalThis).__WAKU_ROUTER_PREFETCH__) === null || _b === void 0 ? void 0 : _b.call(_a, next.path, (id) => preloadModule(id, { as: 'script' }));
|
|
165
242
|
}, [enhanceFetchWithSkip, getRscParams]);
|
|
166
|
-
// Vite HMR:
|
|
167
|
-
//
|
|
168
|
-
// our path/id caches (so a "static" route picks up the new content) and
|
|
169
|
-
// refetches the current route. In production import.meta.hot is undefined
|
|
170
|
-
// 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__.
|
|
171
245
|
useEffect(() => {
|
|
172
246
|
var _a;
|
|
173
247
|
if (!import.meta.hot)
|
|
174
248
|
return;
|
|
175
249
|
const refetchRoute = () => {
|
|
176
250
|
staticPathSetRef.current.clear();
|
|
177
|
-
|
|
251
|
+
cachedEtagsRef.current = {};
|
|
178
252
|
refetch(encodeRoutePath(route.path), getRscParams(route.query));
|
|
179
253
|
};
|
|
180
254
|
const listeners = ((_a = globalThis).__WAKU_RSC_RELOAD_LISTENERS__ || (_a.__WAKU_RSC_RELOAD_LISTENERS__ = []));
|
|
@@ -192,20 +266,29 @@ function InnerRouter({ initialRoute, httpStatus, }) {
|
|
|
192
266
|
return;
|
|
193
267
|
if (event.downloadRequest !== null || event.formData)
|
|
194
268
|
return;
|
|
269
|
+
// React >=19.2's default transition indicator fires fake navigations.
|
|
270
|
+
if (event.info === 'react-transition')
|
|
271
|
+
return;
|
|
195
272
|
const nextRoute = parseRoute(new URL(event.destination.url));
|
|
196
|
-
// useRouter().push/replace forward { scroll } via `info`. The Navigation
|
|
197
|
-
// API itself doesn't persist `info` in history, so it only applies to
|
|
198
|
-
// this single navigation -- exactly what we want.
|
|
199
273
|
const info = event.info;
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
//
|
|
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;
|
|
205
290
|
if (event.hashChange) {
|
|
206
|
-
// Hash-only
|
|
207
|
-
// both fire effectively together; emit both so subscribers don't
|
|
208
|
-
// have to special-case them.
|
|
291
|
+
// Hash-only: no refetch; intercept only to suppress the browser scroll.
|
|
209
292
|
emitRouteEvent('start', nextRoute);
|
|
210
293
|
if (suppressScroll) {
|
|
211
294
|
event.intercept({
|
|
@@ -224,31 +307,25 @@ function InnerRouter({ initialRoute, httpStatus, }) {
|
|
|
224
307
|
}
|
|
225
308
|
emitRouteEvent('start', nextRoute);
|
|
226
309
|
const signal = event.signal;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (!registered) {
|
|
236
|
-
for (const entry of registryRef.current.values()) {
|
|
237
|
-
if (entry.href !== undefined &&
|
|
238
|
-
new URL(entry.href, window.location.href).pathname ===
|
|
239
|
-
nextRoute.path) {
|
|
240
|
-
registered = entry;
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const startTransition = (_d = registered === null || registered === void 0 ? void 0 : registered.startTransition) !== null && _d !== void 0 ? _d : ((fn) => fn());
|
|
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);
|
|
246
318
|
event.intercept({
|
|
247
319
|
...(suppressScroll ? { scroll: 'manual' } : {}),
|
|
248
320
|
handler: () => new Promise((resolve, reject) => {
|
|
321
|
+
// Run in a transition: keeps the previous page visible while the
|
|
322
|
+
// next tree suspends, and scopes the optimistic pending updates so
|
|
323
|
+
// React reverts them on commit/abort/error.
|
|
249
324
|
startTransition(async () => {
|
|
250
325
|
var _a;
|
|
251
326
|
try {
|
|
327
|
+
for (const set of pendingSetters)
|
|
328
|
+
set({ pending: true });
|
|
252
329
|
let targetRoute = nextRoute;
|
|
253
330
|
try {
|
|
254
331
|
if (!staticPathSetRef.current.has(nextRoute.path)) {
|
|
@@ -273,8 +350,18 @@ function InnerRouter({ initialRoute, httpStatus, }) {
|
|
|
273
350
|
throw err;
|
|
274
351
|
}
|
|
275
352
|
}
|
|
276
|
-
|
|
277
|
-
|
|
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 = () => {
|
|
358
|
+
setRenderError(null);
|
|
359
|
+
setRoute(targetRoute);
|
|
360
|
+
};
|
|
361
|
+
if (customTransition)
|
|
362
|
+
customTransition(commitRoute);
|
|
363
|
+
else
|
|
364
|
+
startTransition(commitRoute);
|
|
278
365
|
emitRouteEvent('complete', targetRoute);
|
|
279
366
|
resolve();
|
|
280
367
|
}
|
|
@@ -290,9 +377,8 @@ function InnerRouter({ initialRoute, httpStatus, }) {
|
|
|
290
377
|
window.navigation.removeEventListener('navigate', callback);
|
|
291
378
|
};
|
|
292
379
|
}, [refetch, enhanceFetchWithSkip, getRscParams, emitRouteEvent]);
|
|
293
|
-
// Mirror
|
|
294
|
-
//
|
|
295
|
-
// value is type-compatible.
|
|
380
|
+
// Mirror waku's INTERNAL_ServerRouter context shape; only route and
|
|
381
|
+
// prefetchRoute are used.
|
|
296
382
|
const notAvailable = (name) => () => {
|
|
297
383
|
throw new Error(`${name} is not available in waku-navigation`);
|
|
298
384
|
};
|
|
@@ -303,13 +389,13 @@ function InnerRouter({ initialRoute, httpStatus, }) {
|
|
|
303
389
|
routeChangeEvents,
|
|
304
390
|
fetchingSlices,
|
|
305
391
|
}), [route, prefetchRoute, routeChangeEvents, fetchingSlices]);
|
|
306
|
-
return (_jsx(RouterContext.Provider, { value: routerCtxValue, children: _jsx(
|
|
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) }) }) }) }));
|
|
307
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
|
+
*/
|
|
308
398
|
export function Router() {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
const initialRoute = httpStatus === '404'
|
|
312
|
-
? { path: NOT_FOUND_PATH, query: '', hash: '' }
|
|
313
|
-
: parsed;
|
|
314
|
-
return (_jsx(Root, { initialRscPath: encodeRoutePath(initialRoute.path), children: _jsx(InnerRouter, { initialRoute: initialRoute, httpStatus: httpStatus }) }));
|
|
399
|
+
const initialRoute = parseRoute(new URL(window.navigation.currentEntry.url));
|
|
400
|
+
return (_jsx(Root, { initialRscPath: encodeRoutePath(initialRoute.path), children: _jsx(InnerRouter, { fallbackRoute: initialRoute }) }));
|
|
315
401
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
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 {
|
|
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,36 +30,36 @@
|
|
|
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.
|
|
57
|
-
"waku": "1.0.0-beta.
|
|
54
|
+
"typescript-eslint": "^8.61.1",
|
|
55
|
+
"vite": "^8.0.16",
|
|
56
|
+
"vitest": "^4.1.9",
|
|
57
|
+
"waku": "1.0.0-beta.3",
|
|
58
58
|
"waku-navigation": "link:"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"react": ">=19.0.0",
|
|
62
|
-
"waku": ">=1.0.0-
|
|
62
|
+
"waku": ">=1.0.0-beta.3"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"compile": "rm -rf dist && tsc -p .",
|