Skip to content

fix: improve project favicon detection for monorepos#1024

Open
ponbac wants to merge 1 commit intopingdotgg:mainfrom
ponbac:favicon-recursive
Open

fix: improve project favicon detection for monorepos#1024
ponbac wants to merge 1 commit intopingdotgg:mainfrom
ponbac:favicon-recursive

Conversation

@ponbac
Copy link
Copy Markdown

@ponbac ponbac commented Mar 13, 2026

The XXL tag seems a little weird for this PR? It is a net +288 LoC, excluding test files.

What Changed

Improve project favicon detection without introducing a broad recursive search.
Closes #1020.

Rules, in order:

  • Check well-known favicon paths in the requested project root.
    Example: /repo/favicon.svg or /repo/public/favicon.ico.
  • If none exist, look for <link rel="icon" ... href="..."> tags or object-style { href, rel: "icon" } declarations in a small set of common source files, then resolve that target.
    Example: /repo/index.html points to /brand/logo.svg, so the route first tries /repo/public/brand/logo.svg and then /repo/brand/logo.svg.
  • If still nothing is found, repeat the same checks for first-level children of apps/ and packages/, plus other top-level directories in the requested root, using the same non-git directory ignore rules as workspaceEntries.
    Example: /repo/apps/web/public/favicon.png or /repo/frontend/public/favicon.png is found when the cwd is /repo.
  • Prefer a root match over nested workspace matches.
    Example: /repo/favicon.svg wins over /repo/apps/web/public/favicon.ico.
  • Fall back to the existing generated SVG when nothing is found.

I also extracted the shared gitignore probing into apps/server/src/gitIgnore.ts so both workspaceEntries and projectFaviconRoute use the same chunked git check-ignore --no-index -z --stdin filtering. I also extracted the shared workspace directory ignore policy into apps/server/src/workspaceIgnore.ts, so projectFaviconRoute now uses the same non-git skip rules as workspaceEntries when scanning nested roots instead of keeping a separate ignore list.

Why

The current behavior misses common monorepo layouts where the favicon lives under an app directory instead of the repo root.

There was already an earlier PR for this in the upstream repo: #690. That version was larger and messier. This keeps the rules simple on purpose, leaving more extensive changes to project icons for trusted maintainers.

UI Changes

Not applicable.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Improve project favicon detection to support monorepos with gitignore filtering

  • Rewrites ProjectFaviconResolver.resolvePath to search nested apps/, packages/, and top-level child directories when no root favicon is found, enabling monorepo support.
  • Adds gitignore-aware filtering via new gitIgnore.ts utilities (filterGitIgnoredPaths, isInsideGitWorkTree) that stream candidates to git check-ignore in 256 KiB chunks.
  • Introduces workspaceIgnore.ts to skip well-known directories (.git, node_modules, .next, dist, etc.) during directory traversal.
  • Adds workspaceEntries.ts for a cacheable, git-backed or filesystem-fallback workspace index with fuzzy-ranked search.
  • Fixes a missing FileSystem service injection in the static file handler in wsServer.ts that previously caused runtime errors.
  • Behavioral Change: resolver now fails open on filesystem or git errors (treats unreadable paths as misses) rather than throwing.
📊 Macroscope summarized 7404ccc. 7 files reviewed, 2 issues evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues


Note

Medium Risk
Modifies server-side favicon resolution and adds git-driven ignore filtering/search across workspace roots, which could affect file discovery performance and edge cases around path/permission handling. Git/process failures are handled fail-open, reducing risk of hiding files but still changing behavior.

Overview
Project favicon resolution is expanded to better support monorepos. The resolver now checks well-known favicon paths and icon metadata in common source files in the requested root, then repeats the same checks across first-level apps/, packages/, and other top-level directories (skipping ignored workspace dirs), preferring root matches before falling back to the generated SVG.

Gitignore-aware filtering is added and shared. New gitIgnore.ts provides isInsideGitWorkTree and chunked filterGitIgnoredPaths (git check-ignore --no-index -z --stdin) with fail-open behavior; both the favicon resolver and the new workspaceEntries.ts use this to avoid considering gitignored candidates.

Testing and wiring updates. New unit tests cover gitignore chunking/fail-open and expanded favicon route scenarios (nested roots, unreadable files, gitignored root/app/source cases), and wsServer.ts now provides FileSystem to the static-file handler Effect pipeline.

Written by Cursor Bugbot for commit 7404ccc. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 13, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1d0c0481-2c6f-4031-889c-b7db1c6902ee

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Mar 13, 2026
@ponbac ponbac force-pushed the favicon-recursive branch 2 times, most recently from 30cac2c to b936f17 Compare March 13, 2026 13:47
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4a8d330eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ponbac ponbac force-pushed the favicon-recursive branch 2 times, most recently from c396c88 to 551d9f2 Compare March 14, 2026 08:09
@eddieajau
Copy link
Copy Markdown

My 2c

getIgnore.ts should be separated into individual commands for isInsideGitWorkTree, filterGitIgnoredPaths. You've done the right think pulling it out, but it could go further.

I would add unit tests for this file rather than relying on integration tests.

Since this is a new file, I would also add some solid docblocks to provide good human and AI context for the decisions being made. Hopefully the reviewers will pick that up and think it's a good idea for their code standards.

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Mar 14, 2026
@ponbac
Copy link
Copy Markdown
Author

