@typeroute/router 0.9.0 → 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
@@ -144,7 +144,7 @@ If you believe there's a mistake in the comparison table, please [open an issue]
144
144
  - [Dynamic page titles](#dynamic-page-titles)
145
145
  - [API reference](#api-reference)
146
146
  - [Router class](#router-class)
147
- - [Route class](#route-class)
147
+ - [Route builder](#route-builder)
148
148
  - [Middleware](#middleware)
149
149
  - [Hooks](#hooks)
150
150
  - [Components](#components)
@@ -402,15 +402,20 @@ function About() {
402
402
  }
403
403
  ```
404
404
 
405
- 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`:
406
414
 
407
415
  ```tsx
408
416
  // app.tsx
409
417
  import { RouterRoot } from "@typeroute/router";
410
- import { home } from "./pages/home";
411
- import { about } from "./pages/about";
412
-
413
- const routes = [home, about];
418
+ import * as routes from "./pages/routes";
414
419
 
415
420
  export function App() {
416
421
  return <RouterRoot routes={routes} />;
@@ -423,6 +428,8 @@ declare module "@typeroute/router" {
423
428
  }
424
429
  ```
425
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
+
426
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.
427
434
 
428
435
  ---
@@ -857,12 +864,12 @@ See [Route preloading](#route-preloading) for ways to load these components befo
857
864
 
858
865
  # Data preloading
859
866
 
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:
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:
861
868
 
862
869
  ```tsx
863
870
  const userProfile = route("/users/:id")
864
871
  .search(z.object({ tab: z.enum(["posts", "comments"]).catch("posts") }))
865
- .preload(async ({ params, search }) => {
872
+ .preload(async ({ params, search, context }) => {
866
873
  await queryClient.prefetchQuery({
867
874
  queryKey: ["user", params.id, search.tab],
868
875
  queryFn: () => fetchUser(params.id, search.tab)
@@ -894,6 +901,19 @@ const settings = dashboard.route("/settings").component(Settings);
894
901
  // Preloading /dashboard/settings runs prefetchDashboardData
895
902
  ```
896
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
+
897
917
  ---
898
918
 
899
919
  # Error boundaries
@@ -1529,6 +1549,7 @@ The `Router` class is the core of TypeRoute. You can create an instance directly
1529
1549
  - `router.basePath` - The configured base path
1530
1550
  - `router.routes` - The array of navigable routes
1531
1551
  - `router.history` - The history instance
1552
+ - `router.context` - The router context
1532
1553
  - `router.ssrContext` - The SSR context (if provided)
1533
1554
  - `router.defaultLinkOptions` - Default link options
1534
1555
 
@@ -1541,6 +1562,7 @@ The `Router` class is the core of TypeRoute. You can create an instance directly
1541
1562
  const router = new Router({ routes });
1542
1563
  const router = new Router({ routes, basePath: "/app" });
1543
1564
  const router = new Router({ routes, history: new HashHistory() });
1565
+ const router = new Router({ routes, context: { queryClient } });
1544
1566
  ```
1545
1567
 
1546
1568
  **`router.navigate(options)`** navigates to a new location.
@@ -1610,7 +1632,7 @@ await router.preload({ to: "/user/:id", params: { id: "42" } });
1610
1632
  await router.preload({ to: searchPage, search: { q: "test" } });
1611
1633
  ```
1612
1634
 
1613
- ## Route class
1635
+ ## Route builder
1614
1636
 
1615
1637
  Routes are created with the `route()` function and configured by chaining methods.
1616
1638
 
@@ -1709,13 +1731,13 @@ const risky = route("/risky").error(ErrorPage).component(RiskyPage);
1709
1731
 
1710
1732
  **`.preload(preload)`** registers a preload function for the route.
1711
1733
 
1712
- - `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`
1713
1735
  - Returns: `Route` - A new route object
1714
1736
 
1715
1737
  ```tsx
1716
1738
  const user = route("/users/:id")
1717
1739
  .search(z.object({ tab: z.string().catch("profile") }))
1718
- .preload(async ({ params, search }) => {
1740
+ .preload(async ({ params, search, context }) => {
1719
1741
  // params.id: string, search.tab: string - fully typed
1720
1742
  await prefetchUser(params.id, search.tab);
1721
1743
  });
@@ -1922,13 +1944,14 @@ const unsubscribe = history.subscribe(() => {
1922
1944
  **`RouterOptions`** are options for creating a `Router` instance or passing to `RouterRoot`.
1923
1945
 
1924
1946
  ```tsx
1925
- interface RouterOptions {
1947
+ type RouterOptions = {
1926
1948
  routes: Route[] | Record<string, Route>; // Collection of navigable routes
1927
1949
  basePath?: string; // Base path prefix (default: "/")
1928
1950
  history?: HistoryLike; // History implementation (default: BrowserHistory)
1951
+ context?: Context; // Arbitrary router context
1929
1952
  ssrContext?: SSRContext; // Context for server-side rendering
1930
1953
  defaultLinkOptions?: LinkOptions; // Default options for all Link components
1931
- }
1954
+ };
1932
1955
  ```
1933
1956
 
1934
1957
  **`NavigateOptions`** are options for type-safe navigation.
@@ -2005,12 +2028,13 @@ type SSRContext = {
2005
2028
  };
2006
2029
  ```
2007
2030
 
2008
- **`PreloadContext`** is the context passed to preload functions.
2031
+ **`PreloadOptions`** is the options object passed to preload functions.
2009
2032
 
2010
2033
  ```tsx
2011
- interface PreloadContext {
2034
+ interface PreloadOptions {
2012
2035
  params: Params; // Path params for the route
2013
2036
  search: Search; // Validated search params
2037
+ context: Context; // Router context
2014
2038
  }
2015
2039
  ```
2016
2040
 
@@ -2018,10 +2042,10 @@ interface PreloadContext {
2018
2042
 
2019
2043
  # Roadmap
2020
2044
 
2021
- - Possibility to pass an arbitrary context to the Router instance for later use in preloads?
2022
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.
2023
- - Refactor: APIs like useParams, useSearch and useMatch should accept any route object and not just rely on the global routes array.
2046
+ - Refactor: APIs like useParams, useSearch and useMatch should accept any route object and not just rely on the global routes collection.
2024
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 `"/"`.
2025
2049
  - Document usage in test environments
2026
2050
  - Navigation blockers (`useBlocker`, etc.)
2027
2051
  - Open to suggestions, we can discuss them [here](https://github.com/strblr/typeroute/discussions).
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,12 +20,15 @@ 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>;
29
33
  lazy: (loader: ComponentLoader) => Middleware<S>;
30
34
  suspense: (fallback: ComponentType) => Middleware<S>;
@@ -33,17 +37,18 @@ 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 {
45
+ type RouterOptions = {
41
46
  routes: ReadonlyArray<NavigableRoute> | Record<string, NavigableRoute>;
42
47
  basePath?: string;
43
48
  history?: HistoryLike;
44
49
  ssrContext?: SSRContext;
45
50
  defaultLinkOptions?: LinkOptions;
46
- }
51
+ } & MaybeUndefinedKey<"context", Context>;
47
52
  type Pattern = NavigableRoute["_"]["pattern"];
48
53
  type GetRoute<P extends Pattern> = Extract<NavigableRoute, {
49
54
  _: {
@@ -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>;
@@ -140,6 +145,7 @@ declare class Router {
140
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, 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 };
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=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 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.9.0",
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",