@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
package/README.md
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
# @ovineko/spa-guard
|
|
2
|
+
|
|
3
|
+
Production-ready error handling for Single Page Applications with automatic chunk error recovery, intelligent retry logic, and comprehensive error reporting.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @ovineko/spa-guard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies vary by integration - see sections below for specific requirements.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- ✅ **Automatic chunk load error detection** - Handles `vite:preloadError`, dynamic imports, and chunk failures across Chrome, Firefox, and Safari
|
|
16
|
+
- ✅ **Intelligent retry with cache busting** - Uses query parameters with UUID to bypass HTML cache after deployments
|
|
17
|
+
- ✅ **Configurable retry delays** - Flexible delay arrays (e.g., `[1000, 2000, 5000]`) instead of simple max attempts
|
|
18
|
+
- ✅ **Graceful fallback UI** - Shows user-friendly error screen after all retry attempts are exhausted
|
|
19
|
+
- ✅ **Deep error serialization** - Captures detailed error information for server-side analysis
|
|
20
|
+
- ✅ **Smart beacon reporting** - Sends error reports only after retry exhaustion to prevent spam
|
|
21
|
+
- ✅ **Dual build system** - Production minified (5KB) and trace verbose (10KB) builds for different environments
|
|
22
|
+
- ✅ **Global error listeners** - Captures `error`, `unhandledrejection`, and `securitypolicyviolation` events
|
|
23
|
+
- ✅ **Vite plugin for inline script injection** - Runs before all chunks to catch early errors
|
|
24
|
+
- ✅ **Fastify server integration** - Ready-to-use plugin for handling error reports
|
|
25
|
+
- ✅ **React Router v7 integration** - Works seamlessly with React Router error boundaries
|
|
26
|
+
- ✅ **TypeScript support** - Full type definitions with all exports
|
|
27
|
+
- ✅ **Framework-agnostic core** - Works with or without React
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Vite + React Router Setup
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// vite.config.ts
|
|
35
|
+
import { spaGuardVitePlugin } from "@ovineko/spa-guard/vite-plugin";
|
|
36
|
+
import { defineConfig } from "vite";
|
|
37
|
+
import react from "@vitejs/plugin-react";
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
plugins: [
|
|
41
|
+
spaGuardVitePlugin({
|
|
42
|
+
// Production configuration
|
|
43
|
+
reloadDelays: [1000, 2000, 5000], // 3 attempts with increasing delays
|
|
44
|
+
fallbackHtml: `
|
|
45
|
+
<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif">
|
|
46
|
+
<div style="text-align:center">
|
|
47
|
+
<h1>Something went wrong</h1>
|
|
48
|
+
<p>Please refresh the page to continue.</p>
|
|
49
|
+
<button onclick="location.reload()">Refresh Page</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
`,
|
|
53
|
+
reportBeacon: {
|
|
54
|
+
endpoint: "/api/beacon",
|
|
55
|
+
},
|
|
56
|
+
useRetryId: true, // Use query parameters for cache busting (default: true)
|
|
57
|
+
}),
|
|
58
|
+
react(),
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Development with Trace Mode
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// vite.config.ts
|
|
67
|
+
export default defineConfig({
|
|
68
|
+
plugins: [
|
|
69
|
+
spaGuardVitePlugin({
|
|
70
|
+
trace: true, // Enable verbose logging (10KB instead of 5KB)
|
|
71
|
+
reloadDelays: [1000, 2000],
|
|
72
|
+
reportBeacon: { endpoint: "/api/beacon" },
|
|
73
|
+
}),
|
|
74
|
+
react(),
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Fastify Server
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// server.ts
|
|
83
|
+
import { fastifySPAGuard } from "@ovineko/spa-guard/fastify";
|
|
84
|
+
import Fastify from "fastify";
|
|
85
|
+
|
|
86
|
+
const app = Fastify();
|
|
87
|
+
|
|
88
|
+
app.register(fastifySPAGuard, {
|
|
89
|
+
path: "/api/beacon",
|
|
90
|
+
onBeacon: async (beacon, request) => {
|
|
91
|
+
// Log to Sentry, DataDog, or your monitoring service
|
|
92
|
+
request.log.error(beacon, "Client error received");
|
|
93
|
+
|
|
94
|
+
// Optional: Send to Sentry
|
|
95
|
+
// Sentry.captureException(new Error(beacon.errorMessage), {
|
|
96
|
+
// extra: beacon
|
|
97
|
+
// });
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await app.listen({ port: 3000 });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## How It Works
|
|
105
|
+
|
|
106
|
+
### Chunk Error Recovery Flow
|
|
107
|
+
|
|
108
|
+
When a chunk load error occurs (typically after deployment):
|
|
109
|
+
|
|
110
|
+
1. **Error Detection**: spa-guard detects chunk load error (e.g., "Failed to fetch dynamically imported module")
|
|
111
|
+
2. **Cache Bypass**: Generates unique retry ID using `crypto.randomUUID()` (or secure fallback)
|
|
112
|
+
3. **First Reload**: Adds query parameters `?spaGuardRetryId=uuid&spaGuardRetryAttempt=1`
|
|
113
|
+
4. **Browser Refresh**: Browser sees new URL → bypasses cache → requests fresh HTML from server
|
|
114
|
+
5. **Success or Retry**: If still failing, increases attempt count and tries again with longer delay
|
|
115
|
+
6. **Fallback UI**: After all attempts exhausted, shows custom error screen and sends beacon to server
|
|
116
|
+
|
|
117
|
+
**Why query parameters instead of sessionStorage?**
|
|
118
|
+
|
|
119
|
+
- **Bypasses HTML cache**: Even if `index.html` has aggressive cache headers, unique URL forces fresh fetch
|
|
120
|
+
- **No storage limitations**: Works in private browsing, cross-domain, and storage-disabled environments
|
|
121
|
+
- **Cache-Control agnostic**: Doesn't rely on server cache configuration
|
|
122
|
+
|
|
123
|
+
### Retry Delay Strategy
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
reloadDelays: [1000, 2000, 5000]; // Default
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- **Attempt 1**: Wait 1000ms (1s) → reload with `?spaGuardRetryAttempt=1`
|
|
130
|
+
- **Attempt 2**: Wait 2000ms (2s) → reload with `?spaGuardRetryAttempt=2`
|
|
131
|
+
- **Attempt 3**: Wait 5000ms (5s) → reload with `?spaGuardRetryAttempt=3`
|
|
132
|
+
- **Exhausted**: Show fallback UI, send beacon to server
|
|
133
|
+
|
|
134
|
+
### Secure Random ID Generation
|
|
135
|
+
|
|
136
|
+
Three-tier fallback chain for maximum compatibility:
|
|
137
|
+
|
|
138
|
+
1. **crypto.randomUUID()** - Modern browsers in secure contexts (HTTPS/localhost)
|
|
139
|
+
2. **crypto.getRandomValues()** - Older browsers in secure contexts
|
|
140
|
+
3. **Math.random()** - Last resort for insecure contexts (HTTP)
|
|
141
|
+
|
|
142
|
+
## Detailed Usage
|
|
143
|
+
|
|
144
|
+
### Vite Plugin Configuration
|
|
145
|
+
|
|
146
|
+
The Vite plugin injects an inline script into your HTML `<head>` that runs **before all other chunks**. This ensures error handling is active even if the main bundle fails to load.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { spaGuardVitePlugin } from "@ovineko/spa-guard/vite-plugin";
|
|
150
|
+
|
|
151
|
+
spaGuardVitePlugin({
|
|
152
|
+
// Retry configuration
|
|
153
|
+
reloadDelays: [1000, 2000, 5000], // Array of delays in milliseconds
|
|
154
|
+
useRetryId: true, // Use query parameters for cache busting (default: true)
|
|
155
|
+
|
|
156
|
+
// Fallback UI configuration
|
|
157
|
+
fallbackHtml: `
|
|
158
|
+
<div style="...">
|
|
159
|
+
<h1>Something went wrong</h1>
|
|
160
|
+
<button onclick="location.reload()">Refresh</button>
|
|
161
|
+
</div>
|
|
162
|
+
`,
|
|
163
|
+
|
|
164
|
+
// Beacon reporting
|
|
165
|
+
reportBeacon: {
|
|
166
|
+
endpoint: "/api/beacon", // Server endpoint for error reports
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// Build mode
|
|
170
|
+
trace: false, // Set to true for verbose debug build (10KB vs 5KB)
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**How it works:**
|
|
175
|
+
|
|
176
|
+
1. Reads minified inline script from `dist-inline/index.js` (or `dist-inline-trace/index.js` if trace mode)
|
|
177
|
+
2. Injects `window.__SPA_GUARD_OPTIONS__` configuration object
|
|
178
|
+
3. Prepends inline script to HTML `<head>` (before all other scripts)
|
|
179
|
+
4. Script registers error listeners immediately on page load
|
|
180
|
+
5. Captures errors even if main application bundle fails
|
|
181
|
+
|
|
182
|
+
### Options Interface
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
interface Options {
|
|
186
|
+
reloadDelays?: number[]; // Array of retry delays in ms (default: [1000, 2000, 5000])
|
|
187
|
+
useRetryId?: boolean; // Use query params for cache busting (default: true)
|
|
188
|
+
fallbackHtml?: string; // Custom error UI HTML (default: basic error screen)
|
|
189
|
+
reportBeacon?: {
|
|
190
|
+
endpoint?: string; // Server endpoint for beacon reports
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface VitePluginOptions extends Options {
|
|
195
|
+
trace?: boolean; // Enable verbose trace build (default: false)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Default values:**
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
{
|
|
203
|
+
reloadDelays: [1000, 2000, 5000],
|
|
204
|
+
useRetryId: true,
|
|
205
|
+
fallbackHtml: `
|
|
206
|
+
<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif">
|
|
207
|
+
<div style="text-align:center">
|
|
208
|
+
<h1>Something went wrong</h1>
|
|
209
|
+
<p>Please refresh the page to continue.</p>
|
|
210
|
+
<button onclick="location.reload()">Refresh Page</button>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
`,
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Fastify Integration
|
|
218
|
+
|
|
219
|
+
The Fastify plugin provides a POST endpoint to receive beacon data from clients.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { fastifySPAGuard } from "@ovineko/spa-guard/fastify";
|
|
223
|
+
|
|
224
|
+
app.register(fastifySPAGuard, {
|
|
225
|
+
path: "/api/beacon",
|
|
226
|
+
|
|
227
|
+
// Custom beacon handler
|
|
228
|
+
onBeacon: async (beacon, request) => {
|
|
229
|
+
const error = new Error(beacon.errorMessage || "Unknown client error");
|
|
230
|
+
|
|
231
|
+
// Log structured data
|
|
232
|
+
request.log.error(
|
|
233
|
+
{
|
|
234
|
+
errorMessage: beacon.errorMessage,
|
|
235
|
+
eventName: beacon.eventName,
|
|
236
|
+
eventMessage: beacon.eventMessage,
|
|
237
|
+
serialized: beacon.serialized,
|
|
238
|
+
},
|
|
239
|
+
"SPA Guard beacon received",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Send to monitoring service
|
|
243
|
+
await sendToMonitoring(error, beacon);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// Handle unknown/invalid beacon formats
|
|
247
|
+
onUnknownBeacon: async (body, request) => {
|
|
248
|
+
request.log.warn({ body }, "Received unknown beacon format");
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**BeaconSchema structure:**
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
interface BeaconSchema {
|
|
257
|
+
errorMessage?: string; // Error message
|
|
258
|
+
eventMessage?: string; // Event-specific message
|
|
259
|
+
eventName?: string; // Event type (e.g., 'chunk_error_max_reloads', 'error', 'unhandledrejection')
|
|
260
|
+
serialized?: string; // Serialized error details (JSON string)
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Beacon events:**
|
|
265
|
+
|
|
266
|
+
- `chunk_error_max_reloads` - All reload attempts exhausted for chunk error
|
|
267
|
+
- `error` - Non-chunk global error
|
|
268
|
+
- `unhandledrejection` - Non-chunk promise rejection
|
|
269
|
+
- `uncaughtException` - Uncaught exception
|
|
270
|
+
- `securitypolicyviolation` - CSP violation
|
|
271
|
+
|
|
272
|
+
### React Router Integration
|
|
273
|
+
|
|
274
|
+
spa-guard works seamlessly with React Router v7 error boundaries:
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { createBrowserRouter, RouterProvider } from "react-router";
|
|
278
|
+
import { ErrorBoundaryReloadPage } from "@ovineko/react-error-boundary";
|
|
279
|
+
|
|
280
|
+
const router = createBrowserRouter([
|
|
281
|
+
{
|
|
282
|
+
path: "/",
|
|
283
|
+
element: <App />,
|
|
284
|
+
errorElement: <ErrorBoundaryReloadPage />,
|
|
285
|
+
children: [
|
|
286
|
+
{
|
|
287
|
+
path: "users/:id",
|
|
288
|
+
lazy: () => import("./pages/UserPage"),
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
function Root() {
|
|
295
|
+
return <RouterProvider router={router} />;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Error flow:**
|
|
300
|
+
|
|
301
|
+
1. Chunk load error occurs (e.g., after deployment)
|
|
302
|
+
2. spa-guard inline script detects error
|
|
303
|
+
3. Attempts automatic reload with query parameters (up to `reloadDelays.length` times)
|
|
304
|
+
4. If all reloads fail, sends beacon to server and shows fallback UI
|
|
305
|
+
5. React Router error boundary catches error (if no fallback UI configured)
|
|
306
|
+
6. `ErrorBoundaryReloadPage` displays fallback UI
|
|
307
|
+
|
|
308
|
+
### Core API (Framework-agnostic)
|
|
309
|
+
|
|
310
|
+
The core module provides low-level APIs for custom integrations:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { events, listen, options } from "@ovineko/spa-guard";
|
|
314
|
+
|
|
315
|
+
// Subscribe to spa-guard events
|
|
316
|
+
events.subscribe((event) => {
|
|
317
|
+
console.log("SPA Guard event:", event);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Emit custom event
|
|
321
|
+
events.emit({ type: "custom", data: "..." });
|
|
322
|
+
|
|
323
|
+
// Initialize error listeners (automatically called by inline script)
|
|
324
|
+
listen();
|
|
325
|
+
|
|
326
|
+
// Get merged options
|
|
327
|
+
const opts = options.getOptions();
|
|
328
|
+
console.log("Reload delays:", opts.reloadDelays);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Event system:**
|
|
332
|
+
|
|
333
|
+
- `events.subscribe(listener)` - Subscribe to all spa-guard events
|
|
334
|
+
- `events.emit(event)` - Emit event to all subscribers
|
|
335
|
+
- Uses Symbol-based storage for isolation
|
|
336
|
+
- Safe for server-side rendering (checks for `globalThis.window` availability)
|
|
337
|
+
|
|
338
|
+
## Error Detection
|
|
339
|
+
|
|
340
|
+
spa-guard registers multiple event listeners to catch different error types:
|
|
341
|
+
|
|
342
|
+
**`globalThis.window.addEventListener('error', ...)`**
|
|
343
|
+
|
|
344
|
+
- Resource load failures (`<script>`, `<link>`, `<img>`)
|
|
345
|
+
- Synchronous JavaScript errors
|
|
346
|
+
- Uses capture phase (`{ capture: true }`) to catch early
|
|
347
|
+
|
|
348
|
+
**`globalThis.window.addEventListener('unhandledrejection', ...)`**
|
|
349
|
+
|
|
350
|
+
- Promise rejections
|
|
351
|
+
- Dynamic `import()` failures
|
|
352
|
+
- Async/await errors without try/catch
|
|
353
|
+
|
|
354
|
+
**`globalThis.window.addEventListener('securitypolicyviolation', ...)`**
|
|
355
|
+
|
|
356
|
+
- Content Security Policy violations
|
|
357
|
+
- Blocked scripts/resources
|
|
358
|
+
|
|
359
|
+
**`globalThis.window.addEventListener('vite:preloadError', ...)`** (Vite-specific)
|
|
360
|
+
|
|
361
|
+
- Vite chunk preload failures
|
|
362
|
+
- CSS preload errors
|
|
363
|
+
|
|
364
|
+
### Chunk Error Patterns
|
|
365
|
+
|
|
366
|
+
spa-guard detects chunk errors across browsers using regex patterns:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
const patterns = [
|
|
370
|
+
/Failed to fetch dynamically imported module/i, // Chrome/Edge
|
|
371
|
+
/Importing a module script failed/i, // Firefox
|
|
372
|
+
/error loading dynamically imported module/i, // Safari
|
|
373
|
+
/Unable to preload CSS/i, // CSS chunk errors
|
|
374
|
+
/Loading chunk \d+ failed/i, // Webpack
|
|
375
|
+
/Loading CSS chunk \d+ failed/i, // Webpack CSS
|
|
376
|
+
/ChunkLoadError/i, // Generic
|
|
377
|
+
/Failed to fetch/i, // Network failures
|
|
378
|
+
];
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Deep Error Serialization
|
|
382
|
+
|
|
383
|
+
spa-guard captures maximum error information for server analysis:
|
|
384
|
+
|
|
385
|
+
**Error types:**
|
|
386
|
+
|
|
387
|
+
- `Error` - Standard JavaScript errors (name, message, stack, custom properties)
|
|
388
|
+
- `ErrorEvent` - DOM error events (message, filename, lineno, colno, error)
|
|
389
|
+
- `PromiseRejectionEvent` - Unhandled promise rejections (reason, promise)
|
|
390
|
+
- `SecurityPolicyViolationEvent` - CSP violations (blockedURI, violatedDirective, etc.)
|
|
391
|
+
- `Event` - Generic events (type, target, timeStamp)
|
|
392
|
+
|
|
393
|
+
**Example serialized output:**
|
|
394
|
+
|
|
395
|
+
```json
|
|
396
|
+
{
|
|
397
|
+
"type": "ErrorEvent",
|
|
398
|
+
"message": "Failed to fetch dynamically imported module",
|
|
399
|
+
"filename": "https://example.com/app.js",
|
|
400
|
+
"lineno": 42,
|
|
401
|
+
"colno": 15,
|
|
402
|
+
"error": {
|
|
403
|
+
"type": "Error",
|
|
404
|
+
"name": "TypeError",
|
|
405
|
+
"message": "Failed to fetch dynamically imported module",
|
|
406
|
+
"stack": "TypeError: Failed to fetch...\n at loadChunk..."
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Beacon Reporting
|
|
412
|
+
|
|
413
|
+
Error data is sent to the server using a fire-and-forget pattern:
|
|
414
|
+
|
|
415
|
+
1. **Primary:** `navigator.sendBeacon(endpoint, JSON.stringify(data))`
|
|
416
|
+
- Works even during page unload
|
|
417
|
+
- Non-blocking
|
|
418
|
+
- Reliable delivery
|
|
419
|
+
2. **Fallback:** `fetch(endpoint, { method: 'POST', body: data, keepalive: true })`
|
|
420
|
+
- Used if beacon is unavailable
|
|
421
|
+
- `keepalive: true` ensures delivery during navigation
|
|
422
|
+
|
|
423
|
+
**Important:** Beacons are sent **only after retry exhaustion** for chunk errors to prevent spam during normal recovery.
|
|
424
|
+
|
|
425
|
+
## API Reference
|
|
426
|
+
|
|
427
|
+
### Vite Plugin
|
|
428
|
+
|
|
429
|
+
#### `spaGuardVitePlugin(options: VitePluginOptions): Plugin`
|
|
430
|
+
|
|
431
|
+
Creates a Vite plugin that injects inline error handling script.
|
|
432
|
+
|
|
433
|
+
**Parameters:**
|
|
434
|
+
|
|
435
|
+
- `options: VitePluginOptions` - Configuration object
|
|
436
|
+
|
|
437
|
+
**Returns:** Vite `Plugin` object
|
|
438
|
+
|
|
439
|
+
**Example:**
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { spaGuardVitePlugin } from "@ovineko/spa-guard/vite-plugin";
|
|
443
|
+
|
|
444
|
+
export default defineConfig({
|
|
445
|
+
plugins: [
|
|
446
|
+
spaGuardVitePlugin({
|
|
447
|
+
reloadDelays: [1000, 2000, 5000],
|
|
448
|
+
reportBeacon: { endpoint: "/api/beacon" },
|
|
449
|
+
trace: false,
|
|
450
|
+
}),
|
|
451
|
+
],
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Fastify Plugin
|
|
456
|
+
|
|
457
|
+
#### `fastifySPAGuard(fastify, options: FastifySPAGuardOptions): Promise<void>`
|
|
458
|
+
|
|
459
|
+
Registers a POST endpoint to receive beacon data from clients.
|
|
460
|
+
|
|
461
|
+
**Parameters:**
|
|
462
|
+
|
|
463
|
+
- `fastify` - Fastify instance
|
|
464
|
+
- `options: FastifySPAGuardOptions` - Configuration object
|
|
465
|
+
|
|
466
|
+
**FastifySPAGuardOptions:**
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
interface FastifySPAGuardOptions {
|
|
470
|
+
path: string; // Route path (e.g., "/api/beacon")
|
|
471
|
+
onBeacon?: (beacon: BeaconSchema, request: any) => Promise<void> | void;
|
|
472
|
+
onUnknownBeacon?: (body: unknown, request: any) => Promise<void> | void;
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Types
|
|
477
|
+
|
|
478
|
+
#### `Options`
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
interface Options {
|
|
482
|
+
fallbackHtml?: string; // Custom error UI HTML
|
|
483
|
+
reloadDelays?: number[]; // Retry delays in ms (default: [1000, 2000, 5000])
|
|
484
|
+
reportBeacon?: {
|
|
485
|
+
endpoint?: string; // Error reporting endpoint
|
|
486
|
+
};
|
|
487
|
+
useRetryId?: boolean; // Use query params for cache busting (default: true)
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
#### `VitePluginOptions`
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
interface VitePluginOptions extends Options {
|
|
495
|
+
trace?: boolean; // Enable verbose trace build (default: false)
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
#### `BeaconSchema`
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
interface BeaconSchema {
|
|
503
|
+
errorMessage?: string;
|
|
504
|
+
eventMessage?: string;
|
|
505
|
+
eventName?: string;
|
|
506
|
+
serialized?: string; // JSON stringified error details
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Core Exports
|
|
511
|
+
|
|
512
|
+
From `@ovineko/spa-guard`:
|
|
513
|
+
|
|
514
|
+
- `events.subscribe(listener)` - Subscribe to spa-guard events
|
|
515
|
+
- `events.emit(event)` - Emit event to subscribers
|
|
516
|
+
- `listen()` - Initialize error listeners
|
|
517
|
+
- `options.getOptions()` - Get merged options from globalThis.window
|
|
518
|
+
- `options.optionsWindowKey` - Window storage key constant
|
|
519
|
+
|
|
520
|
+
### Schema Exports
|
|
521
|
+
|
|
522
|
+
From `@ovineko/spa-guard/schema`:
|
|
523
|
+
|
|
524
|
+
- `beaconSchema` - TypeBox schema for validation
|
|
525
|
+
- `BeaconSchema` - TypeScript type
|
|
526
|
+
|
|
527
|
+
From `@ovineko/spa-guard/schema/parse`:
|
|
528
|
+
|
|
529
|
+
- `parseBeacon(data)` - Parse and validate beacon data
|
|
530
|
+
|
|
531
|
+
## Module Exports
|
|
532
|
+
|
|
533
|
+
spa-guard provides 8 export entry points:
|
|
534
|
+
|
|
535
|
+
| Export | Description | Peer Dependencies |
|
|
536
|
+
| ------------------------ | -------------------------------------------- | -------------------------------------- |
|
|
537
|
+
| `.` | Core functionality (events, listen, options) | None |
|
|
538
|
+
| `./schema` | BeaconSchema type definitions | `typebox@^1` |
|
|
539
|
+
| `./schema/parse` | Beacon parsing utilities | `typebox@^1` |
|
|
540
|
+
| `./react` | React integration (placeholder) | `react@^19` |
|
|
541
|
+
| `./react-router` | React Router integration | `react@^19`, `react-router@^7` |
|
|
542
|
+
| `./fastify` | Fastify server plugin | None |
|
|
543
|
+
| `./vite-plugin` | Vite build plugin | `vite@^7 \|\| ^8` |
|
|
544
|
+
| `./react-error-boundary` | Error boundary re-export | `react@^19`, `react-error-boundary@^6` |
|
|
545
|
+
|
|
546
|
+
**Import examples:**
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
// Core
|
|
550
|
+
import { events, listen } from "@ovineko/spa-guard";
|
|
551
|
+
|
|
552
|
+
// Schema
|
|
553
|
+
import type { BeaconSchema } from "@ovineko/spa-guard/schema";
|
|
554
|
+
|
|
555
|
+
// Vite plugin
|
|
556
|
+
import { spaGuardVitePlugin } from "@ovineko/spa-guard/vite-plugin";
|
|
557
|
+
|
|
558
|
+
// Fastify
|
|
559
|
+
import { fastifySPAGuard } from "@ovineko/spa-guard/fastify";
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
## Build Sizes
|
|
563
|
+
|
|
564
|
+
- **Production:** `dist-inline/index.js` ~5KB minified (Terser)
|
|
565
|
+
- **Trace:** `dist-inline-trace/index.js` ~10KB unminified (debug)
|
|
566
|
+
- **Main library:** `dist/` varies by export
|
|
567
|
+
|
|
568
|
+
## Advanced Usage
|
|
569
|
+
|
|
570
|
+
### Disable Query Parameters (PII Concerns)
|
|
571
|
+
|
|
572
|
+
If query parameters are problematic for your use case:
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
spaGuardVitePlugin({
|
|
576
|
+
useRetryId: false, // Disable query parameters
|
|
577
|
+
reloadDelays: [1000, 2000], // Still retries, but without cache busting
|
|
578
|
+
});
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Note:** Without `useRetryId`, retries use `globalThis.window.location.reload()` which may not bypass aggressive HTML cache.
|
|
582
|
+
|
|
583
|
+
### Custom Fallback UI
|
|
584
|
+
|
|
585
|
+
Provide fully custom HTML for error screen (injected into `document.body`):
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
spaGuardVitePlugin({
|
|
589
|
+
fallbackHtml: `
|
|
590
|
+
<style>
|
|
591
|
+
.spa-guard-error {
|
|
592
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
593
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
594
|
+
color: white;
|
|
595
|
+
display: flex;
|
|
596
|
+
align-items: center;
|
|
597
|
+
justify-content: center;
|
|
598
|
+
height: 100vh;
|
|
599
|
+
margin: 0;
|
|
600
|
+
padding: 20px;
|
|
601
|
+
box-sizing: border-box;
|
|
602
|
+
}
|
|
603
|
+
.spa-guard-container {
|
|
604
|
+
max-width: 500px;
|
|
605
|
+
text-align: center;
|
|
606
|
+
background: rgba(255, 255, 255, 0.1);
|
|
607
|
+
backdrop-filter: blur(10px);
|
|
608
|
+
padding: 40px;
|
|
609
|
+
border-radius: 20px;
|
|
610
|
+
}
|
|
611
|
+
.spa-guard-button {
|
|
612
|
+
background: white;
|
|
613
|
+
color: #667eea;
|
|
614
|
+
border: none;
|
|
615
|
+
padding: 12px 24px;
|
|
616
|
+
font-size: 16px;
|
|
617
|
+
font-weight: 600;
|
|
618
|
+
border-radius: 8px;
|
|
619
|
+
cursor: pointer;
|
|
620
|
+
margin-top: 20px;
|
|
621
|
+
}
|
|
622
|
+
.spa-guard-button:hover {
|
|
623
|
+
transform: scale(1.05);
|
|
624
|
+
}
|
|
625
|
+
</style>
|
|
626
|
+
<div class="spa-guard-error">
|
|
627
|
+
<div class="spa-guard-container">
|
|
628
|
+
<h1>⚠️ Application Error</h1>
|
|
629
|
+
<p>We're experiencing technical difficulties. Please try reloading the page.</p>
|
|
630
|
+
<button class="spa-guard-button" onclick="location.reload()">Reload Application</button>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
`,
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### Custom Retry Strategy
|
|
638
|
+
|
|
639
|
+
Adjust retry delays for different environments:
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
// Aggressive retries for production
|
|
643
|
+
spaGuardVitePlugin({
|
|
644
|
+
reloadDelays: [500, 1000, 2000, 5000, 10000], // 5 attempts
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Quick retries for development
|
|
648
|
+
spaGuardVitePlugin({
|
|
649
|
+
trace: true,
|
|
650
|
+
reloadDelays: [100, 500], // 2 fast attempts
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## TypeScript
|
|
655
|
+
|
|
656
|
+
All exports are fully typed with TypeScript definitions:
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
import type { Options, VitePluginOptions } from "@ovineko/spa-guard/vite-plugin";
|
|
660
|
+
import type { BeaconSchema } from "@ovineko/spa-guard/schema";
|
|
661
|
+
import type { FastifySPAGuardOptions } from "@ovineko/spa-guard/fastify";
|
|
662
|
+
|
|
663
|
+
const options: VitePluginOptions = {
|
|
664
|
+
reloadDelays: [1000, 2000, 5000],
|
|
665
|
+
reportBeacon: {
|
|
666
|
+
endpoint: "/api/beacon",
|
|
667
|
+
},
|
|
668
|
+
trace: false,
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const handleBeacon = (beacon: BeaconSchema) => {
|
|
672
|
+
console.log(beacon.errorMessage);
|
|
673
|
+
console.log(beacon.serialized);
|
|
674
|
+
};
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
**Type features:**
|
|
678
|
+
|
|
679
|
+
- JSDoc comments with default values
|
|
680
|
+
- BeaconSchema from TypeBox with runtime validation
|
|
681
|
+
- Fastify plugin types for type-safe integration
|
|
682
|
+
- Options interface with optional fields
|
|
683
|
+
|
|
684
|
+
## Future Enhancements
|
|
685
|
+
|
|
686
|
+
Detailed specifications for planned features are documented in [TODO.md](TODO.md):
|
|
687
|
+
|
|
688
|
+
- **Version Checker Module** - Detect new deployments via HTML or JSON polling
|
|
689
|
+
- **Enhanced Event Emitter Architecture** - Rich event system for SPA integration with React hooks
|
|
690
|
+
|
|
691
|
+
These features are designed to extend spa-guard's capabilities while maintaining the minimal inline script footprint.
|
|
692
|
+
|
|
693
|
+
## License
|
|
694
|
+
|
|
695
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beaconSchema
|
|
3
|
+
} from "./chunk-XV2YCVOR.js";
|
|
4
|
+
|
|
5
|
+
// src/schema/parse.ts
|
|
6
|
+
import { Value } from "typebox/value";
|
|
7
|
+
var parseBeacon = (value) => {
|
|
8
|
+
const cleaned = Value.Clean(beaconSchema, value);
|
|
9
|
+
return Value.Decode(beaconSchema, cleaned);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
parseBeacon
|
|
14
|
+
};
|