waymark 0.4.0 โ†’ 0.6.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
@@ -3,10 +3,10 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- A lightweight, type-safe router for React that just works.
6
+ A type-safe router for React that just works.
7
7
  </p>
8
8
 
9
- <p align="center">
9
+ <div align="center">
10
10
  <a href="https://www.npmjs.com/package/waymark">
11
11
  <img
12
12
  src="https://img.shields.io/npm/v/waymark?style=flat-square&color=0B0D0F&labelColor=0B0D0F"
@@ -37,28 +37,75 @@
37
37
  alt="sponsors"
38
38
  />
39
39
  </a>
40
- </p>
40
+ </div>
41
41
 
42
42
  <p align="center">
43
- <a href="https://waymarkrouter.com">๐Ÿ“– Documentation</a>
43
+ ๐Ÿ“– <a href="https://waymarkrouter.com">Documentation</a> ยท ๐ŸŽฎ <a href="https://stackblitz.com/edit/waymark-demo?file=src%2Fapp.tsx">Live playground</a>
44
44
  </p>
45
45
 
46
46
  ---
47
47
 
48
48
  Waymark is a routing library for React built around three core ideas: **type safety**, **simplicity**, and **minimal overhead**.
49
49
 
50
- - **Fully type-safe** - Complete TypeScript inference for routes, path params, and search params
51
- - **Zero config** - No build plugins, no CLI, no codegen, no config files, very low boilerplate
52
- - **Familiar API** - If you've used React Router or TanStack Router, you'll feel at home
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.
55
- - **Not vibe-coded** - Built with careful design and attention to detail by a human
56
- - **Just works** - Define routes, get autocomplete everywhere
50
+ - ๐Ÿ”’ **Fully type-safe** - Complete TypeScript inference for routes, path params, search params, and more
51
+ - โšก **Zero config** - No build plugins, no CLI, no codegen, no config files, very low boilerplate
52
+ - ๐Ÿชถ **4kB gzipped** - Extremely lightweight, dependency included
53
+ - ๐Ÿค **Familiar API** - If you've used React Router or TanStack Router, you'll feel at home
54
+ - ๐ŸŽฏ **Feature packed** - Search param validation, lazy loading, data preloading, SSR, error boundaries, etc.
55
+ - ๐Ÿง  **Not vibe-coded** - Built with careful design and attention to detail by a human
56
+ - โœจ **Just works** - Simple setup, predictable behavior that never gets in your way
57
+
58
+ ---
59
+
60
+ # Comparison
61
+
62
+ | Feature | Waymark | React Router | TanStack Router | Wouter |
63
+ | -------------------------------- | :-----: | :----------: | :-------------: | :----: |
64
+ | **Bundle size (gzip)**\* | ~4kB | ~26kB+ | ~19kB+ | ~2.2kB |
65
+ | **Zero config**\* | โœ… | โŒ | โš ๏ธ | โœ… |
66
+ | **Full type inference**\* | โœ… | โš ๏ธ | โœ… | โŒ |
67
+ | **Nested routes** | โœ… | โœ… | โœ… | โœ… |
68
+ | **Search param validation**\* | โœ… | โŒ | โœ… | โŒ |
69
+ | **Lazy loading** | โœ… | โœ… | โœ… | โŒ |
70
+ | **Data preloading** | โœ… | โœ… | โœ… | โŒ |
71
+ | **Built-in error boundaries** | โœ… | โœ… | โœ… | โŒ |
72
+ | **Built-in suspense boundaries** | โœ… | โŒ | โœ… | โŒ |
73
+ | **Link preloading strategies** | โœ… | โœ… | โœ… | โŒ |
74
+ | **Active link detection** | โœ… | โœ… | โœ… | โš ๏ธ |
75
+ | **Browser/Hash/Memory history** | โœ… | โœ… | โœ… | โœ… |
76
+ | **SSR support** | โœ… | โœ… | โœ… | โœ… |
77
+ | **Route middlewares**\* | โœ… | โŒ | โŒ | โŒ |
78
+ | **Route handles (metadata)** | โœ… | โœ… | โœ… | โŒ |
79
+ | **Route match ranking**\* | โœ… | โœ… | โœ… | โŒ |
80
+ | **View transitions** | โœ… | โœ… | โœ… | โœ… |
81
+ | **Devtools** | โœ… | โš ๏ธ | โœ… | โŒ |
82
+ | **Navigation blockers** | ๐Ÿ”จ | โœ… | โœ… | โŒ |
83
+ | **File-based routing** | โŒ | โœ… | โœ… | โŒ |
84
+ | **React Native** | โŒ | โœ… | โŒ | โŒ |
85
+
86
+ <details>
87
+ <summary><b>Comparison notes</b></summary>
88
+
89
+ <br />
90
+
91
+ If you believe there's a mistake in the comparison table, please [open an issue](https://github.com/strblr/waymark/issues) or [submit a PR](https://github.com/strblr/waymark/pulls) and it will be fixed.
92
+
93
+ - โš ๏ธ indicates the feature is only partially supported, supported with heavy boilerplate, or requires external libraries.
94
+ - ๐Ÿ”จ indicates the feature is not yet ready but being worked on.
95
+ - **Bundle sizes** are approximate gzipped values. React Router and TanStack Router sizes can vary significantly based on imports and versions; Waymark's ~4kB includes its single ~0.4kB dependency ([regexparam](https://github.com/lukeed/regexparam)), before any tree shaking. Wouter is the smallest option but lacks features.
96
+ - **Zero config** means no CLI tools, build plugins, code generation, or configuration files are required. React Router requires its typegen CLI or bundler plugin for full type safety. Same with TanStack Router for file-based routing. You can use code-based routing but it's more boilerplate.
97
+ - **Full type inference** refers to automatic TypeScript inference for routes, params, search params, and navigation without manual type annotations.
98
+ - **Search params validation** refers to built-in support for validating and typing URL search parameters. Wouter provides `useSearch()` but no validation layer. Same with React Router and `useSearchParams`.
99
+ - **Route middlewares** are reusable configuration bundles (search validation, handles, preload functions, components) that can be applied to multiple routes. This is a Waymark-specific feature.
100
+ - **Route match ranking** automatically picks the most specific route when multiple patterns match (e.g., `/users/new` wins over `/users/:id`). Without ranking, route definition order matters.
101
+
102
+ </details>
57
103
 
