192 lines
5.2 KiB
TypeScript
192 lines
5.2 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|