@ovineko/spa-guard 0.0.2-alpha-1 → 0.0.4

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 (67) hide show
  1. package/README.md +22 -173
  2. package/dist/ForceRetryError-BWLv3UVK.d.mts +6 -0
  3. package/dist/_internal.d.ts +149 -20
  4. package/dist/_internal.js +113 -170
  5. package/dist/chunk-CfYAbeIz.mjs +13 -0
  6. package/dist/common/index.d.ts +29 -9
  7. package/dist/common/index.js +47 -83
  8. package/dist/errorDispatchers-Cl_pa0DT.mjs +105 -0
  9. package/dist/i18n/index.d.ts +2 -21
  10. package/dist/i18n/index.js +344 -341
  11. package/dist/index-DL8CfPXg.d.mts +26 -0
  12. package/dist/index-rPxPv6iu.d.mts +16 -0
  13. package/dist/logger-Cp1Eyk6S.mjs +534 -0
  14. package/dist/retryOrchestrator-DNGIHV2M.mjs +758 -0
  15. package/dist/retryOrchestrator-mn9XcIVu.d.mts +257 -0
  16. package/dist/runtime/debug/index.d.ts +6 -4
  17. package/dist/runtime/debug/index.js +218 -238
  18. package/dist/runtime/index.d.ts +66 -8
  19. package/dist/runtime/index.js +279 -367
  20. package/dist/schema/index.d.ts +2 -13
  21. package/dist/schema/index.js +1 -0
  22. package/dist/schema/parse.d.ts +6 -2
  23. package/dist/schema/parse.js +29 -44
  24. package/dist/spinner-BbZVKZ-6.mjs +60 -0
  25. package/dist/spinner-X23gI09z.d.mts +37 -0
  26. package/dist/state-I20jENMD.mjs +93 -0
  27. package/dist/types-DrN8pgyc.d.mts +107 -0
  28. package/package.json +1 -4
  29. package/dist/chunk-3UJ67DPX.js +0 -98
  30. package/dist/chunk-GE63YJOT.js +0 -865
  31. package/dist/chunk-MLKGABMK.js +0 -9
  32. package/dist/chunk-PERG4557.js +0 -74
  33. package/dist/chunk-VZ2DLGXX.js +0 -111
  34. package/dist/chunk-XIFXSNSD.js +0 -678
  35. package/dist/common/checkVersion.d.ts +0 -5
  36. package/dist/common/constants.d.ts +0 -14
  37. package/dist/common/errors/BeaconError.d.ts +0 -12
  38. package/dist/common/errors/ForceRetryError.d.ts +0 -5
  39. package/dist/common/events/index.d.ts +0 -2
  40. package/dist/common/events/internal.d.ts +0 -13
  41. package/dist/common/events/types.d.ts +0 -104
  42. package/dist/common/fallbackRendering.d.ts +0 -23
  43. package/dist/common/fallbackState.d.ts +0 -3
  44. package/dist/common/handleErrorWithSpaGuard.d.ts +0 -18
  45. package/dist/common/html.generated.d.ts +0 -3
  46. package/dist/common/i18n.d.ts +0 -23
  47. package/dist/common/isChunkError.d.ts +0 -1
  48. package/dist/common/isStaticAssetError.d.ts +0 -3
  49. package/dist/common/lastReloadTime.d.ts +0 -17
  50. package/dist/common/listen/index.d.ts +0 -1
  51. package/dist/common/listen/internal.d.ts +0 -2
  52. package/dist/common/log.d.ts +0 -1
  53. package/dist/common/logger.d.ts +0 -30
  54. package/dist/common/options.d.ts +0 -182
  55. package/dist/common/parseVersion.d.ts +0 -9
  56. package/dist/common/retryImport.d.ts +0 -43
  57. package/dist/common/retryOrchestrator.d.ts +0 -35
  58. package/dist/common/retryState.d.ts +0 -11
  59. package/dist/common/sendBeacon.d.ts +0 -2
  60. package/dist/common/serializeError.d.ts +0 -1
  61. package/dist/common/shouldIgnore.d.ts +0 -13
  62. package/dist/common/spinner.d.ts +0 -8
  63. package/dist/common/staticAssetRecovery.d.ts +0 -2
  64. package/dist/i18n/translations.d.ts +0 -2
  65. package/dist/runtime/debug/errorDispatchers.d.ts +0 -63
  66. package/dist/runtime/recommendedSetup.d.ts +0 -36
  67. package/dist/runtime/state.d.ts +0 -20
