Skip to content

Commit 78ace75

Browse files
authored
Merge pull request #7 from devmaxxx/feat/update-prompt
Release 1.0.0: update prompt, Next.js support and a live demo
2 parents b352f8d + 888d1a4 commit 78ace75

37 files changed

Lines changed: 3112 additions & 570 deletions

‎.github/workflows/pages.yml‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Pages
2+
3+
on:
4+
push:
5+
branches: [master]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
pages: write
11+
id-token: write
12+
13+
concurrency:
14+
group: pages
15+
cancel-in-progress: true
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: pnpm/action-setup@v4
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version-file: .nvmrc
26+
cache: pnpm
27+
- run: pnpm install --frozen-lockfile
28+
- run: pnpm build
29+
- run: pnpm demo:build
30+
- uses: actions/upload-pages-artifact@v3
31+
with:
32+
path: example/dist-demo
33+
34+
deploy:
35+
needs: build
36+
runs-on: ubuntu-latest
37+
environment:
38+
name: github-pages
39+
url: ${{ steps.deployment.outputs.page_url }}
40+
steps:
41+
- id: deployment
42+
uses: actions/deploy-pages@v4

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ node_modules
77

88
# builds
99
dist
10+
dist-demo
1011
build
1112

1213
# misc

‎.prettierignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
dist
2+
dist-demo
23
coverage
34
pnpm-lock.yaml
45
playwright-report

‎CHANGELOG.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Changelog
2+
3+
## 1.0.0
4+
5+
### Breaking changes
6+
7+
- React 18 or newer is required.
8+
- The package ships ESM and CommonJS builds through `exports`; deep imports into `dist/` no longer resolve.
9+
- The stored version is saved only when the cache is cleared, so `isLatestVersion` stays `false` until the reload.
10+
- `lazyRetry(fn, 0)` makes one attempt instead of retrying forever.
11+
- `debug` receives `error: unknown` and always receives `state`.
12+
13+
See [Upgrading from 0.x](README.md#upgrading-from-0x).
14+
15+
### Features
16+
17+
- `onUpdate` (`'prompt' | 'reload' | 'flag'`) decides what a check that finds a newer build does. `auto` still works and is deprecated.
18+
- `prompt` asks the user to reload with your own component, which receives `UpdatePromptProps`; `promptOnChunkError` asks before reloading for a chunk error too.
19+
- `reloadWhenHidden` reloads into a new build when the tab goes to the background, and checks once on the way.
20+
- `canReload` holds back reloads the user did not start.
21+
- `blocking={false}` renders the app before the first check.
22+
- A tab that gets focus back after a whole `duration` checks at once.
23+
- Next.js support: the bundle is marked `'use client'`, renders on the server and hydrates without a mismatch; Turbopack chunk errors are recognised.
24+
- `defaultErrorCheckers` and `isCacheError` are exported.
25+
26+
### Fixes
27+
28+
- `lazyRetry` no longer retries forever on `0`, and keeps its result type.
29+
- Chunk errors from Vite, mini-css-extract-plugin, native dynamic `import()` in every major browser, and chunks answered with HTML are recognised; 0.2 knew only webpack and one SyntaxError form.

‎README.md‎

Lines changed: 304 additions & 88 deletions
Large diffs are not rendered by default.

‎docs/demo-light.png‎

405 KB
Loading

‎e2e/clearBrowserCache.spec.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,75 @@ test('an SPA fallback answering meta.json with HTML does not block or loop', asy
156156
await expect(loads(page)).toHaveText('1');
157157
expect(await storedVersion(page)).toBeNull();
158158
});
159+
160+
test.describe('update prompt', () => {
161+
test('polling opens a modal dialog that Escape dismisses until a newer build', async ({
162+
page
163+
}) => {
164+
const server = await deployServer(page, 'v1');
165+
await page.goto('/?prompt=1&duration=200');
166+
await expect(page.getByTestId('lazy')).toBeVisible();
167+
168+
server.deploy('v2');
169+
170+
const dialog = page.getByRole('dialog', {
171+
name: 'A new version is available'
172+
});
173+
await expect(dialog).toBeVisible();
174+
await expect(dialog.getByRole('button', { name: 'Reload' })).toBeFocused();
175+
// Modal: the page behind is inert.
176+
expect(
177+
await page.evaluate(() =>
178+
document.querySelector('dialog')?.matches(':modal')
179+
)
180+
).toBe(true);
181+
182+
await page.keyboard.press('Escape');
183+
await expect(dialog).toBeHidden();
184+
await page.waitForTimeout(600);
185+
await expect(dialog).toBeHidden();
186+
await expect(loads(page)).toHaveText('1');
187+
188+
server.deploy('v3');
189+
await expect(dialog).toBeVisible();
190+
});
191+
192+
test('Reload in the dialog clears the cache and loads the new build', async ({
193+
page
194+
}) => {
195+
const server = await deployServer(page, 'v1');
196+
await page.goto('/?prompt=1&duration=200');
197+
await expect(loads(page)).toHaveText('1');
198+
199+
server.deploy('v2');
200+
await page.getByRole('button', { name: 'Reload' }).click();
201+
202+
await expect(loads(page)).toHaveText('2');
203+
await expect(page.getByRole('dialog')).toBeHidden();
204+
expect(await storedVersion(page)).toBe('v2');
205+
});
206+
207+
test('promptOnChunkError asks before reloading when a chunk is gone', async ({
208+
page
209+
}) => {
210+
const server = await deployServer(page, 'v1');
211+
await page.goto('/?prompt=1&promptOnChunkError=1');
212+
await expect(page.getByTestId('lazy')).toBeVisible();
213+
214+
server.deploy('v1', 'v2');
215+
await breakLazyChunk(page, 1);
216+
await page.reload();
217+
218+
const dialog = page.getByRole('dialog');
219+
await expect(dialog).toContainText('could not load');
220+
expect(await page.evaluate(() => sessionStorage.getItem('loads'))).toBe(
221+
'2'
222+
);
223+
224+
await dialog.getByRole('button', { name: 'Reload' }).click();
225+
226+
await expect(loads(page)).toHaveText('3');
227+
await expect(page.getByTestId('lazy')).toBeVisible();
228+
expect(await storedVersion(page)).toBe('v2');
229+
});
230+
});

