@ovineko/spa-guard 0.0.1-alpha-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/README.md +695 -0
- package/dist/chunk-BL4EG6R7.js +14 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-VWV6L4Q2.js +51 -0
- package/dist/chunk-W65YKSMF.js +6 -0
- package/dist/chunk-XV2YCVOR.js +12 -0
- package/dist/common/constants.d.ts +5 -0
- package/dist/common/events/index.d.ts +2 -0
- package/dist/common/events/internal.d.ts +4 -0
- package/dist/common/events/types.d.ts +8 -0
- package/dist/common/fallbackHtml.generated.d.ts +1 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +358 -0
- package/dist/common/isChunkError.d.ts +1 -0
- package/dist/common/listen/index.d.ts +1 -0
- package/dist/common/listen/internal.d.ts +1 -0
- package/dist/common/log.d.ts +1 -0
- package/dist/common/options.d.ts +12 -0
- package/dist/common/reload.d.ts +1 -0
- package/dist/common/sendBeacon.d.ts +2 -0
- package/dist/common/serializeError.d.ts +1 -0
- package/dist/fastify/index.d.ts +45 -0
- package/dist/fastify/index.js +66 -0
- package/dist/inline/index.d.ts +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +0 -0
- package/dist/react-error-boundary/index.d.ts +1 -0
- package/dist/react-error-boundary/index.js +7 -0
- package/dist/react-router/index.d.ts +1 -0
- package/dist/react-router/index.js +12 -0
- package/dist/schema/index.d.ts +8 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/parse.d.ts +6 -0
- package/dist/schema/parse.js +8 -0
- package/dist/vite-plugin/index.d.ts +6 -0
- package/dist/vite-plugin/index.js +38 -0
- package/dist-inline/index.js +1 -0
- package/dist-inline-trace/index.js +360 -0
- package/package.json +86 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__export
|
|
3
|
+
} from "./chunk-MLKGABMK.js";
|
|
4
|
+
|
|
5
|
+
// package.json
|
|
6
|
+
var name = "@ovineko/spa-guard";
|
|
7
|
+
|
|
8
|
+
// src/common/constants.ts
|
|
9
|
+
var optionsWindowKey = "__SPA_GUARD_OPTIONS__";
|
|
10
|
+
var eventSubscribersWindowKey = /* @__PURE__ */ Symbol.for(`${name}:event-subscribers`);
|
|
11
|
+
var internalConfigWindowKey = /* @__PURE__ */ Symbol.for(`${name}:internal-config`);
|
|
12
|
+
var RETRY_ID_PARAM = "spaGuardRetryId";
|
|
13
|
+
var RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
|
|
14
|
+
|
|
15
|
+
// src/common/options.ts
|
|
16
|
+
var options_exports = {};
|
|
17
|
+
__export(options_exports, {
|
|
18
|
+
getOptions: () => getOptions,
|
|
19
|
+
optionsWindowKey: () => optionsWindowKey
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// src/common/fallbackHtml.generated.ts
|
|
23
|
+
var defaultFallbackHtml = `<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif"><div style="text-align:center"><h1>Something went wrong</h1><p>Please refresh the page to continue.</p><button onclick="location.reload()">Refresh Page</button></div></div>`;
|
|
24
|
+
|
|
25
|
+
// src/common/options.ts
|
|
26
|
+
var defaultOptions = {
|
|
27
|
+
fallbackHtml: defaultFallbackHtml,
|
|
28
|
+
reloadDelays: [1e3, 2e3, 5e3],
|
|
29
|
+
useRetryId: true
|
|
30
|
+
};
|
|
31
|
+
var getOptions = () => {
|
|
32
|
+
const windowOptions = globalThis.window?.[optionsWindowKey];
|
|
33
|
+
return {
|
|
34
|
+
...defaultOptions,
|
|
35
|
+
...windowOptions,
|
|
36
|
+
reportBeacon: {
|
|
37
|
+
...defaultOptions.reportBeacon,
|
|
38
|
+
...windowOptions?.reportBeacon
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
name,
|
|
45
|
+
optionsWindowKey,
|
|
46
|
+
eventSubscribersWindowKey,
|
|
47
|
+
RETRY_ID_PARAM,
|
|
48
|
+
RETRY_ATTEMPT_PARAM,
|
|
49
|
+
getOptions,
|
|
50
|
+
options_exports
|
|
51
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/schema/index.ts
|
|
2
|
+
import Type from "typebox";
|
|
3
|
+
var beaconSchema = Type.Object({
|
|
4
|
+
errorMessage: Type.Optional(Type.String()),
|
|
5
|
+
eventMessage: Type.Optional(Type.String()),
|
|
6
|
+
eventName: Type.Optional(Type.String()),
|
|
7
|
+
serialized: Type.Optional(Type.String())
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
beaconSchema
|
|
12
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const optionsWindowKey = "__SPA_GUARD_OPTIONS__";
|
|
2
|
+
export declare const eventSubscribersWindowKey: unique symbol;
|
|
3
|
+
export declare const internalConfigWindowKey: unique symbol;
|
|
4
|
+
export declare const RETRY_ID_PARAM = "spaGuardRetryId";
|
|
5
|
+
export declare const RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const defaultFallbackHtml = "<div style=\"display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif\"><div style=\"text-align:center\"><h1>Something went wrong</h1><p>Please refresh the page to continue.</p><button onclick=\"location.reload()\">Refresh Page</button></div></div>";
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logMessage
|
|
3
|
+
} from "../chunk-W65YKSMF.js";
|
|
4
|
+
import {
|
|
5
|
+
RETRY_ATTEMPT_PARAM,
|
|
6
|
+
RETRY_ID_PARAM,
|
|
7
|
+
eventSubscribersWindowKey,
|
|
8
|
+
getOptions,
|
|
9
|
+
options_exports
|
|
10
|
+
} from "../chunk-VWV6L4Q2.js";
|
|
11
|
+
import {
|
|
12
|
+
__export
|
|
13
|
+
} from "../chunk-MLKGABMK.js";
|
|
14
|
+
|
|
15
|
+
// src/common/events/index.ts
|
|
16
|
+
var events_exports = {};
|
|
17
|
+
__export(events_exports, {
|
|
18
|
+
subscribe: () => subscribe
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/common/events/internal.ts
|
|
22
|
+
if (globalThis.window && !globalThis.window[eventSubscribersWindowKey]) {
|
|
23
|
+
globalThis.window[eventSubscribersWindowKey] = /* @__PURE__ */ new Set();
|
|
24
|
+
}
|
|
25
|
+
var subscribers = globalThis.window?.[eventSubscribersWindowKey] ?? /* @__PURE__ */ new Set();
|
|
26
|
+
var emitEvent = (event) => {
|
|
27
|
+
subscribers.forEach((cb) => cb(event));
|
|
28
|
+
};
|
|
29
|
+
var subscribe = (cb) => {
|
|
30
|
+
subscribers.add(cb);
|
|
31
|
+
return () => subscribers.delete(cb);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/common/isChunkError.ts
|
|
35
|
+
var isChunkError = (error) => {
|
|
36
|
+
const message = getErrorMessage(error);
|
|
37
|
+
if (!message) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const patterns = [
|
|
41
|
+
/Failed to fetch dynamically imported module/i,
|
|
42
|
+
/Importing a module script failed/i,
|
|
43
|
+
/error loading dynamically imported module/i,
|
|
44
|
+
/Unable to preload CSS/i,
|
|
45
|
+
/Loading chunk \d+ failed/i,
|
|
46
|
+
/Loading CSS chunk \d+ failed/i,
|
|
47
|
+
/ChunkLoadError/i,
|
|
48
|
+
/Failed to fetch/i,
|
|
49
|
+
/NaN/i
|
|
50
|
+
];
|
|
51
|
+
return patterns.some((pattern) => pattern.test(message));
|
|
52
|
+
};
|
|
53
|
+
var getErrorMessage = (error) => {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return error.message;
|
|
56
|
+
}
|
|
57
|
+
if (typeof error === "string") {
|
|
58
|
+
return error;
|
|
59
|
+
}
|
|
60
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
61
|
+
return String(error.message);
|
|
62
|
+
}
|
|
63
|
+
if (error && typeof error === "object" && "reason" in error) {
|
|
64
|
+
return getErrorMessage(error.reason);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/common/sendBeacon.ts
|
|
70
|
+
var sendBeacon = (beacon) => {
|
|
71
|
+
const options = getOptions();
|
|
72
|
+
if (!options.reportBeacon?.endpoint) {
|
|
73
|
+
console.warn(logMessage("Report endpoint is not configured"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const body = JSON.stringify(beacon);
|
|
77
|
+
const isSendBeaconAvailable = typeof globalThis.window?.navigator.sendBeacon === "function";
|
|
78
|
+
const isSentBeacon = isSendBeaconAvailable && navigator.sendBeacon(options.reportBeacon.endpoint, body);
|
|
79
|
+
if (!isSentBeacon) {
|
|
80
|
+
fetch(options.reportBeacon.endpoint, { body, keepalive: true, method: "POST" }).catch(
|
|
81
|
+
(error) => {
|
|
82
|
+
console.error(logMessage("Failed to send beacon:"), error);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/common/reload.ts
|
|
89
|
+
var getRetryStateFromUrl = () => {
|
|
90
|
+
try {
|
|
91
|
+
const params = new URLSearchParams(window.location.search);
|
|
92
|
+
const retryId = params.get(RETRY_ID_PARAM);
|
|
93
|
+
const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
|
|
94
|
+
if (retryId && retryAttempt) {
|
|
95
|
+
return {
|
|
96
|
+
retryAttempt: parseInt(retryAttempt, 10),
|
|
97
|
+
retryId
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var generateRetryId = () => {
|
|
106
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
107
|
+
return crypto.randomUUID();
|
|
108
|
+
}
|
|
109
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
110
|
+
const array = new Uint32Array(2);
|
|
111
|
+
crypto.getRandomValues(array);
|
|
112
|
+
return `${Date.now()}-${array[0].toString(36)}${array[1].toString(36)}`;
|
|
113
|
+
}
|
|
114
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 15)}`;
|
|
115
|
+
};
|
|
116
|
+
var buildReloadUrl = (retryId, retryAttempt) => {
|
|
117
|
+
const url = new URL(window.location.href);
|
|
118
|
+
url.searchParams.set(RETRY_ID_PARAM, retryId);
|
|
119
|
+
url.searchParams.set(RETRY_ATTEMPT_PARAM, String(retryAttempt));
|
|
120
|
+
return url.toString();
|
|
121
|
+
};
|
|
122
|
+
var attemptReload = (error) => {
|
|
123
|
+
const options = getOptions();
|
|
124
|
+
const reloadDelays = options.reloadDelays ?? [1e3, 2e3, 5e3];
|
|
125
|
+
const useRetryId = options.useRetryId ?? true;
|
|
126
|
+
const retryState = useRetryId ? getRetryStateFromUrl() : null;
|
|
127
|
+
const currentAttempt = retryState ? retryState.retryAttempt : 0;
|
|
128
|
+
if (currentAttempt >= reloadDelays.length) {
|
|
129
|
+
console.error(logMessage("All reload attempts exhausted"), error);
|
|
130
|
+
sendBeacon({
|
|
131
|
+
errorMessage: "Exceeded maximum reload attempts",
|
|
132
|
+
eventName: "chunk_error_max_reloads",
|
|
133
|
+
serialized: JSON.stringify({
|
|
134
|
+
error: String(error),
|
|
135
|
+
retryAttempt: currentAttempt,
|
|
136
|
+
retryId: retryState?.retryId
|
|
137
|
+
})
|
|
138
|
+
});
|
|
139
|
+
showFallbackUI();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const nextAttempt = currentAttempt + 1;
|
|
143
|
+
const retryId = retryState?.retryId ?? generateRetryId();
|
|
144
|
+
const delay = reloadDelays[currentAttempt];
|
|
145
|
+
console.warn(
|
|
146
|
+
logMessage(
|
|
147
|
+
`Reload attempt ${nextAttempt}/${reloadDelays.length} in ${delay}ms (retryId: ${retryId})`
|
|
148
|
+
),
|
|
149
|
+
error
|
|
150
|
+
);
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (useRetryId) {
|
|
153
|
+
const reloadUrl = buildReloadUrl(retryId, nextAttempt);
|
|
154
|
+
window.location.href = reloadUrl;
|
|
155
|
+
} else {
|
|
156
|
+
window.location.reload();
|
|
157
|
+
}
|
|
158
|
+
}, delay);
|
|
159
|
+
};
|
|
160
|
+
var showFallbackUI = () => {
|
|
161
|
+
const options = getOptions();
|
|
162
|
+
const fallbackHtml = options.fallbackHtml;
|
|
163
|
+
if (!fallbackHtml) {
|
|
164
|
+
console.error(logMessage("No fallback UI configured"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
document.body.innerHTML = fallbackHtml;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(logMessage("Failed to inject fallback UI"), error);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/common/serializeError.ts
|
|
175
|
+
var serializeError = (error) => {
|
|
176
|
+
try {
|
|
177
|
+
const serialized = serializeErrorInternal(error);
|
|
178
|
+
return JSON.stringify(serialized, null, 2);
|
|
179
|
+
} catch {
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
error: "Failed to serialize error",
|
|
182
|
+
fallback: String(error)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var serializeErrorInternal = (error) => {
|
|
187
|
+
if (error === null || error === void 0) {
|
|
188
|
+
return { type: "null", value: error };
|
|
189
|
+
}
|
|
190
|
+
if (typeof error !== "object") {
|
|
191
|
+
return { type: typeof error, value: error };
|
|
192
|
+
}
|
|
193
|
+
if (error instanceof Error) {
|
|
194
|
+
return {
|
|
195
|
+
message: error.message,
|
|
196
|
+
name: error.name,
|
|
197
|
+
stack: error.stack,
|
|
198
|
+
type: "Error",
|
|
199
|
+
...extractErrorProperties(error)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if ("reason" in error && "promise" in error) {
|
|
203
|
+
return {
|
|
204
|
+
reason: serializeErrorInternal(error.reason),
|
|
205
|
+
type: "PromiseRejectionEvent"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if ("error" in error && "message" in error && "filename" in error) {
|
|
209
|
+
return {
|
|
210
|
+
colno: error.colno,
|
|
211
|
+
error: serializeErrorInternal(error.error),
|
|
212
|
+
filename: error.filename,
|
|
213
|
+
lineno: error.lineno,
|
|
214
|
+
message: error.message,
|
|
215
|
+
type: "ErrorEvent"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if ("violatedDirective" in error && "blockedURI" in error) {
|
|
219
|
+
const evt = error;
|
|
220
|
+
return {
|
|
221
|
+
blockedURI: evt.blockedURI,
|
|
222
|
+
columnNumber: evt.columnNumber,
|
|
223
|
+
effectiveDirective: evt.effectiveDirective,
|
|
224
|
+
lineNumber: evt.lineNumber,
|
|
225
|
+
originalPolicy: evt.originalPolicy,
|
|
226
|
+
sourceFile: evt.sourceFile,
|
|
227
|
+
type: "SecurityPolicyViolationEvent",
|
|
228
|
+
violatedDirective: evt.violatedDirective
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if ("type" in error && "target" in error) {
|
|
232
|
+
const evt = error;
|
|
233
|
+
return {
|
|
234
|
+
eventType: evt.type,
|
|
235
|
+
target: extractEventTarget(evt.target),
|
|
236
|
+
timeStamp: evt.timeStamp,
|
|
237
|
+
type: "Event"
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
type: "object",
|
|
242
|
+
value: extractOwnProperties(error)
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
var extractErrorProperties = (error) => {
|
|
246
|
+
const props = {};
|
|
247
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
248
|
+
if (!["message", "name", "stack"].includes(key)) {
|
|
249
|
+
try {
|
|
250
|
+
props[key] = error[key];
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return props;
|
|
256
|
+
};
|
|
257
|
+
var extractEventTarget = (target) => {
|
|
258
|
+
if (!target) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (target instanceof HTMLElement) {
|
|
262
|
+
return {
|
|
263
|
+
className: target.className,
|
|
264
|
+
href: target.href,
|
|
265
|
+
id: target.id,
|
|
266
|
+
src: target.src,
|
|
267
|
+
tagName: target.tagName
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { type: String(target) };
|
|
271
|
+
};
|
|
272
|
+
var extractOwnProperties = (obj) => {
|
|
273
|
+
const props = {};
|
|
274
|
+
for (const key of Object.keys(obj)) {
|
|
275
|
+
try {
|
|
276
|
+
const value = obj[key];
|
|
277
|
+
props[key] = typeof value === "object" ? String(value) : value;
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return props;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/common/listen/internal.ts
|
|
285
|
+
var listenInternal = () => {
|
|
286
|
+
emitEvent({ name: "test" });
|
|
287
|
+
const wa = window.addEventListener;
|
|
288
|
+
wa(
|
|
289
|
+
"error",
|
|
290
|
+
(event) => {
|
|
291
|
+
console.error(logMessage("error:capture:"), event);
|
|
292
|
+
if (isChunkError(event)) {
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
attemptReload(event);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const serialized = serializeError(event);
|
|
298
|
+
sendBeacon({
|
|
299
|
+
errorMessage: event.message,
|
|
300
|
+
eventName: "error",
|
|
301
|
+
serialized
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
true
|
|
305
|
+
);
|
|
306
|
+
wa("error", (event) => {
|
|
307
|
+
console.error(logMessage("error:"), event);
|
|
308
|
+
});
|
|
309
|
+
wa("unhandledrejection", (event) => {
|
|
310
|
+
console.error(logMessage("unhandledrejection:"), event);
|
|
311
|
+
if (isChunkError(event.reason)) {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
attemptReload(event.reason);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const serialized = serializeError(event);
|
|
317
|
+
sendBeacon({
|
|
318
|
+
errorMessage: String(event.reason),
|
|
319
|
+
eventName: "unhandledrejection",
|
|
320
|
+
serialized
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
wa("uncaughtException", (event) => {
|
|
324
|
+
console.error(logMessage("uncaughtException:"), event);
|
|
325
|
+
const serialized = serializeError(event);
|
|
326
|
+
sendBeacon({
|
|
327
|
+
eventName: "uncaughtException",
|
|
328
|
+
serialized
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
wa("securitypolicyviolation", (event) => {
|
|
332
|
+
console.error(logMessage("CSP violation:"), event.blockedURI, event.violatedDirective);
|
|
333
|
+
const serialized = serializeError(event);
|
|
334
|
+
sendBeacon({
|
|
335
|
+
eventMessage: `${event.violatedDirective}: ${event.blockedURI}`,
|
|
336
|
+
eventName: "securitypolicyviolation",
|
|
337
|
+
serialized
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
wa("vite:preloadError", (event) => {
|
|
341
|
+
console.error(logMessage("vite:preloadError:"), event);
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
attemptReload(event);
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/common/listen/index.ts
|
|
348
|
+
var listen = () => {
|
|
349
|
+
if (!globalThis.window) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
listenInternal();
|
|
353
|
+
};
|
|
354
|
+
export {
|
|
355
|
+
events_exports as events,
|
|
356
|
+
listen,
|
|
357
|
+
options_exports as options
|
|
358
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isChunkError: (error: unknown) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const listen: () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const listenInternal: () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const logMessage: (msg: string) => string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { optionsWindowKey } from "./constants";
|
|
2
|
+
export interface Options {
|
|
3
|
+
fallbackHtml?: string;
|
|
4
|
+
/** @default [1000, 2000, 5000] */
|
|
5
|
+
reloadDelays?: number[];
|
|
6
|
+
reportBeacon?: {
|
|
7
|
+
endpoint?: string;
|
|
8
|
+
};
|
|
9
|
+
/** @default true */
|
|
10
|
+
useRetryId?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const getOptions: () => Options;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const attemptReload: (error: unknown) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const serializeError: (error: unknown) => string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BeaconSchema } from "../schema";
|
|
2
|
+
export interface FastifySPAGuardOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Custom handler for beacon data
|
|
5
|
+
* @param beacon - Parsed beacon data
|
|
6
|
+
* @param request - Fastify request object
|
|
7
|
+
*/
|
|
8
|
+
onBeacon?: (beacon: BeaconSchema, request: any) => Promise<void> | void;
|
|
9
|
+
/**
|
|
10
|
+
* Custom handler for invalid/unknown beacon data
|
|
11
|
+
* @param body - Raw body data
|
|
12
|
+
* @param request - Fastify request object
|
|
13
|
+
*/
|
|
14
|
+
onUnknownBeacon?: (body: unknown, request: any) => Promise<void> | void;
|
|
15
|
+
/**
|
|
16
|
+
* The route path for the beacon endpoint
|
|
17
|
+
* @example "/api/beacon"
|
|
18
|
+
*/
|
|
19
|
+
path: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* SPA Guard plugin for Fastify
|
|
23
|
+
* Registers a POST endpoint to receive beacon data from the client
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { fastifySPAGuard } from '@ovineko/spa-guard/fastify';
|
|
28
|
+
*
|
|
29
|
+
* app.register(fastifySPAGuard, {
|
|
30
|
+
* path: '/api/beacon',
|
|
31
|
+
* onBeacon: async (beacon, request) => {
|
|
32
|
+
* // Handle beacon data (e.g., log to Sentry)
|
|
33
|
+
* const error = new Error(beacon.errorMessage || 'Unknown error');
|
|
34
|
+
* Sentry.captureException(error, {
|
|
35
|
+
* extra: {
|
|
36
|
+
* eventName: beacon.eventName,
|
|
37
|
+
* eventMessage: beacon.eventMessage,
|
|
38
|
+
* serialized: beacon.serialized,
|
|
39
|
+
* },
|
|
40
|
+
* });
|
|
41
|
+
* },
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare const fastifySPAGuard: (fastify: any, options: FastifySPAGuardOptions) => Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logMessage
|
|
3
|
+
} from "../chunk-W65YKSMF.js";
|
|
4
|
+
import {
|
|
5
|
+
parseBeacon
|
|
6
|
+
} from "../chunk-BL4EG6R7.js";
|
|
7
|
+
import "../chunk-XV2YCVOR.js";
|
|
8
|
+
import "../chunk-MLKGABMK.js";
|
|
9
|
+
|
|
10
|
+
// src/fastify/index.ts
|
|
11
|
+
var parseStringBody = (body) => {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(body);
|
|
14
|
+
} catch {
|
|
15
|
+
return body;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var handleBeaconRequest = async (body, request, options) => {
|
|
19
|
+
try {
|
|
20
|
+
const beacon = parseBeacon(body);
|
|
21
|
+
if (options.onBeacon) {
|
|
22
|
+
await options.onBeacon(beacon, request);
|
|
23
|
+
} else {
|
|
24
|
+
request.log.info(
|
|
25
|
+
{
|
|
26
|
+
errorMessage: beacon.errorMessage,
|
|
27
|
+
eventMessage: beacon.eventMessage,
|
|
28
|
+
eventName: beacon.eventName,
|
|
29
|
+
serialized: beacon.serialized
|
|
30
|
+
},
|
|
31
|
+
logMessage("Beacon received")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
if (options.onUnknownBeacon) {
|
|
36
|
+
await options.onUnknownBeacon(body, request);
|
|
37
|
+
} else {
|
|
38
|
+
request.log.warn({ body }, logMessage("Unknown beacon format"));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var fastifySPAGuard = async (fastify, options) => {
|
|
43
|
+
const { onBeacon, onUnknownBeacon, path } = options;
|
|
44
|
+
fastify.post(
|
|
45
|
+
path,
|
|
46
|
+
{
|
|
47
|
+
schema: {
|
|
48
|
+
body: {
|
|
49
|
+
content: {
|
|
50
|
+
"text/plain": {
|
|
51
|
+
schema: { type: "string" }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async (request, reply) => {
|
|
58
|
+
const body = parseStringBody(request.body);
|
|
59
|
+
await handleBeaconRequest(body, request, { onBeacon, onUnknownBeacon });
|
|
60
|
+
return reply.status(200).send({ success: true });
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
export {
|
|
65
|
+
fastifySPAGuard
|
|
66
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ErrorBoundary } from "react-error-boundary";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ErrorBoundaryReactRouter: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "../chunk-MLKGABMK.js";
|
|
2
|
+
|
|
3
|
+
// src/react-router/index.tsx
|
|
4
|
+
import { useRouteError } from "react-router";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
var ErrorBoundaryReactRouter = () => {
|
|
7
|
+
const error = useRouteError();
|
|
8
|
+
return /* @__PURE__ */ jsx("div", { children: JSON.stringify(error) });
|
|
9
|
+
};
|
|
10
|
+
export {
|
|
11
|
+
ErrorBoundaryReactRouter
|
|
12
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import Type from "typebox";
|
|
2
|
+
export declare const beaconSchema: Type.TObject<{
|
|
3
|
+
errorMessage: Type.TOptional<Type.TString>;
|
|
4
|
+
eventMessage: Type.TOptional<Type.TString>;
|
|
5
|
+
eventName: Type.TOptional<Type.TString>;
|
|
6
|
+
serialized: Type.TOptional<Type.TString>;
|
|
7
|
+
}>;
|
|
8
|
+
export type BeaconSchema = Type.Static<typeof beaconSchema>;
|