@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,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
name,
|
|
3
|
+
optionsWindowKey
|
|
4
|
+
} from "../chunk-VWV6L4Q2.js";
|
|
5
|
+
import "../chunk-MLKGABMK.js";
|
|
6
|
+
|
|
7
|
+
// src/vite-plugin/index.ts
|
|
8
|
+
import fsPromise from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
10
|
+
var getInlineScript = async (options) => {
|
|
11
|
+
const buildDir = options.trace ? "dist-inline-trace" : "dist-inline";
|
|
12
|
+
const script = await fsPromise.readFile(path.join(import.meta.dirname, `../${buildDir}/index.js`), "utf8").then((r) => r.trim());
|
|
13
|
+
return `window.${optionsWindowKey}=${JSON.stringify(options)};${script}`;
|
|
14
|
+
};
|
|
15
|
+
var spaGuardVitePlugin = (options = {}) => {
|
|
16
|
+
return {
|
|
17
|
+
name: `${name}/vite-plugin`,
|
|
18
|
+
transformIndexHtml: {
|
|
19
|
+
handler: async (html) => {
|
|
20
|
+
const inlineScript = await getInlineScript(options);
|
|
21
|
+
return {
|
|
22
|
+
html,
|
|
23
|
+
tags: [
|
|
24
|
+
{
|
|
25
|
+
children: inlineScript,
|
|
26
|
+
injectTo: "head-prepend",
|
|
27
|
+
tag: "script"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
order: "post"
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
spaGuardVitePlugin
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e="@ovineko/spa-guard",r=Symbol.for(e+":event-subscribers"),t=(Symbol.for(e+":internal-config"),"spaGuardRetryId"),o="spaGuardRetryAttempt";globalThis.window&&!globalThis.window[r]&&(globalThis.window[r]=new Set);var n=globalThis.window?.[r]??new Set,i=e=>{let r=a(e);return!!r&&[/Failed to fetch dynamically imported module/i,/Importing a module script failed/i,/error loading dynamically imported module/i,/Unable to preload CSS/i,/Loading chunk \d+ failed/i,/Loading CSS chunk \d+ failed/i,/ChunkLoadError/i,/Failed to fetch/i,/NaN/i].some(e=>e.test(r))},a=e=>e instanceof Error?e.message:"string"==typeof e?e:e&&"object"==typeof e&&"message"in e?e.message+"":e&&"object"==typeof e&&"reason"in e?a(e.reason):null,l=e=>"[spa-guard] "+e,s={fallbackHtml:'<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>',reloadDelays:[1e3,2e3,5e3],useRetryId:!0},c=()=>{let e=globalThis.window?.__SPA_GUARD_OPTIONS__;return{...s,...e,reportBeacon:{...s.reportBeacon,...e?.reportBeacon}}},d=e=>{let r=c();if(!r.reportBeacon?.endpoint)return void console.warn(l("Report endpoint is not configured"));let t=JSON.stringify(e);"function"==typeof globalThis.window?.navigator.sendBeacon&&navigator.sendBeacon(r.reportBeacon.endpoint,t)||fetch(r.reportBeacon.endpoint,{body:t,keepalive:!0,method:"POST"}).catch(e=>{console.error(l("Failed to send beacon:"),e)})},u=e=>{let r=c(),n=r.reloadDelays??[1e3,2e3,5e3],i=r.useRetryId??!0,a=i?(()=>{try{let e=new URLSearchParams(window.location.search),r=e.get(t),n=e.get(o);return r&&n?{retryAttempt:parseInt(n,10),retryId:r}:null}catch{return null}})():null,s=a?a.retryAttempt:0;if(s>=n.length)return console.error(l("All reload attempts exhausted"),e),d({errorMessage:"Exceeded maximum reload attempts",eventName:"chunk_error_max_reloads",serialized:JSON.stringify({error:e+"",retryAttempt:s,retryId:a?.retryId})}),void m();let u=s+1,f=a?.retryId??(()=>{if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();if(typeof crypto<"u"&&crypto.getRandomValues){let e=new Uint32Array(2);return crypto.getRandomValues(e),`${Date.now()}-${e[0].toString(36)}${e[1].toString(36)}`}return`${Date.now()}-${Math.random().toString(36).slice(2,15)}`})(),p=n[s];console.warn(l(`Reload attempt ${u}/${n.length} in ${p}ms (retryId: ${f})`),e),setTimeout(()=>{if(i){let e=((e,r)=>{let n=new URL(window.location.href);return n.searchParams.set(t,e),n.searchParams.set(o,r+""),n.toString()})(f,u);window.location.href=e}else window.location.reload()},p)},m=()=>{let e=c().fallbackHtml;if(e)try{document.body.innerHTML=e}catch(e){console.error(l("Failed to inject fallback UI"),e)}else console.error(l("No fallback UI configured"))},f=e=>{try{let r=p(e);return JSON.stringify(r,null,2)}catch{return JSON.stringify({error:"Failed to serialize error",fallback:e+""})}},p=e=>{if(null==e)return{type:"null",value:e};if("object"!=typeof e)return{type:typeof e,value:e};if(e instanceof Error)return{message:e.message,name:e.name,stack:e.stack,type:"Error",...y(e)};if("reason"in e&&"promise"in e)return{reason:p(e.reason),type:"PromiseRejectionEvent"};if("error"in e&&"message"in e&&"filename"in e)return{colno:e.colno,error:p(e.error),filename:e.filename,lineno:e.lineno,message:e.message,type:"ErrorEvent"};if("violatedDirective"in e&&"blockedURI"in e){let r=e;return{blockedURI:r.blockedURI,columnNumber:r.columnNumber,effectiveDirective:r.effectiveDirective,lineNumber:r.lineNumber,originalPolicy:r.originalPolicy,sourceFile:r.sourceFile,type:"SecurityPolicyViolationEvent",violatedDirective:r.violatedDirective}}if("type"in e&&"target"in e){let r=e;return{eventType:r.type,target:g(r.target),timeStamp:r.timeStamp,type:"Event"}}return{type:"object",value:v(e)}},y=e=>{let r={};for(let t of Object.getOwnPropertyNames(e))if(!["message","name","stack"].includes(t))try{r[t]=e[t]}catch{}return r},g=e=>e?e instanceof HTMLElement?{className:e.className,href:e.href,id:e.id,src:e.src,tagName:e.tagName}:{type:e+""}:null,v=e=>{let r={};for(let t of Object.keys(e))try{let o=e[t];r[t]="object"==typeof o?o+"":o}catch{}return r};(()=>{(e=>{n.forEach(r=>r(e))})({name:"test"});let e=window.addEventListener;e("error",e=>{if(console.error(l("error:capture:"),e),i(e))return e.preventDefault(),void u(e);let r=f(e);d({errorMessage:e.message,eventName:"error",serialized:r})},!0),e("error",e=>{console.error(l("error:"),e)}),e("unhandledrejection",e=>{if(console.error(l("unhandledrejection:"),e),i(e.reason))return e.preventDefault(),void u(e.reason);let r=f(e);d({errorMessage:e.reason+"",eventName:"unhandledrejection",serialized:r})}),e("uncaughtException",e=>{console.error(l("uncaughtException:"),e);let r=f(e);d({eventName:"uncaughtException",serialized:r})}),e("securitypolicyviolation",e=>{console.error(l("CSP violation:"),e.blockedURI,e.violatedDirective);let r=f(e);d({eventMessage:`${e.violatedDirective}: ${e.blockedURI}`,eventName:"securitypolicyviolation",serialized:r})}),e("vite:preloadError",e=>{console.error(l("vite:preloadError:"),e),e.preventDefault(),u(e)})})();
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var name = "@ovineko/spa-guard";
|
|
3
|
+
|
|
4
|
+
// src/common/constants.ts
|
|
5
|
+
var optionsWindowKey = "__SPA_GUARD_OPTIONS__";
|
|
6
|
+
var eventSubscribersWindowKey = /* @__PURE__ */ Symbol.for(`${name}:event-subscribers`);
|
|
7
|
+
var internalConfigWindowKey = /* @__PURE__ */ Symbol.for(`${name}:internal-config`);
|
|
8
|
+
var RETRY_ID_PARAM = "spaGuardRetryId";
|
|
9
|
+
var RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
|
|
10
|
+
|
|
11
|
+
// src/common/events/internal.ts
|
|
12
|
+
if (globalThis.window && !globalThis.window[eventSubscribersWindowKey]) {
|
|
13
|
+
globalThis.window[eventSubscribersWindowKey] = /* @__PURE__ */ new Set();
|
|
14
|
+
}
|
|
15
|
+
var subscribers = globalThis.window?.[eventSubscribersWindowKey] ?? /* @__PURE__ */ new Set();
|
|
16
|
+
var emitEvent = (event) => {
|
|
17
|
+
subscribers.forEach((cb) => cb(event));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/common/isChunkError.ts
|
|
21
|
+
var isChunkError = (error) => {
|
|
22
|
+
const message = getErrorMessage(error);
|
|
23
|
+
if (!message) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const patterns = [
|
|
27
|
+
/Failed to fetch dynamically imported module/i,
|
|
28
|
+
/Importing a module script failed/i,
|
|
29
|
+
/error loading dynamically imported module/i,
|
|
30
|
+
/Unable to preload CSS/i,
|
|
31
|
+
/Loading chunk \d+ failed/i,
|
|
32
|
+
/Loading CSS chunk \d+ failed/i,
|
|
33
|
+
/ChunkLoadError/i,
|
|
34
|
+
/Failed to fetch/i,
|
|
35
|
+
/NaN/i
|
|
36
|
+
];
|
|
37
|
+
return patterns.some((pattern) => pattern.test(message));
|
|
38
|
+
};
|
|
39
|
+
var getErrorMessage = (error) => {
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
return error.message;
|
|
42
|
+
}
|
|
43
|
+
if (typeof error === "string") {
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
47
|
+
return String(error.message);
|
|
48
|
+
}
|
|
49
|
+
if (error && typeof error === "object" && "reason" in error) {
|
|
50
|
+
return getErrorMessage(error.reason);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/common/log.ts
|
|
56
|
+
var logMessage = (msg) => `[spa-guard] ${msg}`;
|
|
57
|
+
|
|
58
|
+
// src/common/fallbackHtml.generated.ts
|
|
59
|
+
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>`;
|
|
60
|
+
|
|
61
|
+
// src/common/options.ts
|
|
62
|
+
var defaultOptions = {
|
|
63
|
+
fallbackHtml: defaultFallbackHtml,
|
|
64
|
+
reloadDelays: [1e3, 2e3, 5e3],
|
|
65
|
+
useRetryId: true
|
|
66
|
+
};
|
|
67
|
+
var getOptions = () => {
|
|
68
|
+
const windowOptions = globalThis.window?.[optionsWindowKey];
|
|
69
|
+
return {
|
|
70
|
+
...defaultOptions,
|
|
71
|
+
...windowOptions,
|
|
72
|
+
reportBeacon: {
|
|
73
|
+
...defaultOptions.reportBeacon,
|
|
74
|
+
...windowOptions?.reportBeacon
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/common/sendBeacon.ts
|
|
80
|
+
var sendBeacon = (beacon) => {
|
|
81
|
+
const options = getOptions();
|
|
82
|
+
if (!options.reportBeacon?.endpoint) {
|
|
83
|
+
console.warn(logMessage("Report endpoint is not configured"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const body = JSON.stringify(beacon);
|
|
87
|
+
const isSendBeaconAvailable = typeof globalThis.window?.navigator.sendBeacon === "function";
|
|
88
|
+
const isSentBeacon = isSendBeaconAvailable && navigator.sendBeacon(options.reportBeacon.endpoint, body);
|
|
89
|
+
if (!isSentBeacon) {
|
|
90
|
+
fetch(options.reportBeacon.endpoint, { body, keepalive: true, method: "POST" }).catch(
|
|
91
|
+
(error) => {
|
|
92
|
+
console.error(logMessage("Failed to send beacon:"), error);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/common/reload.ts
|
|
99
|
+
var getRetryStateFromUrl = () => {
|
|
100
|
+
try {
|
|
101
|
+
const params = new URLSearchParams(window.location.search);
|
|
102
|
+
const retryId = params.get(RETRY_ID_PARAM);
|
|
103
|
+
const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
|
|
104
|
+
if (retryId && retryAttempt) {
|
|
105
|
+
return {
|
|
106
|
+
retryAttempt: parseInt(retryAttempt, 10),
|
|
107
|
+
retryId
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var generateRetryId = () => {
|
|
116
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
117
|
+
return crypto.randomUUID();
|
|
118
|
+
}
|
|
119
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
120
|
+
const array = new Uint32Array(2);
|
|
121
|
+
crypto.getRandomValues(array);
|
|
122
|
+
return `${Date.now()}-${array[0].toString(36)}${array[1].toString(36)}`;
|
|
123
|
+
}
|
|
124
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 15)}`;
|
|
125
|
+
};
|
|
126
|
+
var buildReloadUrl = (retryId, retryAttempt) => {
|
|
127
|
+
const url = new URL(window.location.href);
|
|
128
|
+
url.searchParams.set(RETRY_ID_PARAM, retryId);
|
|
129
|
+
url.searchParams.set(RETRY_ATTEMPT_PARAM, String(retryAttempt));
|
|
130
|
+
return url.toString();
|
|
131
|
+
};
|
|
132
|
+
var attemptReload = (error) => {
|
|
133
|
+
const options = getOptions();
|
|
134
|
+
const reloadDelays = options.reloadDelays ?? [1e3, 2e3, 5e3];
|
|
135
|
+
const useRetryId = options.useRetryId ?? true;
|
|
136
|
+
const retryState = useRetryId ? getRetryStateFromUrl() : null;
|
|
137
|
+
const currentAttempt = retryState ? retryState.retryAttempt : 0;
|
|
138
|
+
if (currentAttempt >= reloadDelays.length) {
|
|
139
|
+
console.error(logMessage("All reload attempts exhausted"), error);
|
|
140
|
+
sendBeacon({
|
|
141
|
+
errorMessage: "Exceeded maximum reload attempts",
|
|
142
|
+
eventName: "chunk_error_max_reloads",
|
|
143
|
+
serialized: JSON.stringify({
|
|
144
|
+
error: String(error),
|
|
145
|
+
retryAttempt: currentAttempt,
|
|
146
|
+
retryId: retryState?.retryId
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
showFallbackUI();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const nextAttempt = currentAttempt + 1;
|
|
153
|
+
const retryId = retryState?.retryId ?? generateRetryId();
|
|
154
|
+
const delay = reloadDelays[currentAttempt];
|
|
155
|
+
console.warn(
|
|
156
|
+
logMessage(
|
|
157
|
+
`Reload attempt ${nextAttempt}/${reloadDelays.length} in ${delay}ms (retryId: ${retryId})`
|
|
158
|
+
),
|
|
159
|
+
error
|
|
160
|
+
);
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
if (useRetryId) {
|
|
163
|
+
const reloadUrl = buildReloadUrl(retryId, nextAttempt);
|
|
164
|
+
window.location.href = reloadUrl;
|
|
165
|
+
} else {
|
|
166
|
+
window.location.reload();
|
|
167
|
+
}
|
|
168
|
+
}, delay);
|
|
169
|
+
};
|
|
170
|
+
var showFallbackUI = () => {
|
|
171
|
+
const options = getOptions();
|
|
172
|
+
const fallbackHtml = options.fallbackHtml;
|
|
173
|
+
if (!fallbackHtml) {
|
|
174
|
+
console.error(logMessage("No fallback UI configured"));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
document.body.innerHTML = fallbackHtml;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(logMessage("Failed to inject fallback UI"), error);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/common/serializeError.ts
|
|
185
|
+
var serializeError = (error) => {
|
|
186
|
+
try {
|
|
187
|
+
const serialized = serializeErrorInternal(error);
|
|
188
|
+
return JSON.stringify(serialized, null, 2);
|
|
189
|
+
} catch {
|
|
190
|
+
return JSON.stringify({
|
|
191
|
+
error: "Failed to serialize error",
|
|
192
|
+
fallback: String(error)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var serializeErrorInternal = (error) => {
|
|
197
|
+
if (error === null || error === void 0) {
|
|
198
|
+
return { type: "null", value: error };
|
|
199
|
+
}
|
|
200
|
+
if (typeof error !== "object") {
|
|
201
|
+
return { type: typeof error, value: error };
|
|
202
|
+
}
|
|
203
|
+
if (error instanceof Error) {
|
|
204
|
+
return {
|
|
205
|
+
message: error.message,
|
|
206
|
+
name: error.name,
|
|
207
|
+
stack: error.stack,
|
|
208
|
+
type: "Error",
|
|
209
|
+
...extractErrorProperties(error)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if ("reason" in error && "promise" in error) {
|
|
213
|
+
return {
|
|
214
|
+
reason: serializeErrorInternal(error.reason),
|
|
215
|
+
type: "PromiseRejectionEvent"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if ("error" in error && "message" in error && "filename" in error) {
|
|
219
|
+
return {
|
|
220
|
+
colno: error.colno,
|
|
221
|
+
error: serializeErrorInternal(error.error),
|
|
222
|
+
filename: error.filename,
|
|
223
|
+
lineno: error.lineno,
|
|
224
|
+
message: error.message,
|
|
225
|
+
type: "ErrorEvent"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if ("violatedDirective" in error && "blockedURI" in error) {
|
|
229
|
+
const evt = error;
|
|
230
|
+
return {
|
|
231
|
+
blockedURI: evt.blockedURI,
|
|
232
|
+
columnNumber: evt.columnNumber,
|
|
233
|
+
effectiveDirective: evt.effectiveDirective,
|
|
234
|
+
lineNumber: evt.lineNumber,
|
|
235
|
+
originalPolicy: evt.originalPolicy,
|
|
236
|
+
sourceFile: evt.sourceFile,
|
|
237
|
+
type: "SecurityPolicyViolationEvent",
|
|
238
|
+
violatedDirective: evt.violatedDirective
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if ("type" in error && "target" in error) {
|
|
242
|
+
const evt = error;
|
|
243
|
+
return {
|
|
244
|
+
eventType: evt.type,
|
|
245
|
+
target: extractEventTarget(evt.target),
|
|
246
|
+
timeStamp: evt.timeStamp,
|
|
247
|
+
type: "Event"
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
type: "object",
|
|
252
|
+
value: extractOwnProperties(error)
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
var extractErrorProperties = (error) => {
|
|
256
|
+
const props = {};
|
|
257
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
258
|
+
if (!["message", "name", "stack"].includes(key)) {
|
|
259
|
+
try {
|
|
260
|
+
props[key] = error[key];
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return props;
|
|
266
|
+
};
|
|
267
|
+
var extractEventTarget = (target) => {
|
|
268
|
+
if (!target) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (target instanceof HTMLElement) {
|
|
272
|
+
return {
|
|
273
|
+
className: target.className,
|
|
274
|
+
href: target.href,
|
|
275
|
+
id: target.id,
|
|
276
|
+
src: target.src,
|
|
277
|
+
tagName: target.tagName
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return { type: String(target) };
|
|
281
|
+
};
|
|
282
|
+
var extractOwnProperties = (obj) => {
|
|
283
|
+
const props = {};
|
|
284
|
+
for (const key of Object.keys(obj)) {
|
|
285
|
+
try {
|
|
286
|
+
const value = obj[key];
|
|
287
|
+
props[key] = typeof value === "object" ? String(value) : value;
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return props;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// src/common/listen/internal.ts
|
|
295
|
+
var listenInternal = () => {
|
|
296
|
+
emitEvent({ name: "test" });
|
|
297
|
+
const wa = window.addEventListener;
|
|
298
|
+
wa(
|
|
299
|
+
"error",
|
|
300
|
+
(event) => {
|
|
301
|
+
console.error(logMessage("error:capture:"), event);
|
|
302
|
+
if (isChunkError(event)) {
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
attemptReload(event);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const serialized = serializeError(event);
|
|
308
|
+
sendBeacon({
|
|
309
|
+
errorMessage: event.message,
|
|
310
|
+
eventName: "error",
|
|
311
|
+
serialized
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
true
|
|
315
|
+
);
|
|
316
|
+
wa("error", (event) => {
|
|
317
|
+
console.error(logMessage("error:"), event);
|
|
318
|
+
});
|
|
319
|
+
wa("unhandledrejection", (event) => {
|
|
320
|
+
console.error(logMessage("unhandledrejection:"), event);
|
|
321
|
+
if (isChunkError(event.reason)) {
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
attemptReload(event.reason);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const serialized = serializeError(event);
|
|
327
|
+
sendBeacon({
|
|
328
|
+
errorMessage: String(event.reason),
|
|
329
|
+
eventName: "unhandledrejection",
|
|
330
|
+
serialized
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
wa("uncaughtException", (event) => {
|
|
334
|
+
console.error(logMessage("uncaughtException:"), event);
|
|
335
|
+
const serialized = serializeError(event);
|
|
336
|
+
sendBeacon({
|
|
337
|
+
eventName: "uncaughtException",
|
|
338
|
+
serialized
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
wa("securitypolicyviolation", (event) => {
|
|
342
|
+
console.error(logMessage("CSP violation:"), event.blockedURI, event.violatedDirective);
|
|
343
|
+
const serialized = serializeError(event);
|
|
344
|
+
sendBeacon({
|
|
345
|
+
eventMessage: `${event.violatedDirective}: ${event.blockedURI}`,
|
|
346
|
+
eventName: "securitypolicyviolation",
|
|
347
|
+
serialized
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
wa("vite:preloadError", (event) => {
|
|
351
|
+
console.error(logMessage("vite:preloadError:"), event);
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
attemptReload(event);
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/inline/index.ts
|
|
358
|
+
(() => {
|
|
359
|
+
listenInternal();
|
|
360
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ovineko/spa-guard",
|
|
3
|
+
"version": "0.0.1-alpha-1",
|
|
4
|
+
"description": "Chunk load error handling for SPAs with automatic recovery, beacon reporting, and deployment monitoring",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"spa",
|
|
7
|
+
"chunk-load-error",
|
|
8
|
+
"vite",
|
|
9
|
+
"react",
|
|
10
|
+
"react-router",
|
|
11
|
+
"error-handling",
|
|
12
|
+
"error-boundary",
|
|
13
|
+
"deployment",
|
|
14
|
+
"beacon",
|
|
15
|
+
"monitoring",
|
|
16
|
+
"fastify",
|
|
17
|
+
"chunk",
|
|
18
|
+
"preload",
|
|
19
|
+
"dynamic-import"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/ovineko/ovineko/tree/main/packages/spa-guard",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/ovineko/ovineko/issues"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/ovineko/ovineko.git",
|
|
28
|
+
"directory": "packages/spa-guard"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"author": "Alexander Svinarev <shibanet0@gmail.com> (shibanet0.com)",
|
|
32
|
+
"type": "module",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/common/index.d.ts",
|
|
36
|
+
"default": "./dist/common/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./schema": {
|
|
39
|
+
"types": "./dist/schema/index.d.ts",
|
|
40
|
+
"default": "./dist/schema/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./schema/parse": {
|
|
43
|
+
"types": "./dist/schema/parse.d.ts",
|
|
44
|
+
"default": "./dist/schema/parse.js"
|
|
45
|
+
},
|
|
46
|
+
"./react": {
|
|
47
|
+
"types": "./dist/react/index.d.ts",
|
|
48
|
+
"default": "./dist/react/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./react-router": {
|
|
51
|
+
"types": "./dist/react-router/index.d.ts",
|
|
52
|
+
"default": "./dist/react-router/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./fastify": {
|
|
55
|
+
"types": "./dist/fastify/index.d.ts",
|
|
56
|
+
"default": "./dist/fastify/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./vite-plugin": {
|
|
59
|
+
"types": "./dist/vite-plugin/index.d.ts",
|
|
60
|
+
"default": "./dist/vite-plugin/index.js"
|
|
61
|
+
},
|
|
62
|
+
"./react-error-boundary": {
|
|
63
|
+
"types": "./dist/react-error-boundary/index.d.ts",
|
|
64
|
+
"default": "./dist/react-error-boundary/index.js"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"files": [
|
|
68
|
+
"dist",
|
|
69
|
+
"dist-inline",
|
|
70
|
+
"dist-inline-trace",
|
|
71
|
+
"README.md"
|
|
72
|
+
],
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"react": "^19",
|
|
75
|
+
"react-error-boundary": "^6",
|
|
76
|
+
"react-router": "^7",
|
|
77
|
+
"typebox": "^1",
|
|
78
|
+
"vite": "^8 || ^7"
|
|
79
|
+
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">=20"
|
|
82
|
+
},
|
|
83
|
+
"publishConfig": {
|
|
84
|
+
"access": "public"
|
|
85
|
+
}
|
|
86
|
+
}
|