From f77bae90b4f728a3ef272325866c0c762255e5f4 Mon Sep 17 00:00:00 2001
From: Max
Date: Fri, 16 Oct 2026 23:01:49 +0200
Subject: [PATCH 1/8] feat: add update prompt for new builds
ClearBrowserCacheBoundary takes an `updatePrompt` element or component and
shows it when polling finds a newer build and `auto` is off, so the app can
ask before reloading instead of only flipping `isLatestVersion`. With
`promptOnError`, a stale-chunk error shows the prompt over `fallback` rather
than reloading at once; dismissing it hands the error to the parent boundary.
The context gains `dismissUpdate`, which hides the prompt until a newer build.
The package also exports `UpdatePrompt`, an unstyled native
}
+ auto={params.get('auto') === '1'}
+ duration={Number(params.get('duration')) || undefined}
+ filename={`${import.meta.env.BASE_URL}meta.json`}
+ debug={debug}
+ updatePrompt={params.get('prompt') === '1' ? UpdatePrompt : undefined}
+ promptOnError={params.get('promptOnError') === '1'}
+ >
+
+
+
+
+ );
+}
diff --git a/example/src/vite-env.d.ts b/example/src/vite-env.d.ts
new file mode 100644
index 0000000..463cd71
--- /dev/null
+++ b/example/src/vite-env.d.ts
@@ -0,0 +1,5 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_DEMO?: string;
+}
diff --git a/package.json b/package.json
index 0a33ea7..f6e25e3 100644
--- a/package.json
+++ b/package.json
@@ -48,13 +48,15 @@
"typecheck": "tsc --noEmit && tsc --noEmit -p example && tsc --noEmit -p e2e",
"lint": "eslint . && prettier --check . && pnpm run lint:html",
"lint:html": "html-validate example/index.html",
- "test": "pnpm run lint && pnpm run build && pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e",
+ "test": "pnpm run lint && pnpm run build && pnpm run typecheck && pnpm run test:unit && playwright test",
"test:unit": "vitest run",
"test:watch": "vitest",
- "test:e2e": "playwright test",
+ "test:e2e": "pnpm run build && playwright test",
"example:dev": "pnpm --filter react-clear-browser-cache-example dev",
"example:build": "pnpm --filter react-clear-browser-cache-example build",
- "deploy": "BASE_PATH=./ pnpm run example:build && gh-pages -d example/dist"
+ "demo:dev": "VITE_DEMO=1 pnpm run example:dev",
+ "demo:build": "VITE_DEMO=1 BASE_PATH=/react-clear-browser-cache/ pnpm --filter react-clear-browser-cache-example exec vite build --outDir dist-demo",
+ "demo:preview": "BASE_PATH=/react-clear-browser-cache/ pnpm --filter react-clear-browser-cache-example preview --outDir dist-demo"
},
"peerDependencies": {
"react": ">=18.0.0"
@@ -71,7 +73,6 @@
"eslint": "^10.13.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
- "gh-pages": "^6.3.0",
"globals": "^17.13.0",
"html-validate": "^11.16.2",
"jsdom": "^30.1.3",
diff --git a/playwright.config.mts b/playwright.config.mts
index b19969c..1ee43e0 100644
--- a/playwright.config.mts
+++ b/playwright.config.mts
@@ -1,6 +1,8 @@
import { defineConfig, devices } from '@playwright/test';
const port = 4173;
+const demoPort = 4174;
+const demoBase = '/react-clear-browser-cache/';
export default defineConfig({
testDir: 'e2e',
@@ -13,15 +15,40 @@ export default defineConfig({
trace: 'retain-on-failure'
},
projects: [
- { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
- { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
- { name: 'webkit', use: { ...devices['Desktop Safari'] } }
+ ...(['Desktop Chrome', 'Desktop Firefox', 'Desktop Safari'] as const).map(
+ (device) => ({
+ name: devices[device].defaultBrowserType,
+ testIgnore: /demo\.spec/,
+ use: { ...devices[device] }
+ })
+ ),
+ {
+ name: 'demo',
+ testMatch: /demo\.spec/,
+ use: {
+ ...devices['Desktop Chrome'],
+ baseURL: `http://localhost:${demoPort}${demoBase}`
+ }
+ }
],
- webServer: {
- // The built index.html is checked too: the build rewrites it.
- command: `pnpm build && pnpm example:build && html-validate example/dist/index.html && pnpm --filter react-clear-browser-cache-example preview --port ${port} --strictPort`,
- url: `http://localhost:${port}`,
- reuseExistingServer: !process.env.CI,
- timeout: 120_000
- }
+ webServer: [
+ {
+ // Both servers need the library built first, which `test:e2e` does; two
+ // tsdown runs here would race on dist. The built index.html is checked
+ // too: the build rewrites it.
+ command: `pnpm example:build && html-validate example/dist/index.html && pnpm --filter react-clear-browser-cache-example preview --port ${port} --strictPort`,
+ url: `http://localhost:${port}`,
+ reuseExistingServer: !process.env.CI,
+ timeout: 120_000
+ },
+ {
+ // Served under the same sub-path as GitHub Pages, so broken asset URLs
+ // fail here first. `demo:build` skips the meta.json prebuild step (the
+ // demo's fetch shim answers it), so it cannot race the build above.
+ command: `pnpm demo:build && pnpm demo:preview --port ${demoPort} --strictPort`,
+ url: `http://localhost:${demoPort}${demoBase}`,
+ reuseExistingServer: !process.env.CI,
+ timeout: 120_000
+ }
+ ]
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6144c89..94c7031 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,9 +41,6 @@ importers:
eslint-plugin-react-hooks:
specifier: ^7.1.1
version: 7.1.1(eslint@10.13.0)
- gh-pages:
- specifier: ^6.3.0
- version: 6.3.0
globals:
specifier: ^17.13.0
version: 17.13.0
@@ -330,18 +327,6 @@ packages:
'@keyv/serialize@1.1.1':
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
- '@nodelib/fs.scandir@2.1.5':
- resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
- engines: {node: '>= 8'}
-
- '@nodelib/fs.stat@2.0.5':
- resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
- engines: {node: '>= 8'}
-
- '@nodelib/fs.walk@1.2.8':
- resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
- engines: {node: '>= 8'}
-
'@oxc-project/types@0.154.0':
resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==}
@@ -697,17 +682,10 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
- array-union@2.1.0:
- resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
- engines: {node: '>=8'}
-
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
- async@3.2.6:
- resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
-
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
@@ -724,10 +702,6 @@ packages:
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
engines: {node: 20 || >=22}
- braces@3.0.3:
- resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
- engines: {node: '>=8'}
-
browserslist@4.29.3:
resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -747,13 +721,6 @@ packages:
resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
engines: {node: '>=18'}
- commander@13.1.0:
- resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
- engines: {node: '>=18'}
-
- commondir@1.0.1:
- resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
-
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -801,10 +768,6 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
- dir-glob@3.0.1:
- resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
- engines: {node: '>=8'}
-
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@@ -823,9 +786,6 @@ packages:
electron-to-chromium@1.5.458:
resolution: {integrity: sha512-/hIH2gRSmxrf4/X4Nw/t/Wc8PVS22l8/3HnWjWTWsTOM5dsnMUPZF/+u+yr3LqweNuTBJE1rq6qAQzKkZVwMfA==}
- email-addresses@5.0.0:
- resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==}
-
empathic@2.1.0:
resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==}
engines: {node: '>=14'}
@@ -841,10 +801,6 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
- escape-string-regexp@1.0.5:
- resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
- engines: {node: '>=0.8.0'}
-
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -913,10 +869,6 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
- fast-glob@3.3.3:
- resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
- engines: {node: '>=8.6.0'}
-
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -926,9 +878,6 @@ packages:
fast-uri@3.1.8:
resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
- fastq@1.20.3:
- resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==}
-
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -941,26 +890,6 @@ packages:
file-entry-cache@11.1.5:
resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==}
- filename-reserved-regex@2.0.0:
- resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
- engines: {node: '>=4'}
-
- filenamify@4.3.0:
- resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==}
- engines: {node: '>=8'}
-
- fill-range@7.1.1:
- resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
- engines: {node: '>=8'}
-
- find-cache-dir@3.3.2:
- resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
- engines: {node: '>=8'}
-
- find-up@4.1.0:
- resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
- engines: {node: '>=8'}
-
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -971,10 +900,6 @@ packages:
flatted@3.4.4:
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
- fs-extra@11.4.1:
- resolution: {integrity: sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==}
- engines: {node: '>=14.14'}
-
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -988,15 +913,6 @@ packages:
resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==}
engines: {node: '>=20.20.0'}
- gh-pages@6.3.0:
- resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==}
- engines: {node: '>=10'}
- hasBin: true
-
- glob-parent@5.1.2:
- resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
- engines: {node: '>= 6'}
-
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
@@ -1005,13 +921,6 @@ packages:
resolution: {integrity: sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==}
engines: {node: '>=18'}
- globby@11.1.0:
- resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
- engines: {node: '>=10'}
-
- graceful-fs@4.2.11:
- resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
-
hashery@1.5.1:
resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==}
engines: {node: '>=20'}
@@ -1085,10 +994,6 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
- is-number@7.0.0:
- resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
- engines: {node: '>=0.12.0'}
-
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@@ -1126,9 +1031,6 @@ packages:
engines: {node: '>=6'}
hasBin: true
- jsonfile@6.2.1:
- resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
-
keyv@5.6.0:
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
@@ -1214,10 +1116,6 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
- locate-path@5.0.0:
- resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
- engines: {node: '>=8'}
-
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -1236,21 +1134,9 @@ packages:
magic-string@1.4.3:
resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==}
- make-dir@3.1.0:
- resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
- engines: {node: '>=8'}
-
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
- merge2@1.4.1:
- resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
- engines: {node: '>= 8'}
-
- micromatch@4.0.8:
- resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
- engines: {node: '>=8.6'}
-
min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
@@ -1286,26 +1172,14 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
- p-limit@2.3.0:
- resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
- engines: {node: '>=6'}
-
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
- p-locate@4.1.0:
- resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
- engines: {node: '>=8'}
-
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
- p-try@2.2.0:
- resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
- engines: {node: '>=6'}
-
package-manager-detector@1.9.0:
resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==}
@@ -1320,25 +1194,13 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
- path-type@4.0.0:
- resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
- engines: {node: '>=8'}
-
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
- picomatch@2.3.2:
- resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
- engines: {node: '>=8.6'}
-
picomatch@4.0.7:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
- pkg-dir@4.2.0:
- resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
- engines: {node: '>=8'}
-
playwright-core@1.64.1:
resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==}
engines: {node: '>=20'}
@@ -1386,9 +1248,6 @@ packages:
quansync@1.0.0:
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
- queue-microtask@1.2.3:
- resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
-
react-dom@19.3.1:
resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==}
peerDependencies:
@@ -1412,10 +1271,6 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
- reusify@1.1.0:
- resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
- engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
-
rolldown-plugin-dts@0.28.8:
resolution: {integrity: sha512-FVhfqxorCWcc20BVT5eVRfRjzLLl6mgzjcMR4gSBf0K4oQMXT0qoHhhzfXy0L7y83fQy7Ug5ZDNk90xM3/LiCQ==}
engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0}
@@ -1443,9 +1298,6 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
- run-parallel@1.2.0:
- resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
-
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -1477,10 +1329,6 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
- slash@3.0.0:
- resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
- engines: {node: '>=8'}
-
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
@@ -1492,10 +1340,6 @@ packages:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
- strip-outer@1.0.1:
- resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
- engines: {node: '>=0.10.0'}
-
tinybench@6.2.2:
resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
engines: {node: '>=20.0.0'}
@@ -1515,10 +1359,6 @@ packages:
resolution: {integrity: sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==}
hasBin: true
- to-regex-range@5.0.1:
- resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
- engines: {node: '>=8.0'}
-
tough-cookie@6.0.2:
resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==}
engines: {node: '>=16'}
@@ -1531,10 +1371,6 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
- trim-repeated@1.0.0:
- resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==}
- engines: {node: '>=0.10.0'}
-
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
@@ -1601,10 +1437,6 @@ packages:
resolution: {integrity: sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==}
engines: {node: '>=22.19.0'}
- universalify@2.0.1:
- resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
- engines: {node: '>= 10.0.0'}
-
unrun@0.3.1:
resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==}
engines: {node: ^22.13.0 || >=24.0.0}
@@ -2023,18 +1855,6 @@ snapshots:
'@keyv/serialize@1.1.1': {}
- '@nodelib/fs.scandir@2.1.5':
- dependencies:
- '@nodelib/fs.stat': 2.0.5
- run-parallel: 1.2.0
-
- '@nodelib/fs.stat@2.0.5': {}
-
- '@nodelib/fs.walk@1.2.8':
- dependencies:
- '@nodelib/fs.scandir': 2.1.5
- fastq: 1.20.3
-
'@oxc-project/types@0.154.0': {}
'@playwright/test@1.64.1':
@@ -2335,12 +2155,8 @@ snapshots:
aria-query@5.3.2: {}
- array-union@2.1.0: {}
-
assertion-error@2.0.1: {}
- async@3.2.6: {}
-
balanced-match@4.0.4: {}
baseline-browser-mapping@2.11.29: {}
@@ -2353,10 +2169,6 @@ snapshots:
dependencies:
balanced-match: 4.0.4
- braces@3.0.3:
- dependencies:
- fill-range: 7.1.1
-
browserslist@4.29.3:
dependencies:
baseline-browser-mapping: 2.11.29
@@ -2379,10 +2191,6 @@ snapshots:
chai@6.3.0: {}
- commander@13.1.0: {}
-
- commondir@1.0.1: {}
-
convert-source-map@2.0.0: {}
cross-spawn@7.0.6:
@@ -2421,10 +2229,6 @@ snapshots:
detect-libc@2.1.2: {}
- dir-glob@3.0.1:
- dependencies:
- path-type: 4.0.0
-
dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {}
@@ -2433,8 +2237,6 @@ snapshots:
electron-to-chromium@1.5.458: {}
- email-addresses@5.0.0: {}
-
empathic@2.1.0: {}
entities@8.1.0: {}
@@ -2443,8 +2245,6 @@ snapshots:
escalade@3.2.0: {}
- escape-string-regexp@1.0.5: {}
-
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@10.13.0):
@@ -2534,24 +2334,12 @@ snapshots:
fast-deep-equal@3.1.3: {}
- fast-glob@3.3.3:
- dependencies:
- '@nodelib/fs.stat': 2.0.5
- '@nodelib/fs.walk': 1.2.8
- glob-parent: 5.1.2
- merge2: 1.4.1
- micromatch: 4.0.8
-
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
fast-uri@3.1.8: {}
- fastq@1.20.3:
- dependencies:
- reusify: 1.1.0
-
fdir@6.5.0(picomatch@4.0.7):
optionalDependencies:
picomatch: 4.0.7
@@ -2560,29 +2348,6 @@ snapshots:
dependencies:
flat-cache: 6.1.23
- filename-reserved-regex@2.0.0: {}
-
- filenamify@4.3.0:
- dependencies:
- filename-reserved-regex: 2.0.0
- strip-outer: 1.0.1
- trim-repeated: 1.0.0
-
- fill-range@7.1.1:
- dependencies:
- to-regex-range: 5.0.1
-
- find-cache-dir@3.3.2:
- dependencies:
- commondir: 1.0.1
- make-dir: 3.1.0
- pkg-dir: 4.2.0
-
- find-up@4.1.0:
- dependencies:
- locate-path: 5.0.0
- path-exists: 4.0.0
-
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -2596,12 +2361,6 @@ snapshots:
flatted@3.4.4: {}
- fs-extra@11.4.1:
- dependencies:
- graceful-fs: 4.2.11
- jsonfile: 6.2.1
- universalify: 2.0.1
-
fsevents@2.3.3:
optional: true
@@ -2611,37 +2370,12 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
- gh-pages@6.3.0:
- dependencies:
- async: 3.2.6
- commander: 13.1.0
- email-addresses: 5.0.0
- filenamify: 4.3.0
- find-cache-dir: 3.3.2
- fs-extra: 11.4.1
- globby: 11.1.0
-
- glob-parent@5.1.2:
- dependencies:
- is-glob: 4.0.3
-
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
globals@17.13.0: {}
- globby@11.1.0:
- dependencies:
- array-union: 2.1.0
- dir-glob: 3.0.1
- fast-glob: 3.3.3
- ignore: 5.3.2
- merge2: 1.4.1
- slash: 3.0.0
-
- graceful-fs@4.2.11: {}
-
hashery@1.5.1:
dependencies:
hookified: 1.15.1
@@ -2691,8 +2425,6 @@ snapshots:
dependencies:
is-extglob: 2.1.1
- is-number@7.0.0: {}
-
is-potential-custom-element-name@1.0.1: {}
isexe@2.0.0: {}
@@ -2734,12 +2466,6 @@ snapshots:
json5@2.2.3: {}
- jsonfile@6.2.1:
- dependencies:
- universalify: 2.0.1
- optionalDependencies:
- graceful-fs: 4.2.11
-
keyv@5.6.0:
dependencies:
'@keyv/serialize': 1.1.1
@@ -2802,10 +2528,6 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
- locate-path@5.0.0:
- dependencies:
- p-locate: 4.1.0
-
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -2822,19 +2544,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.6.0
- make-dir@3.1.0:
- dependencies:
- semver: 6.3.1
-
mdn-data@2.27.1: {}
- merge2@1.4.1: {}
-
- micromatch@4.0.8:
- dependencies:
- braces: 3.0.3
- picomatch: 2.3.2
-
min-indent@1.0.1: {}
minimatch@10.2.6:
@@ -2862,24 +2573,14 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
- p-limit@2.3.0:
- dependencies:
- p-try: 2.2.0
-
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
- p-locate@4.1.0:
- dependencies:
- p-limit: 2.3.0
-
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
- p-try@2.2.0: {}
-
package-manager-detector@1.9.0: {}
parse5@8.0.1:
@@ -2890,18 +2591,10 @@ snapshots:
path-key@3.1.1: {}
- path-type@4.0.0: {}
-
picocolors@1.1.1: {}
- picomatch@2.3.2: {}
-
picomatch@4.0.7: {}
- pkg-dir@4.2.0:
- dependencies:
- find-up: 4.1.0
-
playwright-core@1.64.1: {}
playwright@1.64.1:
@@ -2944,8 +2637,6 @@ snapshots:
quansync@1.0.0: {}
- queue-microtask@1.2.3: {}
-
react-dom@19.3.1(react@19.3.1):
dependencies:
react: 19.3.1
@@ -2964,8 +2655,6 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
- reusify@1.1.0: {}
-
rolldown-plugin-dts@0.28.8(rolldown@1.2.14)(typescript@6.0.3):
dependencies:
dts-resolver: 3.0.0
@@ -3001,10 +2690,6 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.2.14
'@rolldown/binding-win32-x64-msvc': 1.2.14
- run-parallel@1.2.0:
- dependencies:
- queue-microtask: 1.2.3
-
sade@1.8.1:
dependencies:
mri: 1.2.0
@@ -3027,8 +2712,6 @@ snapshots:
sisteransi@1.0.5: {}
- slash@3.0.0: {}
-
source-map-js@1.2.2: {}
std-env@4.3.0: {}
@@ -3037,10 +2720,6 @@ snapshots:
dependencies:
min-indent: 1.0.1
- strip-outer@1.0.1:
- dependencies:
- escape-string-regexp: 1.0.5
-
tinybench@6.2.2: {}
tinyexec@1.3.1: {}
@@ -3056,10 +2735,6 @@ snapshots:
dependencies:
tldts-core: 7.4.18
- to-regex-range@5.0.1:
- dependencies:
- is-number: 7.0.0
-
tough-cookie@6.0.2:
dependencies:
tldts: 7.4.18
@@ -3070,10 +2745,6 @@ snapshots:
tree-kill@1.2.2: {}
- trim-repeated@1.0.0:
- dependencies:
- escape-string-regexp: 1.0.5
-
ts-api-utils@2.5.0(typescript@6.0.3):
dependencies:
typescript: 6.0.3
@@ -3131,8 +2802,6 @@ snapshots:
undici@8.12.2: {}
- universalify@2.0.1: {}
-
unrun@0.3.1:
dependencies:
rolldown: 1.2.14
From bc098c3c5341c172201cb4e83b5fd66e770fa98b Mon Sep 17 00:00:00 2001
From: Max
Date: Fri, 16 Oct 2026 23:39:17 +0200
Subject: [PATCH 3/8] docs: restructure README with quick start, API reference
and troubleshooting
Lead with the problem and a demo screenshot, list features, and add a table
of contents, a three-step quick start, a flow diagram, a full API section,
recipes and collapsible troubleshooting. Replace the inaccurate standard-style
badge with CI, downloads, bundle size, types and license badges.
---
README.md | 354 ++++++++++++++++++++++++++------------------
docs/demo-dark.png | Bin 0 -> 402340 bytes
docs/demo-light.png | Bin 0 -> 414586 bytes
3 files changed, 212 insertions(+), 142 deletions(-)
create mode 100644 docs/demo-dark.png
create mode 100644 docs/demo-light.png
diff --git a/README.md b/README.md
index 0114505..d41a6a5 100644
--- a/README.md
+++ b/README.md
@@ -1,157 +1,139 @@
# react-clear-browser-cache
-> Library for clearing browser cache after react app updates
-
-[](https://www.npmjs.com/package/react-clear-browser-cache) [](https://standardjs.com)
-
-**[Live demo](https://devmaxxx.github.io/react-clear-browser-cache/)**: deploy a new version in one click and watch an open tab notice it, prompt, clear Cache Storage and reload.
+> Keep open tabs on your latest React build: notice a deploy, clear Cache Storage and reload, or ask the user first.
+
+[](https://www.npmjs.com/package/react-clear-browser-cache)
+[](https://www.npmjs.com/package/react-clear-browser-cache)
+[](https://github.com/devmaxxx/react-clear-browser-cache/actions/workflows/ci.yml)
+[](https://bundlephobia.com/package/react-clear-browser-cache)
+
+[](#license)
+
+Users leave tabs open for days. After you deploy, those tabs still run the old build, and the moment they open a lazy route its chunk is gone from the server: `ChunkLoadError`, a blank screen, a support ticket. This library notices the new build and gets the tab onto it.
+
+**[▶ Try the live demo](https://devmaxxx.github.io/react-clear-browser-cache/)** — deploy a new version in one click and watch an open tab notice it, prompt, clear Cache Storage and reload.
+
+
+
+
+
+
+
+
+## Features
+
+- **Detects new builds** by polling a tiny `meta.json` that a CLI writes at build time.
+- **Recovers from stale-chunk errors** — webpack and Vite chunk failures, native `import()` errors in Chrome, Firefox and Safari, and HTML served in place of a script — and passes every other error on to your own error boundary.
+- **Asks or acts:** reload on its own, or show an accessible update prompt (built in, or your own component).
+- **Clears Cache Storage** before reloading, so a service worker cannot serve the old build again.
+- **`lazyRetry`** for flaky networks: retries a dynamic import before giving up.
+- **Small and typed:** React 18 and 19, ESM and CommonJS, TypeScript declarations, no runtime dependencies.
+
+## Contents
+
+- [Install](#install)
+- [Quick start](#quick-start)
+- [How it works](#how-it-works)
+- [Update prompt](#update-prompt)
+- [API](#api)
+- [Recipes](#recipes)
+- [Troubleshooting](#troubleshooting)
+- [Migrating from 0.2](#migrating-from-02)
+- [Contributing](#contributing)
+- [License](#license)
## Install
```bash
-npm install --save react-clear-browser-cache
-
-or
-
+npm install react-clear-browser-cache
+# or
+pnpm add react-clear-browser-cache
+# or
yarn add react-clear-browser-cache
```
Requires React 18 or newer. The `generate-build-meta` CLI needs Node.js 18.3 or newer.
-## Add script in package.json
+## Quick start
-Generate a `meta.json` file with a fresh build version before every build.
+**1. Write a build version on every build.** Add the CLI as a `prebuild` script:
```json
{
- "prebuild": "generate-build-meta"
+ "scripts": {
+ "prebuild": "generate-build-meta"
+ }
}
```
-The file is written to `./public/meta.json` by default; pass
-`--destination ` to write it elsewhere (for example
-`generate-build-meta --destination static/meta.json`). The command exits with
-code 1 when it cannot write the file.
-
-## Usage
+It writes `{ "version": "" }` to `./public/meta.json`. Use `--destination ` for another location, e.g. `generate-build-meta --destination static/meta.json`. It exits with code 1 when it cannot write the file.
-### Using `Boundary`:
-
-`ClearBrowserCacheBoundary` has context provider for child elements (`useClearBrowserCache`), add him after your `ErrorBoundary`, because `ClearBrowserCacheBoundary` only handles certain errors like `ChunkLoadError`.
+**2. Wrap your app** inside your own error boundary:
```tsx
import { createRoot } from 'react-dom/client';
-
-import { ClearBrowserCacheBoundary } from 'react-clear-browser-cache';
+import {
+ ClearBrowserCacheBoundary,
+ UpdatePrompt
+} from 'react-clear-browser-cache';
import App from './App';
import ErrorBoundary from './ErrorBoundary';
createRoot(document.getElementById('root')!).render(
-
+
);
```
-### How it decides to reload
-
-- On mount the boundary shows `fallback` and fetches the meta file. On the first
- visit it records that version and renders the app. If a newer build is found
- later, it clears Cache Storage and reloads the page — on mount always, while
- polling only when `auto` is set; otherwise it sets `isLatestVersion` to
- `false` and leaves the reload to you (`clearCacheAndReload`).
-- Polling runs every `duration` ms while the window has focus.
-- When a child throws an error that looks like a stale build (a missing chunk,
- or a chunk URL answered with `index.html`), the boundary checks the version:
- if a new build is out it reloads, otherwise the error goes on to your
- `ErrorBoundary`. Recognised out of the box: webpack `ChunkLoadError` and CSS
- chunk errors, Vite CSS preload errors, native `import()` failures in
- Chrome, Firefox and Safari, and `SyntaxError: Unexpected token '<'`.
-- A meta file that is missing, has no `version`, or is answered with HTML by an
- SPA fallback is reported to `debug` and otherwise ignored; the app still
- renders.
-
-## Props
-
-| Name | Type | Default | More info |
-| :------------ | :------------------- | :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| duration | number | | You can set the duration when to fetch for new updates. |
-| auto | boolean | false | Set to true to auto-reload the page whenever an update is available. |
-| fallback | any | null | You can set fallback data when fetching new version - preloader etc. |
-| storageKey | string | APP_VERSION | Storage key for saving app version. |
-| storage | object | localStorage | Any `{ get(key), set(key, value) }` object, e.g. [store](https://www.npmjs.com/package/store). |
-| filename | string | meta.json | Meta file to fetch. A bare name is fetched from the site root; a path starting with `/`, `./` or `../`, or a full URL, is used as is (e.g. `` `${import.meta.env.BASE_URL}meta.json` `` for an app under a sub-path). |
-| errorCheckers | array | [] | Extra `(error) => boolean` checks for errors caused by a stale build, added to the defaults. `createErrorChecker(name, regexp)` builds one. |
-| debug | function | | You can debug state and errors. |
-| updatePrompt | element or component | | Shown when a newer build is found and the page is not reloaded on its own. See [Update prompt](#update-prompt). |
-| promptOnError | boolean | false | On a stale-build error, show `updatePrompt` over `fallback` instead of reloading at once. |
-
-### Using `hook`:
-
-```tsx
-import React from 'react';
-
-import { useClearBrowserCache } from 'react-clear-browser-cache';
-
-function App() {
- const contextValue = useClearBrowserCache();
-
- return null;
-}
+**3. Deploy.** Tabs with the old build now see a prompt within a minute, or as soon as a lazy chunk fails to load.
+
+Prefer silent reloads? Drop `updatePrompt` and set `auto`.
+
+## How it works
+
+```mermaid
+flowchart LR
+ A[Mount] --> B{fetch meta.json}
+ B -- first visit --> C[store version, render app]
+ B -- same version --> C
+ B -- newer version --> D[clear Cache Storage, reload]
+ C --> E[poll every duration ms while focused]
+ E -- newer version --> F{auto?}
+ F -- yes --> D
+ F -- no --> G[show updatePrompt / isLatestVersion = false]
+ C --> H[child throws a stale-chunk error]
+ H --> I{newer version?}
+ I -- yes --> J[reload, or prompt with promptOnError]
+ I -- no --> K[rethrow to your ErrorBoundary]
```
-### Using `render props`:
-
-```tsx
-import React from 'react';
-
-import { ClearBrowserCache } from 'react-clear-browser-cache';
-
-function App() {
- return {(contextValue) => null};
-}
-```
-
-## Context value
-
-| Name | Type | More info |
-| :------------------ | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
-| loading | boolean | Indicates request for a new version. |
-| isLatestVersion | boolean | Indicates if the latest version of the app. |
-| latestVersion | string | Latest version of the app. |
-| disabled | boolean | Indicates if the boundary component is disabled, stopped all requests, doesn't handle errors, just render children because of internal errors. |
-| clearCacheAndReload | function | Clear CacheStorage and reload the page. |
-| dismissUpdate | function | Hide the update prompt until a newer build is found. |
+- On mount the boundary shows `fallback` and fetches the meta file. A newer build found on mount always reloads straight away, since nothing has been shown yet.
+- Polling runs every `duration` ms, only while the window has focus. When it finds a newer build it reloads if `auto` is set; otherwise it shows `updatePrompt`, or just sets `isLatestVersion` to `false` and leaves the reload to you (`clearCacheAndReload`).
+- When a child throws an error that looks like a stale build, the boundary checks the version: if a new build is out it reloads (or prompts, with `promptOnError`); otherwise the error goes on to your `ErrorBoundary`.
+- The meta file is fetched with `cache: 'no-store'`. A file that is missing, has no `version`, or is answered with HTML by an SPA fallback is reported to `debug` and otherwise ignored; the app still renders.
+- If storage cannot be read (blocked cookies, private mode), the boundary disables itself and just renders the app.
## Update prompt
-Pass `updatePrompt` to tell users an update is ready instead of leaving it to
-`isLatestVersion`. It shows when polling (`duration`) finds a new build and
-`auto` is off, and — with `promptOnError` — when part of the app fails to load
-because of a new build. On mount a stale build is still reloaded straight away,
-since nothing has been shown yet.
+The built-in `UpdatePrompt` is an unstyled native ``: modal, focus on **Reload**, Escape or **Later** dismisses it, focus returns to where it was, and the buttons lock while the page reloads. A dismissed prompt stays hidden until a newer build appears.
-The built-in `UpdatePrompt` is an unstyled native ``: modal, focus on
-**Reload**, Escape or **Later** dismisses it.
+Change its text or class, or make it non-modal. Define the wrapper outside your component: a new function on every render remounts the prompt.
```tsx
import {
- ClearBrowserCacheBoundary,
- UpdatePrompt
+ UpdatePrompt,
+ type UpdatePromptProps
} from 'react-clear-browser-cache';
-
-
-;
-```
-
-Change its text or class, or make it non-modal. Define the wrapper outside
-your component: a new function on every render remounts the prompt.
-
-```tsx
function AppUpdatePrompt(props: UpdatePromptProps) {
return (
;
```
-Or bring your own component; it receives `UpdatePromptProps`:
-
-| Name | Type | More info |
-| :------------ | :-------------------- | :-------------------------------------------------------------------------------------------------- |
-| latestVersion | string | The new build's version. |
-| reason | `'poll'` \| `'error'` | `error` when a part of the app failed to load; the boundary shows `fallback` behind the prompt. |
-| reload | function | Clear Cache Storage and reload the page. |
-| dismiss | function | Hide until a newer build is found. For `reason: 'error'` the error goes on to your `ErrorBoundary`. |
+Or bring your own component; it receives [`UpdatePromptProps`](#updatepromptprops):
```tsx
import type { UpdatePromptProps } from 'react-clear-browser-cache';
@@ -192,18 +167,76 @@ function UpdateDialog({ reload, dismiss }: UpdatePromptProps) {
);
}
+```
-
-
-;
+A plain element works too (`updatePrompt={}`); it reads `clearCacheAndReload` and `dismissUpdate` from `useClearBrowserCache()`.
+
+## API
+
+### ``
+
+Error boundary and context provider. Place it inside your own error boundary: it only handles stale-build errors.
+
+| Prop | Type | Default | Description |
+| :-------------- | :------------------------------ | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------ |
+| `duration` | `number` | — | Poll interval in ms. Without it the version is checked on mount and on stale-chunk errors only. |
+| `auto` | `boolean` | `false` | Reload as soon as a poll finds a newer build. |
+| `fallback` | `ReactNode` | `null` | Rendered while the first version check runs, e.g. a preloader. |
+| `updatePrompt` | element or component | — | Shown when a newer build is found and the page is not reloaded on its own. See [Update prompt](#update-prompt). |
+| `promptOnError` | `boolean` | `false` | On a stale-build error, show `updatePrompt` over `fallback` instead of reloading at once. |
+| `filename` | `string` | `'meta.json'` | Meta file to fetch. A bare name is fetched from the site root; a path starting with `/`, `./` or `../`, or a full URL, is used as is. |
+| `storageKey` | `string` | `'APP_VERSION'` | Key the current version is stored under. |
+| `storage` | `{ get(key), set(key, value) }` | `localStorage` | Where the version is kept, e.g. [store](https://www.npmjs.com/package/store). |
+| `errorCheckers` | `((error) => boolean)[]` | `[]` | Extra checks for stale-build errors, added to the defaults. See [`createErrorChecker`](#createerrorcheckername-pattern). |
+| `debug` | `(params) => void` | — | Receives state changes and errors, for logging. |
+
+### `useClearBrowserCache()` and ``
+
+Read the boundary's state from any child, by hook or by render prop:
+
+```tsx
+import {
+ ClearBrowserCache,
+ useClearBrowserCache
+} from 'react-clear-browser-cache';
+
+function VersionBadge() {
+ const { isLatestVersion, clearCacheAndReload } = useClearBrowserCache();
+ return isLatestVersion ? null : (
+
+ );
+}
+
+
+ {({ latestVersion }) => {latestVersion}}
+;
```
-A plain element works too (`updatePrompt={}`); it reads
-`clearCacheAndReload` and `dismissUpdate` from `useClearBrowserCache()`.
+| Field | Type | Description |
+| :-------------------- | :-------------------- | :-------------------------------------------------------------------------- |
+| `loading` | `boolean` | A version check is running. |
+| `isLatestVersion` | `boolean` | The running build is the latest one. |
+| `latestVersion` | `string` | Version from the most recent meta file. |
+| `disabled` | `boolean` | Storage failed, so the boundary stopped checking and only renders children. |
+| `clearCacheAndReload` | `() => Promise` | Clear Cache Storage, store the latest version and reload. |
+| `dismissUpdate` | `() => void` | Hide the update prompt until a newer build is found. |
+
+### ``
+
+The built-in dialog. Takes `UpdatePromptProps` plus `title`, `message`, `reloadLabel`, `dismissLabel`, `className` and `modal` (default `true`).
+
+### `UpdatePromptProps`
-## `lazyRetry`
+| Prop | Type | Description |
+| :-------------- | :-------------------- | :-------------------------------------------------------------------------------------------------- |
+| `latestVersion` | `string` | The new build's version. |
+| `reason` | `'poll' \| 'error'` | `'error'` when part of the app failed to load; the boundary shows `fallback` behind the prompt. |
+| `reload` | `() => Promise` | Clear Cache Storage and reload the page. |
+| `dismiss` | `() => void` | Hide until a newer build is found. For `reason: 'error'` the error goes on to your `ErrorBoundary`. |
-Retries a failing dynamic import before giving up:
+### `lazyRetry(fn, retries = 2, interval = 1000)`
+
+Calls `fn`, and while it rejects calls it again up to `retries` more times, `interval` ms apart.
```tsx
import { lazy } from 'react';
@@ -212,25 +245,65 @@ import { lazyRetry } from 'react-clear-browser-cache';
const Settings = lazy(() => lazyRetry(() => import('./Settings'), 2, 1000));
```
-`lazyRetry(fn, retries = 2, interval = 1000)` calls `fn`, and while it rejects
-calls it again up to `retries` more times, `interval` ms apart.
+### `createErrorChecker(name, pattern)`
+
+Builds an error checker that matches an error's `name` and a `RegExp` on its message:
+
+```tsx
+import { createErrorChecker } from 'react-clear-browser-cache';
+
+const errorCheckers = [
+ createErrorChecker('Error', /Failed to load module script/)
+];
+```
+
+Recognised out of the box: webpack `ChunkLoadError` and CSS chunk errors, Vite CSS preload errors, native `import()` failures in Chrome, Firefox and Safari, and `SyntaxError: Unexpected token '<'`.
+
+## Recipes
+
+**App under a sub-path (Vite `base`, GitHub Pages).** Point `filename` at the base URL:
+
+```tsx
+
+```
+
+**Version from your CI instead of a random UUID.** Any JSON with a `version` string works, e.g. `echo "{\"version\":\"$GITHUB_SHA\"}" > public/meta.json`.
+
+**Show the update somewhere other than a dialog.** Leave `updatePrompt` out and render a banner from `isLatestVersion` with the hook.
+
+## Troubleshooting
+
+
+The app never notices a new build
+
+Open `meta.json` in the browser. It must be the JSON file, not your `index.html`: an SPA fallback that rewrites unknown paths to `index.html` hides a missing file (`debug` reports it). Check that the `prebuild` script ran in CI, and that `duration` is set if you expect polling.
+
+
+
+
+It reloads, but the old build comes back
+
+Something in front of the browser cached the HTML. Serve `index.html` and `meta.json` with `Cache-Control: no-cache`; hashed assets can stay immutable.
+
+
+
+
+A chunk error still reaches my error boundary
+
+That happens when the build has not changed — the boundary only recovers when a newer version is out. If your bundler words the error differently, add a checker with `createErrorChecker`.
+
+
## Migrating from 0.2
-- React 18 or newer is required, and the package ships ESM and CommonJS builds
- with type declarations through `exports`.
-- `lazyRetry(fn, retries)` now makes `retries` extra attempts (the default is
- three attempts in total, it used to be two), and `retries = 0` no longer
- retries forever.
-- The stored version is only updated when the cache is actually cleared, so
- `isLatestVersion` stays `false` until the user reloads through
- `clearCacheAndReload`.
-- `storage` is typed as `{ get, set }`; the `@ts-ignore` the old type needed can
- go.
+- React 18 or newer is required, and the package ships ESM and CommonJS builds with type declarations through `exports`.
+- `lazyRetry(fn, retries)` now makes `retries` extra attempts (the default is three attempts in total, it used to be two), and `retries = 0` no longer retries forever.
+- The stored version is only updated when the cache is actually cleared, so `isLatestVersion` stays `false` until the user reloads through `clearCacheAndReload`.
+- `storage` is typed as `{ get, set }`; the `@ts-ignore` the old type needed can go.
-## Development
+## Contributing
-Use Node.js 24 (`.nvmrc`) and pnpm.
+Issues and pull requests are welcome. Use Node.js 24 (`.nvmrc`) and pnpm:
```bash
pnpm install
@@ -240,10 +313,7 @@ pnpm test:e2e # Playwright against the example app in Chromium, Firefox and
pnpm demo:dev # the GitHub Pages demo, with deploys simulated in the browser
```
-The demo is the example app built with `VITE_DEMO=1`. GitHub Pages serves static
-files only, so a fetch shim answers `meta.json` with a version kept in
-localStorage; the version check, cache clearing and reload are the library's own.
-`.github/workflows/pages.yml` publishes it on every push to `master`.
+The demo is the example app built with `VITE_DEMO=1`. GitHub Pages serves static files only, so a fetch shim answers `meta.json` with a version kept in localStorage; the version check, cache clearing and reload are the library's own. `.github/workflows/pages.yml` publishes it on every push to `master`.
## License
diff --git a/docs/demo-dark.png b/docs/demo-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..7229d0d7551f824d754485b815952d78b59d9733
GIT binary patch
literal 402340
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
z