@broberg/pwa 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/bin.cjs +5 -2
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +5 -3
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-JWKQNVNE.js → chunk-33GKPCTR.js} +4 -3
- package/dist/chunk-33GKPCTR.js.map +1 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -26
- package/dist/index.d.ts +17 -26
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.cjs +1 -1
- package/dist/manifest.cjs.map +1 -1
- package/dist/manifest.js +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/preact.cjs +6 -2
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.js +6 -2
- package/dist/preact.js.map +1 -1
- package/dist/raster.cjs +2 -1
- package/dist/raster.cjs.map +1 -1
- package/dist/raster.d.cts +7 -0
- package/dist/raster.d.ts +7 -0
- package/dist/raster.js +1 -1
- package/dist/react.cjs +6 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +6 -2
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-JWKQNVNE.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,6 +8,25 @@ as one small, framework- and bundler-agnostic package instead of a fifth copy.
|
|
|
8
8
|
npm i @broberg/pwa
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## 0.7.0 — four fixes reported by the fleet (F021.11)
|
|
12
|
+
|
|
13
|
+
- **No banner on a first visit.** A brand-new worker is briefly `registration.waiting` on EVERY install (the spec's
|
|
14
|
+
Install algorithm), and the updater checked at exactly that moment — so a first visit showed «Ny version klar»
|
|
15
|
+
(vn-leker). An update now needs an ACTIVE worker to replace. A real update always has one, also on a page with
|
|
16
|
+
no controller, so no genuine banner is lost.
|
|
17
|
+
- **No English body line by default.** `labels.body` is empty unless you set it; the title says it. (A banner with
|
|
18
|
+
translated title/buttons used to show «Reload to get the latest version.» underneath — jam.)
|
|
19
|
+
- **The iPhone home-screen icon.** `rasterizeIconSet()` now also returns `rootFiles`: the 180 PNG at
|
|
20
|
+
`/apple-touch-icon.png` and `/apple-touch-icon-precomposed.png`. iOS asks the web ROOT for these on its own;
|
|
21
|
+
without them it fell back to a letter. Write them to your web root (the `pwa-icons` bin does, into the parent of
|
|
22
|
+
`--out` or `--root`). `pwaMetaTags` now puts `sizes="180x180"` on the apple-touch link.
|
|
23
|
+
- **Only PNG in the manifest.** Put **`rasterizeIconSet().icons` — and nothing else — in the manifest `icons[]`.**
|
|
24
|
+
The SVG set from `buildIconSet()` is source material for the rasterizer; SVG entries in `icons[]` gave a letter on
|
|
25
|
+
iOS 26 (jam), and every fleet app that works has PNG only. Pass `rasterizeIconSet().appleTouchIcon` to
|
|
26
|
+
`pwaMetaTags({ appleTouchIcon })`.
|
|
27
|
+
- **Use `pwaMetaTags` for the head**, not only your framework's metadata: it writes `apple-mobile-web-app-capable`,
|
|
28
|
+
which Next 16's `appleWebApp.capable` does not (it writes `mobile-web-app-capable` only).
|
|
29
|
+
|
|
11
30
|
## 0.6.0 — a classic service worker can use it too: `pwa-sw.global.js` (F021.10)
|
|
12
31
|
|
|
13
32
|
A service worker in `public/` is a static file and cannot `import` — so
|
package/dist/bin.cjs
CHANGED
|
@@ -78,11 +78,12 @@ async function rasterizeIconSet(set, opts = {}) {
|
|
|
78
78
|
});
|
|
79
79
|
const icons = set.icons.map((i) => ({ ...i, src: toPng(i.src), type: "image/png" }));
|
|
80
80
|
const apple = files.find((f) => f.size === 180 && f.purpose === "any");
|
|
81
|
-
|
|
81
|
+
const rootFiles = apple ? ["/apple-touch-icon.png", "/apple-touch-icon-precomposed.png"].map((path) => ({ ...apple, path })) : [];
|
|
82
|
+
return { files, icons, appleTouchIcon: apple?.path, rootFiles };
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// src/cli.ts
|
|
85
|
-
var USAGE = "usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons]";
|
|
86
|
+
var USAGE = "usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons] [--root <web root, default: the parent of --out>]";
|
|
86
87
|
async function main(argv) {
|
|
87
88
|
const arg = (name) => {
|
|
88
89
|
const i = argv.indexOf(`--${name}`);
|
|
@@ -113,6 +114,8 @@ ${USAGE}`);
|
|
|
113
114
|
fs.mkdirSync(out, { recursive: true });
|
|
114
115
|
for (const f of set.files) fs.writeFileSync(path.join(out, path.basename(f.path)), f.content);
|
|
115
116
|
for (const f of raster.files) fs.writeFileSync(path.join(out, path.basename(f.path)), f.content);
|
|
117
|
+
const root = arg("root") ?? path.dirname(out);
|
|
118
|
+
for (const f of raster.rootFiles) fs.writeFileSync(path.join(root, path.basename(f.path)), f.content);
|
|
116
119
|
console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));
|
|
117
120
|
return 0;
|
|
118
121
|
}
|
package/dist/bin.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/manifest.ts","../src/raster.ts","../src/cli.ts","../src/bin.ts"],"names":["readFileSync","mkdirSync","writeFileSync","join","basename"],"mappings":"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","file":"bin.cjs","sourcesContent":["/**\n * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the\n * root export). A dependency-free manifest/meta factory + an SVG-based icon\n * generator, so a repo stops hand-rolling `app/manifest.ts`, a\n * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.\n *\n * Everything here is PURE string/object generation — no filesystem, no\n * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,\n * or the browser, and every piece is offline-unit-testable. Icons are emitted\n * as self-contained SVG documents. iOS does NOT use an SVG apple-touch-icon —\n * rasterise with `@broberg/pwa/raster` / the `pwa-icons` bin (F021.9).\n */\n\n// ---------------------------------------------------------------------------\n// Manifest\n// ---------------------------------------------------------------------------\n\nexport type Display = \"standalone\" | \"fullscreen\" | \"minimal-ui\" | \"browser\";\n\nexport interface ManifestIcon {\n src: string;\n sizes: string;\n type: string;\n /** \"any\" | \"maskable\" | \"monochrome\" (space-separated allowed). */\n purpose?: string;\n}\n\nexport interface WebAppManifest {\n name: string;\n short_name: string;\n start_url: string;\n scope: string;\n display: Display;\n theme_color: string;\n background_color: string;\n icons: ManifestIcon[];\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n [key: string]: unknown;\n}\n\nexport interface DefineManifestOptions {\n /** App name (required). */\n name: string;\n /** Home-screen label. Default: `name`. */\n shortName?: string;\n /** Default `/`. */\n startUrl?: string;\n /** Default `/`. */\n scope?: string;\n /** Default `standalone`. */\n display?: Display;\n /** Address-bar / task-switcher colour. Default `#ffffff`. */\n themeColor?: string;\n /** Splash background. Default `#ffffff`. */\n backgroundColor?: string;\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */\n icons?: ManifestIcon[];\n /** Escape hatch: extra top-level manifest members, merged last. */\n extra?: Record<string, unknown>;\n}\n\n/**\n * Build a spec-valid Web App Manifest object. Required members are defaulted so\n * a one-liner produces an installable manifest; every default is overridable\n * and `extra` merges last for anything not modelled (shortcuts, screenshots…).\n */\nexport function defineManifest(opts: DefineManifestOptions): WebAppManifest {\n if (!opts?.name) throw new Error(\"defineManifest: `name` is required\");\n const manifest: WebAppManifest = {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n start_url: opts.startUrl ?? \"/\",\n scope: opts.scope ?? \"/\",\n display: opts.display ?? \"standalone\",\n theme_color: opts.themeColor ?? \"#ffffff\",\n background_color: opts.backgroundColor ?? \"#ffffff\",\n icons: opts.icons ?? [],\n };\n if (opts.description) manifest.description = opts.description;\n if (opts.lang) manifest.lang = opts.lang;\n if (opts.orientation) manifest.orientation = opts.orientation;\n if (opts.categories) manifest.categories = opts.categories;\n if (opts.id) manifest.id = opts.id;\n return { ...manifest, ...(opts.extra ?? {}) };\n}\n\n/** JSON-stringify a manifest for a `manifest.webmanifest` file. */\nexport function serializeManifest(\n manifest: WebAppManifest,\n opts: { pretty?: boolean } = {},\n): string {\n return JSON.stringify(manifest, null, opts.pretty === false ? undefined : 2);\n}\n\n// ---------------------------------------------------------------------------\n// Icon generation (SVG, zero-dep)\n// ---------------------------------------------------------------------------\n\nexport interface GeneratedIcon {\n /** URL path, e.g. `/icons/icon-192.svg`. */\n path: string;\n /** The SVG document. */\n content: string;\n type: \"image/svg+xml\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface IconSet {\n files: GeneratedIcon[];\n /** Ready to drop into `defineManifest({ icons })`. */\n icons: ManifestIcon[];\n}\n\nexport interface IconSetOptions {\n /**\n * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a\n * data-URI `<image>` so it scales cleanly at any size regardless of its\n * internal coordinate system. Mutually exclusive with `monogram`.\n */\n svg?: string;\n /** Fallback artwork: 1–3 letters rendered centred (e.g. \"AK\"). */\n monogram?: string;\n /** Foreground (monogram text) colour. Default `#ffffff`. */\n color?: string;\n /** Background fill. Default `#111827`. */\n background?: string;\n /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */\n sizes?: number[];\n /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */\n maskable?: boolean;\n /** URL prefix for every icon `src`. Default `/icons`. */\n basePath?: string;\n}\n\nconst DEFAULT_SIZES = [180, 192, 512];\n\nfunction escapeXml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) =>\n c === \"<\" ? \"<\" : c === \">\" ? \">\" : c === \"&\" ? \"&\" : c === '\"' ? \""\" : \"'\",\n );\n}\n\n/** UTF-8-safe base64 for the data-URI embed (works in Node/Bun/browser). */\nfunction toBase64(s: string): string {\n // Reached via globalThis so the package needs no @types/node (browser-safe).\n const g = globalThis as unknown as {\n Buffer?: { from(input: string, enc: string): { toString(enc: string): string } };\n btoa?: (data: string) => string;\n };\n if (typeof g.Buffer !== \"undefined\") return g.Buffer.from(s, \"utf-8\").toString(\"base64\");\n const bytes = new TextEncoder().encode(s);\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return g.btoa!(bin);\n}\n\n/** Render one square SVG icon document at `size`. `inset` = safe-zone fraction (maskable). */\nfunction renderIcon(size: number, inset: number, opts: IconSetOptions): string {\n const bg = opts.background ?? \"#111827\";\n const fg = opts.color ?? \"#ffffff\";\n const pad = Math.round(size * inset);\n const inner = size - pad * 2;\n let art: string;\n if (opts.svg) {\n const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;\n art = `<image x=\"${pad}\" y=\"${pad}\" width=\"${inner}\" height=\"${inner}\" href=\"${href}\" preserveAspectRatio=\"xMidYMid meet\"/>`;\n } else {\n const text = escapeXml((opts.monogram ?? \"\").slice(0, 3).toUpperCase());\n // Font sized to the safe inner box; a touch smaller for breathing room.\n const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));\n art =\n `<text x=\"50%\" y=\"50%\" fill=\"${fg}\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto, sans-serif\" ` +\n `font-weight=\"700\" font-size=\"${fontSize}\" text-anchor=\"middle\" dominant-baseline=\"central\">${text}</text>`;\n }\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\">` +\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${bg}\"/>` +\n art +\n `</svg>`\n );\n}\n\n/**\n * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,\n * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch\n * (180), 192, 512 by default plus a padded maskable-512.\n */\nexport function buildIconSet(opts: IconSetOptions): IconSet {\n if (!opts?.svg && !opts?.monogram) {\n throw new Error(\"buildIconSet: provide either `svg` or `monogram`\");\n }\n const base = (opts.basePath ?? \"/icons\").replace(/\\/$/, \"\");\n const sizes = opts.sizes ?? DEFAULT_SIZES;\n const files: GeneratedIcon[] = [];\n const icons: ManifestIcon[] = [];\n\n for (const size of sizes) {\n const path = `${base}/icon-${size}.svg`;\n files.push({ path, content: renderIcon(size, 0, opts), type: \"image/svg+xml\", size, purpose: \"any\" });\n // apple-touch-icon is referenced via a <link> (see pwaMetaTags), not the\n // manifest, so only real manifest sizes go into icons[].\n if (size !== 180) {\n icons.push({ src: path, sizes: `${size}x${size}`, type: \"image/svg+xml\", purpose: \"any\" });\n }\n }\n\n if (opts.maskable !== false) {\n // Maskable safe-zone: keep content within the central 80% (10% inset).\n const path = `${base}/icon-maskable-512.svg`;\n files.push({ path, content: renderIcon(512, 0.1, opts), type: \"image/svg+xml\", size: 512, purpose: \"maskable\" });\n icons.push({ src: path, sizes: \"512x512\", type: \"image/svg+xml\", purpose: \"maskable\" });\n }\n\n return { files, icons };\n}\n\n// ---------------------------------------------------------------------------\n// Head meta tags\n// ---------------------------------------------------------------------------\n\nexport interface MetaTag {\n tag: \"meta\" | \"link\";\n attrs: Record<string, string>;\n}\n\nexport interface MetaTagsOptions {\n /** `<meta name=\"theme-color\">`. Default `#ffffff`. */\n themeColor?: string;\n /** apple-touch-icon href. Default `/icons/icon-180.svg`. */\n appleTouchIcon?: string;\n /** Manifest href for the `<link rel=\"manifest\">`. Default `/manifest.webmanifest`. */\n manifest?: string;\n /** apple-mobile-web-app-title. Default: omitted. */\n title?: string;\n /** apple-mobile-web-app-status-bar-style. Default `default`. */\n statusBarStyle?: \"default\" | \"black\" | \"black-translucent\";\n /** Emit the `*-web-app-capable` tags. Default `true`. */\n capable?: boolean;\n}\n\n/**\n * The head tags a PWA needs beyond the manifest link — returned as typed\n * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX\n * head, or plain HTML. No framework assumption.\n */\nexport function pwaMetaTags(opts: MetaTagsOptions = {}): MetaTag[] {\n const tags: MetaTag[] = [];\n tags.push({ tag: \"link\", attrs: { rel: \"manifest\", href: opts.manifest ?? \"/manifest.webmanifest\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"theme-color\", content: opts.themeColor ?? \"#ffffff\" } });\n tags.push({ tag: \"link\", attrs: { rel: \"apple-touch-icon\", href: opts.appleTouchIcon ?? \"/icons/icon-180.svg\" } });\n if (opts.capable !== false) {\n tags.push({ tag: \"meta\", attrs: { name: \"mobile-web-app-capable\", content: \"yes\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-capable\", content: \"yes\" } });\n tags.push({\n tag: \"meta\",\n attrs: { name: \"apple-mobile-web-app-status-bar-style\", content: opts.statusBarStyle ?? \"default\" },\n });\n }\n if (opts.title) {\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-title\", content: opts.title } });\n }\n return tags;\n}\n","/**\n * @broberg/pwa/raster — F021.9: PNG icons, because iOS does not use an SVG\n * apple-touch-icon. «Add to Home Screen» on an iPhone with only SVG icons shows\n * a screenshot of the page instead of the logo (measured by sales, 10/10).\n *\n * Kept OUT of the core entries so `@broberg/pwa` and `/manifest` stay\n * dependency-free: the renderer, @resvg/resvg-js (prebuilt binaries, no system\n * libraries), is an optional peer loaded only when this runs.\n */\nimport type { IconSet, ManifestIcon } from \"./manifest.js\";\n\nexport interface RasterIcon {\n /** URL path, e.g. `/icons/icon-180.png`. */\n path: string;\n content: Uint8Array;\n type: \"image/png\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface RasterIconSet {\n files: RasterIcon[];\n /** Manifest `icons[]` pointing at the PNGs. */\n icons: ManifestIcon[];\n /** Href for `<link rel=\"apple-touch-icon\">` — pass it to `pwaMetaTags({ appleTouchIcon })`. Undefined when 180 was not generated. */\n appleTouchIcon?: string;\n}\n\nexport interface RasterizeOptions {\n /** Extra font files for monogram text (system fonts are loaded too). */\n fontFiles?: string[];\n}\n\nexport const RESVG_MISSING =\n \"rasterizeIconSet needs @resvg/resvg-js — install it as a dev dependency: npm i -D @resvg/resvg-js\";\n\nconst toPng = (p: string) => p.replace(/\\.svg$/, \".png\");\n\n/** Rasterise every SVG from `buildIconSet` to a PNG of the same size. */\nexport async function rasterizeIconSet(set: IconSet, opts: RasterizeOptions = {}): Promise<RasterIconSet> {\n let Resvg: typeof import(\"@resvg/resvg-js\").Resvg;\n try {\n ({ Resvg } = await import(\"@resvg/resvg-js\"));\n } catch {\n throw new Error(RESVG_MISSING);\n }\n const files: RasterIcon[] = set.files.map((f) => {\n const png = new Resvg(f.content, {\n fitTo: { mode: \"width\", value: f.size },\n font: { loadSystemFonts: true, fontFiles: opts.fontFiles ?? [] },\n })\n .render()\n .asPng();\n return { path: toPng(f.path), content: new Uint8Array(png), type: \"image/png\", size: f.size, purpose: f.purpose };\n });\n const icons = set.icons.map((i) => ({ ...i, src: toPng(i.src), type: \"image/png\" }));\n const apple = files.find((f) => f.size === 180 && f.purpose === \"any\");\n return { files, icons, appleTouchIcon: apple?.path };\n}\n","/// <reference types=\"node\" />\n// F021.9 — `pwa-icons`: SVG + PNG icon set in one command, so no app keeps its\n// own sharp script. Prints the manifest icons[] and the apple-touch href.\nimport { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { buildIconSet } from \"./manifest.js\";\nimport { rasterizeIconSet } from \"./raster.js\";\n\nconst USAGE =\n \"usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons]\";\n\nexport async function main(argv: string[]): Promise<number> {\n const arg = (name: string) => {\n const i = argv.indexOf(`--${name}`);\n return i >= 0 ? argv[i + 1] : undefined;\n };\n const svgFile = arg(\"svg\");\n const monogram = arg(\"monogram\");\n if (!svgFile && !monogram) {\n console.error(`pwa-icons: give --svg <file> or --monogram <text>\\n${USAGE}`);\n return 1;\n }\n const out = arg(\"out\") ?? \"public/icons\";\n const set = buildIconSet({\n svg: svgFile ? readFileSync(svgFile, \"utf8\") : undefined,\n monogram,\n background: arg(\"background\"),\n color: arg(\"color\"),\n basePath: arg(\"base-path\") ?? \"/icons\",\n });\n let raster;\n try {\n raster = await rasterizeIconSet(set);\n } catch (e) {\n console.error(`pwa-icons: ${(e as Error).message}`);\n return 1;\n }\n mkdirSync(out, { recursive: true });\n for (const f of set.files) writeFileSync(join(out, basename(f.path)), f.content);\n for (const f of raster.files) writeFileSync(join(out, basename(f.path)), f.content);\n console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));\n return 0;\n}\n\n","#!/usr/bin/env node\n/// <reference types=\"node\" />\n// F021.9 — the `pwa-icons` entry point; the logic lives in cli.ts so tests can call it.\nimport { main } from \"./cli.js\";\n\nmain(process.argv.slice(2)).then((code) => process.exit(code));\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/manifest.ts","../src/raster.ts","../src/cli.ts","../src/bin.ts"],"names":["readFileSync","mkdirSync","writeFileSync","join","basename","dirname"],"mappings":"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","file":"bin.cjs","sourcesContent":["/**\n * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the\n * root export). A dependency-free manifest/meta factory + an SVG-based icon\n * generator, so a repo stops hand-rolling `app/manifest.ts`, a\n * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.\n *\n * Everything here is PURE string/object generation — no filesystem, no\n * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,\n * or the browser, and every piece is offline-unit-testable. Icons are emitted\n * as self-contained SVG documents. iOS does NOT use an SVG apple-touch-icon —\n * rasterise with `@broberg/pwa/raster` / the `pwa-icons` bin (F021.9).\n */\n\n// ---------------------------------------------------------------------------\n// Manifest\n// ---------------------------------------------------------------------------\n\nexport type Display = \"standalone\" | \"fullscreen\" | \"minimal-ui\" | \"browser\";\n\nexport interface ManifestIcon {\n src: string;\n sizes: string;\n type: string;\n /** \"any\" | \"maskable\" | \"monochrome\" (space-separated allowed). */\n purpose?: string;\n}\n\nexport interface WebAppManifest {\n name: string;\n short_name: string;\n start_url: string;\n scope: string;\n display: Display;\n theme_color: string;\n background_color: string;\n icons: ManifestIcon[];\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n [key: string]: unknown;\n}\n\nexport interface DefineManifestOptions {\n /** App name (required). */\n name: string;\n /** Home-screen label. Default: `name`. */\n shortName?: string;\n /** Default `/`. */\n startUrl?: string;\n /** Default `/`. */\n scope?: string;\n /** Default `standalone`. */\n display?: Display;\n /** Address-bar / task-switcher colour. Default `#ffffff`. */\n themeColor?: string;\n /** Splash background. Default `#ffffff`. */\n backgroundColor?: string;\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */\n icons?: ManifestIcon[];\n /** Escape hatch: extra top-level manifest members, merged last. */\n extra?: Record<string, unknown>;\n}\n\n/**\n * Build a spec-valid Web App Manifest object. Required members are defaulted so\n * a one-liner produces an installable manifest; every default is overridable\n * and `extra` merges last for anything not modelled (shortcuts, screenshots…).\n */\nexport function defineManifest(opts: DefineManifestOptions): WebAppManifest {\n if (!opts?.name) throw new Error(\"defineManifest: `name` is required\");\n const manifest: WebAppManifest = {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n start_url: opts.startUrl ?? \"/\",\n scope: opts.scope ?? \"/\",\n display: opts.display ?? \"standalone\",\n theme_color: opts.themeColor ?? \"#ffffff\",\n background_color: opts.backgroundColor ?? \"#ffffff\",\n icons: opts.icons ?? [],\n };\n if (opts.description) manifest.description = opts.description;\n if (opts.lang) manifest.lang = opts.lang;\n if (opts.orientation) manifest.orientation = opts.orientation;\n if (opts.categories) manifest.categories = opts.categories;\n if (opts.id) manifest.id = opts.id;\n return { ...manifest, ...(opts.extra ?? {}) };\n}\n\n/** JSON-stringify a manifest for a `manifest.webmanifest` file. */\nexport function serializeManifest(\n manifest: WebAppManifest,\n opts: { pretty?: boolean } = {},\n): string {\n return JSON.stringify(manifest, null, opts.pretty === false ? undefined : 2);\n}\n\n// ---------------------------------------------------------------------------\n// Icon generation (SVG, zero-dep)\n// ---------------------------------------------------------------------------\n\nexport interface GeneratedIcon {\n /** URL path, e.g. `/icons/icon-192.svg`. */\n path: string;\n /** The SVG document. */\n content: string;\n type: \"image/svg+xml\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface IconSet {\n files: GeneratedIcon[];\n /** Ready to drop into `defineManifest({ icons })`. */\n icons: ManifestIcon[];\n}\n\nexport interface IconSetOptions {\n /**\n * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a\n * data-URI `<image>` so it scales cleanly at any size regardless of its\n * internal coordinate system. Mutually exclusive with `monogram`.\n */\n svg?: string;\n /** Fallback artwork: 1–3 letters rendered centred (e.g. \"AK\"). */\n monogram?: string;\n /** Foreground (monogram text) colour. Default `#ffffff`. */\n color?: string;\n /** Background fill. Default `#111827`. */\n background?: string;\n /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */\n sizes?: number[];\n /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */\n maskable?: boolean;\n /** URL prefix for every icon `src`. Default `/icons`. */\n basePath?: string;\n}\n\nconst DEFAULT_SIZES = [180, 192, 512];\n\nfunction escapeXml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) =>\n c === \"<\" ? \"<\" : c === \">\" ? \">\" : c === \"&\" ? \"&\" : c === '\"' ? \""\" : \"'\",\n );\n}\n\n/** UTF-8-safe base64 for the data-URI embed (works in Node/Bun/browser). */\nfunction toBase64(s: string): string {\n // Reached via globalThis so the package needs no @types/node (browser-safe).\n const g = globalThis as unknown as {\n Buffer?: { from(input: string, enc: string): { toString(enc: string): string } };\n btoa?: (data: string) => string;\n };\n if (typeof g.Buffer !== \"undefined\") return g.Buffer.from(s, \"utf-8\").toString(\"base64\");\n const bytes = new TextEncoder().encode(s);\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return g.btoa!(bin);\n}\n\n/** Render one square SVG icon document at `size`. `inset` = safe-zone fraction (maskable). */\nfunction renderIcon(size: number, inset: number, opts: IconSetOptions): string {\n const bg = opts.background ?? \"#111827\";\n const fg = opts.color ?? \"#ffffff\";\n const pad = Math.round(size * inset);\n const inner = size - pad * 2;\n let art: string;\n if (opts.svg) {\n const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;\n art = `<image x=\"${pad}\" y=\"${pad}\" width=\"${inner}\" height=\"${inner}\" href=\"${href}\" preserveAspectRatio=\"xMidYMid meet\"/>`;\n } else {\n const text = escapeXml((opts.monogram ?? \"\").slice(0, 3).toUpperCase());\n // Font sized to the safe inner box; a touch smaller for breathing room.\n const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));\n art =\n `<text x=\"50%\" y=\"50%\" fill=\"${fg}\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto, sans-serif\" ` +\n `font-weight=\"700\" font-size=\"${fontSize}\" text-anchor=\"middle\" dominant-baseline=\"central\">${text}</text>`;\n }\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\">` +\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${bg}\"/>` +\n art +\n `</svg>`\n );\n}\n\n/**\n * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,\n * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch\n * (180), 192, 512 by default plus a padded maskable-512.\n */\nexport function buildIconSet(opts: IconSetOptions): IconSet {\n if (!opts?.svg && !opts?.monogram) {\n throw new Error(\"buildIconSet: provide either `svg` or `monogram`\");\n }\n const base = (opts.basePath ?? \"/icons\").replace(/\\/$/, \"\");\n const sizes = opts.sizes ?? DEFAULT_SIZES;\n const files: GeneratedIcon[] = [];\n const icons: ManifestIcon[] = [];\n\n for (const size of sizes) {\n const path = `${base}/icon-${size}.svg`;\n files.push({ path, content: renderIcon(size, 0, opts), type: \"image/svg+xml\", size, purpose: \"any\" });\n // apple-touch-icon is referenced via a <link> (see pwaMetaTags), not the\n // manifest, so only real manifest sizes go into icons[].\n if (size !== 180) {\n icons.push({ src: path, sizes: `${size}x${size}`, type: \"image/svg+xml\", purpose: \"any\" });\n }\n }\n\n if (opts.maskable !== false) {\n // Maskable safe-zone: keep content within the central 80% (10% inset).\n const path = `${base}/icon-maskable-512.svg`;\n files.push({ path, content: renderIcon(512, 0.1, opts), type: \"image/svg+xml\", size: 512, purpose: \"maskable\" });\n icons.push({ src: path, sizes: \"512x512\", type: \"image/svg+xml\", purpose: \"maskable\" });\n }\n\n return { files, icons };\n}\n\n// ---------------------------------------------------------------------------\n// Head meta tags\n// ---------------------------------------------------------------------------\n\nexport interface MetaTag {\n tag: \"meta\" | \"link\";\n attrs: Record<string, string>;\n}\n\nexport interface MetaTagsOptions {\n /** `<meta name=\"theme-color\">`. Default `#ffffff`. */\n themeColor?: string;\n /** apple-touch-icon href. Default `/icons/icon-180.svg`. */\n appleTouchIcon?: string;\n /** Manifest href for the `<link rel=\"manifest\">`. Default `/manifest.webmanifest`. */\n manifest?: string;\n /** apple-mobile-web-app-title. Default: omitted. */\n title?: string;\n /** apple-mobile-web-app-status-bar-style. Default `default`. */\n statusBarStyle?: \"default\" | \"black\" | \"black-translucent\";\n /** Emit the `*-web-app-capable` tags. Default `true`. */\n capable?: boolean;\n}\n\n/**\n * The head tags a PWA needs beyond the manifest link — returned as typed\n * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX\n * head, or plain HTML. No framework assumption.\n */\nexport function pwaMetaTags(opts: MetaTagsOptions = {}): MetaTag[] {\n const tags: MetaTag[] = [];\n tags.push({ tag: \"link\", attrs: { rel: \"manifest\", href: opts.manifest ?? \"/manifest.webmanifest\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"theme-color\", content: opts.themeColor ?? \"#ffffff\" } });\n // F021.11 — sizes: the icon is 180; iOS picks by it when several are linked.\n tags.push({ tag: \"link\", attrs: { rel: \"apple-touch-icon\", sizes: \"180x180\", href: opts.appleTouchIcon ?? \"/icons/icon-180.svg\" } });\n if (opts.capable !== false) {\n tags.push({ tag: \"meta\", attrs: { name: \"mobile-web-app-capable\", content: \"yes\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-capable\", content: \"yes\" } });\n tags.push({\n tag: \"meta\",\n attrs: { name: \"apple-mobile-web-app-status-bar-style\", content: opts.statusBarStyle ?? \"default\" },\n });\n }\n if (opts.title) {\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-title\", content: opts.title } });\n }\n return tags;\n}\n","/**\n * @broberg/pwa/raster — F021.9: PNG icons, because iOS does not use an SVG\n * apple-touch-icon. «Add to Home Screen» on an iPhone with only SVG icons shows\n * a screenshot of the page instead of the logo (measured by sales, 10/10).\n *\n * Kept OUT of the core entries so `@broberg/pwa` and `/manifest` stay\n * dependency-free: the renderer, @resvg/resvg-js (prebuilt binaries, no system\n * libraries), is an optional peer loaded only when this runs.\n */\nimport type { IconSet, ManifestIcon } from \"./manifest.js\";\n\nexport interface RasterIcon {\n /** URL path, e.g. `/icons/icon-180.png`. */\n path: string;\n content: Uint8Array;\n type: \"image/png\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface RasterIconSet {\n files: RasterIcon[];\n /** Manifest `icons[]` pointing at the PNGs. */\n icons: ManifestIcon[];\n /** Href for `<link rel=\"apple-touch-icon\">` — pass it to `pwaMetaTags({ appleTouchIcon })`. Undefined when 180 was not generated. */\n appleTouchIcon?: string;\n /**\n * F021.11 — the same 180 PNG at the two ROOT paths iOS asks for on its own\n * (`/apple-touch-icon.png`, `/apple-touch-icon-precomposed.png`). Write them to\n * your web root, NOT next to the other icons. Without them iOS fell back to a\n * letter («J») on a real home screen (jam, measured). Empty when 180 was not generated.\n */\n rootFiles: RasterIcon[];\n}\n\nexport interface RasterizeOptions {\n /** Extra font files for monogram text (system fonts are loaded too). */\n fontFiles?: string[];\n}\n\nexport const RESVG_MISSING =\n \"rasterizeIconSet needs @resvg/resvg-js — install it as a dev dependency: npm i -D @resvg/resvg-js\";\n\nconst toPng = (p: string) => p.replace(/\\.svg$/, \".png\");\n\n/** Rasterise every SVG from `buildIconSet` to a PNG of the same size. */\nexport async function rasterizeIconSet(set: IconSet, opts: RasterizeOptions = {}): Promise<RasterIconSet> {\n let Resvg: typeof import(\"@resvg/resvg-js\").Resvg;\n try {\n ({ Resvg } = await import(\"@resvg/resvg-js\"));\n } catch {\n throw new Error(RESVG_MISSING);\n }\n const files: RasterIcon[] = set.files.map((f) => {\n const png = new Resvg(f.content, {\n fitTo: { mode: \"width\", value: f.size },\n font: { loadSystemFonts: true, fontFiles: opts.fontFiles ?? [] },\n })\n .render()\n .asPng();\n return { path: toPng(f.path), content: new Uint8Array(png), type: \"image/png\", size: f.size, purpose: f.purpose };\n });\n const icons = set.icons.map((i) => ({ ...i, src: toPng(i.src), type: \"image/png\" }));\n const apple = files.find((f) => f.size === 180 && f.purpose === \"any\");\n const rootFiles: RasterIcon[] = apple\n ? [\"/apple-touch-icon.png\", \"/apple-touch-icon-precomposed.png\"].map((path) => ({ ...apple, path }))\n : [];\n return { files, icons, appleTouchIcon: apple?.path, rootFiles };\n}\n","/// <reference types=\"node\" />\n// F021.9 — `pwa-icons`: SVG + PNG icon set in one command, so no app keeps its\n// own sharp script. Prints the manifest icons[] and the apple-touch href.\nimport { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { buildIconSet } from \"./manifest.js\";\nimport { rasterizeIconSet } from \"./raster.js\";\n\nconst USAGE =\n \"usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons] [--root <web root, default: the parent of --out>]\";\n\nexport async function main(argv: string[]): Promise<number> {\n const arg = (name: string) => {\n const i = argv.indexOf(`--${name}`);\n return i >= 0 ? argv[i + 1] : undefined;\n };\n const svgFile = arg(\"svg\");\n const monogram = arg(\"monogram\");\n if (!svgFile && !monogram) {\n console.error(`pwa-icons: give --svg <file> or --monogram <text>\\n${USAGE}`);\n return 1;\n }\n const out = arg(\"out\") ?? \"public/icons\";\n const set = buildIconSet({\n svg: svgFile ? readFileSync(svgFile, \"utf8\") : undefined,\n monogram,\n background: arg(\"background\"),\n color: arg(\"color\"),\n basePath: arg(\"base-path\") ?? \"/icons\",\n });\n let raster;\n try {\n raster = await rasterizeIconSet(set);\n } catch (e) {\n console.error(`pwa-icons: ${(e as Error).message}`);\n return 1;\n }\n mkdirSync(out, { recursive: true });\n for (const f of set.files) writeFileSync(join(out, basename(f.path)), f.content);\n for (const f of raster.files) writeFileSync(join(out, basename(f.path)), f.content);\n // F021.11 — iOS asks the web ROOT for these on its own.\n const root = arg(\"root\") ?? dirname(out);\n for (const f of raster.rootFiles) writeFileSync(join(root, basename(f.path)), f.content);\n console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));\n return 0;\n}\n\n","#!/usr/bin/env node\n/// <reference types=\"node\" />\n// F021.9 — the `pwa-icons` entry point; the logic lives in cli.ts so tests can call it.\nimport { main } from \"./cli.js\";\n\nmain(process.argv.slice(2)).then((code) => process.exit(code));\n"]}
|
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { rasterizeIconSet } from './chunk-
|
|
2
|
+
import { rasterizeIconSet } from './chunk-33GKPCTR.js';
|
|
3
3
|
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
-
import { join, basename } from 'path';
|
|
4
|
+
import { join, basename, dirname } from 'path';
|
|
5
5
|
|
|
6
6
|
// src/manifest.ts
|
|
7
7
|
var DEFAULT_SIZES = [180, 192, 512];
|
|
@@ -59,7 +59,7 @@ function buildIconSet(opts) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// src/cli.ts
|
|
62
|
-
var USAGE = "usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons]";
|
|
62
|
+
var USAGE = "usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons] [--root <web root, default: the parent of --out>]";
|
|
63
63
|
async function main(argv) {
|
|
64
64
|
const arg = (name) => {
|
|
65
65
|
const i = argv.indexOf(`--${name}`);
|
|
@@ -90,6 +90,8 @@ ${USAGE}`);
|
|
|
90
90
|
mkdirSync(out, { recursive: true });
|
|
91
91
|
for (const f of set.files) writeFileSync(join(out, basename(f.path)), f.content);
|
|
92
92
|
for (const f of raster.files) writeFileSync(join(out, basename(f.path)), f.content);
|
|
93
|
+
const root = arg("root") ?? dirname(out);
|
|
94
|
+
for (const f of raster.rootFiles) writeFileSync(join(root, basename(f.path)), f.content);
|
|
93
95
|
console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));
|
|
94
96
|
return 0;
|
|
95
97
|
}
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/manifest.ts","../src/cli.ts","../src/bin.ts"],"names":[],"mappings":"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","file":"bin.js","sourcesContent":["/**\n * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the\n * root export). A dependency-free manifest/meta factory + an SVG-based icon\n * generator, so a repo stops hand-rolling `app/manifest.ts`, a\n * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.\n *\n * Everything here is PURE string/object generation — no filesystem, no\n * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,\n * or the browser, and every piece is offline-unit-testable. Icons are emitted\n * as self-contained SVG documents. iOS does NOT use an SVG apple-touch-icon —\n * rasterise with `@broberg/pwa/raster` / the `pwa-icons` bin (F021.9).\n */\n\n// ---------------------------------------------------------------------------\n// Manifest\n// ---------------------------------------------------------------------------\n\nexport type Display = \"standalone\" | \"fullscreen\" | \"minimal-ui\" | \"browser\";\n\nexport interface ManifestIcon {\n src: string;\n sizes: string;\n type: string;\n /** \"any\" | \"maskable\" | \"monochrome\" (space-separated allowed). */\n purpose?: string;\n}\n\nexport interface WebAppManifest {\n name: string;\n short_name: string;\n start_url: string;\n scope: string;\n display: Display;\n theme_color: string;\n background_color: string;\n icons: ManifestIcon[];\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n [key: string]: unknown;\n}\n\nexport interface DefineManifestOptions {\n /** App name (required). */\n name: string;\n /** Home-screen label. Default: `name`. */\n shortName?: string;\n /** Default `/`. */\n startUrl?: string;\n /** Default `/`. */\n scope?: string;\n /** Default `standalone`. */\n display?: Display;\n /** Address-bar / task-switcher colour. Default `#ffffff`. */\n themeColor?: string;\n /** Splash background. Default `#ffffff`. */\n backgroundColor?: string;\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */\n icons?: ManifestIcon[];\n /** Escape hatch: extra top-level manifest members, merged last. */\n extra?: Record<string, unknown>;\n}\n\n/**\n * Build a spec-valid Web App Manifest object. Required members are defaulted so\n * a one-liner produces an installable manifest; every default is overridable\n * and `extra` merges last for anything not modelled (shortcuts, screenshots…).\n */\nexport function defineManifest(opts: DefineManifestOptions): WebAppManifest {\n if (!opts?.name) throw new Error(\"defineManifest: `name` is required\");\n const manifest: WebAppManifest = {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n start_url: opts.startUrl ?? \"/\",\n scope: opts.scope ?? \"/\",\n display: opts.display ?? \"standalone\",\n theme_color: opts.themeColor ?? \"#ffffff\",\n background_color: opts.backgroundColor ?? \"#ffffff\",\n icons: opts.icons ?? [],\n };\n if (opts.description) manifest.description = opts.description;\n if (opts.lang) manifest.lang = opts.lang;\n if (opts.orientation) manifest.orientation = opts.orientation;\n if (opts.categories) manifest.categories = opts.categories;\n if (opts.id) manifest.id = opts.id;\n return { ...manifest, ...(opts.extra ?? {}) };\n}\n\n/** JSON-stringify a manifest for a `manifest.webmanifest` file. */\nexport function serializeManifest(\n manifest: WebAppManifest,\n opts: { pretty?: boolean } = {},\n): string {\n return JSON.stringify(manifest, null, opts.pretty === false ? undefined : 2);\n}\n\n// ---------------------------------------------------------------------------\n// Icon generation (SVG, zero-dep)\n// ---------------------------------------------------------------------------\n\nexport interface GeneratedIcon {\n /** URL path, e.g. `/icons/icon-192.svg`. */\n path: string;\n /** The SVG document. */\n content: string;\n type: \"image/svg+xml\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface IconSet {\n files: GeneratedIcon[];\n /** Ready to drop into `defineManifest({ icons })`. */\n icons: ManifestIcon[];\n}\n\nexport interface IconSetOptions {\n /**\n * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a\n * data-URI `<image>` so it scales cleanly at any size regardless of its\n * internal coordinate system. Mutually exclusive with `monogram`.\n */\n svg?: string;\n /** Fallback artwork: 1–3 letters rendered centred (e.g. \"AK\"). */\n monogram?: string;\n /** Foreground (monogram text) colour. Default `#ffffff`. */\n color?: string;\n /** Background fill. Default `#111827`. */\n background?: string;\n /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */\n sizes?: number[];\n /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */\n maskable?: boolean;\n /** URL prefix for every icon `src`. Default `/icons`. */\n basePath?: string;\n}\n\nconst DEFAULT_SIZES = [180, 192, 512];\n\nfunction escapeXml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) =>\n c === \"<\" ? \"<\" : c === \">\" ? \">\" : c === \"&\" ? \"&\" : c === '\"' ? \""\" : \"'\",\n );\n}\n\n/** UTF-8-safe base64 for the data-URI embed (works in Node/Bun/browser). */\nfunction toBase64(s: string): string {\n // Reached via globalThis so the package needs no @types/node (browser-safe).\n const g = globalThis as unknown as {\n Buffer?: { from(input: string, enc: string): { toString(enc: string): string } };\n btoa?: (data: string) => string;\n };\n if (typeof g.Buffer !== \"undefined\") return g.Buffer.from(s, \"utf-8\").toString(\"base64\");\n const bytes = new TextEncoder().encode(s);\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return g.btoa!(bin);\n}\n\n/** Render one square SVG icon document at `size`. `inset` = safe-zone fraction (maskable). */\nfunction renderIcon(size: number, inset: number, opts: IconSetOptions): string {\n const bg = opts.background ?? \"#111827\";\n const fg = opts.color ?? \"#ffffff\";\n const pad = Math.round(size * inset);\n const inner = size - pad * 2;\n let art: string;\n if (opts.svg) {\n const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;\n art = `<image x=\"${pad}\" y=\"${pad}\" width=\"${inner}\" height=\"${inner}\" href=\"${href}\" preserveAspectRatio=\"xMidYMid meet\"/>`;\n } else {\n const text = escapeXml((opts.monogram ?? \"\").slice(0, 3).toUpperCase());\n // Font sized to the safe inner box; a touch smaller for breathing room.\n const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));\n art =\n `<text x=\"50%\" y=\"50%\" fill=\"${fg}\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto, sans-serif\" ` +\n `font-weight=\"700\" font-size=\"${fontSize}\" text-anchor=\"middle\" dominant-baseline=\"central\">${text}</text>`;\n }\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\">` +\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${bg}\"/>` +\n art +\n `</svg>`\n );\n}\n\n/**\n * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,\n * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch\n * (180), 192, 512 by default plus a padded maskable-512.\n */\nexport function buildIconSet(opts: IconSetOptions): IconSet {\n if (!opts?.svg && !opts?.monogram) {\n throw new Error(\"buildIconSet: provide either `svg` or `monogram`\");\n }\n const base = (opts.basePath ?? \"/icons\").replace(/\\/$/, \"\");\n const sizes = opts.sizes ?? DEFAULT_SIZES;\n const files: GeneratedIcon[] = [];\n const icons: ManifestIcon[] = [];\n\n for (const size of sizes) {\n const path = `${base}/icon-${size}.svg`;\n files.push({ path, content: renderIcon(size, 0, opts), type: \"image/svg+xml\", size, purpose: \"any\" });\n // apple-touch-icon is referenced via a <link> (see pwaMetaTags), not the\n // manifest, so only real manifest sizes go into icons[].\n if (size !== 180) {\n icons.push({ src: path, sizes: `${size}x${size}`, type: \"image/svg+xml\", purpose: \"any\" });\n }\n }\n\n if (opts.maskable !== false) {\n // Maskable safe-zone: keep content within the central 80% (10% inset).\n const path = `${base}/icon-maskable-512.svg`;\n files.push({ path, content: renderIcon(512, 0.1, opts), type: \"image/svg+xml\", size: 512, purpose: \"maskable\" });\n icons.push({ src: path, sizes: \"512x512\", type: \"image/svg+xml\", purpose: \"maskable\" });\n }\n\n return { files, icons };\n}\n\n// ---------------------------------------------------------------------------\n// Head meta tags\n// ---------------------------------------------------------------------------\n\nexport interface MetaTag {\n tag: \"meta\" | \"link\";\n attrs: Record<string, string>;\n}\n\nexport interface MetaTagsOptions {\n /** `<meta name=\"theme-color\">`. Default `#ffffff`. */\n themeColor?: string;\n /** apple-touch-icon href. Default `/icons/icon-180.svg`. */\n appleTouchIcon?: string;\n /** Manifest href for the `<link rel=\"manifest\">`. Default `/manifest.webmanifest`. */\n manifest?: string;\n /** apple-mobile-web-app-title. Default: omitted. */\n title?: string;\n /** apple-mobile-web-app-status-bar-style. Default `default`. */\n statusBarStyle?: \"default\" | \"black\" | \"black-translucent\";\n /** Emit the `*-web-app-capable` tags. Default `true`. */\n capable?: boolean;\n}\n\n/**\n * The head tags a PWA needs beyond the manifest link — returned as typed\n * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX\n * head, or plain HTML. No framework assumption.\n */\nexport function pwaMetaTags(opts: MetaTagsOptions = {}): MetaTag[] {\n const tags: MetaTag[] = [];\n tags.push({ tag: \"link\", attrs: { rel: \"manifest\", href: opts.manifest ?? \"/manifest.webmanifest\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"theme-color\", content: opts.themeColor ?? \"#ffffff\" } });\n tags.push({ tag: \"link\", attrs: { rel: \"apple-touch-icon\", href: opts.appleTouchIcon ?? \"/icons/icon-180.svg\" } });\n if (opts.capable !== false) {\n tags.push({ tag: \"meta\", attrs: { name: \"mobile-web-app-capable\", content: \"yes\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-capable\", content: \"yes\" } });\n tags.push({\n tag: \"meta\",\n attrs: { name: \"apple-mobile-web-app-status-bar-style\", content: opts.statusBarStyle ?? \"default\" },\n });\n }\n if (opts.title) {\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-title\", content: opts.title } });\n }\n return tags;\n}\n","/// <reference types=\"node\" />\n// F021.9 — `pwa-icons`: SVG + PNG icon set in one command, so no app keeps its\n// own sharp script. Prints the manifest icons[] and the apple-touch href.\nimport { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { buildIconSet } from \"./manifest.js\";\nimport { rasterizeIconSet } from \"./raster.js\";\n\nconst USAGE =\n \"usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons]\";\n\nexport async function main(argv: string[]): Promise<number> {\n const arg = (name: string) => {\n const i = argv.indexOf(`--${name}`);\n return i >= 0 ? argv[i + 1] : undefined;\n };\n const svgFile = arg(\"svg\");\n const monogram = arg(\"monogram\");\n if (!svgFile && !monogram) {\n console.error(`pwa-icons: give --svg <file> or --monogram <text>\\n${USAGE}`);\n return 1;\n }\n const out = arg(\"out\") ?? \"public/icons\";\n const set = buildIconSet({\n svg: svgFile ? readFileSync(svgFile, \"utf8\") : undefined,\n monogram,\n background: arg(\"background\"),\n color: arg(\"color\"),\n basePath: arg(\"base-path\") ?? \"/icons\",\n });\n let raster;\n try {\n raster = await rasterizeIconSet(set);\n } catch (e) {\n console.error(`pwa-icons: ${(e as Error).message}`);\n return 1;\n }\n mkdirSync(out, { recursive: true });\n for (const f of set.files) writeFileSync(join(out, basename(f.path)), f.content);\n for (const f of raster.files) writeFileSync(join(out, basename(f.path)), f.content);\n console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));\n return 0;\n}\n\n","#!/usr/bin/env node\n/// <reference types=\"node\" />\n// F021.9 — the `pwa-icons` entry point; the logic lives in cli.ts so tests can call it.\nimport { main } from \"./cli.js\";\n\nmain(process.argv.slice(2)).then((code) => process.exit(code));\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/manifest.ts","../src/cli.ts","../src/bin.ts"],"names":[],"mappings":"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","file":"bin.js","sourcesContent":["/**\n * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the\n * root export). A dependency-free manifest/meta factory + an SVG-based icon\n * generator, so a repo stops hand-rolling `app/manifest.ts`, a\n * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.\n *\n * Everything here is PURE string/object generation — no filesystem, no\n * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,\n * or the browser, and every piece is offline-unit-testable. Icons are emitted\n * as self-contained SVG documents. iOS does NOT use an SVG apple-touch-icon —\n * rasterise with `@broberg/pwa/raster` / the `pwa-icons` bin (F021.9).\n */\n\n// ---------------------------------------------------------------------------\n// Manifest\n// ---------------------------------------------------------------------------\n\nexport type Display = \"standalone\" | \"fullscreen\" | \"minimal-ui\" | \"browser\";\n\nexport interface ManifestIcon {\n src: string;\n sizes: string;\n type: string;\n /** \"any\" | \"maskable\" | \"monochrome\" (space-separated allowed). */\n purpose?: string;\n}\n\nexport interface WebAppManifest {\n name: string;\n short_name: string;\n start_url: string;\n scope: string;\n display: Display;\n theme_color: string;\n background_color: string;\n icons: ManifestIcon[];\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n [key: string]: unknown;\n}\n\nexport interface DefineManifestOptions {\n /** App name (required). */\n name: string;\n /** Home-screen label. Default: `name`. */\n shortName?: string;\n /** Default `/`. */\n startUrl?: string;\n /** Default `/`. */\n scope?: string;\n /** Default `standalone`. */\n display?: Display;\n /** Address-bar / task-switcher colour. Default `#ffffff`. */\n themeColor?: string;\n /** Splash background. Default `#ffffff`. */\n backgroundColor?: string;\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */\n icons?: ManifestIcon[];\n /** Escape hatch: extra top-level manifest members, merged last. */\n extra?: Record<string, unknown>;\n}\n\n/**\n * Build a spec-valid Web App Manifest object. Required members are defaulted so\n * a one-liner produces an installable manifest; every default is overridable\n * and `extra` merges last for anything not modelled (shortcuts, screenshots…).\n */\nexport function defineManifest(opts: DefineManifestOptions): WebAppManifest {\n if (!opts?.name) throw new Error(\"defineManifest: `name` is required\");\n const manifest: WebAppManifest = {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n start_url: opts.startUrl ?? \"/\",\n scope: opts.scope ?? \"/\",\n display: opts.display ?? \"standalone\",\n theme_color: opts.themeColor ?? \"#ffffff\",\n background_color: opts.backgroundColor ?? \"#ffffff\",\n icons: opts.icons ?? [],\n };\n if (opts.description) manifest.description = opts.description;\n if (opts.lang) manifest.lang = opts.lang;\n if (opts.orientation) manifest.orientation = opts.orientation;\n if (opts.categories) manifest.categories = opts.categories;\n if (opts.id) manifest.id = opts.id;\n return { ...manifest, ...(opts.extra ?? {}) };\n}\n\n/** JSON-stringify a manifest for a `manifest.webmanifest` file. */\nexport function serializeManifest(\n manifest: WebAppManifest,\n opts: { pretty?: boolean } = {},\n): string {\n return JSON.stringify(manifest, null, opts.pretty === false ? undefined : 2);\n}\n\n// ---------------------------------------------------------------------------\n// Icon generation (SVG, zero-dep)\n// ---------------------------------------------------------------------------\n\nexport interface GeneratedIcon {\n /** URL path, e.g. `/icons/icon-192.svg`. */\n path: string;\n /** The SVG document. */\n content: string;\n type: \"image/svg+xml\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface IconSet {\n files: GeneratedIcon[];\n /** Ready to drop into `defineManifest({ icons })`. */\n icons: ManifestIcon[];\n}\n\nexport interface IconSetOptions {\n /**\n * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a\n * data-URI `<image>` so it scales cleanly at any size regardless of its\n * internal coordinate system. Mutually exclusive with `monogram`.\n */\n svg?: string;\n /** Fallback artwork: 1–3 letters rendered centred (e.g. \"AK\"). */\n monogram?: string;\n /** Foreground (monogram text) colour. Default `#ffffff`. */\n color?: string;\n /** Background fill. Default `#111827`. */\n background?: string;\n /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */\n sizes?: number[];\n /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */\n maskable?: boolean;\n /** URL prefix for every icon `src`. Default `/icons`. */\n basePath?: string;\n}\n\nconst DEFAULT_SIZES = [180, 192, 512];\n\nfunction escapeXml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) =>\n c === \"<\" ? \"<\" : c === \">\" ? \">\" : c === \"&\" ? \"&\" : c === '\"' ? \""\" : \"'\",\n );\n}\n\n/** UTF-8-safe base64 for the data-URI embed (works in Node/Bun/browser). */\nfunction toBase64(s: string): string {\n // Reached via globalThis so the package needs no @types/node (browser-safe).\n const g = globalThis as unknown as {\n Buffer?: { from(input: string, enc: string): { toString(enc: string): string } };\n btoa?: (data: string) => string;\n };\n if (typeof g.Buffer !== \"undefined\") return g.Buffer.from(s, \"utf-8\").toString(\"base64\");\n const bytes = new TextEncoder().encode(s);\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return g.btoa!(bin);\n}\n\n/** Render one square SVG icon document at `size`. `inset` = safe-zone fraction (maskable). */\nfunction renderIcon(size: number, inset: number, opts: IconSetOptions): string {\n const bg = opts.background ?? \"#111827\";\n const fg = opts.color ?? \"#ffffff\";\n const pad = Math.round(size * inset);\n const inner = size - pad * 2;\n let art: string;\n if (opts.svg) {\n const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;\n art = `<image x=\"${pad}\" y=\"${pad}\" width=\"${inner}\" height=\"${inner}\" href=\"${href}\" preserveAspectRatio=\"xMidYMid meet\"/>`;\n } else {\n const text = escapeXml((opts.monogram ?? \"\").slice(0, 3).toUpperCase());\n // Font sized to the safe inner box; a touch smaller for breathing room.\n const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));\n art =\n `<text x=\"50%\" y=\"50%\" fill=\"${fg}\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto, sans-serif\" ` +\n `font-weight=\"700\" font-size=\"${fontSize}\" text-anchor=\"middle\" dominant-baseline=\"central\">${text}</text>`;\n }\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\">` +\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${bg}\"/>` +\n art +\n `</svg>`\n );\n}\n\n/**\n * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,\n * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch\n * (180), 192, 512 by default plus a padded maskable-512.\n */\nexport function buildIconSet(opts: IconSetOptions): IconSet {\n if (!opts?.svg && !opts?.monogram) {\n throw new Error(\"buildIconSet: provide either `svg` or `monogram`\");\n }\n const base = (opts.basePath ?? \"/icons\").replace(/\\/$/, \"\");\n const sizes = opts.sizes ?? DEFAULT_SIZES;\n const files: GeneratedIcon[] = [];\n const icons: ManifestIcon[] = [];\n\n for (const size of sizes) {\n const path = `${base}/icon-${size}.svg`;\n files.push({ path, content: renderIcon(size, 0, opts), type: \"image/svg+xml\", size, purpose: \"any\" });\n // apple-touch-icon is referenced via a <link> (see pwaMetaTags), not the\n // manifest, so only real manifest sizes go into icons[].\n if (size !== 180) {\n icons.push({ src: path, sizes: `${size}x${size}`, type: \"image/svg+xml\", purpose: \"any\" });\n }\n }\n\n if (opts.maskable !== false) {\n // Maskable safe-zone: keep content within the central 80% (10% inset).\n const path = `${base}/icon-maskable-512.svg`;\n files.push({ path, content: renderIcon(512, 0.1, opts), type: \"image/svg+xml\", size: 512, purpose: \"maskable\" });\n icons.push({ src: path, sizes: \"512x512\", type: \"image/svg+xml\", purpose: \"maskable\" });\n }\n\n return { files, icons };\n}\n\n// ---------------------------------------------------------------------------\n// Head meta tags\n// ---------------------------------------------------------------------------\n\nexport interface MetaTag {\n tag: \"meta\" | \"link\";\n attrs: Record<string, string>;\n}\n\nexport interface MetaTagsOptions {\n /** `<meta name=\"theme-color\">`. Default `#ffffff`. */\n themeColor?: string;\n /** apple-touch-icon href. Default `/icons/icon-180.svg`. */\n appleTouchIcon?: string;\n /** Manifest href for the `<link rel=\"manifest\">`. Default `/manifest.webmanifest`. */\n manifest?: string;\n /** apple-mobile-web-app-title. Default: omitted. */\n title?: string;\n /** apple-mobile-web-app-status-bar-style. Default `default`. */\n statusBarStyle?: \"default\" | \"black\" | \"black-translucent\";\n /** Emit the `*-web-app-capable` tags. Default `true`. */\n capable?: boolean;\n}\n\n/**\n * The head tags a PWA needs beyond the manifest link — returned as typed\n * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX\n * head, or plain HTML. No framework assumption.\n */\nexport function pwaMetaTags(opts: MetaTagsOptions = {}): MetaTag[] {\n const tags: MetaTag[] = [];\n tags.push({ tag: \"link\", attrs: { rel: \"manifest\", href: opts.manifest ?? \"/manifest.webmanifest\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"theme-color\", content: opts.themeColor ?? \"#ffffff\" } });\n // F021.11 — sizes: the icon is 180; iOS picks by it when several are linked.\n tags.push({ tag: \"link\", attrs: { rel: \"apple-touch-icon\", sizes: \"180x180\", href: opts.appleTouchIcon ?? \"/icons/icon-180.svg\" } });\n if (opts.capable !== false) {\n tags.push({ tag: \"meta\", attrs: { name: \"mobile-web-app-capable\", content: \"yes\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-capable\", content: \"yes\" } });\n tags.push({\n tag: \"meta\",\n attrs: { name: \"apple-mobile-web-app-status-bar-style\", content: opts.statusBarStyle ?? \"default\" },\n });\n }\n if (opts.title) {\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-title\", content: opts.title } });\n }\n return tags;\n}\n","/// <reference types=\"node\" />\n// F021.9 — `pwa-icons`: SVG + PNG icon set in one command, so no app keeps its\n// own sharp script. Prints the manifest icons[] and the apple-touch href.\nimport { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { buildIconSet } from \"./manifest.js\";\nimport { rasterizeIconSet } from \"./raster.js\";\n\nconst USAGE =\n \"usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons] [--root <web root, default: the parent of --out>]\";\n\nexport async function main(argv: string[]): Promise<number> {\n const arg = (name: string) => {\n const i = argv.indexOf(`--${name}`);\n return i >= 0 ? argv[i + 1] : undefined;\n };\n const svgFile = arg(\"svg\");\n const monogram = arg(\"monogram\");\n if (!svgFile && !monogram) {\n console.error(`pwa-icons: give --svg <file> or --monogram <text>\\n${USAGE}`);\n return 1;\n }\n const out = arg(\"out\") ?? \"public/icons\";\n const set = buildIconSet({\n svg: svgFile ? readFileSync(svgFile, \"utf8\") : undefined,\n monogram,\n background: arg(\"background\"),\n color: arg(\"color\"),\n basePath: arg(\"base-path\") ?? \"/icons\",\n });\n let raster;\n try {\n raster = await rasterizeIconSet(set);\n } catch (e) {\n console.error(`pwa-icons: ${(e as Error).message}`);\n return 1;\n }\n mkdirSync(out, { recursive: true });\n for (const f of set.files) writeFileSync(join(out, basename(f.path)), f.content);\n for (const f of raster.files) writeFileSync(join(out, basename(f.path)), f.content);\n // F021.11 — iOS asks the web ROOT for these on its own.\n const root = arg(\"root\") ?? dirname(out);\n for (const f of raster.rootFiles) writeFileSync(join(root, basename(f.path)), f.content);\n console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));\n return 0;\n}\n\n","#!/usr/bin/env node\n/// <reference types=\"node\" />\n// F021.9 — the `pwa-icons` entry point; the logic lives in cli.ts so tests can call it.\nimport { main } from \"./cli.js\";\n\nmain(process.argv.slice(2)).then((code) => process.exit(code));\n"]}
|
|
@@ -17,9 +17,10 @@ async function rasterizeIconSet(set, opts = {}) {
|
|
|
17
17
|
});
|
|
18
18
|
const icons = set.icons.map((i) => ({ ...i, src: toPng(i.src), type: "image/png" }));
|
|
19
19
|
const apple = files.find((f) => f.size === 180 && f.purpose === "any");
|
|
20
|
-
|
|
20
|
+
const rootFiles = apple ? ["/apple-touch-icon.png", "/apple-touch-icon-precomposed.png"].map((path) => ({ ...apple, path })) : [];
|
|
21
|
+
return { files, icons, appleTouchIcon: apple?.path, rootFiles };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export { RESVG_MISSING, rasterizeIconSet };
|
|
24
|
-
//# sourceMappingURL=chunk-
|
|
25
|
-
//# sourceMappingURL=chunk-
|
|
25
|
+
//# sourceMappingURL=chunk-33GKPCTR.js.map
|
|
26
|
+
//# sourceMappingURL=chunk-33GKPCTR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/raster.ts"],"names":[],"mappings":";AAwCO,IAAM,aAAA,GACX;AAEF,IAAM,QAAQ,CAAC,CAAA,KAAc,CAAA,CAAE,OAAA,CAAQ,UAAU,MAAM,CAAA;AAGvD,eAAsB,gBAAA,CAAiB,GAAA,EAAc,IAAA,GAAyB,EAAC,EAA2B;AACxG,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI;AACF,IAAA,CAAC,EAAE,KAAA,EAAM,GAAI,MAAM,OAAO,iBAAiB,CAAA;AAAA,EAC7C,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,aAAa,CAAA;AAAA,EAC/B;AACA,EAAA,MAAM,KAAA,GAAsB,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM;AAC/C,IAAA,MAAM,GAAA,GAAM,IAAI,KAAA,CAAM,CAAA,CAAE,OAAA,EAAS;AAAA,MAC/B,OAAO,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,EAAE,IAAA,EAAK;AAAA,MACtC,IAAA,EAAM,EAAE,eAAA,EAAiB,IAAA,EAAM,WAAW,IAAA,CAAK,SAAA,IAAa,EAAC;AAAE,KAChE,CAAA,CACE,MAAA,EAAO,CACP,KAAA,EAAM;AACT,IAAA,OAAO,EAAE,IAAA,EAAM,KAAA,CAAM,EAAE,IAAI,CAAA,EAAG,SAAS,IAAI,UAAA,CAAW,GAAG,CAAA,EAAG,MAAM,WAAA,EAAa,IAAA,EAAM,EAAE,IAAA,EAAM,OAAA,EAAS,EAAE,OAAA,EAAQ;AAAA,EAClH,CAAC,CAAA;AACD,EAAA,MAAM,QAAQ,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,OAAO,EAAE,GAAG,CAAA,EAAG,GAAA,EAAK,MAAM,CAAA,CAAE,GAAG,CAAA,EAAG,IAAA,EAAM,aAAY,CAAE,CAAA;AACnF,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,EAAE,IAAA,KAAS,GAAA,IAAO,CAAA,CAAE,OAAA,KAAY,KAAK,CAAA;AACrE,EAAA,MAAM,SAAA,GAA0B,KAAA,GAC5B,CAAC,uBAAA,EAAyB,mCAAmC,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,MAAU,EAAE,GAAG,KAAA,EAAO,IAAA,EAAK,CAAE,IACjG,EAAC;AACL,EAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,cAAA,EAAgB,KAAA,EAAO,MAAM,SAAA,EAAU;AAChE","file":"chunk-33GKPCTR.js","sourcesContent":["/**\n * @broberg/pwa/raster — F021.9: PNG icons, because iOS does not use an SVG\n * apple-touch-icon. «Add to Home Screen» on an iPhone with only SVG icons shows\n * a screenshot of the page instead of the logo (measured by sales, 10/10).\n *\n * Kept OUT of the core entries so `@broberg/pwa` and `/manifest` stay\n * dependency-free: the renderer, @resvg/resvg-js (prebuilt binaries, no system\n * libraries), is an optional peer loaded only when this runs.\n */\nimport type { IconSet, ManifestIcon } from \"./manifest.js\";\n\nexport interface RasterIcon {\n /** URL path, e.g. `/icons/icon-180.png`. */\n path: string;\n content: Uint8Array;\n type: \"image/png\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface RasterIconSet {\n files: RasterIcon[];\n /** Manifest `icons[]` pointing at the PNGs. */\n icons: ManifestIcon[];\n /** Href for `<link rel=\"apple-touch-icon\">` — pass it to `pwaMetaTags({ appleTouchIcon })`. Undefined when 180 was not generated. */\n appleTouchIcon?: string;\n /**\n * F021.11 — the same 180 PNG at the two ROOT paths iOS asks for on its own\n * (`/apple-touch-icon.png`, `/apple-touch-icon-precomposed.png`). Write them to\n * your web root, NOT next to the other icons. Without them iOS fell back to a\n * letter («J») on a real home screen (jam, measured). Empty when 180 was not generated.\n */\n rootFiles: RasterIcon[];\n}\n\nexport interface RasterizeOptions {\n /** Extra font files for monogram text (system fonts are loaded too). */\n fontFiles?: string[];\n}\n\nexport const RESVG_MISSING =\n \"rasterizeIconSet needs @resvg/resvg-js — install it as a dev dependency: npm i -D @resvg/resvg-js\";\n\nconst toPng = (p: string) => p.replace(/\\.svg$/, \".png\");\n\n/** Rasterise every SVG from `buildIconSet` to a PNG of the same size. */\nexport async function rasterizeIconSet(set: IconSet, opts: RasterizeOptions = {}): Promise<RasterIconSet> {\n let Resvg: typeof import(\"@resvg/resvg-js\").Resvg;\n try {\n ({ Resvg } = await import(\"@resvg/resvg-js\"));\n } catch {\n throw new Error(RESVG_MISSING);\n }\n const files: RasterIcon[] = set.files.map((f) => {\n const png = new Resvg(f.content, {\n fitTo: { mode: \"width\", value: f.size },\n font: { loadSystemFonts: true, fontFiles: opts.fontFiles ?? [] },\n })\n .render()\n .asPng();\n return { path: toPng(f.path), content: new Uint8Array(png), type: \"image/png\", size: f.size, purpose: f.purpose };\n });\n const icons = set.icons.map((i) => ({ ...i, src: toPng(i.src), type: \"image/png\" }));\n const apple = files.find((f) => f.size === 180 && f.purpose === \"any\");\n const rootFiles: RasterIcon[] = apple\n ? [\"/apple-touch-icon.png\", \"/apple-touch-icon-precomposed.png\"].map((path) => ({ ...apple, path }))\n : [];\n return { files, icons, appleTouchIcon: apple?.path, rootFiles };\n}\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -5,8 +5,9 @@ var SKIP_WAITING = "SKIP_WAITING";
|
|
|
5
5
|
var SKIP_WAITING_MESSAGE = { type: SKIP_WAITING };
|
|
6
6
|
|
|
7
7
|
// src/should-offer.ts
|
|
8
|
-
function shouldOfferUpdate({ waiting, snoozedUntil, now }) {
|
|
8
|
+
function shouldOfferUpdate({ waiting, active, snoozedUntil, now }) {
|
|
9
9
|
if (!waiting) return false;
|
|
10
|
+
if (!active) return false;
|
|
10
11
|
if (snoozedUntil !== null && snoozedUntil > now) return false;
|
|
11
12
|
return true;
|
|
12
13
|
}
|
|
@@ -84,6 +85,7 @@ function createPwaUpdater(options = {}) {
|
|
|
84
85
|
const waiting = registrationRef?.waiting ?? null;
|
|
85
86
|
const next = shouldOfferUpdate({
|
|
86
87
|
waiting,
|
|
88
|
+
active: registrationRef?.active ?? null,
|
|
87
89
|
snoozedUntil: readSnooze(),
|
|
88
90
|
now: Date.now()
|
|
89
91
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/message.ts","../src/should-offer.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.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","/**\n * F054.8 — the decision, as a pure function.\n *\n * Split out so it can be proven with NO service worker, NO DOM and no fake\n * container: every branch is reachable from three plain values. The rest of the\n * updater is plumbing around this.\n *\n * Why it is a function and not a flag: the old updater REMEMBERED whether an\n * update was ready instead of READING whether one is waiting, so the answer\n * could only ever be given once.\n */\nexport interface ShouldOfferInput {\n /** `registration.waiting` — read fresh at the moment of the decision. */\n waiting: unknown;\n /** Epoch ms until which the user has snoozed the offer, or null. */\n snoozedUntil: number | null;\n /** Epoch ms now. Passed in so the function stays pure and testable. */\n now: number;\n}\n\n/**\n * Should the consumer be offered the update right now?\n *\n * WHY `controller` IS NOT A TERM HERE, though the sketch we were shown had one\n * and the old code checked it on one of its two paths.\n *\n * The old updater answered this question inconsistently: the attach path\n * (`if (registration.waiting)`) offered with no controller check, while the\n * `updatefound` path required `container.controller` to suppress a first\n * install. Same situation, two answers, depending on which path happened to see\n * it. Making them agree is part of this fix — the question is which way.\n *\n * Requiring a controller would be the tighter rule, and it would REMOVE a banner\n * that some consumer sees today: a page that is not yet controlled (no\n * `clientsClaim`, or a hard reload) but has a genuine update waiting. With\n * fd-sundhed live to 16,838 people on the current version, a change in that\n * direction is not one to make on an unverified reading.\n *\n * And the reading in question is this: a worker only enters `waiting` when\n * another worker is already ACTIVE — on a first install there is nothing to wait\n * behind, so it activates directly. If that is right, `registration.waiting`\n * being non-null already implies this is not a first install, and the controller\n * term is redundant rather than protective. **I have not verified that against a\n * real service worker.** So the old, more permissive behaviour stays as the\n * floor, and first-install suppression rides on the same fact rather than on a\n * second guard that could disagree with it.\n *\n * If someone later measures that a first-install worker CAN sit in `waiting`,\n * this is the function to add the term to — and the test to write first is the\n * one that fails today.\n */\nexport function shouldOfferUpdate({ waiting, snoozedUntil, now }: ShouldOfferInput): boolean {\n if (!waiting) return false;\n if (snoozedUntil !== null && snoozedUntil > now) return false;\n return true;\n}\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\nimport { shouldOfferUpdate } from \"./should-offer.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport { shouldOfferUpdate } from \"./should-offer.js\";\nexport type { ShouldOfferInput } from \"./should-offer.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 * How long `snooze()` holds the offer down (ms). Default 30 min.\n *\n * F054.8 — \"Later\" is a SNOOZE, never a mute. The defect this replaced was a\n * banner that got exactly one chance to be seen; a permanent dismiss is that\n * defect made official.\n */\n snoozeMs?: number;\n /**\n * Where a snooze is remembered, so it survives the reload the banner is\n * asking for. Defaults to `localStorage` when available; pass `null` to keep\n * the snooze in memory only.\n */\n snoozeStorage?: Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\"> | null;\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 /**\n * Tell the waiting worker to activate (posts SKIP_WAITING); the page reloads\n * when it takes over. If an update was offered but nothing waits any more\n * (it already activated), reloads directly. No-op when nothing was offered.\n */\n applyUpdate(): void;\n /**\n * \"Later\". Holds the offer down for `snoozeMs`, then it comes back on the\n * next tick — this is NOT a mute, and it deliberately cannot be made one.\n */\n snooze(): void;\n /** Re-read `registration.waiting` right now and emit if the answer moved. */\n check(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\nconst DEFAULT_SNOOZE_MS = 30 * 60 * 1000;\nconst SNOOZE_KEY = \"broberg-pwa:snoozed-until\";\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 snoozeMs = DEFAULT_SNOOZE_MS,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\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 snooze() {},\n check() {},\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 // F054.8 — READ the answer, never remember it.\n //\n // This used to be markReady(worker), the only writer of `updateReady`, and it\n // was guarded by `if (!updateReady)` — so `emit()` fired AT MOST ONCE for the\n // life of the updater. Both adapters drive their state from subscribe(), so a\n // consumer who dismissed the banner had no channel that could ever raise it\n // again. cardmem measured the consequence in production: a client sat on an\n // old bundle indefinitely, and the only symptom anyone had was a person\n // saying a feature was missing.\n //\n // `registration` is captured once resolved so every tick can re-read\n // `.waiting` rather than wait to be told.\n let registrationRef: ServiceWorkerRegistration | null = null;\n\n const storage: PwaUpdaterOptions[\"snoozeStorage\"] =\n options.snoozeStorage !== undefined\n ? options.snoozeStorage\n : typeof localStorage !== \"undefined\"\n ? localStorage\n : null;\n\n let snoozedUntil: number | null = null;\n const readSnooze = (): number | null => {\n if (!storage) return snoozedUntil;\n try {\n const raw = storage.getItem(SNOOZE_KEY);\n if (!raw) return null;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n } catch {\n // A browser with site data blocked THROWS on read. That is not a reason to\n // stop offering updates — fall back to the in-memory value.\n return snoozedUntil;\n }\n };\n const writeSnooze = (until: number | null): void => {\n snoozedUntil = until;\n if (!storage) return;\n try {\n if (until === null) storage.removeItem(SNOOZE_KEY);\n else storage.setItem(SNOOZE_KEY, String(until));\n } catch {\n // in-memory only for this tab; already recorded above\n }\n };\n\n /**\n * Re-derive from what is true RIGHT NOW and emit only when the answer moved.\n * Emits in BOTH directions: false→true raises the banner, true→false takes it\n * down when the worker is gone or the user snoozed.\n */\n const check = (): void => {\n const waiting = registrationRef?.waiting ?? null;\n const next = shouldOfferUpdate({\n waiting,\n snoozedUntil: readSnooze(),\n now: Date.now(),\n });\n if (next !== updateReady) {\n updateReady = next;\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 //\n // F054.9 — BUT THE GUARD CANNOT TELL A FIRST CLAIM FROM ONE THE USER ASKED FOR.\n // A page opened with no controller (shift-reload bypasses the SW, or the first\n // page after registration) while a worker waits shows the banner; pressing it\n // activates the worker, whose controllerchange the guard then swallowed. Every\n // later press posted SKIP_WAITING to a worker that was already active. The\n // owner pressed «Opdatér nu» 30 times in helpdesk and nothing happened.\n // `applied` is the missing fact: after an explicit applyUpdate() the next\n // takeover is the update, whatever the page was controlled by before.\n let hadController = !!container.controller;\n let applied = false;\n const reloadOnce = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController && !applied) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\n reloadOnce();\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\") check();\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n // update() asks the SERVER for a new worker; check() reads whether one is\n // already WAITING. The second is the half that was missing — reg.update()\n // resolves without firing updatefound when a worker is already waiting, so\n // a tab that missed the original event could never learn about it.\n const tick = (): void => {\n check();\n registration.update().catch(() => {}).then(check, check);\n };\n const onFocus = (): void => tick();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n tick();\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 registrationRef = registration;\n check();\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n check();\n registration\n .update()\n .catch(() => {\n // ignore transient update-check network errors\n })\n .then(check, check);\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 // Re-read, never the cached reference: the cached one can be a worker\n // that has since ACTIVATED, and posting to it does nothing (F054.9).\n const waiting = registrationRef?.waiting ?? null;\n if (waiting) {\n applied = true;\n // Without clientsClaim an uncontrolled page never gets controllerchange,\n // so the worker's own activation is the second signal. reloadOnce()\n // makes the two paths safe to both fire.\n waiting.addEventListener(\"statechange\", () => {\n if (waiting.state === \"activated\") reloadOnce();\n });\n waiting.postMessage(SKIP_WAITING_MESSAGE);\n return;\n }\n // Nothing waits, yet an update was offered: the worker already took over\n // (an earlier press whose reload was lost). Reloading IS the update.\n if (updateReady) reloadOnce();\n },\n snooze() {\n writeSnooze(Date.now() + snoozeMs);\n check();\n },\n check,\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/message.ts","../src/should-offer.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.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","/**\n * F054.8 — the decision, as a pure function.\n *\n * Split out so it can be proven with NO service worker, NO DOM and no fake\n * container: every branch is reachable from three plain values. The rest of the\n * updater is plumbing around this.\n *\n * Why it is a function and not a flag: the old updater REMEMBERED whether an\n * update was ready instead of READING whether one is waiting, so the answer\n * could only ever be given once.\n */\nexport interface ShouldOfferInput {\n /** `registration.waiting` — read fresh at the moment of the decision. */\n waiting: unknown;\n /** `registration.active` — the worker an update would REPLACE. Read fresh too. */\n active: unknown;\n /** Epoch ms until which the user has snoozed the offer, or null. */\n snoozedUntil: number | null;\n /** Epoch ms now. Passed in so the function stays pure and testable. */\n now: number;\n}\n\n/**\n * Should the consumer be offered the update right now?\n *\n * Three terms: something WAITING, something ACTIVE for it to replace, and no\n * snooze.\n *\n * F021.11 — THE `active` TERM, and why the earlier reasoning here was wrong.\n * This comment used to argue that `registration.waiting` being non-null already\n * implied «not a first install», and said in as many words that it had not been\n * verified against a real service worker. The spec's Install algorithm says\n * otherwise: the installed worker is set as `registration.waiting` and fires\n * `statechange` → `installed` on EVERY install, and only afterwards does Try\n * Activate promote it when nothing is active. The updater checks on exactly that\n * `installed` event, so a first visit raised «Ny version klar» (vn-leker,\n * measured in production) and kept it up until the next check.\n *\n * `active`, not `controller`: a page that is not yet controlled (shift-reload, no\n * clientsClaim) but has a genuine update waiting still has the OLD worker active,\n * so it keeps its banner (the F054.9 case). Only a first install has nothing active.\n */\nexport function shouldOfferUpdate({ waiting, active, snoozedUntil, now }: ShouldOfferInput): boolean {\n if (!waiting) return false;\n if (!active) return false;\n if (snoozedUntil !== null && snoozedUntil > now) return false;\n return true;\n}\n","import { SKIP_WAITING_MESSAGE } from \"./message.js\";\nimport { shouldOfferUpdate } from \"./should-offer.js\";\n\nexport { SKIP_WAITING, SKIP_WAITING_MESSAGE } from \"./message.js\";\nexport { shouldOfferUpdate } from \"./should-offer.js\";\nexport type { ShouldOfferInput } from \"./should-offer.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 * How long `snooze()` holds the offer down (ms). Default 30 min.\n *\n * F054.8 — \"Later\" is a SNOOZE, never a mute. The defect this replaced was a\n * banner that got exactly one chance to be seen; a permanent dismiss is that\n * defect made official.\n */\n snoozeMs?: number;\n /**\n * Where a snooze is remembered, so it survives the reload the banner is\n * asking for. Defaults to `localStorage` when available; pass `null` to keep\n * the snooze in memory only.\n */\n snoozeStorage?: Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\"> | null;\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 /**\n * Tell the waiting worker to activate (posts SKIP_WAITING); the page reloads\n * when it takes over. If an update was offered but nothing waits any more\n * (it already activated), reloads directly. No-op when nothing was offered.\n */\n applyUpdate(): void;\n /**\n * \"Later\". Holds the offer down for `snoozeMs`, then it comes back on the\n * next tick — this is NOT a mute, and it deliberately cannot be made one.\n */\n snooze(): void;\n /** Re-read `registration.waiting` right now and emit if the answer moved. */\n check(): void;\n /** Stop polling + remove all listeners. */\n destroy(): void;\n}\n\nconst DEFAULT_POLL_MS = 60 * 60 * 1000;\nconst DEFAULT_SNOOZE_MS = 30 * 60 * 1000;\nconst SNOOZE_KEY = \"broberg-pwa:snoozed-until\";\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 snoozeMs = DEFAULT_SNOOZE_MS,\n disabled = false,\n } = options;\n\n const listeners = new Set<(state: PwaUpdaterState) => void>();\n let updateReady = false;\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 snooze() {},\n check() {},\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 // F054.8 — READ the answer, never remember it.\n //\n // This used to be markReady(worker), the only writer of `updateReady`, and it\n // was guarded by `if (!updateReady)` — so `emit()` fired AT MOST ONCE for the\n // life of the updater. Both adapters drive their state from subscribe(), so a\n // consumer who dismissed the banner had no channel that could ever raise it\n // again. cardmem measured the consequence in production: a client sat on an\n // old bundle indefinitely, and the only symptom anyone had was a person\n // saying a feature was missing.\n //\n // `registration` is captured once resolved so every tick can re-read\n // `.waiting` rather than wait to be told.\n let registrationRef: ServiceWorkerRegistration | null = null;\n\n const storage: PwaUpdaterOptions[\"snoozeStorage\"] =\n options.snoozeStorage !== undefined\n ? options.snoozeStorage\n : typeof localStorage !== \"undefined\"\n ? localStorage\n : null;\n\n let snoozedUntil: number | null = null;\n const readSnooze = (): number | null => {\n if (!storage) return snoozedUntil;\n try {\n const raw = storage.getItem(SNOOZE_KEY);\n if (!raw) return null;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n } catch {\n // A browser with site data blocked THROWS on read. That is not a reason to\n // stop offering updates — fall back to the in-memory value.\n return snoozedUntil;\n }\n };\n const writeSnooze = (until: number | null): void => {\n snoozedUntil = until;\n if (!storage) return;\n try {\n if (until === null) storage.removeItem(SNOOZE_KEY);\n else storage.setItem(SNOOZE_KEY, String(until));\n } catch {\n // in-memory only for this tab; already recorded above\n }\n };\n\n /**\n * Re-derive from what is true RIGHT NOW and emit only when the answer moved.\n * Emits in BOTH directions: false→true raises the banner, true→false takes it\n * down when the worker is gone or the user snoozed.\n */\n const check = (): void => {\n const waiting = registrationRef?.waiting ?? null;\n const next = shouldOfferUpdate({\n waiting,\n active: registrationRef?.active ?? null,\n snoozedUntil: readSnooze(),\n now: Date.now(),\n });\n if (next !== updateReady) {\n updateReady = next;\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 //\n // F054.9 — BUT THE GUARD CANNOT TELL A FIRST CLAIM FROM ONE THE USER ASKED FOR.\n // A page opened with no controller (shift-reload bypasses the SW, or the first\n // page after registration) while a worker waits shows the banner; pressing it\n // activates the worker, whose controllerchange the guard then swallowed. Every\n // later press posted SKIP_WAITING to a worker that was already active. The\n // owner pressed «Opdatér nu» 30 times in helpdesk and nothing happened.\n // `applied` is the missing fact: after an explicit applyUpdate() the next\n // takeover is the update, whatever the page was controlled by before.\n let hadController = !!container.controller;\n let applied = false;\n const reloadOnce = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n reloading = true; // guard against a reload-loop\n if (typeof location !== \"undefined\") location.reload();\n };\n const onControllerChange = (): void => {\n if (!reloadOnControllerChange || reloading) return;\n if (!hadController && !applied) {\n hadController = true; // first claim of an uncontrolled page — not an update\n return;\n }\n reloadOnce();\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 another is active is an update.\n // On a FIRST install it is briefly `waiting` too (spec), with nothing\n // active — shouldOfferUpdate's `active` term keeps that quiet (F021.11).\n if (installing.state === \"installed\") check();\n });\n };\n\n const wireFocusChecks = (registration: ServiceWorkerRegistration): void => {\n if (!updateOnFocus) return;\n // update() asks the SERVER for a new worker; check() reads whether one is\n // already WAITING. The second is the half that was missing — reg.update()\n // resolves without firing updatefound when a worker is already waiting, so\n // a tab that missed the original event could never learn about it.\n const tick = (): void => {\n check();\n registration.update().catch(() => {}).then(check, check);\n };\n const onFocus = (): void => tick();\n const onVisibility = (): void => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"visible\") {\n tick();\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 registrationRef = registration;\n check();\n registration.addEventListener(\"updatefound\", () => watchInstalling(registration));\n if (pollIntervalMs > 0) {\n pollId = setInterval(() => {\n check();\n registration\n .update()\n .catch(() => {\n // ignore transient update-check network errors\n })\n .then(check, check);\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 // Re-read, never the cached reference: the cached one can be a worker\n // that has since ACTIVATED, and posting to it does nothing (F054.9).\n const waiting = registrationRef?.waiting ?? null;\n if (waiting) {\n applied = true;\n // Without clientsClaim an uncontrolled page never gets controllerchange,\n // so the worker's own activation is the second signal. reloadOnce()\n // makes the two paths safe to both fire.\n waiting.addEventListener(\"statechange\", () => {\n if (waiting.state === \"activated\") reloadOnce();\n });\n waiting.postMessage(SKIP_WAITING_MESSAGE);\n return;\n }\n // Nothing waits, yet an update was offered: the worker already took over\n // (an earlier press whose reload was lost). Reloading IS the update.\n if (updateReady) reloadOnce();\n },\n snooze() {\n writeSnooze(Date.now() + snoozeMs);\n check();\n },\n check,\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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,8 @@ export { S as SKIP_WAITING, a as SKIP_WAITING_MESSAGE, b as SkipWaitingMessage }
|
|
|
14
14
|
interface ShouldOfferInput {
|
|
15
15
|
/** `registration.waiting` — read fresh at the moment of the decision. */
|
|
16
16
|
waiting: unknown;
|
|
17
|
+
/** `registration.active` — the worker an update would REPLACE. Read fresh too. */
|
|
18
|
+
active: unknown;
|
|
17
19
|
/** Epoch ms until which the user has snoozed the offer, or null. */
|
|
18
20
|
snoozedUntil: number | null;
|
|
19
21
|
/** Epoch ms now. Passed in so the function stays pure and testable. */
|
|
@@ -22,35 +24,24 @@ interface ShouldOfferInput {
|
|
|
22
24
|
/**
|
|
23
25
|
* Should the consumer be offered the update right now?
|
|
24
26
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
+
* Three terms: something WAITING, something ACTIVE for it to replace, and no
|
|
28
|
+
* snooze.
|
|
27
29
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* F021.11 — THE `active` TERM, and why the earlier reasoning here was wrong.
|
|
31
|
+
* This comment used to argue that `registration.waiting` being non-null already
|
|
32
|
+
* implied «not a first install», and said in as many words that it had not been
|
|
33
|
+
* verified against a real service worker. The spec's Install algorithm says
|
|
34
|
+
* otherwise: the installed worker is set as `registration.waiting` and fires
|
|
35
|
+
* `statechange` → `installed` on EVERY install, and only afterwards does Try
|
|
36
|
+
* Activate promote it when nothing is active. The updater checks on exactly that
|
|
37
|
+
* `installed` event, so a first visit raised «Ny version klar» (vn-leker,
|
|
38
|
+
* measured in production) and kept it up until the next check.
|
|
33
39
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* fd-sundhed live to 16,838 people on the current version, a change in that
|
|
38
|
-
* direction is not one to make on an unverified reading.
|
|
39
|
-
*
|
|
40
|
-
* And the reading in question is this: a worker only enters `waiting` when
|
|
41
|
-
* another worker is already ACTIVE — on a first install there is nothing to wait
|
|
42
|
-
* behind, so it activates directly. If that is right, `registration.waiting`
|
|
43
|
-
* being non-null already implies this is not a first install, and the controller
|
|
44
|
-
* term is redundant rather than protective. **I have not verified that against a
|
|
45
|
-
* real service worker.** So the old, more permissive behaviour stays as the
|
|
46
|
-
* floor, and first-install suppression rides on the same fact rather than on a
|
|
47
|
-
* second guard that could disagree with it.
|
|
48
|
-
*
|
|
49
|
-
* If someone later measures that a first-install worker CAN sit in `waiting`,
|
|
50
|
-
* this is the function to add the term to — and the test to write first is the
|
|
51
|
-
* one that fails today.
|
|
40
|
+
* `active`, not `controller`: a page that is not yet controlled (shift-reload, no
|
|
41
|
+
* clientsClaim) but has a genuine update waiting still has the OLD worker active,
|
|
42
|
+
* so it keeps its banner (the F054.9 case). Only a first install has nothing active.
|
|
52
43
|
*/
|
|
53
|
-
declare function shouldOfferUpdate({ waiting, snoozedUntil, now }: ShouldOfferInput): boolean;
|
|
44
|
+
declare function shouldOfferUpdate({ waiting, active, snoozedUntil, now }: ShouldOfferInput): boolean;
|
|
54
45
|
|
|
55
46
|
interface PwaUpdaterOptions {
|
|
56
47
|
/** Service-worker script URL to register. Default `/sw.js`. */
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { S as SKIP_WAITING, a as SKIP_WAITING_MESSAGE, b as SkipWaitingMessage }
|
|
|
14
14
|
interface ShouldOfferInput {
|
|
15
15
|
/** `registration.waiting` — read fresh at the moment of the decision. */
|
|
16
16
|
waiting: unknown;
|
|
17
|
+
/** `registration.active` — the worker an update would REPLACE. Read fresh too. */
|
|
18
|
+
active: unknown;
|
|
17
19
|
/** Epoch ms until which the user has snoozed the offer, or null. */
|
|
18
20
|
snoozedUntil: number | null;
|
|
19
21
|
/** Epoch ms now. Passed in so the function stays pure and testable. */
|
|
@@ -22,35 +24,24 @@ interface ShouldOfferInput {
|
|
|
22
24
|
/**
|
|
23
25
|
* Should the consumer be offered the update right now?
|
|
24
26
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
+
* Three terms: something WAITING, something ACTIVE for it to replace, and no
|
|
28
|
+
* snooze.
|
|
27
29
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* F021.11 — THE `active` TERM, and why the earlier reasoning here was wrong.
|
|
31
|
+
* This comment used to argue that `registration.waiting` being non-null already
|
|
32
|
+
* implied «not a first install», and said in as many words that it had not been
|
|
33
|
+
* verified against a real service worker. The spec's Install algorithm says
|
|
34
|
+
* otherwise: the installed worker is set as `registration.waiting` and fires
|
|
35
|
+
* `statechange` → `installed` on EVERY install, and only afterwards does Try
|
|
36
|
+
* Activate promote it when nothing is active. The updater checks on exactly that
|
|
37
|
+
* `installed` event, so a first visit raised «Ny version klar» (vn-leker,
|
|
38
|
+
* measured in production) and kept it up until the next check.
|
|
33
39
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* fd-sundhed live to 16,838 people on the current version, a change in that
|
|
38
|
-
* direction is not one to make on an unverified reading.
|
|
39
|
-
*
|
|
40
|
-
* And the reading in question is this: a worker only enters `waiting` when
|
|
41
|
-
* another worker is already ACTIVE — on a first install there is nothing to wait
|
|
42
|
-
* behind, so it activates directly. If that is right, `registration.waiting`
|
|
43
|
-
* being non-null already implies this is not a first install, and the controller
|
|
44
|
-
* term is redundant rather than protective. **I have not verified that against a
|
|
45
|
-
* real service worker.** So the old, more permissive behaviour stays as the
|
|
46
|
-
* floor, and first-install suppression rides on the same fact rather than on a
|
|
47
|
-
* second guard that could disagree with it.
|
|
48
|
-
*
|
|
49
|
-
* If someone later measures that a first-install worker CAN sit in `waiting`,
|
|
50
|
-
* this is the function to add the term to — and the test to write first is the
|
|
51
|
-
* one that fails today.
|
|
40
|
+
* `active`, not `controller`: a page that is not yet controlled (shift-reload, no
|
|
41
|
+
* clientsClaim) but has a genuine update waiting still has the OLD worker active,
|
|
42
|
+
* so it keeps its banner (the F054.9 case). Only a first install has nothing active.
|
|
52
43
|
*/
|
|
53
|
-
declare function shouldOfferUpdate({ waiting, snoozedUntil, now }: ShouldOfferInput): boolean;
|
|
44
|
+
declare function shouldOfferUpdate({ waiting, active, snoozedUntil, now }: ShouldOfferInput): boolean;
|
|
54
45
|
|
|
55
46
|
interface PwaUpdaterOptions {
|
|
56
47
|
/** Service-worker script URL to register. Default `/sw.js`. */
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@ import { SKIP_WAITING_MESSAGE } from './chunk-ERNF6MZL.js';
|
|
|
2
2
|
export { SKIP_WAITING, SKIP_WAITING_MESSAGE } from './chunk-ERNF6MZL.js';
|
|
3
3
|
|
|
4
4
|
// src/should-offer.ts
|
|
5
|
-
function shouldOfferUpdate({ waiting, snoozedUntil, now }) {
|
|
5
|
+
function shouldOfferUpdate({ waiting, active, snoozedUntil, now }) {
|
|
6
6
|
if (!waiting) return false;
|
|
7
|
+
if (!active) return false;
|
|
7
8
|
if (snoozedUntil !== null && snoozedUntil > now) return false;
|
|
8
9
|
return true;
|
|
9
10
|
}
|
|
@@ -81,6 +82,7 @@ function createPwaUpdater(options = {}) {
|
|
|
81
82
|
const waiting = registrationRef?.waiting ?? null;
|
|
82
83
|
const next = shouldOfferUpdate({
|
|
83
84
|
waiting,
|
|
85
|
+
active: registrationRef?.active ?? null,
|
|
84
86
|
snoozedUntil: readSnooze(),
|
|
85
87
|
now: Date.now()
|
|
86
88
|
});
|