revine 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,347 @@
1
1
  const VIRTUAL_ROUTING_ID = "\0revine:routing";
2
+ const errorBoundaryComponent = `
3
+ function RevineErrorDialog() {
4
+ const error = useRouteError();
5
+ const [expanded, setExpanded] = React.useState(false);
6
+ const [copied, setCopied] = React.useState(false);
7
+
8
+ const message = error?.message || String(error) || "An unexpected error occurred.";
9
+ const stack = error?.stack || "";
10
+ const stackLines = stack
11
+ .split("\\n")
12
+ .filter((l) => l.trim().startsWith("at "))
13
+ .slice(0, 8)
14
+ .join("\\n");
15
+
16
+ const handleCopy = () => {
17
+ const text = message + (stackLines ? "\\n\\n" + stackLines : "");
18
+ navigator.clipboard.writeText(text).then(() => {
19
+ setCopied(true);
20
+ setTimeout(() => setCopied(false), 2000);
21
+ });
22
+ };
23
+
24
+ return React.createElement(
25
+ "div",
26
+ { style: overlayStyle },
27
+ React.createElement(
28
+ "div",
29
+ { style: dialogStyle },
30
+
31
+ React.createElement(
32
+ "div",
33
+ { style: topBarStyle },
34
+ React.createElement(
35
+ "div",
36
+ { style: brandStyle },
37
+ React.createElement(
38
+ "svg",
39
+ { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none",
40
+ stroke: "#a78bfa", strokeWidth: "2.2", strokeLinecap: "round",
41
+ strokeLinejoin: "round", style: { flexShrink: 0 } },
42
+ React.createElement("polygon", { points: "13 2 3 14 12 14 11 22 21 10 12 10 13 2" })
43
+ ),
44
+ React.createElement("span", { style: brandNameStyle }, "Revine")
45
+ ),
46
+ React.createElement("span", { style: badgeStyle }, "Runtime Error")
47
+ ),
48
+
49
+ React.createElement("div", { style: dividerStyle }),
50
+
51
+ React.createElement(
52
+ "div",
53
+ { style: headerStyle },
54
+ React.createElement(
55
+ "div",
56
+ { style: iconWrapStyle },
57
+ React.createElement(
58
+ "svg",
59
+ { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none",
60
+ stroke: "#f87171", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
61
+ React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
62
+ React.createElement("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
63
+ React.createElement("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
64
+ )
65
+ ),
66
+ React.createElement("span", { style: titleStyle }, "Application Error")
67
+ ),
68
+
69
+ React.createElement(
70
+ "div",
71
+ { style: messagePanelStyle },
72
+ React.createElement("p", { style: messageStyle }, message),
73
+ React.createElement(
74
+ "button",
75
+ { onClick: handleCopy, style: copyBtnStyle, title: "Copy error" },
76
+ copied
77
+ ? React.createElement(
78
+ "svg",
79
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
80
+ stroke: "#4ade80", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
81
+ React.createElement("polyline", { points: "20 6 9 17 4 12" })
82
+ )
83
+ : React.createElement(
84
+ "svg",
85
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
86
+ stroke: "#888", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
87
+ React.createElement("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
88
+ React.createElement("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
89
+ )
90
+ )
91
+ ),
92
+
93
+ stackLines.length > 0 &&
94
+ React.createElement(
95
+ "div",
96
+ { style: stackSectionStyle },
97
+ React.createElement(
98
+ "button",
99
+ { onClick: () => setExpanded((v) => !v), style: toggleBtnStyle },
100
+ React.createElement(
101
+ "svg",
102
+ { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none",
103
+ stroke: "#666", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round",
104
+ style: { transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
105
+ transition: "transform 200ms ease", flexShrink: 0 } },
106
+ React.createElement("polyline", { points: "9 18 15 12 9 6" })
107
+ ),
108
+ React.createElement("span", null, "Stack trace")
109
+ ),
110
+ expanded &&
111
+ React.createElement("pre", { style: stackStyle }, stackLines)
112
+ ),
113
+
114
+ React.createElement(
115
+ "div",
116
+ { style: actionsStyle },
117
+ React.createElement(
118
+ "button",
119
+ { onClick: () => window.location.reload(), style: primaryBtnStyle },
120
+ React.createElement(
121
+ "svg",
122
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
123
+ stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
124
+ React.createElement("polyline", { points: "23 4 23 10 17 10" }),
125
+ React.createElement("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
126
+ ),
127
+ "Reload page"
128
+ ),
129
+ React.createElement(
130
+ "button",
131
+ { onClick: () => (window.location.href = "/"), style: secondaryBtnStyle },
132
+ React.createElement(
133
+ "svg",
134
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
135
+ stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
136
+ React.createElement("path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
137
+ React.createElement("polyline", { points: "9 22 9 12 15 12 15 22" })
138
+ ),
139
+ "Go to home"
140
+ )
141
+ )
142
+ )
143
+ );
144
+ }
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
+ `;
243
+ // ── Shared overlay HTML builder (used in both the inline script and module error handler)
244
+ // Written as a plain JS string so it can be embedded inside the injected <script> tag.
245
+ const overlayScriptContent = `
246
+ (function () {
247
+ function showOverlay(title, message, detail) {
248
+ if (document.getElementById('__revine_error_overlay__')) return;
249
+
250
+ var overlay = document.createElement('div');
251
+ overlay.id = '__revine_error_overlay__';
252
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.72);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif';
253
+
254
+ var inner = document.createElement('div');
255
+ inner.style.cssText = 'background:#141414;border:1px solid #2a2a2a;border-radius:14px;max-width:580px;width:92%;overflow:hidden;box-shadow:0 32px 80px rgba(0,0,0,0.7)';
256
+
257
+ // Top bar
258
+ var topBar = document.createElement('div');
259
+ topBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:#0e0e0e';
260
+ topBar.innerHTML = '<div style="display:flex;align-items:center;gap:7px"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#a78bfa" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg><span style="font-size:13px;font-weight:700;color:#c4b5fd;letter-spacing:0.04em">Revine</span></div><span style="font-size:11px;font-weight:600;color:#f87171;background:rgba(248,113,113,0.1);border:1px solid rgba(248,113,113,0.2);border-radius:999px;padding:2px 10px">' + title + '</span>';
261
+
262
+ // Divider
263
+ var divider = document.createElement('div');
264
+ divider.style.cssText = 'height:1px;background:#1f1f1f';
265
+
266
+ // Body
267
+ var body = document.createElement('div');
268
+ body.style.cssText = 'padding:20px 22px 0';
269
+
270
+ var header = document.createElement('div');
271
+ header.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:14px';
272
+ header.innerHTML = '<div style="width:28px;height:28px;border-radius:8px;flex-shrink:0;background:rgba(248,113,113,0.1);border:1px solid rgba(248,113,113,0.15);display:flex;align-items:center;justify-content:center"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f87171" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div><span style="font-size:15px;font-weight:600;color:#fff">' + message + '</span>';
273
+
274
+ // Message panel with copy button
275
+ var msgPanel = document.createElement('div');
276
+ msgPanel.style.cssText = 'position:relative;background:rgba(248,113,113,0.05);border:1px solid rgba(248,113,113,0.12);border-radius:8px;padding:12px 44px 12px 14px;margin-bottom:14px';
277
+
278
+ var pre = document.createElement('pre');
279
+ pre.id = '__revine_err_detail__';
280
+ pre.style.cssText = 'font-family:ui-monospace,monospace;font-size:12px;color:#fca5a5;margin:0;line-height:1.65;white-space:pre-wrap;word-break:break-all';
281
+ pre.textContent = detail;
282
+
283
+ var copyBtn = document.createElement('button');
284
+ copyBtn.textContent = '⎘';
285
+ copyBtn.style.cssText = 'position:absolute;top:10px;right:10px;background:rgba(255,255,255,0.05);border:1px solid #2e2e2e;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#888;font-size:13px';
286
+ copyBtn.onclick = function() {
287
+ navigator.clipboard.writeText(pre.textContent || '').then(function() {
288
+ copyBtn.textContent = '✓';
289
+ setTimeout(function() { copyBtn.textContent = '⎘'; }, 2000);
290
+ });
291
+ };
292
+
293
+ msgPanel.appendChild(pre);
294
+ msgPanel.appendChild(copyBtn);
295
+ body.appendChild(header);
296
+ body.appendChild(msgPanel);
297
+
298
+ // Actions
299
+ var actions = document.createElement('div');
300
+ actions.style.cssText = 'display:flex;gap:10px;padding:4px 22px 22px';
301
+
302
+ var reloadBtn = document.createElement('button');
303
+ reloadBtn.textContent = 'Reload page';
304
+ reloadBtn.style.cssText = 'flex:1;padding:10px 0;border-radius:8px;border:none;background:linear-gradient(135deg,#7c3aed,#6d28d9);color:#fff;font-weight:600;font-size:13px;cursor:pointer;box-shadow:0 2px 12px rgba(124,58,237,0.35)';
305
+ reloadBtn.onclick = function() { window.location.reload(); };
306
+
307
+ var dismissBtn = document.createElement('button');
308
+ dismissBtn.textContent = 'Dismiss';
309
+ dismissBtn.style.cssText = 'flex:1;padding:10px 0;border-radius:8px;border:1px solid #2e2e2e;background:rgba(255,255,255,0.03);color:#999;font-size:13px;cursor:pointer';
310
+ dismissBtn.onclick = function() { overlay.remove(); };
311
+
312
+ actions.appendChild(reloadBtn);
313
+ actions.appendChild(dismissBtn);
314
+
315
+ inner.appendChild(topBar);
316
+ inner.appendChild(divider);
317
+ inner.appendChild(body);
318
+ inner.appendChild(actions);
319
+ overlay.appendChild(inner);
320
+ document.body.appendChild(overlay);
321
+ }
322
+
323
+ // Expose globally so the module onerror attribute can call it
324
+ window.__revineShowOverlay = showOverlay;
325
+
326
+ // Catches runtime JS errors and async rejections
327
+ window.addEventListener('error', function(e) {
328
+ // Ignore errors that already have an overlay
329
+ if (document.getElementById('__revine_error_overlay__')) return;
330
+ var msg = e.message || 'Unknown error';
331
+ var src = e.filename ? e.filename.replace(location.origin, '') : '';
332
+ var detail = src ? msg + '\\n\\nSource: ' + src + (e.lineno ? ':' + e.lineno : '') : msg;
333
+ showOverlay('Module Error', 'Failed to load module', detail);
334
+ });
335
+
336
+ window.addEventListener('unhandledrejection', function(e) {
337
+ if (document.getElementById('__revine_error_overlay__')) return;
338
+ var reason = e.reason;
339
+ var msg = (reason && reason.message) ? reason.message : String(reason || 'Unhandled Promise rejection');
340
+ var detail = (reason && reason.stack) ? reason.stack : msg;
341
+ showOverlay('Unhandled Rejection', 'Promise rejected', detail);
342
+ });
343
+ })();
344
+ `;
2
345
  export function revinePlugin() {
3
346
  return {
4
347
  name: "revine",
@@ -8,11 +351,25 @@ export function revinePlugin() {
8
351
  return VIRTUAL_ROUTING_ID;
9
352
  }
10
353
  },
354
+ transformIndexHtml(html) {
355
+ // 1. Inject the overlay listener script into <head>
356
+ let result = html.replace("</head>", `<script>${overlayScriptContent}</script></head>`);
357
+ // 2. Find the main module entry <script> tag and attach an onerror handler.
358
+ // This is the ONLY way to catch ES module link-time errors like
359
+ // "does not provide an export named 'X'" — window.onerror does NOT fire for these.
360
+ result = result.replace(/(<script\s[^>]*type=["']module["'][^>]*src=["'][^"']+["'][^>]*)(\/?>)/g, (_match, opening, closing) => opening +
361
+ ` onerror="window.__revineShowOverlay && window.__revineShowOverlay('Module Error','Failed to load application',event.type+': A module failed to load. Check all imports are valid and run \\'npm run build\\' in the Revine package.')"` +
362
+ closing);
363
+ return result;
364
+ },
11
365
  load(id) {
12
366
  if (id === VIRTUAL_ROUTING_ID) {
13
367
  return `
14
- import { createBrowserRouter } from "react-router-dom";
368
+ import { createBrowserRouter, useRouteError } from "react-router-dom";
15
369
  import { lazy, Suspense, createElement } from "react";
370
+ import React from "react";
371
+
372
+ ${errorBoundaryComponent}
16
373
 
17
374
  const notFoundModules = import.meta.glob("/src/NotFound.tsx", { eager: true });
18
375
  const NotFoundComponent = Object.values(notFoundModules)[0]?.default;
@@ -81,7 +438,7 @@ const routes = pageEntries.map(([filePath, component]) => {
81
438
 
82
439
  const fallback = Loading
83
440
  ? createElement(Loading)
84
- : createElement("div", null, "Loading\\u2026");
441
+ : createElement("div", null, "Loading\u2026");
85
442
 
86
443
  const pageElement = createElement(
87
444
  Suspense,
@@ -92,6 +449,7 @@ const routes = pageEntries.map(([filePath, component]) => {
92
449
  return {
93
450
  path: routePath,
94
451
  element: layouts.length > 0 ? wrapWithLayouts(pageElement, layouts) : pageElement,
452
+ errorElement: createElement(RevineErrorDialog),
95
453
  };
96
454
  });
97
455
 
@@ -100,6 +458,7 @@ routes.push({
100
458
  element: NotFoundComponent
101
459
  ? createElement(NotFoundComponent)
102
460
  : createElement("div", null, "404 - Page Not Found"),
461
+ errorElement: createElement(RevineErrorDialog),
103
462
  });
104
463
 
105
464
  export const router = createBrowserRouter(routes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revine",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "A react framework, but better.",
5
5
  "license": "MIT",
6
6
  "author": "Rachit Bharadwaj",
package/src/client.ts CHANGED
@@ -4,12 +4,14 @@ export {
4
4
  useLocation,
5
5
  useNavigate,
6
6
  useParams,
7
- useSearchParams
7
+ useSearchParams,
8
8
  } from "react-router-dom";
9
- export type { NavLinkProps } from "./components/NavLink.js";
9
+ export { Image } from "./components/Image.js";
10
+ export type { ImageProps } from "./components/Image.js";
10
11
  export { Link } from "./components/Link.js";
12
+ export type { LinkProps } from "./components/Link.js";
11
13
  export { NavLink } from "./components/NavLink.js";
14
+ export type { NavLinkProps } from "./components/NavLink.js";
12
15
  export { defineConfig } from "./runtime/defineConfig.js";
13
- export type { LayoutProps } from "./runtime/types.js";
14
- export type { LinkProps } from "./components/Link.js";
15
16
  export { env, envAll } from "./runtime/env.js";
17
+ export type { LayoutProps } from "./runtime/types.js";