/** * Path resolution utilities for the store. */ /** * Resolves a relative path against a base path, handling various path formats. * Handles: * - Absolute paths: /full/path/file.tsx (returned as-is) * - Relative paths with ./: ./apps/foo/bar.tsx (strips ./) * - Parent paths with ../: ../other/file.tsx (walks up directories) * - Plain paths: apps/foo/bar.tsx (joins with base) * - Paths with @ prefix: @apps/foo/bar.tsx (strips @ then joins) */export function resolveFilePath(base: string, relativePath: string): string { // If already absolute, return as-is if (isAbsolutePath(relativePath)) { return relativePath; } const cleanBase = trimTrailingSeparator(base); // Handle @ prefix (file mention marker) - strip it if present let cleanRelative = relativePath; if (cleanRelative.startsWith('@')) { cleanRelative = cleanRelative.slice(1); } // Tilde paths (~/) are home-relative absolute paths - pass through as-is // The main process will expand ~ to the actual home directory if (cleanRelative.startsWith('~/') || cleanRelative.startsWith('~\\') || cleanRelative === '~') { return cleanRelative; } // Handle ./ prefix (current directory) if (cleanRelative.startsWith('./')) { cleanRelative = cleanRelative.slice(2); } // Handle ../ prefixes (parent directory) const separator = cleanBase.includes('\\') ? '\\' : '/'; const hasUnixRoot = cleanBase.startsWith('/'); const hasUncRoot = cleanBase.startsWith('\\\\'); const normalizedRelative = normalizeSeparators(cleanRelative, separator); const baseParts = splitPath(cleanBase); let remainingRelative = normalizedRelative; while (remainingRelative.startsWith(`..${separator}`)) { remainingRelative = remainingRelative.slice(3); if (baseParts.length > 1) { baseParts.pop(); } } // Join the normalized paths let normalizedBase = baseParts.join(separator); if (hasUnixRoot && !normalizedBase.startsWith('/')) { normalizedBase = `/${normalizedBase}`; } if (hasUncRoot && !normalizedBase.startsWith('\\\\')) { normalizedBase = `\\\\${normalizedBase}`; } return remainingRelative ? `${normalizedBase}${separator}${remainingRelative}` : normalizedBase;} function isAbsolutePath(input: string): boolean { return input.startsWith('/') || input.startsWith('\\\\') || /^[a-zA-Z]:[\\/]/.test(input);} function trimTrailingSeparator(input: string): string { let end = input.length; while (end > 0) { const char = input[end - 1]; if (char !== '/' && char !== '\\') { break; } end--; } return input.slice(0, end);} function normalizeSeparators(input: string, separator: '/' | '\\'): string { let output = ''; let prevWasSeparator = false; for (const char of input) { const isSeparator = char === '/' || char === '\\'; if (isSeparator) { if (!prevWasSeparator) { output += separator; } prevWasSeparator = true; } else { output += char; prevWasSeparator = false; } } return output;} function splitPath(input: string): string[] { const parts: string[] = []; let current = ''; for (const char of input) { if (char === '/' || char === '\\') { if (current.length > 0) { parts.push(current); current = ''; } } else { current += char; } } if (current.length > 0) { parts.push(current); } return parts;}
/**
* Path resolution utilities for the store.
*/
/**
* Resolves a relative path against a base path, handling various path formats.
* Handles:
* - Absolute paths: /full/path/file.tsx (returned as-is)
* - Relative paths with ./: ./apps/foo/bar.tsx (strips ./)
* - Parent paths with ../: ../other/file.tsx (walks up directories)
* - Plain paths: apps/foo/bar.tsx (joins with base)
* - Paths with @ prefix: @apps/foo/bar.tsx (strips @ then joins)
*/
export function resolveFilePath(base: string, relativePath: string): string {
// If already absolute, return as-is
if (isAbsolutePath(relativePath)) {
return relativePath;
}
const cleanBase = trimTrailingSeparator(base);
// Handle @ prefix (file mention marker) - strip it if present
let cleanRelative = relativePath;
if (cleanRelative.startsWith('@')) {
cleanRelative = cleanRelative.slice(1);
}
// Tilde paths (~/) are home-relative absolute paths - pass through as-is
// The main process will expand ~ to the actual home directory
if (cleanRelative.startsWith('~/') || cleanRelative.startsWith('~\\') || cleanRelative === '~') {
return cleanRelative;
}
// Handle ./ prefix (current directory)
if (cleanRelative.startsWith('./')) {
cleanRelative = cleanRelative.slice(2);
}
// Handle ../ prefixes (parent directory)
const separator = cleanBase.includes('\\') ? '\\' : '/';
const hasUnixRoot = cleanBase.startsWith('/');
const hasUncRoot = cleanBase.startsWith('\\\\');
const normalizedRelative = normalizeSeparators(cleanRelative, separator);
const baseParts = splitPath(cleanBase);
let remainingRelative = normalizedRelative;
while (remainingRelative.startsWith(`..${separator}`)) {
remainingRelative = remainingRelative.slice(3);
if (baseParts.length > 1) {
baseParts.pop();
}
}
// Join the normalized paths
let normalizedBase = baseParts.join(separator);
if (hasUnixRoot && !normalizedBase.startsWith('/')) {
normalizedBase = `/${normalizedBase}`;
}
if (hasUncRoot && !normalizedBase.startsWith('\\\\')) {
normalizedBase = `\\\\${normalizedBase}`;
}
return remainingRelative ? `${normalizedBase}${separator}${remainingRelative}` : normalizedBase;
}
function isAbsolutePath(input: string): boolean {
return input.startsWith('/') || input.startsWith('\\\\') || /^[a-zA-Z]:[\\/]/.test(input);
}
function trimTrailingSeparator(input: string): string {
let end = input.length;
while (end > 0) {
const char = input[end - 1];
if (char !== '/' && char !== '\\') {
break;
}
end--;
}
return input.slice(0, end);
}
function normalizeSeparators(input: string, separator: '/' | '\\'): string {
let output = '';
let prevWasSeparator = false;
for (const char of input) {
const isSeparator = char === '/' || char === '\\';
if (isSeparator) {
if (!prevWasSeparator) {
output += separator;
}
prevWasSeparator = true;
} else {
output += char;
prevWasSeparator = false;
}
}
return output;
}
function splitPath(input: string): string[] {
const parts: string[] = [];
let current = '';
for (const char of input) {
if (char === '/' || char === '\\') {
if (current.length > 0) {
parts.push(current);
current = '';
}
} else {
current += char;
}
}
if (current.length > 0) {
parts.push(current);
}
return parts;
}