waymark 0.3.1 → 0.4.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 +99 -2
- package/dist/index.d.ts +38 -20
- package/dist/index.js +1 -1
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
</p>
|
|
41
41
|
|
|
42
42
|
<p align="center">
|
|
43
|
-
<a href="https://
|
|
43
|
+
<a href="https://waymarkrouter.com">📖 Documentation</a>
|
|
44
44
|
</p>
|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -50,7 +50,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
|
|
|
50
50
|
- **Fully type-safe** - Complete TypeScript inference for routes, path params, and search params
|
|
51
51
|
- **Zero config** - No build plugins, no CLI, no codegen, no config files, very low boilerplate
|
|
52
52
|
- **Familiar API** - If you've used React Router or TanStack Router, you'll feel at home
|
|
53
|
-
- **3.
|
|
53
|
+
- **3.7kB gzipped** - Extremely lightweight with just one 0.4kB dependency, so ~4kB total
|
|
54
54
|
- **Feature packed** - Search param validation, lazy loading, data preloading, SSR, error boundaries, etc.
|
|
55
55
|
- **Not vibe-coded** - Built with careful design and attention to detail by a human
|
|
56
56
|
- **Just works** - Define routes, get autocomplete everywhere
|
|
@@ -82,6 +82,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
|
|
|
82
82
|
- [Error boundaries](#error-boundaries)
|
|
83
83
|
- [Suspense boundaries](#suspense-boundaries)
|
|
84
84
|
- [Route handles](#route-handles)
|
|
85
|
+
- [Middlewares](#middlewares)
|
|
85
86
|
- [Route matching and ranking](#route-matching-and-ranking)
|
|
86
87
|
- [History implementations](#history-implementations)
|
|
87
88
|
- [Cookbook](#cookbook)
|
|
@@ -95,6 +96,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
|
|
|
95
96
|
- [API reference](#api-reference)
|
|
96
97
|
- [Router class](#router-class)
|
|
97
98
|
- [Route class](#route-class)
|
|
99
|
+
- [Middleware](#middleware)
|
|
98
100
|
- [Hooks](#hooks)
|
|
99
101
|
- [Components](#components)
|
|
100
102
|
- [History interface](#history-interface)
|
|
@@ -947,6 +949,74 @@ declare module "waymark" {
|
|
|
947
949
|
|
|
948
950
|
---
|
|
949
951
|
|
|
952
|
+
# Middlewares
|
|
953
|
+
|
|
954
|
+
Middlewares bundle reusable configuration that can be applied to multiple routes. Instead of repeating the same configuration across routes, you define it once in a middleware and apply it wherever needed.
|
|
955
|
+
|
|
956
|
+
Create middleware with the `middleware()` function. It returns a middleware object that supports the same builder methods as routes, except `.route()`.
|
|
957
|
+
|
|
958
|
+
```tsx
|
|
959
|
+
import { middleware } from "waymark";
|
|
960
|
+
|
|
961
|
+
const pagination = middleware().search(
|
|
962
|
+
z.object({
|
|
963
|
+
page: z.coerce.number().catch(1),
|
|
964
|
+
limit: z.coerce.number().catch(10)
|
|
965
|
+
})
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
const auth = middleware()
|
|
969
|
+
.handle({ requiresAuth: true })
|
|
970
|
+
.component(AuthRedirect);
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
Here, `pagination` validates pagination search params, and `auth` marks routes as protected via a handle and wraps them in a component that redirects unauthenticated users.
|
|
974
|
+
|
|
975
|
+
Apply middleware to a route with the `.use()` method:
|
|
976
|
+
|
|
977
|
+
```tsx
|
|
978
|
+
const userPage = route("/users").use(pagination).component(UserPage);
|
|
979
|
+
|
|
980
|
+
function UserPage() {
|
|
981
|
+
const [search] = useSearch(userPage);
|
|
982
|
+
// search.page: number
|
|
983
|
+
// search.limit: number
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
The middleware's configuration merges into the route - here, the route gets typed and validated `page` and `limit` search params. You can apply multiple middlewares to the same route:
|
|
988
|
+
|
|
989
|
+
```tsx
|
|
990
|
+
route("/users").use(auth).use(pagination).component(UserPage);
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Middlewares can also use other middlewares:
|
|
994
|
+
|
|
995
|
+
```tsx
|
|
996
|
+
const filter = middleware()
|
|
997
|
+
.use(pagination)
|
|
998
|
+
.search(
|
|
999
|
+
z.object({
|
|
1000
|
+
status: z.enum(["active", "archived", "all"]).catch("all")
|
|
1001
|
+
})
|
|
1002
|
+
);
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
Any route using `filter` gets pagination and filtering by status combined:
|
|
1006
|
+
|
|
1007
|
+
```tsx
|
|
1008
|
+
const userPage = route("/users").use(filter).component(UserPage);
|
|
1009
|
+
|
|
1010
|
+
function UserPage() {
|
|
1011
|
+
const [search] = useSearch(userPage);
|
|
1012
|
+
// search.page: number
|
|
1013
|
+
// search.limit: number
|
|
1014
|
+
// search.status: "active" | "archived" | "all"
|
|
1015
|
+
}
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
950
1020
|
# Route matching and ranking
|
|
951
1021
|
|
|
952
1022
|
When a user navigates to a URL, Waymark needs to determine which route matches. Since multiple routes can potentially match the same path (think `/users/:id` vs `/users/new`), Waymark uses a ranking algorithm to pick the most specific one.
|
|
@@ -1402,6 +1472,16 @@ const userSettings = user.route("/settings");
|
|
|
1402
1472
|
// Pattern becomes "/users/:id/settings"
|
|
1403
1473
|
```
|
|
1404
1474
|
|
|
1475
|
+
**`.use(middleware)`** applies a middleware to the route, merging its configuration.
|
|
1476
|
+
|
|
1477
|
+
- `middleware` - `Middleware` - A middleware object
|
|
1478
|
+
- Returns: `Route` - A new route object
|
|
1479
|
+
|
|
1480
|
+
```tsx
|
|
1481
|
+
const auth = middleware().component(AuthRedirect);
|
|
1482
|
+
const dashboard = route("/dashboard").use(auth).component(Dashboard);
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1405
1485
|
**`.component(component)`** adds a component to render when this route matches.
|
|
1406
1486
|
|
|
1407
1487
|
- `component` - `ComponentType` - A React component
|
|
@@ -1478,6 +1558,23 @@ const user = route("/users/:id")
|
|
|
1478
1558
|
});
|
|
1479
1559
|
```
|
|
1480
1560
|
|
|
1561
|
+
## Middleware
|
|
1562
|
+
|
|
1563
|
+
**`middleware()`** creates a new middleware.
|
|
1564
|
+
|
|
1565
|
+
- Returns: `Middleware` - A new middleware object
|
|
1566
|
+
|
|
1567
|
+
```tsx
|
|
1568
|
+
const paginated = middleware().search(
|
|
1569
|
+
z.object({ page: z.number(), limit: z.number() })
|
|
1570
|
+
);
|
|
1571
|
+
const auth = middleware()
|
|
1572
|
+
.handle({ requiresAuth: true })
|
|
1573
|
+
.component(AuthRedirect);
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
Middlewares support all the same builder methods as `Route` except `.route()`. See the [Route class](#route-class) documentation above for details on each method.
|
|
1577
|
+
|
|
1481
1578
|
## Hooks
|
|
1482
1579
|
|
|
1483
1580
|
**`useRouter()`** returns the Router instance from context.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import * as react2 from "react";
|
|
2
2
|
import { AnchorHTMLAttributes, CSSProperties, ComponentType, ReactNode, RefAttributes } from "react";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { EmptyObject } from "type-fest";
|
|
3
|
+
import { RouteParams } from "regexparam";
|
|
4
|
+
import { EmptyObject, Merge, Simplify } from "type-fest";
|
|
6
5
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
7
6
|
|
|
8
7
|
//#region src/utils/types.d.ts
|
|
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
12
|
type MaybeKey<K extends string, T> = T extends EmptyObject ? { [P in K]?: EmptyObject } : {} extends T ? { [P in K]?: T } : { [P in K]: T };
|
|
13
|
+
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] }>;
|
|
13
14
|
//#endregion
|
|
14
15
|
//#region src/types.d.ts
|
|
15
16
|
interface Register {}
|
|
@@ -19,9 +20,22 @@ type RouteList = Register extends {
|
|
|
19
20
|
type Handle = Register extends {
|
|
20
21
|
handle: infer Handle;
|
|
21
22
|
} ? Handle : any;
|
|
22
|
-
interface
|
|
23
|
-
|
|
24
|
-
search:
|
|
23
|
+
interface Middleware<S extends {} = any> {
|
|
24
|
+
use: <S2 extends {}>(middleware: Middleware<S2>) => Middleware<Merge<S, OptionalOnUndefined<S2>>>;
|
|
25
|
+
search: <S2 extends {}>(validate: Validator<S, S2>) => Middleware<Merge<S, OptionalOnUndefined<S2>>>;
|
|
26
|
+
handle: (handle: Handle) => Middleware<S>;
|
|
27
|
+
preload: (preload: (context: PreloadContext<{}, S>) => Promise<any>) => Middleware<S>;
|
|
28
|
+
component: (component: ComponentType) => Middleware<S>;
|
|
29
|
+
lazy: (loader: ComponentLoader) => Middleware<S>;
|
|
30
|
+
suspense: (fallback: ComponentType) => Middleware<S>;
|
|
31
|
+
error: (fallback: ComponentType<{
|
|
32
|
+
error: unknown;
|
|
33
|
+
}>) => Middleware<S>;
|
|
34
|
+
}
|
|
35
|
+
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> {
|
|
37
|
+
params: Ps;
|
|
38
|
+
search: S;
|
|
25
39
|
}
|
|
26
40
|
interface RouterOptions {
|
|
27
41
|
basePath?: string;
|
|
@@ -30,9 +44,11 @@ interface RouterOptions {
|
|
|
30
44
|
ssrContext?: SSRContext;
|
|
31
45
|
defaultLinkOptions?: LinkOptions;
|
|
32
46
|
}
|
|
33
|
-
type Pattern = RouteList[number]["pattern"];
|
|
47
|
+
type Pattern = RouteList[number]["_"]["pattern"];
|
|
34
48
|
type GetRoute<P extends Pattern> = Extract<RouteList[number], {
|
|
35
|
-
|
|
49
|
+
_: {
|
|
50
|
+
pattern: P;
|
|
51
|
+
};
|
|
36
52
|
}>;
|
|
37
53
|
type Params<P extends Pattern> = GetRoute<P>["_types"]["params"];
|
|
38
54
|
type Search<P extends Pattern> = GetRoute<P>["_types"]["search"];
|
|
@@ -82,14 +98,11 @@ type ComponentLoader = () => Promise<ComponentType | {
|
|
|
82
98
|
}>;
|
|
83
99
|
//#endregion
|
|
84
100
|
//#region src/route.d.ts
|
|
85
|
-
declare function route<P extends string>(pattern: P): Route<NormalizePath<P>,
|
|
86
|
-
declare
|
|
87
|
-
|
|
88
|
-
readonly _types: {
|
|
89
|
-
params: Ps;
|
|
90
|
-
search: S;
|
|
91
|
-
};
|
|
101
|
+
declare function route<P extends string>(pattern: P): Route<NormalizePath<P>, ParsePattern<NormalizePath<P>>, {}>;
|
|
102
|
+
declare function middleware(): Middleware<{}>;
|
|
103
|
+
declare class Route<P extends string = string, Ps extends {} = any, S extends {} = any> implements Middleware<S> {
|
|
92
104
|
readonly _: {
|
|
105
|
+
pattern: P;
|
|
93
106
|
keys: string[];
|
|
94
107
|
regex: RegExp;
|
|
95
108
|
looseRegex: RegExp;
|
|
@@ -99,11 +112,16 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
99
112
|
components: ComponentType[];
|
|
100
113
|
preloads: ((context: PreloadContext) => Promise<any>)[];
|
|
101
114
|
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
readonly _types: {
|
|
116
|
+
params: Ps;
|
|
117
|
+
search: S;
|
|
118
|
+
};
|
|
119
|
+
constructor(_: typeof this._);
|
|
120
|
+
route: <P2 extends string>(pattern: P2) => Route<NormalizePath<`${P}/${P2}`>, ParsePattern<NormalizePath<`${P}/${P2}`>>, S>;
|
|
121
|
+
use: <S2 extends {}>(middleware: Middleware<S2>) => Route<P, Ps, Merge<S, OptionalOnUndefined<S2>>>;
|
|
122
|
+
search: <S2 extends {}>(validate: Validator<S, S2>) => Route<P, Ps, Merge<S, OptionalOnUndefined<S2>>>;
|
|
105
123
|
handle: (handle: Handle) => Route<P, Ps, S>;
|
|
106
|
-
preload: (preload: (context: PreloadContext<
|
|
124
|
+
preload: (preload: (context: PreloadContext<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
|
|
107
125
|
component: (component: ComponentType) => Route<P, Ps, S>;
|
|
108
126
|
lazy: (loader: ComponentLoader) => Route<P, Ps, S>;
|
|
109
127
|
suspense: (fallback: ComponentType) => Route<P, Ps, S>;
|
|
@@ -200,4 +218,4 @@ declare const RouterContext: react2.Context<Router | null>;
|
|
|
200
218
|
declare const MatchContext: react2.Context<Match | null>;
|
|
201
219
|
declare const OutletContext: react2.Context<ReactNode>;
|
|
202
220
|
//#endregion
|
|
203
|
-
export { BrowserHistory, ComponentLoader, GetRoute, Handle, HashHistory, HistoryLike, HistoryPushOptions, Link, LinkOptions, LinkProps, Match, MatchContext, MatchOptions, MemoryHistory, Navigate, NavigateOptions, NavigateProps, Outlet, OutletContext, Params, Pattern, PreloadContext, Register, Route, RouteList, Router, RouterContext, RouterOptions, RouterRoot, RouterRootProps, SSRContext, Search, Updater, route, useHandles, useLocation, useMatch, useNavigate, useOutlet, useParams, useRouter, useSearch, useSubscribe };
|
|
221
|
+
export { BrowserHistory, ComponentLoader, GetRoute, Handle, HashHistory, HistoryLike, HistoryPushOptions, Link, LinkOptions, LinkProps, 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, useSubscribe };
|
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{keys:t,regex:n,looseRegex:_(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(`[Waymark] Validation must be synchronous`);if(n.issues)throw Error(`[Waymark] 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(e,t,n,r){let i=e.exec(D(n,r));if(!i)return null;let a={};return t.forEach((e,t)=>{let n=i[t+1];n&&(a[e]=n)}),a}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);function F(){let e=c(M);if(!e)throw Error(`[Waymark] useRouter must be used within a router context`);return e}function I(){return F().navigate}function L(){let e=F(),t=U(e,e.history.getPath),n=U(e,e.history.getSearch),r=U(e,e.history.getState);return f(()=>({path:t,search:n,state:r}),[t,n,r])}function R(){return c(P)}function z(e){let t=V({from:e});if(!t)throw Error(`[Waymark] Can't read params for non-matching route: ${e}`);return t.params}function B(e){let t=F(),n=t.getRoute(e),r=U(t,t.history.getSearch),i=f(()=>n._.validate(r),[n,r]);return[i,
|
|
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,looseRegex:_(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(`[Waymark] Validation must be synchronous`);if(n.issues)throw Error(`[Waymark] 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(e,t,n,r){let i=e.exec(D(n,r));if(!i)return null;let a={};return t.forEach((e,t)=>{let n=i[t+1];n&&(a[e]=n)}),a}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);function F(){let e=c(M);if(!e)throw Error(`[Waymark] useRouter must be used within a router context`);return e}function I(){return F().navigate}function L(){let e=F(),t=U(e,e.history.getPath),n=U(e,e.history.getSearch),r=U(e,e.history.getState);return f(()=>({path:t,search:n,state:r}),[t,n,r])}function R(){return c(P)}function z(e){let t=V({from:e});if(!t)throw Error(`[Waymark] Can't read params for non-matching route: ${e}`);return t.params}function B(e){let t=F(),n=t.getRoute(e),r=U(t,t.history.getSearch),i=f(()=>n._.validate(r),[n,r]);return[i,Z((e,n)=>{e=typeof e==`function`?e(i):e;let r={...i,...e},a=O(t.history.getPath(),r);t.navigate({url:a,replace:n})})]}function V(e){let t=F(),n=U(t,t.history.getPath);return f(()=>t.match(n,e),[t,n,e])}function H(){let e=c(N);return f(()=>e?.route._.handles??[],[e])}function U(e,t){return h(e.history.subscribe,t,t)}var W=class e{static patch=Symbol.for(`wmbhp01`);memo;constructor(){if(typeof history<`u`&&!(e.patch in window)){for(let e of[G,K]){let t=history[e];history[e]=function(...n){let r=t.apply(this,n),i=new Event(e);return i.arguments=n,dispatchEvent(i),r}}window[e.patch]=!0}}getSearchMemo=e=>this.memo?.search===e?this.memo.parsed:(this.memo={search:e,parsed:C(e)}).parsed;getPath=()=>location.pathname;getSearch=()=>this.getSearchMemo(location.search);getState=()=>history.state;go=e=>history.go(e);push=e=>{let{url:t,replace:n,state:r}=e;history[n?K:G](r,``,t)};subscribe=e=>(q.forEach(t=>window.addEventListener(t,e)),()=>{q.forEach(t=>window.removeEventListener(t,e))})};const G=`pushState`,K=`replaceState`,q=[`popstate`,G,K,`hashchange`];var J=class{basePath;routes;history;ssrContext;defaultLinkOptions;_;constructor(e){let{basePath:t=`/`,routes:n,history:r,ssrContext:i,defaultLinkOptions:a}=e;this.basePath=y(t),this.routes=n,this.history=r??new W,this.ssrContext=i,this.defaultLinkOptions=a,this._={routeMap:new Map(n.map(e=>[e._.pattern,e]))}}getRoute=e=>{if(typeof e!=`string`)return e;let t=this._.routeMap.get(e);if(!t)throw Error(`[Waymark] Route not found for pattern: ${e}`);return t};match=(e,t)=>{let{from:n,strict:r,params:i}=t,a=this.getRoute(n),o=A(r?a._.regex:a._.looseRegex,a._.keys,e,this.basePath);return!o||i&&Object.keys(i).some(e=>i[e]!==o[e])?null:{route:a,params:o}};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,i=this.getRoute(t);await Promise.all(i._.preloads.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})}}},Y=class{stack=[];index=0;listeners=new Set;constructor(e=`/`){this.stack.push({...k(e),state:void 0})}getCurrent=()=>this.stack[this.index];getPath=()=>this.getCurrent().path;getSearch=()=>this.getCurrent().search;getState=()=>this.getCurrent().state;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)})},X=class extends W{getHashUrl=()=>new URL(location.hash.slice(1),`http://w`);getPath=()=>this.getHashUrl().pathname;getSearch=()=>this.getSearchMemo(this.getHashUrl().search);push=e=>{let{url:t,replace:n,state:r}=e;history[n?`replaceState`:`pushState`](r,``,`#${t}`)}};function ee(e){let[t]=m(()=>`router`in e?e.router:new J(e)),n=U(t,t.history.getPath),r=f(()=>t.matchAll(n),[t,n]);return r||console.error(`[Waymark] No matching route found for path:`,n),f(()=>v(M.Provider,{value:t,children:v(N.Provider,{value:r,children:r?.route._.components.reduceRight((e,t)=>v(P.Provider,{value:e,children:v(t,{})}),null)})}),[t,r])}function te(){return R()}function ne(e){let t=F();return d(()=>t.navigate(e),[]),t.ssrContext&&(t.ssrContext.redirect=t.createUrl(e)),null}function re(e){let t=F(),{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=!!V({from:e.to,strict:d,params:c}),O=Z(()=>t.preload(e)),k=s(()=>{T.current!==null&&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()},I=e=>{C.onPointerEnter?.(e),m===`intent`&&!e.defaultPrevented&&A()},L=e=>{C.onPointerLeave?.(e),m===`intent`&&k()},R={...C,...j,ref:ie(w,C.ref),href:E,onClick:M,onFocus:N,onBlur:P,onPointerEnter:I,onPointerLeave:L};return x&&i(S)?n(S,R):v(`a`,{...R,children:S})}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=p(e);return u(()=>{t.current=e},[e]),p(((...e)=>t.current(...e))).current}function ae(e){return()=>v(t,{fallback:v(e,{}),children:R()})}function oe(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:R()})}function Q(e){return new $({...b(y(e)),validate:e=>e,handles:[],components:[],preloads:[]})}function se(){return Q(``)}var $=class e{_;_types;constructor(e){this._=e}route=t=>new e({...this._,...b(y(`${this._.pattern}/${t}`))});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(ae(e));error=e=>this.component(oe(e));toString=()=>this._.pattern};export{W as BrowserHistory,X as HashHistory,re as Link,N as MatchContext,Y as MemoryHistory,ne as Navigate,te as Outlet,P as OutletContext,$ as Route,J as Router,M as RouterContext,ee as RouterRoot,se as middleware,Q as route,H as useHandles,L as useLocation,V as useMatch,I as useNavigate,R as useOutlet,z as useParams,F as useRouter,B as useSearch,U as useSubscribe};
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waymark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "strblr",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Type-safe router for React",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"type": "git",
|
|
18
18
|
"url": "git+https://github.com/strblr/waymark.git"
|
|
19
19
|
},
|
|
20
|
-
"homepage": "https://
|
|
20
|
+
"homepage": "https://waymarkrouter.com",
|
|
21
21
|
"bugs": "https://github.com/strblr/waymark/issues",
|
|
22
22
|
"keywords": [
|
|
23
23
|
"react",
|
|
@@ -34,9 +34,7 @@
|
|
|
34
34
|
"search",
|
|
35
35
|
"params",
|
|
36
36
|
"simple",
|
|
37
|
-
"
|
|
38
|
-
"lightweight",
|
|
39
|
-
"minimal"
|
|
37
|
+
"lightweight"
|
|
40
38
|
],
|
|
41
39
|
"scripts": {
|
|
42
40
|
"build": "tsc --noEmit && tsdown",
|