@ovineko/spa-guard-react 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.
- package/README.md +25 -56
- package/dist/index-Xle6BwDr.d.mts +133 -0
- package/dist/react/index.d.ts +2 -10
- package/dist/react/index.js +2 -20
- package/dist/react-DuyDGIyU.mjs +218 -0
- package/dist/react-error-boundary/index.d.ts +22 -18
- package/dist/react-error-boundary/index.js +67 -77
- package/package.json +3 -6
- package/dist/DefaultErrorFallback.d.ts +0 -17
- package/dist/chunk-6VYHMTKS.js +0 -223
- package/dist/react/DebugSyncErrorTrigger.d.ts +0 -13
- package/dist/react/Spinner.d.ts +0 -9
- package/dist/react/lazyWithRetry.d.ts +0 -34
- package/dist/react/types.d.ts +0 -41
- package/dist/react/useSPAGuardChunkError.d.ts +0 -2
- package/dist/react/useSPAGuardEvents.d.ts +0 -2
package/README.md
CHANGED
|
@@ -7,78 +7,47 @@ React hooks, components, and error boundaries for [spa-guard](../spa-guard/READM
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
npm install @ovineko/spa-guard-react @ovineko/spa-guard react
|
|
12
|
-
```
|
|
10
|
+
**pnpm** (recommended):
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @ovineko/spa-guard-react @ovineko/spa-guard react
|
|
14
|
+
```
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
**npm**:
|
|
19
17
|
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
```bash
|
|
19
|
+
npm install @ovineko/spa-guard-react @ovineko/spa-guard react
|
|
20
|
+
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
**yarn**:
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
retryDelays: [500, 1000, 2000, 4000],
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
export function App() {
|
|
32
|
-
return (
|
|
33
|
-
<Suspense fallback={<div>Loading...</div>}>
|
|
34
|
-
<LazyHome />
|
|
35
|
-
</Suspense>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
24
|
+
```bash
|
|
25
|
+
yarn add @ovineko/spa-guard-react @ovineko/spa-guard react
|
|
38
26
|
```
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Catches errors in child components and integrates with spa-guard retry state.
|
|
28
|
+
**bun**:
|
|
43
29
|
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export function App() {
|
|
48
|
-
return (
|
|
49
|
-
<ErrorBoundary>
|
|
50
|
-
<MyApp />
|
|
51
|
-
</ErrorBoundary>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
30
|
+
```bash
|
|
31
|
+
bun add @ovineko/spa-guard-react @ovineko/spa-guard react
|
|
54
32
|
```
|
|
55
33
|
|
|
56
|
-
|
|
34
|
+
**deno**:
|
|
57
35
|
|
|
58
|
-
|
|
36
|
+
```bash
|
|
37
|
+
deno add npm:@ovineko/spa-guard-react npm:@ovineko/spa-guard npm:react
|
|
38
|
+
```
|
|
59
39
|
|
|
60
|
-
|
|
61
|
-
- `useSpaGuardState()` — reactive hook for current spa-guard state
|
|
62
|
-
- `useSPAGuardChunkError()` — hook to detect chunk load errors
|
|
63
|
-
- `useSPAGuardEvents()` — hook to subscribe to spa-guard events
|
|
64
|
-
- `DefaultErrorFallback` — default fallback UI component
|
|
65
|
-
- `Spinner` — loading spinner component
|
|
66
|
-
- `DebugSyncErrorTrigger` — trigger sync errors for testing
|
|
67
|
-
- `ForceRetryError` — error class to force a retry
|
|
68
|
-
- `LazyRetryOptions` — options type for `lazyWithRetry`
|
|
69
|
-
- `SpaGuardState` — spa-guard state type
|
|
40
|
+
## Usage
|
|
70
41
|
|
|
71
|
-
|
|
42
|
+
```tsx
|
|
43
|
+
import { lazyWithRetry } from "@ovineko/spa-guard-react";
|
|
72
44
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- `FallbackProps` — props passed to fallback component
|
|
45
|
+
const LazyHome = lazyWithRetry(() => import("./pages/Home"));
|
|
46
|
+
```
|
|
76
47
|
|
|
77
|
-
##
|
|
48
|
+
## Documentation
|
|
78
49
|
|
|
79
|
-
|
|
80
|
-
- [@ovineko/spa-guard-react-router](../react-router/README.md) — React Router integration
|
|
81
|
-
- [@ovineko/spa-guard-vite](../vite/README.md) — Vite plugin
|
|
50
|
+
Full documentation: [ovineko.com/docs/spa-guard/react](https://ovineko.com/docs/spa-guard/react)
|
|
82
51
|
|
|
83
52
|
## License
|
|
84
53
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ComponentProps, ComponentType, LazyExoticComponent } from "react";
|
|
2
|
+
import { SPAGuardEvent, SPAGuardEventChunkError } from "@ovineko/spa-guard/_internal";
|
|
3
|
+
import * as _$_ovineko_spa_guard_runtime0 from "@ovineko/spa-guard/runtime";
|
|
4
|
+
import { SpaGuardState, SpaGuardState as SpaGuardState$1 } from "@ovineko/spa-guard/runtime";
|
|
5
|
+
import { ForceRetryError } from "@ovineko/spa-guard";
|
|
6
|
+
|
|
7
|
+
//#region src/DefaultErrorFallback.d.ts
|
|
8
|
+
interface DefaultErrorFallbackProps {
|
|
9
|
+
error: unknown;
|
|
10
|
+
isChunkError: boolean;
|
|
11
|
+
isRetrying: boolean;
|
|
12
|
+
onReset?: () => void;
|
|
13
|
+
spaGuardState: SpaGuardState;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Default fallback UI component for error boundaries.
|
|
17
|
+
*
|
|
18
|
+
* Uses two separate HTML templates: one for loading/retrying state
|
|
19
|
+
* and one for error state. Renders via dangerouslySetInnerHTML with
|
|
20
|
+
* virtual container + data attribute patching for dynamic content.
|
|
21
|
+
*/
|
|
22
|
+
declare const DefaultErrorFallback: React.FC<DefaultErrorFallbackProps>;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/react/DebugSyncErrorTrigger.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* Renders nothing normally. When a CustomEvent of type debugSyncErrorEventType
|
|
27
|
+
* is dispatched on window, this component stores the error in state and throws
|
|
28
|
+
* it during the next render, allowing a parent React Error Boundary to catch it.
|
|
29
|
+
*
|
|
30
|
+
* Place this component inside your ErrorBoundary:
|
|
31
|
+
*
|
|
32
|
+
* <ErrorBoundary fallback={<CrashPage />}>
|
|
33
|
+
* <DebugSyncErrorTrigger />
|
|
34
|
+
* <App />
|
|
35
|
+
* </ErrorBoundary>
|
|
36
|
+
*/
|
|
37
|
+
declare function DebugSyncErrorTrigger(): null;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/react/types.d.ts
|
|
40
|
+
/**
|
|
41
|
+
* Per-import options for lazyWithRetry that override global lazyRetry options.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Override retry delays for a critical component
|
|
45
|
+
* const LazyCheckout = lazyWithRetry(
|
|
46
|
+
* () => import('./pages/Checkout'),
|
|
47
|
+
* { retryDelays: [500, 1000, 2000, 4000] } satisfies LazyRetryOptions
|
|
48
|
+
* );
|
|
49
|
+
*/
|
|
50
|
+
interface LazyRetryOptions {
|
|
51
|
+
/**
|
|
52
|
+
* If true, triggers a full page reload via triggerRetry() after all retry attempts are exhausted.
|
|
53
|
+
* If false, only throws the error to the error boundary without reload.
|
|
54
|
+
* Overrides the global `window.__SPA_GUARD_OPTIONS__.lazyRetry.callReloadOnFailure`.
|
|
55
|
+
*
|
|
56
|
+
* @default true (inherited from global options)
|
|
57
|
+
*/
|
|
58
|
+
callReloadOnFailure?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Array of delays in milliseconds for retry attempts.
|
|
61
|
+
* Each element represents one retry attempt with the given delay.
|
|
62
|
+
* The number of elements determines the number of retry attempts.
|
|
63
|
+
* Overrides the global `window.__SPA_GUARD_OPTIONS__.lazyRetry.retryDelays`.
|
|
64
|
+
*
|
|
65
|
+
* @default [1000, 2000] (inherited from global options)
|
|
66
|
+
* @example [500, 1500, 3000] // 3 attempts: 500ms, 1.5s, 3s
|
|
67
|
+
*/
|
|
68
|
+
retryDelays?: number[];
|
|
69
|
+
/**
|
|
70
|
+
* AbortSignal to cancel pending retry delays between import attempts.
|
|
71
|
+
* When the signal fires, any pending setTimeout between retries is cleared
|
|
72
|
+
* and the import promise rejects with an AbortError.
|
|
73
|
+
*
|
|
74
|
+
* Note: cancels only the wait periods between retry attempts, not an in-flight
|
|
75
|
+
* dynamic import (JavaScript does not support cancelling in-flight module fetches).
|
|
76
|
+
* Most useful when calling `retryImport` directly rather than through `lazyWithRetry`,
|
|
77
|
+
* since `lazyWithRetry` captures the signal at module scope (not per component instance).
|
|
78
|
+
*/
|
|
79
|
+
signal?: AbortSignal;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/react/lazyWithRetry.d.ts
|
|
83
|
+
/**
|
|
84
|
+
* Creates a lazy-loaded React component with automatic retry on chunk load failures.
|
|
85
|
+
*
|
|
86
|
+
* On import failure, retries with configurable delays before falling back to
|
|
87
|
+
* `triggerRetry()` for a full page reload.
|
|
88
|
+
*
|
|
89
|
+
* @param importFn - Function that performs the dynamic import
|
|
90
|
+
* @param options - Per-import options that override global lazyRetry options
|
|
91
|
+
* @returns A lazy React component with retry logic
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Basic usage with global options
|
|
95
|
+
* const LazyHome = lazyWithRetry(() => import('./pages/Home'));
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Override retry delays for a critical component
|
|
99
|
+
* const LazyCheckout = lazyWithRetry(
|
|
100
|
+
* () => import('./pages/Checkout'),
|
|
101
|
+
* { retryDelays: [500, 1000, 2000, 4000] }
|
|
102
|
+
* );
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // Disable page reload for a non-critical component
|
|
106
|
+
* const LazyWidget = lazyWithRetry(
|
|
107
|
+
* () => import('./widgets/Optional'),
|
|
108
|
+
* { retryDelays: [1000], callReloadOnFailure: false }
|
|
109
|
+
* );
|
|
110
|
+
*/
|
|
111
|
+
declare const lazyWithRetry: <T extends ComponentType<any>>(importFn: () => Promise<{
|
|
112
|
+
default: T;
|
|
113
|
+
}>, options?: LazyRetryOptions) => LazyExoticComponent<T>;
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/react/Spinner.d.ts
|
|
116
|
+
type SpinnerProps = Omit<ComponentProps<"div">, "children" | "dangerouslySetInnerHTML">;
|
|
117
|
+
/**
|
|
118
|
+
* Renders the spa-guard spinner inside a div.
|
|
119
|
+
* Returns null if spinner is disabled or no content available.
|
|
120
|
+
* All div props forwarded to wrapper element.
|
|
121
|
+
*/
|
|
122
|
+
declare function Spinner(props: SpinnerProps): null | React.ReactElement;
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/react/useSPAGuardChunkError.d.ts
|
|
125
|
+
declare const useSPAGuardChunkError: () => null | SPAGuardEventChunkError;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/react/useSPAGuardEvents.d.ts
|
|
128
|
+
declare const useSPAGuardEvents: (callback: (event: SPAGuardEvent) => void) => void;
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/react/index.d.ts
|
|
131
|
+
declare const useSpaGuardState: () => _$_ovineko_spa_guard_runtime0.SpaGuardState;
|
|
132
|
+
//#endregion
|
|
133
|
+
export { useSPAGuardChunkError as a, LazyRetryOptions as c, useSPAGuardEvents as i, DebugSyncErrorTrigger as l, SpaGuardState$1 as n, Spinner as o, useSpaGuardState as r, lazyWithRetry as s, ForceRetryError as t, DefaultErrorFallback as u };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export { DebugSyncErrorTrigger
|
|
3
|
-
export { lazyWithRetry } from "./lazyWithRetry";
|
|
4
|
-
export { Spinner } from "./Spinner";
|
|
5
|
-
export type { LazyRetryOptions } from "./types";
|
|
6
|
-
export { useSPAGuardChunkError } from "./useSPAGuardChunkError";
|
|
7
|
-
export { useSPAGuardEvents } from "./useSPAGuardEvents";
|
|
8
|
-
export { ForceRetryError } from "@ovineko/spa-guard";
|
|
9
|
-
export type { SpaGuardState } from "@ovineko/spa-guard/runtime";
|
|
10
|
-
export declare const useSpaGuardState: () => import("@ovineko/spa-guard/runtime").SpaGuardState;
|
|
1
|
+
import { a as useSPAGuardChunkError, c as LazyRetryOptions, i as useSPAGuardEvents, l as DebugSyncErrorTrigger, n as SpaGuardState, o as Spinner, r as useSpaGuardState, s as lazyWithRetry, t as ForceRetryError, u as DefaultErrorFallback } from "../index-Xle6BwDr.mjs";
|
|
2
|
+
export { DebugSyncErrorTrigger, DefaultErrorFallback, ForceRetryError, LazyRetryOptions, SpaGuardState, Spinner, lazyWithRetry, useSPAGuardChunkError, useSPAGuardEvents, useSpaGuardState };
|
package/dist/react/index.js
CHANGED
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
DefaultErrorFallback,
|
|
4
|
-
ForceRetryError,
|
|
5
|
-
Spinner,
|
|
6
|
-
lazyWithRetry,
|
|
7
|
-
useSPAGuardChunkError,
|
|
8
|
-
useSPAGuardEvents,
|
|
9
|
-
useSpaGuardState
|
|
10
|
-
} from "../chunk-6VYHMTKS.js";
|
|
11
|
-
export {
|
|
12
|
-
DebugSyncErrorTrigger,
|
|
13
|
-
DefaultErrorFallback,
|
|
14
|
-
ForceRetryError,
|
|
15
|
-
Spinner,
|
|
16
|
-
lazyWithRetry,
|
|
17
|
-
useSPAGuardChunkError,
|
|
18
|
-
useSPAGuardEvents,
|
|
19
|
-
useSpaGuardState
|
|
20
|
-
};
|
|
1
|
+
import { a as Spinner, c as DefaultErrorFallback, i as useSPAGuardEvents, n as useSpaGuardState, o as lazyWithRetry, r as useSPAGuardChunkError, s as DebugSyncErrorTrigger, t as ForceRetryError } from "../react-DuyDGIyU.mjs";
|
|
2
|
+
export { DebugSyncErrorTrigger, DefaultErrorFallback, ForceRetryError, Spinner, lazyWithRetry, useSPAGuardChunkError, useSPAGuardEvents, useSpaGuardState };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { applyI18n, debugSyncErrorEventType, defaultErrorFallbackHtml, defaultLoadingFallbackHtml, getI18n, getOptions, retryImport, subscribe } from "@ovineko/spa-guard/_internal";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { getState, subscribeToState } from "@ovineko/spa-guard/runtime";
|
|
5
|
+
import { ForceRetryError } from "@ovineko/spa-guard";
|
|
6
|
+
//#region src/DefaultErrorFallback.tsx
|
|
7
|
+
const escapeHtml = (str) => str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
8
|
+
const reloadHandler = () => location.reload();
|
|
9
|
+
/**
|
|
10
|
+
* Build final HTML by parsing the template in a virtual container
|
|
11
|
+
* and patching content via data attributes. This is robust against
|
|
12
|
+
* template changes (minification, styling, element order).
|
|
13
|
+
*/
|
|
14
|
+
function buildHtml(template, patches) {
|
|
15
|
+
const container = document.createElement("div");
|
|
16
|
+
container.innerHTML = template;
|
|
17
|
+
if (patches.spinnerHtml) {
|
|
18
|
+
const spinnerEl = container.querySelector("[data-spa-guard-spinner]");
|
|
19
|
+
if (spinnerEl) spinnerEl.innerHTML = patches.spinnerHtml;
|
|
20
|
+
}
|
|
21
|
+
if (patches.content) for (const [key, value] of Object.entries(patches.content)) {
|
|
22
|
+
const el = container.querySelector(`[data-spa-guard-content="${key}"]`);
|
|
23
|
+
if (el) el.innerHTML = value;
|
|
24
|
+
}
|
|
25
|
+
if (patches.sections) for (const [key, visible] of Object.entries(patches.sections)) {
|
|
26
|
+
const el = container.querySelector(`[data-spa-guard-section="${key}"]`);
|
|
27
|
+
if (el) el.style.display = visible ? "block" : "none";
|
|
28
|
+
}
|
|
29
|
+
if (patches.actions) for (const [key, visible] of Object.entries(patches.actions)) {
|
|
30
|
+
const el = container.querySelector(`[data-spa-guard-action="${key}"]`);
|
|
31
|
+
if (el) el.style.display = visible ? "inline-block" : "none";
|
|
32
|
+
}
|
|
33
|
+
const t = getI18n();
|
|
34
|
+
if (t) applyI18n(container, t);
|
|
35
|
+
return container.innerHTML;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Default fallback UI component for error boundaries.
|
|
39
|
+
*
|
|
40
|
+
* Uses two separate HTML templates: one for loading/retrying state
|
|
41
|
+
* and one for error state. Renders via dangerouslySetInnerHTML with
|
|
42
|
+
* virtual container + data attribute patching for dynamic content.
|
|
43
|
+
*/
|
|
44
|
+
const DefaultErrorFallback = ({ error, isChunkError: isChunk, isRetrying, onReset, spaGuardState }) => {
|
|
45
|
+
const containerRef = useRef(null);
|
|
46
|
+
const html = useMemo(() => {
|
|
47
|
+
const opts = getOptions();
|
|
48
|
+
if (isRetrying) {
|
|
49
|
+
const loadingTemplate = opts.html?.loading?.content ?? defaultLoadingFallbackHtml;
|
|
50
|
+
const spinnerContent = opts.html?.spinner?.content;
|
|
51
|
+
return buildHtml(loadingTemplate, {
|
|
52
|
+
content: { attempt: String(spaGuardState.currentAttempt) },
|
|
53
|
+
sections: { retrying: true },
|
|
54
|
+
...spinnerContent !== void 0 && { spinnerHtml: spinnerContent }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const heading = isChunk ? "Failed to load module" : "Something went wrong";
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
return buildHtml(opts.html?.fallback?.content ?? defaultErrorFallbackHtml, {
|
|
60
|
+
actions: { "try-again": Boolean(onReset) },
|
|
61
|
+
content: {
|
|
62
|
+
heading: escapeHtml(heading),
|
|
63
|
+
message: escapeHtml(message)
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}, [
|
|
67
|
+
isRetrying,
|
|
68
|
+
isChunk,
|
|
69
|
+
error,
|
|
70
|
+
onReset,
|
|
71
|
+
spaGuardState.currentAttempt
|
|
72
|
+
]);
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
const el = containerRef.current;
|
|
75
|
+
if (!el) return;
|
|
76
|
+
const reloadBtn = el.querySelector("[data-spa-guard-action=\"reload\"]");
|
|
77
|
+
reloadBtn?.addEventListener("click", reloadHandler);
|
|
78
|
+
const tryAgainHandler = onReset ? () => onReset() : null;
|
|
79
|
+
const tryAgainBtn = onReset ? el.querySelector("[data-spa-guard-action=\"try-again\"]") : null;
|
|
80
|
+
if (tryAgainHandler && tryAgainBtn) tryAgainBtn.addEventListener("click", tryAgainHandler);
|
|
81
|
+
return () => {
|
|
82
|
+
reloadBtn?.removeEventListener("click", reloadHandler);
|
|
83
|
+
if (tryAgainHandler && tryAgainBtn) tryAgainBtn.removeEventListener("click", tryAgainHandler);
|
|
84
|
+
};
|
|
85
|
+
}, [onReset, html]);
|
|
86
|
+
return /* @__PURE__ */ jsx("div", {
|
|
87
|
+
dangerouslySetInnerHTML: useMemo(() => ({ __html: html }), [html]),
|
|
88
|
+
ref: containerRef
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/react/DebugSyncErrorTrigger.tsx
|
|
93
|
+
/**
|
|
94
|
+
* Renders nothing normally. When a CustomEvent of type debugSyncErrorEventType
|
|
95
|
+
* is dispatched on window, this component stores the error in state and throws
|
|
96
|
+
* it during the next render, allowing a parent React Error Boundary to catch it.
|
|
97
|
+
*
|
|
98
|
+
* Place this component inside your ErrorBoundary:
|
|
99
|
+
*
|
|
100
|
+
* <ErrorBoundary fallback={<CrashPage />}>
|
|
101
|
+
* <DebugSyncErrorTrigger />
|
|
102
|
+
* <App />
|
|
103
|
+
* </ErrorBoundary>
|
|
104
|
+
*/
|
|
105
|
+
function DebugSyncErrorTrigger() {
|
|
106
|
+
const [error, setError] = useState(null);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const handler = (e) => {
|
|
109
|
+
const detail = e.detail;
|
|
110
|
+
if (detail?.error instanceof Error) setError(detail.error);
|
|
111
|
+
};
|
|
112
|
+
globalThis.addEventListener(debugSyncErrorEventType, handler);
|
|
113
|
+
return () => {
|
|
114
|
+
globalThis.removeEventListener(debugSyncErrorEventType, handler);
|
|
115
|
+
};
|
|
116
|
+
}, []);
|
|
117
|
+
if (error) throw error;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/react/lazyWithRetry.tsx
|
|
122
|
+
/**
|
|
123
|
+
* Creates a lazy-loaded React component with automatic retry on chunk load failures.
|
|
124
|
+
*
|
|
125
|
+
* On import failure, retries with configurable delays before falling back to
|
|
126
|
+
* `triggerRetry()` for a full page reload.
|
|
127
|
+
*
|
|
128
|
+
* @param importFn - Function that performs the dynamic import
|
|
129
|
+
* @param options - Per-import options that override global lazyRetry options
|
|
130
|
+
* @returns A lazy React component with retry logic
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* // Basic usage with global options
|
|
134
|
+
* const LazyHome = lazyWithRetry(() => import('./pages/Home'));
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* // Override retry delays for a critical component
|
|
138
|
+
* const LazyCheckout = lazyWithRetry(
|
|
139
|
+
* () => import('./pages/Checkout'),
|
|
140
|
+
* { retryDelays: [500, 1000, 2000, 4000] }
|
|
141
|
+
* );
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* // Disable page reload for a non-critical component
|
|
145
|
+
* const LazyWidget = lazyWithRetry(
|
|
146
|
+
* () => import('./widgets/Optional'),
|
|
147
|
+
* { retryDelays: [1000], callReloadOnFailure: false }
|
|
148
|
+
* );
|
|
149
|
+
*/
|
|
150
|
+
const lazyWithRetry = (importFn, options) => {
|
|
151
|
+
return lazy(() => {
|
|
152
|
+
const globalLazyRetry = getOptions().lazyRetry ?? {};
|
|
153
|
+
const retryDelays = options?.retryDelays ?? globalLazyRetry.retryDelays ?? [1e3, 2e3];
|
|
154
|
+
const callReloadOnFailure = options?.callReloadOnFailure ?? globalLazyRetry.callReloadOnFailure ?? true;
|
|
155
|
+
const signal = options?.signal;
|
|
156
|
+
return retryImport(importFn, retryDelays, {
|
|
157
|
+
callReloadOnFailure,
|
|
158
|
+
...signal !== void 0 && { signal }
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/react/Spinner.tsx
|
|
164
|
+
/**
|
|
165
|
+
* Renders the spa-guard spinner inside a div.
|
|
166
|
+
* Returns null if spinner is disabled or no content available.
|
|
167
|
+
* All div props forwarded to wrapper element.
|
|
168
|
+
*/
|
|
169
|
+
function Spinner(props) {
|
|
170
|
+
const opts = getOptions();
|
|
171
|
+
const content = opts.html?.spinner?.disabled ? void 0 : opts.html?.spinner?.content;
|
|
172
|
+
const innerHtml = useMemo(() => content ? { __html: content } : null, [content]);
|
|
173
|
+
if (!innerHtml) return null;
|
|
174
|
+
return /* @__PURE__ */ jsx("div", {
|
|
175
|
+
...props,
|
|
176
|
+
dangerouslySetInnerHTML: innerHtml
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/react/useSPAGuardEvents.ts
|
|
181
|
+
const useSPAGuardEvents = (callback) => {
|
|
182
|
+
const callbackRef = useRef(callback);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
callbackRef.current = callback;
|
|
185
|
+
});
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
return subscribe((event) => {
|
|
188
|
+
callbackRef.current(event);
|
|
189
|
+
});
|
|
190
|
+
}, []);
|
|
191
|
+
};
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/react/useSPAGuardChunkError.ts
|
|
194
|
+
const useSPAGuardChunkError = () => {
|
|
195
|
+
const [chunkError, setChunkError] = useState(null);
|
|
196
|
+
useSPAGuardEvents(useCallback((event) => {
|
|
197
|
+
if (event.name === "chunk-error") setChunkError(event);
|
|
198
|
+
}, []));
|
|
199
|
+
return chunkError;
|
|
200
|
+
};
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/react/index.tsx
|
|
203
|
+
const useSpaGuardState = () => {
|
|
204
|
+
const [state, setState] = useState(() => {
|
|
205
|
+
if (globalThis.window === void 0) return {
|
|
206
|
+
currentAttempt: 0,
|
|
207
|
+
isFallbackShown: false,
|
|
208
|
+
isWaiting: false
|
|
209
|
+
};
|
|
210
|
+
return getState();
|
|
211
|
+
});
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
return subscribeToState(setState);
|
|
214
|
+
}, []);
|
|
215
|
+
return state;
|
|
216
|
+
};
|
|
217
|
+
//#endregion
|
|
218
|
+
export { Spinner as a, DefaultErrorFallback as c, useSPAGuardEvents as i, useSpaGuardState as n, lazyWithRetry as o, useSPAGuardChunkError as r, DebugSyncErrorTrigger as s, ForceRetryError as t };
|
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { n as SpaGuardState } from "../index-Xle6BwDr.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react-error-boundary/index.d.ts
|
|
3
5
|
/**
|
|
4
6
|
* Props for the ErrorBoundary component
|
|
5
7
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
interface ErrorBoundaryProps {
|
|
9
|
+
autoRetryChunkErrors?: boolean;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
fallback?: ((props: FallbackProps) => React.ReactElement) | React.ComponentType<FallbackProps>;
|
|
12
|
+
fallbackRender?: (props: FallbackProps) => React.ReactElement;
|
|
13
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
14
|
+
resetKeys?: Array<unknown>;
|
|
15
|
+
sendBeaconOnError?: boolean;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Props passed to the fallback component when an error is caught
|
|
17
19
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
interface FallbackProps {
|
|
21
|
+
error: Error;
|
|
22
|
+
errorInfo: null | React.ErrorInfo;
|
|
23
|
+
isChunkError: boolean;
|
|
24
|
+
isRetrying: boolean;
|
|
25
|
+
resetError: () => void;
|
|
26
|
+
spaGuardState: SpaGuardState;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* Error boundary component with spa-guard integration.
|
|
28
30
|
*
|
|
29
31
|
* Catches errors in child components and automatically retries chunk loading errors.
|
|
30
32
|
*/
|
|
31
|
-
|
|
33
|
+
declare const ErrorBoundary: React.FC<ErrorBoundaryProps>;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ErrorBoundary, ErrorBoundaryProps, FallbackProps };
|
|
@@ -1,83 +1,73 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DefaultErrorFallback,
|
|
3
|
-
useSpaGuardState
|
|
4
|
-
} from "../chunk-6VYHMTKS.js";
|
|
5
|
-
|
|
6
|
-
// src/react-error-boundary/index.tsx
|
|
1
|
+
import { c as DefaultErrorFallback, n as useSpaGuardState } from "../react-DuyDGIyU.mjs";
|
|
7
2
|
import { Component } from "react";
|
|
8
3
|
import { handleErrorWithSpaGuard, isChunkError } from "@ovineko/spa-guard/_internal";
|
|
9
4
|
import { jsx } from "react/jsx-runtime";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
error,
|
|
20
|
-
isChunkError: isChunk,
|
|
21
|
-
isRetrying,
|
|
22
|
-
onReset: resetError,
|
|
23
|
-
spaGuardState
|
|
24
|
-
}
|
|
25
|
-
);
|
|
5
|
+
//#region src/react-error-boundary/index.tsx
|
|
6
|
+
const DefaultFallback = ({ error, isChunkError: isChunk, isRetrying, resetError, spaGuardState }) => /* @__PURE__ */ jsx(DefaultErrorFallback, {
|
|
7
|
+
error,
|
|
8
|
+
isChunkError: isChunk,
|
|
9
|
+
isRetrying,
|
|
10
|
+
onReset: resetError,
|
|
11
|
+
spaGuardState
|
|
12
|
+
});
|
|
26
13
|
var ErrorBoundaryImpl = class extends Component {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
resetError = () => {
|
|
74
|
-
this.setState({ error: null, errorInfo: null });
|
|
75
|
-
};
|
|
14
|
+
state = {
|
|
15
|
+
error: null,
|
|
16
|
+
errorInfo: null
|
|
17
|
+
};
|
|
18
|
+
static getDerivedStateFromError(error) {
|
|
19
|
+
return { error };
|
|
20
|
+
}
|
|
21
|
+
componentDidCatch(error, errorInfo) {
|
|
22
|
+
this.setState({ errorInfo });
|
|
23
|
+
const { autoRetryChunkErrors, onError, sendBeaconOnError } = this.props;
|
|
24
|
+
handleErrorWithSpaGuard(error, {
|
|
25
|
+
...autoRetryChunkErrors !== void 0 && { autoRetryChunkErrors },
|
|
26
|
+
errorInfo,
|
|
27
|
+
eventName: "react-error-boundary",
|
|
28
|
+
onError: () => onError?.(error, errorInfo),
|
|
29
|
+
...sendBeaconOnError !== void 0 && { sendBeaconOnError }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
componentDidUpdate(prevProps) {
|
|
33
|
+
const { resetKeys = [] } = this.props;
|
|
34
|
+
const prevResetKeys = prevProps.resetKeys ?? [];
|
|
35
|
+
if (this.state.error !== null && (resetKeys.length !== prevResetKeys.length || resetKeys.some((key, i) => key !== prevResetKeys[i]))) this.resetError();
|
|
36
|
+
}
|
|
37
|
+
render() {
|
|
38
|
+
const { error, errorInfo } = this.state;
|
|
39
|
+
if (!error) return this.props.children;
|
|
40
|
+
const { fallback: Fallback, fallbackRender, spaGuardState } = this.props;
|
|
41
|
+
const fallbackProps = {
|
|
42
|
+
error,
|
|
43
|
+
errorInfo,
|
|
44
|
+
isChunkError: isChunkError(error),
|
|
45
|
+
isRetrying: spaGuardState.isWaiting && spaGuardState.currentAttempt > 0,
|
|
46
|
+
resetError: this.resetError,
|
|
47
|
+
spaGuardState
|
|
48
|
+
};
|
|
49
|
+
if (fallbackRender) return fallbackRender(fallbackProps);
|
|
50
|
+
if (Fallback) return /* @__PURE__ */ jsx(Fallback, { ...fallbackProps });
|
|
51
|
+
return /* @__PURE__ */ jsx(DefaultFallback, { ...fallbackProps });
|
|
52
|
+
}
|
|
53
|
+
resetError = () => {
|
|
54
|
+
this.setState({
|
|
55
|
+
error: null,
|
|
56
|
+
errorInfo: null
|
|
57
|
+
});
|
|
58
|
+
};
|
|
76
59
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Error boundary component with spa-guard integration.
|
|
62
|
+
*
|
|
63
|
+
* Catches errors in child components and automatically retries chunk loading errors.
|
|
64
|
+
*/
|
|
65
|
+
const ErrorBoundary = (props) => {
|
|
66
|
+
const spaGuardState = useSpaGuardState();
|
|
67
|
+
return /* @__PURE__ */ jsx(ErrorBoundaryImpl, {
|
|
68
|
+
...props,
|
|
69
|
+
spaGuardState
|
|
70
|
+
});
|
|
83
71
|
};
|
|
72
|
+
//#endregion
|
|
73
|
+
export { ErrorBoundary };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ovineko/spa-guard-react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "React hooks, components, and error boundaries for spa-guard",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spa",
|
|
@@ -35,11 +35,8 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"react": "
|
|
39
|
-
"@ovineko/spa-guard": "0.0.
|
|
40
|
-
},
|
|
41
|
-
"engines": {
|
|
42
|
-
"node": ">=22.15.0"
|
|
38
|
+
"react": ">=18",
|
|
39
|
+
"@ovineko/spa-guard": "0.0.4"
|
|
43
40
|
},
|
|
44
41
|
"publishConfig": {
|
|
45
42
|
"access": "public"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { SpaGuardState } from "@ovineko/spa-guard/runtime";
|
|
2
|
-
interface DefaultErrorFallbackProps {
|
|
3
|
-
error: unknown;
|
|
4
|
-
isChunkError: boolean;
|
|
5
|
-
isRetrying: boolean;
|
|
6
|
-
onReset?: () => void;
|
|
7
|
-
spaGuardState: SpaGuardState;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Default fallback UI component for error boundaries.
|
|
11
|
-
*
|
|
12
|
-
* Uses two separate HTML templates: one for loading/retrying state
|
|
13
|
-
* and one for error state. Renders via dangerouslySetInnerHTML with
|
|
14
|
-
* virtual container + data attribute patching for dynamic content.
|
|
15
|
-
*/
|
|
16
|
-
export declare const DefaultErrorFallback: React.FC<DefaultErrorFallbackProps>;
|
|
17
|
-
export {};
|
package/dist/chunk-6VYHMTKS.js
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
// src/react/index.tsx
|
|
2
|
-
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
3
|
-
import { getState, subscribeToState } from "@ovineko/spa-guard/runtime";
|
|
4
|
-
|
|
5
|
-
// src/DefaultErrorFallback.tsx
|
|
6
|
-
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
7
|
-
import {
|
|
8
|
-
applyI18n,
|
|
9
|
-
defaultErrorFallbackHtml,
|
|
10
|
-
defaultLoadingFallbackHtml,
|
|
11
|
-
getI18n,
|
|
12
|
-
getOptions
|
|
13
|
-
} from "@ovineko/spa-guard/_internal";
|
|
14
|
-
import { jsx } from "react/jsx-runtime";
|
|
15
|
-
var escapeHtml = (str) => str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
16
|
-
var reloadHandler = () => location.reload();
|
|
17
|
-
function buildHtml(template, patches) {
|
|
18
|
-
const container = document.createElement("div");
|
|
19
|
-
container.innerHTML = template;
|
|
20
|
-
if (patches.spinnerHtml) {
|
|
21
|
-
const spinnerEl = container.querySelector("[data-spa-guard-spinner]");
|
|
22
|
-
if (spinnerEl) {
|
|
23
|
-
spinnerEl.innerHTML = patches.spinnerHtml;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (patches.content) {
|
|
27
|
-
for (const [key, value] of Object.entries(patches.content)) {
|
|
28
|
-
const el = container.querySelector(`[data-spa-guard-content="${key}"]`);
|
|
29
|
-
if (el) {
|
|
30
|
-
el.innerHTML = value;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (patches.sections) {
|
|
35
|
-
for (const [key, visible] of Object.entries(patches.sections)) {
|
|
36
|
-
const el = container.querySelector(`[data-spa-guard-section="${key}"]`);
|
|
37
|
-
if (el) {
|
|
38
|
-
el.style.display = visible ? "block" : "none";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (patches.actions) {
|
|
43
|
-
for (const [key, visible] of Object.entries(patches.actions)) {
|
|
44
|
-
const el = container.querySelector(`[data-spa-guard-action="${key}"]`);
|
|
45
|
-
if (el) {
|
|
46
|
-
el.style.display = visible ? "inline-block" : "none";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const t = getI18n();
|
|
51
|
-
if (t) {
|
|
52
|
-
applyI18n(container, t);
|
|
53
|
-
}
|
|
54
|
-
return container.innerHTML;
|
|
55
|
-
}
|
|
56
|
-
var DefaultErrorFallback = ({
|
|
57
|
-
error,
|
|
58
|
-
isChunkError: isChunk,
|
|
59
|
-
isRetrying,
|
|
60
|
-
onReset,
|
|
61
|
-
spaGuardState
|
|
62
|
-
}) => {
|
|
63
|
-
const containerRef = useRef(null);
|
|
64
|
-
const html = useMemo(() => {
|
|
65
|
-
const opts = getOptions();
|
|
66
|
-
if (isRetrying) {
|
|
67
|
-
const loadingTemplate = opts.html?.loading?.content ?? defaultLoadingFallbackHtml;
|
|
68
|
-
return buildHtml(loadingTemplate, {
|
|
69
|
-
content: {
|
|
70
|
-
attempt: String(spaGuardState.currentAttempt)
|
|
71
|
-
},
|
|
72
|
-
sections: {
|
|
73
|
-
retrying: true
|
|
74
|
-
},
|
|
75
|
-
spinnerHtml: opts.html?.spinner?.content
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const heading = isChunk ? "Failed to load module" : "Something went wrong";
|
|
79
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
-
const errorTemplate = opts.html?.fallback?.content ?? defaultErrorFallbackHtml;
|
|
81
|
-
return buildHtml(errorTemplate, {
|
|
82
|
-
actions: {
|
|
83
|
-
"try-again": Boolean(onReset)
|
|
84
|
-
},
|
|
85
|
-
content: {
|
|
86
|
-
heading: escapeHtml(heading),
|
|
87
|
-
message: escapeHtml(message)
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}, [isRetrying, isChunk, error, onReset, spaGuardState.currentAttempt]);
|
|
91
|
-
useLayoutEffect(() => {
|
|
92
|
-
const el = containerRef.current;
|
|
93
|
-
if (!el) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const reloadBtn = el.querySelector('[data-spa-guard-action="reload"]');
|
|
97
|
-
reloadBtn?.addEventListener("click", reloadHandler);
|
|
98
|
-
const tryAgainHandler = onReset ? () => onReset() : null;
|
|
99
|
-
const tryAgainBtn = onReset ? el.querySelector('[data-spa-guard-action="try-again"]') : null;
|
|
100
|
-
if (tryAgainHandler && tryAgainBtn) {
|
|
101
|
-
tryAgainBtn.addEventListener("click", tryAgainHandler);
|
|
102
|
-
}
|
|
103
|
-
return () => {
|
|
104
|
-
reloadBtn?.removeEventListener("click", reloadHandler);
|
|
105
|
-
if (tryAgainHandler && tryAgainBtn) {
|
|
106
|
-
tryAgainBtn.removeEventListener("click", tryAgainHandler);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}, [onReset, html]);
|
|
110
|
-
const innerHtml = useMemo(() => ({ __html: html }), [html]);
|
|
111
|
-
return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: innerHtml, ref: containerRef });
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// src/react/DebugSyncErrorTrigger.tsx
|
|
115
|
-
import { useEffect, useState } from "react";
|
|
116
|
-
import { debugSyncErrorEventType } from "@ovineko/spa-guard/_internal";
|
|
117
|
-
function DebugSyncErrorTrigger() {
|
|
118
|
-
const [error, setError] = useState(null);
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
const handler = (e) => {
|
|
121
|
-
const detail = e.detail;
|
|
122
|
-
if (detail?.error instanceof Error) {
|
|
123
|
-
setError(detail.error);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
globalThis.addEventListener(debugSyncErrorEventType, handler);
|
|
127
|
-
return () => {
|
|
128
|
-
globalThis.removeEventListener(debugSyncErrorEventType, handler);
|
|
129
|
-
};
|
|
130
|
-
}, []);
|
|
131
|
-
if (error) {
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// src/react/lazyWithRetry.tsx
|
|
138
|
-
import { lazy } from "react";
|
|
139
|
-
import { getOptions as getOptions2, retryImport } from "@ovineko/spa-guard/_internal";
|
|
140
|
-
var lazyWithRetry = (importFn, options) => {
|
|
141
|
-
return lazy(() => {
|
|
142
|
-
const globalLazyRetry = getOptions2().lazyRetry ?? {};
|
|
143
|
-
const retryDelays = options?.retryDelays ?? globalLazyRetry.retryDelays ?? [1e3, 2e3];
|
|
144
|
-
const callReloadOnFailure = options?.callReloadOnFailure ?? globalLazyRetry.callReloadOnFailure ?? true;
|
|
145
|
-
const signal = options?.signal;
|
|
146
|
-
return retryImport(importFn, retryDelays, { callReloadOnFailure, signal });
|
|
147
|
-
});
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// src/react/Spinner.tsx
|
|
151
|
-
import { useMemo as useMemo2 } from "react";
|
|
152
|
-
import { getOptions as getOptions3 } from "@ovineko/spa-guard/_internal";
|
|
153
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
154
|
-
function Spinner(props) {
|
|
155
|
-
const opts = getOptions3();
|
|
156
|
-
const content = opts.html?.spinner?.disabled ? void 0 : opts.html?.spinner?.content;
|
|
157
|
-
const innerHtml = useMemo2(() => content ? { __html: content } : null, [content]);
|
|
158
|
-
if (!innerHtml) {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
return /* @__PURE__ */ jsx2("div", { ...props, dangerouslySetInnerHTML: innerHtml });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// src/react/useSPAGuardChunkError.ts
|
|
165
|
-
import { useCallback, useState as useState2 } from "react";
|
|
166
|
-
|
|
167
|
-
// src/react/useSPAGuardEvents.ts
|
|
168
|
-
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
169
|
-
import { subscribe } from "@ovineko/spa-guard/_internal";
|
|
170
|
-
var useSPAGuardEvents = (callback) => {
|
|
171
|
-
const callbackRef = useRef2(callback);
|
|
172
|
-
useEffect2(() => {
|
|
173
|
-
callbackRef.current = callback;
|
|
174
|
-
});
|
|
175
|
-
useEffect2(() => {
|
|
176
|
-
return subscribe((event) => {
|
|
177
|
-
callbackRef.current(event);
|
|
178
|
-
});
|
|
179
|
-
}, []);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// src/react/useSPAGuardChunkError.ts
|
|
183
|
-
var useSPAGuardChunkError = () => {
|
|
184
|
-
const [chunkError, setChunkError] = useState2(null);
|
|
185
|
-
useSPAGuardEvents(
|
|
186
|
-
useCallback((event) => {
|
|
187
|
-
if (event.name === "chunk-error") {
|
|
188
|
-
setChunkError(event);
|
|
189
|
-
}
|
|
190
|
-
}, [])
|
|
191
|
-
);
|
|
192
|
-
return chunkError;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// src/react/index.tsx
|
|
196
|
-
import { ForceRetryError } from "@ovineko/spa-guard";
|
|
197
|
-
var useSpaGuardState = () => {
|
|
198
|
-
const [state, setState] = useState3(() => {
|
|
199
|
-
if (globalThis.window === void 0) {
|
|
200
|
-
return {
|
|
201
|
-
currentAttempt: 0,
|
|
202
|
-
isFallbackShown: false,
|
|
203
|
-
isWaiting: false
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return getState();
|
|
207
|
-
});
|
|
208
|
-
useEffect3(() => {
|
|
209
|
-
return subscribeToState(setState);
|
|
210
|
-
}, []);
|
|
211
|
-
return state;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
export {
|
|
215
|
-
DefaultErrorFallback,
|
|
216
|
-
DebugSyncErrorTrigger,
|
|
217
|
-
lazyWithRetry,
|
|
218
|
-
Spinner,
|
|
219
|
-
useSPAGuardEvents,
|
|
220
|
-
useSPAGuardChunkError,
|
|
221
|
-
useSpaGuardState,
|
|
222
|
-
ForceRetryError
|
|
223
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Renders nothing normally. When a CustomEvent of type debugSyncErrorEventType
|
|
3
|
-
* is dispatched on window, this component stores the error in state and throws
|
|
4
|
-
* it during the next render, allowing a parent React Error Boundary to catch it.
|
|
5
|
-
*
|
|
6
|
-
* Place this component inside your ErrorBoundary:
|
|
7
|
-
*
|
|
8
|
-
* <ErrorBoundary fallback={<CrashPage />}>
|
|
9
|
-
* <DebugSyncErrorTrigger />
|
|
10
|
-
* <App />
|
|
11
|
-
* </ErrorBoundary>
|
|
12
|
-
*/
|
|
13
|
-
export declare function DebugSyncErrorTrigger(): null;
|
package/dist/react/Spinner.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { ComponentProps } from "react";
|
|
2
|
-
type SpinnerProps = Omit<ComponentProps<"div">, "children" | "dangerouslySetInnerHTML">;
|
|
3
|
-
/**
|
|
4
|
-
* Renders the spa-guard spinner inside a div.
|
|
5
|
-
* Returns null if spinner is disabled or no content available.
|
|
6
|
-
* All div props forwarded to wrapper element.
|
|
7
|
-
*/
|
|
8
|
-
export declare function Spinner(props: SpinnerProps): null | React.ReactElement;
|
|
9
|
-
export {};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { type ComponentType, type LazyExoticComponent } from "react";
|
|
2
|
-
import type { LazyRetryOptions } from "./types";
|
|
3
|
-
export type { LazyRetryOptions } from "./types";
|
|
4
|
-
/**
|
|
5
|
-
* Creates a lazy-loaded React component with automatic retry on chunk load failures.
|
|
6
|
-
*
|
|
7
|
-
* On import failure, retries with configurable delays before falling back to
|
|
8
|
-
* `triggerRetry()` for a full page reload.
|
|
9
|
-
*
|
|
10
|
-
* @param importFn - Function that performs the dynamic import
|
|
11
|
-
* @param options - Per-import options that override global lazyRetry options
|
|
12
|
-
* @returns A lazy React component with retry logic
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* // Basic usage with global options
|
|
16
|
-
* const LazyHome = lazyWithRetry(() => import('./pages/Home'));
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* // Override retry delays for a critical component
|
|
20
|
-
* const LazyCheckout = lazyWithRetry(
|
|
21
|
-
* () => import('./pages/Checkout'),
|
|
22
|
-
* { retryDelays: [500, 1000, 2000, 4000] }
|
|
23
|
-
* );
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* // Disable page reload for a non-critical component
|
|
27
|
-
* const LazyWidget = lazyWithRetry(
|
|
28
|
-
* () => import('./widgets/Optional'),
|
|
29
|
-
* { retryDelays: [1000], callReloadOnFailure: false }
|
|
30
|
-
* );
|
|
31
|
-
*/
|
|
32
|
-
export declare const lazyWithRetry: <T extends ComponentType<any>>(importFn: () => Promise<{
|
|
33
|
-
default: T;
|
|
34
|
-
}>, options?: LazyRetryOptions) => LazyExoticComponent<T>;
|
package/dist/react/types.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-import options for lazyWithRetry that override global lazyRetry options.
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* // Override retry delays for a critical component
|
|
6
|
-
* const LazyCheckout = lazyWithRetry(
|
|
7
|
-
* () => import('./pages/Checkout'),
|
|
8
|
-
* { retryDelays: [500, 1000, 2000, 4000] } satisfies LazyRetryOptions
|
|
9
|
-
* );
|
|
10
|
-
*/
|
|
11
|
-
export interface LazyRetryOptions {
|
|
12
|
-
/**
|
|
13
|
-
* If true, triggers a full page reload via triggerRetry() after all retry attempts are exhausted.
|
|
14
|
-
* If false, only throws the error to the error boundary without reload.
|
|
15
|
-
* Overrides the global `window.__SPA_GUARD_OPTIONS__.lazyRetry.callReloadOnFailure`.
|
|
16
|
-
*
|
|
17
|
-
* @default true (inherited from global options)
|
|
18
|
-
*/
|
|
19
|
-
callReloadOnFailure?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Array of delays in milliseconds for retry attempts.
|
|
22
|
-
* Each element represents one retry attempt with the given delay.
|
|
23
|
-
* The number of elements determines the number of retry attempts.
|
|
24
|
-
* Overrides the global `window.__SPA_GUARD_OPTIONS__.lazyRetry.retryDelays`.
|
|
25
|
-
*
|
|
26
|
-
* @default [1000, 2000] (inherited from global options)
|
|
27
|
-
* @example [500, 1500, 3000] // 3 attempts: 500ms, 1.5s, 3s
|
|
28
|
-
*/
|
|
29
|
-
retryDelays?: number[];
|
|
30
|
-
/**
|
|
31
|
-
* AbortSignal to cancel pending retry delays between import attempts.
|
|
32
|
-
* When the signal fires, any pending setTimeout between retries is cleared
|
|
33
|
-
* and the import promise rejects with an AbortError.
|
|
34
|
-
*
|
|
35
|
-
* Note: cancels only the wait periods between retry attempts, not an in-flight
|
|
36
|
-
* dynamic import (JavaScript does not support cancelling in-flight module fetches).
|
|
37
|
-
* Most useful when calling `retryImport` directly rather than through `lazyWithRetry`,
|
|
38
|
-
* since `lazyWithRetry` captures the signal at module scope (not per component instance).
|
|
39
|
-
*/
|
|
40
|
-
signal?: AbortSignal;
|
|
41
|
-
}
|