@ultimat3/pwa 6.0.0 → 8.0.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/CLAUDE.md +1 -0
- package/package.json +3 -3
- package/src/capabilities.ts +16 -19
- package/src/index.ts +9 -3
- package/src/precache.ts +3 -6
- package/src/strategies.ts +21 -11
package/CLAUDE.md
CHANGED
|
@@ -18,6 +18,7 @@ Tier 4. May import tiers 0–3: `core`, `schema`, `i18n`, `money`, `time`, `cach
|
|
|
18
18
|
| Precache FETCH | the revision is appended with `?` or `&`, picked per entry. `PrecacheAsset.url` is public API and a bundler emits `?v=<hash>` of its own, so a fixed `?` built `...?locale=en?v=<rev>`; `addAll` is all-or-nothing, so one non-200 there means `install` rejects and the worker never activates at all. |
|
|
19
19
|
| SW scope | `assertScope` refuses a relative `swPath`. `lastIndexOf('/')` on `sw.js` is `-1`, so the directory was `''` and `scope.startsWith('')` passed for every scope — the check waved through exactly the config most likely to be wrong. |
|
|
20
20
|
| HTML sinks | one escaper: `escapeAttribute` from `@ultimat3/seo` (tier 1, and the only one reachable — render's `html.ts` is tier 4, sideways). `appleTouchLinks` and `renderThemeColorMeta` interpolate app config into attributes; both escape. Never a second escaper here. |
|
|
21
|
+
| A byte count in a warning | `formatBytes` from `@ultimat3/core`, never a local one. The copy that lived in `precache.ts` stopped at `mb`, so a precache past a gigabyte reported a four-digit `mb` — and `@ultimat3/render`'s copy stopped at `kb`, so the two halves of one build printed different units for the same bytes. Still on this barrel: a size report is what a caller of this package prints. |
|
|
21
22
|
| Cache names | always `cacheNamespace(buildId, kind)`. An unkeyed cache name is a rejected change. |
|
|
22
23
|
| Offline fallback | `requireOfflineFallback` runs inside `generateServiceWorker`. Never optional. |
|
|
23
24
|
| Capabilities | gate the manifest member **and** the SW block, where the capability has one. Disabled → zero bytes. `push`, `backgroundSync` and `badging` emit worker code; `shareTarget`, `fileHandlers` and `protocolHandlers` are **manifest-only** — the OS delivers to a route the app already serves — and their `CAPABILITY_SW_MARKERS` list is empty, which `service-worker.test.ts` checks in both directions: every declared marker is in the worker when its capability is on, none is when they are all off. `shareTarget` named `/_x/share-target` there while no block emitted it. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/pwa",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Generated service worker, web manifest, icons, push and version-skew handling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
35
|
-
"@ultimat3/seo": "
|
|
34
|
+
"@ultimat3/core": "8.0.0",
|
|
35
|
+
"@ultimat3/seo": "8.0.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/capabilities.ts
CHANGED
|
@@ -43,15 +43,14 @@ export function enabledCapabilities(capabilities: ResolvedCapabilities): readonl
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Manifest members a capability owns. Absent capability → absent member. */
|
|
46
|
-
export const CAPABILITY_MANIFEST_KEYS
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
46
|
+
export const CAPABILITY_MANIFEST_KEYS = Object.freeze<Record<Capability, readonly string[]>>({
|
|
47
|
+
push: [],
|
|
48
|
+
backgroundSync: [],
|
|
49
|
+
badging: [],
|
|
50
|
+
shareTarget: ['share_target'],
|
|
51
|
+
fileHandlers: ['file_handlers'],
|
|
52
|
+
protocolHandlers: ['protocol_handlers'],
|
|
53
|
+
});
|
|
55
54
|
|
|
56
55
|
/**
|
|
57
56
|
* The service-worker code each capability emits — its listener, and anything that listener alone
|
|
@@ -64,13 +63,11 @@ export const CAPABILITY_MANIFEST_KEYS: Readonly<Record<Capability, readonly stri
|
|
|
64
63
|
* already serves, so the member is the whole feature and the worker has no branch to add.
|
|
65
64
|
* `shareTarget` named `/_x/share-target` here for two releases and no block ever emitted it.
|
|
66
65
|
*/
|
|
67
|
-
export const CAPABILITY_SW_MARKERS
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
},
|
|
76
|
-
);
|
|
66
|
+
export const CAPABILITY_SW_MARKERS = Object.freeze<Record<Capability, readonly string[]>>({
|
|
67
|
+
push: ["addEventListener('push'", "addEventListener('notificationclick'"],
|
|
68
|
+
backgroundSync: ["addEventListener('sync'", 'class PwaSyncError'],
|
|
69
|
+
badging: ['navigator.setAppBadge'],
|
|
70
|
+
shareTarget: [],
|
|
71
|
+
fileHandlers: [],
|
|
72
|
+
protocolHandlers: [],
|
|
73
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/** Public API of `@ultimat3/pwa`. You never open `sw.js`; you call these. */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* `PwaRenderMode` and `PwaOfflineStrategy` were this package's own NAMES for tier 0's vocabulary —
|
|
5
|
+
* the alias was the copy. `PwaRoute` takes both in its signature, so the canonical names are
|
|
6
|
+
* re-exported here; a consumer still needs one import, and now it names the real type.
|
|
7
|
+
*/
|
|
8
|
+
export type { OfflineStrategy, RenderMode } from '@ultimat3/core';
|
|
9
|
+
// Moved to `@ultimat3/core` (one formatter, `b`/`kb`/`mb`/`gb`); still named here because a service
|
|
10
|
+
// worker's size report is what a caller of this package prints.
|
|
11
|
+
export { formatBytes } from '@ultimat3/core';
|
|
3
12
|
export type { BackgroundSyncOptions, RetryPolicy } from './background-sync';
|
|
4
13
|
export {
|
|
5
14
|
backgroundSyncSource,
|
|
@@ -88,7 +97,6 @@ export type { PrecacheAsset, PrecacheEntry, PrecacheInput, PrecacheManifest } fr
|
|
|
88
97
|
export {
|
|
89
98
|
buildPrecacheManifest,
|
|
90
99
|
DEFAULT_PRECACHE_WARN_BYTES,
|
|
91
|
-
formatBytes,
|
|
92
100
|
serializePrecacheManifest,
|
|
93
101
|
} from './precache';
|
|
94
102
|
export type {
|
|
@@ -111,8 +119,6 @@ export {
|
|
|
111
119
|
export type { RouteRule, ServiceWorkerConfig, ServiceWorkerOutput } from './service-worker';
|
|
112
120
|
export { assertScope, generateServiceWorker, routeRules } from './service-worker';
|
|
113
121
|
export type {
|
|
114
|
-
PwaOfflineStrategy,
|
|
115
|
-
PwaRenderMode,
|
|
116
122
|
PwaRoute,
|
|
117
123
|
StrategyCache,
|
|
118
124
|
StrategyEnv,
|
package/src/precache.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* hash, never the build id, or every deploy would re-fetch everything.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
// One formatter, in `@ultimat3/core`: the copy that lived here stopped at `mb`, and the route
|
|
8
|
+
// budget message on the other side of the build stopped at `kb`, for the same byte count.
|
|
9
|
+
import { formatBytes } from '@ultimat3/core';
|
|
7
10
|
import type { PwaRoute } from './strategies';
|
|
8
11
|
|
|
9
12
|
export interface PrecacheAsset {
|
|
@@ -120,12 +123,6 @@ export function buildPrecacheManifest(input: PrecacheInput): PrecacheManifest {
|
|
|
120
123
|
return { buildId: input.buildId, entries: sorted, totalBytes, warnings };
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
export function formatBytes(bytes: number): string {
|
|
124
|
-
if (bytes < 1024) return `${bytes}b`;
|
|
125
|
-
if (bytes < 1024 * 1024) return `${Math.round((bytes / 1024) * 10) / 10}kb`;
|
|
126
|
-
return `${Math.round((bytes / (1024 * 1024)) * 10) / 10}mb`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
126
|
/** The manifest as it is embedded in `sw.js`; stable key order for determinism. */
|
|
130
127
|
export function serializePrecacheManifest(manifest: PrecacheManifest): string {
|
|
131
128
|
const rows = manifest.entries.map(
|
package/src/strategies.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* its bytes have to be, so the mapping is derived and the override is the exception.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { OfflineStrategy, RenderMode } from '@ultimat3/core';
|
|
7
8
|
import { PwaStrategyExhaustedError } from './errors';
|
|
8
9
|
|
|
9
10
|
export type StrategyName =
|
|
@@ -19,9 +20,6 @@ export const STRATEGY_NAMES: readonly StrategyName[] = [
|
|
|
19
20
|
'network-only',
|
|
20
21
|
];
|
|
21
22
|
|
|
22
|
-
export type PwaRenderMode = 'static' | 'isr' | 'ssr' | 'stream';
|
|
23
|
-
export type PwaOfflineStrategy = 'precache' | 'runtime' | 'network-only';
|
|
24
|
-
|
|
25
23
|
/**
|
|
26
24
|
* Structural view of `@ultimat3/render`'s `RouteDescriptor`. Tier-4 packages must not
|
|
27
25
|
* import each other, so route data arrives as data and this is the shape it must have.
|
|
@@ -29,8 +27,8 @@ export type PwaOfflineStrategy = 'precache' | 'runtime' | 'network-only';
|
|
|
29
27
|
export interface PwaRoute {
|
|
30
28
|
readonly path: string;
|
|
31
29
|
readonly surface: 'site' | 'app' | 'api';
|
|
32
|
-
readonly mode:
|
|
33
|
-
readonly offline:
|
|
30
|
+
readonly mode: RenderMode;
|
|
31
|
+
readonly offline: OfflineStrategy;
|
|
34
32
|
readonly dynamic?: boolean;
|
|
35
33
|
/** Explicit per-route override; wins over the derived strategy. */
|
|
36
34
|
readonly strategy?: StrategyName;
|
|
@@ -41,8 +39,20 @@ export interface PwaRoute {
|
|
|
41
39
|
readonly dataUrl?: string;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Render mode → runtime strategy. The whole reason `sw.js` is generated, not written.
|
|
44
|
+
*
|
|
45
|
+
* Two separate things make the closed set hold, and the table needed both. `Record<RenderMode, …>`
|
|
46
|
+
* over the TIER-0 union is the exhaustiveness check — this was keyed on a hand-copy, which is how
|
|
47
|
+
* `spa` went on mapping to `cache-first` after `spa` was deleted from the vocabulary: the one
|
|
48
|
+
* strategy that gives an `app/` route a SHARED cache entry, i.e. one member's authed HTML served
|
|
49
|
+
* to the next. And `Object.freeze<T>({…})` with an EXPLICIT type argument, never
|
|
50
|
+
* `const X: T = Object.freeze({…})` — the second form infers `T` from the literal and the
|
|
51
|
+
* annotation only checks assignability afterwards, so the literal's freshness is already gone and
|
|
52
|
+
* an EXTRA key compiles in silence. Named, the literal is contextually typed and a missing key
|
|
53
|
+
* AND an extra key are both build errors.
|
|
54
|
+
*/
|
|
55
|
+
export const MODE_STRATEGY = Object.freeze<Record<RenderMode, StrategyName>>({
|
|
46
56
|
static: 'cache-first',
|
|
47
57
|
isr: 'stale-while-revalidate',
|
|
48
58
|
ssr: 'network-first',
|
|
@@ -138,12 +148,12 @@ export async function networkOnly(
|
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
|
|
141
|
-
export const STRATEGY_FNS
|
|
151
|
+
export const STRATEGY_FNS = Object.freeze<
|
|
142
152
|
Record<
|
|
143
153
|
StrategyName,
|
|
144
154
|
(request: Request, env: StrategyEnv, options: StrategyOptions) => Promise<Response>
|
|
145
155
|
>
|
|
146
|
-
>
|
|
156
|
+
>({
|
|
147
157
|
'cache-first': cacheFirst,
|
|
148
158
|
'network-first': networkFirst,
|
|
149
159
|
'stale-while-revalidate': staleWhileRevalidate,
|
|
@@ -176,7 +186,7 @@ async function fallbackOrThrow(options: StrategyOptions): Promise<Response> {
|
|
|
176
186
|
* service worker is a generated artifact with no bundler in the loop — the shapes are
|
|
177
187
|
* identical on purpose and `strategies.test.ts` asserts both halves stay in step.
|
|
178
188
|
*/
|
|
179
|
-
export const STRATEGY_SOURCE
|
|
189
|
+
export const STRATEGY_SOURCE = Object.freeze<Record<StrategyName, string>>({
|
|
180
190
|
'cache-first': `async function cacheFirst(req,cn,fb){
|
|
181
191
|
const c=await caches.open(cn);const hit=await c.match(req);if(hit)return hit;
|
|
182
192
|
try{const r=await fetch(req);if(r.ok)await c.put(req,r.clone());return r}catch(e){if(fb)return fb();throw e}
|
|
@@ -198,7 +208,7 @@ export const STRATEGY_SOURCE: Readonly<Record<StrategyName, string>> = Object.fr
|
|
|
198
208
|
}`,
|
|
199
209
|
});
|
|
200
210
|
|
|
201
|
-
export const STRATEGY_FN_NAMES
|
|
211
|
+
export const STRATEGY_FN_NAMES = Object.freeze<Record<StrategyName, string>>({
|
|
202
212
|
'cache-first': 'cacheFirst',
|
|
203
213
|
'network-first': 'networkFirst',
|
|
204
214
|
'stale-while-revalidate': 'staleWhileRevalidate',
|