/** * Cross-platform path.join function that works in any JavaScript environment * Handles regular paths and file:// URLs from import.meta.url * @param segments - Path segments to join * @returns Joined path string */ export function pathJoin(...segments: string[]): string { // Filter out empty strings and non-string values const validSegments = segments.filter(segment => typeof segment === 'string' && segment.length > 0 ); // If no valid segments, return empty string if (validSegments.length === 0) { return ''; } // Convert file:// URLs to paths const processedSegments = validSegments.map(segment => { return fileUrlToPath(segment); }); // Detect if we're dealing with Windows-style paths const isWindowsPath = processedSegments.some(segment => { // Check for Windows drive letter if (/^[a-zA-Z]:/.test(segment)) return true; // Check if first segment has backslashes (indicating Windows) if (processedSegments[0] === segment && segment.includes('\\')) return true; return false; }); // Choose separator and normalize function based on path style const separator = isWindowsPath ? '\\' : '/'; // Normalize segments based on path style const normalizedSegments = processedSegments.map((segment) => { if (isWindowsPath) { // On Windows, both / and \ are separators return segment.replace(/[\/\\]+/g, '\\'); } else { // On POSIX, only / is a separator, \ is literal return segment.replace(/\/+/g, '/'); } }); // Join segments and handle edge cases let result = ''; for (let i = 0; i < normalizedSegments.length; i++) { const segment = normalizedSegments[i]; if (i === 0) { result = segment; } else { // Remove leading separator from segment if result already ends with one let cleanSegment = segment; if (segment.startsWith(separator)) { cleanSegment = segment.slice(1); } // Add separator if result doesn't end with one if (result && !result.endsWith(separator)) { result += separator; } result += cleanSegment; } } // Handle edge cases if (result === '' && validSegments.some(s => s === '/' || s === '\\')) { result = separator; } // Clean up multiple consecutive separators if (isWindowsPath) { result = result.replace(/\\+/g, '\\'); // Special case for UNC paths if (result.startsWith('\\\\') && !result.startsWith('\\\\\\')) { // Keep double backslash for UNC paths } else if (result.match(/^\\[^\\]/)) { // Single leading backslash on Windows (unusual but valid) } } else { result = result.replace(/\/+/g, '/'); // Preserve leading slash for absolute paths if (processedSegments[0].startsWith('/') && !result.startsWith('/')) { result = '/' + result; } } return result; } /** * Convert a file:// URL to a system path * @param fileUrl - A file:// URL (e.g., from import.meta.url) * @returns System path */ export function fileUrlToPath(fileUrl: string): string { if (!fileUrl.startsWith('file://')) { return fileUrl; } // Remove file:// protocol let path = fileUrl.slice(7); // Handle Windows file URLs: file:///C:/path -> C:\path if (/^\/[a-zA-Z]:/.test(path)) { path = path.slice(1); // Convert forward slashes to backslashes for Windows path = path.replace(/\//g, '\\'); } // Decode URL encoding path = decodeURIComponent(path); return path; } /** * Convert a system path to a file:// URL * @param path - System path * @returns file:// URL */ export function pathToFileUrl(path: string): string { if (path.startsWith('file://')) { return path; } // Normalize slashes to forward slashes for URL let urlPath = path.replace(/\\/g, '/'); // Encode special characters urlPath = encodeURI(urlPath).replace(/[?#]/g, encodeURIComponent); // Check if it's a Windows absolute path if (/^[a-zA-Z]:/.test(urlPath)) { return `file:///${urlPath}`; } // Check if it's an absolute path if (urlPath.startsWith('/')) { return `file://${urlPath}`; } // Relative path - just return as-is (can't make a file URL from relative path) return urlPath; } /** * Get the directory from a file URL or path * @param urlOrPath - File URL (like import.meta.url) or regular path * @returns Directory path */ export function dirname(urlOrPath: string): string { // Convert file URL to path if needed let path = fileUrlToPath(urlOrPath); // Remove trailing slashes (but keep root slashes) if (path.length > 1 && (path.endsWith('/') || path.endsWith('\\'))) { // Special case: don't remove trailing slash for Windows drive root if (!(path.length === 3 && path[1] === ':')) { path = path.slice(0, -1); } } // Special case for Windows drive root (C:\ or C:) if (path.match(/^[a-zA-Z]:\\?$/)) { return path.endsWith('\\') ? path : path + '\\'; } // Find the last separator const lastSlash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); if (lastSlash === -1) { return '.'; } // Special case for root if (lastSlash === 0) { return '/'; } // Special case for Windows drive root (C:\) if (lastSlash === 2 && path[1] === ':') { return path.slice(0, 3); } return path.slice(0, lastSlash); }