Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/server/src/project/Layers/ProjectFaviconResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
}),
);

it.effect("resolves SvelteKit app icons from src/app.html into static assets", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(
cwd,
"src/app.html",
'<link rel="icon" href="%sveltekit.assets%/brand/logo.svg">',
);
yield* writeTextFile(cwd, "static/brand/logo.svg", "<svg>brand</svg>");

const resolved = yield* resolver.resolvePath(cwd);

expect(resolved).not.toBeNull();
expect(resolved).toContain("static/brand/logo.svg");
}),
);

it.effect("returns null when no icon is present", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
Expand Down
32 changes: 28 additions & 4 deletions apps/server/src/project/Layers/ProjectFaviconResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ const FAVICON_CANDIDATES = [
"assets/icon.png",
"assets/logo.svg",
"assets/logo.png",
"static/favicon.svg",
"static/favicon.ico",
"static/favicon.png",
"static/icon.svg",
"static/icon.png",
"static/logo.svg",
"static/logo.png",
] as const;

// Files that may contain a <link rel="icon"> or icon metadata declaration.
Expand All @@ -38,6 +45,8 @@ const ICON_SOURCE_FILES = [
"app/root.tsx",
"src/root.tsx",
"src/index.html",
"src/app.html",
"app.html",
Comment thread
cursor[bot] marked this conversation as resolved.
] as const;

// Matches <link ...> tags or object-like icon metadata where rel/href can appear in any order.
Expand All @@ -58,9 +67,24 @@ export const makeProjectFaviconResolver = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;

const resolveIconHref = (projectCwd: string, href: string): string[] => {
const clean = href.replace(/^\//, "");
return [path.join(projectCwd, "public", clean), path.join(projectCwd, clean)];
const resolveIconHref = (projectCwd: string, sourcePath: string, href: string): string[] => {
if (/^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith("data:")) {
return [];
}

const normalizedHref = href.replace(/^%sveltekit\.assets%/, "");
const clean = normalizedHref.replace(/^\/+/, "").replace(/^\.\//, "");
const projectCandidates = [
path.join(projectCwd, "public", clean),
path.join(projectCwd, "static", clean),
path.join(projectCwd, clean),
];

if (href.startsWith("/") || href.startsWith("%sveltekit.assets%")) {
return projectCandidates;
}

return [path.join(path.dirname(sourcePath), clean), ...projectCandidates];
};

const isPathWithinProject = (projectCwd: string, candidatePath: string): boolean => {
Expand Down Expand Up @@ -109,7 +133,7 @@ export const makeProjectFaviconResolver = Effect.gen(function* () {
if (!href) {
continue;
}
const existing = yield* findExistingFile(cwd, resolveIconHref(cwd, href));
const existing = yield* findExistingFile(cwd, resolveIconHref(cwd, sourcePath, href));
if (existing) {
return existing;
}
Expand Down
21 changes: 21 additions & 0 deletions apps/server/src/projectFaviconRoute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,27 @@ describe("tryHandleProjectFaviconRequest", () => {
});
});

it("resolves SvelteKit src/app.html icon hrefs from static assets", async () => {
const projectDir = makeTempDir("t3code-favicon-route-sveltekit-static-");
const iconPath = path.join(projectDir, "static", "brand", "app.svg");
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
fs.mkdirSync(path.join(projectDir, "src"), { recursive: true });
fs.writeFileSync(
path.join(projectDir, "src", "app.html"),
'<link rel="icon" href="%sveltekit.assets%/brand/app.svg">',
"utf8",
);
fs.writeFileSync(iconPath, "<svg>sveltekit-static</svg>", "utf8");

await withRouteServer(async (baseUrl) => {
const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`;
const response = await request(baseUrl, pathname);
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toBe("<svg>sveltekit-static</svg>");
});
});

it("serves a fallback favicon when no icon exists", async () => {
const projectDir = makeTempDir("t3code-favicon-route-fallback-");

Expand Down
Loading