@typeroute/router 0.10.0 → 0.11.1

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)
@@ -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
- }
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.
201
175
 
202
- declare module "@typeroute/router" {
203
- interface Register {
204
- routes: typeof routes;
205
- }
206
- }
207
- ```
208
-
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
 
@@ -823,6 +790,42 @@ Note that `Navigate` uses `useLayoutEffect` internally to ensure the navigation
823
790
 
824
791
  ---
825
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
+
826
829
  # Lazy loading
827
830
 
828
831
  Load route components on demand with `.lazy()`. The function you pass should return a dynamic import:
@@ -1317,7 +1320,55 @@ import { routes } from "./routes";
1317
1320
  hydrateRoot(rootElement, <RouterRoot routes={routes} />);
1318
1321
  ```
1319
1322
 
1320
- 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
+ ```
1321
1372
 
1322
1373
  ## Scroll to top on navigation
1323
1374
 
@@ -1676,6 +1727,17 @@ const dashboard = route("/dashboard").use(auth).component(Dashboard);
1676
1727
  const users = route("/users").component(UsersPage);
1677
1728
  ```
1678
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
+
1679
1741
  **`.lazy(loader)`** adds a lazy-loaded component to render when this route matches.
1680
1742
 
1681
1743
  - `loader` - `ComponentLoader` - A function returning a dynamic import promise
@@ -2045,7 +2107,6 @@ interface PreloadOptions {
2045
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.
2046
2108
  - Refactor: APIs like useParams, useSearch and useMatch should accept any route object and not just rely on the global routes collection.
2047
2109
  - 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 `"/"`.
2049
2110
  - Document usage in test environments
2050
2111
  - Navigation blockers (`useBlocker`, etc.)
2051
2112
  - Open to suggestions, we can discuss them [here](https://github.com/strblr/typeroute/discussions).
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ interface Middleware<S extends {} = any> {
30
30
  handle: (handle: Handle) => Middleware<S>;
31
31
  preload: (preload: (options: PreloadOptions<{}, S>) => Promise<any>) => Middleware<S>;
32
32
  component: (component: ComponentType) => Middleware<S>;
33
+ index: (component: ComponentType) => Middleware<S>;
33
34
  lazy: (loader: ComponentLoader) => Middleware<S>;
34
35
  suspense: (fallback: ComponentType) => Middleware<S>;
35
36
  error: (fallback: ComponentType<{
@@ -132,6 +133,7 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
132
133
  handle: (handle: Handle) => Route<P, Ps, S>;
133
134
  preload: (preload: (options: PreloadOptions<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
134
135
  component: (component: ComponentType) => Route<P, Ps, S>;
136
+ index: (component: ComponentType) => Route<P, Ps, S>;
135
137
  lazy: (loader: ComponentLoader) => Route<P, Ps, S>;
136
138
  suspense: (fallback: ComponentType) => Route<P, Ps, S>;
137
139
  error: (fallback: ComponentType<{
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;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};
1
+ import{Component as e,Suspense as t,cloneElement as n,createContext as r,isValidElement as i,lazy as a,memo as o,useContext as s,useEffect as c,useInsertionEffect as ee,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={};return new URLSearchParams(e).forEach((e,n)=>{t[n]=C(e)?JSON.parse(e):e}),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=s(A);if(e)return e;throw Error(`[TypeRoute] useRouter must be within a router context`)}function F(){let e=s(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 s(N)}function te(){return P().navigate}function ne(){let e=s(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,Z((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=>addEventListener(t,e)),()=>{W.forEach(t=>removeEventListener(t,e))})};const V=Symbol.for(`wmp01`),H=`pushState`,U=`replaceState`,W=[`popstate`,H,U,`hashchange`];var G=class{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._=new Map(this.routes.map(e=>[e._.pattern,e]))}getRoute=e=>{if(typeof e!=`string`)return e;let t=this._.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 X(e){let t=P();return l(()=>t.navigate(e),[]),t.ssrContext&&(t.ssrContext.redirect=t.createUrl(e)),null}function re(e){let t=P(),{to:r,replace:a,state:o,params:s,search:ee,strict:l,preload:u,preloadDelay:f=50,style:p,className:m,activeStyle:h,activeClassName:_,asChild:v,children:y,...b}={...t.defaultLinkOptions,...e},x=d(null),S=d(null),C=t.createUrl(e),w=!!I({from:r,strict:l,params:s}),T=Z(()=>{clearTimeout(S.current)}),E=Z(()=>{T(),S.current=setTimeout(()=>t.preload(e),f)}),D={"data-active":w,style:{...p,...w&&h},className:[m,w&&_].filter(Boolean).join(` `)||void 0};c(()=>{if(u===`render`)E();else if(u===`viewport`&&x.current){let e=new IntersectionObserver(e=>e.forEach(e=>{e.isIntersecting?E():T()}));return e.observe(x.current),()=>{e.disconnect(),T()}}return T},[u]);let O=e=>{b.onClick?.(e),!(e.ctrlKey||e.metaKey||e.shiftKey||e.altKey||e.button||e.defaultPrevented)&&(e.preventDefault(),t.navigate({url:C,replace:a,state:o}))},k=(e,t)=>n=>{t?.(n),u===`intent`&&!n.defaultPrevented&&e()},A={...b,...D,ref:ie(x,b.ref),href:C,onClick:O,onFocus:k(E,b.onFocus),onBlur:k(T,b.onBlur),onPointerEnter:k(E,b.onPointerEnter),onPointerLeave:k(T,b.onPointerLeave)};return v&&i(y)?n(y,A):g(`a`,{...A,children:y})}function ie(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=d(e);return ee(()=>{t.current=e},[e]),d(((...e)=>t.current(...e))).current}function ae(e){return()=>L()??g(e,{})}function oe(e){return()=>g(t,{fallback:g(e,{}),children:L()})}function se(t){class n extends e{state={...this.props};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 ce(){return Q(``)}var $=class e{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(ae(e));lazy=e=>this.preload(e).component(a(()=>e().then(e=>`default`in e?e:{default:e})));suspense=e=>this.component(oe(e));error=e=>this.component(se(e));toString=()=>this._.pattern};export{B as BrowserHistory,q as HashHistory,re as Link,j as LocationContext,M as MatchContext,K as MemoryHistory,X as Navigate,Y as Outlet,N as OutletContext,$ as Route,G as Router,A as RouterContext,J as RouterRoot,ce as middleware,Q as route,ne as useHandles,F as useLocation,I as useMatch,te 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.10.0",
3
+ "version": "0.11.1",
4
4
  "license": "MIT",
5
5
  "author": "strblr",
6
6
  "description": "Type-safe React router that just works - simple setup, full autocomplete, 4kB gzipped",