@ultimat3/pwa 22.3.1 → 22.3.2
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/CLAUDE.md +2 -1
- package/README.md +8 -4
- package/package.json +3 -3
- package/src/service-worker.ts +15 -0
- package/src/strategies.ts +16 -5
package/CLAUDE.md
CHANGED
|
@@ -15,7 +15,8 @@ Tier 4. May import tiers 0–3: `core`, `schema`, `i18n`, `money`, `time`, `cach
|
|
|
15
15
|
| A generated header | names something that regenerates the file. `sw.js` says `regenerate: x build`, and `As of 2026-08` a build really writes it (#390); it named the CALL for as long as none did. |
|
|
16
16
|
| What is WIRED, and what is not | **every generator here has a caller, `As of 2026-08`.** `generateWebManifest`, `renderThemeColorMeta`, `planIcons`, `appleTouchLinks` and `BuiltinImagePipeline` through `pwa-artifacts.ts` / `dev-assets.ts` (#362); `generateServiceWorker` — and with it `buildPrecacheManifest`, `offlineFallbackSource`, `backgroundSyncSource` and `pushSource` — through `sw-artifacts.ts` (#390). So `x dev`, the container and the static export emit `manifest.webmanifest`, `sw.js` and `x-sw-register.js` alike. The worker half landed a release later because a bad `sw.js` is sticky: it waited on a real browser check (`packages/cli/e2e/service-worker.e2e.test.ts`), which nothing in the gate could run until #400. **`pwa.push` is the one capability still unwired** — the generator emits a push handler only when a VAPID key comes with it and there is no `pwa.vapid` config key, so `x build` reports it as a `serviceWorkerWarnings` entry rather than dropping it in silence. |
|
|
17
17
|
| Route input | `PwaRoute` is a **structural** view of render's `RouteDescriptor`. Never import render. |
|
|
18
|
-
| Strategy choice | derived from render mode via `MODE_STRATEGY`. Per-route override only. |
|
|
18
|
+
| Strategy choice | derived from render mode via `MODE_STRATEGY`. Per-route override only. Every row is `network-first` `As of 22.3.2`: every rule is a DOCUMENT, and `static: 'cache-first'` / `isr`,`stream: 'stale-while-revalidate'` answered online navigations with the old deploy's HTML (and so its old hashed assets) until Shift+F5. Precache ≠ cache-first — the precached copy is the offline answer. |
|
|
19
|
+
| Worker update | `self.skipWaiting()` in `install`, `As of 22.3.2` — worker-side, because the browsers that must converge run a 22.3.1 worker and register script that never post `skip-waiting`. Safe for an open tab: the worker routes no chunk. Never a reload — the next navigation is fresh. The `skip-waiting` message stays. |
|
|
19
20
|
| Precache revision | content hash. Never the build id — that re-downloads everything per deploy. |
|
|
20
21
|
| A truncation of author text | CODE POINTS, never `slice`. `short_name` falls back to the first 12 of `name`, and a UTF-16 `slice` cuts inside a surrogate pair — the lone half encodes as U+FFFD, so an app named with an emoji got a home-screen label ending in a replacement character. `Array.from(name).slice(0, n).join('')` is the form. |
|
|
21
22
|
| Declared and never wired | `ServiceWorkerConfig.shellUrl` / `shellRevision` / `shellBytes` and `PwaRoute.dataUrl`. No caller in the framework's build path sets any of them, so `precache.ts`'s `reason: 'shell'` and `reason: 'route-data'` branches are reachable only from a hand-built `generateServiceWorker` call. Each doc comment now says so; the shell trio's said "precached for every `spa` route" while `spa` was already deleted from `RENDER_MODES`. **Kept, not deleted** — a public field is a major — and carried as candidates for the next major's declared-and-never-wired half. `revision` and `bytes` are the pair that IS fed, by the CLI's prerender pass. |
|
package/README.md
CHANGED
|
@@ -12,11 +12,15 @@ const { source, precache, warnings } = generateServiceWorker(describeRoutes(), c
|
|
|
12
12
|
|
|
13
13
|
| Render mode | Strategy | Why |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| `static` |
|
|
16
|
-
| `isr` |
|
|
17
|
-
| `stream` |
|
|
15
|
+
| `static` | network-first | the document names this deploy's hashed assets; the precache is the offline copy |
|
|
16
|
+
| `isr` | network-first | the server's ISR cache is already the stale one; a browser copy only delays a deploy |
|
|
17
|
+
| `stream` | network-first | the shell names this deploy's chunks; the cache is the offline copy |
|
|
18
18
|
| `ssr` | network-first | freshness is the point; cache is the offline safety net |
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
Every rule is a document, so every mode is network-first (22.3.2): `static` was cache-first and
|
|
21
|
+
`isr`/`stream` stale-while-revalidate, and an online visitor got the previous deploy's HTML until a
|
|
22
|
+
hard reload. `offline: 'precache'` still precaches — for offline. The worker calls `skipWaiting()`
|
|
23
|
+
in `install`, so a deploy takes over without every tab closing; it never reloads a page.
|
|
20
24
|
|
|
21
25
|
Overrides: `offline: 'network-only'` forces `network-only`; a per-route `strategy` wins over
|
|
22
26
|
everything. `api/` routes get no cache rule at all.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/pwa",
|
|
3
|
-
"version": "22.3.
|
|
3
|
+
"version": "22.3.2",
|
|
4
4
|
"description": "Generated service worker, web manifest, icons, push and version-skew handling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"test": "bun test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ultimat3/core": "22.3.
|
|
36
|
-
"@ultimat3/seo": "22.3.
|
|
35
|
+
"@ultimat3/core": "22.3.2",
|
|
36
|
+
"@ultimat3/seo": "22.3.2"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/service-worker.ts
CHANGED
|
@@ -308,9 +308,20 @@ function serializeRules(rules: readonly RouteRule[]): string {
|
|
|
308
308
|
* and a bundler emits a query of its own: a fixed `?` built `...?locale=en?v=<rev>`, and a single
|
|
309
309
|
* non-200 for it rejects `addAll`, which rejects the install — no precache, no offline document,
|
|
310
310
|
* no version-skew header, and a worker that never activates at all.
|
|
311
|
+
*
|
|
312
|
+
* `skipWaiting()` FIRST, `As of 22.3.2`: a new worker activates as soon as its precache is filled,
|
|
313
|
+
* never "once every tab of the origin is closed", which for a returning visitor was days. Nothing
|
|
314
|
+
* else could do it — no page ever posted `skip-waiting` — and it has to be the worker's own call,
|
|
315
|
+
* because the one that must converge is a browser still running a 22.3.1 worker and that release's
|
|
316
|
+
* register script, neither of which will ever ask. Safe for a tab open across the release: this
|
|
317
|
+
* worker routes documents only (content-hashed chunks come from the network and the HTTP cache,
|
|
318
|
+
* never from a cache it deletes on activate), and it never reloads a page — the tab's NEXT
|
|
319
|
+
* navigation is answered network-first by the new worker.
|
|
311
320
|
*/
|
|
312
321
|
const INSTALL_BLOCK = `
|
|
313
322
|
self.addEventListener('install',(event)=>{
|
|
323
|
+
// Take over as soon as the precache is in: a waiting worker otherwise waits for every tab to close.
|
|
324
|
+
self.skipWaiting();
|
|
314
325
|
event.waitUntil((async()=>{
|
|
315
326
|
const cache=await caches.open(PRECACHE);
|
|
316
327
|
// Revision is a content hash: unchanged assets are not re-downloaded across deploys.
|
|
@@ -387,6 +398,10 @@ const SKEW_STATUS = 409;
|
|
|
387
398
|
* is stale, it stops stamping, re-issues the request untagged so the document actually loads, and
|
|
388
399
|
* tells every window an update is waiting. Skew detection is not lost — it has already fired, and
|
|
389
400
|
* its whole purpose is to get the client onto the new build, which is what the app now does.
|
|
401
|
+
*
|
|
402
|
+
* Still needed after the install block's `skipWaiting()` (22.3.2): that removes the WAITING half,
|
|
403
|
+
* but the server can roll forward before the browser has fetched the new `sw.js` at all, and a
|
|
404
|
+
* worker from before 22.3.2 is the one answering in between.
|
|
390
405
|
*/
|
|
391
406
|
function fetchBlock(): string {
|
|
392
407
|
return `
|
package/src/strategies.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The four caching strategies as named functions, plus the render-mode → strategy table.
|
|
3
|
-
* You never choose a strategy by hand: the
|
|
4
|
-
*
|
|
3
|
+
* You never choose a strategy by hand: the mapping is derived and the override is the exception.
|
|
4
|
+
* Every rule the worker routes is a DOCUMENT, and a document is network-first in every mode — the
|
|
5
|
+
* cached copy is what offline gets, never what an online visitor gets (22.3.2).
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { OfflineStrategy, RenderMode } from '@ultimat3/core';
|
|
@@ -52,6 +53,16 @@ export interface PwaRoute {
|
|
|
52
53
|
/**
|
|
53
54
|
* Render mode → runtime strategy. The whole reason `sw.js` is generated, not written.
|
|
54
55
|
*
|
|
56
|
+
* Every row is `network-first`, `As of 22.3.2`, because every rule this table feeds is a
|
|
57
|
+
* DOCUMENT: `routeRules` projects page routes only, and content-hashed chunks never get a rule —
|
|
58
|
+
* they are `immutable` in the browser's HTTP cache. `static: 'cache-first'` and
|
|
59
|
+
* `isr`/`stream: 'stale-while-revalidate'` answered an ONLINE navigation from the copy the old
|
|
60
|
+
* worker held, and that copy is the old HTML naming the old hashed CSS and islands, so a visitor saw
|
|
61
|
+
* a deploy only after Shift+F5 (measured on notificado.co, 22.3.1). A precached or pages-cached
|
|
62
|
+
* document is the OFFLINE answer, which `networkFirst` already is on a failed fetch; the render
|
|
63
|
+
* mode still decides WHERE that copy lives (`cacheFor`: precache or pages), not whether it is
|
|
64
|
+
* served while the network answers. A per-route `strategy` is still the override.
|
|
65
|
+
*
|
|
55
66
|
* Two separate things make the closed set hold, and the table needed both. `Record<RenderMode, …>`
|
|
56
67
|
* over the TIER-0 union is the exhaustiveness check — this was keyed on a hand-copy, which is how
|
|
57
68
|
* `spa` went on mapping to `cache-first` after `spa` was deleted from the vocabulary: the one
|
|
@@ -63,10 +74,10 @@ export interface PwaRoute {
|
|
|
63
74
|
* AND an extra key are both build errors.
|
|
64
75
|
*/
|
|
65
76
|
export const MODE_STRATEGY = Object.freeze<Record<RenderMode, StrategyName>>({
|
|
66
|
-
static: '
|
|
67
|
-
isr: '
|
|
77
|
+
static: 'network-first',
|
|
78
|
+
isr: 'network-first',
|
|
68
79
|
ssr: 'network-first',
|
|
69
|
-
stream: '
|
|
80
|
+
stream: 'network-first',
|
|
70
81
|
});
|
|
71
82
|
|
|
72
83
|
export function strategyFor(route: PwaRoute): StrategyName {
|