Files
smartpath/ts_iso/index.ts

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);
}