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 +71 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +65 -0
- package/dist/index.mjs +38 -0
- package/package.json +37 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|