@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 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)
@@ -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