fix: declare toolbar icons and ship real PNGs in both archives (closes #371)
All checks were successful
check / check (push) Successful in 32s
e2e / e2e-chrome (push) Successful in 1m44s
e2e / e2e-firefox (push) Successful in 31s

Neither manifest declared an `icons` block, so Chrome and Firefox both drew the
generic puzzle piece for this extension. That is the first thing the owner sees
on every launch, and an unbranded placeholder is also how a user fails to tell a
real extension from a look-alike.

Both manifests now declare 16/32/48/128 as `icons/icon<size>.png`, and the four
PNGs live at `icons/` in the tree. build.js copies them into each browser
directory relative to it, so nothing points up and out the way `dist/styles.css`
does, and the receipt records them like every other emitted file.

The sizes copied come from the manifest that will ship next to them, not from a
second list in build.js: a size a manifest declares and `icons/` does not hold
fails the build with the path that is missing, rather than emitting a directory
whose manifest references nothing. script/lib/package.js already resolved `.png`
strings, so an icon that reached a manifest but not the archive fails
self-containment; tests/packaging.test.js now pins that case, since it was
covered only incidentally before.

tests/manifest.test.js asserts the declaration in both manifests, that both
declare the same set, and that each referenced file is a PNG whose IHDR states
the size the entry claims — a declaration alone would still permit a reference
to a file that is not there or is not an image.

The artwork is original, drawn from geometry rather than traced or downloaded: a
flat dark-navy rounded square (#101A2E) with a teal (#35E0C2) triangular "A" —
one outer triangle minus a triangular counter — rasterised with 8x8 supersampling
and encoded as RGBA PNG. One shape, two flat colours, which is what a 16px
toolbar slot can carry.

Verified: make check 56 suites / 1023 tests, make build and make package green,
both archives unpacked and the four icons confirmed inside each with bytes
identical to the tree, make test-e2e 55/55 and 5/5, make test-e2e-firefox 8/8 and
7/7 (the latter installs the packaged XPI). Removing icons/icon48.png fails
make build; making build.js skip one copy fails make package with "the chrome
archive would not be self-contained: it is told to load icons/icon48.png, which
is not in it".
This commit is contained in:
2026-08-23 19:15:42 +00:00
parent 75a5fa9891
commit d0e6758bd8
10 changed files with 144 additions and 0 deletions

View File

@@ -51,6 +51,17 @@ const RECEIPT_ENV = "AUTISTMASK_BUILD_RECEIPT";
// rather than writing a receipt that cannot be checked.
const SAFE_EMITTED_PATH = /^dist\/[A-Za-z0-9._][A-Za-z0-9._/-]*$/;
// Where each browser directory's manifest comes from, and — through its
// "icons" — which image files ship inside that directory.
const MANIFEST_SOURCES = new Map([
[DIST_CHROME, path.join(__dirname, "manifest", "chrome.json")],
[DIST_FIREFOX, path.join(__dirname, "manifest", "firefox.json")],
]);
// What an "icons" entry may name: a plain file under icons/, so a manifest
// value is never joined into a path that leaves the repo.
const ICON_REF_RE = /^icons\/[A-Za-z0-9._-]+\.png$/;
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
@@ -257,6 +268,42 @@ function copyEmitted(src, dest) {
recordEmitted(dest);
}
// Copy the icons one browser directory ships. The sizes come from the manifest
// that will sit next to them, not from a second list here: a size the manifest
// declares and icons/ does not hold fails the build, rather than shipping a
// manifest whose reference resolves to nothing. Relative to the browser
// directory, so nothing points up and out of it the way dist/styles.css does.
function copyIcons(distDir) {
const manifestPath = MANIFEST_SOURCES.get(distDir);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const refs = Object.values(manifest.icons || {});
if (refs.length === 0) {
throw new Error(
`${repoRelative(manifestPath)} declares no icons, so the browser ` +
`renders a generic placeholder for this extension`,
);
}
for (const ref of refs) {
if (!ICON_REF_RE.test(ref)) {
throw new Error(
`${repoRelative(manifestPath)} declares icon ` +
`${JSON.stringify(ref)}, which is not a plain file under ` +
`icons/`,
);
}
const src = path.join(__dirname, ref);
if (!fs.existsSync(src)) {
throw new Error(
`${repoRelative(manifestPath)} declares ${ref}, which is not ` +
`in this tree`,
);
}
const dest = path.join(distDir, ref);
ensureDir(path.dirname(dest));
copyEmitted(src, dest);
}
}
function sha256File(absPath) {
return crypto
.createHash("sha256")
@@ -524,6 +571,8 @@ async function build() {
tailwindOutput,
path.join(distDir, "src", "popup", "styles.css"),
);
copyIcons(distDir);
}
// copy manifests