@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.
Files changed (39) hide show
  1. package/README.md +377 -27
  2. package/dist/chunk-6C6OARM7.js +66 -0
  3. package/dist/chunk-HLRUA3T2.js +349 -0
  4. package/dist/chunk-KUC22DET.js +105 -0
  5. package/dist/{chunk-WKH2B2XS.js → chunk-MCR7F3CG.js} +1 -1
  6. package/dist/{chunk-BLVJHZST.js → chunk-MDTTXXMB.js} +2 -4
  7. package/dist/{chunk-4N3XJ7VS.js → chunk-N4BBFG52.js} +12 -5
  8. package/dist/{chunk-U2DZSTRT.js → chunk-R2J3MNUG.js} +99 -3
  9. package/dist/{chunk-D5NHZ5BD.js → chunk-RR4LFTYF.js} +69 -22
  10. package/dist/chunk-SNM6AOFL.js +16 -0
  11. package/dist/{chunk-XOCUWXQL.js → chunk-Y4SM5PUO.js} +18 -4
  12. package/dist/common/DefaultErrorFallback.d.ts +1 -1
  13. package/dist/common/events/types.d.ts +0 -1
  14. package/dist/common/html.generated.d.ts +3 -0
  15. package/dist/common/i18n.d.ts +15 -0
  16. package/dist/common/index.js +3 -3
  17. package/dist/common/options.d.ts +24 -0
  18. package/dist/common/parseVersion.d.ts +9 -0
  19. package/dist/common/spinner.d.ts +7 -0
  20. package/dist/fastify/index.js +1 -1
  21. package/dist/i18n/index.d.ts +21 -0
  22. package/dist/i18n/index.js +9 -0
  23. package/dist/i18n/translations.d.ts +2 -0
  24. package/dist/node/index.d.ts +58 -0
  25. package/dist/node/index.js +130 -0
  26. package/dist/react/Spinner.d.ts +9 -0
  27. package/dist/react/index.d.ts +1 -0
  28. package/dist/react/index.js +10 -6
  29. package/dist/react-error-boundary/index.js +9 -7
  30. package/dist/react-router/index.js +9 -7
  31. package/dist/runtime/debug/index.js +2 -2
  32. package/dist/runtime/index.d.ts +1 -0
  33. package/dist/runtime/index.js +13 -4
  34. package/dist/vite-plugin/index.js +47 -12
  35. package/dist-inline/index.js +1 -1
  36. package/dist-inline-trace/index.js +1 -1
  37. package/package.json +15 -2
  38. package/dist/chunk-UXLGA3TG.js +0 -95
  39. 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 (~8.3 KB) and trace minified (~13.3 KB) builds for different environments
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:debug:watch
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. Script registers error listeners immediately on page load
303
- 5. Captures errors even if main application bundle fails
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 minimal HTML templates:
713
+ Both error boundaries use `DefaultErrorFallback` by default, which uses two styled HTML templates:
648
714
 
649
- - **Error state:** Heading, message paragraph, "Try again" button (non-chunk errors), "Reload page" button, error ID. No colors, no custom fonts, no animations - plain default browser styling.
650
- - **Loading/retrying state:** Centered "Loading..." text with retry attempt counter. No spinner, no animation.
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__`. No extra server endpoint needed.
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 11 export entry points:
1456
-
1457
- | Export | Description | Peer Dependencies |
1458
- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
1459
- | `.` | Core functionality (events, listen, options, retry control, ForceRetryError, BeaconError) | None |
1460
- | `./schema` | BeaconSchema type definitions | `typebox@^1` |
1461
- | `./schema/parse` | Beacon parsing utilities | `typebox@^1` |
1462
- | `./runtime` | Runtime state management, subscriptions, and ForceRetryError | None |
1463
- | `./react` | React hooks and components (useSpaGuardState, useSPAGuardEvents, useSPAGuardChunkError, lazyWithRetry, DebugSyncErrorTrigger) | `react@^19` |
1464
- | `./runtime/debug` | Debug panel factory (`createDebugger`) - framework-agnostic vanilla JS | None |
1465
- | `./react-router` | React Router error boundary (ErrorBoundaryReactRouter) | `react@^19`, `react-router@^7` |
1466
- | `./fastify` | Fastify server plugin and BeaconError | `fastify@^5 \|\| ^4`, `fastify-plugin@^5 \|\| ^4` |
1467
- | `./vite-plugin` | Vite build plugin | `vite@^8 \|\| ^7` |
1468
- | `./react-error-boundary` | React error boundary component (ErrorBoundary) | `react@^19` |
1469
- | `./eslint` | ESLint plugin with `configs.recommended` preset (`no-direct-error-boundary`, `no-direct-lazy`) | `eslint@^9 \|\| ^10` (optional) |
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` ~8.9 KB minified (Terser)
1525
- - **Trace:** `dist-inline-trace/index.js` ~13.8 KB minified (Terser)
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
+ };