Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .eslintignore

This file was deleted.

34 changes: 0 additions & 34 deletions .eslintrc

This file was deleted.

70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm build
- run: pnpm typecheck

unit:
runs-on: ubuntu-latest
strategy:
matrix:
react: ['18.0.0', '18.3.1', '19']
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Use React ${{ matrix.react }}
if: matrix.react != '19'
run: pnpm add -D -w react@${{ matrix.react }} react-dom@${{ matrix.react }}
- run: pnpm test:unit

cli:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20, 22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: node bin/cli.js --destination tmp/meta/meta.json
- run: node -e "const {version} = require('./tmp/meta/meta.json'); if (!/^[0-9a-f-]{36}$/.test(version)) process.exit(1)"

e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium firefox webkit
- run: pnpm test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: test-results
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ yarn-error.log*

# vim
*.swp

# playwright
playwright-report
test-results
3 changes: 3 additions & 0 deletions .htmlvalidate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["html-validate:recommended", "html-validate:prettier"]
}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dist
coverage
pnpm-lock.yaml
playwright-report
test-results
example/public/meta.json
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"semi": true,
"tabWidth": 2,
"bracketSpacing": true,
"jsxBracketSameLine": false,
"bracketSameLine": false,
"arrowParens": "always",
"trailingComma": "none"
}
4 changes: 0 additions & 4 deletions .travis.yml

This file was deleted.

96 changes: 78 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,54 +14,76 @@ 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

Generate `meta.json` file with latest build version.
Generate a `meta.json` file with a fresh build version before every build.

```json
{
"prebuild": "npm run generate-build-meta",
"generate-build-meta": "node ./node_modules/react-clear-browser-cache/bin/cli.js"
"prebuild": "generate-build-meta"
}
```

The file is written to `./public/meta.json` by default; pass
`--destination <path>` 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

### Using `Boundary`:

`ClearBrowserCacheBoundary` has context provider for child elements (`useClearBrowserCache`), add him after your `ErrorBoundary`, because `ClearBrowserCacheBoundary` only handles certain errors like `ChunkLoadError`.

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';

import { ClearBrowserCacheBoundary } from 'react-clear-browser-cache';

import App from './App';
import ErrorBoundary from './ErrorBoundary';

ReactDOM.render(
createRoot(document.getElementById('root')!).render(
<ErrorBoundary>
<ClearBrowserCacheBoundary auto fallback='Loading' duration={60000}>
<App />
</ClearBrowserCacheBoundary>
</ErrorBoundary>,
document.getElementById('root')
</ErrorBoundary>
);
```

### 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 | You can use another storage to save and get the version of app like [store](https://www.npmjs.com/package/store). |
| filename | string | meta.json | Filename for fetching new app version. |
| errorCheckers | array | [] | You can set custom error checkers for catching errors concerned with invalid browser caches. |
| debug | function | | You can debug state and errors. |
| 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. |

### Using `hook`:

Expand Down Expand Up @@ -99,6 +121,44 @@ function 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. |

## `lazyRetry`

Retries a failing dynamic import before giving up:

```tsx
import { lazy } from 'react';
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.

## 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.

## Development

Use Node.js 24 (`.nvmrc`) and pnpm.

```bash
pnpm install
pnpm test # lint, typecheck, unit tests, build, e2e
pnpm test:unit # Vitest
pnpm test:e2e # Playwright against the example app in Chromium, Firefox and WebKit
```

## License

MIT © [arenuzzz](https://github.com/arenuzzz)
17 changes: 0 additions & 17 deletions __tests__/ClearBrowserCacheProvider.test.tsx

This file was deleted.

62 changes: 27 additions & 35 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,31 @@
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const parseArgs = require('minimist');
const uuid = require('uuid');

const getDirName = path.dirname;
const appVersion = uuid.v4();

const jsonData = {
version: appVersion
};

const jsonContent = JSON.stringify(jsonData);

const args = parseArgs(process.argv.slice(2));

const destination = args.destination || './public/meta.json';
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const { randomUUID } = require('node:crypto');
const { parseArgs } = require('node:util');

// Non-strict like the minimist parser it replaces: existing build scripts may
// pass flags this CLI never read.
const { values } = parseArgs({
options: {
destination: { type: 'string' }
},
strict: false
});

const filename = destination.match(/[^\\/]+$/)[0];
// parseArgs ignores `default` before Node 18.11, so the fallback lives here.
const destination =
typeof values.destination === 'string'
? values.destination
: './public/meta.json';
const filename = path.basename(destination);

writeFile(destination, jsonContent, (err) => {
if (err) {
console.log(`An error occured while writing JSON Object to ${filename}`);
return console.log(err);
}
try {
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(destination, JSON.stringify({ version: randomUUID() }));
console.log(`${filename} file has been saved with latest version number`);
return null;
});

function writeFile(path, contents, cb) {
mkdirp(getDirName(path))
.then(() => {
fs.writeFile(path, contents, cb);
})
.catch(err => {
return cb(err);
});
} catch (error) {
console.error(`An error occurred while writing JSON Object to ${filename}`);
console.error(error);
process.exitCode = 1;
}
Loading
Loading