| ... | ... | @@ -160,6 +160,109 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { |
| 160 | 160 | }), |
| 161 | 161 | ); |
| 162 | 162 | |
| 163 | it.effect("resolves icon hrefs from object-literal route metadata", () => |
| 164 | Effect.gen(function* () { |
| 165 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| 166 | const cwd = yield* makeTempDir; |
| 167 | yield* writeTextFile( |
| 168 | cwd, |
| 169 | "src/routes/__root.tsx", |
| 170 | `export const Route = createRootRoute({ |
| 171 | head: () => ({ |
| 172 | links: [ |
| 173 | { rel: "stylesheet", href: "/app.css" }, |
| 174 | { rel: "icon", href: "/brand/logo.svg" }, |
| 175 | ], |
| 176 | }), |
| 177 | });`, |
| 178 | ); |
| 179 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| 180 | |
| 181 | const resolved = yield* resolver.resolvePath(cwd); |
| 182 | |
| 183 | expect(resolved).not.toBeNull(); |
| 184 | expect(resolved).toContain("public/brand/logo.svg"); |
| 185 | }), |
| 186 | ); |
| 187 | |
| 188 | it.effect("resolves object-literal icon metadata when href precedes rel", () => |
| 189 | Effect.gen(function* () { |
| 190 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| 191 | const cwd = yield* makeTempDir; |
| 192 | yield* writeTextFile( |
| 193 | cwd, |
| 194 | "src/root.tsx", |
| 195 | `const links = [{ href: "/brand/logo.svg", rel: "shortcut icon" }];`, |
| 196 | ); |
| 197 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| 198 | |
| 199 | const resolved = yield* resolver.resolvePath(cwd); |
| 200 | |
| 201 | expect(resolved).not.toBeNull(); |
| 202 | expect(resolved).toContain("public/brand/logo.svg"); |
| 203 | }), |
| 204 | ); |
| 205 | |
| 206 | it.effect("resolves object-literal icon metadata alongside nested objects", () => |
| 207 | Effect.gen(function* () { |
| 208 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| 209 | const cwd = yield* makeTempDir; |
| 210 | yield* writeTextFile( |
| 211 | cwd, |
| 212 | "src/root.tsx", |
| 213 | `const links = [{ attributes: {}, rel: "icon", href: "/brand/logo.svg" }];`, |
| 214 | ); |
| 215 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| 216 | |
| 217 | const resolved = yield* resolver.resolvePath(cwd); |
| 218 | |
| 219 | expect(resolved).not.toBeNull(); |
| 220 | expect(resolved).toContain("public/brand/logo.svg"); |
| 221 | }), |
| 222 | ); |
| 223 | |
| 224 | it.effect("skips icon metadata without an href and keeps scanning", () => |
| 225 | Effect.gen(function* () { |
| 226 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| 227 | const cwd = yield* makeTempDir; |
| 228 | yield* writeTextFile( |
| 229 | cwd, |
| 230 | "src/root.tsx", |
| 231 | `const links = [{ rel: "icon" }, { rel: "icon", href: "/brand/logo.svg" }];`, |
| 232 | ); |
| 233 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| 234 | |
| 235 | const resolved = yield* resolver.resolvePath(cwd); |
| 236 | |
| 237 | expect(resolved).not.toBeNull(); |
| 238 | expect(resolved).toContain("public/brand/logo.svg"); |
| 239 | }), |
| 240 | ); |
| 241 | |
| 242 | // A large icon source with no icon metadata used to pin the server's event loop for |
| 243 | // minutes: the object pattern was unanchored, so it restarted at every offset and |
| 244 | // rescanned forward from each one. Anchoring keeps this proportional to file size. |
| 245 | it.effect("scans large icon sources without an icon in reasonable time", () => |
| 246 | Effect.gen(function* () { |
| 247 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| 248 | const cwd = yield* makeTempDir; |
| 249 | // Mirrors a generated single-file build: large, brace-sparse, and no icon metadata. |
| 250 | const filler = `<p>${"pokopia companion guide ".repeat(24)}</p>\n`; |
| 251 | yield* writeTextFile( |
| 252 | cwd, |
| 253 | "index.html", |
| 254 | `<!doctype html><html><head><title>guide</title></head><body>\n${filler.repeat(1200)}</body></html>`, |
| 255 | ); |
| 256 | |
| 257 | const startedAt = performance.now(); |
| 258 | const resolved = yield* resolver.resolvePath(cwd); |
| 259 | const elapsedMs = performance.now() - startedAt; |
| 260 | |
| 261 | expect(resolved).toBeNull(); |
| 262 | expect(elapsedMs).toBeLessThan(5_000); |
| 263 | }), |
| 264 | ); |
| 265 | |
| 163 | 266 | it.effect("returns null when no icon is present", () => |
| 164 | 267 | Effect.gen(function* () { |
| 165 | 268 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |