@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 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?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
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.badgesize.io/https://cdn.jsdelivr.net/npm/@typeroute/router/dist/index.js?compression=gzip&label=gzip&style=flat-square&color=0B0D0F&labelColor=0B0D0F"
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?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
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?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
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 class](#route-class)
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
- // Layout
175
+ // Routes
160
176
  const layout = route("/").component(() => (
161
177
  <div>
162
178
  <nav>
163
179
  <Link to="/">Home</Link>
164
- <Link to="/users/:id" params={{ id: "42" }}>
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(function UserPage() {
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 an array. 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 array - only the final, navigable routes should be:
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 array doesn't matter - TypeRoute uses a [ranking algorithm](#route-matching-and-ranking) to pick the most specific match.
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 array directly to `RouterRoot`. This creates a router instance internally (accessible via `useRouter`):
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 in your root app component file, import all the routes, register them with module augmentation, and render `RouterRoot`:
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 { home } from "./pages/home";
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 search values:
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 array carefully. Define them in any order and TypeRoute figures out the right match regardless:
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
- return new Response(html, {
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 class
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` - `(context: PreloadContext) => Promise<any>` - An async function receiving typed `params` and `search`
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
- interface RouterOptions {
1920
- routes: Route[]; // Array of navigable routes (required)
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
- **`PreloadContext`** is the context passed to preload functions.
2031
+ **`PreloadOptions`** is the options object passed to preload functions.
2003
2032
 
2004
2033
  ```tsx
2005
- interface PreloadContext {
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 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
16
17
  interface Register {}
17
- type RouteList = Register extends {
18
- routes: infer RouteList extends ReadonlyArray<Route>;
19
- } ? RouteList : ReadonlyArray<Route>;
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 : 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>;
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 PreloadContext<Ps extends {} = any, S extends {} = any> {
40
+ interface PreloadOptions<Ps extends {} = any, S extends {} = any> {
37
41
  params: Ps;
38
42
  search: S;
43
+ context: Context;
39
44
  }
40
- interface RouterOptions {
41
- routes: RouteList;
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 = RouteList[number]["_"]["pattern"];
48
- type GetRoute<P extends Pattern> = Extract<RouteList[number], {
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
- } & MaybeKey<"params", Params<P>> & MaybeKey<"search", Search<P>>;
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: ((context: PreloadContext) => Promise<any>)[];
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: (context: PreloadContext<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
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: RouteList;
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, PreloadContext, Register, Route, RouteList, Router, RouterContext, RouterOptions, RouterRoot, RouterRootProps, SSRContext, Search, Updater, Validator, middleware, route, useHandles, useLocation, useMatch, useNavigate, useOutlet, useParams, useRouter, useSearch };
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,ssrContext:i,defaultLinkOptions:a}=e;this.routes=t,this.basePath=y(n),this.history=r??new W,this.ssrContext=i,this.defaultLinkOptions=a,this._={routeMap:new Map(t.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=>{C.onFocus?.(e),m===`intent`&&!e.defaultPrevented&&A()},P=e=>{C.onBlur?.(e),m===`intent`&&k()},F=e=>{C.onPointerEnter?.(e),m===`intent`&&!e.defaultPrevented&&A()},L=e=>{C.onPointerLeave?.(e),m===`intent`&&k()},z={...C,...j,ref:ae(w,C.ref),href:E,onClick:M,onFocus:N,onBlur:P,onPointerEnter:F,onPointerLeave:L};return x&&i(S)?n(S,z):v(`a`,{...z,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 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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroute/router",
3
- "version": "0.8.1",
3
+ "version": "0.10.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",