@typeroute/router 0.8.0 → 0.9.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)
@@ -135,6 +141,7 @@ If you believe there's a mistake in the comparison table, please [open an issue]
135
141
  - [Global link configuration](#global-link-configuration)
136
142
  - [History middleware](#history-middleware)
137
143
  - [View transitions](#view-transitions)
144
+ - [Dynamic page titles](#dynamic-page-titles)
138
145
  - [API reference](#api-reference)
139
146
  - [Router class](#router-class)
140
147
  - [Route class](#route-class)
@@ -148,6 +155,16 @@ If you believe there's a mistake in the comparison table, please [open an issue]
148
155
 
149
156
  ---
150
157
 
158
+ # Installation
159
+
160
+ ```bash
161
+ npm install @typeroute/router
162
+ ```
163
+
164
+ TypeRoute requires React 18 or higher.
165
+
166
+ ---
167
+
151
168
  # Showcase
152
169
 
153
170
  Here's what routing looks like with TypeRoute:
@@ -155,12 +172,12 @@ Here's what routing looks like with TypeRoute:
155
172
  ```tsx
156
173
  import { route, RouterRoot, Outlet, Link, useParams } from "@typeroute/router";
157
174
 
158
- // Layout
175
+ // Routes
159
176
  const layout = route("/").component(() => (
160
177
  <div>
161
178
  <nav>
162
179
  <Link to="/">Home</Link>
163
- <Link to="/users/:id" params={{ id: "42" }}>
180
+ <Link to={user} params={{ id: "42" }}>
164
181
  User
165
182
  </Link>
166
183
  </nav>
@@ -168,10 +185,9 @@ const layout = route("/").component(() => (
168
185
  </div>
169
186
  ));
170
187
 
171
- // Pages
172
188
  const home = layout.route("/").component(() => <h1>Home</h1>);
173
189
 
174
- const user = layout.route("/users/:id").component(function UserPage() {
190
+ const user = layout.route("/users/:id").component(() => {
175
191
  const { id } = useParams(user); // Fully typed
176
192
  return <h1>User {id}</h1>;
177
193
  });
@@ -196,16 +212,6 @@ Everything autocompletes and type-checks automatically. No heavy setup, no magic
196
212
 
197
213
  ---
198
214
 
199
- # Installation
200
-
201
- ```bash
202
- npm install @typeroute/router
203
- ```
204
-
205
- TypeRoute requires React 18 or higher.
206
-
207
- ---
208
-
209
215
  # Defining routes
210
216
 
211
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.
@@ -297,7 +303,7 @@ Beyond paths and components, child routes also inherit search param validators,
297
303
 
298
304
  # Setting up the router
299
305
 
300
- 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:
301
307
 
302
308
  ```tsx
303
309
  // Intermediate route used for hierarchy
@@ -309,13 +315,16 @@ const about = layout.route("/about").component(About);
309
315
 
310
316
  // Collect only the navigable routes
311
317
  const routes = [home, about]; // ✅ Don't include `layout`
318
+
319
+ // Or equivalently:
320
+ const routes = { home, about };
312
321
  ```
313
322
 
314
- 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.
315
324
 
316
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.
317
326
 
318
- 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`):
319
328
 
320
329
  ```tsx
321
330
  import { RouterRoot } from "@typeroute/router";
@@ -1112,7 +1121,7 @@ For the path `/users/42`:
1112
1121
  /users/* → [static, wildcard] → weights [2, 0]
1113
1122
  ```
1114
1123
 
1115
- 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:
1124
+ 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:
1116
1125
 
1117
1126
  ```tsx
1118
1127
  const routes = [
@@ -1271,9 +1280,7 @@ function handleRequest(req: Request) {
1271
1280
  if (ssrContext.redirect) {
1272
1281
  return Response.redirect(ssrContext.redirect);
1273
1282
  }
1274
- return new Response(html, {
1275
- headers: { "Content-Type": "text/html" }
1276
- });
1283
+ // ... Send pre-rendered HTML
1277
1284
  }
1278
1285
  ```
1279
1286
 
@@ -1452,6 +1459,63 @@ Add CSS to control the transition:
1452
1459
 
1453
1460
  For more advanced techniques, see the [MDN documentation on View Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API).
1454
1461
 
1462
+ ## Dynamic page titles
1463
+
1464
+ Use [route handles](#route-handles) to define page titles and update the browser's document title dynamically as users navigate. First, attach a `title` handle to each route:
1465
+
1466
+ ```tsx
1467
+ const layout = route("/").handle({ title: "App" }).component(Layout);
1468
+ const home = layout.route("/").handle({ title: "Home" }).component(HomePage);
1469
+ const settings = layout
1470
+ .route("/settings")
1471
+ .handle({ title: "Settings" })
1472
+ .component(SettingsPage);
1473
+ ```
1474
+
1475
+ Then create a component that reads the handles and updates `document.title`:
1476
+
1477
+ ```tsx
1478
+ function DocumentTitle() {
1479
+ const handles = useHandles();
1480
+
1481
+ useEffect(() => {
1482
+ document.title = handles
1483
+ .map(h => h.title)
1484
+ .reverse()
1485
+ .join(" - ");
1486
+ }, [handles]);
1487
+
1488
+ return null;
1489
+ }
1490
+ ```
1491
+
1492
+ Place this component somewhere in your layout:
1493
+
1494
+ ```tsx
1495
+ function Layout() {
1496
+ return (
1497
+ <>
1498
+ <DocumentTitle />
1499
+ <Header />
1500
+ <Outlet />
1501
+ </>
1502
+ );
1503
+ }
1504
+ ```
1505
+
1506
+ When visiting `/settings`, the document title becomes "Settings - App". The `useHandles()` hook returns handles from all routes in the current matching chain (from root to leaf), so reversing the array puts the most specific page first.
1507
+
1508
+ For type safety, register your handle type:
1509
+
1510
+ ```tsx
1511
+ declare module "@typeroute/router" {
1512
+ interface Register {
1513
+ routes: typeof routes;
1514
+ handle: { title: string };
1515
+ }
1516
+ }
1517
+ ```
1518
+
1455
1519
  ---
1456
1520
 
1457
1521
  # API reference
@@ -1463,7 +1527,7 @@ The `Router` class is the core of TypeRoute. You can create an instance directly
1463
1527
  **Properties:**
1464
1528
 
1465
1529
  - `router.basePath` - The configured base path
1466
- - `router.routes` - The array of routes
1530
+ - `router.routes` - The array of navigable routes
1467
1531
  - `router.history` - The history instance
1468
1532
  - `router.ssrContext` - The SSR context (if provided)
1469
1533
  - `router.defaultLinkOptions` - Default link options
@@ -1859,7 +1923,7 @@ const unsubscribe = history.subscribe(() => {
1859
1923
 
1860
1924
  ```tsx
1861
1925
  interface RouterOptions {
1862
- routes: Route[]; // Array of navigable routes (required)
1926
+ routes: Route[] | Record<string, Route>; // Collection of navigable routes
1863
1927
  basePath?: string; // Base path prefix (default: "/")
1864
1928
  history?: HistoryLike; // History implementation (default: BrowserHistory)
1865
1929
  ssrContext?: SSRContext; // Context for server-side rendering
@@ -1956,6 +2020,8 @@ interface PreloadContext {
1956
2020
 
1957
2021
  - Possibility to pass an arbitrary context to the Router instance for later use in preloads?
1958
2022
  - 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.
2024
+ - Refactor: allow `route()` and `.route()` to be called without passing an argument (defaulting to "/")?
1959
2025
  - Document usage in test environments
1960
2026
  - Navigation blockers (`useBlocker`, etc.)
1961
2027
  - Open to suggestions, we can discuss them [here](https://github.com/strblr/typeroute/discussions).
package/dist/index.d.ts CHANGED
@@ -14,9 +14,9 @@ type OptionalOnUndefined<T extends object> = Simplify<{ [K in keyof T as undefin
14
14
  //#endregion
15
15
  //#region src/types.d.ts
16
16
  interface Register {}
17
- type RouteList = Register extends {
18
- routes: infer RouteList extends ReadonlyArray<Route>;
19
- } ? RouteList : ReadonlyArray<Route>;
17
+ type NavigableRoute = Register extends {
18
+ routes: infer Routes;
19
+ } ? Routes extends ReadonlyArray<Route> ? Routes[number] : Routes extends Record<string, Route> ? Routes[keyof Routes] : Route : Route;
20
20
  type Handle = Register extends {
21
21
  handle: infer Handle;
22
22
  } ? Handle : any;
@@ -38,14 +38,14 @@ interface PreloadContext<Ps extends {} = any, S extends {} = any> {
38
38
  search: S;
39
39
  }
40
40
  interface RouterOptions {
41
- routes: RouteList;
41
+ routes: ReadonlyArray<NavigableRoute> | Record<string, NavigableRoute>;
42
42
  basePath?: string;
43
43
  history?: HistoryLike;
44
44
  ssrContext?: SSRContext;
45
45
  defaultLinkOptions?: LinkOptions;
46
46
  }
47
- type Pattern = RouteList[number]["_"]["pattern"];
48
- type GetRoute<P extends Pattern> = Extract<RouteList[number], {
47
+ type Pattern = NavigableRoute["_"]["pattern"];
48
+ type GetRoute<P extends Pattern> = Extract<NavigableRoute, {
49
49
  _: {
50
50
  pattern: P;
51
51
  };
@@ -137,7 +137,7 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
137
137
  //#endregion
138
138
  //#region src/router/router.d.ts
139
139
  declare class Router {
140
- readonly routes: RouteList;
140
+ readonly routes: ReadonlyArray<NavigableRoute>;
141
141
  readonly basePath: string;
142
142
  readonly history: HistoryLike;
143
143
  readonly ssrContext?: SSRContext;
@@ -210,4 +210,4 @@ declare const LocationContext: react.Context<HistoryLocation | null>;
210
210
  declare const MatchContext: react.Context<Match | null>;
211
211
  declare const OutletContext: react.Context<ReactNode>;
212
212
  //#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 };
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 };
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);let r=new Event(e);dispatchEvent(r)}}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={children:e.children,error:null}}static getDerivedStateFromError(e){return{error:[e]}}static getDerivedStateFromProps(e,t){return e.children===t.children?t:{children:e.children,error:null}}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;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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroute/router",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",