@revealui/router 0.2.0 → 0.3.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/dist/index.d.ts +12 -3
- package/dist/index.js +253 -36
- package/dist/index.js.map +1 -1
- package/dist/router-B2MrlNC3.d.ts +187 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +239 -60
- package/dist/server.js.map +1 -1
- package/package.json +13 -7
- package/dist/router-DctgwX83.d.ts +0 -126
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware function — runs before route resolution.
|
|
5
|
+
* Return `true` to continue, `false` to abort, or a redirect path string.
|
|
6
|
+
*/
|
|
7
|
+
type RouteMiddleware = (context: MiddlewareContext) => boolean | string | Promise<boolean | string>;
|
|
8
|
+
/**
|
|
9
|
+
* Context passed to middleware functions
|
|
10
|
+
*/
|
|
11
|
+
interface MiddlewareContext {
|
|
12
|
+
/** Current URL pathname */
|
|
13
|
+
pathname: string;
|
|
14
|
+
/** Matched route params (if available at this stage) */
|
|
15
|
+
params: RouteParams;
|
|
16
|
+
/** Route metadata */
|
|
17
|
+
meta?: RouteMeta;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Route configuration
|
|
21
|
+
*/
|
|
22
|
+
interface Route<TData = unknown, TProps = Record<string, unknown>> {
|
|
23
|
+
/** Route path pattern (e.g., '/', '/about', '/posts/:id') */
|
|
24
|
+
path: string;
|
|
25
|
+
/** Component to render for this route */
|
|
26
|
+
component: ComponentType<TProps>;
|
|
27
|
+
/** Optional layout component */
|
|
28
|
+
layout?: ComponentType<{
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}>;
|
|
31
|
+
/** Optional data loader function */
|
|
32
|
+
loader?: (params: RouteParams) => Promise<TData> | TData;
|
|
33
|
+
/** Optional metadata */
|
|
34
|
+
meta?: RouteMeta;
|
|
35
|
+
/** Optional middleware that runs before this route's loader */
|
|
36
|
+
middleware?: RouteMiddleware[];
|
|
37
|
+
/** Nested child routes — children inherit parent's layout and middleware */
|
|
38
|
+
children?: Route[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Route parameters extracted from URL
|
|
42
|
+
*/
|
|
43
|
+
interface RouteParams {
|
|
44
|
+
[key: string]: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Route metadata (for SEO, etc.)
|
|
48
|
+
*/
|
|
49
|
+
interface RouteMeta {
|
|
50
|
+
title?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Matched route result
|
|
56
|
+
*/
|
|
57
|
+
interface RouteMatch<TData = unknown> {
|
|
58
|
+
route: Route<TData>;
|
|
59
|
+
params: RouteParams;
|
|
60
|
+
data?: TData;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Router configuration options
|
|
64
|
+
*/
|
|
65
|
+
interface RouterOptions {
|
|
66
|
+
/** Base URL path */
|
|
67
|
+
basePath?: string;
|
|
68
|
+
/** 404 component */
|
|
69
|
+
notFound?: ComponentType;
|
|
70
|
+
/** Error boundary component */
|
|
71
|
+
errorBoundary?: ComponentType<{
|
|
72
|
+
error: Error;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Navigation options
|
|
77
|
+
*/
|
|
78
|
+
interface NavigateOptions<TState = unknown> {
|
|
79
|
+
/** Replace current history entry instead of pushing */
|
|
80
|
+
replace?: boolean;
|
|
81
|
+
/** State to pass with navigation */
|
|
82
|
+
state?: TState;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Current location state
|
|
86
|
+
*/
|
|
87
|
+
interface Location {
|
|
88
|
+
/** URL pathname (e.g., '/about') */
|
|
89
|
+
pathname: string;
|
|
90
|
+
/** Query string including leading '?' (e.g., '?q=test') or empty string */
|
|
91
|
+
search: string;
|
|
92
|
+
/** Hash fragment including leading '#' (e.g., '#section') or empty string */
|
|
93
|
+
hash: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* RevealUI Router - Lightweight file-based routing with SSR support
|
|
98
|
+
*/
|
|
99
|
+
declare class Router {
|
|
100
|
+
private routes;
|
|
101
|
+
private flatRoutes;
|
|
102
|
+
private globalMiddleware;
|
|
103
|
+
private options;
|
|
104
|
+
private listeners;
|
|
105
|
+
private currentMatch;
|
|
106
|
+
private lastPathname;
|
|
107
|
+
private popstateHandler;
|
|
108
|
+
private clickHandler;
|
|
109
|
+
constructor(options?: RouterOptions);
|
|
110
|
+
/**
|
|
111
|
+
* Add global middleware that runs before all routes.
|
|
112
|
+
*/
|
|
113
|
+
use(...middleware: RouteMiddleware[]): void;
|
|
114
|
+
/**
|
|
115
|
+
* Get router options
|
|
116
|
+
*/
|
|
117
|
+
getOptions(): RouterOptions;
|
|
118
|
+
/**
|
|
119
|
+
* Register a route. Nested children are flattened with combined paths,
|
|
120
|
+
* middleware, and layout chains.
|
|
121
|
+
*/
|
|
122
|
+
register(route: Route): void;
|
|
123
|
+
/**
|
|
124
|
+
* Register multiple routes
|
|
125
|
+
*/
|
|
126
|
+
registerRoutes(routes: Route[]): void;
|
|
127
|
+
/**
|
|
128
|
+
* Flatten nested routes into the flat lookup table.
|
|
129
|
+
* Children inherit parent path prefix, middleware, and layout.
|
|
130
|
+
*/
|
|
131
|
+
private flattenRoute;
|
|
132
|
+
/**
|
|
133
|
+
* Match a URL to a route.
|
|
134
|
+
* Checks flattened routes first (includes nested), then falls back to top-level.
|
|
135
|
+
*/
|
|
136
|
+
match(url: string): RouteMatch | null;
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a route with middleware execution and data loading.
|
|
139
|
+
*
|
|
140
|
+
* Middleware chain: global middleware → route middleware → loader.
|
|
141
|
+
* If any middleware returns `false`, resolution is aborted (returns null).
|
|
142
|
+
* If any middleware returns a string, navigation is redirected to that path.
|
|
143
|
+
*/
|
|
144
|
+
resolve(url: string): Promise<RouteMatch | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Navigate to a URL (client-side only)
|
|
147
|
+
*/
|
|
148
|
+
navigate(url: string, options?: NavigateOptions): void;
|
|
149
|
+
/**
|
|
150
|
+
* Go back in history
|
|
151
|
+
*/
|
|
152
|
+
back(): void;
|
|
153
|
+
/**
|
|
154
|
+
* Go forward in history
|
|
155
|
+
*/
|
|
156
|
+
forward(): void;
|
|
157
|
+
/**
|
|
158
|
+
* Subscribe to route changes
|
|
159
|
+
*/
|
|
160
|
+
subscribe(listener: () => void): () => void;
|
|
161
|
+
/**
|
|
162
|
+
* Get current route match
|
|
163
|
+
*/
|
|
164
|
+
getCurrentMatch(): RouteMatch | null;
|
|
165
|
+
/**
|
|
166
|
+
* Get all registered routes
|
|
167
|
+
*/
|
|
168
|
+
getRoutes(): Route[];
|
|
169
|
+
/**
|
|
170
|
+
* Clear all routes and middleware
|
|
171
|
+
*/
|
|
172
|
+
clear(): void;
|
|
173
|
+
private normalizePath;
|
|
174
|
+
private notifyListeners;
|
|
175
|
+
/**
|
|
176
|
+
* Initialize client-side routing.
|
|
177
|
+
* Uses a global flag to prevent duplicate event listeners on HMR re-invocation.
|
|
178
|
+
*/
|
|
179
|
+
initClient(): void;
|
|
180
|
+
/**
|
|
181
|
+
* Clean up client-side event listeners.
|
|
182
|
+
* Call this before unmounting or during HMR teardown.
|
|
183
|
+
*/
|
|
184
|
+
dispose(): void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { type Location as L, type MiddlewareContext as M, type NavigateOptions as N, Router as R, type RouteMatch as a, type Route as b, type RouteMeta as c, type RouteMiddleware as d, type RouteParams as e, type RouterOptions as f };
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/server.tsx
|
|
9
2
|
import { logger as logger2 } from "@revealui/core/observability/logger";
|
|
10
|
-
import {
|
|
3
|
+
import { renderToReadableStream, renderToString } from "react-dom/server";
|
|
11
4
|
|
|
12
5
|
// src/components.tsx
|
|
13
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Component,
|
|
8
|
+
createContext,
|
|
9
|
+
use,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useSyncExternalStore
|
|
14
|
+
} from "react";
|
|
14
15
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
16
|
var RouterContext = createContext(null);
|
|
16
17
|
var MatchContext = createContext(null);
|
|
@@ -22,6 +23,7 @@ function RouterProvider({
|
|
|
22
23
|
}
|
|
23
24
|
function Routes() {
|
|
24
25
|
const router = useRouter();
|
|
26
|
+
const options = router.getOptions();
|
|
25
27
|
const match = useSyncExternalStore(
|
|
26
28
|
(callback) => router.subscribe(callback),
|
|
27
29
|
() => router.getCurrentMatch(),
|
|
@@ -29,13 +31,15 @@ function Routes() {
|
|
|
29
31
|
// Server-side snapshot (same as client)
|
|
30
32
|
);
|
|
31
33
|
if (!match) {
|
|
32
|
-
|
|
34
|
+
const CustomNotFound = options.notFound;
|
|
35
|
+
return CustomNotFound ? /* @__PURE__ */ jsx(CustomNotFound, {}) : /* @__PURE__ */ jsx(NotFound, {});
|
|
33
36
|
}
|
|
34
37
|
const { route, params, data } = match;
|
|
35
|
-
const
|
|
38
|
+
const RouteComponent = route.component;
|
|
36
39
|
const Layout = route.layout;
|
|
37
|
-
const element = /* @__PURE__ */ jsx(
|
|
38
|
-
|
|
40
|
+
const element = /* @__PURE__ */ jsx(RouteComponent, { params, data });
|
|
41
|
+
const wrapped = Layout ? /* @__PURE__ */ jsx(Layout, { children: element }) : element;
|
|
42
|
+
return /* @__PURE__ */ jsx(MatchContext.Provider, { value: match, children: options.errorBoundary ? /* @__PURE__ */ jsx(RouteErrorBoundary, { fallback: options.errorBoundary, children: wrapped }) : wrapped });
|
|
39
43
|
}
|
|
40
44
|
function Link({
|
|
41
45
|
to,
|
|
@@ -64,7 +68,7 @@ function Link({
|
|
|
64
68
|
return /* @__PURE__ */ jsx("a", { href: to, onClick: handleClick, className, style, ...props, children });
|
|
65
69
|
}
|
|
66
70
|
function useRouter() {
|
|
67
|
-
const router =
|
|
71
|
+
const router = use(RouterContext);
|
|
68
72
|
if (!router) {
|
|
69
73
|
throw new Error("useRouter must be used within a RouterProvider");
|
|
70
74
|
}
|
|
@@ -77,27 +81,97 @@ function NotFound() {
|
|
|
77
81
|
/* @__PURE__ */ jsx(Link, { to: "/", children: "Go Home" })
|
|
78
82
|
] });
|
|
79
83
|
}
|
|
84
|
+
var RouteErrorBoundary = class extends Component {
|
|
85
|
+
constructor(props) {
|
|
86
|
+
super(props);
|
|
87
|
+
this.state = { error: null };
|
|
88
|
+
}
|
|
89
|
+
static getDerivedStateFromError(error) {
|
|
90
|
+
return { error };
|
|
91
|
+
}
|
|
92
|
+
render() {
|
|
93
|
+
if (this.state.error) {
|
|
94
|
+
const Fallback = this.props.fallback;
|
|
95
|
+
return /* @__PURE__ */ jsx(Fallback, { error: this.state.error });
|
|
96
|
+
}
|
|
97
|
+
return this.props.children;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
80
100
|
|
|
81
101
|
// src/router.ts
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
import { logger } from "@revealui/core/observability/logger";
|
|
103
|
+
import { createElement } from "react";
|
|
104
|
+
var MAX_PATTERN_LENGTH = 2048;
|
|
105
|
+
function compilePathPattern(pattern) {
|
|
106
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
107
|
+
throw new Error(`Route pattern exceeds ${MAX_PATTERN_LENGTH} characters`);
|
|
108
|
+
}
|
|
109
|
+
const keys = [];
|
|
110
|
+
let src = "^";
|
|
111
|
+
let i = 0;
|
|
112
|
+
while (i < pattern.length) {
|
|
113
|
+
const ch = pattern[i];
|
|
114
|
+
if (ch === "{") {
|
|
115
|
+
src += "(?:";
|
|
116
|
+
i++;
|
|
117
|
+
} else if (ch === "}") {
|
|
118
|
+
src += ")?";
|
|
119
|
+
i++;
|
|
120
|
+
} else if (ch === ":") {
|
|
121
|
+
i++;
|
|
122
|
+
let name = "";
|
|
123
|
+
while (i < pattern.length && /\w/.test(pattern[i])) name += pattern[i++];
|
|
124
|
+
keys.push({ name, wildcard: false });
|
|
125
|
+
src += "([^/]+)";
|
|
126
|
+
} else if (ch === "*") {
|
|
127
|
+
i++;
|
|
128
|
+
let name = "";
|
|
129
|
+
while (i < pattern.length && /\w/.test(pattern[i])) name += pattern[i++];
|
|
130
|
+
keys.push({ name: name || "0", wildcard: true });
|
|
131
|
+
src += "(.+)";
|
|
132
|
+
} else {
|
|
133
|
+
src += ch.replace(/[.+?^$|()[\]\\]/g, "\\$&");
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
src += "$";
|
|
138
|
+
return { regex: new RegExp(src), keys };
|
|
139
|
+
}
|
|
140
|
+
var patternCache = /* @__PURE__ */ new Map();
|
|
141
|
+
function getCompiledPattern(pattern) {
|
|
142
|
+
let compiled = patternCache.get(pattern);
|
|
143
|
+
if (!compiled) {
|
|
144
|
+
compiled = compilePathPattern(pattern);
|
|
145
|
+
patternCache.set(pattern, compiled);
|
|
146
|
+
}
|
|
147
|
+
return compiled;
|
|
148
|
+
}
|
|
149
|
+
function pathMatch(pattern, options = {}) {
|
|
150
|
+
const { regex, keys } = getCompiledPattern(pattern);
|
|
151
|
+
const decode = options.decode ?? ((s) => s);
|
|
152
|
+
return (path) => {
|
|
153
|
+
const m = regex.exec(path);
|
|
154
|
+
if (!m) return false;
|
|
155
|
+
const params = {};
|
|
156
|
+
for (let j = 0; j < keys.length; j++) {
|
|
157
|
+
const key = keys[j];
|
|
158
|
+
const val = m[j + 1];
|
|
159
|
+
if (val === void 0) continue;
|
|
160
|
+
params[key.name] = key.wildcard ? val.split("/").map(decode) : decode(val);
|
|
161
|
+
}
|
|
162
|
+
return { params };
|
|
163
|
+
};
|
|
164
|
+
}
|
|
96
165
|
var Router = class {
|
|
97
166
|
routes = [];
|
|
167
|
+
flatRoutes = [];
|
|
168
|
+
globalMiddleware = [];
|
|
98
169
|
options;
|
|
99
170
|
listeners = /* @__PURE__ */ new Set();
|
|
100
171
|
currentMatch = null;
|
|
172
|
+
lastPathname = null;
|
|
173
|
+
popstateHandler = null;
|
|
174
|
+
clickHandler = null;
|
|
101
175
|
constructor(options = {}) {
|
|
102
176
|
this.options = {
|
|
103
177
|
basePath: "",
|
|
@@ -105,10 +179,24 @@ var Router = class {
|
|
|
105
179
|
};
|
|
106
180
|
}
|
|
107
181
|
/**
|
|
108
|
-
*
|
|
182
|
+
* Add global middleware that runs before all routes.
|
|
183
|
+
*/
|
|
184
|
+
use(...middleware) {
|
|
185
|
+
this.globalMiddleware.push(...middleware);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get router options
|
|
189
|
+
*/
|
|
190
|
+
getOptions() {
|
|
191
|
+
return this.options;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Register a route. Nested children are flattened with combined paths,
|
|
195
|
+
* middleware, and layout chains.
|
|
109
196
|
*/
|
|
110
197
|
register(route) {
|
|
111
198
|
this.routes.push(route);
|
|
199
|
+
this.flattenRoute(route, "", [], void 0);
|
|
112
200
|
}
|
|
113
201
|
/**
|
|
114
202
|
* Register multiple routes
|
|
@@ -119,11 +207,37 @@ var Router = class {
|
|
|
119
207
|
});
|
|
120
208
|
}
|
|
121
209
|
/**
|
|
122
|
-
*
|
|
210
|
+
* Flatten nested routes into the flat lookup table.
|
|
211
|
+
* Children inherit parent path prefix, middleware, and layout.
|
|
212
|
+
*/
|
|
213
|
+
flattenRoute(route, parentPath, parentMiddleware, parentLayout) {
|
|
214
|
+
const fullPath = joinPaths(parentPath, route.path);
|
|
215
|
+
const combinedMiddleware = [...parentMiddleware, ...route.middleware ?? []];
|
|
216
|
+
const effectiveLayout = parentLayout && route.layout ? wrapLayouts(parentLayout, route.layout) : route.layout ?? parentLayout;
|
|
217
|
+
if (route.component) {
|
|
218
|
+
this.flatRoutes.push({
|
|
219
|
+
...route,
|
|
220
|
+
path: fullPath,
|
|
221
|
+
middleware: combinedMiddleware.length > 0 ? combinedMiddleware : void 0,
|
|
222
|
+
layout: effectiveLayout,
|
|
223
|
+
children: void 0
|
|
224
|
+
// Already flattened
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (route.children) {
|
|
228
|
+
for (const child of route.children) {
|
|
229
|
+
this.flattenRoute(child, fullPath, combinedMiddleware, effectiveLayout);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Match a URL to a route.
|
|
235
|
+
* Checks flattened routes first (includes nested), then falls back to top-level.
|
|
123
236
|
*/
|
|
124
237
|
match(url) {
|
|
125
238
|
const path = this.normalizePath(url);
|
|
126
|
-
|
|
239
|
+
const allRoutes = this.flatRoutes.length > 0 ? this.flatRoutes : this.routes;
|
|
240
|
+
for (const route of allRoutes) {
|
|
127
241
|
const matcher = pathMatch(route.path, { decode: decodeURIComponent });
|
|
128
242
|
const result = matcher(path);
|
|
129
243
|
if (result) {
|
|
@@ -136,13 +250,35 @@ var Router = class {
|
|
|
136
250
|
return null;
|
|
137
251
|
}
|
|
138
252
|
/**
|
|
139
|
-
* Resolve a route with data loading
|
|
253
|
+
* Resolve a route with middleware execution and data loading.
|
|
254
|
+
*
|
|
255
|
+
* Middleware chain: global middleware → route middleware → loader.
|
|
256
|
+
* If any middleware returns `false`, resolution is aborted (returns null).
|
|
257
|
+
* If any middleware returns a string, navigation is redirected to that path.
|
|
140
258
|
*/
|
|
141
259
|
async resolve(url) {
|
|
142
260
|
const matched = this.match(url);
|
|
143
261
|
if (!matched) {
|
|
144
262
|
return null;
|
|
145
263
|
}
|
|
264
|
+
const allMiddleware = [...this.globalMiddleware, ...matched.route.middleware ?? []];
|
|
265
|
+
if (allMiddleware.length > 0) {
|
|
266
|
+
const context = {
|
|
267
|
+
pathname: this.normalizePath(url),
|
|
268
|
+
params: matched.params,
|
|
269
|
+
meta: matched.route.meta
|
|
270
|
+
};
|
|
271
|
+
for (const mw of allMiddleware) {
|
|
272
|
+
const result = await mw(context);
|
|
273
|
+
if (result === false) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (typeof result === "string") {
|
|
277
|
+
this.navigate(result);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
146
282
|
if (matched.route.loader) {
|
|
147
283
|
try {
|
|
148
284
|
matched.data = await matched.route.loader(matched.params);
|
|
@@ -154,9 +290,7 @@ var Router = class {
|
|
|
154
290
|
throw error;
|
|
155
291
|
}
|
|
156
292
|
}
|
|
157
|
-
|
|
158
|
-
this.currentMatch = matched;
|
|
159
|
-
}
|
|
293
|
+
this.currentMatch = matched;
|
|
160
294
|
return matched;
|
|
161
295
|
}
|
|
162
296
|
/**
|
|
@@ -172,6 +306,8 @@ var Router = class {
|
|
|
172
306
|
} else {
|
|
173
307
|
window.history.pushState(options.state || null, "", fullUrl);
|
|
174
308
|
}
|
|
309
|
+
this.lastPathname = window.location.pathname;
|
|
310
|
+
this.currentMatch = this.match(window.location.pathname);
|
|
175
311
|
this.notifyListeners();
|
|
176
312
|
}
|
|
177
313
|
/**
|
|
@@ -206,7 +342,12 @@ var Router = class {
|
|
|
206
342
|
if (typeof window === "undefined") {
|
|
207
343
|
return this.currentMatch;
|
|
208
344
|
}
|
|
209
|
-
|
|
345
|
+
const pathname = window.location.pathname;
|
|
346
|
+
if (pathname !== this.lastPathname) {
|
|
347
|
+
this.lastPathname = pathname;
|
|
348
|
+
this.currentMatch = this.match(pathname);
|
|
349
|
+
}
|
|
350
|
+
return this.currentMatch;
|
|
210
351
|
}
|
|
211
352
|
/**
|
|
212
353
|
* Get all registered routes
|
|
@@ -215,10 +356,12 @@ var Router = class {
|
|
|
215
356
|
return [...this.routes];
|
|
216
357
|
}
|
|
217
358
|
/**
|
|
218
|
-
* Clear all routes
|
|
359
|
+
* Clear all routes and middleware
|
|
219
360
|
*/
|
|
220
361
|
clear() {
|
|
221
362
|
this.routes = [];
|
|
363
|
+
this.flatRoutes = [];
|
|
364
|
+
this.globalMiddleware = [];
|
|
222
365
|
}
|
|
223
366
|
normalizePath(url) {
|
|
224
367
|
let path = url;
|
|
@@ -237,16 +380,23 @@ var Router = class {
|
|
|
237
380
|
});
|
|
238
381
|
}
|
|
239
382
|
/**
|
|
240
|
-
* Initialize client-side routing
|
|
383
|
+
* Initialize client-side routing.
|
|
384
|
+
* Uses a global flag to prevent duplicate event listeners on HMR re-invocation.
|
|
241
385
|
*/
|
|
242
386
|
initClient() {
|
|
243
387
|
if (typeof window === "undefined") {
|
|
244
388
|
return;
|
|
245
389
|
}
|
|
246
|
-
|
|
390
|
+
const g = globalThis;
|
|
391
|
+
if (g.__revealui_router_initialized) return;
|
|
392
|
+
g.__revealui_router_initialized = true;
|
|
393
|
+
this.popstateHandler = () => {
|
|
394
|
+
this.lastPathname = window.location.pathname;
|
|
395
|
+
this.currentMatch = this.match(window.location.pathname);
|
|
247
396
|
this.notifyListeners();
|
|
248
|
-
}
|
|
249
|
-
|
|
397
|
+
};
|
|
398
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
399
|
+
this.clickHandler = (e) => {
|
|
250
400
|
const target = e.target.closest("a");
|
|
251
401
|
if (!target) return;
|
|
252
402
|
const href = target.getAttribute("href");
|
|
@@ -254,9 +404,37 @@ var Router = class {
|
|
|
254
404
|
e.preventDefault();
|
|
255
405
|
this.navigate(href);
|
|
256
406
|
}
|
|
257
|
-
}
|
|
407
|
+
};
|
|
408
|
+
document.addEventListener("click", this.clickHandler);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Clean up client-side event listeners.
|
|
412
|
+
* Call this before unmounting or during HMR teardown.
|
|
413
|
+
*/
|
|
414
|
+
dispose() {
|
|
415
|
+
if (typeof window === "undefined") return;
|
|
416
|
+
if (this.popstateHandler) {
|
|
417
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
418
|
+
this.popstateHandler = null;
|
|
419
|
+
}
|
|
420
|
+
if (this.clickHandler) {
|
|
421
|
+
document.removeEventListener("click", this.clickHandler);
|
|
422
|
+
this.clickHandler = null;
|
|
423
|
+
}
|
|
424
|
+
this.listeners.clear();
|
|
425
|
+
const g = globalThis;
|
|
426
|
+
g.__revealui_router_initialized = false;
|
|
258
427
|
}
|
|
259
428
|
};
|
|
429
|
+
function joinPaths(parent, child) {
|
|
430
|
+
if (!parent || parent === "/") return child;
|
|
431
|
+
if (!child || child === "/") return parent;
|
|
432
|
+
return `${parent.replace(/\/$/, "")}/${child.replace(/^\//, "")}`;
|
|
433
|
+
}
|
|
434
|
+
function wrapLayouts(Parent, Child) {
|
|
435
|
+
const WrappedLayout = ({ children }) => createElement(Parent, null, createElement(Child, null, children));
|
|
436
|
+
return WrappedLayout;
|
|
437
|
+
}
|
|
260
438
|
|
|
261
439
|
// src/server.tsx
|
|
262
440
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
@@ -294,24 +472,19 @@ function createSSRHandler(routes, options = {}) {
|
|
|
294
472
|
return c.html(template("<div>404 - Page Not Found</div>"));
|
|
295
473
|
}
|
|
296
474
|
if (options.streaming) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
resolve(c.body(html2));
|
|
305
|
-
},
|
|
306
|
-
onError(error) {
|
|
307
|
-
logger2.error("SSR error", error instanceof Error ? error : new Error(String(error)));
|
|
308
|
-
if (options.onError) {
|
|
309
|
-
options.onError(error, c);
|
|
310
|
-
}
|
|
311
|
-
reject(error);
|
|
475
|
+
const stream = await renderToReadableStream(
|
|
476
|
+
/* @__PURE__ */ jsx2(RouterProvider, { router, children: /* @__PURE__ */ jsx2(Routes, {}) }),
|
|
477
|
+
{
|
|
478
|
+
onError(error) {
|
|
479
|
+
logger2.error("SSR error", error instanceof Error ? error : new Error(String(error)));
|
|
480
|
+
if (options.onError) {
|
|
481
|
+
options.onError(error, c);
|
|
312
482
|
}
|
|
313
483
|
}
|
|
314
|
-
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
return new Response(stream, {
|
|
487
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
315
488
|
});
|
|
316
489
|
}
|
|
317
490
|
const html = renderToString(
|
|
@@ -354,9 +527,15 @@ async function hydrate(router, rootElement = null) {
|
|
|
354
527
|
return;
|
|
355
528
|
}
|
|
356
529
|
const dataScript = document.getElementById("__REVEALUI_DATA__");
|
|
357
|
-
const
|
|
530
|
+
const ssrData = dataScript ? JSON.parse(dataScript.textContent || "{}") : {};
|
|
531
|
+
if (ssrData.route) {
|
|
532
|
+
const match = router.match(window.location.pathname);
|
|
533
|
+
if (match) {
|
|
534
|
+
match.data = ssrData.data;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
358
537
|
router.initClient();
|
|
359
|
-
const { hydrateRoot } =
|
|
538
|
+
const { hydrateRoot } = await import("react-dom/client");
|
|
360
539
|
hydrateRoot(
|
|
361
540
|
root,
|
|
362
541
|
/* @__PURE__ */ jsx2(RouterProvider, { router, children: /* @__PURE__ */ jsx2(Routes, {}) })
|