@broberg/pwa 0.6.0 → 0.8.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 CHANGED
@@ -8,6 +8,46 @@ 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.8.0 — native icon sets from the same SVG (F021.12)
12
+
13
+ For Capacitor (iOS/Android) and Tauri apps, so none of them keeps its own icon script:
14
+
15
+ ```bash
16
+ pwa-icons --svg assets/icon.svg --native native-icons --background "#0B7A57"
17
+ ```
18
+
19
+ writes
20
+
21
+ | path | what |
22
+ |---|---|
23
+ | `ios/AppIcon.appiconset/AppIcon-1024.png` + `Contents.json` | Xcode 14+ single-size app icon. **No alpha channel** — the App Store rejects an icon that has one, so the PNG is re-encoded as RGB, not merely made opaque. |
24
+ | `android/mipmap-anydpi-v26/ic_launcher.xml`, `ic_launcher_round.xml` | adaptive icon → `@color/ic_launcher_background` + `@mipmap/ic_launcher_foreground` |
25
+ | `android/mipmap-<dpi>/ic_launcher_foreground.png` | 108dp canvas per density (108–432 px); the motif sits inside the **inner 66dp**, the only part every launcher mask keeps |
26
+ | `android/mipmap-<dpi>/ic_launcher.png`, `ic_launcher_round.png` | legacy launcher icons, 48–192 px |
27
+ | `android/values/ic_launcher_background.xml` | the background colour |
28
+ | `desktop/icon.ico`, `desktop/icon.icns` | Windows / macOS (PNG payloads) |
29
+
30
+ `--background` is **required**: a white symbol on a transparent canvas renders as an empty icon. Copy the folders into your Xcode/Gradle project yourself — the package does not edit your native project. Programmatic: `nativeIconSet(svg, { background, platforms? })` from `@broberg/pwa/raster`. The web output (`rasterizeIconSet`, `pwa-icons` without `--native`) is unchanged.
31
+
32
+ ## 0.7.0 — four fixes reported by the fleet (F021.11)
33
+
34
+ - **No banner on a first visit.** A brand-new worker is briefly `registration.waiting` on EVERY install (the spec's
35
+ Install algorithm), and the updater checked at exactly that moment — so a first visit showed «Ny version klar»
36
+ (vn-leker). An update now needs an ACTIVE worker to replace. A real update always has one, also on a page with
37
+ no controller, so no genuine banner is lost.
38
+ - **No English body line by default.** `labels.body` is empty unless you set it; the title says it. (A banner with
39
+ translated title/buttons used to show «Reload to get the latest version.» underneath — jam.)
40
+ - **The iPhone home-screen icon.** `rasterizeIconSet()` now also returns `rootFiles`: the 180 PNG at
41
+ `/apple-touch-icon.png` and `/apple-touch-icon-precomposed.png`. iOS asks the web ROOT for these on its own;
42
+ without them it fell back to a letter. Write them to your web root (the `pwa-icons` bin does, into the parent of
43
+ `--out` or `--root`). `pwaMetaTags` now puts `sizes="180x180"` on the apple-touch link.
44
+ - **Only PNG in the manifest.** Put **`rasterizeIconSet().icons` — and nothing else — in the manifest `icons[]`.**
45
+ The SVG set from `buildIconSet()` is source material for the rasterizer; SVG entries in `icons[]` gave a letter on
46
+ iOS 26 (jam), and every fleet app that works has PNG only. Pass `rasterizeIconSet().appleTouchIcon` to
47
+ `pwaMetaTags({ appleTouchIcon })`.
48
+ - **Use `pwaMetaTags` for the head**, not only your framework's metadata: it writes `apple-mobile-web-app-capable`,
49
+ which Next 16's `appleWebApp.capable` does not (it writes `mobile-web-app-capable` only).
50
+
11
51
  ## 0.6.0 — a classic service worker can use it too: `pwa-sw.global.js` (F021.10)
12
52
 
13
53
  A service worker in `public/` is a static file and cannot `import` — so
package/dist/bin.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  var fs = require('fs');
5
5
  var path = require('path');
6
+ var zlib = require('zlib');
6
7
 
7
8
  // src/manifest.ts
8
9
  var DEFAULT_SIZES = [180, 192, 512];
@@ -58,6 +59,192 @@ function buildIconSet(opts) {
58
59
  }
59
60
  return { files, icons };
60
61
  }
