@broberg/pwa 0.2.1 → 0.3.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/dist/preact.cjs +10 -8
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.js +10 -8
- package/dist/preact.js.map +1 -1
- package/dist/react.cjs +10 -8
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +67 -0
- package/dist/react.d.ts +67 -0
- package/dist/react.js +10 -8
- package/dist/react.js.map +1 -1
- package/package.json +6 -5
package/dist/preact.cjs
CHANGED
|
@@ -128,17 +128,19 @@ function createPwaUpdater(options = {}) {
|
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
// src/options-key.ts
|
|
133
|
+
function optionsKey(options) {
|
|
134
|
+
return JSON.stringify(
|
|
135
|
+
Object.entries(options).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
131
138
|
function usePwaUpdate(options = {}) {
|
|
132
|
-
const
|
|
139
|
+
const key = optionsKey(options);
|
|
133
140
|
const [updateReady, setUpdateReady] = hooks.useState(false);
|
|
134
141
|
const updaterRef = hooks.useRef(null);
|
|
135
142
|
hooks.useEffect(() => {
|
|
136
|
-
const updater = createPwaUpdater(
|
|
137
|
-
swUrl,
|
|
138
|
-
pollIntervalMs,
|
|
139
|
-
reloadOnControllerChange,
|
|
140
|
-
disabled
|
|
141
|
-
});
|
|
143
|
+
const updater = createPwaUpdater(options);
|
|
142
144
|
updaterRef.current = updater;
|
|
143
145
|
setUpdateReady(updater.getState().updateReady);
|
|
144
146
|
const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));
|
|
@@ -147,7 +149,7 @@ function usePwaUpdate(options = {}) {
|
|
|
147
149
|
updater.destroy();
|
|
148
150
|
updaterRef.current = null;
|
|
149
151
|
};
|
|
150
|
-
}, [
|
|
152
|
+
}, [key]);
|
|
151
153
|
return {
|
|
152
154
|
updateReady,
|
|
153
155
|
applyUpdate: () => updaterRef.current?.applyUpdate()
|
package/dist/preact.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/preact.tsx"],"names":["useState","useRef","useEffect","jsxs","jsx"],"mappings":"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","file":"preact.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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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","/** @jsxImportSource preact */\n\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * Preact binding for {@link createPwaUpdater} — API-identical to the React\n * adapter, for the Stack B (Bun/Hono/Preact/Vite) apps. Returns whether an\n * update is ready plus the `applyUpdate` action; the controller is created\n * once and 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 for Preact.\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/options-key.ts","../src/preact.tsx"],"names":["useState","useRef","useEffect","jsxs","jsx"],"mappings":"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","file":"preact.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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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 type { PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * A stable identity for an options object, for an adapter's effect dependency.\n *\n * F054.7 — WHY THIS EXISTS INSTEAD OF A DEPENDENCY LIST. Both adapters used to\n * destructure four named options and list those same four as their effect deps.\n * That is a second copy of the core's option list, and it had already drifted:\n * `register` and `updateOnFocus` were never forwarded, so a consumer writing\n * `register: false` got a service-worker registration anyway, with no error and\n * no warning. fd-sundhed found it by reading our shipped dist, which is not a\n * detector anyone should need.\n *\n * Deriving the key from the options the CALLER passed means a new core option\n * reaches both adapters with neither adapter edited. The list was the defect —\n * not the two names missing from it.\n *\n * Keys are SORTED so `{a, b}` and `{b, a}` are the same identity, and\n * `undefined` values are dropped so an explicitly-omitted option and an absent\n * one do not look like different configurations.\n */\nexport function optionsKey(options: PwaUpdaterOptions): string {\n return JSON.stringify(\n Object.entries(options)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n}\n","/** @jsxImportSource preact */\n\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\nimport { optionsKey } from \"./options-key.js\";\n\n/**\n * Preact binding for {@link createPwaUpdater} — API-identical to the React\n * adapter, for the Stack B (Bun/Hono/Preact/Vite) apps. Returns whether an\n * update is ready plus the `applyUpdate` action; the controller is created\n * once and torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n} {\n // The effect's identity comes from the options the CALLER passed, so a new\n // core option needs no edit here. eslint's exhaustive-deps cannot see\n // through `key`; that is the trade, and it is the right way round.\n const key = optionsKey(options);\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n // THE WHOLE OBJECT, not a hand-picked subset. See options-key.ts: the\n // subset is what silently dropped `register` and `updateOnFocus`.\n const updater = createPwaUpdater(options);\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 // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\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 for Preact.\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/preact.js
CHANGED
|
@@ -126,17 +126,19 @@ function createPwaUpdater(options = {}) {
|
|
|
126
126
|
}
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
// src/options-key.ts
|
|
131
|
+
function optionsKey(options) {
|
|
132
|
+
return JSON.stringify(
|
|
133
|
+
Object.entries(options).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
129
136
|
function usePwaUpdate(options = {}) {
|
|
130
|
-
const
|
|
137
|
+
const key = optionsKey(options);
|
|
131
138
|
const [updateReady, setUpdateReady] = useState(false);
|
|
132
139
|
const updaterRef = useRef(null);
|
|
133
140
|
useEffect(() => {
|
|
134
|
-
const updater = createPwaUpdater(
|
|
135
|
-
swUrl,
|
|
136
|
-
pollIntervalMs,
|
|
137
|
-
reloadOnControllerChange,
|
|
138
|
-
disabled
|
|
139
|
-
});
|
|
141
|
+
const updater = createPwaUpdater(options);
|
|
140
142
|
updaterRef.current = updater;
|
|
141
143
|
setUpdateReady(updater.getState().updateReady);
|
|
142
144
|
const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));
|
|
@@ -145,7 +147,7 @@ function usePwaUpdate(options = {}) {
|
|
|
145
147
|
updater.destroy();
|
|
146
148
|
updaterRef.current = null;
|
|
147
149
|
};
|
|
148
|
-
}, [
|
|
150
|
+
}, [key]);
|
|
149
151
|
return {
|
|
150
152
|
updateReady,
|
|
151
153
|
applyUpdate: () => updaterRef.current?.applyUpdate()
|
package/dist/preact.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/preact.tsx"],"names":[],"mappings":";;;;;;AAKO,IAAM,YAAA,GAAe,cAAA;AAErB,IAAM,oBAAA,GAAuB,EAAE,IAAA,EAAM,YAAA,EAAa;;;ACyCzD,IAAM,eAAA,GAAkB,KAAK,EAAA,GAAK,GAAA;AAY3B,SAAS,gBAAA,CAAiB,OAAA,GAA6B,EAAC,EAAe;AAC5E,EAAA,MAAM;AAAA,IACJ,KAAA,GAAQ,QAAA;AAAA,IACR,QAAA,GAAW,IAAA;AAAA,IACX,cAAA,GAAiB,eAAA;AAAA,IACjB,aAAA,GAAgB,IAAA;AAAA,IAChB,wBAAA,GAA2B,IAAA;AAAA,IAC3B,QAAA,GAAW;AAAA,GACb,GAAI,OAAA;AAEJ,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAsC;AAC5D,EAAA,IAAI,WAAA,GAAc,KAAA;AAClB,EAAA,IAAI,aAAA,GAAsC,IAAA;AAE1C,EAAA,MAAM,QAAA,GAAW,OAAwB,EAAE,WAAA,EAAY,CAAA;AACvD,EAAA,MAAM,OAAO,MAAY;AACvB,IAAA,MAAM,QAAQ,QAAA,EAAS;AACvB,IAAA,KAAA,MAAW,QAAA,IAAY,SAAA,EAAW,QAAA,CAAS,KAAK,CAAA;AAAA,EAClD,CAAA;AACA,EAAA,MAAM,SAAA,GAAY,CAAC,QAAA,KAA6D;AAC9E,IAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,IAAA,OAAO,MAAM,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,MAAM,SAAA,GACJ,OAAO,SAAA,KAAc,WAAA,IAAe,eAAA,IAAmB,SAAA;AAGzD,EAAA,IAAI,QAAA,IAAY,CAAC,SAAA,EAAW;AAC1B,IAAA,OAAO;AAAA,MACL,SAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA,GAAc;AAAA,MAAC,CAAA;AAAA,MACf,OAAA,GAAU;AACR,QAAA,SAAA,CAAU,KAAA,EAAM;AAAA,MAClB;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,YAAY,SAAA,CAAU,aAAA;AAC5B,EAAA,IAAI,MAAA,GAAgD,IAAA;AACpD,EAAA,IAAI,oBAAA,GAA4C,IAAA;AAChD,EAAA,IAAI,SAAA,GAAY,KAAA;AAChB,EAAA,IAAI,SAAA,GAAY,KAAA;AAEhB,EAAA,MAAM,SAAA,GAAY,CAAC,MAAA,KAAgC;AACjD,IAAA,aAAA,GAAgB,MAAA;AAChB,IAAA,IAAI,CAAC,WAAA,EAAa;AAChB,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,IAAA,EAAK;AAAA,IACP;AAAA,EACF,CAAA;AAOA,EAAA,IAAI,aAAA,GAAgB,CAAC,CAAC,SAAA,CAAU,UAAA;AAChC,EAAA,MAAM,qBAAqB,MAAY;AACrC,IAAA,IAAI,CAAC,4BAA4B,SAAA,EAAW;AAC5C,IAAA,IAAI,CAAC,aAAA,EAAe;AAClB,MAAA,aAAA,GAAgB,IAAA;AAChB,MAAA;AAAA,IACF;AACA,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,QAAA,CAAS,MAAA,EAAO;AAAA,EACvD,CAAA;AACA,EAAA,SAAA,CAAU,gBAAA,CAAiB,oBAAoB,kBAAkB,CAAA;AAEjE,EAAA,MAAM,eAAA,GAAkB,CAAC,YAAA,KAAkD;AACzE,IAAA,MAAM,aAAa,YAAA,CAAa,UAAA;AAChC,IAAA,IAAI,CAAC,UAAA,EAAY;AACjB,IAAA,UAAA,CAAW,gBAAA,CAAiB,eAAe,MAAM;AAI/C,MAAA,IAAI,UAAA,CAAW,KAAA,KAAU,WAAA,IAAe,SAAA,CAAU,UAAA,EAAY;AAC5D,QAAA,SAAA,CAAU,UAAU,CAAA;AAAA,MACtB;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,MAAM,eAAA,GAAkB,CAAC,YAAA,KAAkD;AACzE,IAAA,IAAI,CAAC,aAAA,EAAe;AACpB,IAAA,MAAM,QAAQ,MAAY;AACxB,MAAA,YAAA,CAAa,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IACtC,CAAA;AACA,IAAA,MAAM,OAAA,GAAU,MAAY,KAAA,EAAM;AAClC,IAAA,MAAM,eAAe,MAAY;AAC/B,MAAA,IAAI,OAAO,QAAA,KAAa,WAAA,IAAe,QAAA,CAAS,oBAAoB,SAAA,EAAW;AAC7E,QAAA,KAAA,EAAM;AAAA,MACR;AAAA,IACF,CAAA;AACA,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAC3E,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,YAAY,CAAA;AAAA,IAC5D;AACA,IAAA,oBAAA,GAAuB,MAAM;AAC3B,MAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAC9E,MAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,QAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,YAAY,CAAA;AAAA,MAC/D;AAAA,IACF,CAAA;AAAA,EACF,CAAA;AAIA,EAAA,MAAM,sBAAsB,QAAA,GAAW,SAAA,CAAU,QAAA,CAAS,KAAK,IAAI,SAAA,CAAU,KAAA;AAE7E,EAAA,mBAAA,CACG,IAAA,CAAK,CAAC,YAAA,KAAiB;AACtB,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,IAAI,YAAA,CAAa,OAAA,EAAS,SAAA,CAAU,YAAA,CAAa,OAAO,CAAA;AACxD,IAAA,YAAA,CAAa,gBAAA,CAAiB,aAAA,EAAe,MAAM,eAAA,CAAgB,YAAY,CAAC,CAAA;AAChF,IAAA,IAAI,iBAAiB,CAAA,EAAG;AACtB,MAAA,MAAA,GAAS,YAAY,MAAM;AACzB,QAAA,YAAA,CAAa,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM;AAAA,QAElC,CAAC,CAAA;AAAA,MACH,GAAG,cAAc,CAAA;AAAA,IACnB;AACA,IAAA,eAAA,CAAgB,YAAY,CAAA;AAAA,EAC9B,CAAC,CAAA,CACA,KAAA,CAAM,MAAM;AAAA,EAEb,CAAC,CAAA;AAEH,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA,GAAc;AACZ,MAAA,IAAI,aAAA,EAAe,aAAA,CAAc,WAAA,CAAY,oBAAoB,CAAA;AAAA,IACnE,CAAA;AAAA,IACA,OAAA,GAAU;AACR,MAAA,SAAA,GAAY,IAAA;AACZ,MAAA,IAAI,MAAA,KAAW,IAAA,EAAM,aAAA,CAAc,MAAM,CAAA;AACzC,MAAA,oBAAA,IAAuB;AACvB,MAAA,SAAA,CAAU,mBAAA,CAAoB,oBAAoB,kBAAkB,CAAA;AACpE,MAAA,SAAA,CAAU,KAAA,EAAM;AAAA,IAClB;AAAA,GACF;AACF;AC/LO,SAAS,YAAA,CAAa,OAAA,GAA6B,EAAC,EAGzD;AACA,EAAA,MAAM,EAAE,KAAA,EAAO,cAAA,EAAgB,wBAAA,EAA0B,UAAS,GAAI,OAAA;AACtE,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,SAAS,KAAK,CAAA;AACpD,EAAA,MAAM,UAAA,GAAa,OAA0B,IAAI,CAAA;AAEjD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,UAAU,gBAAA,CAAiB;AAAA,MAC/B,KAAA;AAAA,MACA,cAAA;AAAA,MACA,wBAAA;AAAA,MACA;AAAA,KACD,CAAA;AACD,IAAA,UAAA,CAAW,OAAA,GAAU,OAAA;AACrB,IAAA,cAAA,CAAe,OAAA,CAAQ,QAAA,EAAS,CAAE,WAAW,CAAA;AAC7C,IAAA,MAAM,WAAA,GAAc,QAAQ,SAAA,CAAU,CAAC,UAAU,cAAA,CAAe,KAAA,CAAM,WAAW,CAAC,CAAA;AAClF,IAAA,OAAO,MAAM;AACX,MAAA,WAAA,EAAY;AACZ,MAAA,OAAA,CAAQ,OAAA,EAAQ;AAChB,MAAA,UAAA,CAAW,OAAA,GAAU,IAAA;AAAA,IACvB,CAAA;AAAA,EACF,GAAG,CAAC,KAAA,EAAO,cAAA,EAAgB,wBAAA,EAA0B,QAAQ,CAAC,CAAA;AAE9D,EAAA,OAAO;AAAA,IACL,WAAA;AAAA,IACA,WAAA,EAAa,MAAM,UAAA,CAAW,OAAA,EAAS,WAAA;AAAY,GACrD;AACF;AAUA,IAAM,cAAA,GAAkD;AAAA,EACtD,KAAA,EAAO,uBAAA;AAAA,EACP,IAAA,EAAM,mCAAA;AAAA,EACN,MAAA,EAAQ,QAAA;AAAA,EACR,OAAA,EAAS,OAAA;AAAA,EACT,KAAA,EAAO;AACT,CAAA;AAqBO,SAAS,eAAA,CAAgB;AAAA,EAC9B,WAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,EAAyB;AACvB,EAAA,IAAI,CAAC,aAAa,OAAO,IAAA;AACzB,EAAA,MAAM,CAAA,GAAI,EAAE,GAAG,cAAA,EAAgB,GAAG,MAAA,EAAO;AACzC,EAAA,MAAM,OAAA,GAAU,MAAM,SAAA,IAAY;AAClC,EAAA,uBACE,IAAA,CAAC,SAAI,IAAA,EAAK,QAAA,EAAS,aAAU,QAAA,EAAS,aAAA,EAAY,qBAAoB,SAAA,EACpE,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,GAAA,EAAA,EAAE,aAAA,EAAY,kBAAA,EAAoB,QAAA,EAAA,CAAA,CAAE,KAAA,EAAM,CAAA;AAAA,IAC1C,CAAA,CAAE,uBAAO,GAAA,CAAC,GAAA,EAAA,EAAE,eAAY,iBAAA,EAAmB,QAAA,EAAA,CAAA,CAAE,MAAK,CAAA,GAAO,IAAA;AAAA,oBAC1D,GAAA,CAAC,YAAO,IAAA,EAAK,QAAA,EAAS,eAAY,oBAAA,EAAqB,OAAA,EAAS,QAAA,EAC7D,QAAA,EAAA,CAAA,CAAE,MAAA,EACL,CAAA;AAAA,oBACA,GAAA,CAAC,YAAO,IAAA,EAAK,QAAA,EAAS,eAAY,oBAAA,EAAqB,OAAA,EAAS,OAAA,EAC7D,QAAA,EAAA,CAAA,CAAE,OAAA,EACL,CAAA;AAAA,oBACA,GAAA;AAAA,MAAC,QAAA;AAAA,MAAA;AAAA,QACC,IAAA,EAAK,QAAA;AAAA,QACL,aAAA,EAAY,kBAAA;AAAA,QACZ,cAAY,CAAA,CAAE,KAAA;AAAA,QACd,OAAA,EAAS,OAAA;AAAA,QAER,QAAA,EAAA;AAAA;AAAA;AACH,GAAA,EACF,CAAA;AAEJ","file":"preact.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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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","/** @jsxImportSource preact */\n\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * Preact binding for {@link createPwaUpdater} — API-identical to the React\n * adapter, for the Stack B (Bun/Hono/Preact/Vite) apps. Returns whether an\n * update is ready plus the `applyUpdate` action; the controller is created\n * once and 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 for Preact.\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/options-key.ts","../src/preact.tsx"],"names":[],"mappings":"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","file":"preact.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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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 type { PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * A stable identity for an options object, for an adapter's effect dependency.\n *\n * F054.7 — WHY THIS EXISTS INSTEAD OF A DEPENDENCY LIST. Both adapters used to\n * destructure four named options and list those same four as their effect deps.\n * That is a second copy of the core's option list, and it had already drifted:\n * `register` and `updateOnFocus` were never forwarded, so a consumer writing\n * `register: false` got a service-worker registration anyway, with no error and\n * no warning. fd-sundhed found it by reading our shipped dist, which is not a\n * detector anyone should need.\n *\n * Deriving the key from the options the CALLER passed means a new core option\n * reaches both adapters with neither adapter edited. The list was the defect —\n * not the two names missing from it.\n *\n * Keys are SORTED so `{a, b}` and `{b, a}` are the same identity, and\n * `undefined` values are dropped so an explicitly-omitted option and an absent\n * one do not look like different configurations.\n */\nexport function optionsKey(options: PwaUpdaterOptions): string {\n return JSON.stringify(\n Object.entries(options)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n}\n","/** @jsxImportSource preact */\n\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\nimport { optionsKey } from \"./options-key.js\";\n\n/**\n * Preact binding for {@link createPwaUpdater} — API-identical to the React\n * adapter, for the Stack B (Bun/Hono/Preact/Vite) apps. Returns whether an\n * update is ready plus the `applyUpdate` action; the controller is created\n * once and torn down on unmount.\n */\nexport function usePwaUpdate(options: PwaUpdaterOptions = {}): {\n updateReady: boolean;\n applyUpdate: () => void;\n} {\n // The effect's identity comes from the options the CALLER passed, so a new\n // core option needs no edit here. eslint's exhaustive-deps cannot see\n // through `key`; that is the trade, and it is the right way round.\n const key = optionsKey(options);\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n // THE WHOLE OBJECT, not a hand-picked subset. See options-key.ts: the\n // subset is what silently dropped `register` and `updateOnFocus`.\n const updater = createPwaUpdater(options);\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 // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\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 for Preact.\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.cjs
CHANGED
|
@@ -128,17 +128,19 @@ function createPwaUpdater(options = {}) {
|
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
// src/options-key.ts
|
|
133
|
+
function optionsKey(options) {
|
|
134
|
+
return JSON.stringify(
|
|
135
|
+
Object.entries(options).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
131
138
|
function usePwaUpdate(options = {}) {
|
|
132
|
-
const
|
|
139
|
+
const key = optionsKey(options);
|
|
133
140
|
const [updateReady, setUpdateReady] = react.useState(false);
|
|
134
141
|
const updaterRef = react.useRef(null);
|
|
135
142
|
react.useEffect(() => {
|
|
136
|
-
const updater = createPwaUpdater(
|
|
137
|
-
swUrl,
|
|
138
|
-
pollIntervalMs,
|
|
139
|
-
reloadOnControllerChange,
|
|
140
|
-
disabled
|
|
141
|
-
});
|
|
143
|
+
const updater = createPwaUpdater(options);
|
|
142
144
|
updaterRef.current = updater;
|
|
143
145
|
setUpdateReady(updater.getState().updateReady);
|
|
144
146
|
const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));
|
|
@@ -147,7 +149,7 @@ function usePwaUpdate(options = {}) {
|
|
|
147
149
|
updater.destroy();
|
|
148
150
|
updaterRef.current = null;
|
|
149
151
|
};
|
|
150
|
-
}, [
|
|
152
|
+
}, [key]);
|
|
151
153
|
return {
|
|
152
154
|
updateReady,
|
|
153
155
|
applyUpdate: () => updaterRef.current?.applyUpdate()
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/options-key.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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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 type { PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * A stable identity for an options object, for an adapter's effect dependency.\n *\n * F054.7 — WHY THIS EXISTS INSTEAD OF A DEPENDENCY LIST. Both adapters used to\n * destructure four named options and list those same four as their effect deps.\n * That is a second copy of the core's option list, and it had already drifted:\n * `register` and `updateOnFocus` were never forwarded, so a consumer writing\n * `register: false` got a service-worker registration anyway, with no error and\n * no warning. fd-sundhed found it by reading our shipped dist, which is not a\n * detector anyone should need.\n *\n * Deriving the key from the options the CALLER passed means a new core option\n * reaches both adapters with neither adapter edited. The list was the defect —\n * not the two names missing from it.\n *\n * Keys are SORTED so `{a, b}` and `{b, a}` are the same identity, and\n * `undefined` values are dropped so an explicitly-omitted option and an absent\n * one do not look like different configurations.\n */\nexport function optionsKey(options: PwaUpdaterOptions): string {\n return JSON.stringify(\n Object.entries(options)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\nimport { optionsKey } from \"./options-key.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 // The effect's identity comes from the options the CALLER passed, so a new\n // core option needs no edit here. eslint's exhaustive-deps cannot see\n // through `key`; that is the trade, and it is the right way round.\n const key = optionsKey(options);\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n // THE WHOLE OBJECT, not a hand-picked subset. See options-key.ts: the\n // subset is what silently dropped `register` and `updateOnFocus`.\n const updater = createPwaUpdater(options);\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 // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\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
CHANGED
|
@@ -126,17 +126,19 @@ function createPwaUpdater(options = {}) {
|
|
|
126
126
|
}
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
// src/options-key.ts
|
|
131
|
+
function optionsKey(options) {
|
|
132
|
+
return JSON.stringify(
|
|
133
|
+
Object.entries(options).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
129
136
|
function usePwaUpdate(options = {}) {
|
|
130
|
-
const
|
|
137
|
+
const key = optionsKey(options);
|
|
131
138
|
const [updateReady, setUpdateReady] = useState(false);
|
|
132
139
|
const updaterRef = useRef(null);
|
|
133
140
|
useEffect(() => {
|
|
134
|
-
const updater = createPwaUpdater(
|
|
135
|
-
swUrl,
|
|
136
|
-
pollIntervalMs,
|
|
137
|
-
reloadOnControllerChange,
|
|
138
|
-
disabled
|
|
139
|
-
});
|
|
141
|
+
const updater = createPwaUpdater(options);
|
|
140
142
|
updaterRef.current = updater;
|
|
141
143
|
setUpdateReady(updater.getState().updateReady);
|
|
142
144
|
const unsubscribe = updater.subscribe((state) => setUpdateReady(state.updateReady));
|
|
@@ -145,7 +147,7 @@ function usePwaUpdate(options = {}) {
|
|
|
145
147
|
updater.destroy();
|
|
146
148
|
updaterRef.current = null;
|
|
147
149
|
};
|
|
148
|
-
}, [
|
|
150
|
+
}, [key]);
|
|
149
151
|
return {
|
|
150
152
|
updateReady,
|
|
151
153
|
applyUpdate: () => updaterRef.current?.applyUpdate()
|
package/dist/react.js.map
CHANGED
|
@@ -1 +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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/index.ts","../src/options-key.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 // With `clientsClaim: true` a brand-new worker claims a previously\n // uncontrolled page and fires `controllerchange` on the FIRST install — that\n // is not an update, so reloading there would yank a first-time visitor out of\n // whatever they are doing. Only a `controllerchange` that REPLACES an existing\n // controller is a real takeover worth reloading for.\n let hadController = !!container.controller;\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\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 type { PwaUpdaterOptions } from \"./index.js\";\n\n/**\n * A stable identity for an options object, for an adapter's effect dependency.\n *\n * F054.7 — WHY THIS EXISTS INSTEAD OF A DEPENDENCY LIST. Both adapters used to\n * destructure four named options and list those same four as their effect deps.\n * That is a second copy of the core's option list, and it had already drifted:\n * `register` and `updateOnFocus` were never forwarded, so a consumer writing\n * `register: false` got a service-worker registration anyway, with no error and\n * no warning. fd-sundhed found it by reading our shipped dist, which is not a\n * detector anyone should need.\n *\n * Deriving the key from the options the CALLER passed means a new core option\n * reaches both adapters with neither adapter edited. The list was the defect —\n * not the two names missing from it.\n *\n * Keys are SORTED so `{a, b}` and `{b, a}` are the same identity, and\n * `undefined` values are dropped so an explicitly-omitted option and an absent\n * one do not look like different configurations.\n */\nexport function optionsKey(options: PwaUpdaterOptions): string {\n return JSON.stringify(\n Object.entries(options)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { createPwaUpdater, type PwaUpdater, type PwaUpdaterOptions } from \"./index.js\";\nimport { optionsKey } from \"./options-key.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 // The effect's identity comes from the options the CALLER passed, so a new\n // core option needs no edit here. eslint's exhaustive-deps cannot see\n // through `key`; that is the trade, and it is the right way round.\n const key = optionsKey(options);\n const [updateReady, setUpdateReady] = useState(false);\n const updaterRef = useRef<PwaUpdater | null>(null);\n\n useEffect(() => {\n // THE WHOLE OBJECT, not a hand-picked subset. See options-key.ts: the\n // subset is what silently dropped `register` and `updateOnFocus`.\n const updater = createPwaUpdater(options);\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 // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broberg/pwa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Fleet PWA primitive: a framework-agnostic service-worker update-detection controller (new-version-available lifecycle) + a React hook and unstyled banner skeleton + a service-worker skip-waiting helper. Zero runtime deps; ship-dark.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,16 +37,17 @@
|
|
|
37
37
|
"types": "./dist/preact.d.ts",
|
|
38
38
|
"import": "./dist/preact.js",
|
|
39
39
|
"require": "./dist/preact.cjs"
|
|
40
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
|
-
"build": "tsup",
|
|
44
|
+
"build": "rm -rf dist && tsup && node verify-exports.mjs",
|
|
44
45
|
"test": "vitest run",
|
|
45
46
|
"typecheck": "tsc --noEmit"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
49
|
+
"preact": ">=10.0.0",
|
|
50
|
+
"react": ">=18.0.0"
|
|
50
51
|
},
|
|
51
52
|
"peerDependenciesMeta": {
|
|
52
53
|
"react": {
|