@broberg/pwa 0.1.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/README.md +116 -0
- package/dist/chunk-ERNF6MZL.js +7 -0
- package/dist/chunk-ERNF6MZL.js.map +1 -0
- package/dist/index.cjs +126 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -0
- package/dist/message-CBBmtYuB.d.cts +12 -0
- package/dist/message-CBBmtYuB.d.ts +12 -0
- package/dist/react.cjs +189 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +67 -0
- package/dist/react.d.ts +67 -0
- package/dist/react.js +186 -0
- package/dist/react.js.map +1 -0
- package/dist/sw.cjs +21 -0
- package/dist/sw.cjs.map +1 -0
- package/dist/sw.d.cts +30 -0
- package/dist/sw.d.ts +30 -0
- package/dist/sw.js +16 -0
- package/dist/sw.js.map +1 -0
- package/package.json +78 -0
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/react.tsx
|
|
7
|
+
|
|
8
|
+
// src/message.ts
|
|
9
|
+
var SKIP_WAITING = "SKIP_WAITING";
|
|
10
|
+
var SKIP_WAITING_MESSAGE = { type: SKIP_WAITING };
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
var DEFAULT_POLL_MS = 60 * 60 * 1e3;
|
|
14
|
+
function createPwaUpdater(options = {}) {
|
|
15
|
+
const {
|
|
16
|
+
swUrl = "/sw.js",
|
|
17
|
+
register = true,
|
|
18
|
+
pollIntervalMs = DEFAULT_POLL_MS,
|
|
19
|
+
updateOnFocus = true,
|
|
20
|
+
reloadOnControllerChange = true,
|
|
21
|
+
disabled = false
|
|
22
|
+
} = options;
|
|
23
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
24
|
+
let updateReady = false;
|
|
25
|
+
let waitingWorker = null;
|
|
26
|
+
const getState = () => ({ updateReady });
|
|
27
|
+
const emit = () => {
|
|
28
|
+
const state = getState();
|
|
29
|
+
for (const listener of listeners) listener(state);
|
|
30
|
+
};
|
|
31
|
+
const subscribe = (listener) => {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => listeners.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
const supported = typeof navigator !== "undefined" && "serviceWorker" in navigator;
|
|
36
|
+
if (disabled || !supported) {
|
|
37
|
+
return {
|
|
38
|
+
subscribe,
|
|
39
|
+
getState,
|
|
40
|
+
applyUpdate() {
|
|
41
|
+
},
|
|
42
|
+
destroy() {
|
|
43
|
+
listeners.clear();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const container = navigator.serviceWorker;
|
|
48
|
+
let pollId = null;
|
|
49
|
+
let removeFocusListeners = null;
|
|
50
|
+
let reloading = false;
|
|
51
|
+
let destroyed = false;
|
|
52
|
+
const markReady = (worker) => {
|
|
53
|
+
waitingWorker = worker;
|
|
54
|
+
if (!updateReady) {
|
|
55
|
+
updateReady = true;
|
|
56
|
+
emit();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const onControllerChange = () => {
|
|
60
|
+
if (!reloadOnControllerChange || reloading) return;
|
|
61
|
+
reloading = true;
|
|
62
|
+
if (typeof location !== "undefined") location.reload();
|
|
63
|
+
};
|
|
64
|
+
container.addEventListener("controllerchange", onControllerChange);
|
|
65
|
+
const watchInstalling = (registration) => {
|
|
66
|
+
const installing = registration.installing;
|
|
67
|
+
if (!installing) return;
|
|
68
|
+
installing.addEventListener("statechange", () => {
|
|
69
|
+
if (installing.state === "installed" && container.controller) {
|
|
70
|
+
markReady(installing);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const wireFocusChecks = (registration) => {
|
|
75
|
+
if (!updateOnFocus) return;
|
|
76
|
+
const check = () => {
|
|
77
|
+
registration.update().catch(() => {
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const onFocus = () => check();
|
|
81
|
+
const onVisibility = () => {
|
|
82
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
83
|
+
check();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
if (typeof window !== "undefined") window.addEventListener("focus", onFocus);
|
|
87
|
+
if (typeof document !== "undefined") {
|
|
88
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
89
|
+
}
|
|
90
|
+
removeFocusListeners = () => {
|
|
91
|
+
if (typeof window !== "undefined") window.removeEventListener("focus", onFocus);
|
|
92
|
+
if (typeof document !== "undefined") {
|
|
93
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const registrationPromise = register ? container.register(swUrl) : container.ready;
|
|
98
|
+
registrationPromise.then((registration) => {
|
|
99
|
+
if (destroyed) return;
|
|
100
|
+
if (registration.waiting) markReady(registration.waiting);
|
|
101
|
+
registration.addEventListener("updatefound", () => watchInstalling(registration));
|
|
102
|
+
if (pollIntervalMs > 0) {
|
|
103
|
+
pollId = setInterval(() => {
|
|
104
|
+
registration.update().catch(() => {
|
|
105
|
+
});
|
|
106
|
+
}, pollIntervalMs);
|
|
107
|
+
}
|
|
108
|
+
wireFocusChecks(registration);
|
|
109
|
+
}).catch(() => {
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
subscribe,
|
|
113
|
+
getState,
|
|
114
|
+
applyUpdate() {
|
|
115
|
+
if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);
|
|
116
|
+
},
|
|
117
|
+
destroy() {
|
|
118
|
+
destroyed = true;
|
|
119
|
+
if (pollId !== null) clearInterval(pollId);
|
|
120
|
+
removeFocusListeners?.();
|
|
121
|
+
container.removeEventListener("controllerchange", onControllerChange);
|
|
122
|
+
listeners.clear();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function usePwaUpdate(options = {}) {
|
|
127
|
+
const { swUrl, pollIntervalMs, reloadOnControllerChange, disabled } = options;
|
|
128
|
+
const [updateReady, setUpdateReady] = react.useState(false);
|
|
129
|
+
const updaterRef = react.useRef(null);
|
|
130
|
+
react.useEffect(() => {
|
|
131
|
+
const updater = createPwaUpdater({
|
|
132
|
+
swUrl,
|
|
133
|
+
pollIntervalMs,
|
|
134
|
+
reloadOnControllerChange,
|
|
135
|
+
disabled
|
|
136
|
+
});
|
|
137
|
+
updaterRef.current = updater;
|
|
138
|
+
setUpdateReady(updater.getState().updateReady);
|
|
139
|
+
const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));
|
|
140
|
+
return () => {
|
|
141
|
+
unsubscribe();
|
|
142
|
+
updater.destroy();
|
|
143
|
+
updaterRef.current = null;
|
|
144
|
+
};
|
|
145
|
+
}, [swUrl, pollIntervalMs, reloadOnControllerChange, disabled]);
|
|
146
|
+
return {
|
|
147
|
+
updateReady,
|
|
148
|
+
applyUpdate: () => updaterRef.current?.applyUpdate()
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
var DEFAULT_LABELS = {
|
|
152
|
+
title: "New version available",
|
|
153
|
+
body: "Reload to get the latest version.",
|
|
154
|
+
update: "Update",
|
|
155
|
+
dismiss: "Later",
|
|
156
|
+
close: "Close"
|
|
157
|
+
};
|
|
158
|
+
function PwaUpdateBanner({
|
|
159
|
+
updateReady,
|
|
160
|
+
onUpdate,
|
|
161
|
+
onDismiss,
|
|
162
|
+
labels,
|
|
163
|
+
className
|
|
164
|
+
}) {
|
|
165
|
+
if (!updateReady) return null;
|
|
166
|
+
const l = { ...DEFAULT_LABELS, ...labels };
|
|
167
|
+
const dismiss = () => onDismiss?.();
|
|
168
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", "aria-live": "polite", "data-testid": "pwa-update-banner", className, children: [
|
|
169
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": "pwa-update-title", children: l.title }),
|
|
170
|
+
l.body ? /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": "pwa-update-body", children: l.body }) : null,
|
|
171
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "pwa-update-confirm", onClick: onUpdate, children: l.update }),
|
|
172
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-testid": "pwa-update-dismiss", onClick: dismiss, children: l.dismiss }),
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
174
|
+
"button",
|
|
175
|
+
{
|
|
176
|
+
type: "button",
|
|
177
|
+
"data-testid": "pwa-update-close",
|
|
178
|
+
"aria-label": l.close,
|
|
179
|
+
onClick: dismiss,
|
|
180
|
+
children: "\xD7"
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
] });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
exports.PwaUpdateBanner = PwaUpdateBanner;
|
|
187
|
+
exports.usePwaUpdate = usePwaUpdate;
|
|
188
|
+
//# sourceMappingURL=react.cjs.map
|
|
189
|
+
//# sourceMappingURL=react.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/react.tsx"],"names":["useState","useRef","useEffect","jsxs","jsx"],"mappings":"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","file":"react.cjs","sourcesContent":["/**\n * The single source of the client↔service-worker \"activate the waiting worker\"\n * message. Both the client controller (`applyUpdate`) and the SW helper\n * (`listenForSkipWaiting`) import it so the contract can never drift.\n */\nexport const SKIP_WAITING = \"SKIP_WAITING\" as const;\n\nexport const SKIP_WAITING_MESSAGE = { type: SKIP_WAITING } as const;\n\nexport type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport type { SkipWaitingMessage } from \"./message.js\";\n\nexport interface PwaUpdaterOptions {\n /** Service-worker script URL to register. Default `/sw.js`. */\n swUrl?: string;\n /**\n * Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the\n * app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)\n * — the updater then attaches to the existing registration via\n * `navigator.serviceWorker.ready` instead of registering again. Default `true`.\n */\n register?: boolean;\n /** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */\n pollIntervalMs?: number;\n /**\n * Also check for an update when the tab regains focus / becomes visible.\n * A constantly-focused tab never fires `visibilitychange`, so `focus` is the\n * one that catches \"user came back to the app\". Default `true`.\n */\n updateOnFocus?: boolean;\n /** Reload the page once the new worker takes control. Default `true`. */\n reloadOnControllerChange?: boolean;\n /**\n * Consumer guard. When `true` the updater is an inert no-op (registers\n * nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`\n * — the package never hardcodes an environment or a `.native` check.\n */\n disabled?: boolean;\n}\n\nexport interface PwaUpdaterState {\n /** A new service worker is installed and waiting to activate. */\n updateReady: boolean;\n}\n\nexport interface PwaUpdater {\n /** Subscribe to state changes. Returns an unsubscribe fn. */\n subscribe(listener: (state: PwaUpdaterState) => void): () => void;\n getState(): PwaUpdaterState;\n /** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */\n applyUpdate(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\n\n/**\n * Framework- and bundler-agnostic controller for the PWA \"new version\n * available\" lifecycle: register (or attach) → detect a waiting worker\n * (suppressing the first install) → let the consumer activate it → reload on\n * takeover, re-checking on interval + focus/visibility.\n *\n * Distilled from the pattern hand-rolled across the fleet (fds, cardmem,\n * pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)\n * as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.\n */\nexport function createPwaUpdater(options: PwaUpdaterOptions = {}): PwaUpdater {\n const {\n swUrl = \"/sw.js\",\n register = true,\n pollIntervalMs = DEFAULT_POLL_MS,\n updateOnFocus = true,\n reloadOnControllerChange = true,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\n let waitingWorker: ServiceWorker | null = null;\n\n const getState = (): PwaUpdaterState => ({ updateReady });\n const emit = (): void => {\n const state = getState();\n for (const listener of listeners) listener(state);\n };\n const subscribe = (listener: (state: PwaUpdaterState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const supported =\n typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator;\n\n // Inert no-op: disabled by the consumer, or no service-worker support at all.\n if (disabled || !supported) {\n return {\n subscribe,\n getState,\n applyUpdate() {},\n destroy() {\n listeners.clear();\n },\n };\n }\n\n const container = navigator.serviceWorker;\n let pollId: ReturnType<typeof setInterval> | null = null;\n let removeFocusListeners: (() => void) | null = null;\n let reloading = false;\n let destroyed = false;\n\n const markReady = (worker: ServiceWorker): void => {\n waitingWorker = worker;\n if (!updateReady) {\n updateReady = true;\n emit();\n }\n };\n\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n container.addEventListener(\"controllerchange\", onControllerChange);\n\n const watchInstalling = (registration: ServiceWorkerRegistration): void => {\n const installing = registration.installing;\n if (!installing) return;\n installing.addEventListener(\"statechange\", () => {\n // A new worker reaching `installed` WHILE a controller already exists is\n // an update. Reaching `installed` with no controller is the FIRST install\n // — nothing to update, so we stay quiet.\n if (installing.state === \"installed\" && container.controller) {\n markReady(installing);\n }\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n const check = (): void => {\n registration.update().catch(() => {});\n };\n const onFocus = (): void => check();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n check();\n }\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n removeFocusListeners = () => {\n if (typeof window !== \"undefined\") window.removeEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n };\n\n // Manual style registers swUrl; serwist/registerSW style attaches to the\n // registration the app already made via `.ready`.\n const registrationPromise = register ? container.register(swUrl) : container.ready;\n\n registrationPromise\n .then((registration) => {\n if (destroyed) return;\n if (registration.waiting) markReady(registration.waiting);\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n registration.update().catch(() => {\n // ignore transient update-check network errors\n });\n }, pollIntervalMs);\n }\n wireFocusChecks(registration);\n })\n .catch(() => {\n // registration failed — stay inert rather than throw into the app\n });\n\n return {\n subscribe,\n getState,\n applyUpdate() {\n if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);\n },\n destroy() {\n destroyed = true;\n if (pollId !== null) clearInterval(pollId);\n removeFocusListeners?.();\n container.removeEventListener(\"controllerchange\", onControllerChange);\n listeners.clear();\n },\n };\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * React binding for {@link createPwaUpdater}. Returns whether an update is\n * ready plus the `applyUpdate` action. The controller is created once and\n * torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n} {\n const { swUrl, pollIntervalMs, reloadOnControllerChange, disabled } = options;\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n const updater = createPwaUpdater({\n swUrl,\n pollIntervalMs,\n reloadOnControllerChange,\n disabled,\n });\n updaterRef.current = updater;\n setUpdateReady(updater.getState().updateReady);\n const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));\n return () => {\n unsubscribe();\n updater.destroy();\n updaterRef.current = null;\n };\n }, [swUrl, pollIntervalMs, reloadOnControllerChange, disabled]);\n\n return {\n updateReady,\n applyUpdate: () => updaterRef.current?.applyUpdate(),\n };\n}\n\nexport interface PwaUpdateBannerLabels {\n title?: string;\n body?: string;\n update?: string;\n dismiss?: string;\n close?: string;\n}\n\nconst DEFAULT_LABELS: Required<PwaUpdateBannerLabels> = {\n title: \"New version available\",\n body: \"Reload to get the latest version.\",\n update: \"Update\",\n dismiss: \"Later\",\n close: \"Close\",\n};\n\nexport interface PwaUpdateBannerProps {\n /** From `usePwaUpdate().updateReady`. Nothing renders when false. */\n updateReady: boolean;\n /** From `usePwaUpdate().applyUpdate`. */\n onUpdate: () => void;\n /** Optional dismiss handler (wired to both \"Later\" and the close ✕). */\n onDismiss?: () => void;\n /** Override any of the built-in strings (i18n). */\n labels?: PwaUpdateBannerLabels;\n /** Your class(es) — the skeleton carries NO colours or design-system styling. */\n className?: string;\n}\n\n/**\n * Unstyled, accessible \"new version available\" banner skeleton. It ships\n * structure + a11y + stable testids only; you own the look via `className`\n * and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather\n * build the UI yourself.\n */\nexport function PwaUpdateBanner({\n updateReady,\n onUpdate,\n onDismiss,\n labels,\n className,\n}: PwaUpdateBannerProps) {\n if (!updateReady) return null;\n const l = { ...DEFAULT_LABELS, ...labels };\n const dismiss = () => onDismiss?.();\n return (\n <div role=\"status\" aria-live=\"polite\" data-testid=\"pwa-update-banner\" className={className}>\n <p data-testid=\"pwa-update-title\">{l.title}</p>\n {l.body ? <p data-testid=\"pwa-update-body\">{l.body}</p> : null}\n <button type=\"button\" data-testid=\"pwa-update-confirm\" onClick={onUpdate}>\n {l.update}\n </button>\n <button type=\"button\" data-testid=\"pwa-update-dismiss\" onClick={dismiss}>\n {l.dismiss}\n </button>\n <button\n type=\"button\"\n data-testid=\"pwa-update-close\"\n aria-label={l.close}\n onClick={dismiss}\n >\n {\"×\"}\n </button>\n </div>\n );\n}\n"]}
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface PwaUpdaterOptions {
|
|
4
|
+
/** Service-worker script URL to register. Default `/sw.js`. */
|
|
5
|
+
swUrl?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the
|
|
8
|
+
* app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)
|
|
9
|
+
* — the updater then attaches to the existing registration via
|
|
10
|
+
* `navigator.serviceWorker.ready` instead of registering again. Default `true`.
|
|
11
|
+
*/
|
|
12
|
+
register?: boolean;
|
|
13
|
+
/** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Also check for an update when the tab regains focus / becomes visible.
|
|
17
|
+
* A constantly-focused tab never fires `visibilitychange`, so `focus` is the
|
|
18
|
+
* one that catches "user came back to the app". Default `true`.
|
|
19
|
+
*/
|
|
20
|
+
updateOnFocus?: boolean;
|
|
21
|
+
/** Reload the page once the new worker takes control. Default `true`. */
|
|
22
|
+
reloadOnControllerChange?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Consumer guard. When `true` the updater is an inert no-op (registers
|
|
25
|
+
* nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`
|
|
26
|
+
* — the package never hardcodes an environment or a `.native` check.
|
|
27
|
+
*/
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* React binding for {@link createPwaUpdater}. Returns whether an update is
|
|
33
|
+
* ready plus the `applyUpdate` action. The controller is created once and
|
|
34
|
+
* torn down on unmount.
|
|
35
|
+
*/
|
|
36
|
+
declare function usePwaUpdate(options?: PwaUpdaterOptions): {
|
|
37
|
+
updateReady: boolean;
|
|
38
|
+
applyUpdate: () => void;
|
|
39
|
+
};
|
|
40
|
+
interface PwaUpdateBannerLabels {
|
|
41
|
+
title?: string;
|
|
42
|
+
body?: string;
|
|
43
|
+
update?: string;
|
|
44
|
+
dismiss?: string;
|
|
45
|
+
close?: string;
|
|
46
|
+
}
|
|
47
|
+
interface PwaUpdateBannerProps {
|
|
48
|
+
/** From `usePwaUpdate().updateReady`. Nothing renders when false. */
|
|
49
|
+
updateReady: boolean;
|
|
50
|
+
/** From `usePwaUpdate().applyUpdate`. */
|
|
51
|
+
onUpdate: () => void;
|
|
52
|
+
/** Optional dismiss handler (wired to both "Later" and the close ✕). */
|
|
53
|
+
onDismiss?: () => void;
|
|
54
|
+
/** Override any of the built-in strings (i18n). */
|
|
55
|
+
labels?: PwaUpdateBannerLabels;
|
|
56
|
+
/** Your class(es) — the skeleton carries NO colours or design-system styling. */
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Unstyled, accessible "new version available" banner skeleton. It ships
|
|
61
|
+
* structure + a11y + stable testids only; you own the look via `className`
|
|
62
|
+
* and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather
|
|
63
|
+
* build the UI yourself.
|
|
64
|
+
*/
|
|
65
|
+
declare function PwaUpdateBanner({ updateReady, onUpdate, onDismiss, labels, className, }: PwaUpdateBannerProps): react.JSX.Element | null;
|
|
66
|
+
|
|
67
|
+
export { PwaUpdateBanner, type PwaUpdateBannerLabels, type PwaUpdateBannerProps, usePwaUpdate };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface PwaUpdaterOptions {
|
|
4
|
+
/** Service-worker script URL to register. Default `/sw.js`. */
|
|
5
|
+
swUrl?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the
|
|
8
|
+
* app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)
|
|
9
|
+
* — the updater then attaches to the existing registration via
|
|
10
|
+
* `navigator.serviceWorker.ready` instead of registering again. Default `true`.
|
|
11
|
+
*/
|
|
12
|
+
register?: boolean;
|
|
13
|
+
/** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Also check for an update when the tab regains focus / becomes visible.
|
|
17
|
+
* A constantly-focused tab never fires `visibilitychange`, so `focus` is the
|
|
18
|
+
* one that catches "user came back to the app". Default `true`.
|
|
19
|
+
*/
|
|
20
|
+
updateOnFocus?: boolean;
|
|
21
|
+
/** Reload the page once the new worker takes control. Default `true`. */
|
|
22
|
+
reloadOnControllerChange?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Consumer guard. When `true` the updater is an inert no-op (registers
|
|
25
|
+
* nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`
|
|
26
|
+
* — the package never hardcodes an environment or a `.native` check.
|
|
27
|
+
*/
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* React binding for {@link createPwaUpdater}. Returns whether an update is
|
|
33
|
+
* ready plus the `applyUpdate` action. The controller is created once and
|
|
34
|
+
* torn down on unmount.
|
|
35
|
+
*/
|
|
36
|
+
declare function usePwaUpdate(options?: PwaUpdaterOptions): {
|
|
37
|
+
updateReady: boolean;
|
|
38
|
+
applyUpdate: () => void;
|
|
39
|
+
};
|
|
40
|
+
interface PwaUpdateBannerLabels {
|
|
41
|
+
title?: string;
|
|
42
|
+
body?: string;
|
|
43
|
+
update?: string;
|
|
44
|
+
dismiss?: string;
|
|
45
|
+
close?: string;
|
|
46
|
+
}
|
|
47
|
+
interface PwaUpdateBannerProps {
|
|
48
|
+
/** From `usePwaUpdate().updateReady`. Nothing renders when false. */
|
|
49
|
+
updateReady: boolean;
|
|
50
|
+
/** From `usePwaUpdate().applyUpdate`. */
|
|
51
|
+
onUpdate: () => void;
|
|
52
|
+
/** Optional dismiss handler (wired to both "Later" and the close ✕). */
|
|
53
|
+
onDismiss?: () => void;
|
|
54
|
+
/** Override any of the built-in strings (i18n). */
|
|
55
|
+
labels?: PwaUpdateBannerLabels;
|
|
56
|
+
/** Your class(es) — the skeleton carries NO colours or design-system styling. */
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Unstyled, accessible "new version available" banner skeleton. It ships
|
|
61
|
+
* structure + a11y + stable testids only; you own the look via `className`
|
|
62
|
+
* and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather
|
|
63
|
+
* build the UI yourself.
|
|
64
|
+
*/
|
|
65
|
+
declare function PwaUpdateBanner({ updateReady, onUpdate, onDismiss, labels, className, }: PwaUpdateBannerProps): react.JSX.Element | null;
|
|
66
|
+
|
|
67
|
+
export { PwaUpdateBanner, type PwaUpdateBannerLabels, type PwaUpdateBannerProps, usePwaUpdate };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/react.tsx
|
|
5
|
+
|
|
6
|
+
// src/message.ts
|
|
7
|
+
var SKIP_WAITING = "SKIP_WAITING";
|
|
8
|
+
var SKIP_WAITING_MESSAGE = { type: SKIP_WAITING };
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
var DEFAULT_POLL_MS = 60 * 60 * 1e3;
|
|
12
|
+
function createPwaUpdater(options = {}) {
|
|
13
|
+
const {
|
|
14
|
+
swUrl = "/sw.js",
|
|
15
|
+
register = true,
|
|
16
|
+
pollIntervalMs = DEFAULT_POLL_MS,
|
|
17
|
+
updateOnFocus = true,
|
|
18
|
+
reloadOnControllerChange = true,
|
|
19
|
+
disabled = false
|
|
20
|
+
} = options;
|
|
21
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
22
|
+
let updateReady = false;
|
|
23
|
+
let waitingWorker = null;
|
|
24
|
+
const getState = () => ({ updateReady });
|
|
25
|
+
const emit = () => {
|
|
26
|
+
const state = getState();
|
|
27
|
+
for (const listener of listeners) listener(state);
|
|
28
|
+
};
|
|
29
|
+
const subscribe = (listener) => {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => listeners.delete(listener);
|
|
32
|
+
};
|
|
33
|
+
const supported = typeof navigator !== "undefined" && "serviceWorker" in navigator;
|
|
34
|
+
if (disabled || !supported) {
|
|
35
|
+
return {
|
|
36
|
+
subscribe,
|
|
37
|
+
getState,
|
|
38
|
+
applyUpdate() {
|
|
39
|
+
},
|
|
40
|
+
destroy() {
|
|
41
|
+
listeners.clear();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const container = navigator.serviceWorker;
|
|
46
|
+
let pollId = null;
|
|
47
|
+
let removeFocusListeners = null;
|
|
48
|
+
let reloading = false;
|
|
49
|
+
let destroyed = false;
|
|
50
|
+
const markReady = (worker) => {
|
|
51
|
+
waitingWorker = worker;
|
|
52
|
+
if (!updateReady) {
|
|
53
|
+
updateReady = true;
|
|
54
|
+
emit();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const onControllerChange = () => {
|
|
58
|
+
if (!reloadOnControllerChange || reloading) return;
|
|
59
|
+
reloading = true;
|
|
60
|
+
if (typeof location !== "undefined") location.reload();
|
|
61
|
+
};
|
|
62
|
+
container.addEventListener("controllerchange", onControllerChange);
|
|
63
|
+
const watchInstalling = (registration) => {
|
|
64
|
+
const installing = registration.installing;
|
|
65
|
+
if (!installing) return;
|
|
66
|
+
installing.addEventListener("statechange", () => {
|
|
67
|
+
if (installing.state === "installed" && container.controller) {
|
|
68
|
+
markReady(installing);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const wireFocusChecks = (registration) => {
|
|
73
|
+
if (!updateOnFocus) return;
|
|
74
|
+
const check = () => {
|
|
75
|
+
registration.update().catch(() => {
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const onFocus = () => check();
|
|
79
|
+
const onVisibility = () => {
|
|
80
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
81
|
+
check();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
if (typeof window !== "undefined") window.addEventListener("focus", onFocus);
|
|
85
|
+
if (typeof document !== "undefined") {
|
|
86
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
87
|
+
}
|
|
88
|
+
removeFocusListeners = () => {
|
|
89
|
+
if (typeof window !== "undefined") window.removeEventListener("focus", onFocus);
|
|
90
|
+
if (typeof document !== "undefined") {
|
|
91
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const registrationPromise = register ? container.register(swUrl) : container.ready;
|
|
96
|
+
registrationPromise.then((registration) => {
|
|
97
|
+
if (destroyed) return;
|
|
98
|
+
if (registration.waiting) markReady(registration.waiting);
|
|
99
|
+
registration.addEventListener("updatefound", () => watchInstalling(registration));
|
|
100
|
+
if (pollIntervalMs > 0) {
|
|
101
|
+
pollId = setInterval(() => {
|
|
102
|
+
registration.update().catch(() => {
|
|
103
|
+
});
|
|
104
|
+
}, pollIntervalMs);
|
|
105
|
+
}
|
|
106
|
+
wireFocusChecks(registration);
|
|
107
|
+
}).catch(() => {
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
subscribe,
|
|
111
|
+
getState,
|
|
112
|
+
applyUpdate() {
|
|
113
|
+
if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);
|
|
114
|
+
},
|
|
115
|
+
destroy() {
|
|
116
|
+
destroyed = true;
|
|
117
|
+
if (pollId !== null) clearInterval(pollId);
|
|
118
|
+
removeFocusListeners?.();
|
|
119
|
+
container.removeEventListener("controllerchange", onControllerChange);
|
|
120
|
+
listeners.clear();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function usePwaUpdate(options = {}) {
|
|
125
|
+
const { swUrl, pollIntervalMs, reloadOnControllerChange, disabled } = options;
|
|
126
|
+
const [updateReady, setUpdateReady] = useState(false);
|
|
127
|
+
const updaterRef = useRef(null);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const updater = createPwaUpdater({
|
|
130
|
+
swUrl,
|
|
131
|
+
pollIntervalMs,
|
|
132
|
+
reloadOnControllerChange,
|
|
133
|
+
disabled
|
|
134
|
+
});
|
|
135
|
+
updaterRef.current = updater;
|
|
136
|
+
setUpdateReady(updater.getState().updateReady);
|
|
137
|
+
const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));
|
|
138
|
+
return () => {
|
|
139
|
+
unsubscribe();
|
|
140
|
+
updater.destroy();
|
|
141
|
+
updaterRef.current = null;
|
|
142
|
+
};
|
|
143
|
+
}, [swUrl, pollIntervalMs, reloadOnControllerChange, disabled]);
|
|
144
|
+
return {
|
|
145
|
+
updateReady,
|
|
146
|
+
applyUpdate: () => updaterRef.current?.applyUpdate()
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
var DEFAULT_LABELS = {
|
|
150
|
+
title: "New version available",
|
|
151
|
+
body: "Reload to get the latest version.",
|
|
152
|
+
update: "Update",
|
|
153
|
+
dismiss: "Later",
|
|
154
|
+
close: "Close"
|
|
155
|
+
};
|
|
156
|
+
function PwaUpdateBanner({
|
|
157
|
+
updateReady,
|
|
158
|
+
onUpdate,
|
|
159
|
+
onDismiss,
|
|
160
|
+
labels,
|
|
161
|
+
className
|
|
162
|
+
}) {
|
|
163
|
+
if (!updateReady) return null;
|
|
164
|
+
const l = { ...DEFAULT_LABELS, ...labels };
|
|
165
|
+
const dismiss = () => onDismiss?.();
|
|
166
|
+
return /* @__PURE__ */ jsxs("div", { role: "status", "aria-live": "polite", "data-testid": "pwa-update-banner", className, children: [
|
|
167
|
+
/* @__PURE__ */ jsx("p", { "data-testid": "pwa-update-title", children: l.title }),
|
|
168
|
+
l.body ? /* @__PURE__ */ jsx("p", { "data-testid": "pwa-update-body", children: l.body }) : null,
|
|
169
|
+
/* @__PURE__ */ jsx("button", { type: "button", "data-testid": "pwa-update-confirm", onClick: onUpdate, children: l.update }),
|
|
170
|
+
/* @__PURE__ */ jsx("button", { type: "button", "data-testid": "pwa-update-dismiss", onClick: dismiss, children: l.dismiss }),
|
|
171
|
+
/* @__PURE__ */ jsx(
|
|
172
|
+
"button",
|
|
173
|
+
{
|
|
174
|
+
type: "button",
|
|
175
|
+
"data-testid": "pwa-update-close",
|
|
176
|
+
"aria-label": l.close,
|
|
177
|
+
onClick: dismiss,
|
|
178
|
+
children: "\xD7"
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
] });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { PwaUpdateBanner, usePwaUpdate };
|
|
185
|
+
//# sourceMappingURL=react.js.map
|
|
186
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/react.tsx"],"names":[],"mappings":"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","file":"react.js","sourcesContent":["/**\n * The single source of the client↔service-worker \"activate the waiting worker\"\n * message. Both the client controller (`applyUpdate`) and the SW helper\n * (`listenForSkipWaiting`) import it so the contract can never drift.\n */\nexport const SKIP_WAITING = \"SKIP_WAITING\" as const;\n\nexport const SKIP_WAITING_MESSAGE = { type: SKIP_WAITING } as const;\n\nexport type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport type { SkipWaitingMessage } from \"./message.js\";\n\nexport interface PwaUpdaterOptions {\n /** Service-worker script URL to register. Default `/sw.js`. */\n swUrl?: string;\n /**\n * Register `swUrl` ourselves (manual `sw.js` style). Set `false` when the\n * app already registers the SW (e.g. Serwist's `registerSW` / `@serwist/next`)\n * — the updater then attaches to the existing registration via\n * `navigator.serviceWorker.ready` instead of registering again. Default `true`.\n */\n register?: boolean;\n /** How often to poll `registration.update()` (ms). `0` disables. Default 60 min. */\n pollIntervalMs?: number;\n /**\n * Also check for an update when the tab regains focus / becomes visible.\n * A constantly-focused tab never fires `visibilitychange`, so `focus` is the\n * one that catches \"user came back to the app\". Default `true`.\n */\n updateOnFocus?: boolean;\n /** Reload the page once the new worker takes control. Default `true`. */\n reloadOnControllerChange?: boolean;\n /**\n * Consumer guard. When `true` the updater is an inert no-op (registers\n * nothing). Pass your own policy, e.g. `disabled: isNativeCapacitor || isDev`\n * — the package never hardcodes an environment or a `.native` check.\n */\n disabled?: boolean;\n}\n\nexport interface PwaUpdaterState {\n /** A new service worker is installed and waiting to activate. */\n updateReady: boolean;\n}\n\nexport interface PwaUpdater {\n /** Subscribe to state changes. Returns an unsubscribe fn. */\n subscribe(listener: (state: PwaUpdaterState) => void): () => void;\n getState(): PwaUpdaterState;\n /** Tell the waiting worker to activate (posts SKIP_WAITING). No-op if none waits. */\n applyUpdate(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\n\n/**\n * Framework- and bundler-agnostic controller for the PWA \"new version\n * available\" lifecycle: register (or attach) → detect a waiting worker\n * (suppressing the first install) → let the consumer activate it → reload on\n * takeover, re-checking on interval + focus/visibility.\n *\n * Distilled from the pattern hand-rolled across the fleet (fds, cardmem,\n * pitch-vault). Works with any service worker (Serwist, Workbox, hand-rolled)\n * as long as the SW answers a `SKIP_WAITING` message — see `@broberg/pwa/sw`.\n */\nexport function createPwaUpdater(options: PwaUpdaterOptions = {}): PwaUpdater {\n const {\n swUrl = \"/sw.js\",\n register = true,\n pollIntervalMs = DEFAULT_POLL_MS,\n updateOnFocus = true,\n reloadOnControllerChange = true,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\n let waitingWorker: ServiceWorker | null = null;\n\n const getState = (): PwaUpdaterState => ({ updateReady });\n const emit = (): void => {\n const state = getState();\n for (const listener of listeners) listener(state);\n };\n const subscribe = (listener: (state: PwaUpdaterState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const supported =\n typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator;\n\n // Inert no-op: disabled by the consumer, or no service-worker support at all.\n if (disabled || !supported) {\n return {\n subscribe,\n getState,\n applyUpdate() {},\n destroy() {\n listeners.clear();\n },\n };\n }\n\n const container = navigator.serviceWorker;\n let pollId: ReturnType<typeof setInterval> | null = null;\n let removeFocusListeners: (() => void) | null = null;\n let reloading = false;\n let destroyed = false;\n\n const markReady = (worker: ServiceWorker): void => {\n waitingWorker = worker;\n if (!updateReady) {\n updateReady = true;\n emit();\n }\n };\n\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n container.addEventListener(\"controllerchange\", onControllerChange);\n\n const watchInstalling = (registration: ServiceWorkerRegistration): void => {\n const installing = registration.installing;\n if (!installing) return;\n installing.addEventListener(\"statechange\", () => {\n // A new worker reaching `installed` WHILE a controller already exists is\n // an update. Reaching `installed` with no controller is the FIRST install\n // — nothing to update, so we stay quiet.\n if (installing.state === \"installed\" && container.controller) {\n markReady(installing);\n }\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n const check = (): void => {\n registration.update().catch(() => {});\n };\n const onFocus = (): void => check();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n check();\n }\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n removeFocusListeners = () => {\n if (typeof window !== \"undefined\") window.removeEventListener(\"focus\", onFocus);\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n };\n\n // Manual style registers swUrl; serwist/registerSW style attaches to the\n // registration the app already made via `.ready`.\n const registrationPromise = register ? container.register(swUrl) : container.ready;\n\n registrationPromise\n .then((registration) => {\n if (destroyed) return;\n if (registration.waiting) markReady(registration.waiting);\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n registration.update().catch(() => {\n // ignore transient update-check network errors\n });\n }, pollIntervalMs);\n }\n wireFocusChecks(registration);\n })\n .catch(() => {\n // registration failed — stay inert rather than throw into the app\n });\n\n return {\n subscribe,\n getState,\n applyUpdate() {\n if (waitingWorker) waitingWorker.postMessage(SKIP_WAITING_MESSAGE);\n },\n destroy() {\n destroyed = true;\n if (pollId !== null) clearInterval(pollId);\n removeFocusListeners?.();\n container.removeEventListener(\"controllerchange\", onControllerChange);\n listeners.clear();\n },\n };\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * React binding for {@link createPwaUpdater}. Returns whether an update is\n * ready plus the `applyUpdate` action. The controller is created once and\n * torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n} {\n const { swUrl, pollIntervalMs, reloadOnControllerChange, disabled } = options;\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n const updater = createPwaUpdater({\n swUrl,\n pollIntervalMs,\n reloadOnControllerChange,\n disabled,\n });\n updaterRef.current = updater;\n setUpdateReady(updater.getState().updateReady);\n const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));\n return () => {\n unsubscribe();\n updater.destroy();\n updaterRef.current = null;\n };\n }, [swUrl, pollIntervalMs, reloadOnControllerChange, disabled]);\n\n return {\n updateReady,\n applyUpdate: () => updaterRef.current?.applyUpdate(),\n };\n}\n\nexport interface PwaUpdateBannerLabels {\n title?: string;\n body?: string;\n update?: string;\n dismiss?: string;\n close?: string;\n}\n\nconst DEFAULT_LABELS: Required<PwaUpdateBannerLabels> = {\n title: \"New version available\",\n body: \"Reload to get the latest version.\",\n update: \"Update\",\n dismiss: \"Later\",\n close: \"Close\",\n};\n\nexport interface PwaUpdateBannerProps {\n /** From `usePwaUpdate().updateReady`. Nothing renders when false. */\n updateReady: boolean;\n /** From `usePwaUpdate().applyUpdate`. */\n onUpdate: () => void;\n /** Optional dismiss handler (wired to both \"Later\" and the close ✕). */\n onDismiss?: () => void;\n /** Override any of the built-in strings (i18n). */\n labels?: PwaUpdateBannerLabels;\n /** Your class(es) — the skeleton carries NO colours or design-system styling. */\n className?: string;\n}\n\n/**\n * Unstyled, accessible \"new version available\" banner skeleton. It ships\n * structure + a11y + stable testids only; you own the look via `className`\n * and your design tokens. Prefer {@link usePwaUpdate} directly if you'd rather\n * build the UI yourself.\n */\nexport function PwaUpdateBanner({\n updateReady,\n onUpdate,\n onDismiss,\n labels,\n className,\n}: PwaUpdateBannerProps) {\n if (!updateReady) return null;\n const l = { ...DEFAULT_LABELS, ...labels };\n const dismiss = () => onDismiss?.();\n return (\n <div role=\"status\" aria-live=\"polite\" data-testid=\"pwa-update-banner\" className={className}>\n <p data-testid=\"pwa-update-title\">{l.title}</p>\n {l.body ? <p data-testid=\"pwa-update-body\">{l.body}</p> : null}\n <button type=\"button\" data-testid=\"pwa-update-confirm\" onClick={onUpdate}>\n {l.update}\n </button>\n <button type=\"button\" data-testid=\"pwa-update-dismiss\" onClick={dismiss}>\n {l.dismiss}\n </button>\n <button\n type=\"button\"\n data-testid=\"pwa-update-close\"\n aria-label={l.close}\n onClick={dismiss}\n >\n {\"×\"}\n </button>\n </div>\n );\n}\n"]}
|
package/dist/sw.cjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/message.ts
|
|
4
|
+
var SKIP_WAITING = "SKIP_WAITING";
|
|
5
|
+
var SKIP_WAITING_MESSAGE = { type: SKIP_WAITING };
|
|
6
|
+
|
|
7
|
+
// src/sw.ts
|
|
8
|
+
function listenForSkipWaiting(scope) {
|
|
9
|
+
const target = scope ?? globalThis;
|
|
10
|
+
target.addEventListener("message", (event) => {
|
|
11
|
+
if (event.data?.type === SKIP_WAITING) {
|
|
12
|
+
void target.skipWaiting();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.SKIP_WAITING = SKIP_WAITING;
|
|
18
|
+
exports.SKIP_WAITING_MESSAGE = SKIP_WAITING_MESSAGE;
|
|
19
|
+
exports.listenForSkipWaiting = listenForSkipWaiting;
|
|
20
|
+
//# sourceMappingURL=sw.cjs.map
|
|
21
|
+
//# sourceMappingURL=sw.cjs.map
|
package/dist/sw.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/sw.ts"],"names":[],"mappings":";;;AAKO,IAAM,YAAA,GAAe;AAErB,IAAM,oBAAA,GAAuB,EAAE,IAAA,EAAM,YAAA;;;ACyBrC,SAAS,qBAAqB,KAAA,EAAgC;AACnE,EAAA,MAAM,SACJ,KAAA,IAAU,UAAA;AACZ,EAAA,MAAA,CAAO,gBAAA,CAAiB,SAAA,EAAW,CAAC,KAAA,KAAU;AAC5C,IAAA,IAAK,KAAA,CAAM,IAAA,EAA4B,IAAA,KAAS,YAAA,EAAc;AAC5D,MAAA,KAAK,OAAO,WAAA,EAAY;AAAA,IAC1B;AAAA,EACF,CAAC,CAAA;AACH","file":"sw.cjs","sourcesContent":["/**\n * The single source of the client↔service-worker \"activate the waiting worker\"\n * message. Both the client controller (`applyUpdate`) and the SW helper\n * (`listenForSkipWaiting`) import it so the contract can never drift.\n */\nexport const SKIP_WAITING = \"SKIP_WAITING\" as const;\n\nexport const SKIP_WAITING_MESSAGE = { type: SKIP_WAITING } as const;\n\nexport type SkipWaitingMessage = typeof SKIP_WAITING_MESSAGE;\n","import { SKIP_WAITING } from \"./message.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport type { SkipWaitingMessage } from \"./message.js\";\n\n/**\n * The minimal slice of a service-worker global scope this helper needs.\n * Kept as a local interface so the package doesn't pull in the `webworker`\n * TS lib (which clashes with `dom`) and stays trivially testable.\n */\nexport interface SkipWaitingScope {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: unknown }) => void,\n ): void;\n skipWaiting(): Promise<void> | void;\n}\n\n/**\n * Wire the service-worker side of the update handshake: when the client calls\n * `applyUpdate()` (posts `SKIP_WAITING`), the worker activates immediately.\n *\n * Call it once inside your service worker, before other setup:\n *\n * ```ts\n * import { listenForSkipWaiting } from \"@broberg/pwa/sw\";\n * listenForSkipWaiting();\n * ```\n *\n * Serwist/Workbox users: keep `skipWaiting: false` (so activation is\n * user-gated by the banner, not automatic) and `clientsClaim: true`.\n */\nexport function listenForSkipWaiting(scope?: SkipWaitingScope): void {\n const target =\n scope ?? (globalThis as unknown as SkipWaitingScope);\n target.addEventListener(\"message\", (event) => {\n if ((event.data as { type?: string })?.type === SKIP_WAITING) {\n void target.skipWaiting();\n }\n });\n}\n"]}
|
package/dist/sw.d.cts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { S as SKIP_WAITING, a as SKIP_WAITING_MESSAGE, b as SkipWaitingMessage } from './message-CBBmtYuB.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The minimal slice of a service-worker global scope this helper needs.
|
|
5
|
+
* Kept as a local interface so the package doesn't pull in the `webworker`
|
|
6
|
+
* TS lib (which clashes with `dom`) and stays trivially testable.
|
|
7
|
+
*/
|
|
8
|
+
interface SkipWaitingScope {
|
|
9
|
+
addEventListener(type: "message", listener: (event: {
|
|
10
|
+
data?: unknown;
|
|
11
|
+
}) => void): void;
|
|
12
|
+
skipWaiting(): Promise<void> | void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wire the service-worker side of the update handshake: when the client calls
|
|
16
|
+
* `applyUpdate()` (posts `SKIP_WAITING`), the worker activates immediately.
|
|
17
|
+
*
|
|
18
|
+
* Call it once inside your service worker, before other setup:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { listenForSkipWaiting } from "@broberg/pwa/sw";
|
|
22
|
+
* listenForSkipWaiting();
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Serwist/Workbox users: keep `skipWaiting: false` (so activation is
|
|
26
|
+
* user-gated by the banner, not automatic) and `clientsClaim: true`.
|
|
27
|
+
*/
|
|
28
|
+
declare function listenForSkipWaiting(scope?: SkipWaitingScope): void;
|
|
29
|
+
|
|
30
|
+
export { type SkipWaitingScope, listenForSkipWaiting };
|