@revealui/router 0.2.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/LICENSE +22 -0
- package/README.md +353 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/router-DctgwX83.d.ts +126 -0
- package/dist/server.d.ts +32 -0
- package/dist/server.js +371 -0
- package/dist/server.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RevealUI Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# @revealui/router
|
|
2
|
+
|
|
3
|
+
Lightweight, type-safe file-based router for RevealUI with built-in SSR support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Simple & Fast** - Minimal API, maximum performance
|
|
8
|
+
- 📁 **File-based routing** - Convention over configuration
|
|
9
|
+
- 🔒 **Type-safe** - Full TypeScript support
|
|
10
|
+
- 🌊 **SSR & Hydration** - Built-in server-side rendering with React
|
|
11
|
+
- 🎯 **Hono Integration** - First-class support for Hono server
|
|
12
|
+
- 📦 **No dependencies** - Except React and path-to-regexp
|
|
13
|
+
- ⚡ **Code splitting ready** - Supports lazy loading
|
|
14
|
+
- 🔗 **Data loading** - Built-in loader support per route
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @revealui/router
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Define Your Routes
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { Router, type Route } from '@revealui/router'
|
|
28
|
+
import Home from './pages/Home'
|
|
29
|
+
import About from './pages/About'
|
|
30
|
+
import Post from './pages/Post'
|
|
31
|
+
|
|
32
|
+
const routes: Route[] = [
|
|
33
|
+
{
|
|
34
|
+
path: '/',
|
|
35
|
+
component: Home,
|
|
36
|
+
meta: { title: 'Home' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: '/about',
|
|
40
|
+
component: About,
|
|
41
|
+
meta: { title: 'About Us' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: '/posts/:id',
|
|
45
|
+
component: Post,
|
|
46
|
+
loader: async ({ id }) => {
|
|
47
|
+
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
|
|
48
|
+
return { post }
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const router = new Router()
|
|
54
|
+
router.registerRoutes(routes)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Client-Side Usage
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { RouterProvider, Routes, Link } from '@revealui/router'
|
|
61
|
+
|
|
62
|
+
function App() {
|
|
63
|
+
return (
|
|
64
|
+
<RouterProvider router={router}>
|
|
65
|
+
<nav>
|
|
66
|
+
<Link to="/">Home</Link>
|
|
67
|
+
<Link to="/about">About</Link>
|
|
68
|
+
</nav>
|
|
69
|
+
<Routes />
|
|
70
|
+
</RouterProvider>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Server-Side Rendering (SSR)
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Hono } from 'hono'
|
|
79
|
+
import { createSSRHandler } from '@revealui/router/server'
|
|
80
|
+
import routes from './routes'
|
|
81
|
+
|
|
82
|
+
const app = new Hono()
|
|
83
|
+
|
|
84
|
+
app.get('*', createSSRHandler(routes, {
|
|
85
|
+
template: (html, data) => `
|
|
86
|
+
<!DOCTYPE html>
|
|
87
|
+
<html>
|
|
88
|
+
<head>
|
|
89
|
+
<title>${data?.title || 'My App'}</title>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div id="root">${html}</div>
|
|
93
|
+
<script id="__REVEALUI_DATA__" type="application/json">
|
|
94
|
+
${JSON.stringify(data)}
|
|
95
|
+
</script>
|
|
96
|
+
<script type="module" src="/client.js"></script>
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
99
|
+
`,
|
|
100
|
+
}))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API Reference
|
|
104
|
+
|
|
105
|
+
### Router
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const router = new Router(options)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Methods:**
|
|
112
|
+
|
|
113
|
+
- `register(route: Route)` - Register a single route
|
|
114
|
+
- `registerRoutes(routes: Route[])` - Register multiple routes
|
|
115
|
+
- `match(url: string)` - Match a URL to a route
|
|
116
|
+
- `resolve(url: string)` - Match and load route data
|
|
117
|
+
- `navigate(url: string, options?)` - Client-side navigation
|
|
118
|
+
- `back()` / `forward()` - Browser history navigation
|
|
119
|
+
- `subscribe(listener)` - Subscribe to route changes
|
|
120
|
+
- `initClient()` - Initialize client-side routing
|
|
121
|
+
|
|
122
|
+
### Components
|
|
123
|
+
|
|
124
|
+
#### `<RouterProvider>`
|
|
125
|
+
|
|
126
|
+
Provides router instance to your app:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
<RouterProvider router={router}>
|
|
130
|
+
<App />
|
|
131
|
+
</RouterProvider>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `<Routes>`
|
|
135
|
+
|
|
136
|
+
Renders the matched route component:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
<Routes />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### `<Link>`
|
|
143
|
+
|
|
144
|
+
Client-side navigation link:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
<Link to="/about" replace={false}>
|
|
148
|
+
About Us
|
|
149
|
+
</Link>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `<Navigate>`
|
|
153
|
+
|
|
154
|
+
Declarative navigation:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
<Navigate to="/login" replace />
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Hooks
|
|
161
|
+
|
|
162
|
+
#### `useRouter()`
|
|
163
|
+
|
|
164
|
+
Access the router instance:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const router = useRouter()
|
|
168
|
+
router.navigate('/about')
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `useParams()`
|
|
172
|
+
|
|
173
|
+
Get route parameters:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const { id } = useParams<{ id: string }>()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### `useData()`
|
|
180
|
+
|
|
181
|
+
Get route data from loader:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const { post } = useData<{ post: Post }>()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### `useMatch()`
|
|
188
|
+
|
|
189
|
+
Get current route match:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const match = useMatch()
|
|
193
|
+
console.log(match?.route.path, match?.params)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### `useNavigate()`
|
|
197
|
+
|
|
198
|
+
Get navigation function:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const navigate = useNavigate()
|
|
202
|
+
navigate('/about', { replace: true })
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Route Patterns
|
|
206
|
+
|
|
207
|
+
Supports path-to-regexp patterns:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
'/posts/:id' // Named parameter
|
|
211
|
+
'/posts/:id?' // Optional parameter
|
|
212
|
+
'/posts/:id(\\d+)' // Parameter with regex
|
|
213
|
+
'/posts/*' // Wildcard
|
|
214
|
+
'/posts/:path*' // Wildcard with name
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Data Loading
|
|
218
|
+
|
|
219
|
+
Routes can have loaders for data fetching:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
{
|
|
223
|
+
path: '/user/:id',
|
|
224
|
+
component: UserProfile,
|
|
225
|
+
loader: async ({ id }) => {
|
|
226
|
+
const user = await fetchUser(id)
|
|
227
|
+
return { user }
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Access data in your component:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
function UserProfile() {
|
|
236
|
+
const { user } = useData<{ user: User }>()
|
|
237
|
+
return <div>{user.name}</div>
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Layouts
|
|
242
|
+
|
|
243
|
+
Wrap routes with layouts:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
{
|
|
247
|
+
path: '/dashboard',
|
|
248
|
+
component: Dashboard,
|
|
249
|
+
layout: DashboardLayout,
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Layout component:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
257
|
+
return (
|
|
258
|
+
<div className="dashboard">
|
|
259
|
+
<Sidebar />
|
|
260
|
+
<main>{children}</main>
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## SSR with Streaming
|
|
267
|
+
|
|
268
|
+
Enable streaming SSR for better performance:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
createSSRHandler(routes, {
|
|
272
|
+
streaming: true,
|
|
273
|
+
onError: (error, context) => {
|
|
274
|
+
console.error('SSR Error:', error)
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Dev Server
|
|
280
|
+
|
|
281
|
+
Quick development server:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { createDevServer } from '@revealui/router/server'
|
|
285
|
+
|
|
286
|
+
await createDevServer(routes, {
|
|
287
|
+
port: 3000,
|
|
288
|
+
template: (html, data) => `...`,
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Integration with RevealUI
|
|
293
|
+
|
|
294
|
+
Works seamlessly with other RevealUI packages:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Router } from '@revealui/router'
|
|
298
|
+
import { getRevealUI } from '@revealui/core'
|
|
299
|
+
|
|
300
|
+
const router = new Router()
|
|
301
|
+
|
|
302
|
+
router.register({
|
|
303
|
+
path: '/cms/:slug',
|
|
304
|
+
component: CMSPage,
|
|
305
|
+
loader: async ({ slug }) => {
|
|
306
|
+
const revealui = await getRevealUI()
|
|
307
|
+
const page = await revealui.find({
|
|
308
|
+
collection: 'pages',
|
|
309
|
+
where: { slug: { equals: slug } },
|
|
310
|
+
})
|
|
311
|
+
return { page: page.docs[0] }
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## TypeScript
|
|
317
|
+
|
|
318
|
+
Full type safety:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import type { Route, RouteParams } from '@revealui/router'
|
|
322
|
+
|
|
323
|
+
interface PostParams extends RouteParams {
|
|
324
|
+
id: string
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const route: Route = {
|
|
328
|
+
path: '/posts/:id',
|
|
329
|
+
component: Post,
|
|
330
|
+
loader: async (params: PostParams) => {
|
|
331
|
+
// params.id is typed as string
|
|
332
|
+
return { post: await fetchPost(params.id) }
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Comparison with Other Routers
|
|
338
|
+
|
|
339
|
+
| Feature | @revealui/router | TanStack Router | React Router |
|
|
340
|
+
|---------|-----------------|-----------------|--------------|
|
|
341
|
+
| Bundle Size | ~5KB | ~50KB | ~20KB |
|
|
342
|
+
| SSR Built-in | ✅ | ⚠️ Requires Start | ⚠️ Complex setup |
|
|
343
|
+
| Type Safety | ✅ | ✅ | ⚠️ Limited |
|
|
344
|
+
| Data Loading | ✅ | ✅ | ✅ |
|
|
345
|
+
| Learning Curve | Low | Medium | Low |
|
|
346
|
+
|
|
347
|
+
## License
|
|
348
|
+
|
|
349
|
+
MIT - RevealUI
|
|
350
|
+
|
|
351
|
+
## Contributing
|
|
352
|
+
|
|
353
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { R as Router, a as RouteMatch, N as NavigateOptions } from './router-DctgwX83.js';
|
|
4
|
+
export { b as Route, c as RouteMeta, d as RouteParams, e as RouterOptions } from './router-DctgwX83.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RouterProvider - Provides router instance to the app
|
|
8
|
+
*/
|
|
9
|
+
declare function RouterProvider({ router, children, }: {
|
|
10
|
+
router: Router;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}): react_jsx_runtime.JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* Routes - Renders the matched route component
|
|
15
|
+
*/
|
|
16
|
+
declare function Routes(): react_jsx_runtime.JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Link - Client-side navigation link
|
|
19
|
+
*/
|
|
20
|
+
declare function Link({ to, replace, children, className, style, onClick, ...props }: {
|
|
21
|
+
to: string;
|
|
22
|
+
replace?: boolean;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
style?: React.CSSProperties;
|
|
26
|
+
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}): react_jsx_runtime.JSX.Element;
|
|
29
|
+
/**
|
|
30
|
+
* useRouter - Hook to access router instance
|
|
31
|
+
*/
|
|
32
|
+
declare function useRouter(): Router;
|
|
33
|
+
/**
|
|
34
|
+
* useMatch - Hook to access current route match
|
|
35
|
+
*/
|
|
36
|
+
declare function useMatch(): RouteMatch | null;
|
|
37
|
+
/**
|
|
38
|
+
* useParams - Hook to access route parameters
|
|
39
|
+
*/
|
|
40
|
+
declare function useParams<T = Record<string, string>>(): T;
|
|
41
|
+
/**
|
|
42
|
+
* useData - Hook to access route data
|
|
43
|
+
*/
|
|
44
|
+
declare function useData<T = unknown>(): T | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* useNavigate - Hook to get navigation function
|
|
47
|
+
*/
|
|
48
|
+
declare function useNavigate(): (to: string, options?: NavigateOptions) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Navigate - Component for declarative navigation
|
|
51
|
+
*/
|
|
52
|
+
declare function Navigate({ to, replace }: {
|
|
53
|
+
to: string;
|
|
54
|
+
replace?: boolean;
|
|
55
|
+
}): null;
|
|
56
|
+
|
|
57
|
+
export { Link, Navigate, NavigateOptions, RouteMatch, Router, RouterProvider, Routes, useData, useMatch, useNavigate, useParams, useRouter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// src/components.tsx
|
|
2
|
+
import { createContext, useContext, useEffect, useSyncExternalStore } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
var RouterContext = createContext(null);
|
|
5
|
+
var MatchContext = createContext(null);
|
|
6
|
+
function RouterProvider({
|
|
7
|
+
router,
|
|
8
|
+
children
|
|
9
|
+
}) {
|
|
10
|
+
return /* @__PURE__ */ jsx(RouterContext.Provider, { value: router, children });
|
|
11
|
+
}
|
|
12
|
+
function Routes() {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const match = useSyncExternalStore(
|
|
15
|
+
(callback) => router.subscribe(callback),
|
|
16
|
+
() => router.getCurrentMatch(),
|
|
17
|
+
() => router.getCurrentMatch()
|
|
18
|
+
// Server-side snapshot (same as client)
|
|
19
|
+
);
|
|
20
|
+
if (!match) {
|
|
21
|
+
return /* @__PURE__ */ jsx(NotFound, {});
|
|
22
|
+
}
|
|
23
|
+
const { route, params, data } = match;
|
|
24
|
+
const Component = route.component;
|
|
25
|
+
const Layout = route.layout;
|
|
26
|
+
const element = /* @__PURE__ */ jsx(Component, { params, data });
|
|
27
|
+
return /* @__PURE__ */ jsx(MatchContext.Provider, { value: match, children: Layout ? /* @__PURE__ */ jsx(Layout, { children: element }) : element });
|
|
28
|
+
}
|
|
29
|
+
function Link({
|
|
30
|
+
to,
|
|
31
|
+
replace = false,
|
|
32
|
+
children,
|
|
33
|
+
className,
|
|
34
|
+
style,
|
|
35
|
+
onClick,
|
|
36
|
+
...props
|
|
37
|
+
}) {
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
const handleClick = (e) => {
|
|
40
|
+
onClick?.(e);
|
|
41
|
+
if (e.defaultPrevented) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (e.button !== 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
router.navigate(to, { replace });
|
|
52
|
+
};
|
|
53
|
+
return /* @__PURE__ */ jsx("a", { href: to, onClick: handleClick, className, style, ...props, children });
|
|
54
|
+
}
|
|
55
|
+
function useRouter() {
|
|
56
|
+
const router = useContext(RouterContext);
|
|
57
|
+
if (!router) {
|
|
58
|
+
throw new Error("useRouter must be used within a RouterProvider");
|
|
59
|
+
}
|
|
60
|
+
return router;
|
|
61
|
+
}
|
|
62
|
+
function useMatch() {
|
|
63
|
+
return useContext(MatchContext);
|
|
64
|
+
}
|
|
65
|
+
function useParams() {
|
|
66
|
+
const match = useMatch();
|
|
67
|
+
return match?.params || {};
|
|
68
|
+
}
|
|
69
|
+
function useData() {
|
|
70
|
+
const match = useMatch();
|
|
71
|
+
return match?.data;
|
|
72
|
+
}
|
|
73
|
+
function useNavigate() {
|
|
74
|
+
const router = useRouter();
|
|
75
|
+
return (to, options) => {
|
|
76
|
+
router.navigate(to, options);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function NotFound() {
|
|
80
|
+
return /* @__PURE__ */ jsxs("div", { style: { padding: "2rem", textAlign: "center" }, children: [
|
|
81
|
+
/* @__PURE__ */ jsx("h1", { children: "404 - Page Not Found" }),
|
|
82
|
+
/* @__PURE__ */ jsx("p", { children: "The page you're looking for doesn't exist." }),
|
|
83
|
+
/* @__PURE__ */ jsx(Link, { to: "/", children: "Go Home" })
|
|
84
|
+
] });
|
|
85
|
+
}
|
|
86
|
+
function Navigate({ to, replace = false }) {
|
|
87
|
+
const router = useRouter();
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
router.navigate(to, { replace });
|
|
90
|
+
}, [to, replace, router]);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/router.ts
|
|
95
|
+
import { match as pathMatch } from "path-to-regexp";
|
|
96
|
+
|
|
97
|
+
// ../core/src/observability/logger.ts
|
|
98
|
+
import { logger as utilsLogger } from "@revealui/utils/logger";
|
|
99
|
+
import {
|
|
100
|
+
createLogger,
|
|
101
|
+
Logger,
|
|
102
|
+
logAudit,
|
|
103
|
+
logError,
|
|
104
|
+
logger,
|
|
105
|
+
logQuery
|
|
106
|
+
} from "@revealui/utils/logger";
|
|
107
|
+
|
|
108
|
+
// src/router.ts
|
|
109
|
+
var Router = class {
|
|
110
|
+
routes = [];
|
|
111
|
+
options;
|
|
112
|
+
listeners = /* @__PURE__ */ new Set();
|
|
113
|
+
currentMatch = null;
|
|
114
|
+
constructor(options = {}) {
|
|
115
|
+
this.options = {
|
|
116
|
+
basePath: "",
|
|
117
|
+
...options
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Register a route
|
|
122
|
+
*/
|
|
123
|
+
register(route) {
|
|
124
|
+
this.routes.push(route);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Register multiple routes
|
|
128
|
+
*/
|
|
129
|
+
registerRoutes(routes) {
|
|
130
|
+
routes.forEach((route) => {
|
|
131
|
+
this.register(route);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Match a URL to a route
|
|
136
|
+
*/
|
|
137
|
+
match(url) {
|
|
138
|
+
const path = this.normalizePath(url);
|
|
139
|
+
for (const route of this.routes) {
|
|
140
|
+
const matcher = pathMatch(route.path, { decode: decodeURIComponent });
|
|
141
|
+
const result = matcher(path);
|
|
142
|
+
if (result) {
|
|
143
|
+
return {
|
|
144
|
+
route,
|
|
145
|
+
params: result.params || {}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve a route with data loading
|
|
153
|
+
*/
|
|
154
|
+
async resolve(url) {
|
|
155
|
+
const matched = this.match(url);
|
|
156
|
+
if (!matched) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (matched.route.loader) {
|
|
160
|
+
try {
|
|
161
|
+
matched.data = await matched.route.loader(matched.params);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.error(
|
|
164
|
+
"Route loader error",
|
|
165
|
+
error instanceof Error ? error : new Error(String(error))
|
|
166
|
+
);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (typeof window === "undefined") {
|
|
171
|
+
this.currentMatch = matched;
|
|
172
|
+
}
|
|
173
|
+
return matched;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Navigate to a URL (client-side only)
|
|
177
|
+
*/
|
|
178
|
+
navigate(url, options = {}) {
|
|
179
|
+
if (typeof window === "undefined") {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const fullUrl = this.options.basePath + url;
|
|
183
|
+
if (options.replace) {
|
|
184
|
+
window.history.replaceState(options.state || null, "", fullUrl);
|
|
185
|
+
} else {
|
|
186
|
+
window.history.pushState(options.state || null, "", fullUrl);
|
|
187
|
+
}
|
|
188
|
+
this.notifyListeners();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Go back in history
|
|
192
|
+
*/
|
|
193
|
+
back() {
|
|
194
|
+
if (typeof window !== "undefined") {
|
|
195
|
+
window.history.back();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Go forward in history
|
|
200
|
+
*/
|
|
201
|
+
forward() {
|
|
202
|
+
if (typeof window !== "undefined") {
|
|
203
|
+
window.history.forward();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Subscribe to route changes
|
|
208
|
+
*/
|
|
209
|
+
subscribe(listener) {
|
|
210
|
+
this.listeners.add(listener);
|
|
211
|
+
return () => {
|
|
212
|
+
this.listeners.delete(listener);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get current route match
|
|
217
|
+
*/
|
|
218
|
+
getCurrentMatch() {
|
|
219
|
+
if (typeof window === "undefined") {
|
|
220
|
+
return this.currentMatch;
|
|
221
|
+
}
|
|
222
|
+
return this.match(window.location.pathname);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get all registered routes
|
|
226
|
+
*/
|
|
227
|
+
getRoutes() {
|
|
228
|
+
return [...this.routes];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Clear all routes
|
|
232
|
+
*/
|
|
233
|
+
clear() {
|
|
234
|
+
this.routes = [];
|
|
235
|
+
}
|
|
236
|
+
normalizePath(url) {
|
|
237
|
+
let path = url;
|
|
238
|
+
if (this.options.basePath && path.startsWith(this.options.basePath)) {
|
|
239
|
+
path = path.slice(this.options.basePath.length);
|
|
240
|
+
}
|
|
241
|
+
path = path.split("?")[0].split("#")[0];
|
|
242
|
+
if (!path.startsWith("/")) {
|
|
243
|
+
path = `/${path}`;
|
|
244
|
+
}
|
|
245
|
+
return path;
|
|
246
|
+
}
|
|
247
|
+
notifyListeners() {
|
|
248
|
+
this.listeners.forEach((listener) => {
|
|
249
|
+
listener();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Initialize client-side routing
|
|
254
|
+
*/
|
|
255
|
+
initClient() {
|
|
256
|
+
if (typeof window === "undefined") {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
window.addEventListener("popstate", () => {
|
|
260
|
+
this.notifyListeners();
|
|
261
|
+
});
|
|
262
|
+
document.addEventListener("click", (e) => {
|
|
263
|
+
const target = e.target.closest("a");
|
|
264
|
+
if (!target) return;
|
|
265
|
+
const href = target.getAttribute("href");
|
|
266
|
+
if (href?.startsWith("/") && !target.hasAttribute("target") && !target.hasAttribute("download") && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
this.navigate(href);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
export {
|
|
274
|
+
Link,
|
|
275
|
+
Navigate,
|
|
276
|
+
Router,
|
|
277
|
+
RouterProvider,
|
|
278
|
+
Routes,
|
|
279
|
+
useData,
|
|
280
|
+
useMatch,
|
|
281
|
+
useNavigate,
|
|
282
|
+
useParams,
|
|
283
|
+
useRouter
|
|
284
|
+
};
|
|
285
|
+
//# sourceMappingURL=index.js.map
|