@ovineko/spa-guard 0.0.1-alpha-7 → 0.0.1-alpha-8
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 +377 -27
- package/dist/chunk-6C6OARM7.js +66 -0
- package/dist/chunk-HLRUA3T2.js +349 -0
- package/dist/chunk-KUC22DET.js +105 -0
- package/dist/{chunk-WKH2B2XS.js → chunk-MCR7F3CG.js} +1 -1
- package/dist/{chunk-BLVJHZST.js → chunk-MDTTXXMB.js} +2 -4
- package/dist/{chunk-4N3XJ7VS.js → chunk-N4BBFG52.js} +12 -5
- package/dist/{chunk-U2DZSTRT.js → chunk-R2J3MNUG.js} +99 -3
- package/dist/{chunk-D5NHZ5BD.js → chunk-RR4LFTYF.js} +69 -22
- package/dist/chunk-SNM6AOFL.js +16 -0
- package/dist/{chunk-XOCUWXQL.js → chunk-Y4SM5PUO.js} +18 -4
- package/dist/common/DefaultErrorFallback.d.ts +1 -1
- package/dist/common/events/types.d.ts +0 -1
- package/dist/common/html.generated.d.ts +3 -0
- package/dist/common/i18n.d.ts +15 -0
- package/dist/common/index.js +3 -3
- package/dist/common/options.d.ts +24 -0
- package/dist/common/parseVersion.d.ts +9 -0
- package/dist/common/spinner.d.ts +7 -0
- package/dist/fastify/index.js +1 -1
- package/dist/i18n/index.d.ts +21 -0
- package/dist/i18n/index.js +9 -0
- package/dist/i18n/translations.d.ts +2 -0
- package/dist/node/index.d.ts +58 -0
- package/dist/node/index.js +130 -0
- package/dist/react/Spinner.d.ts +9 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +10 -6
- package/dist/react-error-boundary/index.js +9 -7
- package/dist/react-router/index.js +9 -7
- package/dist/runtime/debug/index.js +2 -2
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +13 -4
- package/dist/vite-plugin/index.js +47 -12
- package/dist-inline/index.js +1 -1
- package/dist-inline-trace/index.js +1 -1
- package/package.json +15 -2
- package/dist/chunk-UXLGA3TG.js +0 -95
- package/dist/common/fallbackHtml.generated.d.ts +0 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Peer dependencies vary by integration - see sections below for specific requirem
|
|
|
24
24
|
- ✅ **Error filtering** - Filter out specific errors via `errors.ignore`, or force retry/reload via `errors.forceRetry`
|
|
25
25
|
- ✅ **Deep error serialization** - Captures detailed error information for server-side analysis
|
|
26
26
|
- ✅ **Smart beacon reporting** - Sends error reports only after retry exhaustion to prevent spam
|
|
27
|
-
- ✅ **Dual build system** - Production minified (~
|
|
27
|
+
- ✅ **Dual build system** - Production minified (~12 KB) and trace minified (~17 KB) builds for different environments
|
|
28
28
|
- ✅ **Global error listeners** - Captures `error`, `unhandledrejection`, and `securitypolicyviolation` events
|
|
29
29
|
- ✅ **Vite plugin for inline script injection** - Runs before all chunks to catch early errors
|
|
30
30
|
- ✅ **HTML minification** - Automatically minifies fallback HTML to reduce bundle size
|
|
@@ -41,9 +41,60 @@ Peer dependencies vary by integration - see sections below for specific requirem
|
|
|
41
41
|
- ✅ **appName option** - Beacon source identification for monorepo setups
|
|
42
42
|
- ✅ **BeaconError** - Utility class for error tracking service integration (Sentry, Datadog, etc.)
|
|
43
43
|
- ✅ **Configurable unhandled rejection handling** - Control retry and beacon behavior for non-chunk unhandled promise rejections
|
|
44
|
+
- ✅ **Spinner overlay** - Full-page loading spinner with Vite injection, runtime API (`showSpinner`/`dismissSpinner`), and React component
|
|
45
|
+
- ✅ **i18n support** - Server-side HTML patching with `patchHtmlI18n`, built-in translations for 38 languages (ar, az, ca, cs, da, de, el, en, es, eu, fa, fi, fr, he, hr, hu, id, it, ja, ka, kk, ko, ky, lt, lv, nl, no, pl, pt, ro, ru, sk, sl, sv, th, tr, uk, zh), meta tag approach for client-side patching, RTL support for ar, he, fa
|
|
46
|
+
- ✅ **HTML cache with compression** - `createHtmlCache` pre-generates all language variants at startup with gzip, brotli, and zstd compression, ETag support, and encoding negotiation
|
|
47
|
+
- ✅ **Data attribute templating** - Fallback and loading HTML templates use `data-spa-guard-*` attributes for robust content manipulation
|
|
44
48
|
|
|
45
49
|
## Quick Start
|
|
46
50
|
|
|
51
|
+
## Supported Languages (38)
|
|
52
|
+
|
|
53
|
+
spa-guard includes built-in translations for 38 languages:
|
|
54
|
+
|
|
55
|
+
| Code | Language | RTL |
|
|
56
|
+
| ---- | -------------------- | --- |
|
|
57
|
+
| ar | Arabic | ✓ |
|
|
58
|
+
| az | Azerbaijani | |
|
|
59
|
+
| ca | Catalan | |
|
|
60
|
+
| cs | Czech | |
|
|
61
|
+
| da | Danish | |
|
|
62
|
+
| de | German | |
|
|
63
|
+
| el | Greek | |
|
|
64
|
+
| en | English | |
|
|
65
|
+
| es | Spanish | |
|
|
66
|
+
| eu | Basque | |
|
|
67
|
+
| fa | Persian (Farsi) | ✓ |
|
|
68
|
+
| fi | Finnish | |
|
|
69
|
+
| fr | French | |
|
|
70
|
+
| he | Hebrew | ✓ |
|
|
71
|
+
| hr | Croatian | |
|
|
72
|
+
| hu | Hungarian | |
|
|
73
|
+
| id | Indonesian | |
|
|
74
|
+
| it | Italian | |
|
|
75
|
+
| ja | Japanese | |
|
|
76
|
+
| ka | Georgian | |
|
|
77
|
+
| kk | Kazakh | |
|
|
78
|
+
| ko | Korean | |
|
|
79
|
+
| ky | Kyrgyz | |
|
|
80
|
+
| lt | Lithuanian | |
|
|
81
|
+
| lv | Latvian | |
|
|
82
|
+
| nl | Dutch | |
|
|
83
|
+
| no | Norwegian | |
|
|
84
|
+
| pl | Polish | |
|
|
85
|
+
| pt | Portuguese | |
|
|
86
|
+
| ro | Romanian | |
|
|
87
|
+
| ru | Russian | |
|
|
88
|
+
| sk | Slovak | |
|
|
89
|
+
| sl | Slovenian | |
|
|
90
|
+
| sv | Swedish | |
|
|
91
|
+
| th | Thai | |
|
|
92
|
+
| tr | Turkish | |
|
|
93
|
+
| uk | Ukrainian | |
|
|
94
|
+
| zh | Chinese (Simplified) | |
|
|
95
|
+
|
|
96
|
+
RTL (Right-to-Left) languages are fully supported with automatic text direction.
|
|
97
|
+
|
|
47
98
|
### Vite + React Router Setup
|
|
48
99
|
|
|
49
100
|
```tsx
|
|
@@ -80,6 +131,10 @@ export default defineConfig({
|
|
|
80
131
|
forceRetry: [], // Custom error messages that trigger retry/reload (like chunk errors)
|
|
81
132
|
},
|
|
82
133
|
useRetryId: true, // Use query parameters for cache busting (default: true)
|
|
134
|
+
spinner: {
|
|
135
|
+
// background: "#fff", // Overlay background (default: "#fff")
|
|
136
|
+
// disabled: false, // Set true to disable spinner entirely
|
|
137
|
+
},
|
|
83
138
|
}),
|
|
84
139
|
react(),
|
|
85
140
|
],
|
|
@@ -116,7 +171,7 @@ DEBUG=true pnpm test
|
|
|
116
171
|
pnpm test:debug
|
|
117
172
|
|
|
118
173
|
# Watch mode with console logs
|
|
119
|
-
pnpm test:
|
|
174
|
+
DEBUG=1 pnpm test:watch
|
|
120
175
|
```
|
|
121
176
|
|
|
122
177
|
Other test commands:
|
|
@@ -297,10 +352,11 @@ spaGuardVitePlugin({
|
|
|
297
352
|
**How it works:**
|
|
298
353
|
|
|
299
354
|
1. Reads minified inline script from `dist-inline/index.js` (or `dist-inline-trace/index.js` if trace mode)
|
|
300
|
-
2. Injects `window.__SPA_GUARD_OPTIONS__` configuration object
|
|
355
|
+
2. Injects `window.__SPA_GUARD_VERSION__` (for fast version detection) and `window.__SPA_GUARD_OPTIONS__` configuration object
|
|
301
356
|
3. Prepends inline script to HTML `<head>` (before all other scripts)
|
|
302
|
-
4.
|
|
303
|
-
5.
|
|
357
|
+
4. If spinner is enabled, injects spinner overlay into `<body>` at build time
|
|
358
|
+
5. Script registers error listeners immediately on page load
|
|
359
|
+
6. Captures errors even if main application bundle fails
|
|
304
360
|
|
|
305
361
|
### Options Interface
|
|
306
362
|
|
|
@@ -349,6 +405,12 @@ interface Options {
|
|
|
349
405
|
retryDelays?: number[]; // Delays in ms for module-level retries (default: [1000, 2000])
|
|
350
406
|
callReloadOnFailure?: boolean; // Trigger page reload after all retries fail (default: true)
|
|
351
407
|
};
|
|
408
|
+
|
|
409
|
+
spinner?: {
|
|
410
|
+
content?: string; // Custom spinner HTML element (default: SVG circle animation)
|
|
411
|
+
disabled?: boolean; // Disable spinner entirely (default: false)
|
|
412
|
+
background?: string; // Overlay background color (default: "#fff")
|
|
413
|
+
};
|
|
352
414
|
}
|
|
353
415
|
|
|
354
416
|
interface VitePluginOptions extends Options {
|
|
@@ -386,6 +448,10 @@ interface VitePluginOptions extends Options {
|
|
|
386
448
|
content: defaultLoadingFallbackHtml, // minimal loading screen (auto-generated)
|
|
387
449
|
},
|
|
388
450
|
},
|
|
451
|
+
spinner: {
|
|
452
|
+
background: "#fff",
|
|
453
|
+
disabled: false,
|
|
454
|
+
},
|
|
389
455
|
}
|
|
390
456
|
```
|
|
391
457
|
|
|
@@ -644,12 +710,12 @@ function App() {
|
|
|
644
710
|
|
|
645
711
|
**Default Fallback Component:**
|
|
646
712
|
|
|
647
|
-
Both error boundaries use `DefaultErrorFallback` by default, which uses two
|
|
713
|
+
Both error boundaries use `DefaultErrorFallback` by default, which uses two styled HTML templates:
|
|
648
714
|
|
|
649
|
-
- **Error state:**
|
|
650
|
-
- **Loading/retrying state:** Centered "Loading..."
|
|
715
|
+
- **Error state:** SVG alert icon, heading, message paragraph, "Try again" button (non-chunk errors), "Reload page" button, error ID. Uses `system-ui` and `ui-monospace` fonts with styled buttons.
|
|
716
|
+
- **Loading/retrying state:** Centered spinner animation, "Loading..." heading, retry attempt counter with spinner. Uses `@keyframes` animation.
|
|
651
717
|
|
|
652
|
-
These templates are auto-generated as TypeScript string constants (`defaultErrorFallbackHtml` and `defaultLoadingFallbackHtml`) and can be imported for use in custom integrations.
|
|
718
|
+
These templates are auto-generated as TypeScript string constants (`defaultErrorFallbackHtml` and `defaultLoadingFallbackHtml`) and can be imported for use in custom integrations. Both templates use `data-spa-guard-*` attributes for content manipulation (see [Data Attribute Contract](#data-attribute-contract)).
|
|
653
719
|
|
|
654
720
|
**Error flow:**
|
|
655
721
|
|
|
@@ -878,7 +944,7 @@ spaGuardVitePlugin({
|
|
|
878
944
|
|
|
879
945
|
**Two modes:**
|
|
880
946
|
|
|
881
|
-
- **HTML mode** (default): Re-fetches the current page and parses the version from the injected `__SPA_GUARD_OPTIONS__
|
|
947
|
+
- **HTML mode** (default): Re-fetches the current page and parses the version from the injected `__SPA_GUARD_VERSION__` variable (falls back to `__SPA_GUARD_OPTIONS__` for backward compatibility). No extra server endpoint needed.
|
|
882
948
|
- **JSON mode**: Fetches a dedicated JSON endpoint that returns `{ "version": "1.2.3" }`. Lower bandwidth, but requires a server endpoint.
|
|
883
949
|
|
|
884
950
|
**Auto-reload on version change:**
|
|
@@ -910,6 +976,266 @@ Additionally, if the tab is hidden or the window is unfocused when `startVersion
|
|
|
910
976
|
|
|
911
977
|
This is handled transparently - no configuration needed.
|
|
912
978
|
|
|
979
|
+
### Spinner Overlay
|
|
980
|
+
|
|
981
|
+
spa-guard injects a full-page loading spinner at build time that covers the page until your app initializes. The spinner is automatically dismissed by `recommendedSetup()`.
|
|
982
|
+
|
|
983
|
+
#### Configuration
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
spaGuardVitePlugin({
|
|
987
|
+
spinner: {
|
|
988
|
+
// All options are optional
|
|
989
|
+
background: "#fff", // Overlay background color (default: "#fff")
|
|
990
|
+
disabled: false, // Disable spinner entirely (default: false)
|
|
991
|
+
content: '<div class="my-spinner"></div>', // Custom spinner HTML (default: SVG circle animation)
|
|
992
|
+
},
|
|
993
|
+
});
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
When `disabled: true`, no spinner HTML is injected into the page, `showSpinner()` is a no-op, and the React `Spinner` component returns `null`.
|
|
997
|
+
|
|
998
|
+
#### Runtime API
|
|
999
|
+
|
|
1000
|
+
The spinner can be controlled programmatically at runtime:
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
import { showSpinner, dismissSpinner, getSpinnerHtml } from "@ovineko/spa-guard/runtime";
|
|
1004
|
+
|
|
1005
|
+
// Show spinner overlay (returns a cleanup function)
|
|
1006
|
+
const cleanup = showSpinner();
|
|
1007
|
+
// or with custom background:
|
|
1008
|
+
const cleanup = showSpinner({ background: "#000" });
|
|
1009
|
+
|
|
1010
|
+
// Dismiss spinner
|
|
1011
|
+
dismissSpinner();
|
|
1012
|
+
|
|
1013
|
+
// Get spinner HTML string (for custom rendering)
|
|
1014
|
+
const html = getSpinnerHtml();
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
The background color can be customized via CSS variable:
|
|
1018
|
+
|
|
1019
|
+
```css
|
|
1020
|
+
:root {
|
|
1021
|
+
--spa-guard-spinner-bg: #1a1a1a;
|
|
1022
|
+
}
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
#### React Component
|
|
1026
|
+
|
|
1027
|
+
```tsx
|
|
1028
|
+
import { Spinner } from "@ovineko/spa-guard/react";
|
|
1029
|
+
|
|
1030
|
+
function LoadingScreen() {
|
|
1031
|
+
return (
|
|
1032
|
+
<div>
|
|
1033
|
+
<Spinner className="my-spinner" />
|
|
1034
|
+
<p>Loading application...</p>
|
|
1035
|
+
</div>
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
The `Spinner` component accepts all standard `div` props (except `children` and `dangerouslySetInnerHTML`). Returns `null` if spinner is disabled.
|
|
1041
|
+
|
|
1042
|
+
### i18n (Internationalization)
|
|
1043
|
+
|
|
1044
|
+
spa-guard supports localized error and loading messages through a meta tag approach. Translations are applied server-side via HTML patching, and the inline script + React components automatically pick them up at runtime.
|
|
1045
|
+
|
|
1046
|
+
#### Built-in Languages
|
|
1047
|
+
|
|
1048
|
+
38 languages are included (see the Supported Languages table above). RTL is supported for Arabic (ar), Hebrew (he), and Persian (fa).
|
|
1049
|
+
|
|
1050
|
+
#### Server-Side Setup
|
|
1051
|
+
|
|
1052
|
+
Use `patchHtmlI18n` in your server middleware to inject translations based on the user's language:
|
|
1053
|
+
|
|
1054
|
+
```typescript
|
|
1055
|
+
import { patchHtmlI18n, matchLang } from "@ovineko/spa-guard/node";
|
|
1056
|
+
|
|
1057
|
+
// In your server middleware (Express, Fastify, etc.)
|
|
1058
|
+
app.get("*", (req, res) => {
|
|
1059
|
+
let html = readIndexHtml();
|
|
1060
|
+
|
|
1061
|
+
// Automatic language detection from Accept-Language header
|
|
1062
|
+
html = patchHtmlI18n({
|
|
1063
|
+
html,
|
|
1064
|
+
acceptLanguage: req.headers["accept-language"],
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
res.send(html);
|
|
1068
|
+
});
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
Or with an explicit language:
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
html = patchHtmlI18n({
|
|
1075
|
+
html,
|
|
1076
|
+
lang: "ko", // Explicit language code
|
|
1077
|
+
});
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
#### Custom Translations
|
|
1081
|
+
|
|
1082
|
+
Override or extend built-in translations:
|
|
1083
|
+
|
|
1084
|
+
```typescript
|
|
1085
|
+
html = patchHtmlI18n({
|
|
1086
|
+
html,
|
|
1087
|
+
acceptLanguage: req.headers["accept-language"],
|
|
1088
|
+
translations: {
|
|
1089
|
+
// Override specific English strings
|
|
1090
|
+
en: {
|
|
1091
|
+
heading: "Oops! Something broke",
|
|
1092
|
+
reload: "Refresh now",
|
|
1093
|
+
},
|
|
1094
|
+
// Add a new language
|
|
1095
|
+
fr: {
|
|
1096
|
+
heading: "Une erreur est survenue",
|
|
1097
|
+
message: "Veuillez actualiser la page.",
|
|
1098
|
+
reload: "Recharger",
|
|
1099
|
+
tryAgain: "Réessayer",
|
|
1100
|
+
loading: "Chargement...",
|
|
1101
|
+
retrying: "Nouvelle tentative",
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
#### Translation Keys
|
|
1108
|
+
|
|
1109
|
+
```typescript
|
|
1110
|
+
interface SpaGuardTranslations {
|
|
1111
|
+
heading: string; // Error heading (e.g., "Something went wrong")
|
|
1112
|
+
message: string; // Error description
|
|
1113
|
+
reload: string; // Reload button text
|
|
1114
|
+
tryAgain: string; // Try again button text
|
|
1115
|
+
loading: string; // Loading heading text
|
|
1116
|
+
retrying: string; // Retry attempt label
|
|
1117
|
+
rtl?: boolean; // Enable RTL direction
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
#### Language Matching
|
|
1122
|
+
|
|
1123
|
+
`matchLang` resolves language codes with fallback logic:
|
|
1124
|
+
|
|
1125
|
+
```typescript
|
|
1126
|
+
import { matchLang } from "@ovineko/spa-guard/node";
|
|
1127
|
+
|
|
1128
|
+
matchLang("ko"); // "ko" (exact match)
|
|
1129
|
+
matchLang("zh-CN"); // "zh" (prefix match)
|
|
1130
|
+
matchLang("fr"); // "en" (fallback)
|
|
1131
|
+
matchLang(undefined); // "en" (default)
|
|
1132
|
+
|
|
1133
|
+
// Accept-Language header parsing with q-values
|
|
1134
|
+
matchLang("ko-KR,ko;q=0.9,en-US;q=0.8"); // "ko"
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
#### Server-Side Caching with Compression
|
|
1138
|
+
|
|
1139
|
+
Use `createHtmlCache` to pre-generate all language variants at startup with pre-compressed payloads (gzip, brotli, zstd). The cache's `get()` returns a ready-to-use response object with body and headers.
|
|
1140
|
+
|
|
1141
|
+
```typescript
|
|
1142
|
+
import { createHtmlCache } from "@ovineko/spa-guard/node";
|
|
1143
|
+
|
|
1144
|
+
// At server startup - pre-generates all language variants with compression
|
|
1145
|
+
const cache = await createHtmlCache({
|
|
1146
|
+
html: readIndexHtml(),
|
|
1147
|
+
// Optional: restrict to specific languages (defaults to all 38 built-in)
|
|
1148
|
+
// languages: ["en", "ko", "ja"],
|
|
1149
|
+
// Optional: custom translations
|
|
1150
|
+
// translations: { fr: { heading: "Une erreur est survenue" } },
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// In your request handler (Express, Fastify, Hono, etc.)
|
|
1154
|
+
app.get("*", (req, res) => {
|
|
1155
|
+
const { body, headers } = cache.get({
|
|
1156
|
+
acceptLanguage: req.headers["accept-language"],
|
|
1157
|
+
acceptEncoding: req.headers["accept-encoding"],
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// Spread the returned headers into the response
|
|
1161
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1162
|
+
res.setHeader(key, value);
|
|
1163
|
+
}
|
|
1164
|
+
res.end(body);
|
|
1165
|
+
});
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
The returned headers include `Content-Type`, `Content-Language`, `Content-Encoding` (when compressed), `ETag`, and `Vary`. ETag is derived from `__SPA_GUARD_VERSION__` in the HTML (falls back to sha256 prefix when not found).
|
|
1169
|
+
|
|
1170
|
+
You can also override language explicitly:
|
|
1171
|
+
|
|
1172
|
+
```typescript
|
|
1173
|
+
const { body, headers } = cache.get({
|
|
1174
|
+
lang: "ko", // Explicit language (takes priority over acceptLanguage)
|
|
1175
|
+
acceptEncoding: req.headers["accept-encoding"],
|
|
1176
|
+
});
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
#### How It Works
|
|
1180
|
+
|
|
1181
|
+
1. Server calls `patchHtmlI18n()` → injects `<meta name="spa-guard-i18n" content="...">` into `<head>` and updates `<html lang="...">`
|
|
1182
|
+
2. Client-side inline script reads the meta tag via `getI18n()`
|
|
1183
|
+
3. Translations are applied to elements using `data-spa-guard-content` and `data-spa-guard-action` attributes
|
|
1184
|
+
4. RTL languages automatically set `direction: rtl` on the container
|
|
1185
|
+
5. English without custom translations is a no-op (returns HTML unchanged)
|
|
1186
|
+
|
|
1187
|
+
### Data Attribute Contract
|
|
1188
|
+
|
|
1189
|
+
Custom HTML templates for error and loading states use `data-spa-guard-*` attributes for content manipulation. This makes templates robust against structural changes — spa-guard finds elements by data attributes rather than relying on specific HTML structure.
|
|
1190
|
+
|
|
1191
|
+
#### Content Attributes
|
|
1192
|
+
|
|
1193
|
+
Elements with `data-spa-guard-content="<key>"` have their `textContent` replaced with the corresponding translation:
|
|
1194
|
+
|
|
1195
|
+
| Attribute Value | Translation Key | Default Text |
|
|
1196
|
+
| --------------- | ------------------------ | ------------------------------------ |
|
|
1197
|
+
| `heading` | `heading` | Something went wrong |
|
|
1198
|
+
| `message` | `message` | Please refresh the page to continue. |
|
|
1199
|
+
| `loading` | `loading` | Loading... |
|
|
1200
|
+
| `retrying` | `retrying` | Retry attempt |
|
|
1201
|
+
| `attempt` | _(set programmatically)_ | Attempt number |
|
|
1202
|
+
|
|
1203
|
+
#### Action Attributes
|
|
1204
|
+
|
|
1205
|
+
Elements with `data-spa-guard-action="<action>"` are interactive buttons:
|
|
1206
|
+
|
|
1207
|
+
| Attribute Value | Translation Key | Default Text |
|
|
1208
|
+
| --------------- | --------------- | ------------ |
|
|
1209
|
+
| `try-again` | `tryAgain` | Try again |
|
|
1210
|
+
| `reload` | `reload` | Reload page |
|
|
1211
|
+
|
|
1212
|
+
#### Section Attributes
|
|
1213
|
+
|
|
1214
|
+
Elements with `data-spa-guard-section="<name>"` control visibility:
|
|
1215
|
+
|
|
1216
|
+
| Attribute Value | Purpose |
|
|
1217
|
+
| --------------- | ------------------------------------------------ |
|
|
1218
|
+
| `retrying` | Retry information section (shown during retries) |
|
|
1219
|
+
|
|
1220
|
+
#### Spinner Attribute
|
|
1221
|
+
|
|
1222
|
+
`[data-spa-guard-spinner]` — Container where the spinner SVG/HTML is injected in the loading template.
|
|
1223
|
+
|
|
1224
|
+
#### Custom Template Example
|
|
1225
|
+
|
|
1226
|
+
```html
|
|
1227
|
+
<div>
|
|
1228
|
+
<h1 data-spa-guard-content="heading">Something went wrong</h1>
|
|
1229
|
+
<p data-spa-guard-content="message">Please refresh the page.</p>
|
|
1230
|
+
<button data-spa-guard-action="try-again">Try again</button>
|
|
1231
|
+
<button data-spa-guard-action="reload" onclick="location.reload()">Reload page</button>
|
|
1232
|
+
<div data-spa-guard-section="retrying" style="display:none">
|
|
1233
|
+
<span data-spa-guard-content="retrying">Retry attempt</span>
|
|
1234
|
+
<span data-spa-guard-content="attempt">0</span>
|
|
1235
|
+
</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
```
|
|
1238
|
+
|
|
913
1239
|
### Retry Control
|
|
914
1240
|
|
|
915
1241
|
spa-guard allows your SPA to take over error handling from the inline script by disabling the default retry behavior:
|
|
@@ -1165,6 +1491,12 @@ interface Options {
|
|
|
1165
1491
|
retryDelays?: number[]; // Delays in ms for lazy import retries (default: [1000, 2000])
|
|
1166
1492
|
callReloadOnFailure?: boolean; // Trigger page reload after all retries fail (default: true)
|
|
1167
1493
|
};
|
|
1494
|
+
|
|
1495
|
+
spinner?: {
|
|
1496
|
+
content?: string; // Custom spinner HTML element (default: SVG circle animation)
|
|
1497
|
+
disabled?: boolean; // Disable spinner entirely (default: false)
|
|
1498
|
+
background?: string; // Overlay background color (default: "#fff")
|
|
1499
|
+
};
|
|
1168
1500
|
}
|
|
1169
1501
|
```
|
|
1170
1502
|
|
|
@@ -1225,6 +1557,9 @@ From `@ovineko/spa-guard/runtime`:
|
|
|
1225
1557
|
- `subscribeToState(callback)` - Subscribe to state changes, returns unsubscribe function
|
|
1226
1558
|
- `startVersionCheck()` - Start periodic version polling
|
|
1227
1559
|
- `stopVersionCheck()` - Stop version polling
|
|
1560
|
+
- `showSpinner(options?)` - Show spinner overlay, returns cleanup function
|
|
1561
|
+
- `dismissSpinner()` - Remove spinner overlay
|
|
1562
|
+
- `getSpinnerHtml(backgroundOverride?)` - Get spinner HTML string
|
|
1228
1563
|
- `ForceRetryError` - Error class that triggers automatic retry when thrown
|
|
1229
1564
|
- `SpaGuardState` - TypeScript type for state object
|
|
1230
1565
|
- `RecommendedSetupOptions` - TypeScript type for recommendedSetup overrides
|
|
@@ -1452,21 +1787,23 @@ interface Options {
|
|
|
1452
1787
|
|
|
1453
1788
|
## Module Exports
|
|
1454
1789
|
|
|
1455
|
-
spa-guard provides
|
|
1456
|
-
|
|
1457
|
-
| Export | Description
|
|
1458
|
-
| ------------------------ |
|
|
1459
|
-
| `.` | Core functionality (events, listen, options, retry control, ForceRetryError, BeaconError)
|
|
1460
|
-
| `./schema` | BeaconSchema type definitions
|
|
1461
|
-
| `./schema/parse` | Beacon parsing utilities
|
|
1462
|
-
| `./runtime` | Runtime state management, subscriptions, and ForceRetryError
|
|
1463
|
-
| `./react` | React hooks and components (useSpaGuardState, useSPAGuardEvents, useSPAGuardChunkError, lazyWithRetry, DebugSyncErrorTrigger) | `react@^19` |
|
|
1464
|
-
| `./runtime/debug` | Debug panel factory (`createDebugger`) - framework-agnostic vanilla JS
|
|
1465
|
-
| `./react-router` | React Router error boundary (ErrorBoundaryReactRouter)
|
|
1466
|
-
| `./fastify` | Fastify server plugin and BeaconError
|
|
1467
|
-
| `./vite-plugin` | Vite build plugin
|
|
1468
|
-
| `./react-error-boundary` | React error boundary component (ErrorBoundary)
|
|
1469
|
-
| `./eslint` | ESLint plugin with `configs.recommended` preset (`no-direct-error-boundary`, `no-direct-lazy`)
|
|
1790
|
+
spa-guard provides 13 export entry points:
|
|
1791
|
+
|
|
1792
|
+
| Export | Description | Peer Dependencies |
|
|
1793
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
1794
|
+
| `.` | Core functionality (events, listen, options, retry control, ForceRetryError, BeaconError) | None |
|
|
1795
|
+
| `./schema` | BeaconSchema type definitions | `typebox@^1` |
|
|
1796
|
+
| `./schema/parse` | Beacon parsing utilities | `typebox@^1` |
|
|
1797
|
+
| `./runtime` | Runtime state management, subscriptions, spinner API, and ForceRetryError | None |
|
|
1798
|
+
| `./react` | React hooks and components (useSpaGuardState, useSPAGuardEvents, useSPAGuardChunkError, lazyWithRetry, DebugSyncErrorTrigger, Spinner) | `react@^19` |
|
|
1799
|
+
| `./runtime/debug` | Debug panel factory (`createDebugger`) - framework-agnostic vanilla JS | None |
|
|
1800
|
+
| `./react-router` | React Router error boundary (ErrorBoundaryReactRouter) | `react@^19`, `react-router@^7` |
|
|
1801
|
+
| `./fastify` | Fastify server plugin and BeaconError | `fastify@^5 \|\| ^4`, `fastify-plugin@^5 \|\| ^4` |
|
|
1802
|
+
| `./vite-plugin` | Vite build plugin | `vite@^8 \|\| ^7` |
|
|
1803
|
+
| `./react-error-boundary` | React error boundary component (ErrorBoundary) | `react@^19` |
|
|
1804
|
+
| `./eslint` | ESLint plugin with `configs.recommended` preset (`no-direct-error-boundary`, `no-direct-lazy`) | `eslint@^9 \|\| ^10` (optional) |
|
|
1805
|
+
| `./i18n` | Translation types and utilities (SpaGuardTranslations, translations, matchLang) | None |
|
|
1806
|
+
| `./node` | Server-side utilities (patchHtmlI18n, createHtmlCache, escapeAttr, matchLang, translations) | `happy-dom@^20` |
|
|
1470
1807
|
|
|
1471
1808
|
**Import examples:**
|
|
1472
1809
|
|
|
@@ -1517,12 +1854,25 @@ import { fastifySPAGuard, BeaconError } from "@ovineko/spa-guard/fastify";
|
|
|
1517
1854
|
|
|
1518
1855
|
// ESLint plugin
|
|
1519
1856
|
import spaGuardEslint from "@ovineko/spa-guard/eslint";
|
|
1857
|
+
|
|
1858
|
+
// i18n (translations and language matching)
|
|
1859
|
+
import type { SpaGuardTranslations } from "@ovineko/spa-guard/i18n";
|
|
1860
|
+
import { translations, matchLang } from "@ovineko/spa-guard/i18n";
|
|
1861
|
+
|
|
1862
|
+
// Server utilities (i18n HTML patching + caching)
|
|
1863
|
+
import {
|
|
1864
|
+
patchHtmlI18n,
|
|
1865
|
+
createHtmlCache,
|
|
1866
|
+
escapeAttr,
|
|
1867
|
+
translations,
|
|
1868
|
+
matchLang,
|
|
1869
|
+
} from "@ovineko/spa-guard/node";
|
|
1520
1870
|
```
|
|
1521
1871
|
|
|
1522
1872
|
## Build Sizes
|
|
1523
1873
|
|
|
1524
|
-
- **Production:** `dist-inline/index.js` ~
|
|
1525
|
-
- **Trace:** `dist-inline-trace/index.js` ~
|
|
1874
|
+
- **Production:** `dist-inline/index.js` ~12 KB minified (Terser)
|
|
1875
|
+
- **Trace:** `dist-inline-trace/index.js` ~17 KB minified (Terser)
|
|
1526
1876
|
- **Main library:** `dist/` varies by export
|
|
1527
1877
|
|
|
1528
1878
|
## Advanced Usage
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultSpinnerHtml,
|
|
3
|
+
getOptions
|
|
4
|
+
} from "./chunk-KUC22DET.js";
|
|
5
|
+
|
|
6
|
+
// src/common/spinner.ts
|
|
7
|
+
var SPINNER_ID = "__spa-guard-spinner";
|
|
8
|
+
var defaultSpinnerSvg = defaultSpinnerHtml;
|
|
9
|
+
var savedOverflow = null;
|
|
10
|
+
function dismissSpinner() {
|
|
11
|
+
if (typeof document === "undefined") {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const el = document.getElementById(SPINNER_ID);
|
|
15
|
+
if (el) {
|
|
16
|
+
el.remove();
|
|
17
|
+
}
|
|
18
|
+
if (savedOverflow !== null) {
|
|
19
|
+
document.body.style.overflow = savedOverflow;
|
|
20
|
+
savedOverflow = null;
|
|
21
|
+
} else if (el) {
|
|
22
|
+
document.body.style.overflow = "";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function getSpinnerHtml(backgroundOverride) {
|
|
26
|
+
const opts = getOptions();
|
|
27
|
+
if (opts.spinner?.disabled) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
const spinnerContent = opts.spinner?.content ?? defaultSpinnerSvg;
|
|
31
|
+
const bg = backgroundOverride ?? opts.spinner?.background ?? "#fff";
|
|
32
|
+
return `<div id="${SPINNER_ID}" style="position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;background:var(--spa-guard-spinner-bg,${bg})">${spinnerContent}</div>`;
|
|
33
|
+
}
|
|
34
|
+
function showSpinner(options) {
|
|
35
|
+
const opts = getOptions();
|
|
36
|
+
if (opts.spinner?.disabled) {
|
|
37
|
+
return () => {
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const existing = document.getElementById(SPINNER_ID);
|
|
41
|
+
if (existing) {
|
|
42
|
+
existing.remove();
|
|
43
|
+
}
|
|
44
|
+
const html = getSpinnerHtml(options?.background);
|
|
45
|
+
if (!html) {
|
|
46
|
+
return () => {
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const wrapper = document.createElement("div");
|
|
50
|
+
wrapper.innerHTML = html;
|
|
51
|
+
const overlay = wrapper.firstElementChild;
|
|
52
|
+
if (!existing) {
|
|
53
|
+
savedOverflow = document.body.style.overflow;
|
|
54
|
+
}
|
|
55
|
+
document.body.style.overflow = "hidden";
|
|
56
|
+
document.body.append(overlay);
|
|
57
|
+
return () => dismissSpinner();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
SPINNER_ID,
|
|
62
|
+
defaultSpinnerSvg,
|
|
63
|
+
dismissSpinner,
|
|
64
|
+
getSpinnerHtml,
|
|
65
|
+
showSpinner
|
|
66
|
+
};
|