waymark 0.3.0 → 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 CHANGED
@@ -7,15 +7,40 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- <a href="https://www.npmjs.com/package/waymark"><img src="https://img.shields.io/npm/v/waymark?style=flat-square&color=000&labelColor=000" alt="npm version" /></a>
11
- <a href="https://www.npmjs.com/package/waymark"><img src="https://img.badgesize.io/https://unpkg.com/waymark/dist/index.js?compression=gzip&label=gzip&style=flat-square&color=000&labelColor=000" alt="gzip size" /></a>
12
- <a href="https://www.npmjs.com/package/waymark"><img src="https://img.shields.io/npm/dm/waymark?style=flat-square&color=000&labelColor=000" alt="downloads" /></a>
13
- <a href="https://github.com/strblr/waymark/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/waymark?style=flat-square&color=000&labelColor=000" alt="license" /></a>
14
- <a href="https://github.com/sponsors/strblr"><img src="https://img.shields.io/github/sponsors/strblr?style=flat-square&color=000&labelColor=000" alt="sponsors" /></a>
10
+ <a href="https://www.npmjs.com/package/waymark">
11
+ <img
12
+ src="https://img.shields.io/npm/v/waymark?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
13
+ alt="npm version"
14
+ />
15
+ </a>
16
+ <a href="https://www.npmjs.com/package/waymark">
17
+ <img
18
+ src="https://img.badgesize.io/https://unpkg.com/waymark/dist/index.js?compression=gzip&label=gzip&style=flat-square&color=0B0D0F&labelColor=0B0D0F"
19
+ alt="gzip size"
20
+ />
21
+ </a>
22
+ <a href="https://www.npmjs.com/package/waymark">
23
+ <img
24
+ src="https://img.shields.io/npm/dm/waymark?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
25
+ alt="downloads"
26
+ />
27
+ </a>
28
+ <a href="https://github.com/strblr/waymark/blob/master/LICENSE">
29
+ <img
30
+ src="https://img.shields.io/npm/l/waymark?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
31
+ alt="license"
32
+ />
33
+ </a>
34
+ <a href="https://github.com/sponsors/strblr">
35
+ <img
36
+ src="https://img.shields.io/github/sponsors/strblr?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
37
+ alt="sponsors"
38
+ />
39
+ </a>
15
40
  </p>
16
41
 
17
42
  <p align="center">
18
- <a href="https://waymark.strblr.workers.dev">📖 Documentation</a>
43
+ <a href="https://waymarkrouter.com">📖 Documentation</a>
19
44
  </p>
20
45
 
21
46
  ---
@@ -23,9 +48,10 @@
23
48
  Waymark is a routing library for React built around three core ideas: **type safety**, **simplicity**, and **minimal overhead**.
24
49
 
25
50
  - **Fully type-safe** - Complete TypeScript inference for routes, path params, and search params
26
- - **Zero config** - No build plugins, no CLI tools, no configuration files, very low boilerplate
51
+ - **Zero config** - No build plugins, no CLI, no codegen, no config files, very low boilerplate
27
52
  - **Familiar API** - If you've used React Router or TanStack Router, you'll feel at home
28
- - **3.6kB gzipped** - Extremely lightweight with just one 0.4kB dependency, so around 4kB total
53
+ - **3.7kB gzipped** - Extremely lightweight with just one 0.4kB dependency, so ~4kB total
54
+ - **Feature packed** - Search param validation, lazy loading, data preloading, SSR, error boundaries, etc.
29
55
  - **Not vibe-coded** - Built with careful design and attention to detail by a human
30
56
  - **Just works** - Define routes, get autocomplete everywhere
31
57
 
