Skip to content

Commit 4dd9da6

Browse files
committed
feat(paths): add isUnixPath, make normalizePath handle MSYS drives, fromUnixPath produces native Windows paths
1 parent 725c735 commit 4dd9da6

2 files changed

Lines changed: 194 additions & 61 deletions

File tree

src/paths/normalize.ts

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const CHAR_UPPERCASE_A = 65
2424
const CHAR_UPPERCASE_Z = 90
2525

2626
// Regular expressions.
27+
const msysDriveRegExp = /^\/([a-zA-Z])(\/|$)/
2728
const slashRegExp = /[/\\]/
2829
const nodeModulesPathRegExp = /(?:^|[/\\])node_modules(?:[/\\]|$)/
2930

@@ -112,6 +113,17 @@ function getUrl() {
112113
return _url as typeof import('node:url')
113114
}
114115

116+
// On Windows, convert MSYS drive notation to native: /c/path → C:/path
117+
function msysDriveToNative(normalized: string): string {
118+
if (WIN32) {
119+
return normalized.replace(
120+
msysDriveRegExp,
121+
(_, letter, sep) => `${letter.toUpperCase()}:${sep || '/'}`,
122+
)
123+
}
124+
return normalized
125+
}
126+
115127
/**
116128
* Check if a path contains node_modules directory.
117129
*
@@ -359,58 +371,82 @@ export function isRelative(pathLike: string | Buffer | URL): boolean {
359371
}
360372

361373
/**
362-
* Convert Unix-style POSIX paths (MSYS/Git Bash format) back to native Windows paths.
374+
* Check if a path uses MSYS/Git Bash Unix-style drive letter notation.
375+
*
376+
* Detects paths in the format `/c/...` where a single letter after the leading
377+
* slash represents a Windows drive letter. These paths are produced by MSYS2,
378+
* Git Bash, and `command -v` on Windows.
379+
*
380+
* Detection rules:
381+
* - Must start with `/` followed by a single ASCII letter (a-z, A-Z)
382+
* - The letter must be followed by `/` or be at the end of the string
383+
* - Examples: `/c/Users/name`, `/d/projects`, `/c`
384+
* - Non-matches: `/tmp`, `/usr/local`, `C:/Windows`
385+
*
386+
* @param {string | Buffer | URL} pathLike - The path to check
387+
* @returns {boolean} `true` if the path uses MSYS drive letter notation
388+
*
389+
* @example
390+
* ```typescript
391+
* // MSYS drive letter paths
392+
* isUnixPath('/c/Users/name') // true
393+
* isUnixPath('/d/projects/app') // true
394+
* isUnixPath('/c') // true
395+
* isUnixPath('/C/Windows') // true
396+
*
397+
* // Not MSYS drive paths
398+
* isUnixPath('/tmp/build') // false
399+
* isUnixPath('/usr/local/bin') // false
400+
* isUnixPath('C:/Windows') // false
401+
* isUnixPath('./relative') // false
402+
* isUnixPath('') // false
403+
* ```
404+
*/
405+
/*@__NO_SIDE_EFFECTS__*/
406+
export function isUnixPath(pathLike: string | Buffer | URL): boolean {
407+
const filepath = pathLikeToString(pathLike)
408+
return typeof filepath === 'string' && msysDriveRegExp.test(filepath)
409+
}
410+
411+
/**
412+
* Convert Unix-style POSIX paths to native Windows paths.
363413
*
364-
* This is the inverse of {@link toUnixPath}. MSYS-style paths use `/c/` notation
365-
* for drive letters, which PowerShell and cmd.exe cannot resolve. This function
366-
* converts them back to native Windows format.
414+
* This is the inverse of {@link toUnixPath}. On Windows, MSYS-style paths use
415+
* `/c/` notation for drive letters and forward slashes, which PowerShell and
416+
* cmd.exe cannot resolve. This function converts them to native Windows format
417+
* with backslashes and proper drive letters.
367418
*
368419
* Conversion rules:
369-
* - On Windows: Converts Unix drive notation to Windows drive letters
370-
* - `/c/path/to/file` becomes `C:/path/to/file`
371-
* - `/d/projects/app` becomes `D:/projects/app`
420+
* - On Windows: Converts drive notation and separators to native format
421+
* - `/c/path/to/file` becomes `C:\path\to\file`
422+
* - `/d/projects/app` becomes `D:\projects\app`
423+
* - Forward slashes become backslashes
372424
* - Drive letters are always uppercase in the output
373-
* - On Unix: Returns the path unchanged (passes through normalization)
374-
*
375-
* This is particularly important for:
376-
* - GitHub Actions runners where `command -v` returns MSYS paths
377-
* - Tools like sfw that need to resolve real binary paths on Windows
378-
* - Scripts that receive paths from Git Bash but need to pass them to native Windows tools
425+
* - On Unix: Returns the normalized path unchanged (forward slashes preserved)
379426
*
380427
* @param {string | Buffer | URL} pathLike - The MSYS/Unix-style path to convert
381-
* @returns {string} Native Windows path (e.g., `C:/path/to/file`) or normalized Unix path
428+
* @returns {string} Native Windows path (e.g., `C:\path\to\file`) or normalized Unix path
382429
*
383430
* @example
384431
* ```typescript
385-
* // MSYS drive letter paths
386-
* fromUnixPath('/c/projects/app/file.txt') // 'C:/projects/app/file.txt'
387-
* fromUnixPath('/d/projects/foo/bar') // 'D:/projects/foo/bar'
388-
*
389-
* // Non-drive Unix paths (unchanged)
390-
* fromUnixPath('/tmp/build/output') // '/tmp/build/output'
391-
* fromUnixPath('/usr/local/bin') // '/usr/local/bin'
432+
* // MSYS drive letter paths (Windows)
433+
* fromUnixPath('/c/projects/app/file.txt') // 'C:\\projects\\app\\file.txt'
434+
* fromUnixPath('/d/projects/foo/bar') // 'D:\\projects\\foo\\bar'
435+
* fromUnixPath('/c') // 'C:\\'
392436
*
393-
* // Already Windows paths (unchanged)
394-
* fromUnixPath('C:/Windows/System32') // 'C:/Windows/System32'
437+
* // Forward-slash paths (Windows)
438+
* fromUnixPath('C:/Windows/System32') // 'C:\\Windows\\System32'
395439
*
396-
* // Edge cases
397-
* fromUnixPath('/c') // 'C:/'
398-
* fromUnixPath('') // '.'
440+
* // Unix (unchanged, forward slashes preserved)
441+
* fromUnixPath('/tmp/build/output') // '/tmp/build/output'
399442
* ```
400443
*/
401444
/*@__NO_SIDE_EFFECTS__*/
402445
export function fromUnixPath(pathLike: string | Buffer | URL): string {
403446
const normalized = normalizePath(pathLike)
404-
405-
// On Windows, convert MSYS drive notation back to native: /c/path → C:/path
406447
if (WIN32) {
407-
return normalized.replace(
408-
/^\/([a-zA-Z])(\/|$)/,
409-
(_, letter, sep) => `${letter.toUpperCase()}:${sep || '/'}`,
410-
)
448+
return normalized.replace(/\//g, '\\')
411449
}
412-
413-
// On Unix, just return the normalized path
414450
return normalized
415451
}
416452

@@ -427,6 +463,7 @@ export function fromUnixPath(pathLike: string | Buffer | URL): string {
427463
* - Returns `.` for empty or collapsed paths
428464
*
429465
* Special handling:
466+
* - MSYS drive letters (Windows only): `/c/path` becomes `C:/path`
430467
* - UNC paths: Maintains double leading slashes for `//server/share` format
431468
* - Windows namespaces: Preserves `//./` and `//?/` prefixes
432469
* - Leading `..` segments: Preserved in relative paths without prefix
@@ -453,6 +490,10 @@ export function fromUnixPath(pathLike: string | Buffer | URL): string {
453490
* normalizePath('C:\\Users\\username\\file.txt') // 'C:/Users/username/file.txt'
454491
* normalizePath('foo\\bar\\baz') // 'foo/bar/baz'
455492
*
493+
* // MSYS drive letters (Windows only)
494+
* normalizePath('/c/projects/app') // 'C:/projects/app' (on Windows)
495+
* normalizePath('/d/Users/name') // 'D:/Users/name' (on Windows)
496+
*
456497
* // UNC paths
457498
* normalizePath('\\\\server\\share\\file') // '//server/share/file'
458499
*
@@ -592,7 +633,7 @@ export function normalizePath(pathLike: string | Buffer | URL): string {
592633
if (segment === '..') {
593634
return prefix ? prefix.slice(0, -1) || '/' : '..'
594635
}
595-
return prefix + segment
636+
return msysDriveToNative(prefix + segment)
596637
}
597638
// Process segments and handle '.', '..', and empty segments.
598639
let collapsed = ''
@@ -688,7 +729,7 @@ export function normalizePath(pathLike: string | Buffer | URL): string {
688729
if (collapsed.length === 0) {
689730
return prefix || '.'
690731
}
691-
return prefix + collapsed
732+
return msysDriveToNative(prefix + collapsed)
692733
}
693734

694735
/**

0 commit comments

Comments
 (0)