@typeroute/router 0.9.0 → 0.11.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 +143 -58
- package/dist/index.d.ts +18 -10
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ If you believe there's a mistake in the comparison table, please [open an issue]
|
|
|
107
107
|
|
|
108
108
|
- [Comparison](#comparison)
|
|
109
109
|
- [Installation](#installation)
|
|
110
|
-
- [
|
|
110
|
+
- [Motivation](#motivation)
|
|
111
111
|
- [Defining routes](#defining-routes)
|
|
112
112
|
- [Nested routes and layouts](#nested-routes-and-layouts)
|
|
113
113
|
- [Setting up the router](#setting-up-the-router)
|
|
@@ -124,6 +124,7 @@ If you believe there's a mistake in the comparison table, please [open an issue]
|
|
|
124
124
|
- [Route preloading](#route-preloading)
|
|
125
125
|
- [Programmatic navigation](#programmatic-navigation)
|
|
126
126
|
- [Declarative navigation](#declarative-navigation)
|
|
127
|
+
- [Index routes](#index-routes)
|
|
127
128
|
- [Lazy loading](#lazy-loading)
|
|
128
129
|
- [Data preloading](#data-preloading)
|
|
129
130
|
- [Error boundaries](#error-boundaries)
|
|
@@ -136,6 +137,7 @@ If you believe there's a mistake in the comparison table, please [open an issue]
|
|
|
136
137
|
- [Cookbook](#cookbook)
|
|
137
138
|
- [Quick start example](#quick-start-example)
|
|
138
139
|
- [Server-side rendering (SSR)](#server-side-rendering-ssr)
|
|
140
|
+
- [Not-found pages](#not-found-pages)
|
|
139
141
|
- [Scroll to top on navigation](#scroll-to-top-on-navigation)
|
|
140
142
|
- [Matching a route anywhere](#matching-a-route-anywhere)
|
|
141
143
|
- [Global link configuration](#global-link-configuration)
|
|
@@ -144,7 +146,7 @@ If you believe there's a mistake in the comparison table, please [open an issue]
|
|
|
144
146
|
- [Dynamic page titles](#dynamic-page-titles)
|
|
145
147
|
- [API reference](#api-reference)
|
|
146
148
|
- [Router class](#router-class)
|
|
147
|
-
- [Route
|
|
149
|
+
- [Route builder](#route-builder)
|
|
148
150
|
- [Middleware](#middleware)
|
|
149
151
|
- [Hooks](#hooks)
|
|
150
152
|
- [Components](#components)
|
|
@@ -165,48 +167,13 @@ TypeRoute requires React 18 or higher.
|
|
|
165
167
|
|
|
166
168
|
---
|
|
167
169
|
|
|
168
|
-
#
|
|
170
|
+
# Motivation
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
Most React routers today either lack type safety entirely, or achieve it through build plugins and code generation. TypeRoute takes a different path: it uses TypeScript's own inference to give you full autocompletion and type checking - for routes, params, search params, navigation - without any tooling beyond the TypeScript compiler you're already running.
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
import { route, RouterRoot, Outlet, Link, useParams } from "@typeroute/router";
|
|
174
|
-
|
|
175
|
-
// Routes
|
|
176
|
-
const layout = route("/").component(() => (
|
|
177
|
-
<div>
|
|
178
|
-
<nav>
|
|
179
|
-
<Link to="/">Home</Link>
|
|
180
|
-
<Link to={user} params={{ id: "42" }}>
|
|
181
|
-
User
|
|
182
|
-
</Link>
|
|
183
|
-
</nav>
|
|
184
|
-
<Outlet />
|
|
185
|
-
</div>
|
|
186
|
-
));
|
|
187
|
-
|
|
188
|
-
const home = layout.route("/").component(() => <h1>Home</h1>);
|
|
189
|
-
|
|
190
|
-
const user = layout.route("/users/:id").component(() => {
|
|
191
|
-
const { id } = useParams(user); // Fully typed
|
|
192
|
-
return <h1>User {id}</h1>;
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// Setup
|
|
196
|
-
const routes = [home, user];
|
|
197
|
-
|
|
198
|
-
function App() {
|
|
199
|
-
return <RouterRoot routes={routes} />;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
declare module "@typeroute/router" {
|
|
203
|
-
interface Register {
|
|
204
|
-
routes: typeof routes;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
174
|
+
The API is deliberately small. You define routes with a builder, register them once through module augmentation, and that's it. Routes nest, middlewares compose, and types inherit down the tree. There's no config file, no CLI, no codegen step. The whole thing ships at ~4kB gzipped before tree-shaking.
|
|
208
175
|
|
|
209
|
-
|
|
176
|
+
TypeRoute doesn't try to be a framework. It doesn't own your data fetching, your file structure, or force you into SSR. It handles routing - matching URLs to components and managing navigation - and stays out of the way for everything else.
|
|
210
177
|
|
|
211
178
|
👉 [Try it live in the StackBlitz playground](https://stackblitz.com/edit/typeroute-demo?file=src%2Fapp.tsx)
|
|
212
179
|
|
|
@@ -402,15 +369,20 @@ function About() {
|
|
|
402
369
|
}
|
|
403
370
|
```
|
|
404
371
|
|
|
405
|
-
Then
|
|
372
|
+
Then add a file that re-exports all your routes:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// pages/routes.ts
|
|
376
|
+
export * from "./home";
|
|
377
|
+
export * from "./about";
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Now in your root app component file, import all the routes as a namespace, register them with module augmentation, and render `RouterRoot`:
|
|
406
381
|
|
|
407
382
|
```tsx
|
|
408
383
|
// app.tsx
|
|
409
384
|
import { RouterRoot } from "@typeroute/router";
|
|
410
|
-
import
|
|
411
|
-
import { about } from "./pages/about";
|
|
412
|
-
|
|
413
|
-
const routes = [home, about];
|
|
385
|
+
import * as routes from "./pages/routes";
|
|
414
386
|
|
|
415
387
|
export function App() {
|
|
416
388
|
return <RouterRoot routes={routes} />;
|
|
@@ -423,6 +395,8 @@ declare module "@typeroute/router" {
|
|
|
423
395
|
}
|
|
424
396
|
```
|
|
425
397
|
|
|
398
|
+
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`.
|
|
399
|
+
|
|
426
400
|
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.
|
|
427
401
|
|
|
428
402
|
---
|
|
@@ -816,6 +790,42 @@ Note that `Navigate` uses `useLayoutEffect` internally to ensure the navigation
|
|
|
816
790
|
|
|
817
791
|
---
|
|
818
792
|
|
|
793
|
+
# Index routes
|
|
794
|
+
|
|
795
|
+
Layout routes often need a child route at `"/"` just to show default content at the parent's path:
|
|
796
|
+
|
|
797
|
+
```tsx
|
|
798
|
+
const dashboard = route("/dashboard").component(DashboardLayout);
|
|
799
|
+
|
|
800
|
+
const overview = dashboard.route("/").component(Overview);
|
|
801
|
+
const settings = dashboard.route("/settings").component(Settings);
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
This is perfectly fine, but `.index()` offers a shorthand. It defines what renders when no child route matches, directly on the parent:
|
|
805
|
+
|
|
806
|
+
```tsx
|
|
807
|
+
const dashboard = route("/dashboard")
|
|
808
|
+
.component(DashboardLayout)
|
|
809
|
+
.index(Overview);
|
|
810
|
+
|
|
811
|
+
const settings = dashboard.route("/settings").component(Settings);
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Here's what renders at each path:
|
|
815
|
+
|
|
816
|
+
```
|
|
817
|
+
/dashboard → DashboardLayout > Overview
|
|
818
|
+
/dashboard/settings → DashboardLayout > Settings
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Under the hood, `.index(Comp)` is equivalent to `.component(() => useOutlet() ?? <Comp />)`. Note that when using `.index()`, the layout route itself becomes navigable. Include it in your routes collection instead of the former child route:
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
824
|
+
const routes = [dashboard, settings];
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
819
829
|
# Lazy loading
|
|
820
830
|
|
|
821
831
|
Load route components on demand with `.lazy()`. The function you pass should return a dynamic import:
|
|
@@ -857,12 +867,12 @@ See [Route preloading](#route-preloading) for ways to load these components befo
|
|
|
857
867
|
|
|
858
868
|
# Data preloading
|
|
859
869
|
|
|
860
|
-
Use `.preload()` to run logic before navigation occurs, typically to prefetch data. Preload functions receive the target route's typed params and
|
|
870
|
+
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:
|
|
861
871
|
|
|
862
872
|
```tsx
|
|
863
873
|
const userProfile = route("/users/:id")
|
|
864
874
|
.search(z.object({ tab: z.enum(["posts", "comments"]).catch("posts") }))
|
|
865
|
-
.preload(async ({ params, search }) => {
|
|
875
|
+
.preload(async ({ params, search, context }) => {
|
|
866
876
|
await queryClient.prefetchQuery({
|
|
867
877
|
queryKey: ["user", params.id, search.tab],
|
|
868
878
|
queryFn: () => fetchUser(params.id, search.tab)
|
|
@@ -894,6 +904,19 @@ const settings = dashboard.route("/settings").component(Settings);
|
|
|
894
904
|
// Preloading /dashboard/settings runs prefetchDashboardData
|
|
895
905
|
```
|
|
896
906
|
|
|
907
|
+
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:
|
|
908
|
+
|
|
909
|
+
```tsx
|
|
910
|
+
<RouterRoot routes={routes} context={{ queryClient }} />;
|
|
911
|
+
|
|
912
|
+
declare module "@typeroute/router" {
|
|
913
|
+
interface Register {
|
|
914
|
+
routes: typeof routes;
|
|
915
|
+
context: { queryClient: QueryClient };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
897
920
|
---
|
|
898
921
|
|
|
899
922
|
# Error boundaries
|
|
@@ -1297,7 +1320,55 @@ import { routes } from "./routes";
|
|
|
1297
1320
|
hydrateRoot(rootElement, <RouterRoot routes={routes} />);
|
|
1298
1321
|
```
|
|
1299
1322
|
|
|
1300
|
-
You can also manually set `ssrContext.statusCode` in your components during SSR to control the response status (like 404 for not
|
|
1323
|
+
You can also manually set `ssrContext.statusCode` in your components during SSR to control the response status (like 404 for not-found pages).
|
|
1324
|
+
|
|
1325
|
+
## Not-found pages
|
|
1326
|
+
|
|
1327
|
+
Since TypeRoute uses a [ranking algorithm](#route-matching-and-ranking) where wildcards have the lowest weight, a catch-all `/*` route naturally acts as a fallback. It only matches when no other route does, regardless of definition order:
|
|
1328
|
+
|
|
1329
|
+
```tsx
|
|
1330
|
+
const home = route("/").component(HomePage);
|
|
1331
|
+
const notFound = route("/*").component(NotFoundPage);
|
|
1332
|
+
const about = route("/about").component(AboutPage);
|
|
1333
|
+
|
|
1334
|
+
const routes = [home, notFound, about];
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
```tsx
|
|
1338
|
+
function NotFoundPage() {
|
|
1339
|
+
return (
|
|
1340
|
+
<div>
|
|
1341
|
+
<h1>404</h1>
|
|
1342
|
+
<p>This page doesn't exist.</p>
|
|
1343
|
+
<Link to="/">Go home</Link>
|
|
1344
|
+
</div>
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
This also works for scoped not-found pages. If you want a fallback specific to a section of your app, attach the catch-all to that section's parent route:
|
|
1350
|
+
|
|
1351
|
+
```tsx
|
|
1352
|
+
const dashboard = route("/dashboard").component(DashboardLayout);
|
|
1353
|
+
|
|
1354
|
+
const overview = dashboard.route("/").component(Overview);
|
|
1355
|
+
const settings = dashboard.route("/settings").component(Settings);
|
|
1356
|
+
const dashboardNotFound = dashboard.route("/*").component(DashboardNotFound);
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
Here, `/dashboard/anything-else` renders `DashboardNotFound` inside the dashboard layout.
|
|
1360
|
+
|
|
1361
|
+
If you're doing SSR, you can set a 404 status code from the not-found component using `ssrContext`:
|
|
1362
|
+
|
|
1363
|
+
```tsx
|
|
1364
|
+
function NotFoundPage() {
|
|
1365
|
+
const router = useRouter();
|
|
1366
|
+
if (router.ssrContext) {
|
|
1367
|
+
router.ssrContext.statusCode = 404;
|
|
1368
|
+
}
|
|
1369
|
+
// ...
|
|
1370
|
+
}
|
|
1371
|
+
```
|
|
1301
1372
|
|
|
1302
1373
|
## Scroll to top on navigation
|
|
1303
1374
|
|
|
@@ -1529,6 +1600,7 @@ The `Router` class is the core of TypeRoute. You can create an instance directly
|
|
|
1529
1600
|
- `router.basePath` - The configured base path
|
|
1530
1601
|
- `router.routes` - The array of navigable routes
|
|
1531
1602
|
- `router.history` - The history instance
|
|
1603
|
+
- `router.context` - The router context
|
|
1532
1604
|
- `router.ssrContext` - The SSR context (if provided)
|
|
1533
1605
|
- `router.defaultLinkOptions` - Default link options
|
|
1534
1606
|
|
|
@@ -1541,6 +1613,7 @@ The `Router` class is the core of TypeRoute. You can create an instance directly
|
|
|
1541
1613
|
const router = new Router({ routes });
|
|
1542
1614
|
const router = new Router({ routes, basePath: "/app" });
|
|
1543
1615
|
const router = new Router({ routes, history: new HashHistory() });
|
|
1616
|
+
const router = new Router({ routes, context: { queryClient } });
|
|
1544
1617
|
```
|
|
1545
1618
|
|
|
1546
1619
|
**`router.navigate(options)`** navigates to a new location.
|
|
@@ -1610,7 +1683,7 @@ await router.preload({ to: "/user/:id", params: { id: "42" } });
|
|
|
1610
1683
|
await router.preload({ to: searchPage, search: { q: "test" } });
|
|
1611
1684
|
```
|
|
1612
1685
|
|
|
1613
|
-
## Route
|
|
1686
|
+
## Route builder
|
|
1614
1687
|
|
|
1615
1688
|
Routes are created with the `route()` function and configured by chaining methods.
|
|
1616
1689
|
|
|
@@ -1654,6 +1727,17 @@ const dashboard = route("/dashboard").use(auth).component(Dashboard);
|
|
|
1654
1727
|
const users = route("/users").component(UsersPage);
|
|
1655
1728
|
```
|
|
1656
1729
|
|
|
1730
|
+
**`.index(component)`** renders a component when no child route matches. See [Index routes](#index-routes).
|
|
1731
|
+
|
|
1732
|
+
- `component` - `ComponentType` - A React component
|
|
1733
|
+
- Returns: `Route` - A new route object
|
|
1734
|
+
|
|
1735
|
+
```tsx
|
|
1736
|
+
const dashboard = route("/dashboard")
|
|
1737
|
+
.component(DashboardLayout)
|
|
1738
|
+
.index(Overview);
|
|
1739
|
+
```
|
|
1740
|
+
|
|
1657
1741
|
**`.lazy(loader)`** adds a lazy-loaded component to render when this route matches.
|
|
1658
1742
|
|
|
1659
1743
|
- `loader` - `ComponentLoader` - A function returning a dynamic import promise
|
|
@@ -1709,13 +1793,13 @@ const risky = route("/risky").error(ErrorPage).component(RiskyPage);
|
|
|
1709
1793
|
|
|
1710
1794
|
**`.preload(preload)`** registers a preload function for the route.
|
|
1711
1795
|
|
|
1712
|
-
- `preload` - `(
|
|
1796
|
+
- `preload` - `(options: PreloadOptions) => Promise<any>` - An async function receiving typed `params`, `search`, and `context`
|
|
1713
1797
|
- Returns: `Route` - A new route object
|
|
1714
1798
|
|
|
1715
1799
|
```tsx
|
|
1716
1800
|
const user = route("/users/:id")
|
|
1717
1801
|
.search(z.object({ tab: z.string().catch("profile") }))
|
|
1718
|
-
.preload(async ({ params, search }) => {
|
|
1802
|
+
.preload(async ({ params, search, context }) => {
|
|
1719
1803
|
// params.id: string, search.tab: string - fully typed
|
|
1720
1804
|
await prefetchUser(params.id, search.tab);
|
|
1721
1805
|
});
|
|
@@ -1922,13 +2006,14 @@ const unsubscribe = history.subscribe(() => {
|
|
|
1922
2006
|
**`RouterOptions`** are options for creating a `Router` instance or passing to `RouterRoot`.
|
|
1923
2007
|
|
|
1924
2008
|
```tsx
|
|
1925
|
-
|
|
2009
|
+
type RouterOptions = {
|
|
1926
2010
|
routes: Route[] | Record<string, Route>; // Collection of navigable routes
|
|
1927
2011
|
basePath?: string; // Base path prefix (default: "/")
|
|
1928
2012
|
history?: HistoryLike; // History implementation (default: BrowserHistory)
|
|
2013
|
+
context?: Context; // Arbitrary router context
|
|
1929
2014
|
ssrContext?: SSRContext; // Context for server-side rendering
|
|
1930
2015
|
defaultLinkOptions?: LinkOptions; // Default options for all Link components
|
|
1931
|
-
}
|
|
2016
|
+
};
|
|
1932
2017
|
```
|
|
1933
2018
|
|
|
1934
2019
|
**`NavigateOptions`** are options for type-safe navigation.
|
|
@@ -2005,12 +2090,13 @@ type SSRContext = {
|
|
|
2005
2090
|
};
|
|
2006
2091
|
```
|
|
2007
2092
|
|
|
2008
|
-
**`
|
|
2093
|
+
**`PreloadOptions`** is the options object passed to preload functions.
|
|
2009
2094
|
|
|
2010
2095
|
```tsx
|
|
2011
|
-
interface
|
|
2096
|
+
interface PreloadOptions {
|
|
2012
2097
|
params: Params; // Path params for the route
|
|
2013
2098
|
search: Search; // Validated search params
|
|
2099
|
+
context: Context; // Router context
|
|
2014
2100
|
}
|
|
2015
2101
|
```
|
|
2016
2102
|
|
|
@@ -2018,9 +2104,8 @@ interface PreloadContext {
|
|
|
2018
2104
|
|
|
2019
2105
|
# Roadmap
|
|
2020
2106
|
|
|
2021
|
-
- Possibility to pass an arbitrary context to the Router instance for later use in preloads?
|
|
2022
2107
|
- 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.
|
|
2023
|
-
- Refactor: APIs like useParams, useSearch and useMatch should accept any route object and not just rely on the global routes
|
|
2108
|
+
- Refactor: APIs like useParams, useSearch and useMatch should accept any route object and not just rely on the global routes collection.
|
|
2024
2109
|
- Refactor: allow `route()` and `.route()` to be called without passing an argument (defaulting to "/")?
|
|
2025
2110
|
- Document usage in test environments
|
|
2026
2111
|
- Navigation blockers (`useBlocker`, etc.)
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ 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
|
|
@@ -19,13 +20,17 @@ type NavigableRoute = Register extends {
|
|
|
19
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>;
|
|
33
|
+
index: (component: ComponentType) => Middleware<S>;
|
|
29
34
|
lazy: (loader: ComponentLoader) => Middleware<S>;
|
|
30
35
|
suspense: (fallback: ComponentType) => Middleware<S>;
|
|
31
36
|
error: (fallback: ComponentType<{
|
|
@@ -33,17 +38,18 @@ interface Middleware<S extends {} = any> {
|
|
|
33
38
|
}>) => Middleware<S>;
|
|
34
39
|
}
|
|
35
40
|
type Validator<S extends {}, S2 extends {}> = ((search: S & Record<string, unknown>) => S2) | StandardSchemaV1<Record<string, unknown>, S2>;
|
|
36
|
-
interface
|
|
41
|
+
interface PreloadOptions<Ps extends {} = any, S extends {} = any> {
|
|
37
42
|
params: Ps;
|
|
38
43
|
search: S;
|
|
44
|
+
context: Context;
|
|
39
45
|
}
|
|
40
|
-
|
|
46
|
+
type RouterOptions = {
|
|
41
47
|
routes: ReadonlyArray<NavigableRoute> | Record<string, NavigableRoute>;
|
|
42
48
|
basePath?: string;
|
|
43
49
|
history?: HistoryLike;
|
|
44
50
|
ssrContext?: SSRContext;
|
|
45
51
|
defaultLinkOptions?: LinkOptions;
|
|
46
|
-
}
|
|
52
|
+
} & MaybeUndefinedKey<"context", Context>;
|
|
47
53
|
type Pattern = NavigableRoute["_"]["pattern"];
|
|
48
54
|
type GetRoute<P extends Pattern> = Extract<NavigableRoute, {
|
|
49
55
|
_: {
|
|
@@ -65,7 +71,7 @@ type NavigateOptions<P extends Pattern> = {
|
|
|
65
71
|
to: P | GetRoute<P>;
|
|
66
72
|
replace?: boolean;
|
|
67
73
|
state?: any;
|
|
68
|
-
} &
|
|
74
|
+
} & MaybeObjectKey<"params", Params<P>> & MaybeObjectKey<"search", Search<P>>;
|
|
69
75
|
interface LinkOptions {
|
|
70
76
|
strict?: boolean;
|
|
71
77
|
preload?: "intent" | "render" | "viewport" | false;
|
|
@@ -113,7 +119,7 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
113
119
|
validate: (search: Record<string, unknown>) => S;
|
|
114
120
|
handles: Handle[];
|
|
115
121
|
components: ComponentType[];
|
|
116
|
-
preloads: ((
|
|
122
|
+
preloads: ((options: PreloadOptions) => Promise<any>)[];
|
|
117
123
|
p?: Route;
|
|
118
124
|
};
|
|
119
125
|
readonly _types: {
|
|
@@ -125,8 +131,9 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
125
131
|
use: <S2 extends {}>(middleware: Middleware<S2>) => Route<P, Ps, Merge<S, OptionalOnUndefined<S2>>>;
|
|
126
132
|
search: <S2 extends {}>(validate: Validator<S, S2>) => Route<P, Ps, Merge<S, OptionalOnUndefined<S2>>>;
|
|
127
133
|
handle: (handle: Handle) => Route<P, Ps, S>;
|
|
128
|
-
preload: (preload: (
|
|
134
|
+
preload: (preload: (options: PreloadOptions<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
|
|
129
135
|
component: (component: ComponentType) => Route<P, Ps, S>;
|
|
136
|
+
index: (component: ComponentType) => Route<P, Ps, S>;
|
|
130
137
|
lazy: (loader: ComponentLoader) => Route<P, Ps, S>;
|
|
131
138
|
suspense: (fallback: ComponentType) => Route<P, Ps, S>;
|
|
132
139
|
error: (fallback: ComponentType<{
|
|
@@ -140,6 +147,7 @@ declare class Router {
|
|
|
140
147
|
readonly routes: ReadonlyArray<NavigableRoute>;
|
|
141
148
|
readonly basePath: string;
|
|
142
149
|
readonly history: HistoryLike;
|
|
150
|
+
readonly context: Context;
|
|
143
151
|
readonly ssrContext?: SSRContext;
|
|
144
152
|
readonly defaultLinkOptions?: LinkOptions;
|
|
145
153
|
private readonly _;
|
|
@@ -210,4 +218,4 @@ declare const LocationContext: react.Context<HistoryLocation | null>;
|
|
|
210
218
|
declare const MatchContext: react.Context<Match | null>;
|
|
211
219
|
declare const OutletContext: react.Context<ReactNode>;
|
|
212
220
|
//#endregion
|
|
213
|
-
export { BrowserHistory, ComponentLoader, GetRoute, Handle, HashHistory, HistoryLike, HistoryLocation, HistoryPushOptions, Link, LinkOptions, LinkProps, LocationContext, Match, MatchContext, MatchOptions, MemoryHistory, Middleware, NavigableRoute, Navigate, NavigateOptions, NavigateProps, Outlet, OutletContext, Params, Pattern,
|
|
221
|
+
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
|
|
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 ee,useInsertionEffect as te,useLayoutEffect as l,useMemo as u,useRef as d,useState as f,useSyncExternalStore as p}from"react";import{inject as m,parse as h}from"regexparam";import{jsx as g}from"react/jsx-runtime";function _(e){return`/${e}`.replaceAll(/\/+/g,`/`).replace(/(.+)\/$/,`$1`)}function v(e){let{keys:t,pattern:n}=h(e);return{pattern:e,keys:t,regex:n,loose:h(e,!0).pattern,weights:e.split(`/`).slice(1).map(e=>e.includes(`*`)?0:e.includes(`:`)?1:2)}}function y(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 b(e){return Object.entries(e).filter(([e,t])=>t!==void 0).map(([e,t])=>`${e}=${encodeURIComponent(S(t))}`).join(`&`)}function x(e){let t=new URLSearchParams(e);return Object.fromEntries([...t.entries()].map(([e,t])=>(t=decodeURIComponent(t),[e,C(t)?JSON.parse(t):t])))}function S(e){return typeof e==`string`&&!C(e)?e:JSON.stringify(e)}function C(e){try{return JSON.parse(e),!0}catch{return!1}}function w(e,t){return _(`${t}/${e}`)}function T(e,t){return(e===t||e.startsWith(`${t}/`))&&(e=e.slice(t.length)||`/`),e}function E(e,t){return[e,b(t)].filter(Boolean).join(`?`)}function D(e){let{pathname:t,search:n}=new URL(e,`http://w`);return{path:t,search:x(n)}}function O({keys:e,regex:t,loose:n},r,i,a){let o=(r?t:n).exec(T(i,a));if(!o)return null;let s={};return e.forEach((e,t)=>{let n=o[t+1];n&&(s[e]=n)}),s}function k(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 A=r(null),j=r(null),M=r(null),N=r(null);function P(){let e=c(A);if(e)return e;throw Error(`[TypeRoute] useRouter must be within a router context`)}function F(){let e=c(j);if(e)return e;throw Error(`[TypeRoute] useLocation must be within a router context`)}function I(e){let t=P(),{path:n}=F();return u(()=>t.match(n,e),[t,n,e.from,e.strict,e.params])}function L(){return c(N)}function ne(){return P().navigate}function re(){let e=c(M);return u(()=>e?.route._.handles??[],[e])}function R(e){let t=I({from:e});if(t)return t.params;throw Error(`[TypeRoute] Can't read params for non-matching route ${e}`)}function z(e){let t=P(),{search:n,path:r}=F(),i=t.getRoute(e),a=u(()=>i._.validate(n),[i,n]);return[a,X((e,n)=>{e=typeof e==`function`?e(a):e;let i=E(r,{...a,...e});t.navigate({url:i,replace:n})})]}var B=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:x(t),state:n}])[1]};constructor(){if(!window[V]){for(let e of[H,U]){let t=history[e];history[e]=function(...n){t.apply(this,n),dispatchEvent(new Event(e))}}window[V]=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?U:H](r,``,t)};subscribe=e=>(W.forEach(t=>window.addEventListener(t,e)),()=>{W.forEach(t=>window.removeEventListener(t,e))})};const V=Symbol.for(`wmp01`),H=`pushState`,U=`replaceState`,W=[`popstate`,H,U,`hashchange`];var G=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=_(n),this.history=r??new B,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=O(a._,r,e,this.basePath);return o&&(!i||Object.keys(i).every(e=>i[e]===o[e]))?{route:a,params:o}:null};matchAll=e=>k(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 E(w(m(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})}}},K=class{stack=[];index=0;listeners=new Set;constructor(e=`/`){this.stack.push({...D(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={...D(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)})},q=class extends B{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 J(e){let[t]=f(()=>`router`in e?e.router:new G(e)),{subscribe:n,location:r}=t.history,i=p(n,r,r),a=u(()=>t.matchAll(i.path),[t,i.path]);return a||console.error(`[TypeRoute] No matching route for path`,i.path),u(()=>g(A.Provider,{value:t,children:g(j.Provider,{value:i,children:g(M.Provider,{value:a,children:a?.route._.components.reduceRight((e,t)=>g(N.Provider,{value:e,children:g(t,{})}),null)})})}),[t,i,a])}function Y(){return L()}function ie(e){let t=P();return l(()=>t.navigate(e),[]),t.ssrContext&&(t.ssrContext.redirect=t.createUrl(e)),null}function ae(e){let t=P(),{to:r,replace:a,state:o,params:c,search:te,strict:l,preload:f,preloadDelay:p=50,style:m,className:h,activeStyle:_,activeClassName:v,asChild:y,children:b,...x}={...t.defaultLinkOptions,...e},S=d(null),C=d(null),w=t.createUrl(e),T=!!I({from:r,strict:l,params:c}),E=X(()=>t.preload(e)),D=s(()=>{clearTimeout(C.current)},[]),O=s(()=>{D(),C.current=setTimeout(E,p)},[p,D]),k=u(()=>({"data-active":T,style:{...m,...T&&_},className:[h,T&&v].filter(Boolean).join(` `)||void 0}),[T,m,h,_,v]);ee(()=>{if(f===`render`)O();else if(f===`viewport`&&S.current){let e=new IntersectionObserver(e=>e.forEach(e=>{e.isIntersecting?O():D()}));return e.observe(S.current),()=>{e.disconnect(),D()}}return D},[f,O,D]);let A=e=>{x.onClick?.(e),!(e.ctrlKey||e.metaKey||e.shiftKey||e.altKey||e.button!==0||e.defaultPrevented)&&(e.preventDefault(),t.navigate({url:w,replace:a,state:o}))},j=(e,t)=>n=>{t?.(n),f===`intent`&&!n.defaultPrevented&&e()},M={...x,...k,ref:oe(S,x.ref),href:w,onClick:A,onFocus:j(O,x.onFocus),onBlur:j(D,x.onBlur),onPointerEnter:j(O,x.onPointerEnter),onPointerLeave:j(D,x.onPointerLeave)};return y&&i(b)?n(b,M):g(`a`,{...M,children:b})}function oe(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 X(e){let t=d(e);return te(()=>{t.current=e},[e]),d(((...e)=>t.current(...e))).current}function Z(e){return()=>L()??g(e,{})}function se(e){return()=>g(t,{fallback:g(e,{}),children:L()})}function ce(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?g(t,{error:this.state.error[0]}):this.props.children}}return()=>g(n,{children:L()})}function Q(e){return new $({...v(_(e)),validate:e=>e,handles:[],components:[],preloads:[]})}function le(){return Q(``)}var $=class e{_;_types;constructor(e){this._=e}route=t=>new e({...this._,...v(_(`${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=y(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)]});index=e=>this.component(Z(e));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(se(e));error=e=>this.component(ce(e));toString=()=>this._.pattern};export{B as BrowserHistory,q as HashHistory,ae as Link,j as LocationContext,M as MatchContext,K as MemoryHistory,ie as Navigate,Y as Outlet,N as OutletContext,$ as Route,G as Router,A as RouterContext,J as RouterRoot,le as middleware,Q as route,re as useHandles,F as useLocation,I as useMatch,ne as useNavigate,L as useOutlet,R as useParams,P as useRouter,z as useSearch};
|