@funstack/router 0.0.1-alpha.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 +225 -0
- package/dist/index.d.mts +108 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +295 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# FUNSTACK Router
|
|
2
|
+
|
|
3
|
+
A modern React router built on the [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API).
|
|
4
|
+
|
|
5
|
+
> **Warning**
|
|
6
|
+
> This project is in early development and is not ready for production use. APIs may change without notice.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Navigation API based** - Uses the modern Navigation API instead of the History API
|
|
11
|
+
- **Object-based routes** - Define routes as plain JavaScript objects
|
|
12
|
+
- **Nested routing** - Support for layouts and nested routes with `<Outlet>`
|
|
13
|
+
- **Type-safe** - Full TypeScript support
|
|
14
|
+
- **Lightweight** - ~2.5 kB gzipped
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @funstack/router
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Router, Link, Outlet, useParams } from "@funstack/router";
|
|
26
|
+
import type { RouteDefinition } from "@funstack/router";
|
|
27
|
+
|
|
28
|
+
function Layout() {
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<nav>
|
|
32
|
+
<Link to="/">Home</Link>
|
|
33
|
+
<Link to="/users">Users</Link>
|
|
34
|
+
</nav>
|
|
35
|
+
<Outlet />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Home() {
|
|
41
|
+
return <h1>Home</h1>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Users() {
|
|
45
|
+
return <h1>Users</h1>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function UserDetail() {
|
|
49
|
+
const { id } = useParams<{ id: string }>();
|
|
50
|
+
return <h1>User {id}</h1>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const routes: RouteDefinition[] = [
|
|
54
|
+
{
|
|
55
|
+
path: "/",
|
|
56
|
+
component: Layout,
|
|
57
|
+
children: [
|
|
58
|
+
{ path: "", component: Home },
|
|
59
|
+
{ path: "users", component: Users },
|
|
60
|
+
{ path: "users/:id", component: UserDetail },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function App() {
|
|
66
|
+
return <Router routes={routes} />;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### Components
|
|
73
|
+
|
|
74
|
+
#### `<Router>`
|
|
75
|
+
|
|
76
|
+
The root component that provides routing context.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<Router routes={routes} />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Prop | Type | Description |
|
|
83
|
+
| ---------- | ------------------- | ------------------------------------------- |
|
|
84
|
+
| `routes` | `RouteDefinition[]` | Array of route definitions |
|
|
85
|
+
| `children` | `ReactNode` | Optional children rendered alongside routes |
|
|
86
|
+
|
|
87
|
+
#### `<Link>`
|
|
88
|
+
|
|
89
|
+
Navigation link component.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<Link to="/users" replace state={{ from: "home" }}>
|
|
93
|
+
Users
|
|
94
|
+
</Link>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Prop | Type | Description |
|
|
98
|
+
| --------- | --------- | ------------------------------------- |
|
|
99
|
+
| `to` | `string` | Destination URL |
|
|
100
|
+
| `replace` | `boolean` | Replace history entry instead of push |
|
|
101
|
+
| `state` | `unknown` | State to pass to the destination |
|
|
102
|
+
|
|
103
|
+
#### `<Outlet>`
|
|
104
|
+
|
|
105
|
+
Renders the matched child route. Used in layout components.
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
function Layout() {
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
<nav>...</nav>
|
|
112
|
+
<Outlet />
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Hooks
|
|
119
|
+
|
|
120
|
+
#### `useNavigate()`
|
|
121
|
+
|
|
122
|
+
Returns a function for programmatic navigation.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
const navigate = useNavigate();
|
|
126
|
+
|
|
127
|
+
// Basic navigation
|
|
128
|
+
navigate("/users");
|
|
129
|
+
|
|
130
|
+
// With options
|
|
131
|
+
navigate("/users", { replace: true, state: { from: "home" } });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `useLocation()`
|
|
135
|
+
|
|
136
|
+
Returns the current location.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const location = useLocation();
|
|
140
|
+
// { pathname: "/users", search: "?page=1", hash: "#section" }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `useParams()`
|
|
144
|
+
|
|
145
|
+
Returns the current route's path parameters.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// Route: /users/:id
|
|
149
|
+
const { id } = useParams<{ id: string }>();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `useSearchParams()`
|
|
153
|
+
|
|
154
|
+
Returns and allows updating URL search parameters.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
158
|
+
|
|
159
|
+
// Read
|
|
160
|
+
const page = searchParams.get("page");
|
|
161
|
+
|
|
162
|
+
// Update
|
|
163
|
+
setSearchParams({ page: "2" });
|
|
164
|
+
|
|
165
|
+
// Update with function
|
|
166
|
+
setSearchParams((prev) => {
|
|
167
|
+
prev.set("page", "2");
|
|
168
|
+
return prev;
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Types
|
|
173
|
+
|
|
174
|
+
#### `RouteDefinition`
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
type RouteDefinition = {
|
|
178
|
+
path: string;
|
|
179
|
+
component?: React.ComponentType;
|
|
180
|
+
children?: RouteDefinition[];
|
|
181
|
+
};
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `Location`
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
type Location = {
|
|
188
|
+
pathname: string;
|
|
189
|
+
search: string;
|
|
190
|
+
hash: string;
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `NavigateOptions`
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
type NavigateOptions = {
|
|
198
|
+
replace?: boolean;
|
|
199
|
+
state?: unknown;
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Path Patterns
|
|
204
|
+
|
|
205
|
+
FUNSTACK Router uses the [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) for path matching.
|
|
206
|
+
|
|
207
|
+
| Pattern | Example | Matches |
|
|
208
|
+
| ------------ | -------------- | --------------- |
|
|
209
|
+
| `/users` | `/users` | Exact match |
|
|
210
|
+
| `/users/:id` | `/users/123` | Named parameter |
|
|
211
|
+
| `/files/*` | `/files/a/b/c` | Wildcard |
|
|
212
|
+
|
|
213
|
+
## Browser Support
|
|
214
|
+
|
|
215
|
+
The Navigation API is supported in:
|
|
216
|
+
|
|
217
|
+
- Chrome 102+
|
|
218
|
+
- Edge 102+
|
|
219
|
+
- Opera 88+
|
|
220
|
+
|
|
221
|
+
Firefox and Safari do not yet support the Navigation API. For these browsers, consider using a polyfill.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as react0 from "react";
|
|
2
|
+
import { AnchorHTMLAttributes, ComponentType, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Route definition for the router.
|
|
7
|
+
*/
|
|
8
|
+
type RouteDefinition = {
|
|
9
|
+
/** Path pattern to match (e.g., "users/:id") */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Component to render when this route matches */
|
|
12
|
+
component?: ComponentType;
|
|
13
|
+
/** Child routes for nested routing */
|
|
14
|
+
children?: RouteDefinition[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A matched route with its parameters.
|
|
18
|
+
*/
|
|
19
|
+
type MatchedRoute = {
|
|
20
|
+
/** The original route definition */
|
|
21
|
+
route: RouteDefinition;
|
|
22
|
+
/** Extracted path parameters */
|
|
23
|
+
params: Record<string, string>;
|
|
24
|
+
/** The matched pathname segment */
|
|
25
|
+
pathname: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Options for navigation.
|
|
29
|
+
*/
|
|
30
|
+
type NavigateOptions = {
|
|
31
|
+
/** Replace current history entry instead of pushing */
|
|
32
|
+
replace?: boolean;
|
|
33
|
+
/** State to associate with the navigation */
|
|
34
|
+
state?: unknown;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Location object representing current URL state.
|
|
38
|
+
*/
|
|
39
|
+
type Location = {
|
|
40
|
+
pathname: string;
|
|
41
|
+
search: string;
|
|
42
|
+
hash: string;
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/Router.d.ts
|
|
46
|
+
type RouterProps = {
|
|
47
|
+
routes: RouteDefinition[];
|
|
48
|
+
children?: ReactNode;
|
|
49
|
+
};
|
|
50
|
+
declare function Router({
|
|
51
|
+
routes,
|
|
52
|
+
children
|
|
53
|
+
}: RouterProps): ReactNode;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/Link.d.ts
|
|
56
|
+
type LinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
|
|
57
|
+
/** The destination URL */
|
|
58
|
+
to: string;
|
|
59
|
+
/** Replace current history entry instead of pushing */
|
|
60
|
+
replace?: boolean;
|
|
61
|
+
/** State to associate with the navigation */
|
|
62
|
+
state?: unknown;
|
|
63
|
+
children?: ReactNode;
|
|
64
|
+
};
|
|
65
|
+
declare const Link: react0.ForwardRefExoticComponent<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
|
|
66
|
+
/** The destination URL */
|
|
67
|
+
to: string;
|
|
68
|
+
/** Replace current history entry instead of pushing */
|
|
69
|
+
replace?: boolean;
|
|
70
|
+
/** State to associate with the navigation */
|
|
71
|
+
state?: unknown;
|
|
72
|
+
children?: ReactNode;
|
|
73
|
+
} & react0.RefAttributes<HTMLAnchorElement>>;
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/Outlet.d.ts
|
|
76
|
+
/**
|
|
77
|
+
* Renders the matched child route.
|
|
78
|
+
* Used in layout components to specify where child routes should render.
|
|
79
|
+
*/
|
|
80
|
+
declare function Outlet(): ReactNode;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/hooks/useNavigate.d.ts
|
|
83
|
+
/**
|
|
84
|
+
* Returns a function for programmatic navigation.
|
|
85
|
+
*/
|
|
86
|
+
declare function useNavigate(): (to: string, options?: NavigateOptions) => void;
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/hooks/useLocation.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* Returns the current location object.
|
|
91
|
+
*/
|
|
92
|
+
declare function useLocation(): Location;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/hooks/useParams.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* Returns route parameters from the matched path.
|
|
97
|
+
*/
|
|
98
|
+
declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/hooks/useSearchParams.d.ts
|
|
101
|
+
type SetSearchParams = (params: URLSearchParams | Record<string, string> | ((prev: URLSearchParams) => URLSearchParams | Record<string, string>)) => void;
|
|
102
|
+
/**
|
|
103
|
+
* Returns and allows manipulation of URL search parameters.
|
|
104
|
+
*/
|
|
105
|
+
declare function useSearchParams(): [URLSearchParams, SetSearchParams];
|
|
106
|
+
//#endregion
|
|
107
|
+
export { Link, type LinkProps, type Location, type MatchedRoute, type NavigateOptions, Outlet, type RouteDefinition, Router, type RouterProps, useLocation, useNavigate, useParams, useSearchParams };
|
|
108
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Link.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useParams.ts","../src/hooks/useSearchParams.ts"],"sourcesContent":[],"mappings":";;;;;;;AAKY,KAAA,eAAA,GAAe;EAYf;EAYA,IAAA,EAAA,MAAA;EAUA;cA9BE;;aAED;ACKb,CAAA;AA4CA;;;AAA6C,KD3CjC,YAAA,GC2CiC;EAAc;EAAS,KAAA,EDzC3D,eCyC2D;;UDvC1D;;EEZE,QAAA,EAAA,MAAS;CACE;;;;AASD,KFUV,eAAA,GEVU;EAGT;EAAI,OAAA,CAAA,EAAA,OAAA;EAAA;EAAA,KAAA,CAAA,EAAA,OAAA;CAHJ;;;;AAGI,KFiBL,QAAA,GEjBK;;;;ACfjB,CAAA;;;KFSY,WAAA;UACF;EDZE,QAAA,CAAA,ECaC,SDbc;AAY3B,CAAA;AAYY,iBC+BI,MAAA,CD/BW;EAAA,MAAA;EAAA;AAAA,CAAA,EC+BkB,WD/BlB,CAAA,EC+BgC,SD/BhC;;;KEpBf,SAAA,GAAY,KACtB,qBAAqB;;;EFLX;EAYA,OAAA,CAAA,EAAA,OAAY;EAYZ;EAUA,KAAA,CAAA,EAAA,OAAQ;aEpBP;;cAGA,aAAI,0BAAA,KAAA,qBAAA;EDNL;EA4CI,EAAA,EAAA,MAAM;EAAG;EAAQ,OAAA,CAAA,EAAA,OAAA;EAAY;EAAc,KAAA,CAAA,EAAA,OAAA;EAAS,QAAA,CAAA,ECzCvD,SDyCuD;;;;;;;ADvDpE;AAYY,iBGVI,MAAA,CAAA,CHYP,EGZiB,SHYjB;;;;;;AAdG,iBIEI,WAAA,CAAA,CJEF,EAAA,CAAA,EAAA,EAAA,MAED,EAAA,OAAe,CAAf,EIJyC,eJI1B,EAAA,GAAA,IAAA;;;;;;AANhB,iBKEI,WAAA,CAAA,CLEF,EKFiB,QLEjB;;;;;;iBMHE,oBACJ,yBAAyB,2BAChC;;;KCLA,eAAA,YAEC,kBACA,iCACQ,oBAAoB,kBAAkB;;;;APFxC,iBOQI,eAAA,CAAA,CPJF,EAAA,COIsB,ePFvB,EOEwC,ePFzB,CAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/context/RouterContext.ts
|
|
5
|
+
const RouterContext = createContext(null);
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/context/RouteContext.ts
|
|
9
|
+
const RouteContext = createContext(null);
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/core/matchRoutes.ts
|
|
13
|
+
/**
|
|
14
|
+
* Match a pathname against a route tree, returning the matched route stack.
|
|
15
|
+
* Returns null if no match is found.
|
|
16
|
+
*/
|
|
17
|
+
function matchRoutes(routes, pathname) {
|
|
18
|
+
for (const route of routes) {
|
|
19
|
+
const matched = matchRoute(route, pathname);
|
|
20
|
+
if (matched) return matched;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Match a single route and its children recursively.
|
|
26
|
+
*/
|
|
27
|
+
function matchRoute(route, pathname) {
|
|
28
|
+
const hasChildren = Boolean(route.children?.length);
|
|
29
|
+
const { matched, params, consumedPathname } = matchPath(route.path, pathname, !hasChildren);
|
|
30
|
+
if (!matched) return null;
|
|
31
|
+
const result = {
|
|
32
|
+
route,
|
|
33
|
+
params,
|
|
34
|
+
pathname: consumedPathname
|
|
35
|
+
};
|
|
36
|
+
if (hasChildren) {
|
|
37
|
+
let remainingPathname = pathname.slice(consumedPathname.length);
|
|
38
|
+
if (!remainingPathname.startsWith("/")) remainingPathname = "/" + remainingPathname;
|
|
39
|
+
if (remainingPathname === "") remainingPathname = "/";
|
|
40
|
+
for (const child of route.children) {
|
|
41
|
+
const childMatch = matchRoute(child, remainingPathname);
|
|
42
|
+
if (childMatch) return [result, ...childMatch.map((m) => ({
|
|
43
|
+
...m,
|
|
44
|
+
params: {
|
|
45
|
+
...params,
|
|
46
|
+
...m.params
|
|
47
|
+
}
|
|
48
|
+
}))];
|
|
49
|
+
}
|
|
50
|
+
if (route.component) return [result];
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return [result];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Match a path pattern against a pathname.
|
|
57
|
+
*/
|
|
58
|
+
function matchPath(pattern, pathname, exact) {
|
|
59
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
60
|
+
let urlPatternPath;
|
|
61
|
+
if (exact) urlPatternPath = normalizedPattern;
|
|
62
|
+
else if (normalizedPattern === "/") urlPatternPath = "/*";
|
|
63
|
+
else urlPatternPath = `${normalizedPattern}{/*}?`;
|
|
64
|
+
const match = new URLPattern({ pathname: urlPatternPath }).exec({ pathname });
|
|
65
|
+
if (!match) return {
|
|
66
|
+
matched: false,
|
|
67
|
+
params: {},
|
|
68
|
+
consumedPathname: ""
|
|
69
|
+
};
|
|
70
|
+
const params = {};
|
|
71
|
+
for (const [key, value] of Object.entries(match.pathname.groups)) if (value !== void 0 && key !== "0") params[key] = value;
|
|
72
|
+
let consumedPathname;
|
|
73
|
+
if (exact) consumedPathname = pathname;
|
|
74
|
+
else if (normalizedPattern === "/") consumedPathname = "/";
|
|
75
|
+
else {
|
|
76
|
+
const patternSegments = normalizedPattern.split("/").filter(Boolean);
|
|
77
|
+
consumedPathname = "/" + pathname.split("/").filter(Boolean).slice(0, patternSegments.length).join("/");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
matched: true,
|
|
81
|
+
params,
|
|
82
|
+
consumedPathname
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/Router.tsx
|
|
88
|
+
/**
|
|
89
|
+
* Check if Navigation API is available.
|
|
90
|
+
*/
|
|
91
|
+
function hasNavigation() {
|
|
92
|
+
return typeof navigation !== "undefined";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Subscribe to Navigation API's currententrychange event.
|
|
96
|
+
*/
|
|
97
|
+
function subscribeToNavigation(callback) {
|
|
98
|
+
if (!hasNavigation()) return () => {};
|
|
99
|
+
navigation.addEventListener("currententrychange", callback);
|
|
100
|
+
return () => {
|
|
101
|
+
if (hasNavigation()) navigation.removeEventListener("currententrychange", callback);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get current navigation entry snapshot.
|
|
106
|
+
*/
|
|
107
|
+
function getNavigationSnapshot() {
|
|
108
|
+
if (!hasNavigation()) return null;
|
|
109
|
+
return navigation.currentEntry;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Server snapshot - Navigation API not available on server.
|
|
113
|
+
*/
|
|
114
|
+
function getServerSnapshot() {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function Router({ routes, children }) {
|
|
118
|
+
const currentEntry = useSyncExternalStore(subscribeToNavigation, getNavigationSnapshot, getServerSnapshot);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!hasNavigation()) return;
|
|
121
|
+
const handleNavigate = (event) => {
|
|
122
|
+
if (!event.canIntercept || event.hashChange) return;
|
|
123
|
+
if (matchRoutes(routes, new URL(event.destination.url).pathname)) event.intercept({ handler: async () => {} });
|
|
124
|
+
};
|
|
125
|
+
navigation.addEventListener("navigate", handleNavigate);
|
|
126
|
+
return () => {
|
|
127
|
+
if (hasNavigation()) navigation.removeEventListener("navigate", handleNavigate);
|
|
128
|
+
};
|
|
129
|
+
}, [routes]);
|
|
130
|
+
const navigate = useCallback((to, options) => {
|
|
131
|
+
if (!hasNavigation()) return;
|
|
132
|
+
navigation.navigate(to, {
|
|
133
|
+
history: options?.replace ? "replace" : "push",
|
|
134
|
+
state: options?.state
|
|
135
|
+
});
|
|
136
|
+
}, []);
|
|
137
|
+
const currentUrl = currentEntry?.url;
|
|
138
|
+
const matchedRoutes = useMemo(() => {
|
|
139
|
+
if (!currentUrl) return null;
|
|
140
|
+
return matchRoutes(routes, new URL(currentUrl).pathname);
|
|
141
|
+
}, [currentUrl, routes]);
|
|
142
|
+
const routerContextValue = useMemo(() => ({
|
|
143
|
+
currentEntry,
|
|
144
|
+
navigate
|
|
145
|
+
}), [currentEntry, navigate]);
|
|
146
|
+
return /* @__PURE__ */ jsxs(RouterContext.Provider, {
|
|
147
|
+
value: routerContextValue,
|
|
148
|
+
children: [matchedRoutes ? /* @__PURE__ */ jsx(RouteRenderer, {
|
|
149
|
+
matchedRoutes,
|
|
150
|
+
index: 0
|
|
151
|
+
}) : null, children]
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Recursively render matched routes with proper context.
|
|
156
|
+
*/
|
|
157
|
+
function RouteRenderer({ matchedRoutes, index }) {
|
|
158
|
+
const match = matchedRoutes[index];
|
|
159
|
+
if (!match) return null;
|
|
160
|
+
const { route, params, pathname } = match;
|
|
161
|
+
const Component = route.component;
|
|
162
|
+
const outlet = index < matchedRoutes.length - 1 ? /* @__PURE__ */ jsx(RouteRenderer, {
|
|
163
|
+
matchedRoutes,
|
|
164
|
+
index: index + 1
|
|
165
|
+
}) : null;
|
|
166
|
+
const routeContextValue = useMemo(() => ({
|
|
167
|
+
params,
|
|
168
|
+
matchedPath: pathname,
|
|
169
|
+
outlet
|
|
170
|
+
}), [
|
|
171
|
+
params,
|
|
172
|
+
pathname,
|
|
173
|
+
outlet
|
|
174
|
+
]);
|
|
175
|
+
return /* @__PURE__ */ jsx(RouteContext.Provider, {
|
|
176
|
+
value: routeContextValue,
|
|
177
|
+
children: Component ? /* @__PURE__ */ jsx(Component, {}) : outlet
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/hooks/useNavigate.ts
|
|
183
|
+
/**
|
|
184
|
+
* Returns a function for programmatic navigation.
|
|
185
|
+
*/
|
|
186
|
+
function useNavigate() {
|
|
187
|
+
const context = useContext(RouterContext);
|
|
188
|
+
if (!context) throw new Error("useNavigate must be used within a Router");
|
|
189
|
+
return context.navigate;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/Link.tsx
|
|
194
|
+
const Link = forwardRef(function Link$1({ to, replace, state, onClick, children, ...rest }, ref) {
|
|
195
|
+
const navigate = useNavigate();
|
|
196
|
+
return /* @__PURE__ */ jsx("a", {
|
|
197
|
+
ref,
|
|
198
|
+
href: to,
|
|
199
|
+
onClick: useCallback((event) => {
|
|
200
|
+
onClick?.(event);
|
|
201
|
+
if (event.defaultPrevented) return;
|
|
202
|
+
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
|
|
203
|
+
if (event.button !== 0) return;
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
navigate(to, {
|
|
206
|
+
replace,
|
|
207
|
+
state
|
|
208
|
+
});
|
|
209
|
+
}, [
|
|
210
|
+
navigate,
|
|
211
|
+
to,
|
|
212
|
+
replace,
|
|
213
|
+
state,
|
|
214
|
+
onClick
|
|
215
|
+
]),
|
|
216
|
+
...rest,
|
|
217
|
+
children
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/Outlet.tsx
|
|
223
|
+
/**
|
|
224
|
+
* Renders the matched child route.
|
|
225
|
+
* Used in layout components to specify where child routes should render.
|
|
226
|
+
*/
|
|
227
|
+
function Outlet() {
|
|
228
|
+
const routeContext = useContext(RouteContext);
|
|
229
|
+
if (!routeContext) return null;
|
|
230
|
+
return routeContext.outlet;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/hooks/useLocation.ts
|
|
235
|
+
/**
|
|
236
|
+
* Returns the current location object.
|
|
237
|
+
*/
|
|
238
|
+
function useLocation() {
|
|
239
|
+
const context = useContext(RouterContext);
|
|
240
|
+
if (!context) throw new Error("useLocation must be used within a Router");
|
|
241
|
+
return useMemo(() => {
|
|
242
|
+
if (!context.currentEntry?.url) return {
|
|
243
|
+
pathname: "/",
|
|
244
|
+
search: "",
|
|
245
|
+
hash: ""
|
|
246
|
+
};
|
|
247
|
+
const url = new URL(context.currentEntry.url);
|
|
248
|
+
return {
|
|
249
|
+
pathname: url.pathname,
|
|
250
|
+
search: url.search,
|
|
251
|
+
hash: url.hash
|
|
252
|
+
};
|
|
253
|
+
}, [context.currentEntry?.url]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/hooks/useParams.ts
|
|
258
|
+
/**
|
|
259
|
+
* Returns route parameters from the matched path.
|
|
260
|
+
*/
|
|
261
|
+
function useParams() {
|
|
262
|
+
const context = useContext(RouteContext);
|
|
263
|
+
if (!context) throw new Error("useParams must be used within a Router");
|
|
264
|
+
return context.params;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/hooks/useSearchParams.ts
|
|
269
|
+
/**
|
|
270
|
+
* Returns and allows manipulation of URL search parameters.
|
|
271
|
+
*/
|
|
272
|
+
function useSearchParams() {
|
|
273
|
+
const context = useContext(RouterContext);
|
|
274
|
+
if (!context) throw new Error("useSearchParams must be used within a Router");
|
|
275
|
+
return [useMemo(() => {
|
|
276
|
+
if (!context.currentEntry?.url) return new URLSearchParams();
|
|
277
|
+
return new URL(context.currentEntry.url).searchParams;
|
|
278
|
+
}, [context.currentEntry?.url]), useCallback((params) => {
|
|
279
|
+
const currentUrl = context.currentEntry?.url;
|
|
280
|
+
if (!currentUrl) return;
|
|
281
|
+
const url = new URL(currentUrl);
|
|
282
|
+
let newParams;
|
|
283
|
+
if (typeof params === "function") {
|
|
284
|
+
const result = params(new URLSearchParams(url.search));
|
|
285
|
+
newParams = result instanceof URLSearchParams ? result : new URLSearchParams(result);
|
|
286
|
+
} else if (params instanceof URLSearchParams) newParams = params;
|
|
287
|
+
else newParams = new URLSearchParams(params);
|
|
288
|
+
url.search = newParams.toString();
|
|
289
|
+
context.navigate(url.pathname + url.search + url.hash, { replace: true });
|
|
290
|
+
}, [context])];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
//#endregion
|
|
294
|
+
export { Link, Outlet, Router, useLocation, useNavigate, useParams, useSearchParams };
|
|
295
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["result: MatchedRoute","urlPatternPath: string","params: Record<string, string>","consumedPathname: string","Link","newParams: URLSearchParams"],"sources":["../src/context/RouterContext.ts","../src/context/RouteContext.ts","../src/core/matchRoutes.ts","../src/Router.tsx","../src/hooks/useNavigate.ts","../src/Link.tsx","../src/Outlet.tsx","../src/hooks/useLocation.ts","../src/hooks/useParams.ts","../src/hooks/useSearchParams.ts"],"sourcesContent":["import { createContext } from \"react\";\nimport type { NavigateOptions } from \"../types.js\";\n\nexport type RouterContextValue = {\n /** Current navigation entry */\n currentEntry: NavigationHistoryEntry | null;\n /** Navigate to a new URL */\n navigate: (to: string, options?: NavigateOptions) => void;\n};\n\nexport const RouterContext = createContext<RouterContextValue | null>(null);\n","import { createContext, type ReactNode } from \"react\";\n\nexport type RouteContextValue = {\n /** Matched route parameters */\n params: Record<string, string>;\n /** The matched path pattern */\n matchedPath: string;\n /** Child route element to render via Outlet */\n outlet: ReactNode;\n};\n\nexport const RouteContext = createContext<RouteContextValue | null>(null);\n","import type { RouteDefinition, MatchedRoute } from \"../types.js\";\n\n/**\n * Match a pathname against a route tree, returning the matched route stack.\n * Returns null if no match is found.\n */\nexport function matchRoutes(\n routes: RouteDefinition[],\n pathname: string,\n): MatchedRoute[] | null {\n for (const route of routes) {\n const matched = matchRoute(route, pathname);\n if (matched) {\n return matched;\n }\n }\n return null;\n}\n\n/**\n * Match a single route and its children recursively.\n */\nfunction matchRoute(\n route: RouteDefinition,\n pathname: string,\n): MatchedRoute[] | null {\n const hasChildren = Boolean(route.children?.length);\n\n // For parent routes (with children), we need to match as a prefix\n // For leaf routes (no children), we need an exact match\n const { matched, params, consumedPathname } = matchPath(\n route.path,\n pathname,\n !hasChildren,\n );\n\n if (!matched) {\n return null;\n }\n\n const result: MatchedRoute = {\n route,\n params,\n pathname: consumedPathname,\n };\n\n // If this route has children, try to match them\n if (hasChildren) {\n // Calculate remaining pathname, ensuring it starts with /\n let remainingPathname = pathname.slice(consumedPathname.length);\n if (!remainingPathname.startsWith(\"/\")) {\n remainingPathname = \"/\" + remainingPathname;\n }\n if (remainingPathname === \"\") {\n remainingPathname = \"/\";\n }\n\n for (const child of route.children!) {\n const childMatch = matchRoute(child, remainingPathname);\n if (childMatch) {\n // Merge params from parent into children\n return [\n result,\n ...childMatch.map((m) => ({\n ...m,\n params: { ...params, ...m.params },\n })),\n ];\n }\n }\n\n // If no children matched but this route has a component, it's still a valid match\n if (route.component) {\n return [result];\n }\n\n return null;\n }\n\n return [result];\n}\n\n/**\n * Match a path pattern against a pathname.\n */\nfunction matchPath(\n pattern: string,\n pathname: string,\n exact: boolean,\n): {\n matched: boolean;\n params: Record<string, string>;\n consumedPathname: string;\n} {\n // Normalize pattern\n const normalizedPattern = pattern.startsWith(\"/\") ? pattern : `/${pattern}`;\n\n // Build URLPattern\n let urlPatternPath: string;\n if (exact) {\n urlPatternPath = normalizedPattern;\n } else if (normalizedPattern === \"/\") {\n // Special case: root path as prefix matches anything\n urlPatternPath = \"/*\";\n } else {\n // For other prefix matches, add optional wildcard suffix\n urlPatternPath = `${normalizedPattern}{/*}?`;\n }\n\n const urlPattern = new URLPattern({ pathname: urlPatternPath });\n\n const match = urlPattern.exec({ pathname });\n if (!match) {\n return { matched: false, params: {}, consumedPathname: \"\" };\n }\n\n // Extract params (excluding the wildcard group \"0\")\n const params: Record<string, string> = {};\n for (const [key, value] of Object.entries(match.pathname.groups)) {\n if (value !== undefined && key !== \"0\") {\n params[key] = value;\n }\n }\n\n // Calculate consumed pathname\n let consumedPathname: string;\n if (exact) {\n consumedPathname = pathname;\n } else if (normalizedPattern === \"/\") {\n // Root pattern consumes just \"/\"\n consumedPathname = \"/\";\n } else {\n // For prefix matches, calculate based on pattern segments\n const patternSegments = normalizedPattern.split(\"/\").filter(Boolean);\n const pathnameSegments = pathname.split(\"/\").filter(Boolean);\n consumedPathname =\n \"/\" + pathnameSegments.slice(0, patternSegments.length).join(\"/\");\n }\n\n return { matched: true, params, consumedPathname };\n}\n","import {\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport { RouterContext } from \"./context/RouterContext.js\";\nimport { RouteContext } from \"./context/RouteContext.js\";\nimport type {\n RouteDefinition,\n NavigateOptions,\n MatchedRoute,\n} from \"./types.js\";\nimport { matchRoutes } from \"./core/matchRoutes.js\";\n\nexport type RouterProps = {\n routes: RouteDefinition[];\n children?: ReactNode;\n};\n\n/**\n * Check if Navigation API is available.\n */\nfunction hasNavigation(): boolean {\n return typeof navigation !== \"undefined\";\n}\n\n/**\n * Subscribe to Navigation API's currententrychange event.\n */\nfunction subscribeToNavigation(callback: () => void): () => void {\n if (!hasNavigation()) {\n return () => {};\n }\n navigation.addEventListener(\"currententrychange\", callback);\n return () => {\n if (hasNavigation()) {\n navigation.removeEventListener(\"currententrychange\", callback);\n }\n };\n}\n\n/**\n * Get current navigation entry snapshot.\n */\nfunction getNavigationSnapshot(): NavigationHistoryEntry | null {\n if (!hasNavigation()) {\n return null;\n }\n return navigation.currentEntry;\n}\n\n/**\n * Server snapshot - Navigation API not available on server.\n */\nfunction getServerSnapshot(): null {\n return null;\n}\n\nexport function Router({ routes, children }: RouterProps): ReactNode {\n const currentEntry = useSyncExternalStore(\n subscribeToNavigation,\n getNavigationSnapshot,\n getServerSnapshot,\n );\n\n // Set up navigation interception\n useEffect(() => {\n if (!hasNavigation()) {\n return;\n }\n\n const handleNavigate = (event: NavigateEvent) => {\n // Only intercept same-origin navigations\n if (!event.canIntercept || event.hashChange) {\n return;\n }\n\n // Check if the URL matches any of our routes\n const url = new URL(event.destination.url);\n const matched = matchRoutes(routes, url.pathname);\n\n if (matched) {\n event.intercept({\n handler: async () => {\n // Navigation will complete and currententrychange will fire\n },\n });\n }\n };\n\n navigation.addEventListener(\"navigate\", handleNavigate);\n return () => {\n if (hasNavigation()) {\n navigation.removeEventListener(\"navigate\", handleNavigate);\n }\n };\n }, [routes]);\n\n // Navigate function for programmatic navigation\n const navigate = useCallback((to: string, options?: NavigateOptions) => {\n if (!hasNavigation()) {\n return;\n }\n navigation.navigate(to, {\n history: options?.replace ? \"replace\" : \"push\",\n state: options?.state,\n });\n }, []);\n\n // Match current URL against routes\n const currentUrl = currentEntry?.url;\n const matchedRoutes = useMemo(() => {\n if (!currentUrl) return null;\n const url = new URL(currentUrl);\n return matchRoutes(routes, url.pathname);\n }, [currentUrl, routes]);\n\n const routerContextValue = useMemo(\n () => ({ currentEntry, navigate }),\n [currentEntry, navigate],\n );\n\n return (\n <RouterContext.Provider value={routerContextValue}>\n {matchedRoutes ? (\n <RouteRenderer matchedRoutes={matchedRoutes} index={0} />\n ) : null}\n {children}\n </RouterContext.Provider>\n );\n}\n\ntype RouteRendererProps = {\n matchedRoutes: MatchedRoute[];\n index: number;\n};\n\n/**\n * Recursively render matched routes with proper context.\n */\nfunction RouteRenderer({\n matchedRoutes,\n index,\n}: RouteRendererProps): ReactNode {\n const match = matchedRoutes[index];\n if (!match) return null;\n\n const { route, params, pathname } = match;\n const Component = route.component;\n\n // Create outlet for child routes\n const outlet =\n index < matchedRoutes.length - 1 ? (\n <RouteRenderer matchedRoutes={matchedRoutes} index={index + 1} />\n ) : null;\n\n const routeContextValue = useMemo(\n () => ({ params, matchedPath: pathname, outlet }),\n [params, pathname, outlet],\n );\n\n return (\n <RouteContext.Provider value={routeContextValue}>\n {Component ? <Component /> : outlet}\n </RouteContext.Provider>\n );\n}\n","import { useContext } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\nimport type { NavigateOptions } from \"../types.js\";\n\n/**\n * Returns a function for programmatic navigation.\n */\nexport function useNavigate(): (to: string, options?: NavigateOptions) => void {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useNavigate must be used within a Router\");\n }\n\n return context.navigate;\n}\n","import {\n type AnchorHTMLAttributes,\n type MouseEvent,\n type ReactNode,\n forwardRef,\n useCallback,\n} from \"react\";\nimport { useNavigate } from \"./hooks/useNavigate.js\";\n\nexport type LinkProps = Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n \"href\"\n> & {\n /** The destination URL */\n to: string;\n /** Replace current history entry instead of pushing */\n replace?: boolean;\n /** State to associate with the navigation */\n state?: unknown;\n children?: ReactNode;\n};\n\nexport const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(\n { to, replace, state, onClick, children, ...rest },\n ref,\n) {\n const navigate = useNavigate();\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLAnchorElement>) => {\n // Call user's onClick handler if provided\n onClick?.(event);\n\n // Don't handle if default was prevented\n if (event.defaultPrevented) return;\n\n // Don't handle modified clicks (new tab, etc.)\n if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {\n return;\n }\n\n // Don't handle right clicks\n if (event.button !== 0) return;\n\n // Prevent default and navigate via Navigation API\n event.preventDefault();\n navigate(to, { replace, state });\n },\n [navigate, to, replace, state, onClick],\n );\n\n return (\n <a ref={ref} href={to} onClick={handleClick} {...rest}>\n {children}\n </a>\n );\n});\n","import { type ReactNode, useContext } from \"react\";\nimport { RouteContext } from \"./context/RouteContext.js\";\n\n/**\n * Renders the matched child route.\n * Used in layout components to specify where child routes should render.\n */\nexport function Outlet(): ReactNode {\n const routeContext = useContext(RouteContext);\n\n if (!routeContext) {\n return null;\n }\n\n return routeContext.outlet;\n}\n","import { useContext, useMemo } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\nimport type { Location } from \"../types.js\";\n\n/**\n * Returns the current location object.\n */\nexport function useLocation(): Location {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useLocation must be used within a Router\");\n }\n\n return useMemo(() => {\n if (!context.currentEntry?.url) {\n return { pathname: \"/\", search: \"\", hash: \"\" };\n }\n\n const url = new URL(context.currentEntry.url);\n return {\n pathname: url.pathname,\n search: url.search,\n hash: url.hash,\n };\n }, [context.currentEntry?.url]);\n}\n","import { useContext } from \"react\";\nimport { RouteContext } from \"../context/RouteContext.js\";\n\n/**\n * Returns route parameters from the matched path.\n */\nexport function useParams<\n T extends Record<string, string> = Record<string, string>,\n>(): T {\n const context = useContext(RouteContext);\n\n if (!context) {\n throw new Error(\"useParams must be used within a Router\");\n }\n\n return context.params as T;\n}\n","import { useCallback, useContext, useMemo } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\n\ntype SetSearchParams = (\n params:\n | URLSearchParams\n | Record<string, string>\n | ((prev: URLSearchParams) => URLSearchParams | Record<string, string>),\n) => void;\n\n/**\n * Returns and allows manipulation of URL search parameters.\n */\nexport function useSearchParams(): [URLSearchParams, SetSearchParams] {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useSearchParams must be used within a Router\");\n }\n\n const searchParams = useMemo(() => {\n if (!context.currentEntry?.url) {\n return new URLSearchParams();\n }\n const url = new URL(context.currentEntry.url);\n return url.searchParams;\n }, [context.currentEntry?.url]);\n\n const setSearchParams = useCallback<SetSearchParams>(\n (params) => {\n const currentUrl = context.currentEntry?.url;\n if (!currentUrl) return;\n\n const url = new URL(currentUrl);\n\n let newParams: URLSearchParams;\n if (typeof params === \"function\") {\n const result = params(new URLSearchParams(url.search));\n newParams =\n result instanceof URLSearchParams\n ? result\n : new URLSearchParams(result);\n } else if (params instanceof URLSearchParams) {\n newParams = params;\n } else {\n newParams = new URLSearchParams(params);\n }\n\n url.search = newParams.toString();\n context.navigate(url.pathname + url.search + url.hash, { replace: true });\n },\n [context],\n );\n\n return [searchParams, setSearchParams];\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@funstack/router",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "A modern React router based on the Navigation API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
17
|
+
"dev": "tsdown --watch",
|
|
18
|
+
"test": "vitest",
|
|
19
|
+
"test:run": "vitest run",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"format": "prettier --write .",
|
|
22
|
+
"format:check": "prettier --check ."
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"react",
|
|
26
|
+
"router",
|
|
27
|
+
"navigation-api"
|
|
28
|
+
],
|
|
29
|
+
"author": "uhyo <uhyo@uhy.ooo>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
36
|
+
"@testing-library/react": "^16.3.0",
|
|
37
|
+
"@types/react": "^19.0.0",
|
|
38
|
+
"jsdom": "^27.3.0",
|
|
39
|
+
"prettier": "^3.7.4",
|
|
40
|
+
"react": "^19.0.0",
|
|
41
|
+
"tsdown": "^0.17.2",
|
|
42
|
+
"typescript": "^5.7.0",
|
|
43
|
+
"urlpattern-polyfill": "^10.1.0",
|
|
44
|
+
"vitest": "^4.0.15"
|
|
45
|
+
}
|
|
46
|
+
}
|