revine 1.3.0 → 1.4.1
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/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/components/MiddlewareGuard.d.ts +10 -0
- package/dist/components/MiddlewareGuard.d.ts.map +1 -0
- package/dist/components/MiddlewareGuard.js +28 -0
- package/dist/runtime/bundler/revinePlugin.d.ts.map +1 -1
- package/dist/runtime/bundler/revinePlugin.js +157 -102
- package/dist/runtime/middleware.d.ts +24 -0
- package/dist/runtime/middleware.d.ts.map +1 -0
- package/dist/runtime/middleware.js +7 -0
- package/package.json +1 -1
- package/roadmap.md +25 -0
- package/src/client.ts +2 -0
- package/src/components/MiddlewareGuard.tsx +39 -0
- package/src/runtime/bundler/revinePlugin.ts +157 -103
- package/src/runtime/middleware.ts +29 -0
package/dist/client.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ export type { NavLinkProps } from "./components/NavLink.js";
|
|
|
8
8
|
export { useRouter } from "./hooks/useRouter.js";
|
|
9
9
|
export { defineConfig } from "./runtime/defineConfig.js";
|
|
10
10
|
export { env, envAll } from "./runtime/env.js";
|
|
11
|
+
export { middlewareResponse } from "./runtime/middleware.js";
|
|
12
|
+
export type { MiddlewareConfig, MiddlewareFn, MiddlewareRequest, MiddlewareResponse } from "./runtime/middleware.js";
|
|
11
13
|
export type { LayoutProps } from "./runtime/types.js";
|
|
12
14
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,cAAc,EACd,WAAW,EACX,WAAW,EACX,SAAS,EACT,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,cAAc,EACd,WAAW,EACX,WAAW,EACX,SAAS,EACT,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACrH,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -5,3 +5,4 @@ export { NavLink } from "./components/NavLink.js";
|
|
|
5
5
|
export { useRouter } from "./hooks/useRouter.js";
|
|
6
6
|
export { defineConfig } from "./runtime/defineConfig.js";
|
|
7
7
|
export { env, envAll } from "./runtime/env.js";
|
|
8
|
+
export { middlewareResponse } from "./runtime/middleware.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { MiddlewareFn } from "../runtime/middleware.js";
|
|
3
|
+
type Props = {
|
|
4
|
+
middleware: MiddlewareFn;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
loadingFallback?: React.ReactNode;
|
|
7
|
+
};
|
|
8
|
+
export declare function MiddlewareGuard({ middleware, children, loadingFallback }: Props): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=MiddlewareGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MiddlewareGuard.d.ts","sourceRoot":"","sources":["../../src/components/MiddlewareGuard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAEnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,KAAK,KAAK,GAAG;IACT,UAAU,EAAE,YAAY,CAAC;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACrC,CAAC;AAEF,wBAAgB,eAAe,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,EAAE,KAAK,2CA4B/E"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
4
|
+
export function MiddlewareGuard({ middleware, children, loadingFallback }) {
|
|
5
|
+
const location = useLocation();
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
const [status, setStatus] = useState("pending");
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setStatus("pending");
|
|
10
|
+
const request = {
|
|
11
|
+
pathname: location.pathname,
|
|
12
|
+
searchParams: new URLSearchParams(location.search),
|
|
13
|
+
};
|
|
14
|
+
Promise.resolve(middleware(request)).then((response) => {
|
|
15
|
+
if (response.type === "redirect") {
|
|
16
|
+
setStatus("redirecting");
|
|
17
|
+
navigate(response.destination, { replace: true });
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
setStatus("allowed");
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}, [location.pathname]);
|
|
24
|
+
if (status === "pending" || status === "redirecting") {
|
|
25
|
+
return _jsx(_Fragment, { children: loadingFallback ?? null });
|
|
26
|
+
}
|
|
27
|
+
return _jsx(_Fragment, { children: children });
|
|
28
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revinePlugin.d.ts","sourceRoot":"","sources":["../../../src/runtime/bundler/revinePlugin.ts"],"names":[],"mappings":"AA2VA,wBAAgB,YAAY,IAAI,GAAG,
|
|
1
|
+
{"version":3,"file":"revinePlugin.d.ts","sourceRoot":"","sources":["../../../src/runtime/bundler/revinePlugin.ts"],"names":[],"mappings":"AA2VA,wBAAgB,YAAY,IAAI,GAAG,CAmMlC"}
|
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
const VIRTUAL_ROUTING_ID = "\0revine:routing";
|
|
2
2
|
const errorBoundaryComponent = `
|
|
3
|
+
const overlayStyle = {
|
|
4
|
+
position: "fixed", inset: 0,
|
|
5
|
+
background: "rgba(0,0,0,0.72)",
|
|
6
|
+
backdropFilter: "blur(6px)",
|
|
7
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
8
|
+
zIndex: 9999,
|
|
9
|
+
};
|
|
10
|
+
const dialogStyle = {
|
|
11
|
+
background: "#141414",
|
|
12
|
+
border: "1px solid #2a2a2a",
|
|
13
|
+
borderRadius: "14px",
|
|
14
|
+
padding: "0",
|
|
15
|
+
maxWidth: "580px", width: "92%",
|
|
16
|
+
boxShadow: "0 32px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.04) inset",
|
|
17
|
+
color: "#e5e5e5",
|
|
18
|
+
overflow: "hidden",
|
|
19
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
20
|
+
};
|
|
21
|
+
const topBarStyle = {
|
|
22
|
+
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
23
|
+
padding: "12px 18px", background: "#0e0e0e",
|
|
24
|
+
};
|
|
25
|
+
const brandStyle = { display: "flex", alignItems: "center", gap: "7px" };
|
|
26
|
+
const brandNameStyle = {
|
|
27
|
+
fontSize: "13px", fontWeight: 700,
|
|
28
|
+
color: "#c4b5fd", letterSpacing: "0.04em",
|
|
29
|
+
fontFamily: "system-ui, sans-serif",
|
|
30
|
+
};
|
|
31
|
+
const badgeStyle = {
|
|
32
|
+
fontSize: "11px", fontWeight: 600, color: "#f87171",
|
|
33
|
+
background: "rgba(248,113,113,0.1)",
|
|
34
|
+
border: "1px solid rgba(248,113,113,0.2)",
|
|
35
|
+
borderRadius: "999px", padding: "2px 10px", letterSpacing: "0.03em",
|
|
36
|
+
};
|
|
37
|
+
const dividerStyle = { height: "1px", background: "#1f1f1f" };
|
|
38
|
+
const headerStyle = {
|
|
39
|
+
display: "flex", alignItems: "center", gap: "10px",
|
|
40
|
+
padding: "20px 22px 0 22px",
|
|
41
|
+
};
|
|
42
|
+
const iconWrapStyle = {
|
|
43
|
+
width: "28px", height: "28px", borderRadius: "8px",
|
|
44
|
+
background: "rgba(248,113,113,0.1)",
|
|
45
|
+
border: "1px solid rgba(248,113,113,0.15)",
|
|
46
|
+
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
47
|
+
};
|
|
48
|
+
const titleStyle = { fontSize: "15px", fontWeight: 650, color: "#fff", letterSpacing: "-0.01em" };
|
|
49
|
+
const messagePanelStyle = {
|
|
50
|
+
position: "relative", margin: "14px 22px 0 22px",
|
|
51
|
+
background: "rgba(248,113,113,0.05)",
|
|
52
|
+
border: "1px solid rgba(248,113,113,0.12)",
|
|
53
|
+
borderRadius: "8px", padding: "12px 40px 12px 14px",
|
|
54
|
+
};
|
|
55
|
+
const messageStyle = {
|
|
56
|
+
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
|
57
|
+
fontSize: "12.5px", color: "#fca5a5",
|
|
58
|
+
margin: 0, lineHeight: 1.65, wordBreak: "break-word",
|
|
59
|
+
};
|
|
60
|
+
const copyBtnStyle = {
|
|
61
|
+
position: "absolute", top: "10px", right: "10px",
|
|
62
|
+
background: "rgba(255,255,255,0.05)", border: "1px solid #2e2e2e",
|
|
63
|
+
borderRadius: "6px", width: "28px", height: "28px",
|
|
64
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
65
|
+
cursor: "pointer", transition: "background 150ms ease", flexShrink: 0,
|
|
66
|
+
};
|
|
67
|
+
const stackSectionStyle = { margin: "14px 22px 0 22px" };
|
|
68
|
+
const toggleBtnStyle = {
|
|
69
|
+
background: "none", border: "none", cursor: "pointer",
|
|
70
|
+
color: "#666", fontSize: "12px", padding: "4px 0",
|
|
71
|
+
display: "flex", alignItems: "center", gap: "6px",
|
|
72
|
+
letterSpacing: "0.02em", transition: "color 150ms ease",
|
|
73
|
+
};
|
|
74
|
+
const stackStyle = {
|
|
75
|
+
background: "#0a0a0a", border: "1px solid #222", borderRadius: "8px",
|
|
76
|
+
padding: "14px 16px", fontSize: "11px", color: "#888",
|
|
77
|
+
overflowX: "auto", lineHeight: 1.8, marginTop: "8px", marginBottom: 0,
|
|
78
|
+
whiteSpace: "pre-wrap", wordBreak: "break-all",
|
|
79
|
+
fontFamily: "ui-monospace, 'Cascadia Code', monospace",
|
|
80
|
+
};
|
|
81
|
+
const actionsStyle = {
|
|
82
|
+
display: "flex", gap: "10px", padding: "18px 22px 22px 22px", marginTop: "16px",
|
|
83
|
+
};
|
|
84
|
+
const primaryBtnStyle = {
|
|
85
|
+
flex: 1, padding: "10px 0", borderRadius: "8px", border: "none",
|
|
86
|
+
background: "linear-gradient(135deg, #7c3aed, #6d28d9)",
|
|
87
|
+
color: "#fff", fontWeight: 600, fontSize: "13px", cursor: "pointer",
|
|
88
|
+
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
89
|
+
letterSpacing: "0.01em", boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
90
|
+
fontFamily: "system-ui, sans-serif",
|
|
91
|
+
};
|
|
92
|
+
const secondaryBtnStyle = {
|
|
93
|
+
flex: 1, padding: "10px 0", borderRadius: "8px",
|
|
94
|
+
border: "1px solid #2e2e2e", background: "rgba(255,255,255,0.03)",
|
|
95
|
+
color: "#999", fontSize: "13px", cursor: "pointer",
|
|
96
|
+
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
97
|
+
letterSpacing: "0.01em", fontFamily: "system-ui, sans-serif",
|
|
98
|
+
};
|
|
99
|
+
|
|
3
100
|
function RevineErrorDialog() {
|
|
4
101
|
const error = useRouteError();
|
|
5
102
|
const [expanded, setExpanded] = React.useState(false);
|
|
@@ -142,104 +239,8 @@ function RevineErrorDialog() {
|
|
|
142
239
|
)
|
|
143
240
|
);
|
|
144
241
|
}
|
|
145
|
-
|
|
146
|
-
const overlayStyle = {
|
|
147
|
-
position: "fixed", inset: 0,
|
|
148
|
-
background: "rgba(0,0,0,0.72)",
|
|
149
|
-
backdropFilter: "blur(6px)",
|
|
150
|
-
display: "flex", alignItems: "center", justifyContent: "center",
|
|
151
|
-
zIndex: 9999,
|
|
152
|
-
};
|
|
153
|
-
const dialogStyle = {
|
|
154
|
-
background: "#141414",
|
|
155
|
-
border: "1px solid #2a2a2a",
|
|
156
|
-
borderRadius: "14px",
|
|
157
|
-
padding: "0",
|
|
158
|
-
maxWidth: "580px", width: "92%",
|
|
159
|
-
boxShadow: "0 32px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.04) inset",
|
|
160
|
-
color: "#e5e5e5",
|
|
161
|
-
overflow: "hidden",
|
|
162
|
-
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
163
|
-
};
|
|
164
|
-
const topBarStyle = {
|
|
165
|
-
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
166
|
-
padding: "12px 18px", background: "#0e0e0e",
|
|
167
|
-
};
|
|
168
|
-
const brandStyle = { display: "flex", alignItems: "center", gap: "7px" };
|
|
169
|
-
const brandNameStyle = {
|
|
170
|
-
fontSize: "13px", fontWeight: 700,
|
|
171
|
-
color: "#c4b5fd", letterSpacing: "0.04em",
|
|
172
|
-
fontFamily: "system-ui, sans-serif",
|
|
173
|
-
};
|
|
174
|
-
const badgeStyle = {
|
|
175
|
-
fontSize: "11px", fontWeight: 600, color: "#f87171",
|
|
176
|
-
background: "rgba(248,113,113,0.1)",
|
|
177
|
-
border: "1px solid rgba(248,113,113,0.2)",
|
|
178
|
-
borderRadius: "999px", padding: "2px 10px", letterSpacing: "0.03em",
|
|
179
|
-
};
|
|
180
|
-
const dividerStyle = { height: "1px", background: "#1f1f1f" };
|
|
181
|
-
const headerStyle = {
|
|
182
|
-
display: "flex", alignItems: "center", gap: "10px",
|
|
183
|
-
padding: "20px 22px 0 22px",
|
|
184
|
-
};
|
|
185
|
-
const iconWrapStyle = {
|
|
186
|
-
width: "28px", height: "28px", borderRadius: "8px",
|
|
187
|
-
background: "rgba(248,113,113,0.1)",
|
|
188
|
-
border: "1px solid rgba(248,113,113,0.15)",
|
|
189
|
-
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
190
|
-
};
|
|
191
|
-
const titleStyle = { fontSize: "15px", fontWeight: 650, color: "#fff", letterSpacing: "-0.01em" };
|
|
192
|
-
const messagePanelStyle = {
|
|
193
|
-
position: "relative", margin: "14px 22px 0 22px",
|
|
194
|
-
background: "rgba(248,113,113,0.05)",
|
|
195
|
-
border: "1px solid rgba(248,113,113,0.12)",
|
|
196
|
-
borderRadius: "8px", padding: "12px 40px 12px 14px",
|
|
197
|
-
};
|
|
198
|
-
const messageStyle = {
|
|
199
|
-
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
|
200
|
-
fontSize: "12.5px", color: "#fca5a5",
|
|
201
|
-
margin: 0, lineHeight: 1.65, wordBreak: "break-word",
|
|
202
|
-
};
|
|
203
|
-
const copyBtnStyle = {
|
|
204
|
-
position: "absolute", top: "10px", right: "10px",
|
|
205
|
-
background: "rgba(255,255,255,0.05)", border: "1px solid #2e2e2e",
|
|
206
|
-
borderRadius: "6px", width: "28px", height: "28px",
|
|
207
|
-
display: "flex", alignItems: "center", justifyContent: "center",
|
|
208
|
-
cursor: "pointer", transition: "background 150ms ease", flexShrink: 0,
|
|
209
|
-
};
|
|
210
|
-
const stackSectionStyle = { margin: "14px 22px 0 22px" };
|
|
211
|
-
const toggleBtnStyle = {
|
|
212
|
-
background: "none", border: "none", cursor: "pointer",
|
|
213
|
-
color: "#666", fontSize: "12px", padding: "4px 0",
|
|
214
|
-
display: "flex", alignItems: "center", gap: "6px",
|
|
215
|
-
letterSpacing: "0.02em", transition: "color 150ms ease",
|
|
216
|
-
};
|
|
217
|
-
const stackStyle = {
|
|
218
|
-
background: "#0a0a0a", border: "1px solid #222", borderRadius: "8px",
|
|
219
|
-
padding: "14px 16px", fontSize: "11px", color: "#888",
|
|
220
|
-
overflowX: "auto", lineHeight: 1.8, marginTop: "8px", marginBottom: 0,
|
|
221
|
-
whiteSpace: "pre-wrap", wordBreak: "break-all",
|
|
222
|
-
fontFamily: "ui-monospace, 'Cascadia Code', monospace",
|
|
223
|
-
};
|
|
224
|
-
const actionsStyle = {
|
|
225
|
-
display: "flex", gap: "10px", padding: "18px 22px 22px 22px", marginTop: "16px",
|
|
226
|
-
};
|
|
227
|
-
const primaryBtnStyle = {
|
|
228
|
-
flex: 1, padding: "10px 0", borderRadius: "8px", border: "none",
|
|
229
|
-
background: "linear-gradient(135deg, #7c3aed, #6d28d9)",
|
|
230
|
-
color: "#fff", fontWeight: 600, fontSize: "13px", cursor: "pointer",
|
|
231
|
-
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
232
|
-
letterSpacing: "0.01em", boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
233
|
-
fontFamily: "system-ui, sans-serif",
|
|
234
|
-
};
|
|
235
|
-
const secondaryBtnStyle = {
|
|
236
|
-
flex: 1, padding: "10px 0", borderRadius: "8px",
|
|
237
|
-
border: "1px solid #2e2e2e", background: "rgba(255,255,255,0.03)",
|
|
238
|
-
color: "#999", fontSize: "13px", cursor: "pointer",
|
|
239
|
-
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
240
|
-
letterSpacing: "0.01em", fontFamily: "system-ui, sans-serif",
|
|
241
|
-
};
|
|
242
242
|
`;
|
|
243
|
+
;
|
|
243
244
|
// ── Shared overlay HTML builder (used in both the inline script and module error handler)
|
|
244
245
|
// Written as a plain JS string so it can be embedded inside the injected <script> tag.
|
|
245
246
|
const overlayScriptContent = `
|
|
@@ -365,12 +366,51 @@ export function revinePlugin() {
|
|
|
365
366
|
load(id) {
|
|
366
367
|
if (id === VIRTUAL_ROUTING_ID) {
|
|
367
368
|
return `
|
|
368
|
-
import { createBrowserRouter, useRouteError } from "react-router-dom";
|
|
369
|
-
import { lazy, Suspense, createElement } from "react";
|
|
369
|
+
import { createBrowserRouter, useRouteError, Outlet } from "react-router-dom";
|
|
370
|
+
import { lazy, Suspense, createElement, useState, useEffect, useRef } from "react";
|
|
370
371
|
import React from "react";
|
|
371
372
|
|
|
373
|
+
// ── Middleware support ──────────────────────────────────────────────
|
|
374
|
+
const middlewareModules = import.meta.glob("/src/middleware.{ts,tsx}", { eager: true });
|
|
375
|
+
const middlewareMod = Object.values(middlewareModules)[0];
|
|
376
|
+
const userMiddleware = middlewareMod?.default ?? null;
|
|
377
|
+
|
|
372
378
|
${errorBoundaryComponent}
|
|
373
379
|
|
|
380
|
+
// ── MiddlewareGuard ─────────────────────────────────────────────────
|
|
381
|
+
function MiddlewareGuard({ children }) {
|
|
382
|
+
const [status, setStatus] = React.useState("pending");
|
|
383
|
+
const lastPathnameRef = React.useRef(null);
|
|
384
|
+
|
|
385
|
+
React.useEffect(() => {
|
|
386
|
+
if (!userMiddleware) { setStatus("allowed"); return; }
|
|
387
|
+
|
|
388
|
+
const run = async (pathname, search) => {
|
|
389
|
+
if (pathname === lastPathnameRef.current) return;
|
|
390
|
+
lastPathnameRef.current = pathname;
|
|
391
|
+
|
|
392
|
+
setStatus("pending");
|
|
393
|
+
const req = { pathname, searchParams: new URLSearchParams(search) };
|
|
394
|
+
const res = await Promise.resolve(userMiddleware(req));
|
|
395
|
+
if (res.type === "redirect") {
|
|
396
|
+
router.navigate(res.destination, { replace: true });
|
|
397
|
+
setStatus("redirecting");
|
|
398
|
+
} else {
|
|
399
|
+
setStatus("allowed");
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
run(window.location.pathname, window.location.search);
|
|
404
|
+
|
|
405
|
+
return router.subscribe((state) => {
|
|
406
|
+
run(state.location.pathname, state.location.search);
|
|
407
|
+
});
|
|
408
|
+
}, []);
|
|
409
|
+
|
|
410
|
+
if (status === "pending" || status === "redirecting") return null;
|
|
411
|
+
return React.createElement(React.Fragment, null, children);
|
|
412
|
+
}
|
|
413
|
+
|
|
374
414
|
const notFoundModules = import.meta.glob("/src/NotFound.tsx", { eager: true });
|
|
375
415
|
const NotFoundComponent = Object.values(notFoundModules)[0]?.default;
|
|
376
416
|
|
|
@@ -414,11 +454,18 @@ function wrapWithLayouts(element, layouts) {
|
|
|
414
454
|
|
|
415
455
|
function toRoutePath(filePath) {
|
|
416
456
|
let p = filePath;
|
|
417
|
-
p = p.replace(
|
|
457
|
+
p = p.replace(/\\\\\\\\/g, "/");
|
|
418
458
|
p = p.replace(/.*\\/pages\\//, "");
|
|
419
459
|
p = p.replace(/\\.tsx$/i, "");
|
|
420
460
|
p = p.replace(/\\([^)]+\\)\\//g, "");
|
|
421
461
|
p = p.replace(/\\/index$/, "");
|
|
462
|
+
|
|
463
|
+
// Handle dynamic routes: [param] -> :param
|
|
464
|
+
p = p.replace(/\\[([^ \\]]+)\\]/g, (_match, param) => {
|
|
465
|
+
if (param.startsWith("...")) return "*";
|
|
466
|
+
return ":" + param;
|
|
467
|
+
});
|
|
468
|
+
|
|
422
469
|
if (p === "index" || p === "") return "/";
|
|
423
470
|
return "/" + p;
|
|
424
471
|
}
|
|
@@ -430,7 +477,7 @@ const pageEntries = Object.entries(pages).filter(([filePath]) => {
|
|
|
430
477
|
return !segments.some((s) => s.startsWith("_"));
|
|
431
478
|
});
|
|
432
479
|
|
|
433
|
-
const
|
|
480
|
+
const innerRoutes = pageEntries.map(([filePath, component]) => {
|
|
434
481
|
const routePath = toRoutePath(filePath);
|
|
435
482
|
const Component = lazy(component);
|
|
436
483
|
const layouts = getLayoutsForPath(filePath);
|
|
@@ -453,7 +500,7 @@ const routes = pageEntries.map(([filePath, component]) => {
|
|
|
453
500
|
};
|
|
454
501
|
});
|
|
455
502
|
|
|
456
|
-
|
|
503
|
+
innerRoutes.push({
|
|
457
504
|
path: "*",
|
|
458
505
|
element: NotFoundComponent
|
|
459
506
|
? createElement(NotFoundComponent)
|
|
@@ -461,6 +508,14 @@ routes.push({
|
|
|
461
508
|
errorElement: createElement(RevineErrorDialog),
|
|
462
509
|
});
|
|
463
510
|
|
|
511
|
+
const routes = [
|
|
512
|
+
{
|
|
513
|
+
element: createElement(MiddlewareGuard, null, createElement(Outlet)),
|
|
514
|
+
children: innerRoutes,
|
|
515
|
+
errorElement: createElement(RevineErrorDialog),
|
|
516
|
+
},
|
|
517
|
+
];
|
|
518
|
+
|
|
464
519
|
export const router = createBrowserRouter(routes, {
|
|
465
520
|
future: {
|
|
466
521
|
v7_startTransition: true,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type MiddlewareRequest = {
|
|
2
|
+
pathname: string;
|
|
3
|
+
searchParams: URLSearchParams;
|
|
4
|
+
};
|
|
5
|
+
export type MiddlewareResponse = {
|
|
6
|
+
type: "next";
|
|
7
|
+
} | {
|
|
8
|
+
type: "redirect";
|
|
9
|
+
destination: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const middlewareResponse: {
|
|
12
|
+
next: () => MiddlewareResponse;
|
|
13
|
+
redirect: (destination: string) => MiddlewareResponse;
|
|
14
|
+
};
|
|
15
|
+
export type MiddlewareFn = (request: MiddlewareRequest) => MiddlewareResponse | Promise<MiddlewareResponse>;
|
|
16
|
+
export type MiddlewareConfig = {
|
|
17
|
+
publicPaths?: string[];
|
|
18
|
+
authPaths?: string[];
|
|
19
|
+
redirects?: {
|
|
20
|
+
whenAuthenticated?: string;
|
|
21
|
+
whenUnauthenticated?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/runtime/middleware.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,eAAe,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,eAAO,MAAM,kBAAkB;gBACnB,kBAAkB;4BACJ,MAAM,KAAG,kBAAkB;CAIpD,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,CACzB,OAAO,EAAE,iBAAiB,KACvB,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAEtD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE;QACV,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;CACH,CAAC"}
|
package/package.json
CHANGED
package/roadmap.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
## SEO
|
|
4
|
+
- remove the index.html file from template
|
|
5
|
+
- remove irrelevant configs from root.tsx
|
|
6
|
+
- add the html like config in the root.tsx
|
|
7
|
+
- meta tags
|
|
8
|
+
- title tag
|
|
9
|
+
- favicon
|
|
10
|
+
and everything else
|
|
11
|
+
|
|
12
|
+
## Rendering
|
|
13
|
+
- add SSG, SSR, ISR, and CSR rendering
|
|
14
|
+
- add a way to configure rendering for each file or component or even function
|
|
15
|
+
|
|
16
|
+
## Image
|
|
17
|
+
- add default declaration for image files (.png, .jpg, .jpeg, .gif, .svg, .webp etc)
|
|
18
|
+
|
|
19
|
+
## File Imports
|
|
20
|
+
- add support for alias imports for public folder
|
|
21
|
+
|
|
22
|
+
## Styling
|
|
23
|
+
- add shadcn support
|
|
24
|
+
- add font support ( google fonts)
|
|
25
|
+
- add theme support with tailwind
|
package/src/client.ts
CHANGED
|
@@ -15,4 +15,6 @@ export type { NavLinkProps } from "./components/NavLink.js";
|
|
|
15
15
|
export { useRouter } from "./hooks/useRouter.js";
|
|
16
16
|
export { defineConfig } from "./runtime/defineConfig.js";
|
|
17
17
|
export { env, envAll } from "./runtime/env.js";
|
|
18
|
+
export { middlewareResponse } from "./runtime/middleware.js";
|
|
19
|
+
export type { MiddlewareConfig, MiddlewareFn, MiddlewareRequest, MiddlewareResponse } from "./runtime/middleware.js";
|
|
18
20
|
export type { LayoutProps } from "./runtime/types.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
3
|
+
import type { MiddlewareFn } from "../runtime/middleware.js";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
middleware: MiddlewareFn;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
loadingFallback?: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function MiddlewareGuard({ middleware, children, loadingFallback }: Props) {
|
|
12
|
+
const location = useLocation();
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
const [status, setStatus] = useState<"pending" | "allowed" | "redirecting">("pending");
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setStatus("pending");
|
|
18
|
+
|
|
19
|
+
const request = {
|
|
20
|
+
pathname: location.pathname,
|
|
21
|
+
searchParams: new URLSearchParams(location.search),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
Promise.resolve(middleware(request)).then((response) => {
|
|
25
|
+
if (response.type === "redirect") {
|
|
26
|
+
setStatus("redirecting");
|
|
27
|
+
navigate(response.destination, { replace: true });
|
|
28
|
+
} else {
|
|
29
|
+
setStatus("allowed");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}, [location.pathname]);
|
|
33
|
+
|
|
34
|
+
if (status === "pending" || status === "redirecting") {
|
|
35
|
+
return <>{loadingFallback ?? null}</>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return <>{children}</>;
|
|
39
|
+
}
|
|
@@ -1,6 +1,103 @@
|
|
|
1
1
|
const VIRTUAL_ROUTING_ID = "\0revine:routing";
|
|
2
2
|
|
|
3
3
|
const errorBoundaryComponent = `
|
|
4
|
+
const overlayStyle = {
|
|
5
|
+
position: "fixed", inset: 0,
|
|
6
|
+
background: "rgba(0,0,0,0.72)",
|
|
7
|
+
backdropFilter: "blur(6px)",
|
|
8
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
9
|
+
zIndex: 9999,
|
|
10
|
+
};
|
|
11
|
+
const dialogStyle = {
|
|
12
|
+
background: "#141414",
|
|
13
|
+
border: "1px solid #2a2a2a",
|
|
14
|
+
borderRadius: "14px",
|
|
15
|
+
padding: "0",
|
|
16
|
+
maxWidth: "580px", width: "92%",
|
|
17
|
+
boxShadow: "0 32px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.04) inset",
|
|
18
|
+
color: "#e5e5e5",
|
|
19
|
+
overflow: "hidden",
|
|
20
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
21
|
+
};
|
|
22
|
+
const topBarStyle = {
|
|
23
|
+
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
24
|
+
padding: "12px 18px", background: "#0e0e0e",
|
|
25
|
+
};
|
|
26
|
+
const brandStyle = { display: "flex", alignItems: "center", gap: "7px" };
|
|
27
|
+
const brandNameStyle = {
|
|
28
|
+
fontSize: "13px", fontWeight: 700,
|
|
29
|
+
color: "#c4b5fd", letterSpacing: "0.04em",
|
|
30
|
+
fontFamily: "system-ui, sans-serif",
|
|
31
|
+
};
|
|
32
|
+
const badgeStyle = {
|
|
33
|
+
fontSize: "11px", fontWeight: 600, color: "#f87171",
|
|
34
|
+
background: "rgba(248,113,113,0.1)",
|
|
35
|
+
border: "1px solid rgba(248,113,113,0.2)",
|
|
36
|
+
borderRadius: "999px", padding: "2px 10px", letterSpacing: "0.03em",
|
|
37
|
+
};
|
|
38
|
+
const dividerStyle = { height: "1px", background: "#1f1f1f" };
|
|
39
|
+
const headerStyle = {
|
|
40
|
+
display: "flex", alignItems: "center", gap: "10px",
|
|
41
|
+
padding: "20px 22px 0 22px",
|
|
42
|
+
};
|
|
43
|
+
const iconWrapStyle = {
|
|
44
|
+
width: "28px", height: "28px", borderRadius: "8px",
|
|
45
|
+
background: "rgba(248,113,113,0.1)",
|
|
46
|
+
border: "1px solid rgba(248,113,113,0.15)",
|
|
47
|
+
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
48
|
+
};
|
|
49
|
+
const titleStyle = { fontSize: "15px", fontWeight: 650, color: "#fff", letterSpacing: "-0.01em" };
|
|
50
|
+
const messagePanelStyle = {
|
|
51
|
+
position: "relative", margin: "14px 22px 0 22px",
|
|
52
|
+
background: "rgba(248,113,113,0.05)",
|
|
53
|
+
border: "1px solid rgba(248,113,113,0.12)",
|
|
54
|
+
borderRadius: "8px", padding: "12px 40px 12px 14px",
|
|
55
|
+
};
|
|
56
|
+
const messageStyle = {
|
|
57
|
+
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
|
58
|
+
fontSize: "12.5px", color: "#fca5a5",
|
|
59
|
+
margin: 0, lineHeight: 1.65, wordBreak: "break-word",
|
|
60
|
+
};
|
|
61
|
+
const copyBtnStyle = {
|
|
62
|
+
position: "absolute", top: "10px", right: "10px",
|
|
63
|
+
background: "rgba(255,255,255,0.05)", border: "1px solid #2e2e2e",
|
|
64
|
+
borderRadius: "6px", width: "28px", height: "28px",
|
|
65
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
66
|
+
cursor: "pointer", transition: "background 150ms ease", flexShrink: 0,
|
|
67
|
+
};
|
|
68
|
+
const stackSectionStyle = { margin: "14px 22px 0 22px" };
|
|
69
|
+
const toggleBtnStyle = {
|
|
70
|
+
background: "none", border: "none", cursor: "pointer",
|
|
71
|
+
color: "#666", fontSize: "12px", padding: "4px 0",
|
|
72
|
+
display: "flex", alignItems: "center", gap: "6px",
|
|
73
|
+
letterSpacing: "0.02em", transition: "color 150ms ease",
|
|
74
|
+
};
|
|
75
|
+
const stackStyle = {
|
|
76
|
+
background: "#0a0a0a", border: "1px solid #222", borderRadius: "8px",
|
|
77
|
+
padding: "14px 16px", fontSize: "11px", color: "#888",
|
|
78
|
+
overflowX: "auto", lineHeight: 1.8, marginTop: "8px", marginBottom: 0,
|
|
79
|
+
whiteSpace: "pre-wrap", wordBreak: "break-all",
|
|
80
|
+
fontFamily: "ui-monospace, 'Cascadia Code', monospace",
|
|
81
|
+
};
|
|
82
|
+
const actionsStyle = {
|
|
83
|
+
display: "flex", gap: "10px", padding: "18px 22px 22px 22px", marginTop: "16px",
|
|
84
|
+
};
|
|
85
|
+
const primaryBtnStyle = {
|
|
86
|
+
flex: 1, padding: "10px 0", borderRadius: "8px", border: "none",
|
|
87
|
+
background: "linear-gradient(135deg, #7c3aed, #6d28d9)",
|
|
88
|
+
color: "#fff", fontWeight: 600, fontSize: "13px", cursor: "pointer",
|
|
89
|
+
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
90
|
+
letterSpacing: "0.01em", boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
91
|
+
fontFamily: "system-ui, sans-serif",
|
|
92
|
+
};
|
|
93
|
+
const secondaryBtnStyle = {
|
|
94
|
+
flex: 1, padding: "10px 0", borderRadius: "8px",
|
|
95
|
+
border: "1px solid #2e2e2e", background: "rgba(255,255,255,0.03)",
|
|
96
|
+
color: "#999", fontSize: "13px", cursor: "pointer",
|
|
97
|
+
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
98
|
+
letterSpacing: "0.01em", fontFamily: "system-ui, sans-serif",
|
|
99
|
+
};
|
|
100
|
+
|
|
4
101
|
function RevineErrorDialog() {
|
|
5
102
|
const error = useRouteError();
|
|
6
103
|
const [expanded, setExpanded] = React.useState(false);
|
|
@@ -143,104 +240,7 @@ function RevineErrorDialog() {
|
|
|
143
240
|
)
|
|
144
241
|
);
|
|
145
242
|
}
|
|
146
|
-
|
|
147
|
-
const overlayStyle = {
|
|
148
|
-
position: "fixed", inset: 0,
|
|
149
|
-
background: "rgba(0,0,0,0.72)",
|
|
150
|
-
backdropFilter: "blur(6px)",
|
|
151
|
-
display: "flex", alignItems: "center", justifyContent: "center",
|
|
152
|
-
zIndex: 9999,
|
|
153
|
-
};
|
|
154
|
-
const dialogStyle = {
|
|
155
|
-
background: "#141414",
|
|
156
|
-
border: "1px solid #2a2a2a",
|
|
157
|
-
borderRadius: "14px",
|
|
158
|
-
padding: "0",
|
|
159
|
-
maxWidth: "580px", width: "92%",
|
|
160
|
-
boxShadow: "0 32px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.04) inset",
|
|
161
|
-
color: "#e5e5e5",
|
|
162
|
-
overflow: "hidden",
|
|
163
|
-
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
164
|
-
};
|
|
165
|
-
const topBarStyle = {
|
|
166
|
-
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
167
|
-
padding: "12px 18px", background: "#0e0e0e",
|
|
168
|
-
};
|
|
169
|
-
const brandStyle = { display: "flex", alignItems: "center", gap: "7px" };
|
|
170
|
-
const brandNameStyle = {
|
|
171
|
-
fontSize: "13px", fontWeight: 700,
|
|
172
|
-
color: "#c4b5fd", letterSpacing: "0.04em",
|
|
173
|
-
fontFamily: "system-ui, sans-serif",
|
|
174
|
-
};
|
|
175
|
-
const badgeStyle = {
|
|
176
|
-
fontSize: "11px", fontWeight: 600, color: "#f87171",
|
|
177
|
-
background: "rgba(248,113,113,0.1)",
|
|
178
|
-
border: "1px solid rgba(248,113,113,0.2)",
|
|
179
|
-
borderRadius: "999px", padding: "2px 10px", letterSpacing: "0.03em",
|
|
180
|
-
};
|
|
181
|
-
const dividerStyle = { height: "1px", background: "#1f1f1f" };
|
|
182
|
-
const headerStyle = {
|
|
183
|
-
display: "flex", alignItems: "center", gap: "10px",
|
|
184
|
-
padding: "20px 22px 0 22px",
|
|
185
|
-
};
|
|
186
|
-
const iconWrapStyle = {
|
|
187
|
-
width: "28px", height: "28px", borderRadius: "8px",
|
|
188
|
-
background: "rgba(248,113,113,0.1)",
|
|
189
|
-
border: "1px solid rgba(248,113,113,0.15)",
|
|
190
|
-
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
191
|
-
};
|
|
192
|
-
const titleStyle = { fontSize: "15px", fontWeight: 650, color: "#fff", letterSpacing: "-0.01em" };
|
|
193
|
-
const messagePanelStyle = {
|
|
194
|
-
position: "relative", margin: "14px 22px 0 22px",
|
|
195
|
-
background: "rgba(248,113,113,0.05)",
|
|
196
|
-
border: "1px solid rgba(248,113,113,0.12)",
|
|
197
|
-
borderRadius: "8px", padding: "12px 40px 12px 14px",
|
|
198
|
-
};
|
|
199
|
-
const messageStyle = {
|
|
200
|
-
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
|
201
|
-
fontSize: "12.5px", color: "#fca5a5",
|
|
202
|
-
margin: 0, lineHeight: 1.65, wordBreak: "break-word",
|
|
203
|
-
};
|
|
204
|
-
const copyBtnStyle = {
|
|
205
|
-
position: "absolute", top: "10px", right: "10px",
|
|
206
|
-
background: "rgba(255,255,255,0.05)", border: "1px solid #2e2e2e",
|
|
207
|
-
borderRadius: "6px", width: "28px", height: "28px",
|
|
208
|
-
display: "flex", alignItems: "center", justifyContent: "center",
|
|
209
|
-
cursor: "pointer", transition: "background 150ms ease", flexShrink: 0,
|
|
210
|
-
};
|
|
211
|
-
const stackSectionStyle = { margin: "14px 22px 0 22px" };
|
|
212
|
-
const toggleBtnStyle = {
|
|
213
|
-
background: "none", border: "none", cursor: "pointer",
|
|
214
|
-
color: "#666", fontSize: "12px", padding: "4px 0",
|
|
215
|
-
display: "flex", alignItems: "center", gap: "6px",
|
|
216
|
-
letterSpacing: "0.02em", transition: "color 150ms ease",
|
|
217
|
-
};
|
|
218
|
-
const stackStyle = {
|
|
219
|
-
background: "#0a0a0a", border: "1px solid #222", borderRadius: "8px",
|
|
220
|
-
padding: "14px 16px", fontSize: "11px", color: "#888",
|
|
221
|
-
overflowX: "auto", lineHeight: 1.8, marginTop: "8px", marginBottom: 0,
|
|
222
|
-
whiteSpace: "pre-wrap", wordBreak: "break-all",
|
|
223
|
-
fontFamily: "ui-monospace, 'Cascadia Code', monospace",
|
|
224
|
-
};
|
|
225
|
-
const actionsStyle = {
|
|
226
|
-
display: "flex", gap: "10px", padding: "18px 22px 22px 22px", marginTop: "16px",
|
|
227
|
-
};
|
|
228
|
-
const primaryBtnStyle = {
|
|
229
|
-
flex: 1, padding: "10px 0", borderRadius: "8px", border: "none",
|
|
230
|
-
background: "linear-gradient(135deg, #7c3aed, #6d28d9)",
|
|
231
|
-
color: "#fff", fontWeight: 600, fontSize: "13px", cursor: "pointer",
|
|
232
|
-
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
233
|
-
letterSpacing: "0.01em", boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
234
|
-
fontFamily: "system-ui, sans-serif",
|
|
235
|
-
};
|
|
236
|
-
const secondaryBtnStyle = {
|
|
237
|
-
flex: 1, padding: "10px 0", borderRadius: "8px",
|
|
238
|
-
border: "1px solid #2e2e2e", background: "rgba(255,255,255,0.03)",
|
|
239
|
-
color: "#999", fontSize: "13px", cursor: "pointer",
|
|
240
|
-
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
241
|
-
letterSpacing: "0.01em", fontFamily: "system-ui, sans-serif",
|
|
242
|
-
};
|
|
243
|
-
`;
|
|
243
|
+
`;;
|
|
244
244
|
|
|
245
245
|
// ── Shared overlay HTML builder (used in both the inline script and module error handler)
|
|
246
246
|
// Written as a plain JS string so it can be embedded inside the injected <script> tag.
|
|
@@ -380,12 +380,51 @@ export function revinePlugin(): any {
|
|
|
380
380
|
load(id: string) {
|
|
381
381
|
if (id === VIRTUAL_ROUTING_ID) {
|
|
382
382
|
return `
|
|
383
|
-
import { createBrowserRouter, useRouteError } from "react-router-dom";
|
|
384
|
-
import { lazy, Suspense, createElement } from "react";
|
|
383
|
+
import { createBrowserRouter, useRouteError, Outlet } from "react-router-dom";
|
|
384
|
+
import { lazy, Suspense, createElement, useState, useEffect, useRef } from "react";
|
|
385
385
|
import React from "react";
|
|
386
386
|
|
|
387
|
+
// ── Middleware support ──────────────────────────────────────────────
|
|
388
|
+
const middlewareModules = import.meta.glob("/src/middleware.{ts,tsx}", { eager: true });
|
|
389
|
+
const middlewareMod = Object.values(middlewareModules)[0];
|
|
390
|
+
const userMiddleware = middlewareMod?.default ?? null;
|
|
391
|
+
|
|
387
392
|
${errorBoundaryComponent}
|
|
388
393
|
|
|
394
|
+
// ── MiddlewareGuard ─────────────────────────────────────────────────
|
|
395
|
+
function MiddlewareGuard({ children }) {
|
|
396
|
+
const [status, setStatus] = React.useState("pending");
|
|
397
|
+
const lastPathnameRef = React.useRef(null);
|
|
398
|
+
|
|
399
|
+
React.useEffect(() => {
|
|
400
|
+
if (!userMiddleware) { setStatus("allowed"); return; }
|
|
401
|
+
|
|
402
|
+
const run = async (pathname, search) => {
|
|
403
|
+
if (pathname === lastPathnameRef.current) return;
|
|
404
|
+
lastPathnameRef.current = pathname;
|
|
405
|
+
|
|
406
|
+
setStatus("pending");
|
|
407
|
+
const req = { pathname, searchParams: new URLSearchParams(search) };
|
|
408
|
+
const res = await Promise.resolve(userMiddleware(req));
|
|
409
|
+
if (res.type === "redirect") {
|
|
410
|
+
router.navigate(res.destination, { replace: true });
|
|
411
|
+
setStatus("redirecting");
|
|
412
|
+
} else {
|
|
413
|
+
setStatus("allowed");
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
run(window.location.pathname, window.location.search);
|
|
418
|
+
|
|
419
|
+
return router.subscribe((state) => {
|
|
420
|
+
run(state.location.pathname, state.location.search);
|
|
421
|
+
});
|
|
422
|
+
}, []);
|
|
423
|
+
|
|
424
|
+
if (status === "pending" || status === "redirecting") return null;
|
|
425
|
+
return React.createElement(React.Fragment, null, children);
|
|
426
|
+
}
|
|
427
|
+
|
|
389
428
|
const notFoundModules = import.meta.glob("/src/NotFound.tsx", { eager: true });
|
|
390
429
|
const NotFoundComponent = Object.values(notFoundModules)[0]?.default;
|
|
391
430
|
|
|
@@ -429,11 +468,18 @@ function wrapWithLayouts(element, layouts) {
|
|
|
429
468
|
|
|
430
469
|
function toRoutePath(filePath) {
|
|
431
470
|
let p = filePath;
|
|
432
|
-
p = p.replace(
|
|
471
|
+
p = p.replace(/\\\\\\\\/g, "/");
|
|
433
472
|
p = p.replace(/.*\\/pages\\//, "");
|
|
434
473
|
p = p.replace(/\\.tsx$/i, "");
|
|
435
474
|
p = p.replace(/\\([^)]+\\)\\//g, "");
|
|
436
475
|
p = p.replace(/\\/index$/, "");
|
|
476
|
+
|
|
477
|
+
// Handle dynamic routes: [param] -> :param
|
|
478
|
+
p = p.replace(/\\[([^ \\]]+)\\]/g, (_match, param) => {
|
|
479
|
+
if (param.startsWith("...")) return "*";
|
|
480
|
+
return ":" + param;
|
|
481
|
+
});
|
|
482
|
+
|
|
437
483
|
if (p === "index" || p === "") return "/";
|
|
438
484
|
return "/" + p;
|
|
439
485
|
}
|
|
@@ -445,7 +491,7 @@ const pageEntries = Object.entries(pages).filter(([filePath]) => {
|
|
|
445
491
|
return !segments.some((s) => s.startsWith("_"));
|
|
446
492
|
});
|
|
447
493
|
|
|
448
|
-
const
|
|
494
|
+
const innerRoutes = pageEntries.map(([filePath, component]) => {
|
|
449
495
|
const routePath = toRoutePath(filePath);
|
|
450
496
|
const Component = lazy(component);
|
|
451
497
|
const layouts = getLayoutsForPath(filePath);
|
|
@@ -468,7 +514,7 @@ const routes = pageEntries.map(([filePath, component]) => {
|
|
|
468
514
|
};
|
|
469
515
|
});
|
|
470
516
|
|
|
471
|
-
|
|
517
|
+
innerRoutes.push({
|
|
472
518
|
path: "*",
|
|
473
519
|
element: NotFoundComponent
|
|
474
520
|
? createElement(NotFoundComponent)
|
|
@@ -476,6 +522,14 @@ routes.push({
|
|
|
476
522
|
errorElement: createElement(RevineErrorDialog),
|
|
477
523
|
});
|
|
478
524
|
|
|
525
|
+
const routes = [
|
|
526
|
+
{
|
|
527
|
+
element: createElement(MiddlewareGuard, null, createElement(Outlet)),
|
|
528
|
+
children: innerRoutes,
|
|
529
|
+
errorElement: createElement(RevineErrorDialog),
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
|
|
479
533
|
export const router = createBrowserRouter(routes, {
|
|
480
534
|
future: {
|
|
481
535
|
v7_startTransition: true,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type MiddlewareRequest = {
|
|
2
|
+
pathname: string;
|
|
3
|
+
searchParams: URLSearchParams;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type MiddlewareResponse =
|
|
7
|
+
| { type: "next" }
|
|
8
|
+
| { type: "redirect"; destination: string };
|
|
9
|
+
|
|
10
|
+
export const middlewareResponse = {
|
|
11
|
+
next: (): MiddlewareResponse => ({ type: "next" }),
|
|
12
|
+
redirect: (destination: string): MiddlewareResponse => ({
|
|
13
|
+
type: "redirect",
|
|
14
|
+
destination,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type MiddlewareFn = (
|
|
19
|
+
request: MiddlewareRequest,
|
|
20
|
+
) => MiddlewareResponse | Promise<MiddlewareResponse>;
|
|
21
|
+
|
|
22
|
+
export type MiddlewareConfig = {
|
|
23
|
+
publicPaths?: string[]; // paths accessible WITHOUT login
|
|
24
|
+
authPaths?: string[]; // paths only for GUESTS (login, register)
|
|
25
|
+
redirects?: {
|
|
26
|
+
whenAuthenticated?: string; // redirect auth pages to (default: "/")
|
|
27
|
+
whenUnauthenticated?: string; // redirect private pages to (default: "/login")
|
|
28
|
+
};
|
|
29
|
+
};
|