Skip to content
Open
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
101 changes: 101 additions & 0 deletions apps/server/src/gitIgnore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { runProcess } from "./processRunner";

const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;

function splitNullSeparatedPaths(input: string, truncated: boolean): string[] {
const parts = input.split("\0");
if (parts.length === 0) return [];
if (truncated && parts[parts.length - 1]?.length) {
parts.pop();
}
return parts.filter((value) => value.length > 0);
}

export async function isInsideGitWorkTree(cwd: string): Promise<boolean> {
const insideWorkTree = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], {
cwd,
allowNonZeroExit: true,
timeoutMs: 5_000,
maxBufferBytes: 4_096,
}).catch(() => null);

return Boolean(
insideWorkTree && insideWorkTree.code === 0 && insideWorkTree.stdout.trim() === "true",
);
}

export async function filterGitIgnoredPaths(
cwd: string,
relativePaths: readonly string[],
): Promise<string[]> {
if (relativePaths.length === 0) {
return [...relativePaths];
}

const ignoredPaths = new Set<string>();
let chunk: string[] = [];
let chunkBytes = 0;

const flushChunk = async (): Promise<boolean> => {
if (chunk.length === 0) {
return true;
}

const checkIgnore = await runProcess("git", ["check-ignore", "--no-index", "-z", "--stdin"], {
cwd,
allowNonZeroExit: true,
timeoutMs: 20_000,
maxBufferBytes: 16 * 1024 * 1024,
outputMode: "truncate",
stdin: `${chunk.join("\0")}\0`,
}).catch(() => null);
chunk = [];
chunkBytes = 0;

if (!checkIgnore) {
return false;
}

// git-check-ignore exits with 1 when no paths match.
if (checkIgnore.code !== 0 && checkIgnore.code !== 1) {
return false;
}

const matchedIgnoredPaths = splitNullSeparatedPaths(
checkIgnore.stdout,
Boolean(checkIgnore.stdoutTruncated),
);
for (const ignoredPath of matchedIgnoredPaths) {
ignoredPaths.add(ignoredPath);
}
return true;
};

for (const relativePath of relativePaths) {
const relativePathBytes = Buffer.byteLength(relativePath) + 1;
if (
chunk.length > 0 &&
chunkBytes + relativePathBytes > GIT_CHECK_IGNORE_MAX_STDIN_BYTES &&
!(await flushChunk())
) {
return [...relativePaths];
}

chunk.push(relativePath);
chunkBytes += relativePathBytes;

if (chunkBytes >= GIT_CHECK_IGNORE_MAX_STDIN_BYTES && !(await flushChunk())) {
return [...relativePaths];
}
}

if (!(await flushChunk())) {
return [...relativePaths];
}

if (ignoredPaths.size === 0) {
return [...relativePaths];
}

return relativePaths.filter((relativePath) => !ignoredPaths.has(relativePath));
}
230 changes: 172 additions & 58 deletions apps/server/src/projectFaviconRoute.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { Effect } from "effect";
import { afterEach, describe, expect, it } from "vitest";
import { tryHandleProjectFaviconRequest } from "./projectFaviconRoute";

Expand All @@ -20,14 +23,48 @@ function makeTempDir(prefix: string): string {
return dir;
}

function writeFile(filePath: string, contents: string): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, contents, "utf8");
}

function makeUnreadable(filePath: string): void {
fs.chmodSync(filePath, 0o000);
}

function runGit(cwd: string, args: readonly string[]): void {
execFileSync("git", args, {
cwd,
stdio: "ignore",
env: {
...process.env,
GIT_AUTHOR_NAME: "Test User",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test User",
GIT_COMMITTER_EMAIL: "test@example.com",
},
});
}

async function withRouteServer(run: (baseUrl: string) => Promise<void>): Promise<void> {
const server = http.createServer((req, res) => {
const url = new URL(req.url ?? "/", "http://127.0.0.1");
if (tryHandleProjectFaviconRequest(url, res)) {
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
void Effect.runPromise(
Effect.gen(function* () {
if (yield* tryHandleProjectFaviconRequest(url, res)) {
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
}).pipe(Effect.provide(NodeServices.layer)),
).catch((error) => {
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "text/plain" });
}
if (!res.writableEnded) {
res.end(error instanceof Error ? error.message : "Unhandled error");
}
});
});