@@ -56,6 +82,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
56
82
  - [Error boundaries](#error-boundaries)
57
83
  - [Suspense boundaries](#suspense-boundaries)
58
84
  - [Route handles](#route-handles)
85
+ - [Middlewares](#middlewares)
59
86
  - [Route matching and ranking](#route-matching-and-ranking)
60
87
  - [History implementations](#history-implementations)
61
88
  - [Cookbook](#cookbook)
@@ -69,6 +96,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
69
96
  - [API reference](#api-reference)
70
97
  - [Router class](#router-class)
71
98
  - [Route class](#route-class)
99
+ - [Middleware](#middleware)
72
100
  - [Hooks](#hooks)
73
101
  - [Components](#components)
74
102
  - [History interface](#history-interface)
@@ -80,34 +108,39 @@ Waymark is a routing library for React built around three core ideas: **type saf
80
108
 
81
109
  # Showcase
82
110
 
83
- Here's what a small routing setup looks like. Define some routes, render them, and get full type safety:
111
+ Here's what routing looks like with Waymark:
84
112
 
85
113
  ```tsx
86
- import { route, RouterRoot, Link, useParams } from "waymark";
114
+ import { route, RouterRoot, Outlet, Link, useParams } from "waymark";
87
115
 
88
- // Define routes
89
- const home = route("/").component(() => <h1>Home</h1>);
116
+ // Layout
117
+ const layout = route("/").component(() => (
118
+ <div>
119
+ <nav>
120
+ <Link to="/">Home</Link>
121
+ <Link to="/users/:id" params={{ id: "42" }}>
122
+ User
123
+ </Link>
124
+ </nav>
125
+ <Outlet />
126
+ </div>
127
+ ));
90
128
 
91
- const user = route("/users/:id").component(UserPage);
129
+ // Pages
130
+ const home = layout.route("/").component(() => <h1>Home</h1>);
92
131
 
93
- function UserPage() {
132
+ const user = layout.route("/users/:id").component(function UserPage() {
94
133
  const { id } = useParams(user); // Fully typed
95
- return (
96
- <div>
97
- <h1>User {id}</h1>
98
- <Link to="/">Back to home</Link> {/* Also fully typed */}
99
- </div>
100
- );
101
- }
134
+ return <h1>User {id}</h1>;
135
+ });
102
136
 
103
- // Render
137
+ // Setup
104
138
  const routes = [home, user];
105
139
 
106
140
  function App() {
107
141
  return <RouterRoot routes={routes} />;
108
142
  }
109
143
 
110
- // Register for type safety
111
144
  declare module "waymark" {
112
145
  interface Register {
113
146
  routes: typeof routes;
@@ -115,7 +148,7 @@ declare module "waymark" {
115
148
  }
116
149
  ```
117
150
 
118
- Links, navigation, path params, search params - everything autocompletes and type-checks automatically. That's it. No config files, no build plugins, no CLI.
151
+ Everything autocompletes and type-checks automatically. No heavy setup, no magic, just a simple API that gets out of your way.
119
152
 
120
153
  ---
121
154
 
@@ -916,6 +949,74 @@ declare module "waymark" {
916
949
 
917
950
  ---
918
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
+
919
1020
  # Route matching and ranking
920
1021
 
921
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.
@@ -1371,6 +1472,16 @@ const userSettings = user.route("/settings");
1371
1472
  // Pattern becomes "/users/:id/settings"
1372
1473
  ```
1373
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
+
1374
1485
  **`.component(component)`** adds a component to render when this route matches.
1375
1486
 
1376
1487
  - `component` - `ComponentType` - A React component
@@ -1447,6 +1558,23 @@ const user = route("/users/:id")
1447
1558
  });
1448
1559
  ```
1449
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
+
1450
1578
  ## Hooks
1451
1579
 
1452
1580
  **`useRouter()`** returns the Router instance from context.
@@ -1733,6 +1861,7 @@ interface PreloadContext {
1733
1861
  - Possibility to pass an arbitrary context to the Router instance for later use in preloads?
1734
1862
  - Relative path navigation? Not sure it's indispensable given that users can export/import route objects and pass them as navigation option.
1735
1863
  - Document usage in test environments
1864
+ - Devtools? Let me know if needed.
1736
1865
  - Open to suggestions, we can discuss them [here](https://github.com/strblr/waymark/discussions).
1737
1866
 
1738
1867
  ---
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 * as regexparam0 from "regexparam";
4
- import * as type_fest0 from "type-fest";
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 PreloadContext<R extends Route = Route> {
23
- params: R["_types"]["params"];
24
- search: R["_types"]["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
- pattern: P;
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>, regexparam0.RouteParams<NormalizePath<P>> extends infer T ? { [KeyType in keyof T]: T[KeyType] } : never, {}>;
86
- declare class Route<P extends string = string, Ps extends {} = any, S extends {} = any> {
87
- readonly pattern: P;
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
- constructor(pattern: P, _: typeof this._);
103
- route: <P2 extends string>(pattern: P2) => Route<NormalizePath<`${P}/${P2}`>, regexparam0.RouteParams<NormalizePath<`${P}/${P2}`>> extends infer T ? { [KeyType in keyof T]: T[KeyType] } : never, S>;
104
- search: <S2 extends {}>(validate: ((search: S & Record<string, unknown>) => S2) | StandardSchemaV1<Record<string, unknown>, S2>) => Route<P, Ps, (type_fest0.PickIndexSignature<S> extends infer T_1 ? { [Key in keyof T_1 as Key extends keyof type_fest0.PickIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K] } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined } extends infer T_2 ? { [KeyType_1 in keyof T_2]: T_2[KeyType_1] } : never> ? never : Key]: T_1[Key] } : never) & type_fest0.PickIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K] } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined } extends infer T_2 ? { [KeyType_1 in keyof T_2]: T_2[KeyType_1] } : never> & (type_fest0.OmitIndexSignature<S> extends infer T_3 ? { [Key_1 in keyof T_3 as Key_1 extends keyof type_fest0.OmitIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K] } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined } extends infer T_4 ? { [KeyType_1 in keyof T_4]: T_4[KeyType_1] } : never> ? never : Key_1]: T_3[Key_1] } : never) & type_fest0.OmitIndexSignature<{ [K in keyof S2 as undefined extends S2[K] ? never : K]: S2[K] } & { [K_1 in keyof S2 as undefined extends S2[K_1] ? K_1 : never]?: S2[K_1] | undefined } extends infer T_4 ? { [KeyType_1 in keyof T_4]: T_4[KeyType_1] } : never> extends infer T ? { [KeyType in keyof T]: T[KeyType] } : never>;
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<this>) => Promise<any>) => Route<P, Ps, S>;
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,Q((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 Z(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 ee(){return R()}function te(e){let t=F();return d(()=>t.navigate(e),[]),t.ssrContext&&(t.ssrContext.redirect=t.createUrl(e)),null}function ne(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=Q(()=>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:re(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 re(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 Q(e){let t=p(e);return u(()=>{t.current=e},[e]),p(((...e)=>t.current(...e))).current}function ie(e){return()=>v(t,{fallback:v(e,{}),children:R()})}function ae(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 oe(e){let t=y(e);return new $(t,{...b(t),validate:e=>e,handles:[],components:[],preloads:[]})}var $=class e{pattern;_types;_;constructor(e,t){this.pattern=e,this._=t}route=t=>{let n=y(`${this.pattern}/${t}`);return new e(n,{...this._,...b(n)})};search=t=>(t=x(t),new e(this.pattern,{...this._,validate:e=>{let n=this._.validate(e);return{...n,...t({...e,...n})}}}));handle=t=>new e(this.pattern,{...this._,handles:[...this._.handles,t]});preload=t=>new e(this.pattern,{...this._,preloads:[...this._.preloads,e=>t({params:e.params,search:this._.validate(e.search)})]});component=t=>new e(this.pattern,{...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(ie(e));error=e=>this.component(ae(e));toString=()=>this.pattern};export{W as BrowserHistory,X as HashHistory,ne as Link,N as MatchContext,Y as MemoryHistory,te as Navigate,ee as Outlet,P as OutletContext,$ as Route,J as Router,M as RouterContext,Z as RouterRoot,oe 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};
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.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "author": "strblr",
6
- "description": "Lightweight type-safe router for React",
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://waymark.strblr.workers.dev",
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
- "tiny",
38
- "lightweight",
39
- "minimal"
37
+ "lightweight"
40
38
  ],
41
39
  "scripts": {
42
40
  "build": "tsc --noEmit && tsdown",