58
104
  ---
59
105
 
60
106
  # Table of contents
61
107
 
108
+ - [Comparison](#comparison)
62
109
  - [Showcase](#showcase)
63
110
  - [Installation](#installation)
64
111
  - [Defining routes](#defining-routes)
@@ -85,6 +132,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
85
132
  - [Middlewares](#middlewares)
86
133
  - [Route matching and ranking](#route-matching-and-ranking)
87
134
  - [History implementations](#history-implementations)
135
+ - [Devtools](#devtools)
88
136
  - [Cookbook](#cookbook)
89
137
  - [Quick start example](#quick-start-example)
90
138
  - [Server-side rendering (SSR)](#server-side-rendering-ssr)
@@ -150,6 +198,8 @@ declare module "waymark" {
150
198
 
151
199
  Everything autocompletes and type-checks automatically. No heavy setup, no magic, just a simple API that gets out of your way.
152
200
 
201
+ ๐Ÿ‘‰ [Try it live in the StackBlitz playground](https://stackblitz.com/edit/waymark-demo?file=src%2Fapp.tsx)
202
+
153
203
  ---
154
204
 
155
205
  # Installation
@@ -192,31 +242,25 @@ const files = route("/files/*").component(FileBrowser);
192
242
  const optional = route("/books/*?").component(FileBrowser);
193
243
  ```
194
244
 
195
- Route building is immutable: every method on a route returns a new route instance, which means you can branch off at any point to create variations or nested routes without affecting the original.
245
+ Route building is immutable: every method on a route returns a new route instance.
196
246
 
197
247
  ---
198
248
 
199
249
  # Nested routes and layouts
200
250
 
201
- Nesting is the core mechanism for building layouts and route hierarchies in Waymark. When you call `.route()` on an existing route, you create a child route that inherits everything from the parent: its path as a prefix, its params, its components, etc.
202
-
203
- Here's how it works. Start with any route:
251
+ Any route can have child routes. Call `.route()` on an existing route to create one:
204
252
 
205
253
  ```tsx
206
254
  const dashboard = route("/dashboard").component(DashboardLayout);
207
- ```
208
255
 
209
- Then create child routes by calling `.route()` on it:
210
-
211
- ```tsx
212
256
  const overview = dashboard.route("/").component(Overview);
213
257
  const settings = dashboard.route("/settings").component(Settings);
214
258
  const profile = dashboard.route("/profile").component(Profile);
215
259
  ```
216
260
 
217
- The child routes combine the parent's path pattern with their own. So `overview` has the full pattern `/dashboard`, `settings` has `/dashboard/settings`, and `profile` has `/dashboard/profile`.
261
+ Child routes build on their parent's path. So `overview` matches `/dashboard`, `settings` matches `/dashboard/settings`, and `profile` matches `/dashboard/profile`.
218
262
 
219
- The parent component must render an `<Outlet />` where child routes should appear:
263
+ They also nest inside the parent's component. The parent renders an `<Outlet />` to mark where child routes should appears:
220
264
 
221
265
  ```tsx
222
266
  function DashboardLayout() {
@@ -231,7 +275,7 @@ function DashboardLayout() {
231
275
  }
232
276
  ```
233
277
 
234
- When the URL is `/dashboard/settings`, Waymark renders `DashboardLayout` with `Settings` inside the outlet. The layout stays mounted (and doesn't even rerender) as users navigate between child routes.
278
+ When the URL is `/dashboard/settings`, Waymark renders `DashboardLayout` with `Settings` inside the outlet. This is how you build layouts - shared UI like navigation or sidebars that stays mounted as users navigate between child routes.
235
279
 
236
280
  You can nest as deep as you need:
237
281
 
@@ -253,6 +297,8 @@ AppShell
253
297
 
254
298
  Each level must include an `<Outlet />` to render the next level.
255
299
 
300
+ Beyond paths and components, child routes also inherit search param validators, handles, and preload functions from their parent chain. While you can think of nesting as building a tree, every route is self-contained: it carries everything it needs to render, including all parent components.
301
+
256
302
  ---
257
303
 
258
304
  # Setting up the router
@@ -319,7 +365,9 @@ declare module "waymark" {
319
365
  }
320
366
  ```
321
367
 
322
- With this in place, `Link`, `navigate`, `useParams`, `useSearch`, and other APIs will know exactly which routes exist and what input they expect, and you're good to go.
368
+ With this in place, `Link`, `navigate`, `useParams`, `useSearch`, and other APIs will know exactly which routes exist and what input they expect.
369
+
370
+ **You're all set up!**
323
371
 
324
372
  ---
325
373
 
@@ -459,6 +507,9 @@ function SearchPage() {
459
507
  const [search, setSearch] = useSearch(searchPage);
460
508
  // search.q: string
461
509
  // search.page: number
510
+
511
+ const [search, setSearch] = useSearch("/search");
512
+ // Also works
462
513
  }
463
514
  ```
464
515
 
@@ -1015,6 +1066,16 @@ function UserPage() {
1015
1066
  }
1016
1067
  ```
1017
1068
 
1069
+ For parametrized middlewares, define a function that returns a middleware:
1070
+
1071
+ ```tsx
1072
+ const guard = (role: string) =>
1073
+ middleware().handle({ requiredRole: role }).component(RoleGuard);
1074
+
1075
+ const adminPage = route("/admin").use(guard("admin")).component(AdminPage);
1076
+ const editorPage = route("/editor").use(guard("editor")).component(EditorPage);
1077
+ ```
1078
+
1018
1079
  ---
1019
1080
 
1020
1081
  # Route matching and ranking
@@ -1103,6 +1164,53 @@ All history implementations conform to the `HistoryLike` interface, so you can c
1103
1164
 
1104
1165
  ---
1105
1166
 
1167
+ # Devtools
1168
+
1169
+ Waymark has a companion devtools package for inspecting routes, matches, parameters, and navigation state.
1170
+
1171
+ ```bash
1172
+ npm install waymark-devtools
1173
+ ```
1174
+
1175
+ Render the `Devtools` component anywhere inside your routes. It displays a toggle button that opens a draggable and resizable floating panel:
1176
+
1177
+ ```tsx
1178
+ import { Devtools } from "waymark-devtools";
1179
+
1180
+ const layout = route("/").component(Layout);
1181
+
1182
+ function Layout() {
1183
+ return (
1184
+ <div>
1185
+ <Outlet />
1186
+ <Devtools />
1187
+ </div>
1188
+ );
1189
+ }
1190
+ ```
1191
+
1192
+ If you'd rather embed the panel directly into your layout instead of using the floating window, use `DevtoolsPanel`:
1193
+
1194
+ ```tsx
1195
+ import { DevtoolsPanel } from "waymark-devtools";
1196
+
1197
+ function DebugSidebar() {
1198
+ return (
1199
+ <aside>
1200
+ <DevtoolsPanel />
1201
+ </aside>
1202
+ );
1203
+ }
1204
+ ```
1205
+
1206
+ To exclude devtools from production builds (Vite example):
1207
+
1208
+ ```tsx
1209
+ import.meta.env.DEV && <Devtools />;
1210
+ ```
1211
+
1212
+ ---
1213
+
1106
1214
  # Cookbook
1107
1215
 
1108
1216
  ## Quick start example
@@ -1219,27 +1327,24 @@ function AppLayout() {
1219
1327
 
1220
1328
  Use `useMatch` to check if a route matches the current path from anywhere in your component tree. You can pass either a route pattern string or a route object, just like with `Link` and `navigate`. This is useful for conditional rendering, styling, access control, and more. It's also used internally by `useParams` and `Link`.
1221
1329
 
1222
- By default, `useMatch` uses loose matching where the current path only needs to start with the route's path. To require an exact match instead, pass `strict: true`:
1330
+ The hook returns a Match object (containing `route` and `params`) if there's a match, or `null` otherwise. There are two matching modes:
1331
+
1332
+ - **Loose matching** (default): Matches if the path starts with the route pattern (e.g., `/dashboard` matches `/dashboard/settings`).
1333
+ - **Strict matching** (`strict: true`): Matches only if the path exactly matches the route pattern.
1223
1334
 
1224
1335
  ```tsx
1225
1336
  import { useMatch } from "waymark";
1226
1337
 
1227
1338
  const dashboard = route("/dashboard").component(Dashboard);
1228
- const settings = route("/settings").component(Settings);
1229
1339
 
1230
1340
  function Sidebar() {
1231
- // Loose matching: matches /dashboard and /dashboard/literally/anything
1232
- const dashboardMatch = useMatch({ from: "/dashboard" });
1341
+ // Matches /dashboard, /dashboard/anything, etc.
1342
+ const match = useMatch({ from: dashboard });
1233
1343
 
1234
- // Strict matching: matches only /settings
1235
- const settingsMatch = useMatch({ from: settings, strict: true });
1344
+ // Matches only /dashboard
1345
+ const match = useMatch({ from: dashboard, strict: true });
1236
1346
 
1237
- return (
1238
- <nav>
1239
- {dashboardMatch && <DashboardMenu />}
1240
- {settingsMatch && <SettingsSubmenu />}
1241
- </nav>
1242
- );
1347
+ return <nav>{match && <DashboardMenu />}</nav>;
1243
1348
  }
1244
1349
  ```
1245
1350
 
@@ -1415,7 +1520,7 @@ const url = router.createUrl({ to: userProfile, params: { id: "42" } });
1415
1520
 
1416
1521
  ```tsx
1417
1522
  const match = router.match("/users/42", { from: "/users/:id" });
1418
- // Returns { route, params: { id: "42" } } or null
1523
+ // Returns { route, params: { id: "42" } }
1419
1524
  ```
1420
1525
 
1421
1526
  **`router.matchAll(path)`** finds the best match from all registered routes.
@@ -1565,8 +1670,11 @@ const user = route("/users/:id")
1565
1670
  - Returns: `Middleware` - A new middleware object
1566
1671
 
1567
1672
  ```tsx
1568
- const paginated = middleware().search(
1569
- z.object({ page: z.number(), limit: z.number() })
1673
+ const pagination = middleware().search(
1674
+ z.object({
1675
+ page: z.coerce.number().catch(1),
1676
+ limit: z.coerce.number().catch(10)
1677
+ })
1570
1678
  );
1571
1679
  const auth = middleware()
1572
1680
  .handle({ requiresAuth: true })
@@ -1597,7 +1705,7 @@ navigate(-1);
1597
1705
 
1598
1706
  **`useLocation()`** returns the current location, subscribes to changes.
1599
1707
 
1600
- - Returns: `{ path: string, search: Record<string, unknown>, state: any }` - The current path, parsed search params, and history state
1708
+ - Returns: `HistoryLocation` - The current location with path, parsed search params, and history state
1601
1709
 
1602
1710
  ```tsx
1603
1711
  const { path, search, state } = useLocation();
@@ -1706,31 +1814,15 @@ new MemoryHistory("/initial"); // In-memory only.
1706
1814
 
1707
1815
  See [History implementations](#history-implementations) for detailed usage.
1708
1816
 
1709
- **`history.getPath()`** returns the current path.
1710
-
1711
- - Returns: `string` - The current path
1712
-
1713
- ```tsx
1714
- const path = history.getPath();
1715
- // Returns "/users/42"
1716
- ```
1817
+ **`history.location()`** returns the current location.
1717
1818
 
1718
- **`history.getSearch()`** returns the current search params as a parsed JSON object.
1719
-
1720
- - Returns: `Record<string, unknown>` - The parsed search params
1819
+ - Returns: `HistoryLocation` - The current location with path, parsed search params, and history state
1721
1820
 
1722
1821
  ```tsx
1723
- const search = history.getSearch();
1724
- // Returns { tab: "posts", page: 2 }
1725
- ```
1726
-
1727
- **`history.getState()`** returns the current history state.
1728
-
1729
- - Returns: `any` - The state passed during navigation, or undefined
1730
-
1731
- ```tsx
1732
- const state = history.getState();
1733
- // Returns any state passed during navigation
1822
+ const { path, search, state } = history.location();
1823
+ // path: "/users/42"
1824
+ // search: { tab: "posts", page: 2 }
1825
+ // state: any state passed during navigation
1734
1826
  ```
1735
1827
 
1736
1828
  **`history.go(delta)`** navigates forward or back in history.
@@ -1793,6 +1885,16 @@ type NavigateOptions = {
1793
1885
  };
1794
1886
  ```
1795
1887
 
1888
+ **`HistoryLocation`** represents a history location.
1889
+
1890
+ ```tsx
1891
+ interface HistoryLocation {
1892
+ path: string; // The current path
1893
+ search: Record<string, unknown>; // Parsed search params
1894
+ state: any; // History state passed during navigation
1895
+ }
1896
+ ```
1897
+
1796
1898
  **`HistoryPushOptions`** are options for untyped navigation.
1797
1899
 
1798
1900
  ```tsx
@@ -1859,9 +1961,9 @@ interface PreloadContext {
1859
1961
  # Roadmap
1860
1962
 
1861
1963
  - Possibility to pass an arbitrary context to the Router instance for later use in preloads?
1862
- - Relative path navigation? Not sure it's indispensable given that users can export/import route objects and pass them as navigation option.
1964
+ - Relative path navigation? Not sure it's worth the extra bundle size given that users can export/import route objects and pass them as navigation option.
1863
1965
  - Document usage in test environments
1864
- - Devtools? Let me know if needed.
1966
+ - Navigation blockers (`useBlocker`, etc.)
1865
1967
  - Open to suggestions, we can discuss them [here](https://github.com/strblr/waymark/discussions).
1866
1968
 
1867
1969
  ---
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as react2 from "react";
1
+ import * as react0 from "react";
2
2
  import { AnchorHTMLAttributes, CSSProperties, ComponentType, ReactNode, RefAttributes } from "react";
3
3
  import { RouteParams } from "regexparam";
4
4
  import { EmptyObject, Merge, Simplify } from "type-fest";
@@ -10,7 +10,6 @@ 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] }>;
14
13
  //#endregion
15
14
  //#region src/types.d.ts
16
15
  interface Register {}
@@ -21,8 +20,8 @@ type Handle = Register extends {
21
20
  handle: infer Handle;
22
21
  } ? Handle : any;
23
22
  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>>>;
23
+ use: <S2 extends {}>(middleware: Middleware<S2>) => Middleware<Merge<S, S2>>;
24
+ search: <S2 extends {}>(validate: Validator<S, S2>) => Middleware<Merge<S, S2>>;
26
25
  handle: (handle: Handle) => Middleware<S>;
27
26
  preload: (preload: (context: PreloadContext<{}, S>) => Promise<any>) => Middleware<S>;
28
27
  component: (component: ComponentType) => Middleware<S>;
@@ -38,8 +37,8 @@ interface PreloadContext<Ps extends {} = any, S extends {} = any> {
38
37
  search: S;
39
38
  }
40
39
  interface RouterOptions {
41
- basePath?: string;
42
40
  routes: RouteList;
41
+ basePath?: string;
43
42
  history?: HistoryLike;
44
43
  ssrContext?: SSRContext;
45
44
  defaultLinkOptions?: LinkOptions;
@@ -65,7 +64,7 @@ type NavigateOptions<P extends Pattern> = {
65
64
  to: P | GetRoute<P>;
66
65
  replace?: boolean;
67
66
  state?: any;
68
- } & MaybeKey<"params", Params<P>> & MaybeKey<"search", Search<P>>;
67
+ } & MaybeKey<"params", Params<P>> & MaybeKey<"search", Partial<Search<P>>>;
69
68
  interface LinkOptions {
70
69
  strict?: boolean;
71
70
  preload?: "intent" | "render" | "viewport" | false;
@@ -79,19 +78,22 @@ type SSRContext = {
79
78
  redirect?: string;
80
79
  statusCode?: number;
81
80
  };
82
- interface HistoryPushOptions {
83
- url: string;
84
- replace?: boolean;
85
- state?: any;
86
- }
87
81
  interface HistoryLike {
88
- getPath: () => string;
89
- getSearch: () => Record<string, unknown>;
90
- getState: () => any;
82
+ location: () => HistoryLocation;
91
83
  go: (delta: number) => void;
92
84
  push: (options: HistoryPushOptions) => void;
93
85
  subscribe: (listener: () => void) => () => void;
94
86
  }
87
+ interface HistoryLocation {
88
+ path: string;
89
+ search: Record<string, unknown>;
90
+ state: any;
91
+ }
92
+ interface HistoryPushOptions {
93
+ url: string;
94
+ replace?: boolean;
95
+ state?: any;
96
+ }
95
97
  type Updater<T extends object> = Partial<T> | ((prev: T) => Partial<T>);
96
98
  type ComponentLoader = () => Promise<ComponentType | {
97
99
  default: ComponentType;
@@ -105,12 +107,13 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
105
107
  pattern: P;
106
108
  keys: string[];
107
109
  regex: RegExp;
108
- looseRegex: RegExp;
110
+ loose: RegExp;
109
111
  weights: number[];
110
112
  validate: (search: Record<string, unknown>) => S;
111
113
  handles: Handle[];
112
114
  components: ComponentType[];
113
115
  preloads: ((context: PreloadContext) => Promise<any>)[];
116
+ p?: Route;
114
117
  };
115
118
  readonly _types: {
116
119
  params: Ps;
@@ -118,8 +121,8 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
118
121
  };
119
122
  constructor(_: typeof this._);
120
123
  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>>>;
124
+ use: <S2 extends {}>(middleware: Middleware<S2>) => Route<P, Ps, Merge<S, S2>>;
125
+ search: <S2 extends {}>(validate: Validator<S, S2>) => Route<P, Ps, Merge<S, S2>>;
123
126
  handle: (handle: Handle) => Route<P, Ps, S>;
124
127
  preload: (preload: (context: PreloadContext<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
125
128
  component: (component: ComponentType) => Route<P, Ps, S>;
@@ -133,8 +136,8 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
133
136
  //#endregion
134
137
  //#region src/router/router.d.ts
135
138
  declare class Router {
136
- readonly basePath: string;
137
139
  readonly routes: RouteList;
140
+ readonly basePath: string;
138
141
  readonly history: HistoryLike;
139
142
  readonly ssrContext?: SSRContext;
140
143
  readonly defaultLinkOptions?: LinkOptions;
@@ -150,13 +153,10 @@ declare class Router {
150
153
  //#endregion
151
154
  //#region src/router/browser-history.d.ts
152
155
  declare class BrowserHistory implements HistoryLike {
153
- private static patch;
154
- private memo?;
156
+ private _?;
157
+ protected _loc: (path: string, search: string) => HistoryLocation;
155
158
  constructor();
156
- protected getSearchMemo: (search: string) => Record<string, unknown>;
157
- getPath: () => string;
158
- getSearch: () => Record<string, unknown>;
159
- getState: () => any;
159
+ location: () => HistoryLocation;
160
160
  go: (delta: number) => void;
161
161
  push: (options: HistoryPushOptions) => void;
162
162
  subscribe: (listener: () => void) => () => void;
@@ -168,10 +168,7 @@ declare class MemoryHistory implements HistoryLike {
168
168
  private index;
169
169
  private listeners;
170
170
  constructor(url?: string);
171
- private getCurrent;
172
- getPath: () => string;
173
- getSearch: () => Record<string, unknown>;
174
- getState: () => any;
171
+ location: () => HistoryLocation;
175
172
  go: (delta: number) => void;
176
173
  push: (options: HistoryPushOptions) => void;
177
174
  subscribe: (listener: () => void) => () => void;
@@ -179,9 +176,7 @@ declare class MemoryHistory implements HistoryLike {
179
176
  //#endregion
180
177
  //#region src/router/hash-history.d.ts
181
178
  declare class HashHistory extends BrowserHistory {
182
- private getHashUrl;
183
- getPath: () => string;
184
- getSearch: () => Record<string, unknown>;
179
+ location: () => HistoryLocation;
185
180
  push: (options: HistoryPushOptions) => void;
186
181
  }
187
182
  //#endregion
@@ -200,22 +195,18 @@ declare function Link<P extends Pattern>(props: LinkProps<P>): ReactNode;
200
195
  //#endregion
201
196
  //#region src/react/hooks.d.ts
202
197
  declare function useRouter(): Router;
198
+ declare function useLocation(): HistoryLocation;
199
+ declare function useMatch<P extends Pattern>(options: MatchOptions<P>): Match<P> | null;
200
+ declare function useOutlet(): ReactNode;
203
201
  declare function useNavigate(): <P extends Pattern>(options: number | HistoryPushOptions | NavigateOptions<P>) => void;
204
- declare function useLocation(): {
205
- path: string;
206
- search: Record<string, unknown>;
207
- state: any;
208
- };
209
- declare function useOutlet(): react2.ReactNode;
202
+ declare function useHandles(): Handle[];
210
203
  declare function useParams<P extends Pattern>(from: P | GetRoute<P>): Params<P>;
211
204
  declare function useSearch<P extends Pattern>(from: P | GetRoute<P>): readonly [Search<P>, (update: Updater<Search<P>>, replace?: boolean) => void];
212
- declare function useMatch<P extends Pattern>(options: MatchOptions<P>): Match<P> | null;
213
- declare function useHandles(): Handle[];
214
- declare function useSubscribe<T>(router: Router, getSnapshot: () => T): T;
215
205
  //#endregion
216
206
  //#region src/react/contexts.d.ts
217
- declare const RouterContext: react2.Context<Router | null>;
218
- declare const MatchContext: react2.Context<Match | null>;
219
- declare const OutletContext: react2.Context<ReactNode>;
207
+ declare const RouterContext: react0.Context<Router | null>;
208
+ declare const LocationContext: react0.Context<HistoryLocation | null>;
209
+ declare const MatchContext: react0.Context<Match | null>;
210
+ declare const OutletContext: react0.Context<ReactNode>;
220
211
  //#endregion
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 };
212
+ export { BrowserHistory, ComponentLoader, GetRoute, Handle, HashHistory, HistoryLike, HistoryLocation, HistoryPushOptions, Link, LinkOptions, LinkProps, LocationContext, Match, MatchContext, MatchOptions, MemoryHistory, Middleware, Navigate, NavigateOptions, NavigateProps, Outlet, OutletContext, Params, Pattern, PreloadContext, Register, Route, RouteList, Router, RouterContext, RouterOptions, RouterRoot, RouterRootProps, SSRContext, Search, Updater, Validator, middleware, route, useHandles, useLocation, useMatch, useNavigate, useOutlet, useParams, useRouter, useSearch };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{Component as e,Suspense as t,cloneElement as n,createContext as r,isValidElement as i,lazy as a,memo as o,useCallback as s,useContext as c,useEffect as l,useInsertionEffect as u,useLayoutEffect as d,useMemo as f,useRef as p,useState as m,useSyncExternalStore as h}from"react";import{inject as g,parse as _}from"regexparam";import{jsx as v}from"react/jsx-runtime";function y(e){return`/${e}`.replaceAll(/\/+/g,`/`).replace(/(.+)\/$/,`$1`)}function b(e){let{keys:t,pattern:n}=_(e);return{pattern:e,keys:t,regex:n,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};
1
+ import{Component as e,Suspense as t,cloneElement as n,createContext as r,isValidElement as i,lazy as a,memo as o,useCallback as s,useContext as c,useEffect as l,useInsertionEffect as u,useLayoutEffect as d,useMemo as f,useRef as p,useState as m,useSyncExternalStore as h}from"react";import{inject as g,parse as _}from"regexparam";import{jsx as v}from"react/jsx-runtime";function y(e){return`/${e}`.replaceAll(/\/+/g,`/`).replace(/(.+)\/$/,`$1`)}function b(e){let{keys:t,pattern:n}=_(e);return{pattern:e,keys:t,regex:n,loose:_(e,!0).pattern,weights:e.split(`/`).slice(1).map(e=>e.includes(`*`)?0:e.includes(`:`)?1:2)}}function x(e){return typeof e==`function`?e:t=>{let n=e[`~standard`].validate(t);if(n instanceof Promise)throw Error(`[Waymark] Validation can't be async`);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({keys:e,regex:t,loose:n},r,i,a){let o=(r?t:n).exec(D(i,a));if(!o)return null;let s={};return e.forEach((e,t)=>{let n=o[t+1];n&&(s[e]=n)}),s}function j(e){return[...e].sort((e,t)=>{let n=e.route._.weights,r=t.route._.weights,i=Math.max(n.length,r.length);for(let e=0;e<i;e++){let t=n[e]??-1,i=r[e]??-1;if(t!==i)return i-t}return 0})}const M=r(null),N=r(null),P=r(null),F=r(null);function I(){let e=c(M);if(e)return e;throw Error(`[Waymark] useRouter must be within a router context`)}function L(){let e=c(N);if(e)return e;throw Error(`[Waymark] useLocation must be within a router context`)}function R(e){let t=I(),{path:n}=L();return f(()=>t.match(n,e),[t,n,e.from,e.strict,e.params])}function z(){return c(F)}function B(){return I().navigate}function V(){let e=c(P);return f(()=>e?.route._.handles??[],[e])}function H(e){let t=R({from:e});if(t)return t.params;throw Error(`[Waymark] Can't read params for non-matching route ${e}`)}function U(e){let t=I(),{search:n,path:r}=L(),i=t.getRoute(e),a=f(()=>i._.validate(n),[i,n]);return[a,Z((e,n)=>{e=typeof e==`function`?e(a):e;let i=O(r,{...a,...e});t.navigate({url:i,replace:n})})]}var W=class{_;_loc=(e,t)=>{let{state:n}=history,[r,i]=this._??[];return i?.path===e&&r===t&&i.state===n?i:(this._=[t,{path:e,search:C(t),state:n}])[1]};constructor(){if(!window[G]){for(let e of[K,q]){let t=history[e];history[e]=function(...n){t.apply(this,n);let r=new Event(e);dispatchEvent(r)}}window[G]=1}}location=()=>this._loc(location.pathname,location.search);go=e=>history.go(e);push=e=>{let{url:t,replace:n,state:r}=e;history[n?q:K](r,``,t)};subscribe=e=>(J.forEach(t=>window.addEventListener(t,e)),()=>{J.forEach(t=>window.removeEventListener(t,e))})};const G=Symbol.for(`wmp01`),K=`pushState`,q=`replaceState`,J=[`popstate`,K,q,`hashchange`];var Y=class{routes;basePath;history;ssrContext;defaultLinkOptions;_;constructor(e){let{routes:t,basePath:n=`/`,history:r,ssrContext:i,defaultLinkOptions:a}=e;this.routes=t,this.basePath=y(n),this.history=r??new W,this.ssrContext=i,this.defaultLinkOptions=a,this._={routeMap:new Map(t.map(e=>[e._.pattern,e]))}}getRoute=e=>{if(typeof e!=`string`)return e;let t=this._.routeMap.get(e);if(!t)throw Error(`[Waymark] Route not found for ${e}`);return t};match=(e,t)=>{let{from:n,strict:r,params:i}=t,a=this.getRoute(n),o=A(a._,r,e,this.basePath);return o&&(!i||Object.keys(i).every(e=>i[e]===o[e]))?{route:a,params:o}:null};matchAll=e=>j(this.routes.map(t=>this.match(e,{from:t,strict:!0})).filter(e=>!!e))[0]??null;createUrl=e=>{let{to:t,params:n={},search:r={}}=e,{pattern:i}=this.getRoute(t)._;return O(E(g(i,n),this.basePath),r)};preload=async e=>{let{to:t,params:n={},search:r={}}=e,{preloads:i}=this.getRoute(t)._;await Promise.all(i.map(e=>e({params:n,search:r})))};navigate=e=>{if(typeof e==`number`)this.history.go(e);else if(`url`in e)this.history.push(e);else{let{replace:t,state:n}=e;this.history.push({url:this.createUrl(e),replace:t,state:n})}}},X=class{stack=[];index=0;listeners=new Set;constructor(e=`/`){this.stack.push({...k(e),state:void 0})}location=()=>this.stack[this.index];go=e=>{let t=this.index+e;this.stack[t]&&(this.index=t,this.listeners.forEach(e=>e()))};push=e=>{let{url:t,replace:n,state:r}=e,i={...k(t),state:r};this.stack=this.stack.slice(0,this.index+1),n?this.stack[this.index]=i:this.index=this.stack.push(i)-1,this.listeners.forEach(e=>e())};subscribe=e=>(this.listeners.add(e),()=>{this.listeners.delete(e)})},ee=class extends W{location=()=>{let{pathname:e,search:t}=new URL(location.hash.slice(1),`http://w`);return this._loc(e,t)};push=e=>{let{url:t,replace:n,state:r}=e;history[n?`replaceState`:`pushState`](r,``,`#${t}`)}};function te(e){let[t]=m(()=>`router`in e?e.router:new Y(e)),{subscribe:n,location:r}=t.history,i=h(n,r,r),a=f(()=>t.matchAll(i.path),[t,i.path]);return a||console.error(`[Waymark] No matching route for path`,i.path),f(()=>v(M.Provider,{value:t,children:v(N.Provider,{value:i,children:v(P.Provider,{value:a,children:a?.route._.components.reduceRight((e,t)=>v(F.Provider,{value:e,children:v(t,{})}),null)})})}),[t,i,a])}function ne(){return z()}function re(e){let t=I();return d(()=>t.navigate(e),[]),t.ssrContext&&(t.ssrContext.redirect=t.createUrl(e)),null}function ie(e){let t=I(),{to:r,replace:a,state:o,params:c,search:u,strict:d,preload:m,preloadDelay:h=50,style:g,className:_,activeStyle:y,activeClassName:b,asChild:x,children:S,...C}={...t.defaultLinkOptions,...e},w=p(null),T=p(null),E=t.createUrl(e),D=!!R({from:r,strict:d,params:c}),O=Z(()=>t.preload(e)),k=s(()=>{clearTimeout(T.current)},[]),A=s(()=>{k(),T.current=setTimeout(O,h)},[h,k]),j=f(()=>({"data-active":D,style:{...g,...D&&y},className:[_,D&&b].filter(Boolean).join(` `)||void 0}),[D,g,_,y,b]);l(()=>{if(m===`render`)A();else if(m===`viewport`&&w.current){let e=new IntersectionObserver(e=>e.forEach(e=>{e.isIntersecting?A():k()}));return e.observe(w.current),()=>{e.disconnect(),k()}}return k},[m,A,k]);let M=e=>{C.onClick?.(e),!(e.ctrlKey||e.metaKey||e.shiftKey||e.altKey||e.button!==0||e.defaultPrevented)&&(e.preventDefault(),t.navigate({url:E,replace:a,state:o}))},N=e=>{C.onFocus?.(e),m===`intent`&&!e.defaultPrevented&&A()},P=e=>{C.onBlur?.(e),m===`intent`&&k()},F=e=>{C.onPointerEnter?.(e),m===`intent`&&!e.defaultPrevented&&A()},L=e=>{C.onPointerLeave?.(e),m===`intent`&&k()},z={...C,...j,ref:ae(w,C.ref),href:E,onClick:M,onFocus:N,onBlur:P,onPointerEnter:F,onPointerLeave:L};return x&&i(S)?n(S,z):v(`a`,{...z,children:S})}function ae(e,t){return t?n=>{e.current=n;let r=typeof t==`function`?t(n):void(t.current=n);return r&&(()=>{e.current=null,r()})}:e}function Z(e){let t=p(e);return u(()=>{t.current=e},[e]),p(((...e)=>t.current(...e))).current}function oe(e){return()=>v(t,{fallback:v(e,{}),children:z()})}function se(t){class n extends e{constructor(e){super(e),this.state={children:e.children,error:null}}static getDerivedStateFromError(e){return{error:[e]}}static getDerivedStateFromProps(e,t){return e.children===t.children?t:{children:e.children,error:null}}render(){return this.state.error?v(t,{error:this.state.error[0]}):this.props.children}}return()=>v(n,{children:z()})}function Q(e){return new $({...b(y(e)),validate:e=>e,handles:[],components:[],preloads:[]})}function ce(){return Q(``)}var $=class e{_;_types;constructor(e){this._=e}route=t=>new e({...this._,...b(y(`${this._.pattern}/${t}`)),p:this});use=t=>{let{_:n}=t;return new e({...this._,handles:[...this._.handles,...n.handles],components:[...this._.components,...n.components],preloads:[...this._.preloads,...n.preloads]}).search(n.validate)};search=t=>(t=x(t),new e({...this._,validate:e=>{let n=this._.validate(e);return{...n,...t({...e,...n})}}}));handle=t=>new e({...this._,handles:[...this._.handles,t]});preload=t=>new e({...this._,preloads:[...this._.preloads,e=>t({params:e.params,search:this._.validate(e.search)})]});component=t=>new e({...this._,components:[...this._.components,o(t)]});lazy=e=>{let t=a(async()=>{let t=await e();return`default`in t?t:{default:t}});return this.preload(e).component(t)};suspense=e=>this.component(oe(e));error=e=>this.component(se(e));toString=()=>this._.pattern};export{W as BrowserHistory,ee as HashHistory,ie as Link,N as LocationContext,P as MatchContext,X as MemoryHistory,re as Navigate,ne as Outlet,F as OutletContext,$ as Route,Y as Router,M as RouterContext,te as RouterRoot,ce as middleware,Q as route,V as useHandles,L as useLocation,R as useMatch,B as useNavigate,z as useOutlet,H as useParams,I as useRouter,U as useSearch};
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "waymark",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "author": "strblr",
6
- "description": "Type-safe router for React",
6
+ "description": "Type-safe React router that just works - simple setup, full autocomplete, 4kB gzipped",
7
7
  "type": "module",
8
8
  "main": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
@@ -38,6 +38,7 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "tsc --noEmit && tsdown",
41
+ "dev": "tsdown --watch",
41
42
  "prepublishOnly": "bun run build && cp ../../README.md README.md",
42
43
  "postpublish": "rm -f README.md"
43
44
  },