@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 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
- - [Showcase](#showcase)
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 class](#route-class)
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
- # Showcase
170
+ # Motivation
169
171
 
170
- Here's what routing looks like with TypeRoute:
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
- ```tsx
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
- Everything autocompletes and type-checks automatically. No heavy setup, no magic, just a simple API that gets out of your way.
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 in your root app component file, import all the routes, register them with module augmentation, and render `RouterRoot`:
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 { home } from "./pages/home";
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 search values:
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 found pages).
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 class
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` - `(context: PreloadContext) => Promise<any>` - An async function receiving typed `params` and `search`
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
- interface RouterOptions {
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
- **`PreloadContext`** is the context passed to preload functions.
2093
+ **`PreloadOptions`** is the options object passed to preload functions.
2009
2094
 
2010
2095
  ```tsx
2011
- interface PreloadContext {
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 array.
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 MaybeKey<K extends string, T> = T extends EmptyObject ? { [P in K]?: EmptyObject } : {} extends T ? { [P in K]?: T } : { [P in K]: T };
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 : any;
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: (context: PreloadContext<{}, S>) => Promise<any>) => Middleware<S>;
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 PreloadContext<Ps extends {} = any, S extends {} = any> {
41
+ interface PreloadOptions<Ps extends {} = any, S extends {} = any> {
37
42
  params: Ps;
38
43
  search: S;
44
+ context: Context;
39
45
  }
40
- interface RouterOptions {
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
- } & MaybeKey<"params", Params<P>> & MaybeKey<"search", Search<P>>;
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: ((context: PreloadContext) => Promise<any>)[];
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: (context: PreloadContext<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
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, PreloadContext, Register, Route, Router, RouterContext, RouterOptions, RouterRoot, RouterRootProps, SSRContext, Search, Updater, Validator, middleware, route, useHandles, useLocation, useMatch, useNavigate, useOutlet, useParams, useRouter, useSearch };
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 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,ssrContext:i,defaultLinkOptions:a}=e;this.routes=Object.values(t),this.basePath=y(n),this.history=r??new W,this.ssrContext=i,this.defaultLinkOptions=a,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})))};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({params:e.params,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};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroute/router",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "author": "strblr",
6
6
  "description": "Type-safe React router that just works - simple setup, full autocomplete, 4kB gzipped",