fix(server): prefer portable project icons

Project icon discovery could select an ICO that native clients cannot decode even when a renderable SVG or PNG was available. Keep ICO as the final fallback and recognize common sized icon names.
This commit is contained in:
Mohamed Boudra
2026-08-13 19:34:37 +02:00
parent a4b92768d9
commit 52e4060d76
2 changed files with 34 additions and 7 deletions
+23 -2
View File
@@ -125,11 +125,11 @@ describe("findProjectIcon", () => {
}); });
it("prioritizes favicon over logo", async () => { it("prioritizes favicon over logo", async () => {
writeFileSync(join(tempDir, "favicon.ico"), "favicon"); writeFileSync(join(tempDir, "favicon.png"), "favicon");
writeFileSync(join(tempDir, "logo.png"), "logo"); writeFileSync(join(tempDir, "logo.png"), "logo");
const result = await findProjectIcon(tempDir); const result = await findProjectIcon(tempDir);
expect(result).toBe(join(tempDir, "favicon.ico")); expect(result).toBe(join(tempDir, "favicon.png"));
}); });
it("prioritizes priority dirs over root", async () => { it("prioritizes priority dirs over root", async () => {
@@ -180,6 +180,14 @@ describe("findProjectIcon", () => {
expect(result).toBe(join(tempDir, "apple-touch-icon.png")); expect(result).toBe(join(tempDir, "apple-touch-icon.png"));
}); });
it("prefers an Apple touch PNG over ICO", async () => {
writeFileSync(join(tempDir, "favicon.ico"), "ico");
writeFileSync(join(tempDir, "apple-touch-icon.png"), "apple icon");
const result = await findProjectIcon(tempDir);
expect(result).toBe(join(tempDir, "apple-touch-icon.png"));
});
it("finds icon-*.png patterns", async () => { it("finds icon-*.png patterns", async () => {
writeFileSync(join(tempDir, "icon-192.png"), "192 icon"); writeFileSync(join(tempDir, "icon-192.png"), "192 icon");
@@ -187,6 +195,19 @@ describe("findProjectIcon", () => {
expect(result).toBe(join(tempDir, "icon-192.png")); expect(result).toBe(join(tempDir, "icon-192.png"));
}); });
it.each([
"favicon-32x32.png",
"apple-touch-icon-180x180.png",
"android-chrome-192x192.png",
"safari-pinned-tab.svg",
"mstile-150x150.png",
])("finds common sized icon %s", async (fileName) => {
writeFileSync(join(tempDir, fileName), "icon");
const result = await findProjectIcon(tempDir);
expect(result).toBe(join(tempDir, fileName));
});
it("handles non-existent directory gracefully", async () => { it("handles non-existent directory gracefully", async () => {
const result = await findProjectIcon(join(tempDir, "nonexistent")); const result = await findProjectIcon(join(tempDir, "nonexistent"));
expect(result).toBeNull(); expect(result).toBeNull();
+11 -5
View File
@@ -5,25 +5,31 @@ import { extname, join } from "path";
* Icon file patterns to search for, in priority order. * Icon file patterns to search for, in priority order.
* Patterns starting with '*' are glob patterns (e.g., icon-*.png). * Patterns starting with '*' are glob patterns (e.g., icon-*.png).
* *
* Within each name, SVG and PNG outrank ICO: every client renders them (React * SVG and PNG outrank ICO across all known names: every client renders them
* Native's <Image> cannot decode ICO at all) and they scale better. ICO is the * and they scale better. ICO is the final fallback because native clients
* last resort for each name, not the first. * cannot decode containers whose frames use the legacy bitmap format.
*/ */
export const ICON_PATTERNS = [ export const ICON_PATTERNS = [
"favicon.svg", "favicon.svg",
"favicon.png", "favicon.png",
"favicon.ico", "favicon-*.svg",
"favicon-*.png",
"favico.svg", "favico.svg",
"favico.png", "favico.png",
"favico.ico",
"icon.svg", "icon.svg",
"icon.png", "icon.png",
"app-icon.svg", "app-icon.svg",
"app-icon.png", "app-icon.png",
"apple-touch-icon.png", "apple-touch-icon.png",
"apple-touch-icon-*.png",
"icon-*.png", "icon-*.png",
"android-chrome-*.png",
"safari-pinned-tab.svg",
"mstile-*.png",
"logo.svg", "logo.svg",
"logo.png", "logo.png",
"favicon.ico",
"favico.ico",
]; ];
/** /**