@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.
Files changed (39) hide show
  1. package/README.md +695 -0
  2. package/dist/chunk-BL4EG6R7.js +14 -0
  3. package/dist/chunk-MLKGABMK.js +9 -0
  4. package/dist/chunk-VWV6L4Q2.js +51 -0
  5. package/dist/chunk-W65YKSMF.js +6 -0
  6. package/dist/chunk-XV2YCVOR.js +12 -0
  7. package/dist/common/constants.d.ts +5 -0
  8. package/dist/common/events/index.d.ts +2 -0
  9. package/dist/common/events/internal.d.ts +4 -0
  10. package/dist/common/events/types.d.ts +8 -0
  11. package/dist/common/fallbackHtml.generated.d.ts +1 -0
  12. package/dist/common/index.d.ts +3 -0
  13. package/dist/common/index.js +358 -0
  14. package/dist/common/isChunkError.d.ts +1 -0
  15. package/dist/common/listen/index.d.ts +1 -0
  16. package/dist/common/listen/internal.d.ts +1 -0
  17. package/dist/common/log.d.ts +1 -0
  18. package/dist/common/options.d.ts +12 -0
  19. package/dist/common/reload.d.ts +1 -0
  20. package/dist/common/sendBeacon.d.ts +2 -0
  21. package/dist/common/serializeError.d.ts +1 -0
  22. package/dist/fastify/index.d.ts +45 -0
  23. package/dist/fastify/index.js +66 -0
  24. package/dist/inline/index.d.ts +1 -0
  25. package/dist/react/index.d.ts +1 -0
  26. package/dist/react/index.js +0 -0
  27. package/dist/react-error-boundary/index.d.ts +1 -0
  28. package/dist/react-error-boundary/index.js +7 -0
  29. package/dist/react-router/index.d.ts +1 -0
  30. package/dist/react-router/index.js +12 -0
  31. package/dist/schema/index.d.ts +8 -0
  32. package/dist/schema/index.js +7 -0
  33. package/dist/schema/parse.d.ts +6 -0
  34. package/dist/schema/parse.js +8 -0
  35. package/dist/vite-plugin/index.d.ts +6 -0
  36. package/dist/vite-plugin/index.js +38 -0
  37. package/dist-inline/index.js +1 -0
  38. package/dist-inline-trace/index.js +360 -0
  39. 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
+ };