@pajh/buldng 0.0.4 → 0.0.6
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/package.json +7 -2
- package/src/MIME.json +11 -0
- package/src/buldng-async.js +20 -0
- package/src/buldng-files.js +59 -0
- package/src/buldng-html.js +262 -0
- package/src/buldng-lib.js +67 -411
- package/src/buldng-modal.ts +348 -0
- package/src/buldng-ops.js +21 -6
- package/src/buldng-typescript.js +77 -0
- package/src/buldng-validate.js +11 -4
- package/src/buldng-web.ts +55 -180
- package/src/buldng.css +130 -0
- package/src/dev-errors.js +16 -0
- package/src/layout-err.ts +171 -0
- package/src/layout.ts +4 -0
- package/src/serve-lib.js +574 -0
- package/src/serve.js +194 -0
- package/src/types/global.d.ts +9 -0
- package/src/wrapper.html +2 -0
- package/src/buldng-dev-errors.js +0 -84
package/src/buldng-web.ts
CHANGED
|
@@ -1,76 +1,41 @@
|
|
|
1
1
|
// buldng-web.ts
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
id: string;
|
|
18
|
-
type: "error" | "unhandledrejection";
|
|
19
|
-
message: string;
|
|
20
|
-
stack: string;
|
|
21
|
-
time: string;
|
|
22
|
-
url: string;
|
|
23
|
-
userAgent: string;
|
|
24
|
-
platform: string;
|
|
25
|
-
language: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Configure the global error handler.
|
|
30
|
-
* @param url A full URL path like "/error/" or "/fatal/".
|
|
31
|
-
*/
|
|
32
|
-
export function setErrorPage(url: string) {
|
|
33
|
-
if (typeof url !== "string" || !url.length) {
|
|
34
|
-
throw new Error("setErrorPage: URL must be a non-empty string");
|
|
2
|
+
// Runtime-only listener installer for BULDR.
|
|
3
|
+
// This module contains NO production logic and NO module-level state.
|
|
4
|
+
// It is safe to be bundled multiple times.
|
|
5
|
+
|
|
6
|
+
export { showModal } from "./buldng-modal";
|
|
7
|
+
export type {
|
|
8
|
+
ModalIcon,
|
|
9
|
+
ModalResult,
|
|
10
|
+
ModalSettings,
|
|
11
|
+
ModalType,
|
|
12
|
+
} from "./buldng-modal";
|
|
13
|
+
|
|
14
|
+
export function setErrorPage(page: string) {
|
|
15
|
+
if (typeof page !== "string" || !page.length) {
|
|
16
|
+
throw new Error("setErrorPage: page must be a non-empty string");
|
|
35
17
|
}
|
|
36
18
|
|
|
37
|
-
//
|
|
38
|
-
errorPage = url.endsWith("/") ? url.slice(0, -1) : url;
|
|
39
|
-
|
|
40
|
-
// Install global listeners once. Calling setErrorPage again only changes the destination.
|
|
41
|
-
installGlobalErrorHandler();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Internal: installs global error + unhandled rejection handlers.
|
|
46
|
-
*/
|
|
47
|
-
function installGlobalErrorHandler() {
|
|
48
|
-
if (!errorPage || listenersInstalled || isErrorPage() || window.__buldngErrorOverlay) return;
|
|
49
|
-
|
|
50
|
-
listenersInstalled = true;
|
|
51
|
-
|
|
19
|
+
// Capture `page` in closure — NOT stored on window
|
|
52
20
|
window.addEventListener("error", (event) => {
|
|
53
|
-
|
|
54
|
-
type: "error",
|
|
55
|
-
reason: event.error ?? event.message,
|
|
56
|
-
fallbackMessage: event.message,
|
|
57
|
-
});
|
|
21
|
+
window.__error_handler?.(event, page);
|
|
58
22
|
});
|
|
59
23
|
|
|
60
24
|
window.addEventListener("unhandledrejection", (event) => {
|
|
61
|
-
|
|
62
|
-
type: "unhandledrejection",
|
|
63
|
-
reason: event.reason,
|
|
64
|
-
fallbackMessage: "Unhandled promise rejection",
|
|
65
|
-
});
|
|
25
|
+
window.__error_handler?.(event, page);
|
|
66
26
|
});
|
|
27
|
+
|
|
28
|
+
console.log(`BULDR: Error handler listener installed, redirect ${page}`);
|
|
29
|
+
|
|
67
30
|
}
|
|
68
31
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
32
|
+
// ------------------------------------------------------------
|
|
33
|
+
// ERROR PAGE SUPPORT — moved from layout-err.ts
|
|
34
|
+
// ------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
import type { ErrorPayload } from "./layout-err"; // or wherever you export it
|
|
37
|
+
|
|
38
|
+
export function readErrorPayload(id: string | null): ErrorPayload | null {
|
|
74
39
|
if (!id) return null;
|
|
75
40
|
|
|
76
41
|
try {
|
|
@@ -84,33 +49,25 @@ export function readErrorPayload(id = getErrorId()): ErrorPayload | null {
|
|
|
84
49
|
}
|
|
85
50
|
}
|
|
86
51
|
|
|
87
|
-
|
|
88
|
-
* Read and remove an error payload. Useful for one-shot error pages.
|
|
89
|
-
*/
|
|
90
|
-
export function consumeErrorPayload(id = getErrorId()): ErrorPayload | null {
|
|
52
|
+
export function consumeErrorPayload(id: string | null): ErrorPayload | null {
|
|
91
53
|
const payload = readErrorPayload(id);
|
|
92
54
|
if (id) {
|
|
93
55
|
try {
|
|
94
56
|
localStorage.removeItem(storageKey(id));
|
|
95
|
-
} catch {
|
|
96
|
-
// Storage may be unavailable or blocked; the payload was still safely read if possible.
|
|
97
|
-
}
|
|
57
|
+
} catch {}
|
|
98
58
|
}
|
|
99
59
|
return payload;
|
|
100
60
|
}
|
|
101
61
|
|
|
102
|
-
/**
|
|
103
|
-
* Render a payload as safe text inside a caller-owned element.
|
|
104
|
-
* Returns the payload that was rendered, or null when it was unavailable.
|
|
105
|
-
*/
|
|
106
62
|
export function renderErrorPayload(
|
|
107
63
|
elementId: string,
|
|
108
|
-
payload
|
|
64
|
+
payload: ErrorPayload | null
|
|
109
65
|
): ErrorPayload | null {
|
|
110
66
|
const container = document.getElementById(elementId);
|
|
111
67
|
if (!container) return null;
|
|
112
68
|
|
|
113
69
|
container.replaceChildren();
|
|
70
|
+
|
|
114
71
|
if (!payload) {
|
|
115
72
|
const empty = document.createElement("p");
|
|
116
73
|
empty.textContent = "Error details are no longer available.";
|
|
@@ -129,19 +86,24 @@ export function renderErrorPayload(
|
|
|
129
86
|
];
|
|
130
87
|
|
|
131
88
|
const list = document.createElement("dl");
|
|
89
|
+
|
|
132
90
|
for (const [label, value] of details) {
|
|
133
91
|
const term = document.createElement("dt");
|
|
134
92
|
term.textContent = label;
|
|
93
|
+
|
|
135
94
|
const description = document.createElement("dd");
|
|
136
95
|
description.textContent = value;
|
|
96
|
+
|
|
137
97
|
list.append(term, description);
|
|
138
98
|
}
|
|
139
99
|
|
|
140
100
|
if (payload.stack) {
|
|
141
101
|
const stackLabel = document.createElement("h3");
|
|
142
102
|
stackLabel.textContent = "Stack";
|
|
103
|
+
|
|
143
104
|
const stack = document.createElement("pre");
|
|
144
105
|
stack.textContent = payload.stack;
|
|
106
|
+
|
|
145
107
|
list.append(stackLabel, stack);
|
|
146
108
|
}
|
|
147
109
|
|
|
@@ -149,119 +111,32 @@ export function renderErrorPayload(
|
|
|
149
111
|
return payload;
|
|
150
112
|
}
|
|
151
113
|
|
|
152
|
-
/**
|
|
153
|
-
* Return the error record ID from the current URL.
|
|
154
|
-
*/
|
|
155
114
|
export function getErrorId(): string | null {
|
|
156
115
|
return new URLSearchParams(location.search).get("errorId");
|
|
157
116
|
}
|
|
158
117
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
fallbackMessage: string;
|
|
163
|
-
}) {
|
|
164
|
-
if (redirectStarted || !errorPage) return;
|
|
165
|
-
redirectStarted = true;
|
|
166
|
-
|
|
167
|
-
const payload = buildPayload(info);
|
|
168
|
-
const stored = saveErrorPayload(payload);
|
|
169
|
-
redirect(payload, stored);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function buildPayload(info: {
|
|
173
|
-
type: ErrorPayload["type"];
|
|
174
|
-
reason: unknown;
|
|
175
|
-
fallbackMessage: string;
|
|
176
|
-
}): ErrorPayload {
|
|
177
|
-
const id = createId();
|
|
178
|
-
const error = info.reason instanceof Error ? info.reason : null;
|
|
118
|
+
// ------------------------------------------------------------
|
|
119
|
+
// TYPE GUARDS & UTILITIES — also needed by error page
|
|
120
|
+
// ------------------------------------------------------------
|
|
179
121
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
type: info.type,
|
|
183
|
-
message: getErrorMessage(info.reason, info.fallbackMessage),
|
|
184
|
-
stack: error?.stack ?? getErrorStack(info.reason),
|
|
185
|
-
time: new Date().toISOString(),
|
|
186
|
-
url: location.href,
|
|
187
|
-
userAgent: navigator.userAgent,
|
|
188
|
-
platform: navigator.platform,
|
|
189
|
-
language: navigator.language,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Redirect to the configured error page with a storage key, plus a small fallback.
|
|
195
|
-
*/
|
|
196
|
-
function redirect(payload: ErrorPayload, stored: boolean) {
|
|
197
|
-
if (!errorPage) return;
|
|
198
|
-
|
|
199
|
-
const destination = new URL(errorPage, location.href);
|
|
200
|
-
if (stored) {
|
|
201
|
-
destination.searchParams.set("errorId", payload.id);
|
|
202
|
-
} else {
|
|
203
|
-
destination.searchParams.set("type", payload.type);
|
|
204
|
-
destination.searchParams.set("message", payload.message.slice(0, 500));
|
|
205
|
-
}
|
|
122
|
+
function isErrorPayload(value: unknown): value is ErrorPayload {
|
|
123
|
+
if (!value || typeof value !== "object") return false;
|
|
206
124
|
|
|
207
|
-
|
|
208
|
-
}
|
|
125
|
+
const p = value as Partial<ErrorPayload>;
|
|
209
126
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
127
|
+
return (
|
|
128
|
+
typeof p.id === "string" &&
|
|
129
|
+
(p.type === "error" || p.type === "unhandledrejection") &&
|
|
130
|
+
typeof p.message === "string" &&
|
|
131
|
+
typeof p.stack === "string" &&
|
|
132
|
+
typeof p.time === "string" &&
|
|
133
|
+
typeof p.url === "string" &&
|
|
134
|
+
typeof p.userAgent === "string" &&
|
|
135
|
+
typeof p.platform === "string" &&
|
|
136
|
+
typeof p.language === "string"
|
|
137
|
+
);
|
|
217
138
|
}
|
|
218
139
|
|
|
219
140
|
function storageKey(id: string) {
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function createId() {
|
|
224
|
-
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
225
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function isErrorPage() {
|
|
229
|
-
const destination = new URL(errorPage!, location.href);
|
|
230
|
-
const destinationPath = destination.pathname.replace(/\/$/, "") || "/";
|
|
231
|
-
const currentPath = location.pathname.replace(/\/$/, "") || "/";
|
|
232
|
-
return destination.origin === location.origin && destinationPath === currentPath;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function getErrorMessage(reason: unknown, fallback: string) {
|
|
236
|
-
if (reason instanceof Error && reason.message) return reason.message;
|
|
237
|
-
if (typeof reason === "string" && reason) return reason;
|
|
238
|
-
if (reason !== null && reason !== undefined) {
|
|
239
|
-
try {
|
|
240
|
-
const text = JSON.stringify(reason);
|
|
241
|
-
if (text) return text;
|
|
242
|
-
} catch {
|
|
243
|
-
// Fall through to the stable message below.
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return fallback;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function getErrorStack(reason: unknown) {
|
|
250
|
-
if (!reason || typeof reason !== "object") return "";
|
|
251
|
-
const stack = (reason as { stack?: unknown }).stack;
|
|
252
|
-
return typeof stack === "string" ? stack : "";
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function isErrorPayload(value: unknown): value is ErrorPayload {
|
|
256
|
-
if (!value || typeof value !== "object") return false;
|
|
257
|
-
const payload = value as Partial<ErrorPayload>;
|
|
258
|
-
return typeof payload.id === "string"
|
|
259
|
-
&& (payload.type === "error" || payload.type === "unhandledrejection")
|
|
260
|
-
&& typeof payload.message === "string"
|
|
261
|
-
&& typeof payload.stack === "string"
|
|
262
|
-
&& typeof payload.time === "string"
|
|
263
|
-
&& typeof payload.url === "string"
|
|
264
|
-
&& typeof payload.userAgent === "string"
|
|
265
|
-
&& typeof payload.platform === "string"
|
|
266
|
-
&& typeof payload.language === "string";
|
|
141
|
+
return `buldng:error:${id}`;
|
|
267
142
|
}
|
package/src/buldng.css
CHANGED
|
@@ -146,3 +146,133 @@ button {
|
|
|
146
146
|
background: var(--button);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/* --------------------------------------------------
|
|
150
|
+
MODAL SYSTEM
|
|
151
|
+
-------------------------------------------------- */
|
|
152
|
+
|
|
153
|
+
#b-modal-root {
|
|
154
|
+
position: fixed;
|
|
155
|
+
inset: 0;
|
|
156
|
+
z-index: 1000;
|
|
157
|
+
pointer-events: none;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#b-modal-root .buldng-modal-backdrop {
|
|
161
|
+
position: absolute;
|
|
162
|
+
inset: 0;
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: center;
|
|
166
|
+
padding: 20px;
|
|
167
|
+
background: var(--modal-backdrop-color, rgba(0, 0, 0, 0.5));
|
|
168
|
+
pointer-events: auto;
|
|
169
|
+
}
|
|
170
|
+
#b-modal-root .buldng-modal {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: column;
|
|
173
|
+
width: min(100%, 480px);
|
|
174
|
+
max-height: min(100%, 720px);
|
|
175
|
+
overflow: auto;
|
|
176
|
+
color: var(--text, #222222);
|
|
177
|
+
background: var(--modal-bg, var(--panel, #ffffff));
|
|
178
|
+
border: 2px solid var(--modal-accent-color, #0066cc);
|
|
179
|
+
border-radius: var(--modal-radius, 12px);
|
|
180
|
+
box-shadow: var(--modal-shadow, 0 4px 20px rgba(0, 0, 0, 0.3));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#b-modal-root .buldng-modal-header {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 12px;
|
|
187
|
+
padding: var(--modal-padding, 20px) var(--modal-padding, 20px) 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#b-modal-root .buldng-modal-icon {
|
|
191
|
+
display: inline-flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
flex: 0 0 32px;
|
|
195
|
+
width: 32px;
|
|
196
|
+
height: 32px;
|
|
197
|
+
color: var(--modal-accent-color, #0066cc);
|
|
198
|
+
font-weight: 700;
|
|
199
|
+
border: 2px solid currentColor;
|
|
200
|
+
border-radius: 50%;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#b-modal-root .buldng-modal-title {
|
|
204
|
+
margin: 0;
|
|
205
|
+
color: inherit;
|
|
206
|
+
font-size: 1.2em;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#b-modal-root .buldng-modal-content {
|
|
210
|
+
padding: var(--modal-padding, 20px);
|
|
211
|
+
overflow: auto;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#b-modal-root .buldng-modal-footer {
|
|
215
|
+
display: flex;
|
|
216
|
+
justify-content: flex-end;
|
|
217
|
+
gap: 10px;
|
|
218
|
+
padding: 0 var(--modal-padding, 20px) var(--modal-padding, 20px);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#b-modal-root .buldng-modal-button {
|
|
222
|
+
min-width: 88px;
|
|
223
|
+
min-height: 44px;
|
|
224
|
+
padding: 8px 16px;
|
|
225
|
+
color: var(--text, #222222);
|
|
226
|
+
font: inherit;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
background: var(--button, #f0f0f0);
|
|
229
|
+
border: 1px solid var(--line, #9c9c9c);
|
|
230
|
+
border-radius: 4px;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#b-modal-root .buldng-modal-button-primary {
|
|
235
|
+
color: #ffffff;
|
|
236
|
+
background: var(--modal-accent-color, #0066cc);
|
|
237
|
+
border-color: var(--modal-accent-color, #0066cc);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#b-modal-root .buldng-modal-button:focus-visible {
|
|
241
|
+
outline: 3px solid var(--highlight1, #4aa3ff);
|
|
242
|
+
outline-offset: 2px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#b-modal-root .buldng-modal.info {
|
|
246
|
+
--modal-accent-color: #0066cc;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#b-modal-root .buldng-modal.confirmation {
|
|
250
|
+
--modal-accent-color: #444444;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#b-modal-root .buldng-modal.destructive,
|
|
254
|
+
#b-modal-root .buldng-modal.error {
|
|
255
|
+
--modal-accent-color: #cc0000;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@media (max-width: 520px) {
|
|
259
|
+
#b-modal-root .buldng-modal-backdrop {
|
|
260
|
+
align-items: flex-end;
|
|
261
|
+
padding: 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#b-modal-root .buldng-modal {
|
|
265
|
+
width: 100%;
|
|
266
|
+
max-height: 90%;
|
|
267
|
+
border-radius: var(--modal-radius, 12px) var(--modal-radius, 12px) 0 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#b-modal-root .buldng-modal-footer {
|
|
271
|
+
flex-direction: column-reverse;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#b-modal-root .buldng-modal-button {
|
|
275
|
+
width: 100%;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Development-only override. setErrorPage() owns the event listeners.
|
|
2
|
+
console.log("[dev-errors] overriding window.__error_handler");
|
|
3
|
+
|
|
4
|
+
window.__error_handler = (event, page) => {
|
|
5
|
+
const reason =
|
|
6
|
+
event.type === "error"
|
|
7
|
+
? event.error ?? event.message
|
|
8
|
+
: event.reason;
|
|
9
|
+
|
|
10
|
+
const message =
|
|
11
|
+
reason instanceof Error
|
|
12
|
+
? reason.message
|
|
13
|
+
: String(reason || "Unknown error");
|
|
14
|
+
|
|
15
|
+
alert(`BULDR DEV ERROR:\n\n${message}\n\nCheck DevTools for full details.`);
|
|
16
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* BULDR PRODUCTION ERROR SUBSYSTEM — layout-err.ts
|
|
4
|
+
* ============================================================
|
|
5
|
+
*
|
|
6
|
+
* This module is bundled ONLY by layout.ts.
|
|
7
|
+
* It owns ALL production error logic:
|
|
8
|
+
* - capturing errors
|
|
9
|
+
* - building payloads
|
|
10
|
+
* - storing payloads
|
|
11
|
+
* - redirecting to the error page
|
|
12
|
+
*
|
|
13
|
+
* buldng-web.ts owns:
|
|
14
|
+
* - reading payloads
|
|
15
|
+
* - consuming payloads
|
|
16
|
+
* - rendering payloads
|
|
17
|
+
* - runtime utilities
|
|
18
|
+
*
|
|
19
|
+
* error-main.ts owns:
|
|
20
|
+
* - UI for the error page
|
|
21
|
+
*
|
|
22
|
+
* ============================================================
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const ERROR_STORAGE_PREFIX = "buldng:error:";
|
|
26
|
+
|
|
27
|
+
// ------------------------------------------------------------
|
|
28
|
+
// PUBLIC API
|
|
29
|
+
// ------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Install the production error handler wrapper.
|
|
33
|
+
* layout.ts calls this once during startup.
|
|
34
|
+
*/
|
|
35
|
+
export function installErrorHandler() {
|
|
36
|
+
window.__error_handler = (event, page) => {
|
|
37
|
+
handleError(event, page);
|
|
38
|
+
};
|
|
39
|
+
console.log("BULDR: Production error handler installed");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ------------------------------------------------------------
|
|
43
|
+
// CORE PRODUCTION LOGIC
|
|
44
|
+
// ------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function handleError(
|
|
47
|
+
event: ErrorEvent | PromiseRejectionEvent,
|
|
48
|
+
page: string
|
|
49
|
+
) {
|
|
50
|
+
if (window.__buldngErrorOverlay) return;
|
|
51
|
+
|
|
52
|
+
const info =
|
|
53
|
+
event instanceof ErrorEvent
|
|
54
|
+
? {
|
|
55
|
+
type: "error" as const,
|
|
56
|
+
reason: event.error ?? event.message,
|
|
57
|
+
fallbackMessage: event.message,
|
|
58
|
+
}
|
|
59
|
+
: {
|
|
60
|
+
type: "unhandledrejection" as const,
|
|
61
|
+
reason: event.reason,
|
|
62
|
+
fallbackMessage: "Unhandled promise rejection",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const payload = buildPayload(info);
|
|
66
|
+
const stored = saveErrorPayload(payload);
|
|
67
|
+
|
|
68
|
+
redirect(payload, stored, page);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ------------------------------------------------------------
|
|
72
|
+
// PAYLOAD BUILDING
|
|
73
|
+
// ------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export type ErrorPayload = {
|
|
76
|
+
id: string;
|
|
77
|
+
type: "error" | "unhandledrejection";
|
|
78
|
+
message: string;
|
|
79
|
+
stack: string;
|
|
80
|
+
time: string;
|
|
81
|
+
url: string;
|
|
82
|
+
userAgent: string;
|
|
83
|
+
platform: string;
|
|
84
|
+
language: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function buildPayload(info: {
|
|
88
|
+
type: ErrorPayload["type"];
|
|
89
|
+
reason: unknown;
|
|
90
|
+
fallbackMessage: string;
|
|
91
|
+
}): ErrorPayload {
|
|
92
|
+
const id = createId();
|
|
93
|
+
const error = info.reason instanceof Error ? info.reason : null;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
type: info.type,
|
|
98
|
+
message: getErrorMessage(info.reason, info.fallbackMessage),
|
|
99
|
+
stack: error?.stack ?? getErrorStack(info.reason),
|
|
100
|
+
time: new Date().toISOString(),
|
|
101
|
+
url: location.href,
|
|
102
|
+
userAgent: navigator.userAgent,
|
|
103
|
+
platform: navigator.platform,
|
|
104
|
+
language: navigator.language,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ------------------------------------------------------------
|
|
109
|
+
// REDIRECT
|
|
110
|
+
// ------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function redirect(payload: ErrorPayload, stored: boolean, page: string) {
|
|
113
|
+
const destination = new URL(page, location.href);
|
|
114
|
+
|
|
115
|
+
if (stored) {
|
|
116
|
+
destination.searchParams.set("errorId", payload.id);
|
|
117
|
+
} else {
|
|
118
|
+
destination.searchParams.set("type", payload.type);
|
|
119
|
+
destination.searchParams.set("message", payload.message.slice(0, 500));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
window.location.href = destination.href;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ------------------------------------------------------------
|
|
126
|
+
// STORAGE (production only)
|
|
127
|
+
// ------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function saveErrorPayload(payload: ErrorPayload): boolean {
|
|
130
|
+
try {
|
|
131
|
+
localStorage.setItem(storageKey(payload.id), JSON.stringify(payload));
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function storageKey(id: string) {
|
|
139
|
+
return `${ERROR_STORAGE_PREFIX}${id}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createId() {
|
|
143
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
144
|
+
return `${Date.now().toString(36)}-${Math.random()
|
|
145
|
+
.toString(36)
|
|
146
|
+
.slice(2)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ------------------------------------------------------------
|
|
150
|
+
// UTILITIES (production only)
|
|
151
|
+
// ------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
function getErrorMessage(reason: unknown, fallback: string) {
|
|
154
|
+
if (reason instanceof Error && reason.message) return reason.message;
|
|
155
|
+
if (typeof reason === "string" && reason) return reason;
|
|
156
|
+
|
|
157
|
+
if (reason !== null && reason !== undefined) {
|
|
158
|
+
try {
|
|
159
|
+
const text = JSON.stringify(reason);
|
|
160
|
+
if (text) return text;
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return fallback;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getErrorStack(reason: unknown) {
|
|
168
|
+
if (!reason || typeof reason !== "object") return "";
|
|
169
|
+
const stack = (reason as { stack?: unknown }).stack;
|
|
170
|
+
return typeof stack === "string" ? stack : "";
|
|
171
|
+
}
|