import * as NodeServices from "@effect/platform-node/NodeServices";import { it, describe, expect } from "@effect/vitest";import * as Duration from "effect/Duration";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 { TestClock } from "effect/testing"; 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("serves repeated resolves from cache instead of re-walking candidates", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const path = yield* Path.Path; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "public/favicon.svg", "<svg>public</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved?.endsWith(path.join("public", "favicon.svg"))).toBe(true); // `favicon.svg` outranks `public/favicon.svg`, so a resolver that walked // the candidate list again would switch to it. Staying on the original // answer is only possible from cache. yield* writeTextFile(cwd, "favicon.svg", "<svg>root</svg>"); for (const _attempt of [1, 2, 3]) { expect(yield* resolver.resolvePath(cwd)).toBe(resolved); } yield* TestClock.adjust(Duration.minutes(11)); expect(yield* resolver.resolvePath(cwd)).toBe(path.join(cwd, "favicon.svg")); expect(yield* resolver.resolvePath(cwd)).not.toBe(resolved); }).pipe(Effect.provide(TestClock.layer())), ); it.effect("falls back at once when a cached favicon is deleted", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "favicon.svg", "<svg>favicon</svg>"); expect(yield* resolver.resolvePath(cwd)).not.toBeNull(); yield* fileSystem.remove(path.join(cwd, "favicon.svg")).pipe(Effect.orDie); // Still inside the positive TTL: the cached path must not be served. expect(yield* resolver.resolvePath(cwd)).toBeNull(); }).pipe(Effect.provide(TestClock.layer())), ); it.effect("re-probes for a favicon added after a miss once the negative TTL expires", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; expect(yield* resolver.resolvePath(cwd)).toBeNull(); yield* writeTextFile(cwd, "favicon.svg", "<svg>favicon</svg>"); expect(yield* resolver.resolvePath(cwd)).toBeNull(); yield* TestClock.adjust(Duration.minutes(2)); expect(yield* resolver.resolvePath(cwd)).not.toBeNull(); }).pipe(Effect.provide(TestClock.layer())), ); it.effect("prefers well-known favicon files", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "favicon.svg", "<svg>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 path = yield* Path.Path; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "brand/mark.svg" }'); yield* writeTextFile(cwd, "brand/mark.svg", "<svg>mark</svg>"); yield* writeTextFile(cwd, "favicon.svg", "<svg>favicon</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved).not.toBeNull(); expect(resolved).toBe(path.join(cwd, "brand", "mark.svg")); }), ); it.effect("uses a saved project favicon override", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const path = yield* Path.Path; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "brand/custom.svg", "<svg>custom</svg>"); yield* writeTextFile(cwd, "favicon.svg", "<svg>automatic</svg>"); const resolved = yield* resolver.resolvePath(cwd, "brand/custom.svg"); expect(resolved).not.toBeNull(); expect(resolved).toBe(path.join(cwd, "brand", "custom.svg")); }), ); it.effect("uses a saved project favicon outside the workspace", () => Effect.gen(function* () { const path = yield* Path.Path; const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; const pictures = yield* makeTempDir; yield* writeTextFile(pictures, "custom.png", "image"); const externalPath = path.join(pictures, "custom.png"); const resolved = yield* resolver.resolvePath(cwd, externalPath); expect(resolved).toBe(externalPath); }), ); it.effect("falls back when a saved override is missing from a checkout", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "favicon.svg", "<svg>automatic</svg>"); const resolved = yield* resolver.resolvePath(cwd, "brand/missing.svg"); expect(resolved).not.toBeNull(); expect(resolved).toContain("favicon.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", "<svg>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", "<svg>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", "<svg>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", '<link rel="icon" href="/brand/logo.svg">'); yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved).not.toBeNull(); expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg")); }), ); it.effect("resolves icon hrefs from object-literal route metadata", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; yield* writeTextFile( cwd, "src/routes/__root.tsx", `export const Route = createRootRoute({ head: () => ({ links: [ { rel: "stylesheet", href: "/app.css" }, { rel: "icon", href: "/brand/logo.svg" }, ], }),});`, ); yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved).not.toBeNull(); expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg")); }), ); it.effect("resolves object-literal icon metadata when href precedes rel", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; yield* writeTextFile( cwd, "src/root.tsx", `const links = [{ href: "/brand/logo.svg", rel: "shortcut icon" }];`, ); yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved).not.toBeNull(); expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg")); }), ); it.effect("resolves object-literal icon metadata alongside nested objects", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; yield* writeTextFile( cwd, "src/root.tsx", `const links = [{ attributes: {}, rel: "icon", href: "/brand/logo.svg" }];`, ); yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved).not.toBeNull(); expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg")); }), ); it.effect("skips icon metadata without an href and keeps scanning", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; yield* writeTextFile( cwd, "src/root.tsx", `const links = [{ rel: "icon" }, { rel: "icon", href: "/brand/logo.svg" }];`, ); yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved).not.toBeNull(); expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg")); }), ); // A large icon source with no icon metadata used to pin the server's event loop for // minutes: the object pattern was unanchored, so it restarted at every offset and // rescanned forward from each one. Anchoring keeps this proportional to file size. it.effect("scans large icon sources without an icon in reasonable time", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const cwd = yield* makeTempDir; // Mirrors a generated single-file build: large, brace-sparse, and no icon metadata. const filler = `<p>${"pokopia companion guide ".repeat(24)}</p>\n`; yield* writeTextFile( cwd, "index.html", `<!doctype html><html><head><title>guide</title></head><body>\n${filler.repeat(1200)}</body></html>`, ); const startedAt = performance.now(); const resolved = yield* resolver.resolvePath(cwd); const elapsedMs = performance.now() - startedAt; expect(resolved).toBeNull(); expect(elapsedMs).toBeLessThan(5_000); }), ); 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", '<link rel="icon" href="/favicon.svg">'); 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", '<link rel="icon" href="../../secret.svg">'); 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", '<link rel="icon" href="../../secret.svg">'); yield* writeTextFile(cwd, "public/index.html", '<link rel="icon" href="/brand/logo.svg">'); yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); const resolved = yield* resolver.resolvePath(cwd); expect(resolved).not.toBeNull(); expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg")); }), ); });});
import * as NodeServices from "@effect/platform-node/NodeServices";
import { it, describe, expect } from "@effect/vitest";
import * as Duration from "effect/Duration";
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 { TestClock } from "effect/testing";
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("serves repeated resolves from cache instead of re-walking candidates", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const path = yield* Path.Path;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "public/favicon.svg", "<svg>public</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved?.endsWith(path.join("public", "favicon.svg"))).toBe(true);
// `favicon.svg` outranks `public/favicon.svg`, so a resolver that walked
// the candidate list again would switch to it. Staying on the original
// answer is only possible from cache.
yield* writeTextFile(cwd, "favicon.svg", "<svg>root</svg>");
for (const _attempt of [1, 2, 3]) {
expect(yield* resolver.resolvePath(cwd)).toBe(resolved);
}
yield* TestClock.adjust(Duration.minutes(11));
expect(yield* resolver.resolvePath(cwd)).toBe(path.join(cwd, "favicon.svg"));
expect(yield* resolver.resolvePath(cwd)).not.toBe(resolved);
}).pipe(Effect.provide(TestClock.layer())),
);
it.effect("falls back at once when a cached favicon is deleted", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "favicon.svg", "<svg>favicon</svg>");
expect(yield* resolver.resolvePath(cwd)).not.toBeNull();
yield* fileSystem.remove(path.join(cwd, "favicon.svg")).pipe(Effect.orDie);
// Still inside the positive TTL: the cached path must not be served.
expect(yield* resolver.resolvePath(cwd)).toBeNull();
}).pipe(Effect.provide(TestClock.layer())),
);
it.effect("re-probes for a favicon added after a miss once the negative TTL expires", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
expect(yield* resolver.resolvePath(cwd)).toBeNull();
yield* writeTextFile(cwd, "favicon.svg", "<svg>favicon</svg>");
expect(yield* resolver.resolvePath(cwd)).toBeNull();
yield* TestClock.adjust(Duration.minutes(2));
expect(yield* resolver.resolvePath(cwd)).not.toBeNull();
}).pipe(Effect.provide(TestClock.layer())),
);
it.effect("prefers well-known favicon files", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "favicon.svg", "<svg>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 path = yield* Path.Path;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "brand/mark.svg" }');
yield* writeTextFile(cwd, "brand/mark.svg", "<svg>mark</svg>");
yield* writeTextFile(cwd, "favicon.svg", "<svg>favicon</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toBe(path.join(cwd, "brand", "mark.svg"));
}),
);
it.effect("uses a saved project favicon override", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const path = yield* Path.Path;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "brand/custom.svg", "<svg>custom</svg>");
yield* writeTextFile(cwd, "favicon.svg", "<svg>automatic</svg>");
const resolved = yield* resolver.resolvePath(cwd, "brand/custom.svg");
expect(resolved).not.toBeNull();
expect(resolved).toBe(path.join(cwd, "brand", "custom.svg"));
}),
);
it.effect("uses a saved project favicon outside the workspace", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
const pictures = yield* makeTempDir;
yield* writeTextFile(pictures, "custom.png", "image");
const externalPath = path.join(pictures, "custom.png");
const resolved = yield* resolver.resolvePath(cwd, externalPath);
expect(resolved).toBe(externalPath);
}),
);
it.effect("falls back when a saved override is missing from a checkout", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(cwd, "favicon.svg", "<svg>automatic</svg>");
const resolved = yield* resolver.resolvePath(cwd, "brand/missing.svg");
expect(resolved).not.toBeNull();
expect(resolved).toContain("favicon.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", "<svg>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", "<svg>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", "<svg>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", '<link rel="icon" href="/brand/logo.svg">');
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
}),
);
it.effect("resolves icon hrefs from object-literal route metadata", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(
cwd,
"src/routes/__root.tsx",
`export const Route = createRootRoute({
head: () => ({
links: [
{ rel: "stylesheet", href: "/app.css" },
{ rel: "icon", href: "/brand/logo.svg" },
],
}),
});`,
);
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
}),
);
it.effect("resolves object-literal icon metadata when href precedes rel", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(
cwd,
"src/root.tsx",
`const links = [{ href: "/brand/logo.svg", rel: "shortcut icon" }];`,
);
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
}),
);
it.effect("resolves object-literal icon metadata alongside nested objects", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(
cwd,
"src/root.tsx",
`const links = [{ attributes: {}, rel: "icon", href: "/brand/logo.svg" }];`,
);
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
}),
);
it.effect("skips icon metadata without an href and keeps scanning", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
yield* writeTextFile(
cwd,
"src/root.tsx",
`const links = [{ rel: "icon" }, { rel: "icon", href: "/brand/logo.svg" }];`,
);
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
}),
);
// A large icon source with no icon metadata used to pin the server's event loop for
// minutes: the object pattern was unanchored, so it restarted at every offset and
// rescanned forward from each one. Anchoring keeps this proportional to file size.
it.effect("scans large icon sources without an icon in reasonable time", () =>
Effect.gen(function* () {
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
const cwd = yield* makeTempDir;
// Mirrors a generated single-file build: large, brace-sparse, and no icon metadata.
const filler = `<p>${"pokopia companion guide ".repeat(24)}</p>\n`;
yield* writeTextFile(
cwd,
"index.html",
`<!doctype html><html><head><title>guide</title></head><body>\n${filler.repeat(1200)}</body></html>`,
);
const startedAt = performance.now();
const resolved = yield* resolver.resolvePath(cwd);
const elapsedMs = performance.now() - startedAt;
expect(resolved).toBeNull();
expect(elapsedMs).toBeLessThan(5_000);
}),
);
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", '<link rel="icon" href="/favicon.svg">');
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", '<link rel="icon" href="../../secret.svg">');
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", '<link rel="icon" href="../../secret.svg">');
yield* writeTextFile(cwd, "public/index.html", '<link rel="icon" href="/brand/logo.svg">');
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");
const resolved = yield* resolver.resolvePath(cwd);
expect(resolved).not.toBeNull();
expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
}),
);
});
});