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.
- package/dist/client.d.ts +6 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -1
- package/dist/components/Image.d.ts +42 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +106 -0
- package/dist/runtime/bundler/errorBoundary.d.ts +2 -0
- package/dist/runtime/bundler/errorBoundary.d.ts.map +1 -0
- package/dist/runtime/bundler/errorBoundary.js +122 -0
- package/dist/runtime/bundler/revinePlugin.d.ts.map +1 -1
- package/dist/runtime/bundler/revinePlugin.js +361 -2
- package/package.json +1 -1
- package/src/client.ts +6 -4
- package/src/components/Image.tsx +269 -0
- package/src/runtime/bundler/revinePlugin.ts +373 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ImgHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
type ObjectFit = "contain" | "cover" | "fill" | "none" | "scale-down";
|
|
4
|
+
|
|
5
|
+
export interface ImageProps extends Omit<
|
|
6
|
+
ImgHTMLAttributes<HTMLImageElement>,
|
|
7
|
+
"src" | "width" | "height" | "placeholder"
|
|
8
|
+
> {
|
|
9
|
+
/** Image source URL or import (e.g. import logo from './logo.png') */
|
|
10
|
+
src: string;
|
|
11
|
+
/** Alt text — required for accessibility */
|
|
12
|
+
alt: string;
|
|
13
|
+
/** Intrinsic width in px. Required unless fill={true} */
|
|
14
|
+
width?: number;
|
|
15
|
+
/** Intrinsic height in px. Required unless fill={true} */
|
|
16
|
+
height?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Stretch the image to fill its parent container (parent must be position:relative).
|
|
19
|
+
* When true, width/height are not required.
|
|
20
|
+
*/
|
|
21
|
+
fill?: boolean;
|
|
22
|
+
/** CSS object-fit when fill is true. Defaults to "cover" */
|
|
23
|
+
objectFit?: ObjectFit;
|
|
24
|
+
/** CSS object-position when fill is true. Defaults to "center" */
|
|
25
|
+
objectPosition?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Eagerly load the image and skip lazy loading.
|
|
28
|
+
* Use for above-the-fold images (hero, LCP element).
|
|
29
|
+
*/
|
|
30
|
+
priority?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Show a blurred low-quality placeholder while the image loads.
|
|
33
|
+
* Pass a base64 data URL or a solid color string like "#e5e7eb".
|
|
34
|
+
* Defaults to a subtle gray shimmer if not provided.
|
|
35
|
+
*/
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** Called when the image fails to load */
|
|
38
|
+
onError?: () => void;
|
|
39
|
+
/** Custom fallback element shown on error */
|
|
40
|
+
fallback?: React.ReactNode;
|
|
41
|
+
className?: string;
|
|
42
|
+
style?: React.CSSProperties;
|
|
43
|
+
quality?: never; // reserved for future CDN support — not implemented yet
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const shimmerBase64 =
|
|
47
|
+
"data:image/svg+xml;base64," +
|
|
48
|
+
btoa(`<svg xmlns='http://www.w3.org/2000/svg' width='400' height='300'>
|
|
49
|
+
<defs>
|
|
50
|
+
<linearGradient id='g' x1='0%' y1='0%' x2='100%' y2='0%'>
|
|
51
|
+
<stop offset='0%' stop-color='#e8e8e8'/>
|
|
52
|
+
<stop offset='50%' stop-color='#f0f0f0'/>
|
|
53
|
+
<stop offset='100%' stop-color='#e8e8e8'/>
|
|
54
|
+
</linearGradient>
|
|
55
|
+
</defs>
|
|
56
|
+
<rect width='400' height='300' fill='url(#g)'/>
|
|
57
|
+
</svg>`);
|
|
58
|
+
|
|
59
|
+
export function Image({
|
|
60
|
+
src,
|
|
61
|
+
alt,
|
|
62
|
+
width,
|
|
63
|
+
height,
|
|
64
|
+
fill = false,
|
|
65
|
+
objectFit = "cover",
|
|
66
|
+
objectPosition = "center",
|
|
67
|
+
priority = false,
|
|
68
|
+
placeholder,
|
|
69
|
+
onError,
|
|
70
|
+
fallback,
|
|
71
|
+
className,
|
|
72
|
+
style,
|
|
73
|
+
...rest
|
|
74
|
+
}: ImageProps) {
|
|
75
|
+
const [loaded, setLoaded] = useState(false);
|
|
76
|
+
const [errored, setErrored] = useState(false);
|
|
77
|
+
const imgRef = useRef<HTMLImageElement>(null);
|
|
78
|
+
|
|
79
|
+
// If image is already cached (e.g. browser back-nav), mark loaded immediately
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (imgRef.current?.complete && imgRef.current.naturalWidth > 0) {
|
|
82
|
+
setLoaded(true);
|
|
83
|
+
}
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const handleError = () => {
|
|
87
|
+
setErrored(true);
|
|
88
|
+
onError?.();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const placeholderSrc = placeholder ?? shimmerBase64;
|
|
92
|
+
|
|
93
|
+
// ── Fill mode: stretch to parent container
|
|
94
|
+
if (fill) {
|
|
95
|
+
return (
|
|
96
|
+
<span
|
|
97
|
+
style={{
|
|
98
|
+
position: "absolute",
|
|
99
|
+
inset: 0,
|
|
100
|
+
display: "block",
|
|
101
|
+
overflow: "hidden",
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{/* Placeholder layer */}
|
|
105
|
+
{!loaded && !errored && (
|
|
106
|
+
<span
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
style={{
|
|
109
|
+
position: "absolute",
|
|
110
|
+
inset: 0,
|
|
111
|
+
backgroundImage: placeholder
|
|
112
|
+
? `url(${placeholderSrc})`
|
|
113
|
+
: undefined,
|
|
114
|
+
backgroundColor: placeholder ? undefined : "#e8e8e8",
|
|
115
|
+
backgroundSize: "cover",
|
|
116
|
+
backgroundPosition: "center",
|
|
117
|
+
filter: placeholder ? "blur(8px)" : undefined,
|
|
118
|
+
transform: "scale(1.05)",
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
{!errored ? (
|
|
123
|
+
<img
|
|
124
|
+
ref={imgRef}
|
|
125
|
+
src={src}
|
|
126
|
+
alt={alt}
|
|
127
|
+
loading={priority ? "eager" : "lazy"}
|
|
128
|
+
decoding={priority ? "sync" : "async"}
|
|
129
|
+
fetchPriority={priority ? "high" : "auto"}
|
|
130
|
+
onLoad={() => setLoaded(true)}
|
|
131
|
+
onError={handleError}
|
|
132
|
+
className={className}
|
|
133
|
+
style={{
|
|
134
|
+
position: "absolute",
|
|
135
|
+
inset: 0,
|
|
136
|
+
width: "100%",
|
|
137
|
+
height: "100%",
|
|
138
|
+
objectFit,
|
|
139
|
+
objectPosition,
|
|
140
|
+
opacity: loaded ? 1 : 0,
|
|
141
|
+
transition: "opacity 300ms ease",
|
|
142
|
+
...style,
|
|
143
|
+
}}
|
|
144
|
+
{...rest}
|
|
145
|
+
/>
|
|
146
|
+
) : fallback ? (
|
|
147
|
+
<>{fallback}</>
|
|
148
|
+
) : (
|
|
149
|
+
<DefaultFallback fill />
|
|
150
|
+
)}
|
|
151
|
+
</span>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Fixed size mode
|
|
156
|
+
if (!width || !height) {
|
|
157
|
+
console.warn(
|
|
158
|
+
"[Revine <Image>] `width` and `height` are required when `fill` is not set. " +
|
|
159
|
+
`Missing on: ${src}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<span
|
|
165
|
+
style={{
|
|
166
|
+
display: "inline-block",
|
|
167
|
+
position: "relative",
|
|
168
|
+
width: width ? `${width}px` : undefined,
|
|
169
|
+
height: height ? `${height}px` : undefined,
|
|
170
|
+
overflow: "hidden",
|
|
171
|
+
flexShrink: 0,
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{/* Placeholder layer */}
|
|
175
|
+
{!loaded && !errored && (
|
|
176
|
+
<span
|
|
177
|
+
aria-hidden="true"
|
|
178
|
+
style={{
|
|
179
|
+
position: "absolute",
|
|
180
|
+
inset: 0,
|
|
181
|
+
backgroundImage: placeholder ? `url(${placeholderSrc})` : undefined,
|
|
182
|
+
backgroundColor: placeholder ? undefined : "#e8e8e8",
|
|
183
|
+
backgroundSize: "cover",
|
|
184
|
+
backgroundPosition: "center",
|
|
185
|
+
filter: placeholder ? "blur(8px)" : undefined,
|
|
186
|
+
transform: "scale(1.05)",
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
{!errored ? (
|
|
191
|
+
<img
|
|
192
|
+
ref={imgRef}
|
|
193
|
+
src={src}
|
|
194
|
+
alt={alt}
|
|
195
|
+
width={width}
|
|
196
|
+
height={height}
|
|
197
|
+
loading={priority ? "eager" : "lazy"}
|
|
198
|
+
decoding={priority ? "sync" : "async"}
|
|
199
|
+
fetchPriority={priority ? "high" : "auto"}
|
|
200
|
+
onLoad={() => setLoaded(true)}
|
|
201
|
+
onError={handleError}
|
|
202
|
+
className={className}
|
|
203
|
+
style={{
|
|
204
|
+
display: "block",
|
|
205
|
+
width: "100%",
|
|
206
|
+
height: "100%",
|
|
207
|
+
objectFit: "cover",
|
|
208
|
+
opacity: loaded ? 1 : 0,
|
|
209
|
+
transition: "opacity 300ms ease",
|
|
210
|
+
...style,
|
|
211
|
+
}}
|
|
212
|
+
{...rest}
|
|
213
|
+
/>
|
|
214
|
+
) : fallback ? (
|
|
215
|
+
<>{fallback}</>
|
|
216
|
+
) : (
|
|
217
|
+
<DefaultFallback width={width} height={height} />
|
|
218
|
+
)}
|
|
219
|
+
</span>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function DefaultFallback({
|
|
224
|
+
width,
|
|
225
|
+
height,
|
|
226
|
+
fill,
|
|
227
|
+
}: {
|
|
228
|
+
width?: number;
|
|
229
|
+
height?: number;
|
|
230
|
+
fill?: boolean;
|
|
231
|
+
}) {
|
|
232
|
+
return (
|
|
233
|
+
<span
|
|
234
|
+
role="img"
|
|
235
|
+
aria-label="Image failed to load"
|
|
236
|
+
style={{
|
|
237
|
+
position: fill ? "absolute" : "relative",
|
|
238
|
+
inset: fill ? 0 : undefined,
|
|
239
|
+
display: "flex",
|
|
240
|
+
alignItems: "center",
|
|
241
|
+
justifyContent: "center",
|
|
242
|
+
width: fill ? "100%" : width ? `${width}px` : "100%",
|
|
243
|
+
height: fill ? "100%" : height ? `${height}px` : "100%",
|
|
244
|
+
background: "#f3f4f6",
|
|
245
|
+
color: "#9ca3af",
|
|
246
|
+
fontSize: "12px",
|
|
247
|
+
fontFamily: "system-ui, sans-serif",
|
|
248
|
+
gap: "6px",
|
|
249
|
+
flexDirection: "column",
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<svg
|
|
253
|
+
width="24"
|
|
254
|
+
height="24"
|
|
255
|
+
viewBox="0 0 24 24"
|
|
256
|
+
fill="none"
|
|
257
|
+
stroke="currentColor"
|
|
258
|
+
strokeWidth="1.5"
|
|
259
|
+
strokeLinecap="round"
|
|
260
|
+
strokeLinejoin="round"
|
|
261
|
+
>
|
|
262
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
263
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
264
|
+
<polyline points="21 15 16 10 5 21" />
|
|
265
|
+
</svg>
|
|
266
|
+
<span>Failed to load</span>
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
@@ -1,5 +1,350 @@
|
|
|
1
1
|
const VIRTUAL_ROUTING_ID = "\0revine:routing";
|
|
2
2
|
|
|
3
|
+
const errorBoundaryComponent = `
|
|
4
|
+
function RevineErrorDialog() {
|
|
5
|
+
const error = useRouteError();
|
|
6
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
7
|
+
const [copied, setCopied] = React.useState(false);
|
|
8
|
+
|
|
9
|
+
const message = error?.message || String(error) || "An unexpected error occurred.";
|
|
10
|
+
const stack = error?.stack || "";
|
|
11
|
+
const stackLines = stack
|
|
12
|
+
.split("\\n")
|
|
13
|
+
.filter((l) => l.trim().startsWith("at "))
|
|
14
|
+
.slice(0, 8)
|
|
15
|
+
.join("\\n");
|
|
16
|
+
|
|
17
|
+
const handleCopy = () => {
|
|
18
|
+
const text = message + (stackLines ? "\\n\\n" + stackLines : "");
|
|
19
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
20
|
+
setCopied(true);
|
|
21
|
+
setTimeout(() => setCopied(false), 2000);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return React.createElement(
|
|
26
|
+
"div",
|
|
27
|
+
{ style: overlayStyle },
|
|
28
|
+
React.createElement(
|
|
29
|
+
"div",
|
|
30
|
+
{ style: dialogStyle },
|
|
31
|
+
|
|
32
|
+
React.createElement(
|
|
33
|
+
"div",
|
|
34
|
+
{ style: topBarStyle },
|
|
35
|
+
React.createElement(
|
|
36
|
+
"div",
|
|
37
|
+
{ style: brandStyle },
|
|
38
|
+
React.createElement(
|
|
39
|
+
"svg",
|
|
40
|
+
{ width: "18", height: "18", viewBox: "0 0 24 24", fill: "none",
|
|
41
|
+
stroke: "#a78bfa", strokeWidth: "2.2", strokeLinecap: "round",
|
|
42
|
+
strokeLinejoin: "round", style: { flexShrink: 0 } },
|
|
43
|
+
React.createElement("polygon", { points: "13 2 3 14 12 14 11 22 21 10 12 10 13 2" })
|
|
44
|
+
),
|
|
45
|
+
React.createElement("span", { style: brandNameStyle }, "Revine")
|
|
46
|
+
),
|
|
47
|
+
React.createElement("span", { style: badgeStyle }, "Runtime Error")
|
|
48
|
+
),
|
|
49
|
+
|
|
50
|
+
React.createElement("div", { style: dividerStyle }),
|
|
51
|
+
|
|
52
|
+
React.createElement(
|
|
53
|
+
"div",
|
|
54
|
+
{ style: headerStyle },
|
|
55
|
+
React.createElement(
|
|
56
|
+
"div",
|
|
57
|
+
{ style: iconWrapStyle },
|
|
58
|
+
React.createElement(
|
|
59
|
+
"svg",
|
|
60
|
+
{ width: "16", height: "16", viewBox: "0 0 24 24", fill: "none",
|
|
61
|
+
stroke: "#f87171", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
62
|
+
React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
|
|
63
|
+
React.createElement("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
|
|
64
|
+
React.createElement("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
React.createElement("span", { style: titleStyle }, "Application Error")
|
|
68
|
+
),
|
|
69
|
+
|
|
70
|
+
React.createElement(
|
|
71
|
+
"div",
|
|
72
|
+
{ style: messagePanelStyle },
|
|
73
|
+
React.createElement("p", { style: messageStyle }, message),
|
|
74
|
+
React.createElement(
|
|
75
|
+
"button",
|
|
76
|
+
{ onClick: handleCopy, style: copyBtnStyle, title: "Copy error" },
|
|
77
|
+
copied
|
|
78
|
+
? React.createElement(
|
|
79
|
+
"svg",
|
|
80
|
+
{ width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
|
|
81
|
+
stroke: "#4ade80", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
82
|
+
React.createElement("polyline", { points: "20 6 9 17 4 12" })
|
|
83
|
+
)
|
|
84
|
+
: React.createElement(
|
|
85
|
+
"svg",
|
|
86
|
+
{ width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
|
|
87
|
+
stroke: "#888", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
88
|
+
React.createElement("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
|
|
89
|
+
React.createElement("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
),
|
|
93
|
+
|
|
94
|
+
stackLines.length > 0 &&
|
|
95
|
+
React.createElement(
|
|
96
|
+
"div",
|
|
97
|
+
{ style: stackSectionStyle },
|
|
98
|
+
React.createElement(
|
|
99
|
+
"button",
|
|
100
|
+
{ onClick: () => setExpanded((v) => !v), style: toggleBtnStyle },
|
|
101
|
+
React.createElement(
|
|
102
|
+
"svg",
|
|
103
|
+
{ width: "12", height: "12", viewBox: "0 0 24 24", fill: "none",
|
|
104
|
+
stroke: "#666", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round",
|
|
105
|
+
style: { transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
|
|
106
|
+
transition: "transform 200ms ease", flexShrink: 0 } },
|
|
107
|
+
React.createElement("polyline", { points: "9 18 15 12 9 6" })
|
|
108
|
+
),
|
|
109
|
+
React.createElement("span", null, "Stack trace")
|
|
110
|
+
),
|
|
111
|
+
expanded &&
|
|
112
|
+
React.createElement("pre", { style: stackStyle }, stackLines)
|
|
113
|
+
),
|
|
114
|
+
|
|
115
|
+
React.createElement(
|
|
116
|
+
"div",
|
|
117
|
+
{ style: actionsStyle },
|
|
118
|
+
React.createElement(
|
|
119
|
+
"button",
|
|
120
|
+
{ onClick: () => window.location.reload(), style: primaryBtnStyle },
|
|
121
|
+
React.createElement(
|
|
122
|
+
"svg",
|
|
123
|
+
{ width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
|
|
124
|
+
stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
125
|
+
React.createElement("polyline", { points: "23 4 23 10 17 10" }),
|
|
126
|
+
React.createElement("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
|
|
127
|
+
),
|
|
128
|
+
"Reload page"
|
|
129
|
+
),
|
|
130
|
+
React.createElement(
|
|
131
|
+
"button",
|
|
132
|
+
{ onClick: () => (window.location.href = "/"), style: secondaryBtnStyle },
|
|
133
|
+
React.createElement(
|
|
134
|
+
"svg",
|
|
135
|
+
{ width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
|
|
136
|
+
stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
137
|
+
React.createElement("path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
|
|
138
|
+
React.createElement("polyline", { points: "9 22 9 12 15 12 15 22" })
|
|
139
|
+
),
|
|
140
|
+
"Go to home"
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
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
|
+
`;
|
|
244
|
+
|
|
245
|
+
// ── Shared overlay HTML builder (used in both the inline script and module error handler)
|
|
246
|
+
// Written as a plain JS string so it can be embedded inside the injected <script> tag.
|
|
247
|
+
const overlayScriptContent = `
|
|
248
|
+
(function () {
|
|
249
|
+
function showOverlay(title, message, detail) {
|
|
250
|
+
if (document.getElementById('__revine_error_overlay__')) return;
|
|
251
|
+
|
|
252
|
+
var overlay = document.createElement('div');
|
|
253
|
+
overlay.id = '__revine_error_overlay__';
|
|
254
|
+
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';
|
|
255
|
+
|
|
256
|
+
var inner = document.createElement('div');
|
|
257
|
+
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)';
|
|
258
|
+
|
|
259
|
+
// Top bar
|
|
260
|
+
var topBar = document.createElement('div');
|
|
261
|
+
topBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:#0e0e0e';
|
|
262
|
+
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>';
|
|
263
|
+
|
|
264
|
+
// Divider
|
|
265
|
+
var divider = document.createElement('div');
|
|
266
|
+
divider.style.cssText = 'height:1px;background:#1f1f1f';
|
|
267
|
+
|
|
268
|
+
// Body
|
|
269
|
+
var body = document.createElement('div');
|
|
270
|
+
body.style.cssText = 'padding:20px 22px 0';
|
|
271
|
+
|
|
272
|
+
var header = document.createElement('div');
|
|
273
|
+
header.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:14px';
|
|
274
|
+
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>';
|
|
275
|
+
|
|
276
|
+
// Message panel with copy button
|
|
277
|
+
var msgPanel = document.createElement('div');
|
|
278
|
+
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';
|
|
279
|
+
|
|
280
|
+
var pre = document.createElement('pre');
|
|
281
|
+
pre.id = '__revine_err_detail__';
|
|
282
|
+
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';
|
|
283
|
+
pre.textContent = detail;
|
|
284
|
+
|
|
285
|
+
var copyBtn = document.createElement('button');
|
|
286
|
+
copyBtn.textContent = '⎘';
|
|
287
|
+
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';
|
|
288
|
+
copyBtn.onclick = function() {
|
|
289
|
+
navigator.clipboard.writeText(pre.textContent || '').then(function() {
|
|
290
|
+
copyBtn.textContent = '✓';
|
|
291
|
+
setTimeout(function() { copyBtn.textContent = '⎘'; }, 2000);
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
msgPanel.appendChild(pre);
|
|
296
|
+
msgPanel.appendChild(copyBtn);
|
|
297
|
+
body.appendChild(header);
|
|
298
|
+
body.appendChild(msgPanel);
|
|
299
|
+
|
|
300
|
+
// Actions
|
|
301
|
+
var actions = document.createElement('div');
|
|
302
|
+
actions.style.cssText = 'display:flex;gap:10px;padding:4px 22px 22px';
|
|
303
|
+
|
|
304
|
+
var reloadBtn = document.createElement('button');
|
|
305
|
+
reloadBtn.textContent = 'Reload page';
|
|
306
|
+
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)';
|
|
307
|
+
reloadBtn.onclick = function() { window.location.reload(); };
|
|
308
|
+
|
|
309
|
+
var dismissBtn = document.createElement('button');
|
|
310
|
+
dismissBtn.textContent = 'Dismiss';
|
|
311
|
+
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';
|
|
312
|
+
dismissBtn.onclick = function() { overlay.remove(); };
|
|
313
|
+
|
|
314
|
+
actions.appendChild(reloadBtn);
|
|
315
|
+
actions.appendChild(dismissBtn);
|
|
316
|
+
|
|
317
|
+
inner.appendChild(topBar);
|
|
318
|
+
inner.appendChild(divider);
|
|
319
|
+
inner.appendChild(body);
|
|
320
|
+
inner.appendChild(actions);
|
|
321
|
+
overlay.appendChild(inner);
|
|
322
|
+
document.body.appendChild(overlay);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Expose globally so the module onerror attribute can call it
|
|
326
|
+
window.__revineShowOverlay = showOverlay;
|
|
327
|
+
|
|
328
|
+
// Catches runtime JS errors and async rejections
|
|
329
|
+
window.addEventListener('error', function(e) {
|
|
330
|
+
// Ignore errors that already have an overlay
|
|
331
|
+
if (document.getElementById('__revine_error_overlay__')) return;
|
|
332
|
+
var msg = e.message || 'Unknown error';
|
|
333
|
+
var src = e.filename ? e.filename.replace(location.origin, '') : '';
|
|
334
|
+
var detail = src ? msg + '\\n\\nSource: ' + src + (e.lineno ? ':' + e.lineno : '') : msg;
|
|
335
|
+
showOverlay('Module Error', 'Failed to load module', detail);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
339
|
+
if (document.getElementById('__revine_error_overlay__')) return;
|
|
340
|
+
var reason = e.reason;
|
|
341
|
+
var msg = (reason && reason.message) ? reason.message : String(reason || 'Unhandled Promise rejection');
|
|
342
|
+
var detail = (reason && reason.stack) ? reason.stack : msg;
|
|
343
|
+
showOverlay('Unhandled Rejection', 'Promise rejected', detail);
|
|
344
|
+
});
|
|
345
|
+
})();
|
|
346
|
+
`;
|
|
347
|
+
|
|
3
348
|
export function revinePlugin(): any {
|
|
4
349
|
return {
|
|
5
350
|
name: "revine",
|
|
@@ -11,11 +356,35 @@ export function revinePlugin(): any {
|
|
|
11
356
|
}
|
|
12
357
|
},
|
|
13
358
|
|
|
359
|
+
transformIndexHtml(html: string) {
|
|
360
|
+
// 1. Inject the overlay listener script into <head>
|
|
361
|
+
let result = html.replace(
|
|
362
|
+
"</head>",
|
|
363
|
+
`<script>${overlayScriptContent}</script></head>`,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// 2. Find the main module entry <script> tag and attach an onerror handler.
|
|
367
|
+
// This is the ONLY way to catch ES module link-time errors like
|
|
368
|
+
// "does not provide an export named 'X'" — window.onerror does NOT fire for these.
|
|
369
|
+
result = result.replace(
|
|
370
|
+
/(<script\s[^>]*type=["']module["'][^>]*src=["'][^"']+["'][^>]*)(\/?>)/g,
|
|
371
|
+
(_match: string, opening: string, closing: string) =>
|
|
372
|
+
opening +
|
|
373
|
+
` 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.')"` +
|
|
374
|
+
closing,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
return result;
|
|
378
|
+
},
|
|
379
|
+
|
|
14
380
|
load(id: string) {
|
|
15
381
|
if (id === VIRTUAL_ROUTING_ID) {
|
|
16
382
|
return `
|
|
17
|
-
import { createBrowserRouter } from "react-router-dom";
|
|
383
|
+
import { createBrowserRouter, useRouteError } from "react-router-dom";
|
|
18
384
|
import { lazy, Suspense, createElement } from "react";
|
|
385
|
+
import React from "react";
|
|
386
|
+
|
|
387
|
+
${errorBoundaryComponent}
|
|
19
388
|
|
|
20
389
|
const notFoundModules = import.meta.glob("/src/NotFound.tsx", { eager: true });
|
|
21
390
|
const NotFoundComponent = Object.values(notFoundModules)[0]?.default;
|
|
@@ -84,7 +453,7 @@ const routes = pageEntries.map(([filePath, component]) => {
|
|
|
84
453
|
|
|
85
454
|
const fallback = Loading
|
|
86
455
|
? createElement(Loading)
|
|
87
|
-
: createElement("div", null, "Loading
|
|
456
|
+
: createElement("div", null, "Loading\u2026");
|
|
88
457
|
|
|
89
458
|
const pageElement = createElement(
|
|
90
459
|
Suspense,
|
|
@@ -95,6 +464,7 @@ const routes = pageEntries.map(([filePath, component]) => {
|
|
|
95
464
|
return {
|
|
96
465
|
path: routePath,
|
|
97
466
|
element: layouts.length > 0 ? wrapWithLayouts(pageElement, layouts) : pageElement,
|
|
467
|
+
errorElement: createElement(RevineErrorDialog),
|
|
98
468
|
};
|
|
99
469
|
});
|
|
100
470
|
|
|
@@ -103,6 +473,7 @@ routes.push({
|
|
|
103
473
|
element: NotFoundComponent
|
|
104
474
|
? createElement(NotFoundComponent)
|
|
105
475
|
: createElement("div", null, "404 - Page Not Found"),
|
|
476
|
+
errorElement: createElement(RevineErrorDialog),
|
|
106
477
|
});
|
|
107
478
|
|
|
108
479
|
export const router = createBrowserRouter(routes);
|