Files
kandev/apps/web/lib/workspace-file-path.ts
T
João Salavisa 2c06cf5b6f fix: keep file tree paths workspace-relative (#3855)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-09-22 08:15:41 +01:00

74 lines
2.7 KiB
TypeScript

function normalizeSeparators(path: string): string {
return path.replaceAll("\\", "/");
}
function trimTrailingSeparators(path: string): string {
if (path === "/" || /^[A-Za-z]:\/$/.test(path) || /^file:\/\/\/(?:[A-Za-z]:\/)?$/i.test(path)) {
return path;
}
return path.replace(/\/+$/, "");
}
function usesWindowsPathSemantics(path: string): boolean {
return (
/^[A-Za-z]:\//.test(path) ||
path.startsWith("//") ||
/^file:\/\/\/[A-Za-z]:(?:\/|$)/i.test(path) ||
/^file:\/\/[^/]/i.test(path)
);
}
function hasAbsoluteURIForm(path: string): boolean {
return /^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(path) || /^file:\//i.test(path);
}
function isAbsoluteFilePath(path: string): boolean {
return path.startsWith("/") || usesWindowsPathSemantics(path) || hasAbsoluteURIForm(path);
}
/** Return whether a path is a canonical workspace-relative tree identity. */
export function isWorkspaceTreePath(path: string): boolean {
if (path === "") return true;
if (
path.startsWith("/") ||
path.includes("\\") ||
path.includes("\0") ||
/^[A-Za-z]:/.test(path) ||
hasAbsoluteURIForm(path)
) {
return false;
}
return path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
}
/** Return a workspace-relative alias for a contained path, or null when it is outside. */
export function workspaceRelativeFilePath(
filePath: string,
workspaceRoot: string | null | undefined,
): string | null {
if (!workspaceRoot) return null;
const path = trimTrailingSeparators(normalizeSeparators(filePath));
const root = trimTrailingSeparators(normalizeSeparators(workspaceRoot));
if (!root) return null;
const windowsSemantics = usesWindowsPathSemantics(path) || usesWindowsPathSemantics(root);
const comparablePath = windowsSemantics ? path.toLocaleLowerCase("en-US") : path;
const comparableRoot = windowsSemantics ? root.toLocaleLowerCase("en-US") : root;
if (comparablePath === comparableRoot) return "";
const rootPrefix = comparableRoot.endsWith("/") ? comparableRoot : `${comparableRoot}/`;
if (!comparablePath.startsWith(rootPrefix)) return null;
const relativePath = path.slice(rootPrefix.length);
return isWorkspaceTreePath(relativePath) ? relativePath : null;
}
/** Normalize a workspace-contained path while preserving external absolute paths. */
export function normalizeWorkspaceFilePath(
filePath: string,
workspaceRoot: string | null | undefined,
): string {
const relativePath = workspaceRelativeFilePath(filePath, workspaceRoot);
if (relativePath !== null) return relativePath;
const normalizedPath = normalizeSeparators(filePath);
return isAbsoluteFilePath(normalizedPath) ? filePath : normalizedPath;
}