waymark 0.4.0 โ†’ 0.5.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,74 @@
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
+ | **File-based routing** | โŒ | โœ… | โœ… | โŒ |
83
+ | **React Native** | โŒ | โœ… | โŒ | โŒ |
84
+
85
+ <details>
86
+ <summary><b>Comparison notes</b></summary>
87
+
88
+ <br />
89
+
90
+ 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.
91
+
92
+ - โš ๏ธ indicates the feature is only partially supported, supported with heavy boilerplate, or requires external libraries.
93
+ - ๐Ÿ”จ indicates the feature is not yet ready but being worked on.
94
+ - **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.
95
+ - **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.
96
+ - **Full type inference** refers to automatic TypeScript inference for routes, params, search params, and navigation without manual type annotations.
97
+ - **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`.
98
+ - **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.
99
+ - **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.
100
+
101
+ </details>
57
102
 
58
103
  ---
59
104
 
60
105
  # Table of contents
61
106
 
107
+ - [Comparison](#comparison)
62
108
  - [Showcase](#showcase)
63
109
  - [Installation](#installation)
64
110
  - [Defining routes](#defining-routes)
@@ -85,6 +131,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
85
131
  - [Middlewares](#middlewares)
86
132
  - [Route matching and ranking](#route-matching-and-ranking)
87
133
  - [History implementations](#history-implementations)
134
+ - [Devtools](#devtools)
88
135
  - [Cookbook](#cookbook)
89
136
  - [Quick start example](#quick-start-example)
90
137
  - [Server-side rendering (SSR)](#server-side-rendering-ssr)
@@ -150,6 +197,8 @@ declare module "waymark" {
150
197
 
151
198
  Everything autocompletes and type-checks automatically. No heavy setup, no magic, just a simple API that gets out of your way.
152
199
 
200
+ ๐Ÿ‘‰ [Try it live in the StackBlitz playground](https://stackblitz.com/edit/waymark-demo?file=src%2Fapp.tsx)
201
+
153
202
  ---
154
203
 
155
204
  # Installation
@@ -192,31 +241,25 @@ const files = route("/files/*").component(FileBrowser);
192
241
  const optional = route("/books/*?").component(FileBrowser);
193
242
  ```
194
243
 
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.
244
+ Route building is immutable: every method on a route returns a new route instance.
196
245
 
197
246
  ---
198
247
 
199
248
  # Nested routes and layouts
200
249
 
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:
250
+ Any route can have child routes. Call `.route()` on an existing route to create one:
204
251
 
205
252
  ```tsx
206
253
  const dashboard = route("/dashboard").component(DashboardLayout);
207
- ```
208
-
209
- Then create child routes by calling `.route()` on it:
210
254
 
211
- ```tsx
212
255
  const overview = dashboard.route("/").component(Overview);
213
256
  const settings = dashboard.route("/settings").component(Settings);
214
257
  const profile = dashboard.route("/profile").component(Profile);
215
258
  ```
216
259
 
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`.
260
+ Child routes build on their parent's path. So `overview` matches `/dashboard`, `settings` matches `/dashboard/settings`, and `profile` matches `/dashboard/profile`.
218
261
 
219
- The parent component must render an `<Outlet />` where child routes should appear:
262
+ They also nest inside the parent's component. The parent renders an `<Outlet />` to mark where child routes should appears:
220
263
 
221
264
  ```tsx
