Skip to content
Draft
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
86 changes: 81 additions & 5 deletions apps/server/src/project/Layers/ProjectFaviconResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { it, describe, expect } from "@effect/vitest";
import { Effect, FileSystem, Layer, Path } from "effect";

import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
import * as VcsProcess from "../../vcs/VcsProcess.ts";
import { ProjectFaviconResolver } from "../Services/ProjectFaviconResolver.ts";
import { ProjectFaviconResolverLive } from "./ProjectFaviconResolver.ts";

const TestLayer = Layer.empty.pipe(
Layer.provideMerge(ProjectFaviconResolverLive),
Layer.provideMerge(VcsProcess.layer),
Layer.provideMerge(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer))),
Layer.provideMerge(NodeServices.layer),
);

const makeTempDir = Effect.gen(function* () {
const makeTempDir = Effect.fn(function* (opts?: { git?: boolean }) {
const fileSystem = yield* FileSystem.FileSystem;
return yield* fileSystem.makeTempDirectoryScoped({
const dir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3code-project-favicon-",
});
if (opts?.git) {
yield* git(dir, ["init"]);
}
return dir;
});

const writeTextFile = Effect.fn("writeTextFile")(function* (
Expand All @@ -31,12 +39,24 @@ const writeTextFile = Effect.fn("writeTextFile")(function* (
yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);
});

const git = (cwd: string, args: ReadonlyArray<string>) =>
Effect.gen(function* () {
const process = yield* VcsProcess.VcsProcess;
yield* process.run({
operation: "ProjectFaviconResolver.test.git",
command: "git",
cwd,
args,
timeoutMs: 10_000,
});
});

it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
describe("resolvePath", () => {
it.effect("prefers well-known favicon files", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "favicon.svg", "<svg>favicon</svg>");

const resolved = yield* resolver.resolvePath(cwd);
Expand All @@ -49,7 +69,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
it.effect("resolves icon hrefs from project source files", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "index.html", '<link rel="icon" href="/brand/logo.svg">');
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");

Expand All @@ -63,12 +83,68 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
it.effect("returns null when no icon is present", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir;
const cwd = yield* makeTempDir();

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

expect(resolved).toBeNull();
}),
);

it.effect("finds nested app favicon metadata from a monorepo root", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "apps/web/index.html", '<link rel="icon" href="/icons/app.svg">');
yield* writeTextFile(cwd, "apps/web/public/icons/app.svg", "<svg>app</svg>");

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

expect(resolved).not.toBeNull();
expect(resolved).toContain("apps/web/public/icons/app.svg");
}),
);

it.effect("prefers a root favicon over nested workspace matches", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "favicon.svg", "<svg>root</svg>");
yield* writeTextFile(cwd, "apps/web/public/favicon.svg", "<svg>nested</svg>");

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

expect(resolved).not.toBeNull();
expect(resolved).toContain("favicon.svg");
expect(resolved).not.toContain("apps/web");
}),
);

it.effect("skips ignored workspace directories when searching nested icons", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, ".next/public/favicon.svg", "<svg>ignored</svg>");

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

expect(resolved).toBeNull();
}),
);

it.effect("skips gitignored root favicons and falls through to nested apps", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver;
const cwd = yield* makeTempDir({ git: true });
yield* writeTextFile(cwd, ".gitignore", "/favicon.svg\n");
yield* writeTextFile(cwd, "favicon.svg", "<svg>ignored-root</svg>");
yield* writeTextFile(cwd, "apps/web/public/favicon.svg", "<svg>nested</svg>");

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

expect(resolved).not.toBeNull();
expect(resolved).toContain("apps/web/public/favicon.svg");
}),
);
});
});
Loading
Loading