fix(brand): close Task 3 spec gaps surfaced by code review

Three issues caught by the code review of scripts/gen-icons.mjs:

(1) Missing source SVG validation. Spec requires the script "refuses
to run if any source SVG is missing." Added an existsSync guard at the
top of main() that throws a targeted message rather than letting sharp
fail with a raw ENOENT.

(2) Summary table missing byte sizes. Spec says the summary prints
"each output, its size in pixels, its file size in bytes." trace() now
captures statSync().size; the printout has a size column with
human-readable formatting and a total at the end.

(3) writeMaskablePng's intermediate buffer used .png() without the
compressionLevel/palette options that every other render uses. Zero
functional impact (the buffer is only piped into composite, never
written to disk) but inconsistent and misleading. Aligned with the
rest of the script.

README: replaced 'git add -A' with explicit directory paths matching
the project's convention — the generator writes to a fixed set of
three directories and shouldn't accidentally stage unrelated
working-tree changes during a regen.

Plan updated to match the corrected code so Task 3's source-of-truth
stays consistent.
This commit is contained in:
Jannis Braun
2026-04-27 14:29:05 +02:00
parent bdfd38bc90
commit 81ad0292a5
2 changed files with 31 additions and 8 deletions
+3 -1
View File
@@ -15,10 +15,12 @@ the same PR.
```bash
pnpm gen-icons
git status # review which files changed
git add -A
git add packages/desktop/build/ packages/desktop/resources/ packages/web/public/icons/
git commit -m "chore: regenerate brand icons"
```
(Stage explicit paths rather than `git add -A` — the generator only writes to those three directories, and an unrelated working-tree change shouldn't accidentally land in a "regenerate icons" commit.)
## Determinism
Output is byte-stable for a given lockfile. The same SVGs in produce the
+28 -7
View File
@@ -22,7 +22,7 @@
import { fileURLToPath } from 'node:url';
import { dirname, join, relative } from 'node:path';
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'node:fs';
import sharp from 'sharp';
import pngToIco from 'png-to-ico';
import png2icons from 'png2icons';
@@ -102,7 +102,7 @@ async function writeMaskablePng(path, markSvg, canvas, scale, bgHex) {
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.png()
.png({ compressionLevel: 9, palette: false })
.toBuffer();
mkdirSync(dirname(path), { recursive: true });
const composed = await sharp({
@@ -122,13 +122,28 @@ async function writeMaskablePng(path, markSvg, canvas, scale, bgHex) {
// ---- main ----
async function main() {
// Spec: refuse to run if any source SVG is missing — fail loudly, not on
// a downstream sharp error with a cryptic ENOENT.
for (const [, path] of Object.entries(SRC)) {
if (!existsSync(path)) {
throw new Error(
`Missing source SVG: ${relative(ROOT, path)} — copy from Artworks-Backspace/SVG/`,
);
}
}
const appIcon = loadSvg(SRC.appIcon);
const mark = loadSvg(SRC.mark);
const markMonoDark = loadSvg(SRC.markMonoDark);
const written = [];
const trace = (label, path, info) =>
written.push({ label, info, path: relative(ROOT, path) });
written.push({
label,
info,
bytes: statSync(path).size,
path: relative(ROOT, path),
});
// --- Desktop: application icon ---
const linuxSizes = [16, 32, 48, 64, 128, 256, 512, 1024];
@@ -189,13 +204,19 @@ async function main() {
trace('in-app-logo', join(WEB_ICONS, 'logo.png'), '256 (transparent)');
// --- Summary ---
const fmtBytes = (n) => {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
};
console.log('\nGenerated icons:');
console.log(' ' + 'kind'.padEnd(14) + 'info'.padEnd(30) + 'path');
console.log(' ' + '----'.padEnd(14) + '----'.padEnd(30) + '----');
console.log(' ' + 'kind'.padEnd(14) + 'info'.padEnd(30) + 'size'.padStart(10) + ' path');
console.log(' ' + '----'.padEnd(14) + '----'.padEnd(30) + '----'.padStart(10) + ' ----');
for (const r of written) {
console.log(' ' + r.label.padEnd(14) + r.info.padEnd(30) + r.path);
console.log(' ' + r.label.padEnd(14) + r.info.padEnd(30) + fmtBytes(r.bytes).padStart(10) + ' ' + r.path);
}
console.log(`\n${written.length} files written.`);
const totalBytes = written.reduce((sum, r) => sum + r.bytes, 0);
console.log(`\n${written.length} files written, ${fmtBytes(totalBytes)} total.`);
}
main().catch((e) => {