222
265
  function DashboardLayout() {
@@ -231,7 +274,7 @@ function DashboardLayout() {
231
274
  }
232
275
  ```
233
276
 
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.
277
+ 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
278
 
236
279
  You can nest as deep as you need:
237
280
 
@@ -253,6 +296,8 @@ AppShell
253
296
 
254
297
  Each level must include an `<Outlet />` to render the next level.
255
298
 
299
+ 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.
300
+
256
301
  ---
257
302
 
258
303
  # Setting up the router
@@ -319,7 +364,9 @@ declare module "waymark" {
319
364
  }
320
365
  ```
321
366
 
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.
367
+ With this in place, `Link`, `navigate`, `useParams`, `useSearch`, and other APIs will know exactly which routes exist and what input they expect.
368
+
369
+ **You're all set up!**
323
370
 
324
371
  ---
325
372
 
@@ -459,6 +506,9 @@ function SearchPage() {
459
506
  const [search, setSearch] = useSearch(searchPage);
460
507
  // search.q: string
461
508
  // search.page: number
509
+
510
+ const [search, setSearch] = useSearch("/search");
511
+ // Also works
462
512
  }
463
513
  ```
464
514
 
@@ -1015,6 +1065,16 @@ function UserPage() {
1015
1065
  }
1016
1066
  ```
1017
1067
 
1068
+ For parametrized middlewares, define a function that returns a middleware:
1069
+
1070
+ ```tsx
1071
+ const guard = (role: string) =>
1072
+ middleware().handle({ requiredRole: role }).component(RoleGuard);
1073
+
1074
+ const adminPage = route("/admin").use(guard("admin")).component(AdminPage);
1075
+ const editorPage = route("/editor").use(guard("editor")).component(EditorPage);
1076
+ ```
1077
+
1018
1078
  ---
1019
1079
 
1020
1080
  # Route matching and ranking
@@ -1103,6 +1163,53 @@ All history implementations conform to the `HistoryLike` interface, so you can c
1103
1163
 
1104
1164
  ---
1105
1165
 
1166
+ # Devtools
1167
+
1168
+ Waymark has a companion devtools package for inspecting routes, matches, parameters, and navigation state.
1169
+
1170
+ ```bash
1171
+ npm install waymark-devtools
1172
+ ```
1173
+
1174
+ Render the `Devtools` component anywhere inside your routes. It displays a toggle button that opens a draggable and resizable floating panel:
1175
+
1176
+ ```tsx
1177
+ import { Devtools } from "waymark-devtools";
1178
+
1179
+ const layout = route("/").component(Layout);
1180
+
1181
+ function Layout() {
1182
+ return (
1183
+ <div>
1184
+ <Outlet />
1185
+ <Devtools />
1186
+ </div>
1187
+ );
1188
+ }
1189
+ ```
1190
+
1191
+ If you'd rather embed the panel directly into your layout instead of using the floating window, use `DevtoolsPanel`:
1192
+
1193
+ ```tsx
1194
+ import { DevtoolsPanel } from "waymark-devtools";
1195
+
1196
+ function DebugSidebar() {
1197
+ return (
1198
+ <aside>
1199
+ <DevtoolsPanel />
1200
+ </aside>
1201
+ );
1202
+ }
1203
+ ```
1204
+
1205
+ To exclude devtools from production builds (Vite example):
1206
+
1207
+ ```tsx
1208
+ import.meta.env.DEV && <Devtools />;
1209
+ ```
1210
+
1211
+ ---
1212
+
1106
1213
  # Cookbook
1107
1214
 
1108
1215
  ## Quick start example
@@ -1217,29 +1324,27 @@ function AppLayout() {
1217
1324
 
1218
1325
  ## Matching a route anywhere
1219
1326
 
1220
- 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`.
1327
+ Use `useMatch` to check if a route is part of the current match. 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`.
1328
+
1329
+ The hook returns the matched params if there's a match, or `null` otherwise. There are two matching modes:
1221
1330
 
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`:
1331
+ - **Loose matching** (default): Matches if you're on the route or any of its child routes.
1332
+ - **Strict matching** (`strict: true`): Matches only if you're on the exact route.
1223
1333
 
1224
1334
  ```tsx
1225
- import { useMatch } from "waymark";
1335
+ import { route, useMatch } from "waymark";
1226
1336
 
1227
1337
  const dashboard = route("/dashboard").component(Dashboard);
1228
- const settings = route("/settings").component(Settings);
1338
+ const settings = dashboard.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 and any child route like /dashboard/settings
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 exactly
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
 
@@ -1407,15 +1512,15 @@ const url = router.createUrl({ to: userProfile, params: { id: "42" } });
1407
1512
  // Returns "/users/42"
1408
1513
  ```
1409
1514
 
1410
- **`router.match(path, options)`** checks if a path matches a specific route.
1515
+ **`router.match(path, route)`** checks if a path matches a specific route.
1411
1516
 
1412
1517
  - `path` - `string` - The path to match against
1413
- - `options` - `MatchOptions` - Matching options
1518
+ - `route` - `Route` - The route object to match against
1414
1519
  - Returns: `Match | null` - The match result or null if no match
1415
1520
 
1416
1521
  ```tsx
1417
- const match = router.match("/users/42", { from: "/users/:id" });
1418
- // Returns { route, params: { id: "42" } } or null
1522
+ const match = router.match("/users/42", userRoute);
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();
@@ -1635,7 +1743,7 @@ setSearch({ page: 1 }, true); // Replace instead of push
1635
1743
  **`useMatch(options)`** checks if a route matches the current path.
1636
1744
 
1637
1745
  - `options` - `MatchOptions` - Matching options
1638
- - Returns: `Match | null` - The match result or null if no match
1746
+ - Returns: `Params | null` - The extracted path params if matched, or null if no match
1639
1747
 
1640
1748
  ```tsx
1641
1749
  const match = useMatch({ from: "/users/:id" });
@@ -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
- ```
1717
-
1718
- **`history.getSearch()`** returns the current search params as a parsed JSON object.
1817
+ **`history.location()`** returns the current location.
1719
1818
 
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
@@ -1808,7 +1910,7 @@ interface HistoryPushOptions {
1808
1910
  ```tsx
1809
1911
  type MatchOptions = {
1810
1912
  from: Pattern | Route; // The route to match against
1811
- strict?: boolean; // Require exact match (default: false, matches prefixes)
1913
+ strict?: boolean; // Strict matching mode (default: false)
1812
1914
  params?: Partial<Params>; // Optional param values to filter by
1813
1915
  };
1814
1916
  ```
@@ -1859,9 +1961,8 @@ 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.
1865
1966
  - Open to suggestions, we can discuss them [here](https://github.com/strblr/waymark/discussions).
1866
1967
 
1867
1968
  ---
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";
@@ -38,8 +38,8 @@ interface PreloadContext<Ps extends {} = any, S extends {} = any> {
38
38
  search: S;
39
39
  }
40
40
  interface RouterOptions {
41
- basePath?: string;
42
41
  routes: RouteList;
42
+ basePath?: string;
43
43
  history?: HistoryLike;
44
44
  ssrContext?: SSRContext;
45
45
  defaultLinkOptions?: LinkOptions;
@@ -79,19 +79,22 @@ type SSRContext = {
79
79
  redirect?: string;
80
80
  statusCode?: number;
81
81
  };
82
- interface HistoryPushOptions {
83
- url: string;
84
- replace?: boolean;
85
- state?: any;
86
- }
87
82
  interface HistoryLike {
88
- getPath: () => string;
89
- getSearch: () => Record<string, unknown>;
90
- getState: () => any;
83
+ location: () => HistoryLocation;
91
84
  go: (delta: number) => void;
92
85
  push: (options: HistoryPushOptions) => void;
93
86
  subscribe: (listener: () => void) => () => void;
94
87
  }
88
+ interface HistoryLocation {
89
+ path: string;
90
+ search: Record<string, unknown>;
91
+ state: any;
92
+ }
93
+ interface HistoryPushOptions {
94
+ url: string;
95
+ replace?: boolean;
96
+ state?: any;
97
+ }
95
98
  type Updater<T extends object> = Partial<T> | ((prev: T) => Partial<T>);
96
99
  type ComponentLoader = () => Promise<ComponentType | {
97
100
  default: ComponentType;
@@ -105,12 +108,12 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
105
108
  pattern: P;
106
109
  keys: string[];
107
110
  regex: RegExp;
108
- looseRegex: 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
+ chain: Set<Route>;
114
117
  };
115
118
  readonly _types: {
116
119
  params: Ps;
@@ -133,15 +136,15 @@ 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;
141
144
  private readonly _;
142
145
  constructor(options: RouterOptions);
143
146
  getRoute: <P extends Pattern>(pattern: P | GetRoute<P>) => GetRoute<P>;
144
- match: <P extends Pattern>(path: string, options: MatchOptions<P>) => Match<P> | null;
147
+ match: <P extends Pattern>(path: string, route: GetRoute<P>) => Match<P> | null;
145
148
  matchAll: (path: string) => Match | null;
146
149
  createUrl: <P extends Pattern>(options: NavigateOptions<P>) => string;
147
150
  preload: <P extends Pattern>(options: NavigateOptions<P>) => Promise<void>;
@@ -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>): Params<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,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),F=r(null);function I(){let e=c(M);if(e)return e;throw Error(`[Waymark] useRouter must be used within a router context`)}function L(){let e=c(N);if(e)return e;throw Error(`[Waymark] useLocation must be used within a router context`)}function R(e){let t=I(),n=c(P),{from:r,strict:i,params:a}=e,o=t.getRoute(r);return n&&(n.route===o||!i&&n.route._.chain.has(o))&&(!a||Object.keys(a).every(e=>a[e]===n.params[e]))?n.params:null}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;throw Error(`[Waymark] Can't read params for non-matching route: ${e}`)}function U(e){let t=I(),n=L(),r=t.getRoute(e),i=f(()=>r._.validate(n.search),[r,n.search]);return[i,Z((e,r)=>{e=typeof e==`function`?e(i):e;let a={...i,...e},o=O(n.path,a);t.navigate({url:o,replace:r})})]}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 pattern: ${e}`);return t};match=(e,t)=>{let{regex:n,keys:r}=t._,i=A(n,r,e,this.basePath);return i?{route:t,params:i}:null};matchAll=e=>j(this.routes.map(t=>this.match(e,t)).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:[],chain:new Set})}function ce(){return Q(``)}var $=class e{_;_types;constructor(e){this._=e}route=t=>new e({...this._,...b(y(`${this._.pattern}/${t}`)),chain:new Set([...this._.chain,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.5.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
  },