@typeroute/router 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -44
- package/dist/index.d.ts +23 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,25 +9,31 @@
|
|
|
9
9
|
<div align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@typeroute/router">
|
|
11
11
|
<img
|
|
12
|
-
src="https://img.shields.io/npm/v/%40typeroute%2Frouter?
|
|
12
|
+
src="https://img.shields.io/npm/v/%40typeroute%2Frouter?color=0d1117&labelColor=0d1117"
|
|
13
13
|
alt="npm version"
|
|
14
14
|
/>
|
|
15
15
|
</a>
|
|
16
16
|
<a href="https://www.npmjs.com/package/@typeroute/router">
|
|
17
17
|
<img
|
|
18
|
-
src="https://img.
|
|
18
|
+
src="https://img.shields.io/npm/dw/%40typeroute%2Frouter?color=0d1117&labelColor=0d1117"
|
|
19
|
+
alt="weekly downloads"
|
|
20
|
+
/>
|
|
21
|
+
</a>
|
|
22
|
+
<a href="https://www.npmjs.com/package/@typeroute/router">
|
|
23
|
+
<img
|
|
24
|
+
src="https://img.badgesize.io/https://cdn.jsdelivr.net/npm/@typeroute/router/dist/index.js?compression=gzip&label=gzip&color=0d1117&labelColor=0d1117"
|
|
19
25
|
alt="gzip size"
|
|
20
26
|
/>
|
|
21
27
|
</a>
|
|
22
28
|
<a href="https://github.com/strblr/typeroute/blob/master/LICENSE">
|
|
23
29
|
<img
|
|
24
|
-
src="https://img.shields.io/npm/l/%40typeroute%2Frouter?
|
|
30
|
+
src="https://img.shields.io/npm/l/%40typeroute%2Frouter?color=0d1117&labelColor=0d1117"
|
|
25
31
|
alt="license"
|
|
26
32
|
/>
|
|
27
33
|
</a>
|
|
28
34
|
<a href="https://github.com/sponsors/strblr">
|
|
29
35
|
<img
|
|
30
|
-
src="https://img.shields.io/github/sponsors/strblr?
|
|
36
|
+
src="https://img.shields.io/github/sponsors/strblr?color=0d1117&labelColor=0d1117"
|
|
31
37
|
alt="sponsors"
|
|
32
38
|
/>
|
|
33
39
|
</a>
|
|
@@ -100,8 +106,8 @@ If you believe there's a mistake in the comparison table, please [open an issue]
|
|
|
100
106
|
# Table of contents
|
|
101
107
|
|
|
102
108
|
- [Comparison](#comparison)
|
|
103
|
-
- [Showcase](#showcase)
|
|
104
109
|
- [Installation](#installation)
|
|
110
|
+
- [Showcase](#showcase)
|
|
105
111
|
- [Defining routes](#defining-routes)
|
|
106
112
|
- [Nested routes and layouts](#nested-routes-and-layouts)
|
|
107
113
|
- [Setting up the router](#setting-up-the-router)
|
|
@@ -138,7 +144,7 @@ If you believe there's a mistake in the comparison table, please [open an issue]
|
|
|
138
144
|
- [Dynamic page titles](#dynamic-page-titles)
|
|
139
145
|
- [API reference](#api-reference)
|
|
140
146
|
- [Router class](#router-class)
|
|
141
|
-
- [Route
|
|
147
|
+
- [Route builder](#route-builder)
|
|
142
148
|
- [Middleware](#middleware)
|
|
143
149
|
- [Hooks](#hooks)
|
|
144
150
|
- [Components](#components)
|
|
@@ -149,6 +155,16 @@ If you believe there's a mistake in the comparison table, please [open an issue]
|
|
|
149
155
|
|
|
150
156
|
---
|
|
151
157
|
|
|
158
|
+
# Installation
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm install @typeroute/router
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
TypeRoute requires React 18 or higher.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
152
168
|
# Showcase
|
|
153
169
|
|
|
154
170
|
Here's what routing looks like with TypeRoute:
|
|
@@ -156,12 +172,12 @@ Here's what routing looks like with TypeRoute:
|
|
|
156
172
|
```tsx
|
|
157
173
|
import { route, RouterRoot, Outlet, Link, useParams } from "@typeroute/router";
|
|
158
174
|
|
|
159
|
-
//
|
|
175
|
+
// Routes
|
|
160
176
|
const layout = route("/").component(() => (
|
|
161
177
|
<div>
|
|
162
178
|
<nav>
|
|
163
179
|
<Link to="/">Home</Link>
|
|
164
|
-
<Link to=
|
|
180
|
+
<Link to={user} params={{ id: "42" }}>
|
|
165
181
|
User
|
|
166
182
|
</Link>
|
|
167
183
|
</nav>
|
|
@@ -169,10 +185,9 @@ const layout = route("/").component(() => (
|
|
|
169
185
|
</div>
|
|
170
186
|
));
|
|
171
187
|
|
|
172
|
-
// Pages
|
|
173
188
|
const home = layout.route("/").component(() => <h1>Home</h1>);
|
|
174
189
|
|
|
175
|
-
const user = layout.route("/users/:id").component(
|
|
190
|
+
const user = layout.route("/users/:id").component(() => {
|
|
176
191
|
const { id } = useParams(user); // Fully typed
|
|
177
192
|
return <h1>User {id}</h1>;
|
|
178
193
|
});
|
|
@@ -197,16 +212,6 @@ Everything autocompletes and type-checks automatically. No heavy setup, no magic
|
|
|
197
212
|
|
|
198
213
|
---
|
|
199
214
|
|
|
200
|
-
# Installation
|
|
201
|
-
|
|
202
|
-
```bash
|
|
203
|
-
npm install @typeroute/router
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
TypeRoute requires React 18 or higher.
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
215
|
# Defining routes
|
|
211
216
|
|
|
212
217
|
Routes are created using the `route()` function, following the [builder pattern](https://dev.to/superviz/design-pattern-7-builder-pattern-10j4). You pass it a path and chain methods to configure the route.
|
|
@@ -298,7 +303,7 @@ Beyond paths and components, child routes also inherit search param validators,
|
|
|
298
303
|
|
|
299
304
|
# Setting up the router
|
|
300
305
|
|
|
301
|
-
Before setting up the router, you need to collect your navigable routes into
|
|
306
|
+
Before setting up the router, you need to collect your navigable routes into a collection (either array or record). When building nested route hierarchies, you'll often create intermediate parent routes solely for grouping and shared layouts. These intermediate routes shouldn't be included in your routes collection - only the final, navigable routes should be:
|
|
302
307
|
|
|
303
308
|
```tsx
|
|
304
309
|
// Intermediate route used for hierarchy
|
|
@@ -310,13 +315,16 @@ const about = layout.route("/about").component(About);
|
|
|
310
315
|
|
|
311
316
|
// Collect only the navigable routes
|
|
312
317
|
const routes = [home, about]; // ✅ Don't include `layout`
|
|
318
|
+
|
|
319
|
+
// Or equivalently:
|
|
320
|
+
const routes = { home, about };
|
|
313
321
|
```
|
|
314
322
|
|
|
315
|
-
This makes sure that only actual pages can be matched and appear in autocomplete. The intermediate routes still exist as part of the hierarchy, they just aren't directly navigable. Note that the order of routes in the
|
|
323
|
+
This makes sure that only actual pages can be matched and appear in autocomplete. The intermediate routes still exist as part of the hierarchy, they just aren't directly navigable. Note that the order of routes in the collection doesn't matter - TypeRoute uses a [ranking algorithm](#route-matching-and-ranking) to pick the most specific match.
|
|
316
324
|
|
|
317
325
|
The `RouterRoot` component is the entry point to TypeRoute. It listens to URL changes, matches the current path against your routes, and renders the matching route's component hierarchy.
|
|
318
326
|
|
|
319
|
-
There are two ways to set it up. The simplest is passing your routes
|
|
327
|
+
There are two ways to set it up. The simplest is passing your routes collection directly to `RouterRoot`. This creates a router instance internally (accessible via `useRouter`):
|
|
320
328
|
|
|
321
329
|
```tsx
|
|
322
330
|
import { RouterRoot } from "@typeroute/router";
|
|
@@ -394,15 +402,20 @@ function About() {
|
|
|
394
402
|
}
|
|
395
403
|
```
|
|
396
404
|
|
|
397
|
-
Then
|
|
405
|
+
Then add a file that re-exports all your routes:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
// pages/routes.ts
|
|
409
|
+
export * from "./home";
|
|
410
|
+
export * from "./about";
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Now in your root app component file, import all the routes as a namespace, register them with module augmentation, and render `RouterRoot`:
|
|
398
414
|
|
|
399
415
|
```tsx
|
|
400
416
|
// app.tsx
|
|
401
417
|
import { RouterRoot } from "@typeroute/router";
|
|
402
|
-
import
|
|
403
|
-
import { about } from "./pages/about";
|
|
404
|
-
|
|
405
|
-
const routes = [home, about];
|
|
418
|
+
import * as routes from "./pages/routes";
|
|
406
419
|
|
|
407
420
|
export function App() {
|
|
408
421
|
return <RouterRoot routes={routes} />;
|
|
@@ -415,6 +428,8 @@ declare module "@typeroute/router" {
|
|
|
415
428
|
}
|
|
416
429
|
```
|
|
417
430
|
|
|
431
|
+
Because `routes` is a namespace object whose values are your route definitions, TypeRoute picks them up automatically - no need to manually maintain a routes collection. When you add a new page, just create the file and re-export it from `routes.ts`.
|
|
432
|
+
|
|
418
433
|
But again, this is just one approach. You could keep all routes in a single file, split them by feature, organize them by route depth, whatever fits your project. TypeRoute doesn't care where the routes come from or how you structure your files.
|
|
419
434
|
|
|
420
435
|
---
|
|
@@ -849,12 +864,12 @@ See [Route preloading](#route-preloading) for ways to load these components befo
|
|
|
849
864
|
|
|
850
865
|
# Data preloading
|
|
851
866
|
|
|
852
|
-
Use `.preload()` to run logic before navigation occurs, typically to prefetch data. Preload functions receive the target route's typed params and
|
|
867
|
+
Use `.preload()` to run logic before navigation occurs, typically to prefetch data. Preload functions receive the target route's typed params, search values, and router context:
|
|
853
868
|
|
|
854
869
|
```tsx
|
|
855
870
|
const userProfile = route("/users/:id")
|
|
856
871
|
.search(z.object({ tab: z.enum(["posts", "comments"]).catch("posts") }))
|
|
857
|
-
.preload(async ({ params, search }) => {
|
|
872
|
+
.preload(async ({ params, search, context }) => {
|
|
858
873
|
await queryClient.prefetchQuery({
|
|
859
874
|
queryKey: ["user", params.id, search.tab],
|
|
860
875
|
queryFn: () => fetchUser(params.id, search.tab)
|
|
@@ -886,6 +901,19 @@ const settings = dashboard.route("/settings").component(Settings);
|
|
|
886
901
|
// Preloading /dashboard/settings runs prefetchDashboardData
|
|
887
902
|
```
|
|
888
903
|
|
|
904
|
+
The `context` parameter provides access to any arbitrary data you passed to the router. This is useful for sharing instances like query clients across your preload functions. To use it, pass the context when creating your router and register its type:
|
|
905
|
+
|
|
906
|
+
```tsx
|
|
907
|
+
<RouterRoot routes={routes} context={{ queryClient }} />;
|
|
908
|
+
|
|
909
|
+
declare module "@typeroute/router" {
|
|
910
|
+
interface Register {
|
|
911
|
+
routes: typeof routes;
|
|
912
|
+
context: { queryClient: QueryClient };
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
889
917
|
---
|
|
890
918
|
|
|
891
919
|
# Error boundaries
|
|
@@ -1113,7 +1141,7 @@ For the path `/users/42`:
|
|
|
1113
1141
|
/users/* → [static, wildcard] → weights [2, 0]
|
|
1114
1142
|
```
|
|
1115
1143
|
|
|
1116
|
-
This ranking algorithm means you don't need to order your routes
|
|
1144
|
+
This ranking algorithm means you don't need to order your routes carefully. Define them in any order and TypeRoute figures out the right match regardless:
|
|
1117
1145
|
|
|
1118
1146
|
```tsx
|
|
1119
1147
|
const routes = [
|
|
@@ -1272,9 +1300,7 @@ function handleRequest(req: Request) {
|
|
|
1272
1300
|
if (ssrContext.redirect) {
|
|
1273
1301
|
return Response.redirect(ssrContext.redirect);
|
|
1274
1302
|
}
|
|
1275
|
-
|
|
1276
|
-
headers: { "Content-Type": "text/html" }
|
|
1277
|
-
});
|
|
1303
|
+
// ... Send pre-rendered HTML
|
|
1278
1304
|
}
|
|
1279
1305
|
```
|
|
1280
1306
|
|
|
@@ -1521,8 +1547,9 @@ The `Router` class is the core of TypeRoute. You can create an instance directly
|
|
|
1521
1547
|
**Properties:**
|
|
1522
1548
|
|
|
1523
1549
|
- `router.basePath` - The configured base path
|
|
1524
|
-
- `router.routes` - The array of routes
|
|
1550
|
+
- `router.routes` - The array of navigable routes
|
|
1525
1551
|
- `router.history` - The history instance
|
|
1552
|
+
- `router.context` - The router context
|
|
1526
1553
|
- `router.ssrContext` - The SSR context (if provided)
|
|
1527
1554
|
- `router.defaultLinkOptions` - Default link options
|
|
1528
1555
|
|
|
@@ -1535,6 +1562,7 @@ The `Router` class is the core of TypeRoute. You can create an instance directly
|
|
|
1535
1562
|
const router = new Router({ routes });
|
|
1536
1563
|
const router = new Router({ routes, basePath: "/app" });
|
|
1537
1564
|
const router = new Router({ routes, history: new HashHistory() });
|
|
1565
|
+
const router = new Router({ routes, context: { queryClient } });
|
|
1538
1566
|
```
|
|
1539
1567
|
|
|
1540
1568
|
**`router.navigate(options)`** navigates to a new location.
|
|
@@ -1604,7 +1632,7 @@ await router.preload({ to: "/user/:id", params: { id: "42" } });
|
|
|
1604
1632
|
await router.preload({ to: searchPage, search: { q: "test" } });
|
|
1605
1633
|
```
|
|
1606
1634
|
|
|
1607
|
-
## Route
|
|
1635
|
+
## Route builder
|
|
1608
1636
|
|
|
1609
1637
|
Routes are created with the `route()` function and configured by chaining methods.
|
|
1610
1638
|
|
|
@@ -1703,13 +1731,13 @@ const risky = route("/risky").error(ErrorPage).component(RiskyPage);
|
|
|
1703
1731
|
|
|
1704
1732
|
**`.preload(preload)`** registers a preload function for the route.
|
|
1705
1733
|
|
|
1706
|
-
- `preload` - `(
|
|
1734
|
+
- `preload` - `(options: PreloadOptions) => Promise<any>` - An async function receiving typed `params`, `search`, and `context`
|
|
1707
1735
|
- Returns: `Route` - A new route object
|
|
1708
1736
|
|
|
1709
1737
|
```tsx
|
|
1710
1738
|
const user = route("/users/:id")
|
|
1711
1739
|
.search(z.object({ tab: z.string().catch("profile") }))
|
|
1712
|
-
.preload(async ({ params, search }) => {
|
|
1740
|
+
.preload(async ({ params, search, context }) => {
|
|
1713
1741
|
// params.id: string, search.tab: string - fully typed
|
|
1714
1742
|
await prefetchUser(params.id, search.tab);
|
|
1715
1743
|
});
|
|
@@ -1916,13 +1944,14 @@ const unsubscribe = history.subscribe(() => {
|
|
|
1916
1944
|
**`RouterOptions`** are options for creating a `Router` instance or passing to `RouterRoot`.
|
|
1917
1945
|
|
|
1918
1946
|
```tsx
|
|
1919
|
-
|
|
1920
|
-
routes: Route[]
|
|
1947
|
+
type RouterOptions = {
|
|
1948
|
+
routes: Route[] | Record<string, Route>; // Collection of navigable routes
|
|
1921
1949
|
basePath?: string; // Base path prefix (default: "/")
|
|
1922
1950
|
history?: HistoryLike; // History implementation (default: BrowserHistory)
|
|
1951
|
+
context?: Context; // Arbitrary router context
|
|
1923
1952
|
ssrContext?: SSRContext; // Context for server-side rendering
|
|
1924
1953
|
defaultLinkOptions?: LinkOptions; // Default options for all Link components
|
|
1925
|
-
}
|
|
1954
|
+
};
|
|
1926
1955
|
```
|
|
1927
1956
|
|
|
1928
1957
|
**`NavigateOptions`** are options for type-safe navigation.
|
|
@@ -1999,12 +2028,13 @@ type SSRContext = {
|
|
|
1999
2028
|
};
|
|
2000
2029
|
```
|
|
2001
2030
|
|
|
2002
|
-
**`
|
|
2031
|
+
**`PreloadOptions`** is the options object passed to preload functions.
|
|
2003
2032
|
|
|
2004
2033
|
```tsx
|
|
2005
|
-
interface
|
|
2034
|
+
interface PreloadOptions {
|
|
2006
2035
|
params: Params; // Path params for the route
|
|
2007
2036
|
search: Search; // Validated search params
|
|
2037
|
+
context: Context; // Router context
|
|
2008
2038
|
}
|
|
2009
2039
|
```
|
|
2010
2040
|
|
|
@@ -2012,8 +2042,10 @@ interface PreloadContext {
|
|
|
2012
2042
|
|
|
2013
2043
|
# Roadmap
|
|
2014
2044
|
|
|
2015
|
-
- Possibility to pass an arbitrary context to the Router instance for later use in preloads?
|
|
2016
2045
|
- Relative path navigation? Not sure it's worth the extra bundle size given that users can export/import route objects and pass them as navigation option.
|
|
2046
|
+
- Refactor: APIs like useParams, useSearch and useMatch should accept any route object and not just rely on the global routes collection.
|
|
2047
|
+
- Refactor: allow `route()` and `.route()` to be called without passing an argument (defaulting to "/")?
|
|
2048
|
+
- A builder method `.index(component)` to simplify patterns like `useOutlet() ?? <div>Index page</div>`, rendering a component only when no child route matched. In practice, this can spare the definition of a child route for `"/"`.
|
|
2017
2049
|
- Document usage in test environments
|
|
2018
2050
|
- Navigation blockers (`useBlocker`, etc.)
|
|
2019
2051
|
- Open to suggestions, we can discuss them [here](https://github.com/strblr/typeroute/discussions).
|
package/dist/index.d.ts
CHANGED
|
@@ -9,22 +9,26 @@ type ParsePattern<P extends string> = Simplify<RouteParams<P>>;
|
|
|
9
9
|
type NormalizePath<P extends string> = RemoveTrailingSlash<DedupSlashes<`/${P}`>>;
|
|
10
10
|
type DedupSlashes<P extends string> = P extends `${infer Prefix}//${infer Rest}` ? `${Prefix}${DedupSlashes<`/${Rest}`>}` : P;
|
|
11
11
|
type RemoveTrailingSlash<P extends string> = P extends `${infer Prefix}/` ? Prefix extends "" ? "/" : Prefix : P;
|
|
12
|
-
type
|
|
12
|
+
type MaybeUndefinedKey<K extends string, T> = undefined extends T ? { [P in K]?: T } : { [P in K]: T };
|
|
13
|
+
type MaybeObjectKey<K extends string, T> = T extends EmptyObject ? { [P in K]?: EmptyObject } : {} extends T ? { [P in K]?: T } : { [P in K]: T };
|
|
13
14
|
type OptionalOnUndefined<T extends object> = Simplify<{ [K in keyof T as undefined extends T[K] ? never : K]: T[K] } & { [K in keyof T as undefined extends T[K] ? K : never]?: T[K] }>;
|
|
14
15
|
//#endregion
|
|
15
16
|
//#region src/types.d.ts
|
|
16
17
|
interface Register {}
|
|
17
|
-
type
|
|
18
|
-
routes: infer
|
|
19
|
-
} ?
|
|
18
|
+
type NavigableRoute = Register extends {
|
|
19
|
+
routes: infer Routes;
|
|
20
|
+
} ? Routes extends ReadonlyArray<Route> ? Routes[number] : Routes extends Record<string, Route> ? Routes[keyof Routes] : Route : Route;
|
|
20
21
|
type Handle = Register extends {
|
|
21
22
|
handle: infer Handle;
|
|
22
|
-
} ? Handle :
|
|
23
|
+
} ? Handle : undefined;
|
|
24
|
+
type Context = Register extends {
|
|
25
|
+
context: infer Context;
|
|
26
|
+
} ? Context : undefined;
|
|
23
27
|
interface Middleware<S extends {} = any> {
|
|
24
28
|
use: <S2 extends {}>(middleware: Middleware<S2>) => Middleware<Merge<S, OptionalOnUndefined<S2>>>;
|
|
25
29
|
search: <S2 extends {}>(validate: Validator<S, S2>) => Middleware<Merge<S, OptionalOnUndefined<S2>>>;
|
|
26
30
|
handle: (handle: Handle) => Middleware<S>;
|
|
27
|
-
preload: (preload: (
|
|
31
|
+
preload: (preload: (options: PreloadOptions<{}, S>) => Promise<any>) => Middleware<S>;
|
|
28
32
|
component: (component: ComponentType) => Middleware<S>;
|
|
29
33
|
lazy: (loader: ComponentLoader) => Middleware<S>;
|
|
30
34
|
suspense: (fallback: ComponentType) => Middleware<S>;
|
|
@@ -33,19 +37,20 @@ interface Middleware<S extends {} = any> {
|
|
|
33
37
|
}>) => Middleware<S>;
|
|
34
38
|
}
|
|
35
39
|
type Validator<S extends {}, S2 extends {}> = ((search: S & Record<string, unknown>) => S2) | StandardSchemaV1<Record<string, unknown>, S2>;
|
|
36
|
-
interface
|
|
40
|
+
interface PreloadOptions<Ps extends {} = any, S extends {} = any> {
|
|
37
41
|
params: Ps;
|
|
38
42
|
search: S;
|
|
43
|
+
context: Context;
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
routes:
|
|
45
|
+
type RouterOptions = {
|
|
46
|
+
routes: ReadonlyArray<NavigableRoute> | Record<string, NavigableRoute>;
|
|
42
47
|
basePath?: string;
|
|
43
48
|
history?: HistoryLike;
|
|
44
49
|
ssrContext?: SSRContext;
|
|
45
50
|
defaultLinkOptions?: LinkOptions;
|
|
46
|
-
}
|
|
47
|
-
type Pattern =
|
|
48
|
-
type GetRoute<P extends Pattern> = Extract<
|
|
51
|
+
} & MaybeUndefinedKey<"context", Context>;
|
|
52
|
+
type Pattern = NavigableRoute["_"]["pattern"];
|
|
53
|
+
type GetRoute<P extends Pattern> = Extract<NavigableRoute, {
|
|
49
54
|
_: {
|
|
50
55
|
pattern: P;
|
|
51
56
|
};
|
|
@@ -65,7 +70,7 @@ type NavigateOptions<P extends Pattern> = {
|
|
|
65
70
|
to: P | GetRoute<P>;
|
|
66
71
|
replace?: boolean;
|
|
67
72
|
state?: any;
|
|
68
|
-
} &
|
|
73
|
+
} & MaybeObjectKey<"params", Params<P>> & MaybeObjectKey<"search", Search<P>>;
|
|
69
74
|
interface LinkOptions {
|
|
70
75
|
strict?: boolean;
|
|
71
76
|
preload?: "intent" | "render" | "viewport" | false;
|
|
@@ -113,7 +118,7 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
113
118
|
validate: (search: Record<string, unknown>) => S;
|
|
114
119
|
handles: Handle[];
|
|
115
120
|
components: ComponentType[];
|
|
116
|
-
preloads: ((
|
|
121
|
+
preloads: ((options: PreloadOptions) => Promise<any>)[];
|
|
117
122
|
p?: Route;
|
|
118
123
|
};
|
|
119
124
|
readonly _types: {
|
|
@@ -125,7 +130,7 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
125
130
|
use: <S2 extends {}>(middleware: Middleware<S2>) => Route<P, Ps, Merge<S, OptionalOnUndefined<S2>>>;
|
|
126
131
|
search: <S2 extends {}>(validate: Validator<S, S2>) => Route<P, Ps, Merge<S, OptionalOnUndefined<S2>>>;
|
|
127
132
|
handle: (handle: Handle) => Route<P, Ps, S>;
|
|
128
|
-
preload: (preload: (
|
|
133
|
+
preload: (preload: (options: PreloadOptions<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
|
|
129
134
|
component: (component: ComponentType) => Route<P, Ps, S>;
|
|
130
135
|
lazy: (loader: ComponentLoader) => Route<P, Ps, S>;
|
|
131
136
|
suspense: (fallback: ComponentType) => Route<P, Ps, S>;
|
|
@@ -137,9 +142,10 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
137
142
|
//#endregion
|
|
138
143
|
//#region src/router/router.d.ts
|
|
139
144
|
declare class Router {
|
|
140
|
-
readonly routes:
|
|
145
|
+
readonly routes: ReadonlyArray<NavigableRoute>;
|
|
141
146
|
readonly basePath: string;
|
|
142
147
|
readonly history: HistoryLike;
|
|
148
|
+
readonly context: Context;
|
|
143
149
|
readonly ssrContext?: SSRContext;
|
|
144
150
|
readonly defaultLinkOptions?: LinkOptions;
|
|
145
151
|
private readonly _;
|
|
@@ -210,4 +216,4 @@ declare const LocationContext: react.Context<HistoryLocation | null>;
|
|
|
210
216
|
declare const MatchContext: react.Context<Match | null>;
|
|
211
217
|
declare const OutletContext: react.Context<ReactNode>;
|
|
212
218
|
//#endregion
|
|
213
|
-
export { BrowserHistory, ComponentLoader, GetRoute, Handle, HashHistory, HistoryLike, HistoryLocation, HistoryPushOptions, Link, LinkOptions, LinkProps, LocationContext, Match, MatchContext, MatchOptions, MemoryHistory, Middleware, Navigate, NavigateOptions, NavigateProps, Outlet, OutletContext, Params, Pattern,
|
|
219
|
+
export { BrowserHistory, ComponentLoader, Context, GetRoute, Handle, HashHistory, HistoryLike, HistoryLocation, HistoryPushOptions, Link, LinkOptions, LinkProps, LocationContext, Match, MatchContext, MatchOptions, MemoryHistory, Middleware, NavigableRoute, Navigate, NavigateOptions, NavigateProps, Outlet, OutletContext, Params, Pattern, PreloadOptions, Register, Route, Router, RouterContext, RouterOptions, RouterRoot, RouterRootProps, SSRContext, Search, Updater, Validator, middleware, route, useHandles, useLocation, useMatch, useNavigate, useOutlet, useParams, useRouter, useSearch };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Component as e,Suspense as t,cloneElement as n,createContext as r,isValidElement as i,lazy as a,memo as o,useCallback as s,useContext as c,useEffect as l,useInsertionEffect as u,useLayoutEffect as d,useMemo as f,useRef as p,useState as m,useSyncExternalStore as h}from"react";import{inject as g,parse as _}from"regexparam";import{jsx as v}from"react/jsx-runtime";function y(e){return`/${e}`.replaceAll(/\/+/g,`/`).replace(/(.+)\/$/,`$1`)}function b(e){let{keys:t,pattern:n}=_(e);return{pattern:e,keys:t,regex:n,loose:_(e,!0).pattern,weights:e.split(`/`).slice(1).map(e=>e.includes(`*`)?0:e.includes(`:`)?1:2)}}function x(e){return typeof e==`function`?e:t=>{let n=e[`~standard`].validate(t);if(n instanceof Promise)throw Error(`[TypeRoute] Validation can't be async`);if(n.issues)throw Error(`[TypeRoute] Validation failed`,{cause:n.issues});return n.value}}function S(e){return Object.entries(e).filter(([e,t])=>t!==void 0).map(([e,t])=>`${e}=${encodeURIComponent(w(t))}`).join(`&`)}function C(e){let t=new URLSearchParams(e);return Object.fromEntries([...t.entries()].map(([e,t])=>(t=decodeURIComponent(t),[e,T(t)?JSON.parse(t):t])))}function w(e){return typeof e==`string`&&!T(e)?e:JSON.stringify(e)}function T(e){try{return JSON.parse(e),!0}catch{return!1}}function E(e,t){return y(`${t}/${e}`)}function D(e,t){return(e===t||e.startsWith(`${t}/`))&&(e=e.slice(t.length)||`/`),e}function O(e,t){return[e,S(t)].filter(Boolean).join(`?`)}function k(e){let{pathname:t,search:n}=new URL(e,`http://w`);return{path:t,search:C(n)}}function A({keys:e,regex:t,loose:n},r,i,a){let o=(r?t:n).exec(D(i,a));if(!o)return null;let s={};return e.forEach((e,t)=>{let n=o[t+1];n&&(s[e]=n)}),s}function j(e){return[...e].sort((e,t)=>{let n=e.route._.weights,r=t.route._.weights,i=Math.max(n.length,r.length);for(let e=0;e<i;e++){let t=n[e]??-1,i=r[e]??-1;if(t!==i)return i-t}return 0})}const M=r(null),N=r(null),P=r(null),F=r(null);function I(){let e=c(M);if(e)return e;throw Error(`[TypeRoute] useRouter must be within a router context`)}function L(){let e=c(N);if(e)return e;throw Error(`[TypeRoute] useLocation must be within a router context`)}function R(e){let t=I(),{path:n}=L();return f(()=>t.match(n,e),[t,n,e.from,e.strict,e.params])}function z(){return c(F)}function B(){return I().navigate}function V(){let e=c(P);return f(()=>e?.route._.handles??[],[e])}function H(e){let t=R({from:e});if(t)return t.params;throw Error(`[TypeRoute] Can't read params for non-matching route ${e}`)}function U(e){let t=I(),{search:n,path:r}=L(),i=t.getRoute(e),a=f(()=>i._.validate(n),[i,n]);return[a,Z((e,n)=>{e=typeof e==`function`?e(a):e;let i=O(r,{...a,...e});t.navigate({url:i,replace:n})})]}var W=class{_;_loc=(e,t)=>{let{state:n}=history,[r,i]=this._??[];return i?.path===e&&r===t&&i.state===n?i:(this._=[t,{path:e,search:C(t),state:n}])[1]};constructor(){if(!window[G]){for(let e of[K,q]){let t=history[e];history[e]=function(...n){t.apply(this,n),dispatchEvent(new Event(e))}}window[G]=1}}location=()=>this._loc(location.pathname,location.search);go=e=>history.go(e);push=e=>{let{url:t,replace:n,state:r}=e;history[n?q:K](r,``,t)};subscribe=e=>(J.forEach(t=>window.addEventListener(t,e)),()=>{J.forEach(t=>window.removeEventListener(t,e))})};const G=Symbol.for(`wmp01`),K=`pushState`,q=`replaceState`,J=[`popstate`,K,q,`hashchange`];var Y=class{routes;basePath;history;ssrContext;defaultLinkOptions;_;constructor(e){let{routes:t,basePath:n=`/`,history:r,
|
|
1
|
+
import{Component as e,Suspense as t,cloneElement as n,createContext as r,isValidElement as i,lazy as a,memo as o,useCallback as s,useContext as c,useEffect as l,useInsertionEffect as u,useLayoutEffect as d,useMemo as f,useRef as p,useState as m,useSyncExternalStore as h}from"react";import{inject as g,parse as _}from"regexparam";import{jsx as v}from"react/jsx-runtime";function y(e){return`/${e}`.replaceAll(/\/+/g,`/`).replace(/(.+)\/$/,`$1`)}function b(e){let{keys:t,pattern:n}=_(e);return{pattern:e,keys:t,regex:n,loose:_(e,!0).pattern,weights:e.split(`/`).slice(1).map(e=>e.includes(`*`)?0:e.includes(`:`)?1:2)}}function x(e){return typeof e==`function`?e:t=>{let n=e[`~standard`].validate(t);if(n instanceof Promise)throw Error(`[TypeRoute] Validation can't be async`);if(n.issues)throw Error(`[TypeRoute] Validation failed`,{cause:n.issues});return n.value}}function S(e){return Object.entries(e).filter(([e,t])=>t!==void 0).map(([e,t])=>`${e}=${encodeURIComponent(w(t))}`).join(`&`)}function C(e){let t=new URLSearchParams(e);return Object.fromEntries([...t.entries()].map(([e,t])=>(t=decodeURIComponent(t),[e,T(t)?JSON.parse(t):t])))}function w(e){return typeof e==`string`&&!T(e)?e:JSON.stringify(e)}function T(e){try{return JSON.parse(e),!0}catch{return!1}}function E(e,t){return y(`${t}/${e}`)}function D(e,t){return(e===t||e.startsWith(`${t}/`))&&(e=e.slice(t.length)||`/`),e}function O(e,t){return[e,S(t)].filter(Boolean).join(`?`)}function k(e){let{pathname:t,search:n}=new URL(e,`http://w`);return{path:t,search:C(n)}}function A({keys:e,regex:t,loose:n},r,i,a){let o=(r?t:n).exec(D(i,a));if(!o)return null;let s={};return e.forEach((e,t)=>{let n=o[t+1];n&&(s[e]=n)}),s}function j(e){return[...e].sort((e,t)=>{let n=e.route._.weights,r=t.route._.weights,i=Math.max(n.length,r.length);for(let e=0;e<i;e++){let t=n[e]??-1,i=r[e]??-1;if(t!==i)return i-t}return 0})}const M=r(null),N=r(null),P=r(null),F=r(null);function I(){let e=c(M);if(e)return e;throw Error(`[TypeRoute] useRouter must be within a router context`)}function L(){let e=c(N);if(e)return e;throw Error(`[TypeRoute] useLocation must be within a router context`)}function R(e){let t=I(),{path:n}=L();return f(()=>t.match(n,e),[t,n,e.from,e.strict,e.params])}function z(){return c(F)}function B(){return I().navigate}function V(){let e=c(P);return f(()=>e?.route._.handles??[],[e])}function H(e){let t=R({from:e});if(t)return t.params;throw Error(`[TypeRoute] Can't read params for non-matching route ${e}`)}function U(e){let t=I(),{search:n,path:r}=L(),i=t.getRoute(e),a=f(()=>i._.validate(n),[i,n]);return[a,Z((e,n)=>{e=typeof e==`function`?e(a):e;let i=O(r,{...a,...e});t.navigate({url:i,replace:n})})]}var W=class{_;_loc=(e,t)=>{let{state:n}=history,[r,i]=this._??[];return i?.path===e&&r===t&&i.state===n?i:(this._=[t,{path:e,search:C(t),state:n}])[1]};constructor(){if(!window[G]){for(let e of[K,q]){let t=history[e];history[e]=function(...n){t.apply(this,n),dispatchEvent(new Event(e))}}window[G]=1}}location=()=>this._loc(location.pathname,location.search);go=e=>history.go(e);push=e=>{let{url:t,replace:n,state:r}=e;history[n?q:K](r,``,t)};subscribe=e=>(J.forEach(t=>window.addEventListener(t,e)),()=>{J.forEach(t=>window.removeEventListener(t,e))})};const G=Symbol.for(`wmp01`),K=`pushState`,q=`replaceState`,J=[`popstate`,K,q,`hashchange`];var Y=class{routes;basePath;history;context;ssrContext;defaultLinkOptions;_;constructor(e){let{routes:t,basePath:n=`/`,history:r,context:i,ssrContext:a,defaultLinkOptions:o}=e;this.routes=Object.values(t),this.basePath=y(n),this.history=r??new W,this.context=i,this.ssrContext=a,this.defaultLinkOptions=o,this._={routeMap:new Map(this.routes.map(e=>[e._.pattern,e]))}}getRoute=e=>{if(typeof e!=`string`)return e;let t=this._.routeMap.get(e);if(!t)throw Error(`[TypeRoute] Route not found for ${e}`);return t};match=(e,t)=>{let{from:n,strict:r,params:i}=t,a=this.getRoute(n),o=A(a._,r,e,this.basePath);return o&&(!i||Object.keys(i).every(e=>i[e]===o[e]))?{route:a,params:o}:null};matchAll=e=>j(this.routes.map(t=>this.match(e,{from:t,strict:!0})).filter(e=>!!e))[0]??null;createUrl=e=>{let{to:t,params:n={},search:r={}}=e,{pattern:i}=this.getRoute(t)._;return O(E(g(i,n),this.basePath),r)};preload=async e=>{let{to:t,params:n={},search:r={}}=e,{preloads:i}=this.getRoute(t)._;await Promise.all(i.map(e=>e({params:n,search:r,context:this.context})))};navigate=e=>{if(typeof e==`number`)this.history.go(e);else if(`url`in e)this.history.push(e);else{let{replace:t,state:n}=e;this.history.push({url:this.createUrl(e),replace:t,state:n})}}},X=class{stack=[];index=0;listeners=new Set;constructor(e=`/`){this.stack.push({...k(e),state:void 0})}location=()=>this.stack[this.index];go=e=>{let t=this.index+e;this.stack[t]&&(this.index=t,this.listeners.forEach(e=>e()))};push=e=>{let{url:t,replace:n,state:r}=e,i={...k(t),state:r};this.stack=this.stack.slice(0,this.index+1),n?this.stack[this.index]=i:this.index=this.stack.push(i)-1,this.listeners.forEach(e=>e())};subscribe=e=>(this.listeners.add(e),()=>{this.listeners.delete(e)})},ee=class extends W{location=()=>{let{pathname:e,search:t}=new URL(location.hash.slice(1),`http://w`);return this._loc(e,t)};push=e=>{let{url:t,replace:n,state:r}=e;history[n?`replaceState`:`pushState`](r,``,`#${t}`)}};function te(e){let[t]=m(()=>`router`in e?e.router:new Y(e)),{subscribe:n,location:r}=t.history,i=h(n,r,r),a=f(()=>t.matchAll(i.path),[t,i.path]);return a||console.error(`[TypeRoute] No matching route for path`,i.path),f(()=>v(M.Provider,{value:t,children:v(N.Provider,{value:i,children:v(P.Provider,{value:a,children:a?.route._.components.reduceRight((e,t)=>v(F.Provider,{value:e,children:v(t,{})}),null)})})}),[t,i,a])}function ne(){return z()}function re(e){let t=I();return d(()=>t.navigate(e),[]),t.ssrContext&&(t.ssrContext.redirect=t.createUrl(e)),null}function ie(e){let t=I(),{to:r,replace:a,state:o,params:c,search:u,strict:d,preload:m,preloadDelay:h=50,style:g,className:_,activeStyle:y,activeClassName:b,asChild:x,children:S,...C}={...t.defaultLinkOptions,...e},w=p(null),T=p(null),E=t.createUrl(e),D=!!R({from:r,strict:d,params:c}),O=Z(()=>t.preload(e)),k=s(()=>{clearTimeout(T.current)},[]),A=s(()=>{k(),T.current=setTimeout(O,h)},[h,k]),j=f(()=>({"data-active":D,style:{...g,...D&&y},className:[_,D&&b].filter(Boolean).join(` `)||void 0}),[D,g,_,y,b]);l(()=>{if(m===`render`)A();else if(m===`viewport`&&w.current){let e=new IntersectionObserver(e=>e.forEach(e=>{e.isIntersecting?A():k()}));return e.observe(w.current),()=>{e.disconnect(),k()}}return k},[m,A,k]);let M=e=>{C.onClick?.(e),!(e.ctrlKey||e.metaKey||e.shiftKey||e.altKey||e.button!==0||e.defaultPrevented)&&(e.preventDefault(),t.navigate({url:E,replace:a,state:o}))},N=(e,t)=>n=>{t?.(n),m===`intent`&&!n.defaultPrevented&&e()},P={...C,...j,ref:ae(w,C.ref),href:E,onClick:M,onFocus:N(A,C.onFocus),onBlur:N(k,C.onBlur),onPointerEnter:N(A,C.onPointerEnter),onPointerLeave:N(k,C.onPointerLeave)};return x&&i(S)?n(S,P):v(`a`,{...P,children:S})}function ae(e,t){return t?n=>{e.current=n;let r=typeof t==`function`?t(n):void(t.current=n);return r&&(()=>{e.current=null,r()})}:e}function Z(e){let t=p(e);return u(()=>{t.current=e},[e]),p(((...e)=>t.current(...e))).current}function oe(e){return()=>v(t,{fallback:v(e,{}),children:z()})}function se(t){class n extends e{constructor(e){super(e),this.state={...e}}static getDerivedStateFromError(e){return{error:[e]}}static getDerivedStateFromProps(e,t){return e.children===t.children?t:{...e,error:void 0}}render(){return this.state.error?v(t,{error:this.state.error[0]}):this.props.children}}return()=>v(n,{children:z()})}function Q(e){return new $({...b(y(e)),validate:e=>e,handles:[],components:[],preloads:[]})}function ce(){return Q(``)}var $=class e{_;_types;constructor(e){this._=e}route=t=>new e({...this._,...b(y(`${this._.pattern}/${t}`)),p:this});use=t=>{let{_:n}=t;return new e({...this._,handles:[...this._.handles,...n.handles],components:[...this._.components,...n.components],preloads:[...this._.preloads,...n.preloads]}).search(n.validate)};search=t=>(t=x(t),new e({...this._,validate:e=>{let n=this._.validate(e);return{...n,...t({...e,...n})}}}));handle=t=>new e({...this._,handles:[...this._.handles,t]});preload=t=>new e({...this._,preloads:[...this._.preloads,e=>t({...e,search:this._.validate(e.search)})]});component=t=>new e({...this._,components:[...this._.components,o(t)]});lazy=e=>{let t=a(async()=>{let t=await e();return`default`in t?t:{default:t}});return this.preload(e).component(t)};suspense=e=>this.component(oe(e));error=e=>this.component(se(e));toString=()=>this._.pattern};export{W as BrowserHistory,ee as HashHistory,ie as Link,N as LocationContext,P as MatchContext,X as MemoryHistory,re as Navigate,ne as Outlet,F as OutletContext,$ as Route,Y as Router,M as RouterContext,te as RouterRoot,ce as middleware,Q as route,V as useHandles,L as useLocation,R as useMatch,B as useNavigate,z as useOutlet,H as useParams,I as useRouter,U as useSearch};
|