lazy-retry 0.1.0

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 ADDED
@@ -0,0 +1,71 @@
1
+ # lazy-retry
2
+
3
+ > React.lazy wrapper that auto-recovers from chunk 404s after deployment.
4
+
5
+ When you deploy a new version of your app, old chunk hashes are gone from the server. Users who have the old page open get a `Loading chunk N failed` error the next time they lazy-load a route. This package catches that, does a **single page reload** (debounced to 30 s via `sessionStorage`), and gets them onto the new bundle — silently.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install lazy-retry
11
+ # or
12
+ pnpm add lazy-retry
13
+ ```
14
+
15
+ React 17+ is a peer dependency.
16
+
17
+ ## Usage
18
+
19
+ ```tsx
20
+ // Before
21
+ const Dashboard = React.lazy(() => import('./pages/Dashboard'));
22
+
23
+ // After — one word change
24
+ import { lazyWithRetry } from 'lazy-retry';
25
+
26
+ const Dashboard = lazyWithRetry(() => import('./pages/Dashboard'));
27
+ ```
28
+
29
+ Wrap your routes in a `<Suspense>` and an `ErrorBoundary` as you normally would. `lazyWithRetry` handles the chunk-404 case; the `ErrorBoundary` handles anything else.
30
+
31
+ ```tsx
32
+ <ErrorBoundary fallback={<ErrorPage />}>
33
+ <Suspense fallback={<Spinner />}>
34
+ <Dashboard />
35
+ </Suspense>
36
+ </ErrorBoundary>
37
+ ```
38
+
39
+ ## How it works
40
+
41
+ 1. Calls your import factory normally.
42
+ 2. If the import throws and the error message matches known chunk-load failure patterns (Webpack, Vite, Safari), it checks `sessionStorage` for a recent reload timestamp.
43
+ 3. If no reload has happened in the last 30 s, it stamps the timestamp and calls `window.location.reload()`.
44
+ 4. If a reload already happened recently (user is on a bad connection, not a stale chunk), it re-throws so your `ErrorBoundary` can show a proper fallback.
45
+
46
+ ## API
47
+
48
+ ### `lazyWithRetry(factory)`
49
+
50
+ | Param | Type | Description |
51
+ |-------|------|-------------|
52
+ | `factory` | `() => Promise<{ default: ComponentType }>` | Same signature as `React.lazy` |
53
+
54
+ Returns the same type as `React.lazy` — a lazy component you use identically.
55
+
56
+ ### `isChunkError(err)` *(exported for testing)*
57
+
58
+ Returns `true` if the error looks like a chunk load failure. Covers Webpack, Vite, and Safari error messages.
59
+
60
+ ### `hasReloadedRecently()` *(exported for testing)*
61
+
62
+ Returns `true` if a reload was recorded in `sessionStorage` within the last 30 s.
63
+
64
+ ## Caveats
65
+
66
+ - Calls `window.location.reload()` — SSR/Node environments should not use this package.
67
+ - Only one auto-reload per 30 s per browser tab. Subsequent failures surface to your `ErrorBoundary`.
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,19 @@
1
+ import { ComponentType, lazy } from 'react';
2
+
3
+ type ImportFactory<T extends ComponentType<unknown>> = () => Promise<{
4
+ default: T;
5
+ }>;
6
+ declare function hasReloadedRecently(): boolean;
7
+ declare function isChunkError(err: unknown): boolean;
8
+ /**
9
+ * Drop-in replacement for React.lazy that recovers from chunk 404s after
10
+ * a new deployment. On failure it does a single full-page reload (debounced
11
+ * to 30 s via sessionStorage) then re-throws so an ErrorBoundary can render
12
+ * a fallback if reloading doesn't help.
13
+ *
14
+ * @example
15
+ * const MyPage = lazyWithRetry(() => import('./MyPage'));
16
+ */
17
+ declare function lazyWithRetry<T extends ComponentType<unknown>>(factory: ImportFactory<T>): ReturnType<typeof lazy<T>>;
18
+
19
+ export { hasReloadedRecently, isChunkError, lazyWithRetry };
@@ -0,0 +1,19 @@
1
+ import { ComponentType, lazy } from 'react';
2
+
3
+ type ImportFactory<T extends ComponentType<unknown>> = () => Promise<{
4
+ default: T;
5
+ }>;
6
+ declare function hasReloadedRecently(): boolean;
7
+ declare function isChunkError(err: unknown): boolean;
8
+ /**
9
+ * Drop-in replacement for React.lazy that recovers from chunk 404s after
10
+ * a new deployment. On failure it does a single full-page reload (debounced
11
+ * to 30 s via sessionStorage) then re-throws so an ErrorBoundary can render
12
+ * a fallback if reloading doesn't help.
13
+ *
14
+ * @example
15
+ * const MyPage = lazyWithRetry(() => import('./MyPage'));
16
+ */
17
+ declare function lazyWithRetry<T extends ComponentType<unknown>>(factory: ImportFactory<T>): ReturnType<typeof lazy<T>>;
18
+
19
+ export { hasReloadedRecently, isChunkError, lazyWithRetry };
package/dist/index.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ hasReloadedRecently: () => hasReloadedRecently,
24
+ isChunkError: () => isChunkError,
25
+ lazyWithRetry: () => lazyWithRetry
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_react = require("react");
29
+ var RELOAD_KEY = "lazy-retry:reloaded";
30
+ var DEBOUNCE_MS = 3e4;
31
+ function hasReloadedRecently() {
32
+ const raw = sessionStorage.getItem(RELOAD_KEY);
33
+ if (!raw) return false;
34
+ return Date.now() - parseInt(raw, 10) < DEBOUNCE_MS;
35
+ }
36
+ function markReloaded() {
37
+ sessionStorage.setItem(RELOAD_KEY, String(Date.now()));
38
+ }
39
+ function isChunkError(err) {
40
+ if (!(err instanceof Error)) return false;
41
+ return /loading chunk [\w]+ failed|failed to fetch dynamically imported module|importing a module script failed/i.test(
42
+ err.message
43
+ );
44
+ }
45
+ function lazyWithRetry(factory) {
46
+ return (0, import_react.lazy)(async () => {
47
+ try {
48
+ return await factory();
49
+ } catch (err) {
50
+ if (isChunkError(err) && !hasReloadedRecently()) {
51
+ markReloaded();
52
+ window.location.reload();
53
+ return new Promise(() => {
54
+ });
55
+ }
56
+ throw err;
57
+ }
58
+ });
59
+ }
60
+ // Annotate the CommonJS export names for ESM import in node:
61
+ 0 && (module.exports = {
62
+ hasReloadedRecently,
63
+ isChunkError,
64
+ lazyWithRetry
65
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,38 @@
1
+ // src/index.ts
2
+ import { lazy } from "react";
3
+ var RELOAD_KEY = "lazy-retry:reloaded";
4
+ var DEBOUNCE_MS = 3e4;
5
+ function hasReloadedRecently() {
6
+ const raw = sessionStorage.getItem(RELOAD_KEY);
7
+ if (!raw) return false;
8
+ return Date.now() - parseInt(raw, 10) < DEBOUNCE_MS;
9
+ }
10
+ function markReloaded() {
11
+ sessionStorage.setItem(RELOAD_KEY, String(Date.now()));
12
+ }
13
+ function isChunkError(err) {
14
+ if (!(err instanceof Error)) return false;
15
+ return /loading chunk [\w]+ failed|failed to fetch dynamically imported module|importing a module script failed/i.test(
16
+ err.message
17
+ );
18
+ }
19
+ function lazyWithRetry(factory) {
20
+ return lazy(async () => {
21
+ try {
22
+ return await factory();
23
+ } catch (err) {
24
+ if (isChunkError(err) && !hasReloadedRecently()) {
25
+ markReloaded();
26
+ window.location.reload();
27
+ return new Promise(() => {
28
+ });
29
+ }
30
+ throw err;
31
+ }
32
+ });
33
+ }
34
+ export {
35
+ hasReloadedRecently,
36
+ isChunkError,
37
+ lazyWithRetry
38
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "lazy-retry",
3
+ "version": "0.1.0",
4
+ "description": "React.lazy wrapper that auto-reloads on chunk 404s after deployment",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
18
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "lint": "tsc --noEmit"
22
+ },
23
+ "keywords": ["react", "lazy", "retry", "chunk", "404", "deployment", "code-splitting"],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "peerDependencies": {
27
+ "react": ">=17.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@testing-library/react": "^14.0.0",
31
+ "@types/react": "^18.0.0",
32
+ "jsdom": "^24.0.0",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.0.0",
35
+ "vitest": "^1.0.0"
36
+ }
37
+ }