import * as NodeServices from "@effect/platform-node/NodeServices";
import { it, describe, expect } from "@effect/vitest";
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 * as PlatformError from "effect/PlatformError";
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts";
import * as T3ProjectFileLoader from "./T3ProjectFileLoader.ts";
const TestLayer = Layer.empty.pipe(
Layer.provideMerge(
ProjectFaviconResolver.layer.pipe(
Layer.provide(WorkspacePaths.layer),
Layer.provide(T3ProjectFileLoader.layer),
),
),
Layer.provideMerge(NodeServices.layer),
);
const makeTempDir = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
return yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3code-project-favicon-",
});
});
const writeTextFile = Effect.fn("writeTextFile")(function* (
cwd: string,
relativePath: string,
contents: string,
) {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const absolutePath = path.join(cwd, relativePath);
yield* fileSystem
.makeDirectory(path.dirname(absolutePath), { recursive: true })
.pipe(Effect.orDie);
yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie);
});
const makeResolverWithFileSystem = (fileSystem: FileSystem.FileSystem) =>
ProjectFaviconResolver.make.pipe(
Effect.provide([WorkspacePaths.layer, T3ProjectFileLoader.layer]),
Effect.provideService(FileSystem.FileSystem, fileSystem),
);
it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
describe("resolvePath", () => {
it.effect("prefers well-known favicon files", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "favicon.svg", "");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toContain("favicon.svg");
}),
);
it.effect("prefers a t3.json iconPath over well-known files", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "brand/mark.svg" }');
yield* writeTextFile(cwd, "brand/mark.svg", "");
yield* writeTextFile(cwd, "favicon.svg", "");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toContain("brand/mark.svg");
}),
);
it.effect("falls back to well-known files when the t3.json iconPath does not exist", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "brand/missing.svg" }');
yield* writeTextFile(cwd, "favicon.svg", "");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toContain("favicon.svg");
}),
);
it.effect("ignores invalid t3.json files", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "t3.json", "{ not json");
yield* writeTextFile(cwd, "favicon.svg", "");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toContain("favicon.svg");
}),
);
it.effect("does not resolve a t3.json iconPath outside the workspace root", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const parent = yield* makeTempDir;
const cwd = `${parent}/app`;
yield* writeTextFile(parent, "secret.svg", "");
yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "../secret.svg" }');
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).toBeNull();
}),
);
it.effect("resolves icon hrefs from project source files", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "index.html", '');
yield* writeTextFile(cwd, "public/brand/logo.svg", "");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toContain("public/brand/logo.svg");
}),
);
it.effect("returns null when no icon is present", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).toBeNull();
}),
);
it.effect("preserves workspace normalization context", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
const missingCwd = `${cwd}/missing`;
const error = yield* resolver.resolvePath(missingCwd).pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "ProjectFaviconResolutionError",
operation: "normalize-workspace",
workspaceRoot: missingCwd,
});
expect(error.cause).toBeInstanceOf(WorkspacePaths.WorkspaceRootNotExistsError);
}),
);
it.effect("preserves non-missing candidate stat failures", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const cwd = yield* makeTempDir;
const faviconPath = path.join(cwd, "favicon.svg");
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "stat",
pathOrDescriptor: faviconPath,
});
const resolver = yield* makeResolverWithFileSystem(
FileSystem.FileSystem.of({
...fileSystem,
stat: (filePath) =>
filePath === faviconPath ? Effect.fail(cause) : fileSystem.stat(filePath),
}),
);
const error = yield* resolver.resolvePath(cwd).pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "ProjectFaviconResolutionError",
operation: "stat-candidate",
workspaceRoot: cwd,
relativePath: "favicon.svg",
absolutePath: faviconPath,
});
expect(error.cause).toBe(cause);
}),
);
it.effect("preserves icon source read failures", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const cwd = yield* makeTempDir;
const sourcePath = path.join(cwd, "index.html");
yield* writeTextFile(cwd, "index.html", '');
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "readFileString",
pathOrDescriptor: sourcePath,
});
const resolver = yield* makeResolverWithFileSystem(
FileSystem.FileSystem.of({
...fileSystem,
readFileString: (filePath, options) =>
filePath === sourcePath
? Effect.fail(cause)
: fileSystem.readFileString(filePath, options),
}),
);
const error = yield* resolver.resolvePath(cwd).pipe(Effect.flip);
expect(error).toMatchObject({
_tag: "ProjectFaviconResolutionError",
operation: "read-source",
workspaceRoot: cwd,
relativePath: "index.html",
absolutePath: sourcePath,
});
expect(error.cause).toBe(cause);
}),
);
it.effect("skips icon metadata paths outside the workspace", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "index.html", '');
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).toBeNull();
}),
);
it.effect("continues to later sources after an outside-root icon href", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "index.html", '');
yield* writeTextFile(cwd, "public/index.html", '');
yield* writeTextFile(cwd, "public/brand/logo.svg", "");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toContain("public/brand/logo.svg");
}),
);
});
});