ponbac commented Mar 14, 2026

My 2c

getIgnore.ts should be separated into individual commands for isInsideGitWorkTree, filterGitIgnoredPaths. You've done the right think pulling it out, but it could go further.

I would add unit tests for this file rather than relying on integration tests.

Since this is a new file, I would also add some solid docblocks to provide good human and AI context for the decisions being made. Hopefully the reviewers will pick that up and think it's a good idea for their code standards.

Not sure about separating them right now, but I also feel like isInsideGitWorkTree() should live somewhere else. It does not really have anything to do with .gitignore.

I agree with the unit tests, added now.

Also added an explanatory comment to the filterGitIgnoredPaths(). I should maybe add additional ones, but I am not sure what is preferred here.

@ponbac ponbac force-pushed the favicon-recursive branch 10 times, most recently from a079d93 to 470ab3d Compare March 20, 2026 20:46
@ponbac ponbac force-pushed the favicon-recursive branch 2 times, most recently from 9f02bfb to 8cc9e19 Compare March 24, 2026 07:11
@github-actions github-actions bot added size:XL 500-999 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Mar 24, 2026
@ponbac ponbac force-pushed the favicon-recursive branch 3 times, most recently from 47c7166 to b41ebd3 Compare March 25, 2026 15:13
@ponbac ponbac force-pushed the favicon-recursive branch 4 times, most recently from ea10d7f to 49c8309 Compare March 30, 2026 06:31
// and nested scans only hit `git check-ignore` once per candidate.
const allowedRelativePaths = yield* Effect.promise(() =>
filterGitIgnoredPaths(projectRoot, uncachedRelativePaths),
).pipe(Effect.orElseSucceed(() => uncachedRelativePaths));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

orElseSucceed cannot catch Effect.promise defects

Low Severity

Effect.promise converts promise rejections into defects (unchecked exceptions), but Effect.orElseSucceed only catches expected (typed) errors. The fallback () => uncachedRelativePaths is unreachable — if filterGitIgnoredPaths ever rejects, the defect propagates instead of gracefully falling back. Compare with the isInsideGitWorkTree call at line 392–393, which correctly uses .catch(() => false) on the promise itself before wrapping with Effect.promise. Using Effect.tryPromise here instead of Effect.promise would make orElseSucceed actually effective.

Fix in Cursor Fix in Web

@ponbac ponbac force-pushed the favicon-recursive branch from 49c8309 to 09da607 Compare March 31, 2026 06:35
@ponbac ponbac force-pushed the favicon-recursive branch from 09da607 to 7404ccc Compare April 1, 2026 06:02
return nextPromise;
}

export function clearWorkspaceIndexCache(cwd: string): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low src/workspaceEntries.ts:425

clearWorkspaceIndexCache deletes entries from workspaceIndexCache and inFlightWorkspaceIndexBuilds, but any in-flight buildWorkspaceIndex promise (already spawned before the clear) will still execute its .then() handler and repopulate workspaceIndexCache after the function returns. This means a caller who clears the cache to force a fresh rebuild may unexpectedly receive stale data from the completing in-flight build instead of a new index.

🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/workspaceEntries.ts around line 425:

`clearWorkspaceIndexCache` deletes entries from `workspaceIndexCache` and `inFlightWorkspaceIndexBuilds`, but any in-flight `buildWorkspaceIndex` promise (already spawned before the clear) will still execute its `.then()` handler and repopulate `workspaceIndexCache` after the function returns. This means a caller who clears the cache to force a fresh rebuild may unexpectedly receive stale data from the completing in-flight build instead of a new index.

Evidence trail:
- apps/server/src/workspaceEntries.ts lines 408-421 (REVIEWED_COMMIT): Promise creation with `.then()` handler that unconditionally sets `workspaceIndexCache.set(cwd, next)`
- apps/server/src/workspaceEntries.ts lines 425-428 (REVIEWED_COMMIT): `clearWorkspaceIndexCache` deletes from maps but cannot cancel the running promise
- apps/server/src/workspaceEntries.ts lines 34-35 (REVIEWED_COMMIT): Cache definitions as module-level Maps

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

entries: rankedEntries.map((candidate) => candidate.entry),
truncated: index.truncated || matchedEntryCount > limit,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entire new file is dead code, never imported

Medium Severity

The entire 453-line workspaceEntries.ts file is dead code. Its exports clearWorkspaceIndexCache and searchWorkspaceEntries are not imported anywhere in the codebase. The file duplicates nearly all logic from apps/server/src/workspace/Layers/WorkspaceEntries.ts (the Effect-based service version) using raw async functions instead. All existing consumers continue to use WorkspaceEntriesLive from workspace/Layers/WorkspaceEntries.ts.

Fix in Cursor Fix in Web

lookup.fileSystem.readFile(resolvedPath),
);
if (Option.isSome(fileOption)) {
return Option.some(fileOption.value.path);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Favicon probe reads entire file content unnecessarily

Low Severity

findFirstReadableFaviconPath calls readFileIfExists with readFile (full binary read) for each favicon candidate, but only uses the resulting path — the file content is discarded. Since resolveExistingPath already confirms the file exists via realPath and stat, the full content read is redundant. The route handler in projectFaviconRoute.ts then reads the same file again to serve it, doubling I/O for potentially large PNG/ICO files.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

monorepo projects do not resolve nested app favicons

2 participants