await new Promise<void>((resolve, reject) => {
Expand Down Expand Up @@ -70,6 +107,22 @@ async function request(baseUrl: string, pathname: string): Promise<HttpResponse>
};
}

function requestProjectFavicon(baseUrl: string, projectDir: string): Promise<HttpResponse> {
return request(baseUrl, `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`);
}

function expectSvgResponse(response: HttpResponse, expectedBody: string): void {
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toBe(expectedBody);
}

function expectFallbackSvgResponse(response: HttpResponse): void {
expect(response.statusCode).toBe(200);
expect(response.contentType).toContain("image/svg+xml");
expect(response.body).toContain('data-fallback="project-favicon"');
}

describe("tryHandleProjectFaviconRequest", () => {
afterEach(() => {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
Expand All @@ -87,85 +140,146 @@ describe("tryHandleProjectFaviconRequest", () => {

it("serves a well-known favicon file from the project root", async () => {
const projectDir = makeTempDir("t3code-favicon-route-root-");
fs.writeFileSync(path.join(projectDir, "favicon.svg"), "<svg>favicon</svg>", "utf8");
writeFile(path.join(projectDir, "favicon.svg"), "<svg>favicon</svg>");

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>favicon</svg>");
expectSvgResponse(await requestProjectFavicon(baseUrl, projectDir), "<svg>favicon</svg>");
});
});

it("resolves icon href from source files when no well-known favicon exists", async () => {
const projectDir = makeTempDir("t3code-favicon-route-source-");
const iconPath = path.join(projectDir, "public", "brand", "logo.svg");
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
fs.writeFileSync(
path.join(projectDir, "index.html"),
it.each([
{
name: "resolves icon link when href appears before rel in HTML",
prefix: "t3code-favicon-route-html-order-",
sourcePath: ["index.html"],
sourceContents: '<link href="/brand/logo.svg" rel="icon">',
iconPath: ["public", "brand", "logo.svg"],
expectedBody: "<svg>brand-html-order</svg>",
},
{
name: "resolves object-style icon metadata when href appears before rel",
prefix: "t3code-favicon-route-obj-order-",
sourcePath: ["src", "root.tsx"],
sourceContents: 'const links = [{ href: "/brand/obj.svg", rel: "icon" }];',
iconPath: ["public", "brand", "obj.svg"],
expectedBody: "<svg>brand-obj-order</svg>",
},
])("$name", async ({ prefix, sourcePath, sourceContents, iconPath, expectedBody }) => {
const projectDir = makeTempDir(prefix);
writeFile(path.join(projectDir, ...sourcePath), sourceContents);
writeFile(path.join(projectDir, ...iconPath), expectedBody);

await withRouteServer(async (baseUrl) => {
expectSvgResponse(await requestProjectFavicon(baseUrl, projectDir), expectedBody);
});
});

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

await withRouteServer(async (baseUrl) => {
expectFallbackSvgResponse(await requestProjectFavicon(baseUrl, projectDir));
});
});

it("treats unreadable favicon probes as misses and continues searching", async () => {
const projectDir = makeTempDir("t3code-favicon-route-unreadable-probes-");
const unreadableFaviconPath = path.join(projectDir, "favicon.svg");
writeFile(unreadableFaviconPath, "<svg>blocked-root</svg>");
makeUnreadable(unreadableFaviconPath);
const unreadableSourcePath = path.join(projectDir, "index.html");
writeFile(unreadableSourcePath, '<link rel="icon" href="/brand/blocked.svg">');
makeUnreadable(unreadableSourcePath);
writeFile(
path.join(projectDir, "src", "root.tsx"),
'const links = [{ rel: "icon", href: "/brand/readable.svg" }];',
);
writeFile(
path.join(projectDir, "public", "brand", "readable.svg"),
"<svg>readable-from-source</svg>",
);

await withRouteServer(async (baseUrl) => {
expectSvgResponse(
await requestProjectFavicon(baseUrl, projectDir),
"<svg>readable-from-source</svg>",
);
});
});

it("finds a nested app favicon from source metadata when cwd is a monorepo root", async () => {
const projectDir = makeTempDir("t3code-favicon-route-monorepo-source-");
writeFile(
path.join(projectDir, "apps", "frontend", "index.html"),
'<link rel="icon" href="/brand/logo.svg">',
);
fs.writeFileSync(iconPath, "<svg>brand</svg>", "utf8");
writeFile(
path.join(projectDir, "apps", "frontend", "public", "brand", "logo.svg"),
"<svg>nested-app</svg>",
);

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>brand</svg>");
expectSvgResponse(await requestProjectFavicon(baseUrl, projectDir), "<svg>nested-app</svg>");
});
});

it("resolves icon link when href appears before rel in HTML", async () => {
const projectDir = makeTempDir("t3code-favicon-route-html-order-");
const iconPath = path.join(projectDir, "public", "brand", "logo.svg");
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
fs.writeFileSync(
path.join(projectDir, "index.html"),
'<link href="/brand/logo.svg" rel="icon">',
it("skips nested search roots that workspace entries ignore", async () => {
const projectDir = makeTempDir("t3code-favicon-route-ignored-search-root-");
writeFile(path.join(projectDir, ".next", "public", "favicon.svg"), "<svg>ignored-next</svg>");

await withRouteServer(async (baseUrl) => {
expectFallbackSvgResponse(await requestProjectFavicon(baseUrl, projectDir));
});
});

it("prefers a root favicon over nested workspace matches", async () => {
const projectDir = makeTempDir("t3code-favicon-route-root-priority-");
writeFile(path.join(projectDir, "favicon.svg"), "<svg>root-first</svg>");
writeFile(path.join(projectDir, "apps", "frontend", "public", "favicon.ico"), "nested-ico");

await withRouteServer(async (baseUrl) => {
expectSvgResponse(await requestProjectFavicon(baseUrl, projectDir), "<svg>root-first</svg>");
});
});

it("skips a gitignored nested app directory", async () => {
const projectDir = makeTempDir("t3code-favicon-route-gitignored-app-");
runGit(projectDir, ["init"]);
writeFile(path.join(projectDir, ".gitignore"), "apps/frontend/\n");
writeFile(
path.join(projectDir, "apps", "frontend", "public", "favicon.svg"),
"<svg>ignored-app</svg>",
);
fs.writeFileSync(iconPath, "<svg>brand-html-order</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>brand-html-order</svg>");
expectFallbackSvgResponse(await requestProjectFavicon(baseUrl, projectDir));
});
});

it("resolves object-style icon metadata when href appears before rel", async () => {
const projectDir = makeTempDir("t3code-favicon-route-obj-order-");
const iconPath = path.join(projectDir, "public", "brand", "obj.svg");
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
fs.mkdirSync(path.join(projectDir, "src"), { recursive: true });
fs.writeFileSync(
path.join(projectDir, "src", "root.tsx"),
'const links = [{ href: "/brand/obj.svg", rel: "icon" }];',
"utf8",
it("skips a gitignored root favicon and falls through to a nested app", async () => {
const projectDir = makeTempDir("t3code-favicon-route-gitignored-root-");
runGit(projectDir, ["init"]);
writeFile(path.join(projectDir, ".gitignore"), "/favicon.svg\n");
writeFile(path.join(projectDir, "favicon.svg"), "<svg>ignored-root</svg>");
writeFile(
path.join(projectDir, "apps", "frontend", "public", "favicon.svg"),
"<svg>nested-kept</svg>",
);
fs.writeFileSync(iconPath, "<svg>brand-obj-order</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>brand-obj-order</svg>");
expectSvgResponse(await requestProjectFavicon(baseUrl, projectDir), "<svg>nested-kept</svg>");
});
});

it("serves a fallback favicon when no icon exists", async () => {
const projectDir = makeTempDir("t3code-favicon-route-fallback-");
it("skips a gitignored source file when resolving icon metadata", async () => {
const projectDir = makeTempDir("t3code-favicon-route-gitignored-source-");
runGit(projectDir, ["init"]);
writeFile(path.join(projectDir, ".gitignore"), "index.html\n");
writeFile(path.join(projectDir, "index.html"), '<link rel="icon" href="/brand/logo.svg">');
writeFile(path.join(projectDir, "public", "brand", "logo.svg"), "<svg>ignored-source</svg>");

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).toContain('data-fallback="project-favicon"');
expectFallbackSvgResponse(await requestProjectFavicon(baseUrl, projectDir));
});
});
});
Loading