feat(iso): add isomorphic path module with cross-platform utilities
This commit is contained in:
191
ts_iso/index.ts
Normal file
191
ts_iso/index.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
Reference in New Issue
Block a user