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 +153 -24
- package/dist/index.d.ts +38 -20
- package/dist/index.js +1 -1
- package/package.json +4 -6
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"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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://
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
89
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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",
|