149 lines · 4.3 KB · TypeScript
RawDownload
import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path"; import {  ProjectFaviconResolver,  type ProjectFaviconResolverShape,} from "../Services/ProjectFaviconResolver.ts";import { WorkspacePaths } from "../../workspace/Services/WorkspacePaths.ts"; // Well-known favicon paths checked in order.const FAVICON_CANDIDATES = [  "favicon.svg",  "favicon.ico",  "favicon.png",  "public/favicon.svg",  "public/favicon.ico",  "public/favicon.png",  "app/favicon.ico",  "app/favicon.png",  "app/icon.svg",  "app/icon.png",  "app/icon.ico",  "src/favicon.ico",  "src/favicon.svg",  "src/app/favicon.ico",  "src/app/icon.svg",  "src/app/icon.png",  "assets/icon.svg",  "assets/icon.png",  "assets/logo.svg",  "assets/logo.png",  ".idea/icon.svg",] as const; // Files that may contain a <link rel="icon"> or icon metadata declaration.const ICON_SOURCE_FILES = [  "index.html",  "public/index.html",  "app/routes/__root.tsx",  "src/routes/__root.tsx",  "app/root.tsx",  "src/root.tsx",  "src/index.html",] as const; // Matches <link ...> tags or object-like icon metadata where rel/href can appear in any order.const LINK_ICON_HTML_RE =  /<link\b(?=[^>]*\brel=["'](?:icon|shortcut icon)["'])(?=[^>]*\bhref=["']([^"'?]+))[^>]*>/i;const LINK_ICON_OBJ_RE =  /(?=[^}]*\brel\s*:\s*["'](?:icon|shortcut icon)["'])(?=[^}]*\bhref\s*:\s*["']([^"'?]+))[^}]*/i; function extractIconHref(source: string): string | null {  const htmlMatch = source.match(LINK_ICON_HTML_RE);  if (htmlMatch?.[1]) return htmlMatch[1];  const objMatch = source.match(LINK_ICON_OBJ_RE);  if (objMatch?.[1]) return objMatch[1];  return null;} export const makeProjectFaviconResolver = Effect.gen(function* () {  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const workspacePaths = yield* WorkspacePaths;   const resolveIconHref = (href: string): string[] => {    const clean = href.replace(/^\//, "");    return [path.join("public", clean), clean];  };   const findExistingFile = Effect.fn("ProjectFaviconResolver.findExistingFile")(function* (    projectCwd: string,    relativeCandidates: ReadonlyArray<string>,  ): Effect.fn.Return<string | null> {    for (const relativePath of relativeCandidates) {      const candidate = yield* workspacePaths        .resolveRelativePathWithinRoot({          workspaceRoot: projectCwd,          relativePath,        })        .pipe(Effect.orElseSucceed(() => null));      if (!candidate) {        continue;      }      const stats = yield* fileSystem        .stat(candidate.absolutePath)        .pipe(Effect.orElseSucceed(() => null));      if (stats?.type === "File") {        return candidate.absolutePath;      }    }    return null;  });   const resolvePath: ProjectFaviconResolverShape["resolvePath"] = Effect.fn(    "ProjectFaviconResolver.resolvePath",  )(function* (cwd: string): Effect.fn.Return<string | null> {    const projectCwd = yield* workspacePaths      .normalizeWorkspaceRoot(cwd)      .pipe(Effect.orElseSucceed(() => null));    if (!projectCwd) {      return null;    }    for (const candidate of FAVICON_CANDIDATES) {      const existing = yield* findExistingFile(projectCwd, [candidate]);      if (existing) {        return existing;      }    }     for (const sourceFile of ICON_SOURCE_FILES) {      const sourcePath = yield* workspacePaths        .resolveRelativePathWithinRoot({          workspaceRoot: projectCwd,          relativePath: sourceFile,        })        .pipe(Effect.orElseSucceed(() => null));      if (!sourcePath) {        continue;      }      const source = yield* fileSystem        .readFileString(sourcePath.absolutePath)        .pipe(Effect.orElseSucceed(() => null));      if (!source) {        continue;      }      const href = extractIconHref(source);      if (!href) {        continue;      }      const existing = yield* findExistingFile(projectCwd, resolveIconHref(href));      if (existing) {        return existing;      }    }     return null;  });   return {    resolvePath,  } satisfies ProjectFaviconResolverShape;}); export const ProjectFaviconResolverLive = Layer.effect(  ProjectFaviconResolver,  makeProjectFaviconResolver,);