Skip to content

Commit 7ace303

Browse files
committed
fix: skip outside-root candidates in findExistingFile instead of aborting
In findExistingFile, resolveRelativePathWithinRoot failures (e.g. paths outside the workspace from icon metadata) now skip the candidate instead of aborting the entire effect. This restores the scan-order behavior so that a bad icon href in an earlier ICON_SOURCE_FILES entry does not prevent later sources from being consulted. Updated the test to reflect the corrected behavior and added a test verifying that a valid icon from a later source file is found even when an earlier source has an outside-root href.
1 parent 26f8074 commit 7ace303

2 files changed

Lines changed: 26 additions & 21 deletions

File tree

apps/server/src/project/ProjectFaviconResolver.test.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,30 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
167167
}),
168168
);
169169

170-
it.effect("rejects icon metadata paths outside the workspace", () =>
170+
it.effect("skips icon metadata paths outside the workspace", () =>
171171
Effect.gen(function* () {
172172
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
173173
const cwd = yield* makeTempDir;
174174
yield* writeTextFile(cwd, "index.html", '<link rel="icon" href="../../secret.svg">');
175175

176-
const error = yield* resolver.resolvePath(cwd).pipe(Effect.flip);
176+
const resolved = yield* resolver.resolvePath(cwd);
177177

178-
expect(error).toMatchObject({
179-
_tag: "ProjectFaviconResolutionError",
180-
operation: "resolve-path",
181-
workspaceRoot: cwd,
182-
relativePath: "../secret.svg",
183-
});
184-
expect(error.cause).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError);
178+
expect(resolved).toBeNull();
179+
}),
180+
);
181+
182+
it.effect("skips outside-root href and finds valid icon from later source", () =>
183+
Effect.gen(function* () {
184+
const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
185+
const cwd = yield* makeTempDir;
186+
yield* writeTextFile(cwd, "index.html", '<link rel="icon" href="../../secret.svg">');
187+
yield* writeTextFile(cwd, "public/index.html", '<link rel="icon" href="/brand/logo.svg">');
188+
yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>");
189+
190+
const resolved = yield* resolver.resolvePath(cwd);
191+
192+
expect(resolved).not.toBeNull();
193+
expect(resolved).toContain("public/brand/logo.svg");
185194
}),
186195
);
187196
});

apps/server/src/project/ProjectFaviconResolver.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,30 +134,26 @@ export const make = Effect.gen(function* () {
134134
relativePath,
135135
})
136136
.pipe(
137-
Effect.mapError(
138-
(cause) =>
139-
new ProjectFaviconResolutionError({
140-
operation: "resolve-path",
141-
workspaceRoot: projectCwd,
142-
relativePath,
143-
cause,
144-
}),
145-
),
137+
Effect.map(Option.some),
138+
Effect.orElseSucceed(() => Option.none<{ absolutePath: string; relativePath: string }>()),
146139
);
147-
const stats = yield* optionOnNotFound(fileSystem.stat(candidate.absolutePath)).pipe(
140+
if (Option.isNone(candidate)) {
141+
continue;
142+
}
143+
const stats = yield* optionOnNotFound(fileSystem.stat(candidate.value.absolutePath)).pipe(
148144
Effect.mapError(
149145
(cause) =>
150146
new ProjectFaviconResolutionError({
151147
operation: "stat-candidate",
152148
workspaceRoot: projectCwd,
153149
relativePath,
154-
absolutePath: candidate.absolutePath,
150+
absolutePath: candidate.value.absolutePath,
155151
cause,
156152
}),
157153
),
158154
);
159155
if (Option.isSome(stats) && stats.value.type === "File") {
160-
return candidate.absolutePath;
156+
return candidate.value.absolutePath;
161157
}
162158
}
163159
return null;

0 commit comments

Comments
 (0)