62
+ var NATIVE_BACKGROUND_REQUIRED = "nativeIconSet needs `background` (#RRGGBB): a white symbol on a transparent canvas renders as an empty icon";
63
+ var HEX = /^#[0-9a-f]{6}$/i;
64
+ var ANDROID_DENSITIES = [
65
+ { dpi: "mdpi", foreground: 108, legacy: 48 },
66
+ { dpi: "hdpi", foreground: 162, legacy: 72 },
67
+ { dpi: "xhdpi", foreground: 216, legacy: 96 },
68
+ { dpi: "xxhdpi", foreground: 324, legacy: 144 },
69
+ { dpi: "xxxhdpi", foreground: 432, legacy: 192 }
70
+ ];
71
+ var ANDROID_SAFE_ZONE = 66 / 108;
72
+ var ICO_SIZES = [16, 32, 48, 256];
73
+ var ICNS_CHUNKS = [
74
+ ["ic11", 32],
75
+ ["ic12", 64],
76
+ ["ic07", 128],
77
+ ["ic08", 256],
78
+ ["ic13", 256],
79
+ ["ic09", 512],
80
+ ["ic14", 512],
81
+ ["ic10", 1024]
82
+ ];
83
+ function composeSvg(svg, size, opts) {
84
+ const d = size * opts.scale;
85
+ const o = (size - d) / 2;
86
+ const bg = opts.shape === "square" && opts.background ? `<rect width="${size}" height="${size}" fill="${opts.background}"/>` : opts.shape === "circle" && opts.background ? `<circle cx="${size / 2}" cy="${size / 2}" r="${size / 2}" fill="${opts.background}"/>` : "";
87
+ const href = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
88
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">${bg}<image href="${href}" x="${o}" y="${o}" width="${d}" height="${d}"/></svg>`;
89
+ }
90
+ var CRC_TABLE = (() => {
91
+ const t = new Uint32Array(256);
92
+ for (let n = 0; n < 256; n++) {
93
+ let c = n;
94
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
95
+ t[n] = c >>> 0;
96
+ }
97
+ return t;
98
+ })();
99
+ function crc32(bytes) {
100
+ let c = 4294967295;
101
+ for (const b of bytes) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
102
+ return (c ^ 4294967295) >>> 0;
103
+ }
104
+ function chunk(type, data) {
105
+ const out = new Uint8Array(12 + data.length);
106
+ const v = new DataView(out.buffer);
107
+ v.setUint32(0, data.length);
108
+ for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
109
+ out.set(data, 8);
110
+ v.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
111
+ return out;
112
+ }
113
+ function encodeRgbPng(rgba, width, height) {
114
+ const raw = new Uint8Array(height * (1 + width * 3));
115
+ for (let y = 0; y < height; y++) {
116
+ const row = y * (1 + width * 3);
117
+ raw[row] = 0;
118
+ for (let x = 0; x < width; x++) {
119
+ const s = (y * width + x) * 4;
120
+ const t = row + 1 + x * 3;
121
+ raw[t] = rgba[s];
122
+ raw[t + 1] = rgba[s + 1];
123
+ raw[t + 2] = rgba[s + 2];
124
+ }
125
+ }
126
+ const ihdr = new Uint8Array(13);
127
+ const v = new DataView(ihdr.buffer);
128
+ v.setUint32(0, width);
129
+ v.setUint32(4, height);
130
+ ihdr[8] = 8;
131
+ ihdr[9] = 2;
132
+ const sig = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
133
+ const parts = [sig, chunk("IHDR", ihdr), chunk("IDAT", new Uint8Array(zlib.deflateSync(raw))), chunk("IEND", new Uint8Array())];
134
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
135
+ let off = 0;
136
+ for (const p of parts) {
137
+ out.set(p, off);
138
+ off += p.length;
139
+ }
140
+ return out;
141
+ }
142
+ function encodeIco(pngs) {
143
+ const header = 6 + 16 * pngs.length;
144
+ const total = header + pngs.reduce((n, p) => n + p.png.length, 0);
145
+ const out = new Uint8Array(total);
146
+ const v = new DataView(out.buffer);
147
+ v.setUint16(0, 0, true);
148
+ v.setUint16(2, 1, true);
149
+ v.setUint16(4, pngs.length, true);
150
+ let off = header;
151
+ pngs.forEach((p, i) => {
152
+ const e = 6 + i * 16;
153
+ out[e] = p.size >= 256 ? 0 : p.size;
154
+ out[e + 1] = p.size >= 256 ? 0 : p.size;
155
+ v.setUint16(e + 4, 1, true);
156
+ v.setUint16(e + 6, 32, true);
157
+ v.setUint32(e + 8, p.png.length, true);
158
+ v.setUint32(e + 12, off, true);
159
+ out.set(p.png, off);
160
+ off += p.png.length;
161
+ });
162
+ return out;
163
+ }
164
+ function encodeIcns(chunks) {
165
+ const total = 8 + chunks.reduce((n, c) => n + 8 + c.png.length, 0);
166
+ const out = new Uint8Array(total);
167
+ const v = new DataView(out.buffer);
168
+ const tag = (at, s) => {
169
+ for (let i = 0; i < 4; i++) out[at + i] = s.charCodeAt(i);
170
+ };
171
+ tag(0, "icns");
172
+ v.setUint32(4, total);
173
+ let off = 8;
174
+ for (const c of chunks) {
175
+ tag(off, c.type);
176
+ v.setUint32(off + 4, 8 + c.png.length);
177
+ out.set(c.png, off + 8);
178
+ off += 8 + c.png.length;
179
+ }
180
+ return out;
181
+ }
182
+ var enc = (s) => new TextEncoder().encode(s);
183
+ var ADAPTIVE_XML = `<?xml version="1.0" encoding="utf-8"?>
184
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
185
+ <background android:drawable="@color/ic_launcher_background"/>
186
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
187
+ </adaptive-icon>
188
+ `;
189
+ var IOS_CONTENTS = JSON.stringify(
190
+ {
191
+ images: [{ filename: "AppIcon-1024.png", idiom: "universal", platform: "ios", size: "1024x1024" }],
192
+ info: { author: "@broberg/pwa", version: 1 }
193
+ },
194
+ null,
195
+ 2
196
+ );
197
+ async function nativeIconSet(svg, opts) {
198
+ if (!opts?.background || !HEX.test(opts.background)) throw new Error(NATIVE_BACKGROUND_REQUIRED);
199
+ let Resvg;
200
+ try {
201
+ ({ Resvg } = await import('@resvg/resvg-js'));
202
+ } catch {
203
+ throw new Error("nativeIconSet needs @resvg/resvg-js \u2014 install it as a dev dependency: npm i -D @resvg/resvg-js");
204
+ }
205
+ const bg = opts.background;
206
+ const want = new Set(opts.platforms ?? ["ios", "android", "desktop"]);
207
+ const render = (size, shape, scale) => new Resvg(composeSvg(svg, size, { background: bg, shape, scale }), { font: { loadSystemFonts: false } }).render();
208
+ const png = (size, shape, scale) => new Uint8Array(render(size, shape, scale).asPng());
209
+ const files = [];
210
+ if (want.has("ios")) {
211
+ const r = render(1024, "square", 0.8);
212
+ files.push(
213
+ { path: "ios/AppIcon.appiconset/AppIcon-1024.png", content: encodeRgbPng(new Uint8Array(r.pixels), r.width, r.height) },
214
+ { path: "ios/AppIcon.appiconset/Contents.json", content: enc(IOS_CONTENTS + "\n") }
215
+ );
216
+ }
217
+ if (want.has("android")) {
218
+ files.push(
219
+ { path: "android/mipmap-anydpi-v26/ic_launcher.xml", content: enc(ADAPTIVE_XML) },
220
+ { path: "android/mipmap-anydpi-v26/ic_launcher_round.xml", content: enc(ADAPTIVE_XML) },
221
+ {
222
+ path: "android/values/ic_launcher_background.xml",
223
+ content: enc(
224
+ `<?xml version="1.0" encoding="utf-8"?>
225
+ <resources>
226
+ <color name="ic_launcher_background">${bg}</color>
227
+ </resources>
228
+ `
229
+ )
230
+ }
231
+ );
232
+ for (const d of ANDROID_DENSITIES) {
233
+ files.push(
234
+ { path: `android/mipmap-${d.dpi}/ic_launcher_foreground.png`, content: png(d.foreground, "none", ANDROID_SAFE_ZONE) },
235
+ { path: `android/mipmap-${d.dpi}/ic_launcher.png`, content: png(d.legacy, "square", 0.7) },
236
+ { path: `android/mipmap-${d.dpi}/ic_launcher_round.png`, content: png(d.legacy, "circle", 0.62) }
237
+ );
238
+ }
239
+ }
240
+ if (want.has("desktop")) {
241
+ files.push(
242
+ { path: "desktop/icon.ico", content: encodeIco(ICO_SIZES.map((size) => ({ size, png: png(size, "square", 0.8) }))) },
243
+ { path: "desktop/icon.icns", content: encodeIcns(ICNS_CHUNKS.map(([type, size]) => ({ type, png: png(size, "square", 0.8) }))) }
244
+ );
245
+ }
246
+ return files;
247
+ }
61
248
 