‎e2e/demo.spec.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test('a simulated deploy is offered through the dialog and applied on Reload', async ({
4+
page
5+
}) => {
6+
await page.goto('./');
7+
await expect(page.getByTestId('stored')).toHaveText('v1');
8+
9+
await page.getByRole('button', { name: 'Deploy new version' }).click();
10+
await expect(page.getByTestId('deployed')).toHaveText('v2');
11+
12+
const dialog = page.getByRole('dialog', {
13+
name: 'A new version is available'
14+
});
15+
await expect(dialog).toBeVisible({ timeout: 10_000 });
16+
await dialog.getByRole('button', { name: 'Reload' }).click();
17+
18+
await expect(page.getByTestId('loads')).toHaveText('2');
19+
await expect(page.getByTestId('running')).toHaveText('v2');
20+
await expect(page.getByTestId('stored')).toHaveText('v2');
21+
await expect(page.getByText('Cache Storage is empty')).toBeVisible();
22+
});
23+
24+
test('a lazy chunk removed by a deploy prompts with the error message', async ({
25+
page
26+
}) => {
27+
await page.goto('./?duration=0');
28+
await page.getByRole('button', { name: 'Deploy new version' }).click();
29+
await page.getByRole('button', { name: 'Open reports (lazy chunk)' }).click();
30+
31+
const dialog = page.getByRole('dialog');
32+
await expect(dialog).toContainText('could not load');
33+
await dialog.getByRole('button', { name: 'Reload' }).click();
34+
35+
await expect(page.getByTestId('running')).toHaveText('v2');
36+
await page.getByRole('button', { name: 'Open reports (lazy chunk)' }).click();
37+
await expect(page.getByTestId('reports')).toHaveText(/v2 chunk/);
38+
});
39+
40+
test('the custom banner component replaces the dialog', async ({ page }) => {
41+
await page.goto('./?prompt=banner&duration=3000');
42+
await page.getByRole('button', { name: 'Deploy new version' }).click();
43+
44+
const banner = page.getByRole('status');
45+
await expect(banner).toContainText('v2 is ready', { timeout: 10_000 });
46+
await banner.getByRole('button', { name: 'Dismiss' }).click();
47+
await expect(banner).toBeHidden();
48+
await expect(page.getByTestId('is-latest')).toHaveText('false');
49+
});

‎eslint.config.mjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import tseslint from 'typescript-eslint';
66

77
export default tseslint.config(
88
{
9-
ignores: ['**/dist', 'coverage', 'playwright-report', 'test-results']
9+
ignores: [
10+
'**/dist',
11+
'**/dist-demo',
12+
'coverage',
13+
'playwright-report',
14+
'test-results'
15+
]
1016
},
1117
js.configs.recommended,
1218
tseslint.configs.recommended,

‎example/README.md‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@ pnpm example:dev
1111

1212
Query parameters configure the boundary, so one build covers every scenario:
1313

14-
| Parameter | Effect |
15-
| :--------- | :-------------------------------------- |
16-
| `auto=1` | Reload as soon as a new build is found. |
17-
| `duration` | Polling interval in ms. |
14+
| Parameter | Effect |
15+
| :---------------- | :-------------------------------------------------- |
16+
| `auto=1` | Reload as soon as a new build is found. |
17+
| `duration` | Polling interval in ms. |
18+
| `prompt=1` | Show the built-in `UpdatePrompt` dialog. |
19+
| `promptOnError=1` | Prompt instead of reloading on a stale-chunk error. |
20+
21+
## GitHub Pages demo
22+
23+
`pnpm demo:dev` and `pnpm demo:build` set `VITE_DEMO=1`, which swaps the entry
24+
for `src/demo`: a playground with a **Deploy new version** button, live boundary
25+
props, the tab's state and an event log. A fetch shim stands in for the server,
26+
since Pages serves static files only. `e2e/demo.spec.ts` covers it under the
27+
same `/react-clear-browser-cache/` sub-path that Pages uses.

0 commit comments

Comments
 (0)