@@ -1,13 +1,2 @@
1
- export interface BeaconSchema {
2
- appName?: string;
3
- errorContext?: string;
4
- errorMessage?: string;
5
- errorType?: string;
6
- eventMessage?: string;
7
- eventName?: string;
8
- httpStatus?: number;
9
- retryAttempt?: number;
10
- retryId?: string;
11
- serialized?: string;
12
- url?: string;
13
- }
1
+ import { t as BeaconSchema } from "../index-rPxPv6iu.mjs";
2
+ export { BeaconSchema };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,2 +1,6 @@
1
- import type { BeaconSchema } from ".";
2
- export declare function parseBeacon(data: unknown): BeaconSchema;
1
+ import { t as BeaconSchema } from "../index-rPxPv6iu.mjs";
2
+
3
+ //#region src/schema/parse.d.ts
4
+ declare function parseBeacon(data: unknown): BeaconSchema;
5
+ //#endregion
6
+ export { parseBeacon };
@@ -1,47 +1,32 @@
1
- import "../chunk-MLKGABMK.js";
2
-
3
- // src/schema/parse.ts
4
- var STRING_FIELDS = [
5
- "appName",
6
- "errorContext",
7
- "errorMessage",
8
- "errorType",
9
- "eventMessage",
10
- "eventName",
11
- "retryId",
12
- "serialized",
13
- "url"
1
+ //#region src/schema/parse.ts
2
+ const STRING_FIELDS = [
3
+ "appName",
4
+ "errorContext",
5
+ "errorMessage",
6
+ "errorType",
7
+ "eventMessage",
8
+ "eventName",
9
+ "retryId",
10
+ "serialized",
11
+ "url"
14
12
  ];
15
- var MAX_STRING_FIELD_LENGTH = 500;
16
- var MAX_SERIALIZED_LENGTH = 1e4;
13
+ const MAX_STRING_FIELD_LENGTH = 500;
14
+ const MAX_SERIALIZED_LENGTH = 1e4;
17
15
  function parseBeacon(data) {
18
- if (!data || typeof data !== "object" || Array.isArray(data)) {
19
- throw new Error("Invalid beacon");
20
- }
21
- const d = data;
22
- const result = {};
23
- for (const field of STRING_FIELDS) {
24
- if (field in d) {
25
- if (typeof d[field] !== "string") {
26
- throw new TypeError(`Beacon validation failed: ${field} must be a string`);
27
- }
28
- const maxLen = field === "serialized" ? MAX_SERIALIZED_LENGTH : MAX_STRING_FIELD_LENGTH;
29
- if (d[field].length > maxLen) {
30
- throw new TypeError(`Beacon validation failed: ${field} exceeds maximum length`);
31
- }
32
- result[field] = d[field];
33
- }
34
- }
35
- for (const field of ["retryAttempt", "httpStatus"]) {
36
- if (field in d) {
37
- if (typeof d[field] !== "number" || !Number.isFinite(d[field])) {
38
- throw new TypeError(`Beacon validation failed: ${field} must be a finite number`);
39
- }
40
- result[field] = d[field];
41
- }
42
- }
43
- return result;
16
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("Invalid beacon");
17
+ const d = data;
18
+ const result = {};
19
+ for (const field of STRING_FIELDS) if (field in d) {
20
+ if (typeof d[field] !== "string") throw new TypeError(`Beacon validation failed: ${field} must be a string`);
21
+ const maxLen = field === "serialized" ? MAX_SERIALIZED_LENGTH : MAX_STRING_FIELD_LENGTH;
22
+ if (d[field].length > maxLen) throw new TypeError(`Beacon validation failed: ${field} exceeds maximum length`);
23
+ result[field] = d[field];
24
+ }
25
+ for (const field of ["retryAttempt", "httpStatus"]) if (field in d) {
26
+ if (typeof d[field] !== "number" || !Number.isFinite(d[field])) throw new TypeError(`Beacon validation failed: ${field} must be a finite number`);
27
+ result[field] = d[field];
28
+ }
29
+ return result;
44
30
  }
45
- export {
46
- parseBeacon
47
- };
31
+ //#endregion
32
+ export { parseBeacon };
@@ -0,0 +1,60 @@
1
+ import { F as spinnerStateWindowKey, b as defaultSpinnerHtml, g as getOptions } from "./retryOrchestrator-DNGIHV2M.mjs";
2
+ //#region src/common/parseVersion.ts
3
+ /**
4
+ * Extract the SPA Guard version string from an HTML document.
5
+ *
6
+ * Tries the new `__SPA_GUARD_VERSION__` format first, then falls back
7
+ * to the legacy `__SPA_GUARD_OPTIONS__` object with a `version` key.
8
+ *
9
+ * Returns `null` when neither marker is found.
10
+ */
11
+ function extractVersionFromHtml(html) {
12
+ const collapsed = html.replaceAll(/[\r\n]+/g, "");
13
+ const versionMatch = collapsed.match(/__SPA_GUARD_VERSION__\s*=\s*"([^"]+)"/);
14
+ if (versionMatch?.[1]) return versionMatch[1];
15
+ const markerIdx = collapsed.indexOf("__SPA_GUARD_OPTIONS__");
16
+ if (markerIdx === -1) return null;
17
+ return collapsed.slice(markerIdx, markerIdx + 1e3).match(/__SPA_GUARD_OPTIONS__\s*=\s*\{.*?"?version"?\s*:\s*"([^"]+)"/)?.[1] ?? null;
18
+ }
19
+ //#endregion
20
+ //#region src/common/spinner.ts
21
+ const SPINNER_ID = "__spa-guard-spinner";
22
+ const defaultSpinnerSvg = defaultSpinnerHtml;
23
+ const getState = () => {
24
+ if (!globalThis.window[spinnerStateWindowKey]) globalThis.window[spinnerStateWindowKey] = { savedOverflow: null };
25
+ return globalThis.window[spinnerStateWindowKey];
26
+ };
27
+ function dismissSpinner() {
28
+ if (typeof document === "undefined") return;
29
+ const el = document.getElementById(SPINNER_ID);
30
+ if (el) el.remove();
31
+ const state = getState();
32
+ if (state.savedOverflow !== null) {
33
+ document.body.style.overflow = state.savedOverflow;
34
+ state.savedOverflow = null;
35
+ } else if (el) document.body.style.overflow = "";
36
+ }
37
+ const sanitizeCssValue = (value) => value.replaceAll(/["'<>\\{};\n]/g, "");
38
+ function getSpinnerHtml(backgroundOverride) {
39
+ const opts = getOptions();
40
+ if (opts.html?.spinner?.disabled) return "";
41
+ const spinnerContent = opts.html?.spinner?.content ?? defaultSpinnerSvg;
42
+ 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,${sanitizeCssValue(backgroundOverride ?? opts.html?.spinner?.background ?? "#fff")})">${spinnerContent}</div>`;
43
+ }
44
+ function showSpinner(options) {
45
+ if (typeof document === "undefined") return () => {};
46
+ if (getOptions().html?.spinner?.disabled) return () => {};
47
+ const existing = document.getElementById(SPINNER_ID);
48
+ if (existing) existing.remove();
49
+ const html = getSpinnerHtml(options?.background);
50
+ if (!html) return () => {};
51
+ const wrapper = document.createElement("div");
52
+ wrapper.innerHTML = html;
53
+ const overlay = wrapper.firstElementChild;
54
+ if (!existing) getState().savedOverflow = document.body.style.overflow;
55
+ document.body.style.overflow = "hidden";
56
+ document.body.append(overlay);
57
+ return () => dismissSpinner();
58
+ }
59
+ //#endregion
60
+ export { sanitizeCssValue as a, getSpinnerHtml as i, defaultSpinnerSvg as n, showSpinner as o, dismissSpinner as r, extractVersionFromHtml as s, SPINNER_ID as t };
@@ -0,0 +1,37 @@
1
+ import { t as SpaGuardTranslations } from "./index-DL8CfPXg.mjs";
2
+
3
+ //#region src/common/i18n.d.ts
4
+ /**
5
+ * Apply i18n translations to a virtual container's data-attributed elements.
6
+ * Patches `[data-spa-guard-content]` and `[data-spa-guard-action]` elements,
7
+ * and applies RTL direction if needed.
8
+ *
9
+ * Must be called on a virtual (detached) container BEFORE inserting into DOM
10
+ * to avoid flash of untranslated content.
11
+ */
12
+ declare function applyI18n(container: HTMLElement, t: SpaGuardTranslations): void;
13
+ /**
14
+ * Read i18n translations from the `<meta name="spa-guard-i18n">` tag.
15
+ * Returns parsed translations or null if the tag is absent or malformed.
16
+ */
17
+ declare function getI18n(): null | SpaGuardTranslations;
18
+ /**
19
+ * Write i18n translations to the `<meta name="spa-guard-i18n">` tag.
20
+ * Creates the tag if it doesn't exist, or updates it if it does.
21
+ *
22
+ * Use this at runtime to dynamically patch the inline fallback/loading UI
23
+ * translations without server-side rendering.
24
+ */
25
+ declare function setTranslations(translations: SpaGuardTranslations): void;
26
+ //#endregion
27
+ //#region src/common/spinner.d.ts
28
+ declare const SPINNER_ID = "__spa-guard-spinner";
29
+ declare const defaultSpinnerSvg = "<svg width=\"40\" height=\"40\" viewBox=\"0 0 40 40\" style=\"animation:spa-guard-spin .8s linear infinite\"><circle cx=\"20\" cy=\"20\" r=\"16\" fill=\"none\" stroke=\"#e8e8e8\" stroke-width=\"3\"/><circle cx=\"20\" cy=\"20\" r=\"16\" fill=\"none\" stroke=\"#666\" stroke-width=\"3\" stroke-dasharray=\"80\" stroke-dashoffset=\"60\" stroke-linecap=\"round\"/></svg><style>@keyframes spa-guard-spin{to{transform:rotate(360deg)}}</style>";
30
+ declare function dismissSpinner(): void;
31
+ declare const sanitizeCssValue: (value: string) => string;
32
+ declare function getSpinnerHtml(backgroundOverride?: string): string;
33
+ declare function showSpinner(options?: {
34
+ background?: string;
35
+ }): () => void;
36
+ //#endregion
37
+ export { sanitizeCssValue as a, getI18n as c, getSpinnerHtml as i, setTranslations as l, defaultSpinnerSvg as n, showSpinner as o, dismissSpinner as r, applyI18n as s, SPINNER_ID as t };
@@ -0,0 +1,93 @@
1
+ import { M as subscribe, h as getRetryStateFromUrl, p as getRetryAttemptFromUrl, u as getLastRetryResetInfo } from "./retryOrchestrator-DNGIHV2M.mjs";
2
+ //#region src/runtime/state.ts
3
+ const getInitialStateFromUrl = () => {
4
+ const resetInfo = getLastRetryResetInfo();
5
+ const resetInfoSpread = {
6
+ ...resetInfo?.previousRetryId !== void 0 && { lastResetRetryId: resetInfo.previousRetryId },
7
+ ...resetInfo?.timestamp !== void 0 && { lastRetryResetTime: resetInfo.timestamp }
8
+ };
9
+ if (globalThis.window === void 0) return {
10
+ currentAttempt: 0,
11
+ isFallbackShown: false,
12
+ isWaiting: false,
13
+ ...resetInfoSpread
14
+ };
15
+ const retryState = getRetryStateFromUrl();
16
+ if (!retryState) {
17
+ const attempt = getRetryAttemptFromUrl();
18
+ if (attempt !== null) return {
19
+ currentAttempt: attempt,
20
+ isFallbackShown: false,
21
+ isWaiting: false,
22
+ ...resetInfoSpread
23
+ };
24
+ return {
25
+ currentAttempt: 0,
26
+ isFallbackShown: false,
27
+ isWaiting: false,
28
+ ...resetInfoSpread
29
+ };
30
+ }
31
+ return {
32
+ currentAttempt: retryState.retryAttempt,
33
+ isFallbackShown: false,
34
+ isWaiting: false,
35
+ ...resetInfoSpread
36
+ };
37
+ };
38
+ let currentState = getInitialStateFromUrl();
39
+ const stateSubscribers = /* @__PURE__ */ new Set();
40
+ const updateState = (nextState) => {
41
+ currentState = nextState;
42
+ stateSubscribers.forEach((cb) => {
43
+ try {
44
+ cb(currentState);
45
+ } catch {}
46
+ });
47
+ };
48
+ subscribe((event) => {
49
+ switch (event.name) {
50
+ case "fallback-ui-shown":
51
+ updateState({
52
+ ...currentState,
53
+ isFallbackShown: true
54
+ });
55
+ break;
56
+ case "retry-attempt":
57
+ updateState({
58
+ ...currentState,
59
+ currentAttempt: event.attempt,
60
+ isFallbackShown: false,
61
+ isWaiting: true
62
+ });
63
+ break;
64
+ case "retry-exhausted":
65
+ updateState({
66
+ ...currentState,
67
+ currentAttempt: event.finalAttempt,
68
+ isFallbackShown: false,
69
+ isWaiting: false
70
+ });
71
+ break;
72
+ case "retry-reset":
73
+ updateState({
74
+ ...currentState,
75
+ currentAttempt: 0,
76
+ isFallbackShown: false,
77
+ isWaiting: false,
78
+ lastResetRetryId: event.previousRetryId,
79
+ lastRetryResetTime: Date.now()
80
+ });
81
+ break;
82
+ }
83
+ });
84
+ const getState = () => currentState;
85
+ const subscribeToState = (cb) => {
86
+ cb(currentState);
87
+ stateSubscribers.add(cb);
88
+ return () => {
89
+ stateSubscribers.delete(cb);
90
+ };
91
+ };
92
+ //#endregion
93
+ export { subscribeToState as n, getState as t };
@@ -0,0 +1,107 @@
1
+ //#region src/common/events/types.d.ts
2
+ interface EmitOptions {
3
+ silent?: boolean;
4
+ }
5
+ interface InternalConfig {
6
+ defaultRetryEnabled: boolean;
7
+ initialized: boolean;
8
+ }
9
+ type SPAGuardEvent = (SPAGuardEventChunkError & {
10
+ name: "chunk-error";
11
+ }) | (SPAGuardEventFallbackUINotRendered & {
12
+ name: "fallback-ui-not-rendered";
13
+ }) | (SPAGuardEventFallbackUIShown & {
14
+ name: "fallback-ui-shown";
15
+ }) | (SPAGuardEventLazyRetryAttempt & {
16
+ name: "lazy-retry-attempt";
17
+ }) | (SPAGuardEventLazyRetryExhausted & {
18
+ name: "lazy-retry-exhausted";
19
+ }) | (SPAGuardEventLazyRetryStart & {
20
+ name: "lazy-retry-start";
21
+ }) | (SPAGuardEventLazyRetrySuccess & {
22
+ name: "lazy-retry-success";
23
+ }) | (SPAGuardEventRetryAttempt & {
24
+ name: "retry-attempt";
25
+ }) | (SPAGuardEventRetryExhausted & {
26
+ name: "retry-exhausted";
27
+ }) | (SPAGuardEventRetryReset & {
28
+ name: "retry-reset";
29
+ }) | (SPAGuardEventStaticAssetLoadFailed & {
30
+ name: "static-asset-load-failed";
31
+ });
32
+ interface SPAGuardEventChunkError {
33
+ error: unknown;
34
+ isRetrying: boolean;
35
+ name: "chunk-error";
36
+ }
37
+ interface SPAGuardEventFallbackUINotRendered {
38
+ name: "fallback-ui-not-rendered";
39
+ reason: "no-html-configured" | "target-not-found";
40
+ selector?: string;
41
+ }
42
+ interface SPAGuardEventFallbackUIShown {
43
+ name: "fallback-ui-shown";
44
+ }
45
+ /** Emitted before each module-level retry attempt initiated by retryImport. */
46
+ interface SPAGuardEventLazyRetryAttempt {
47
+ /** 1-based index of the current retry attempt. */
48
+ attempt: number;
49
+ /** Delay in milliseconds before this retry attempt. */
50
+ delay: number;
51
+ /** The error that caused the previous attempt to fail. */
52
+ error?: unknown;
53
+ name: "lazy-retry-attempt";
54
+ /** Total number of attempts including the initial try (delays.length + 1). */
55
+ totalAttempts: number;
56
+ }
57
+ /** Emitted when all module-level retry attempts are exhausted. */
58
+ interface SPAGuardEventLazyRetryExhausted {
59
+ /** The final error after all attempts failed. */
60
+ error: unknown;
61
+ name: "lazy-retry-exhausted";
62
+ /** Total number of attempts that were made (delays.length + 1). */
63
+ totalAttempts: number;
64
+ /** Whether triggerRetry() will be called after this event. */
65
+ willReload: boolean;
66
+ }
67
+ /** Emitted once before the first import attempt, when retries are configured. */
68
+ interface SPAGuardEventLazyRetryStart {
69
+ name: "lazy-retry-start";
70
+ /** Total number of attempts that will be made (delays.length + 1). */
71
+ totalAttempts: number;
72
+ }
73
+ /** Emitted when a module loads successfully after one or more retry attempts. */
74
+ interface SPAGuardEventLazyRetrySuccess {
75
+ /** 1-based retry number on which the import succeeded (1 = first retry). */
76
+ attempt: number;
77
+ name: "lazy-retry-success";
78
+ /** Total time in milliseconds from first attempt to success. */
79
+ totalTime?: number;
80
+ }
81
+ interface SPAGuardEventRetryAttempt {
82
+ attempt: number;
83
+ delay: number;
84
+ name: "retry-attempt";
85
+ retryId: string;
86
+ }
87
+ interface SPAGuardEventRetryExhausted {
88
+ finalAttempt: number;
89
+ name: "retry-exhausted";
90
+ retryId: string;
91
+ }
92
+ interface SPAGuardEventRetryReset {
93
+ name: "retry-reset";
94
+ previousAttempt: number;
95
+ previousRetryId: string;
96
+ timeSinceReload: number;
97
+ }
98
+ /** Emitted when a hashed static asset (script/link) fails to load, likely due to a stale deployment. */
99
+ interface SPAGuardEventStaticAssetLoadFailed {
100
+ name: "static-asset-load-failed";
101
+ /** The URL of the asset that failed to load. */
102
+ url: string;
103
+ }
104
+ type SubscribeFn = (event: SPAGuardEvent) => void;
105
+ type UnsubscribeFn = () => void;
106
+ //#endregion
107
+ export { SPAGuardEventFallbackUINotRendered as a, SPAGuardEventLazyRetryExhausted as c, SPAGuardEventRetryAttempt as d, SPAGuardEventRetryExhausted as f, UnsubscribeFn as g, SubscribeFn as h, SPAGuardEventChunkError as i, SPAGuardEventLazyRetryStart as l, SPAGuardEventStaticAssetLoadFailed as m, InternalConfig as n, SPAGuardEventFallbackUIShown as o, SPAGuardEventRetryReset as p, SPAGuardEvent as r, SPAGuardEventLazyRetryAttempt as s, EmitOptions as t, SPAGuardEventLazyRetrySuccess as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovineko/spa-guard",
3
- "version": "0.0.2-alpha-1",
3
+ "version": "0.0.4",
4
4
  "description": "Chunk load error handling for SPAs — core runtime, error handling, schema, i18n",
5
5
  "keywords": [
6
6
  "spa",
@@ -61,9 +61,6 @@
61
61
  "files": [
62
62
  "dist"
63
63
  ],
64
- "engines": {
65
- "node": ">=22.15.0"
66
- },
67
64
  "publishConfig": {
68
65
  "access": "public"
69
66
  }
@@ -1,98 +0,0 @@
1
- import {
2
- defaultSpinnerHtml,
3
- getOptions,
4
- spinnerStateWindowKey
5
- } from "./chunk-GE63YJOT.js";
6
-
7
- // src/common/parseVersion.ts
8
- function extractVersionFromHtml(html) {
9
- const collapsed = html.replaceAll(/[\r\n]+/g, "");
10
- const versionMatch = collapsed.match(/__SPA_GUARD_VERSION__\s*=\s*"([^"]+)"/);
11
- if (versionMatch?.[1]) {
12
- return versionMatch[1];
13
- }
14
- const markerIdx = collapsed.indexOf("__SPA_GUARD_OPTIONS__");
15
- if (markerIdx === -1) {
16
- return null;
17
- }
18
- const segment = collapsed.slice(markerIdx, markerIdx + 1e3);
19
- const optionsMatch = segment.match(
20
- /__SPA_GUARD_OPTIONS__\s*=\s*\{.*?"?version"?\s*:\s*"([^"]+)"/
21
- );
22
- return optionsMatch?.[1] ?? null;
23
- }
24
-
25
- // src/common/spinner.ts
26
- var SPINNER_ID = "__spa-guard-spinner";
27
- var defaultSpinnerSvg = defaultSpinnerHtml;
28
- var getState = () => {
29
- if (!globalThis.window[spinnerStateWindowKey]) {
30
- globalThis.window[spinnerStateWindowKey] = { savedOverflow: null };
31
- }
32
- return globalThis.window[spinnerStateWindowKey];
33
- };
34
- function dismissSpinner() {
35
- if (typeof document === "undefined") {
36
- return;
37
- }
38
- const el = document.getElementById(SPINNER_ID);
39
- if (el) {
40
- el.remove();
41
- }
42
- const state = getState();
43
- if (state.savedOverflow !== null) {
44
- document.body.style.overflow = state.savedOverflow;
45
- state.savedOverflow = null;
46
- } else if (el) {
47
- document.body.style.overflow = "";
48
- }
49
- }
50
- var sanitizeCssValue = (value) => value.replaceAll(/["'<>\\{};\n]/g, "");
51
- function getSpinnerHtml(backgroundOverride) {
52
- const opts = getOptions();
53
- if (opts.html?.spinner?.disabled) {
54
- return "";
55
- }
56
- const spinnerContent = opts.html?.spinner?.content ?? defaultSpinnerSvg;
57
- const bg = sanitizeCssValue(backgroundOverride ?? opts.html?.spinner?.background ?? "#fff");
58
- 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>`;
59
- }
60
- function showSpinner(options) {
61
- if (typeof document === "undefined") {
62
- return () => {
63
- };
64
- }
65
- const opts = getOptions();
66
- if (opts.html?.spinner?.disabled) {
67
- return () => {
68
- };
69
- }
70
- const existing = document.getElementById(SPINNER_ID);
71
- if (existing) {
72
- existing.remove();
73
- }
74
- const html = getSpinnerHtml(options?.background);
75
- if (!html) {
76
- return () => {
77
- };
78
- }
79
- const wrapper = document.createElement("div");
80
- wrapper.innerHTML = html;
81
- const overlay = wrapper.firstElementChild;
82
- if (!existing) {
83
- getState().savedOverflow = document.body.style.overflow;
84
- }
85
- document.body.style.overflow = "hidden";
86
- document.body.append(overlay);
87
- return () => dismissSpinner();
88
- }
89
-
90
- export {
91
- extractVersionFromHtml,
92
- SPINNER_ID,
93
- defaultSpinnerSvg,
94
- dismissSpinner,
95
- sanitizeCssValue,
96
- getSpinnerHtml,
97
- showSpinner
98
- };