62
249
  // src/raster.ts
63
250
  var RESVG_MISSING = "rasterizeIconSet needs @resvg/resvg-js \u2014 install it as a dev dependency: npm i -D @resvg/resvg-js";
@@ -78,11 +265,12 @@ async function rasterizeIconSet(set, opts = {}) {
78
265
  });
79
266
  const icons = set.icons.map((i) => ({ ...i, src: toPng(i.src), type: "image/png" }));
80
267
  const apple = files.find((f) => f.size === 180 && f.purpose === "any");
81
- return { files, icons, appleTouchIcon: apple?.path };
268
+ const rootFiles = apple ? ["/apple-touch-icon.png", "/apple-touch-icon-precomposed.png"].map((path) => ({ ...apple, path })) : [];
269
+ return { files, icons, appleTouchIcon: apple?.path, rootFiles };
82
270
  }
83
271
 
84
272
  // src/cli.ts
85
- var USAGE = "usage: pwa-icons (--svg <file> | --monogram <text>) [--background <hex>] [--color <hex>] [--out public/icons] [--base-path /icons]";
273
+ 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>]\n pwa-icons --svg <file> --native <out dir> --background <#RRGGBB> (iOS AppIcon, Android adaptive, .ico, .icns)";
86
274
  async function main(argv) {
87
275
  const arg = (name) => {
88
276
  const i = argv.indexOf(`--${name}`);
@@ -95,6 +283,32 @@ async function main(argv) {
95
283
  ${USAGE}`);
96
284
  return 1;
97
285
  }
286
+ const native = arg("native");
287
+ if (native !== void 0) {
288
+ if (!svgFile) {
289
+ console.error(`pwa-icons: --native needs --svg <file>
290
+ ${USAGE}`);
291
+ return 1;
292
+ }
293
+ const background = arg("background");
294
+ if (!background) {
295
+ console.error(`pwa-icons: --native needs --background <#RRGGBB> \u2014 ${NATIVE_BACKGROUND_REQUIRED}`);
296
+ return 1;
297
+ }
298
+ let files;
299
+ try {
300
+ files = await nativeIconSet(fs.readFileSync(svgFile, "utf8"), { background });
301
+ } catch (e) {
302
+ console.error(`pwa-icons: ${e.message}`);
303
+ return 1;
304
+ }
305
+ for (const f of files) {
306
+ fs.mkdirSync(path.dirname(path.join(native, f.path)), { recursive: true });
307
+ fs.writeFileSync(path.join(native, f.path), f.content);
308
+ }
309
+ console.log(JSON.stringify({ native: files.map((f) => f.path) }, null, 2));
310
+ return 0;
311
+ }
98
312
  const out = arg("out") ?? "public/icons";
99
313
  const set = buildIconSet({
100
314
  svg: svgFile ? fs.readFileSync(svgFile, "utf8") : void 0,
@@ -113,6 +327,8 @@ ${USAGE}`);
113
327
  fs.mkdirSync(out, { recursive: true });
114
328
  for (const f of set.files) fs.writeFileSync(path.join(out, path.basename(f.path)), f.content);
115
329
  for (const f of raster.files) fs.writeFileSync(path.join(out, path.basename(f.path)), f.content);
330
+ const root = arg("root") ?? path.dirname(out);
331
+ for (const f of raster.rootFiles) fs.writeFileSync(path.join(root, path.basename(f.path)), f.content);
116
332
  console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));
117
333
  return 0;
118
334
  }
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 === \"<\" ? \"&lt;\" : c === \">\" ? \"&gt;\" : c === \"&\" ? \"&amp;\" : c === '\"' ? \"&quot;\" : \"&apos;\",\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/native.ts","../src/raster.ts","../src/cli.ts","../src/bin.ts"],"names":["deflateSync","readFileSync","mkdirSync","dirname","join","writeFileSync","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 === \"<\" ? \"&lt;\" : c === \">\" ? \"&gt;\" : c === \"&\" ? \"&amp;\" : c === '\"' ? \"&quot;\" : \"&apos;\",\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.12: the NATIVE icon sets, from the same SVG the web\n * icons come from, so no Capacitor or Tauri app keeps its own icon script.\n *\n * ios/AppIcon.appiconset/ AppIcon-1024.png + Contents.json (Xcode 14+ single size)\n * android/mipmap-anydpi-v26/ ic_launcher.xml, ic_launcher_round.xml (adaptive)\n * android/mipmap-<dpi>/ ic_launcher_foreground.png, ic_launcher.png, ic_launcher_round.png\n * android/values/ ic_launcher_background.xml\n * desktop/ icon.ico, icon.icns\n *\n * Two platform rules decide the code, and both fail silently if ignored:\n * - App Store rejects an app icon with an alpha channel. resvg always renders\n * RGBA, so the iOS PNG is RE-ENCODED as RGB (colour type 2) — flattening onto\n * the background is not enough on its own, the channel must be gone.\n * - Android crops an adaptive icon's 108dp layer to whatever mask the launcher\n * uses; only the inner 66dp is guaranteed visible. The motif is drawn inside it.\n *\n * `background` is REQUIRED: a white symbol on a transparent canvas renders as an\n * empty icon (measured by buzzalert with `tauri icon`, 25/10).\n *\n * ICO and ICNS are written as plain byte containers holding PNGs — no dependency.\n */\nimport { deflateSync } from \"node:zlib\";\n\nexport interface NativeIconFile {\n /** Path relative to the output directory, e.g. `ios/AppIcon.appiconset/AppIcon-1024.png`. */\n path: string;\n content: Uint8Array;\n}\n\nexport interface NativeIconOptions {\n /** Hex colour behind the motif (`#RRGGBB`). Required — see the module comment. */\n background: string;\n /** Default: all three. */\n platforms?: Array<\"ios\" | \"android\" | \"desktop\">;\n}\n\nexport const NATIVE_BACKGROUND_REQUIRED =\n \"nativeIconSet needs `background` (#RRGGBB): a white symbol on a transparent canvas renders as an empty icon\";\n\nconst HEX = /^#[0-9a-f]{6}$/i;\n\n/** Android density → adaptive foreground px (108dp) and legacy launcher px (48dp). */\nexport const ANDROID_DENSITIES = [\n { dpi: \"mdpi\", foreground: 108, legacy: 48 },\n { dpi: \"hdpi\", foreground: 162, legacy: 72 },\n { dpi: \"xhdpi\", foreground: 216, legacy: 96 },\n { dpi: \"xxhdpi\", foreground: 324, legacy: 144 },\n { dpi: \"xxxhdpi\", foreground: 432, legacy: 192 },\n] as const;\n\n/** Share of the 108dp adaptive canvas that every launcher mask keeps visible. */\nexport const ANDROID_SAFE_ZONE = 66 / 108;\n\nexport const ICO_SIZES = [16, 32, 48, 256] as const;\n/** ICNS chunk type → pixel size (PNG payloads). */\nexport const ICNS_CHUNKS = [\n [\"ic11\", 32],\n [\"ic12\", 64],\n [\"ic07\", 128],\n [\"ic08\", 256],\n [\"ic13\", 256],\n [\"ic09\", 512],\n [\"ic14\", 512],\n [\"ic10\", 1024],\n] as const;\n\ntype Shape = \"square\" | \"circle\" | \"none\";\n\nfunction composeSvg(svg: string, size: number, opts: { background?: string; shape: Shape; scale: number }): string {\n const d = size * opts.scale;\n const o = (size - d) / 2;\n const bg =\n opts.shape === \"square\" && opts.background\n ? `<rect width=\"${size}\" height=\"${size}\" fill=\"${opts.background}\"/>`\n : opts.shape === \"circle\" && opts.background\n ? `<circle cx=\"${size / 2}\" cy=\"${size / 2}\" r=\"${size / 2}\" fill=\"${opts.background}\"/>`\n : \"\";\n const href = `data:image/svg+xml;base64,${Buffer.from(svg).toString(\"base64\")}`;\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\">` +\n `${bg}<image href=\"${href}\" x=\"${o}\" y=\"${o}\" width=\"${d}\" height=\"${d}\"/></svg>`\n );\n}\n\n// ---- PNG encoding (RGB, no alpha) -------------------------------------------\n\nconst CRC_TABLE = (() => {\n const t = new Uint32Array(256);\n for (let n = 0; n < 256; n++) {\n let c = n;\n for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;\n t[n] = c >>> 0;\n }\n return t;\n})();\n\nfunction crc32(bytes: Uint8Array): number {\n let c = 0xffffffff;\n for (const b of bytes) c = CRC_TABLE[(c ^ b) & 0xff]! ^ (c >>> 8);\n return (c ^ 0xffffffff) >>> 0;\n}\n\nfunction chunk(type: string, data: Uint8Array): Uint8Array {\n const out = new Uint8Array(12 + data.length);\n const v = new DataView(out.buffer);\n v.setUint32(0, data.length);\n for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);\n out.set(data, 8);\n v.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));\n return out;\n}\n\n/** Encode RGBA pixels as an RGB PNG (colour type 2) — the alpha channel is dropped, not just made opaque. */\nexport function encodeRgbPng(rgba: Uint8Array, width: number, height: number): Uint8Array {\n const raw = new Uint8Array(height * (1 + width * 3));\n for (let y = 0; y < height; y++) {\n const row = y * (1 + width * 3);\n raw[row] = 0; // filter: none\n for (let x = 0; x < width; x++) {\n const s = (y * width + x) * 4;\n const t = row + 1 + x * 3;\n raw[t] = rgba[s]!;\n raw[t + 1] = rgba[s + 1]!;\n raw[t + 2] = rgba[s + 2]!;\n }\n }\n const ihdr = new Uint8Array(13);\n const v = new DataView(ihdr.buffer);\n v.setUint32(0, width);\n v.setUint32(4, height);\n ihdr[8] = 8; // bit depth\n ihdr[9] = 2; // colour type: truecolour, NO alpha\n const sig = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);\n const parts = [sig, chunk(\"IHDR\", ihdr), chunk(\"IDAT\", new Uint8Array(deflateSync(raw))), chunk(\"IEND\", new Uint8Array())];\n const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));\n let off = 0;\n for (const p of parts) {\n out.set(p, off);\n off += p.length;\n }\n return out;\n}\n\n// ---- containers --------------------------------------------------------------\n\n/** An ICO holding PNG images (Vista+ format). */\nexport function encodeIco(pngs: Array<{ size: number; png: Uint8Array }>): Uint8Array {\n const header = 6 + 16 * pngs.length;\n const total = header + pngs.reduce((n, p) => n + p.png.length, 0);\n const out = new Uint8Array(total);\n const v = new DataView(out.buffer);\n v.setUint16(0, 0, true); // reserved\n v.setUint16(2, 1, true); // type: icon\n v.setUint16(4, pngs.length, true);\n let off = header;\n pngs.forEach((p, i) => {\n const e = 6 + i * 16;\n out[e] = p.size >= 256 ? 0 : p.size; // 0 means 256\n out[e + 1] = p.size >= 256 ? 0 : p.size;\n v.setUint16(e + 4, 1, true); // colour planes\n v.setUint16(e + 6, 32, true); // bits per pixel\n v.setUint32(e + 8, p.png.length, true);\n v.setUint32(e + 12, off, true);\n out.set(p.png, off);\n off += p.png.length;\n });\n return out;\n}\n\n/** An ICNS holding PNG chunks. */\nexport function encodeIcns(chunks: Array<{ type: string; png: Uint8Array }>): Uint8Array {\n const total = 8 + chunks.reduce((n, c) => n + 8 + c.png.length, 0);\n const out = new Uint8Array(total);\n const v = new DataView(out.buffer);\n const tag = (at: number, s: string) => {\n for (let i = 0; i < 4; i++) out[at + i] = s.charCodeAt(i);\n };\n tag(0, \"icns\");\n v.setUint32(4, total);\n let off = 8;\n for (const c of chunks) {\n tag(off, c.type);\n v.setUint32(off + 4, 8 + c.png.length);\n out.set(c.png, off + 8);\n off += 8 + c.png.length;\n }\n return out;\n}\n\n// ---- the set -------------------------------------------------------------------\n\nconst enc = (s: string) => new TextEncoder().encode(s);\n\nconst ADAPTIVE_XML =\n `<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n` +\n `<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\\n` +\n ` <background android:drawable=\"@color/ic_launcher_background\"/>\\n` +\n ` <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\\n` +\n `</adaptive-icon>\\n`;\n\nconst IOS_CONTENTS = JSON.stringify(\n {\n images: [{ filename: \"AppIcon-1024.png\", idiom: \"universal\", platform: \"ios\", size: \"1024x1024\" }],\n info: { author: \"@broberg/pwa\", version: 1 },\n },\n null,\n 2,\n);\n\n/**\n * Every native icon file for `svg`. Requires the optional peer @resvg/resvg-js,\n * like `rasterizeIconSet`.\n */\nexport async function nativeIconSet(svg: string, opts: NativeIconOptions): Promise<NativeIconFile[]> {\n if (!opts?.background || !HEX.test(opts.background)) throw new Error(NATIVE_BACKGROUND_REQUIRED);\n let Resvg: typeof import(\"@resvg/resvg-js\").Resvg;\n try {\n ({ Resvg } = await import(\"@resvg/resvg-js\"));\n } catch {\n throw new Error(\"nativeIconSet needs @resvg/resvg-js — install it as a dev dependency: npm i -D @resvg/resvg-js\");\n }\n const bg = opts.background;\n const want = new Set(opts.platforms ?? [\"ios\", \"android\", \"desktop\"]);\n const render = (size: number, shape: Shape, scale: number) =>\n new Resvg(composeSvg(svg, size, { background: bg, shape, scale }), { font: { loadSystemFonts: false } }).render();\n const png = (size: number, shape: Shape, scale: number) => new Uint8Array(render(size, shape, scale).asPng());\n\n const files: NativeIconFile[] = [];\n\n if (want.has(\"ios\")) {\n const r = render(1024, \"square\", 0.8);\n files.push(\n { path: \"ios/AppIcon.appiconset/AppIcon-1024.png\", content: encodeRgbPng(new Uint8Array(r.pixels), r.width, r.height) },\n { path: \"ios/AppIcon.appiconset/Contents.json\", content: enc(IOS_CONTENTS + \"\\n\") },\n );\n }\n\n if (want.has(\"android\")) {\n files.push(\n { path: \"android/mipmap-anydpi-v26/ic_launcher.xml\", content: enc(ADAPTIVE_XML) },\n { path: \"android/mipmap-anydpi-v26/ic_launcher_round.xml\", content: enc(ADAPTIVE_XML) },\n {\n path: \"android/values/ic_launcher_background.xml\",\n content: enc(\n `<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<resources>\\n <color name=\"ic_launcher_background\">${bg}</color>\\n</resources>\\n`,\n ),\n },\n );\n for (const d of ANDROID_DENSITIES) {\n files.push(\n { path: `android/mipmap-${d.dpi}/ic_launcher_foreground.png`, content: png(d.foreground, \"none\", ANDROID_SAFE_ZONE) },\n { path: `android/mipmap-${d.dpi}/ic_launcher.png`, content: png(d.legacy, \"square\", 0.7) },\n { path: `android/mipmap-${d.dpi}/ic_launcher_round.png`, content: png(d.legacy, \"circle\", 0.62) },\n );\n }\n }\n\n if (want.has(\"desktop\")) {\n files.push(\n { path: \"desktop/icon.ico\", content: encodeIco(ICO_SIZES.map((size) => ({ size, png: png(size, \"square\", 0.8) }))) },\n { path: \"desktop/icon.icns\", content: encodeIcns(ICNS_CHUNKS.map(([type, size]) => ({ type, png: png(size, \"square\", 0.8) }))) },\n );\n }\n\n return files;\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\n// F021.12 — native icon sets (iOS AppIcon, Android adaptive, .ico/.icns) from the same SVG.\nexport { nativeIconSet, encodeRgbPng, encodeIco, encodeIcns, NATIVE_BACKGROUND_REQUIRED, ANDROID_DENSITIES, ANDROID_SAFE_ZONE, ICO_SIZES, ICNS_CHUNKS } from \"./native.js\";\nexport type { NativeIconFile, NativeIconOptions } from \"./native.js\";\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, nativeIconSet, NATIVE_BACKGROUND_REQUIRED } 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 pwa-icons --svg <file> --native <out dir> --background <#RRGGBB> (iOS AppIcon, Android adaptive, .ico, .icns)\";\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 // F021.12 — native sets. Background is required: a white symbol on a transparent\n // canvas renders as an empty icon (buzzalert, 25/10).\n const native = arg(\"native\");\n if (native !== undefined) {\n if (!svgFile) {\n console.error(`pwa-icons: --native needs --svg <file>\\n${USAGE}`);\n return 1;\n }\n const background = arg(\"background\");\n if (!background) {\n console.error(`pwa-icons: --native needs --background <#RRGGBB> — ${NATIVE_BACKGROUND_REQUIRED}`);\n return 1;\n }\n let files;\n try {\n files = await nativeIconSet(readFileSync(svgFile, \"utf8\"), { background });\n } catch (e) {\n console.error(`pwa-icons: ${(e as Error).message}`);\n return 1;\n }\n for (const f of files) {\n mkdirSync(dirname(join(native, f.path)), { recursive: true });\n writeFileSync(join(native, f.path), f.content);\n }\n console.log(JSON.stringify({ native: files.map((f) => f.path) }, null, 2));\n return 0;\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-JWKQNVNE.js';
2
+ import { NATIVE_BACKGROUND_REQUIRED, nativeIconSet, rasterizeIconSet } from './chunk-TZPDZ6IO.js';
3
3
  import { readFileSync, mkdirSync, writeFileSync } from 'fs';
4
- import { join, basename } from 'path';
4
+ import { dirname, join, basename } 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>]\n pwa-icons --svg <file> --native <out dir> --background <#RRGGBB> (iOS AppIcon, Android adaptive, .ico, .icns)";
63
63
  async function main(argv) {
64
64
  const arg = (name) => {
65
65
  const i = argv.indexOf(`--${name}`);
@@ -72,6 +72,32 @@ async function main(argv) {
72
72
  ${USAGE}`);
73
73
  return 1;
74
74
  }
75
+ const native = arg("native");
76
+ if (native !== void 0) {
77
+ if (!svgFile) {
78
+ console.error(`pwa-icons: --native needs --svg <file>
79
+ ${USAGE}`);
80
+ return 1;
81
+ }
82
+ const background = arg("background");
83
+ if (!background) {
84
+ console.error(`pwa-icons: --native needs --background <#RRGGBB> \u2014 ${NATIVE_BACKGROUND_REQUIRED}`);
85
+ return 1;
86
+ }
87
+ let files;
88
+ try {
89
+ files = await nativeIconSet(readFileSync(svgFile, "utf8"), { background });
90
+ } catch (e) {
91
+ console.error(`pwa-icons: ${e.message}`);
92
+ return 1;
93
+ }
94
+ for (const f of files) {
95
+ mkdirSync(dirname(join(native, f.path)), { recursive: true });
96
+ writeFileSync(join(native, f.path), f.content);
97
+ }
98
+ console.log(JSON.stringify({ native: files.map((f) => f.path) }, null, 2));
99
+ return 0;
100
+ }
75
101
  const out = arg("out") ?? "public/icons";
76
102
  const set = buildIconSet({
77
103
  svg: svgFile ? readFileSync(svgFile, "utf8") : void 0,
@@ -90,6 +116,8 @@ ${USAGE}`);
90
116
  mkdirSync(out, { recursive: true });
91
117
  for (const f of set.files) writeFileSync(join(out, basename(f.path)), f.content);
92
118
  for (const f of raster.files) writeFileSync(join(out, basename(f.path)), f.content);
119
+ const root = arg("root") ?? dirname(out);
120
+ for (const f of raster.rootFiles) writeFileSync(join(root, basename(f.path)), f.content);
93
121
  console.log(JSON.stringify({ icons: raster.icons, appleTouchIcon: raster.appleTouchIcon }, null, 2));
94
122
  return 0;
95
123
  }
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 === \"<\" ? \"&lt;\" : c === \">\" ? \"&gt;\" : c === \"&\" ? \"&amp;\" : c === '\"' ? \"&quot;\" : \"&apos;\",\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 === \"<\" ? \"&lt;\" : c === \">\" ? \"&gt;\" : c === \"&\" ? \"&amp;\" : c === '\"' ? \"&quot;\" : \"&apos;\",\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, nativeIconSet, NATIVE_BACKGROUND_REQUIRED } 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 pwa-icons --svg <file> --native <out dir> --background <#RRGGBB> (iOS AppIcon, Android adaptive, .ico, .icns)\";\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 // F021.12 — native sets. Background is required: a white symbol on a transparent\n // canvas renders as an empty icon (buzzalert, 25/10).\n const native = arg(\"native\");\n if (native !== undefined) {\n if (!svgFile) {\n console.error(`pwa-icons: --native needs --svg <file>\\n${USAGE}`);\n return 1;\n }\n const background = arg(\"background\");\n if (!background) {\n console.error(`pwa-icons: --native needs --background <#RRGGBB> — ${NATIVE_BACKGROUND_REQUIRED}`);\n return 1;\n }\n let files;\n try {\n files = await nativeIconSet(readFileSync(svgFile, \"utf8\"), { background });\n } catch (e) {\n console.error(`pwa-icons: ${(e as Error).message}`);\n return 1;\n }\n for (const f of files) {\n mkdirSync(dirname(join(native, f.path)), { recursive: true });\n writeFileSync(join(native, f.path), f.content);\n }\n console.log(JSON.stringify({ native: files.map((f) => f.path) }, null, 2));\n return 0;\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"]}