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 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
@@ -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,CA6IlC"}
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(/\\\\/g, "/");
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 routes = pageEntries.map(([filePath, component]) => {
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
- routes.push({
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"}
@@ -0,0 +1,7 @@
1
+ export const middlewareResponse = {
2
+ next: () => ({ type: "next" }),
3
+ redirect: (destination) => ({
4
+ type: "redirect",
5
+ destination,
6
+ }),
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revine",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "A react framework, but better.",
5
5
  "license": "MIT",
6
6
  "author": "Rachit Bharadwaj",
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(/\\\\/g, "/");
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 routes = pageEntries.map(([filePath, component]) => {
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
- routes.push({
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
+ };