@ovineko/spa-guard-react 0.0.1-alpha-18
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/dist/DefaultErrorFallback.d.ts +17 -0
- package/dist/chunk-RR7KT33N.js +223 -0
- package/dist/react/DebugSyncErrorTrigger.d.ts +13 -0
- package/dist/react/Spinner.d.ts +9 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.js +20 -0
- package/dist/react/lazyWithRetry.d.ts +34 -0
- package/dist/react/types.d.ts +42 -0
- package/dist/react/useSPAGuardChunkError.d.ts +2 -0
- package/dist/react/useSPAGuardEvents.d.ts +2 -0
- package/dist/react-error-boundary/index.d.ts +31 -0
- package/dist/react-error-boundary/index.js +83 -0
- package/package.json +50 -0
|
@@ -0,0 +1,17 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,223 @@
|
|
|
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.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.spinner?.disabled ? void 0 : opts.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
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
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;
|
|
@@ -0,0 +1,9 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { DefaultErrorFallback } from "../DefaultErrorFallback";
|
|
2
|
+
export { DebugSyncErrorTrigger } from "./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;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DebugSyncErrorTrigger,
|
|
3
|
+
DefaultErrorFallback,
|
|
4
|
+
ForceRetryError,
|
|
5
|
+
Spinner,
|
|
6
|
+
lazyWithRetry,
|
|
7
|
+
useSPAGuardChunkError,
|
|
8
|
+
useSPAGuardEvents,
|
|
9
|
+
useSpaGuardState
|
|
10
|
+
} from "../chunk-RR7KT33N.js";
|
|
11
|
+
export {
|
|
12
|
+
DebugSyncErrorTrigger,
|
|
13
|
+
DefaultErrorFallback,
|
|
14
|
+
ForceRetryError,
|
|
15
|
+
Spinner,
|
|
16
|
+
lazyWithRetry,
|
|
17
|
+
useSPAGuardChunkError,
|
|
18
|
+
useSPAGuardEvents,
|
|
19
|
+
useSpaGuardState
|
|
20
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
* `attemptReload()` 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>;
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
* Call attemptReload() after all retry attempts are exhausted.
|
|
14
|
+
* If true, triggers page reload logic after all retries fail.
|
|
15
|
+
* If false, only throws the error to the error boundary without reload.
|
|
16
|
+
* Overrides the global `window.__SPA_GUARD_OPTIONS__.lazyRetry.callReloadOnFailure`.
|
|
17
|
+
*
|
|
18
|
+
* @default true (inherited from global options)
|
|
19
|
+
*/
|
|
20
|
+
callReloadOnFailure?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Array of delays in milliseconds for retry attempts.
|
|
23
|
+
* Each element represents one retry attempt with the given delay.
|
|
24
|
+
* The number of elements determines the number of retry attempts.
|
|
25
|
+
* Overrides the global `window.__SPA_GUARD_OPTIONS__.lazyRetry.retryDelays`.
|
|
26
|
+
*
|
|
27
|
+
* @default [1000, 2000] (inherited from global options)
|
|
28
|
+
* @example [500, 1500, 3000] // 3 attempts: 500ms, 1.5s, 3s
|
|
29
|
+
*/
|
|
30
|
+
retryDelays?: number[];
|
|
31
|
+
/**
|
|
32
|
+
* AbortSignal to cancel pending retry delays between import attempts.
|
|
33
|
+
* When the signal fires, any pending setTimeout between retries is cleared
|
|
34
|
+
* and the import promise rejects with an AbortError.
|
|
35
|
+
*
|
|
36
|
+
* Note: cancels only the wait periods between retry attempts, not an in-flight
|
|
37
|
+
* dynamic import (JavaScript does not support cancelling in-flight module fetches).
|
|
38
|
+
* Most useful when calling `retryImport` directly rather than through `lazyWithRetry`,
|
|
39
|
+
* since `lazyWithRetry` captures the signal at module scope (not per component instance).
|
|
40
|
+
*/
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type SpaGuardState } from "../react";
|
|
3
|
+
/**
|
|
4
|
+
* Props for the ErrorBoundary component
|
|
5
|
+
*/
|
|
6
|
+
export interface ErrorBoundaryProps {
|
|
7
|
+
autoRetryChunkErrors?: boolean;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
fallback?: ((props: FallbackProps) => React.ReactElement) | React.ComponentType<FallbackProps>;
|
|
10
|
+
fallbackRender?: (props: FallbackProps) => React.ReactElement;
|
|
11
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
12
|
+
resetKeys?: Array<unknown>;
|
|
13
|
+
sendBeaconOnError?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Props passed to the fallback component when an error is caught
|
|
17
|
+
*/
|
|
18
|
+
export interface FallbackProps {
|
|
19
|
+
error: Error;
|
|
20
|
+
errorInfo: null | React.ErrorInfo;
|
|
21
|
+
isChunkError: boolean;
|
|
22
|
+
isRetrying: boolean;
|
|
23
|
+
resetError: () => void;
|
|
24
|
+
spaGuardState: SpaGuardState;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Error boundary component with spa-guard integration.
|
|
28
|
+
*
|
|
29
|
+
* Catches errors in child components and automatically retries chunk loading errors.
|
|
30
|
+
*/
|
|
31
|
+
export declare const ErrorBoundary: React.FC<ErrorBoundaryProps>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultErrorFallback,
|
|
3
|
+
useSpaGuardState
|
|
4
|
+
} from "../chunk-RR7KT33N.js";
|
|
5
|
+
|
|
6
|
+
// src/react-error-boundary/index.tsx
|
|
7
|
+
import { Component } from "react";
|
|
8
|
+
import { handleErrorWithSpaGuard, isChunkError } from "@ovineko/spa-guard/_internal";
|
|
9
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
var DefaultFallback = ({
|
|
11
|
+
error,
|
|
12
|
+
isChunkError: isChunk,
|
|
13
|
+
isRetrying,
|
|
14
|
+
resetError,
|
|
15
|
+
spaGuardState
|
|
16
|
+
}) => /* @__PURE__ */ jsx(
|
|
17
|
+
DefaultErrorFallback,
|
|
18
|
+
{
|
|
19
|
+
error,
|
|
20
|
+
isChunkError: isChunk,
|
|
21
|
+
isRetrying,
|
|
22
|
+
onReset: resetError,
|
|
23
|
+
spaGuardState
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
var ErrorBoundaryImpl = class extends Component {
|
|
27
|
+
state = { error: null, errorInfo: null };
|
|
28
|
+
static getDerivedStateFromError(error) {
|
|
29
|
+
return { error };
|
|
30
|
+
}
|
|
31
|
+
componentDidCatch(error, errorInfo) {
|
|
32
|
+
this.setState({ errorInfo });
|
|
33
|
+
const { autoRetryChunkErrors, onError, sendBeaconOnError } = this.props;
|
|
34
|
+
handleErrorWithSpaGuard(error, {
|
|
35
|
+
autoRetryChunkErrors,
|
|
36
|
+
errorInfo,
|
|
37
|
+
eventName: "react-error-boundary",
|
|
38
|
+
onError: () => onError?.(error, errorInfo),
|
|
39
|
+
sendBeaconOnError
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
componentDidUpdate(prevProps) {
|
|
43
|
+
const { resetKeys = [] } = this.props;
|
|
44
|
+
const prevResetKeys = prevProps.resetKeys ?? [];
|
|
45
|
+
if (resetKeys.length !== prevResetKeys.length || resetKeys.some((key, i) => key !== prevResetKeys[i])) {
|
|
46
|
+
this.resetError();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
render() {
|
|
50
|
+
const { error, errorInfo } = this.state;
|
|
51
|
+
if (!error) {
|
|
52
|
+
return this.props.children;
|
|
53
|
+
}
|
|
54
|
+
const { fallback: Fallback, fallbackRender, spaGuardState } = this.props;
|
|
55
|
+
const isChunk = isChunkError(error);
|
|
56
|
+
const isRetrying = spaGuardState.isWaiting && spaGuardState.currentAttempt > 0;
|
|
57
|
+
const fallbackProps = {
|
|
58
|
+
error,
|
|
59
|
+
errorInfo,
|
|
60
|
+
isChunkError: isChunk,
|
|
61
|
+
isRetrying,
|
|
62
|
+
resetError: this.resetError,
|
|
63
|
+
spaGuardState
|
|
64
|
+
};
|
|
65
|
+
if (fallbackRender) {
|
|
66
|
+
return fallbackRender(fallbackProps);
|
|
67
|
+
}
|
|
68
|
+
if (Fallback) {
|
|
69
|
+
return /* @__PURE__ */ jsx(Fallback, { ...fallbackProps });
|
|
70
|
+
}
|
|
71
|
+
return /* @__PURE__ */ jsx(DefaultFallback, { ...fallbackProps });
|
|
72
|
+
}
|
|
73
|
+
resetError = () => {
|
|
74
|
+
this.setState({ error: null, errorInfo: null });
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
var ErrorBoundary = (props) => {
|
|
78
|
+
const spaGuardState = useSpaGuardState();
|
|
79
|
+
return /* @__PURE__ */ jsx(ErrorBoundaryImpl, { ...props, spaGuardState });
|
|
80
|
+
};
|
|
81
|
+
export {
|
|
82
|
+
ErrorBoundary
|
|
83
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ovineko/spa-guard-react",
|
|
3
|
+
"version": "0.0.1-alpha-18",
|
|
4
|
+
"description": "React hooks, components, and error boundaries for spa-guard",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"spa",
|
|
7
|
+
"react",
|
|
8
|
+
"error-boundary",
|
|
9
|
+
"chunk-load-error"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/ovineko/ovineko/tree/main/spa-guard/react",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ovineko/ovineko/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ovineko/ovineko.git",
|
|
18
|
+
"directory": "spa-guard/react"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Alexander Svinarev <shibanet0@gmail.com> (shibanet0.com)",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/react/index.d.ts",
|
|
27
|
+
"default": "./dist/react/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./error-boundary": {
|
|
30
|
+
"types": "./dist/react-error-boundary/index.d.ts",
|
|
31
|
+
"default": "./dist/react-error-boundary/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@ovineko/spa-guard": "0.0.1-alpha-18"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": "^19"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22.15.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|