@revealui/router 0.2.1 → 0.3.2
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 +18 -9
- package/dist/index.js +203 -23
- package/dist/index.js.map +1 -1
- package/dist/router-SBtAXNTB.d.ts +190 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +189 -47
- package/dist/server.js.map +1 -1
- package/package.json +16 -9
- package/dist/router-DctgwX83.d.ts +0 -126
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
declare global {
|
|
97
|
+
var __revealui_router_initialized: boolean | undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* RevealUI Router - Lightweight file-based routing with SSR support
|
|
101
|
+
*/
|
|
102
|
+
declare class Router {
|
|
103
|
+
private routes;
|
|
104
|
+
private flatRoutes;
|
|
105
|
+
private globalMiddleware;
|
|
106
|
+
private options;
|
|
107
|
+
private listeners;
|
|
108
|
+
private currentMatch;
|
|
109
|
+
private lastPathname;
|
|
110
|
+
private popstateHandler;
|
|
111
|
+
private clickHandler;
|
|
112
|
+
constructor(options?: RouterOptions);
|
|
113
|
+
/**
|
|
114
|
+
* Add global middleware that runs before all routes.
|
|
115
|
+
*/
|
|
116
|
+
use(...middleware: RouteMiddleware[]): void;
|
|
117
|
+
/**
|
|
118
|
+
* Get router options
|
|
119
|
+
*/
|
|
120
|
+
getOptions(): RouterOptions;
|
|
121
|
+
/**
|
|
122
|
+
* Register a route. Nested children are flattened with combined paths,
|
|
123
|
+
* middleware, and layout chains.
|
|
124
|
+
*/
|
|
125
|
+
register(route: Route): void;
|
|
126
|
+
/**
|
|
127
|
+
* Register multiple routes
|
|
128
|
+
*/
|
|
129
|
+
registerRoutes(routes: Route[]): void;
|
|
130
|
+
/**
|
|
131
|
+
* Flatten nested routes into the flat lookup table.
|
|
132
|
+
* Children inherit parent path prefix, middleware, and layout.
|
|
133
|
+
*/
|
|
134
|
+
private flattenRoute;
|
|
135
|
+
/**
|
|
136
|
+
* Match a URL to a route.
|
|
137
|
+
* Checks flattened routes first (includes nested), then falls back to top-level.
|
|
138
|
+
*/
|
|
139
|
+
match(url: string): RouteMatch | null;
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a route with middleware execution and data loading.
|
|
142
|
+
*
|
|
143
|
+
* Middleware chain: global middleware → route middleware → loader.
|
|
144
|
+
* If any middleware returns `false`, resolution is aborted (returns null).
|
|
145
|
+
* If any middleware returns a string, navigation is redirected to that path.
|
|
146
|
+
*/
|
|
147
|
+
resolve(url: string): Promise<RouteMatch | null>;
|
|
148
|
+
/**
|
|
149
|
+
* Navigate to a URL (client-side only)
|
|
150
|
+
*/
|
|
151
|
+
navigate(url: string, options?: NavigateOptions): void;
|
|
152
|
+
/**
|
|
153
|
+
* Go back in history
|
|
154
|
+
*/
|
|
155
|
+
back(): void;
|
|
156
|
+
/**
|
|
157
|
+
* Go forward in history
|
|
158
|
+
*/
|
|
159
|
+
forward(): void;
|
|
160
|
+
/**
|
|
161
|
+
* Subscribe to route changes
|
|
162
|
+
*/
|
|
163
|
+
subscribe(listener: () => void): () => void;
|
|
164
|
+
/**
|
|
165
|
+
* Get current route match
|
|
166
|
+
*/
|
|
167
|
+
getCurrentMatch(): RouteMatch | null;
|
|
168
|
+
/**
|
|
169
|
+
* Get all registered routes
|
|
170
|
+
*/
|
|
171
|
+
getRoutes(): Route[];
|
|
172
|
+
/**
|
|
173
|
+
* Clear all routes and middleware
|
|
174
|
+
*/
|
|
175
|
+
clear(): void;
|
|
176
|
+
private normalizePath;
|
|
177
|
+
private notifyListeners;
|
|
178
|
+
/**
|
|
179
|
+
* Initialize client-side routing.
|
|
180
|
+
* Uses a global flag to prevent duplicate event listeners on HMR re-invocation.
|
|
181
|
+
*/
|
|
182
|
+
initClient(): void;
|
|
183
|
+
/**
|
|
184
|
+
* Clean up client-side event listeners.
|
|
185
|
+
* Call this before unmounting or during HMR teardown.
|
|
186
|
+
*/
|
|
187
|
+
dispose(): void;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
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,10 +81,31 @@ 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
102
|
import { logger } from "@revealui/core/observability/logger";
|
|
103
|
+
import { createElement } from "react";
|
|
104
|
+
var MAX_PATTERN_LENGTH = 2048;
|
|
83
105
|
function compilePathPattern(pattern) {
|
|
106
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
107
|
+
throw new Error(`Route pattern exceeds ${MAX_PATTERN_LENGTH} characters`);
|
|
108
|
+
}
|
|
84
109
|
const keys = [];
|
|
85
110
|
let src = "^";
|
|
86
111
|
let i = 0;
|
|
@@ -112,8 +137,17 @@ function compilePathPattern(pattern) {
|
|
|
112
137
|
src += "$";
|
|
113
138
|
return { regex: new RegExp(src), keys };
|
|
114
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
|
+
}
|
|
115
149
|
function pathMatch(pattern, options = {}) {
|
|
116
|
-
const { regex, keys } =
|
|
150
|
+
const { regex, keys } = getCompiledPattern(pattern);
|
|
117
151
|
const decode = options.decode ?? ((s) => s);
|
|
118
152
|
return (path) => {
|
|
119
153
|
const m = regex.exec(path);
|
|
@@ -130,9 +164,14 @@ function pathMatch(pattern, options = {}) {
|
|
|
130
164
|
}
|
|
131
165
|
var Router = class {
|
|
132
166
|
routes = [];
|
|
167
|
+
flatRoutes = [];
|
|
168
|
+
globalMiddleware = [];
|
|
133
169
|
options;
|
|
134
170
|
listeners = /* @__PURE__ */ new Set();
|
|
135
171
|
currentMatch = null;
|
|
172
|
+
lastPathname = null;
|
|
173
|
+
popstateHandler = null;
|
|
174
|
+
clickHandler = null;
|
|
136
175
|
constructor(options = {}) {
|
|
137
176
|
this.options = {
|
|
138
177
|
basePath: "",
|
|
@@ -140,10 +179,24 @@ var Router = class {
|
|
|
140
179
|
};
|
|
141
180
|
}
|
|
142
181
|
/**
|
|
143
|
-
*
|
|
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.
|
|
144
196
|
*/
|
|
145
197
|
register(route) {
|
|
146
198
|
this.routes.push(route);
|
|
199
|
+
this.flattenRoute(route, "", [], void 0);
|
|
147
200
|
}
|
|
148
201
|
/**
|
|
149
202
|
* Register multiple routes
|
|
@@ -154,11 +207,37 @@ var Router = class {
|
|
|
154
207
|
});
|
|
155
208
|
}
|
|
156
209
|
/**
|
|
157
|
-
*
|
|
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.
|
|
158
236
|
*/
|
|
159
237
|
match(url) {
|
|
160
238
|
const path = this.normalizePath(url);
|
|
161
|
-
|
|
239
|
+
const allRoutes = this.flatRoutes.length > 0 ? this.flatRoutes : this.routes;
|
|
240
|
+
for (const route of allRoutes) {
|
|
162
241
|
const matcher = pathMatch(route.path, { decode: decodeURIComponent });
|
|
163
242
|
const result = matcher(path);
|
|
164
243
|
if (result) {
|
|
@@ -171,13 +250,35 @@ var Router = class {
|
|
|
171
250
|
return null;
|
|
172
251
|
}
|
|
173
252
|
/**
|
|
174
|
-
* 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.
|
|
175
258
|
*/
|
|
176
259
|
async resolve(url) {
|
|
177
260
|
const matched = this.match(url);
|
|
178
261
|
if (!matched) {
|
|
179
262
|
return null;
|
|
180
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
|
+
}
|
|
181
282
|
if (matched.route.loader) {
|
|
182
283
|
try {
|
|
183
284
|
matched.data = await matched.route.loader(matched.params);
|
|
@@ -189,9 +290,7 @@ var Router = class {
|
|
|
189
290
|
throw error;
|
|
190
291
|
}
|
|
191
292
|
}
|
|
192
|
-
|
|
193
|
-
this.currentMatch = matched;
|
|
194
|
-
}
|
|
293
|
+
this.currentMatch = matched;
|
|
195
294
|
return matched;
|
|
196
295
|
}
|
|
197
296
|
/**
|
|
@@ -207,6 +306,8 @@ var Router = class {
|
|
|
207
306
|
} else {
|
|
208
307
|
window.history.pushState(options.state || null, "", fullUrl);
|
|
209
308
|
}
|
|
309
|
+
this.lastPathname = window.location.pathname;
|
|
310
|
+
this.currentMatch = this.match(window.location.pathname);
|
|
210
311
|
this.notifyListeners();
|
|
211
312
|
}
|
|
212
313
|
/**
|
|
@@ -241,7 +342,12 @@ var Router = class {
|
|
|
241
342
|
if (typeof window === "undefined") {
|
|
242
343
|
return this.currentMatch;
|
|
243
344
|
}
|
|
244
|
-
|
|
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;
|
|
245
351
|
}
|
|
246
352
|
/**
|
|
247
353
|
* Get all registered routes
|
|
@@ -250,10 +356,12 @@ var Router = class {
|
|
|
250
356
|
return [...this.routes];
|
|
251
357
|
}
|
|
252
358
|
/**
|
|
253
|
-
* Clear all routes
|
|
359
|
+
* Clear all routes and middleware
|
|
254
360
|
*/
|
|
255
361
|
clear() {
|
|
256
362
|
this.routes = [];
|
|
363
|
+
this.flatRoutes = [];
|
|
364
|
+
this.globalMiddleware = [];
|
|
257
365
|
}
|
|
258
366
|
normalizePath(url) {
|
|
259
367
|
let path = url;
|
|
@@ -272,16 +380,22 @@ var Router = class {
|
|
|
272
380
|
});
|
|
273
381
|
}
|
|
274
382
|
/**
|
|
275
|
-
* Initialize client-side routing
|
|
383
|
+
* Initialize client-side routing.
|
|
384
|
+
* Uses a global flag to prevent duplicate event listeners on HMR re-invocation.
|
|
276
385
|
*/
|
|
277
386
|
initClient() {
|
|
278
387
|
if (typeof window === "undefined") {
|
|
279
388
|
return;
|
|
280
389
|
}
|
|
281
|
-
|
|
390
|
+
if (globalThis.__revealui_router_initialized) return;
|
|
391
|
+
globalThis.__revealui_router_initialized = true;
|
|
392
|
+
this.popstateHandler = () => {
|
|
393
|
+
this.lastPathname = window.location.pathname;
|
|
394
|
+
this.currentMatch = this.match(window.location.pathname);
|
|
282
395
|
this.notifyListeners();
|
|
283
|
-
}
|
|
284
|
-
|
|
396
|
+
};
|
|
397
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
398
|
+
this.clickHandler = (e) => {
|
|
285
399
|
const target = e.target.closest("a");
|
|
286
400
|
if (!target) return;
|
|
287
401
|
const href = target.getAttribute("href");
|
|
@@ -289,9 +403,36 @@ var Router = class {
|
|
|
289
403
|
e.preventDefault();
|
|
290
404
|
this.navigate(href);
|
|
291
405
|
}
|
|
292
|
-
}
|
|
406
|
+
};
|
|
407
|
+
document.addEventListener("click", this.clickHandler);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Clean up client-side event listeners.
|
|
411
|
+
* Call this before unmounting or during HMR teardown.
|
|
412
|
+
*/
|
|
413
|
+
dispose() {
|
|
414
|
+
if (typeof window === "undefined") return;
|
|
415
|
+
if (this.popstateHandler) {
|
|
416
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
417
|
+
this.popstateHandler = null;
|
|
418
|
+
}
|
|
419
|
+
if (this.clickHandler) {
|
|
420
|
+
document.removeEventListener("click", this.clickHandler);
|
|
421
|
+
this.clickHandler = null;
|
|
422
|
+
}
|
|
423
|
+
this.listeners.clear();
|
|
424
|
+
globalThis.__revealui_router_initialized = false;
|
|
293
425
|
}
|
|
294
426
|
};
|
|
427
|
+
function joinPaths(parent, child) {
|
|
428
|
+
if (!parent || parent === "/") return child;
|
|
429
|
+
if (!child || child === "/") return parent;
|
|
430
|
+
return `${parent.replace(/\/$/, "")}/${child.replace(/^\//, "")}`;
|
|
431
|
+
}
|
|
432
|
+
function wrapLayouts(Parent, Child) {
|
|
433
|
+
const WrappedLayout = ({ children }) => createElement(Parent, null, createElement(Child, null, children));
|
|
434
|
+
return WrappedLayout;
|
|
435
|
+
}
|
|
295
436
|
|
|
296
437
|
// src/server.tsx
|
|
297
438
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
@@ -329,24 +470,19 @@ function createSSRHandler(routes, options = {}) {
|
|
|
329
470
|
return c.html(template("<div>404 - Page Not Found</div>"));
|
|
330
471
|
}
|
|
331
472
|
if (options.streaming) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
resolve(c.body(html2));
|
|
340
|
-
},
|
|
341
|
-
onError(error) {
|
|
342
|
-
logger2.error("SSR error", error instanceof Error ? error : new Error(String(error)));
|
|
343
|
-
if (options.onError) {
|
|
344
|
-
options.onError(error, c);
|
|
345
|
-
}
|
|
346
|
-
reject(error);
|
|
473
|
+
const stream = await renderToReadableStream(
|
|
474
|
+
/* @__PURE__ */ jsx2(RouterProvider, { router, children: /* @__PURE__ */ jsx2(Routes, {}) }),
|
|
475
|
+
{
|
|
476
|
+
onError(error) {
|
|
477
|
+
logger2.error("SSR error", error instanceof Error ? error : new Error(String(error)));
|
|
478
|
+
if (options.onError) {
|
|
479
|
+
options.onError(error, c);
|
|
347
480
|
}
|
|
348
481
|
}
|
|
349
|
-
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
return new Response(stream, {
|
|
485
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
350
486
|
});
|
|
351
487
|
}
|
|
352
488
|
const html = renderToString(
|
|
@@ -389,9 +525,15 @@ async function hydrate(router, rootElement = null) {
|
|
|
389
525
|
return;
|
|
390
526
|
}
|
|
391
527
|
const dataScript = document.getElementById("__REVEALUI_DATA__");
|
|
392
|
-
const
|
|
528
|
+
const ssrData = dataScript ? JSON.parse(dataScript.textContent || "{}") : {};
|
|
529
|
+
if (ssrData.route) {
|
|
530
|
+
const match = router.match(window.location.pathname);
|
|
531
|
+
if (match) {
|
|
532
|
+
match.data = ssrData.data;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
393
535
|
router.initClient();
|
|
394
|
-
const { hydrateRoot } =
|
|
536
|
+
const { hydrateRoot } = await import("react-dom/client");
|
|
395
537
|
hydrateRoot(
|
|
396
538
|
root,
|
|
397
539
|
/* @__PURE__ */ jsx2(RouterProvider, { router, children: /* @__PURE__ */ jsx2(Routes, {}) })
|