Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ packages/*/__tests__/_temp/
.DS_Store
*.xar
packages/*/audit.json
.nx/
83 changes: 83 additions & 0 deletions packages/glob/__tests__/windows-path-matching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Test to validate that glob works correctly on Windows with backslash paths
* This test validates the fix for glob not working on GitHub's Windows runners
*/

import {MatchKind} from '../src/internal-match-kind'
import {Pattern} from '../src/internal-pattern'

const IS_WINDOWS = process.platform === 'win32'

describe('Windows path matching', () => {
it('matches paths with backslashes on Windows', () => {
if (!IS_WINDOWS) {
// This test is only relevant on Windows
return
}

// Test basic pattern matching with Windows paths
const pattern = new Pattern('C:\\Users\\test\\*')

// The itemPath would come from fs.readdir with backslashes on Windows
const itemPath = 'C:\\Users\\test\\file.txt'

// This should match because the pattern and path both refer to the same file
expect(pattern.match(itemPath)).toBe(MatchKind.All)
})

it('partial matches work with backslashes on Windows', () => {
if (!IS_WINDOWS) {
return
}

// Test partial matching with Windows paths
const pattern = new Pattern('C:\\Users\\test\\**')

// Should partially match parent directory
expect(pattern.partialMatch('C:\\Users')).toBe(true)
expect(pattern.partialMatch('C:\\Users\\test')).toBe(true)
})

it('matches globstar patterns with backslashes on Windows', () => {
if (!IS_WINDOWS) {
return
}

const pattern = new Pattern('C:\\foo\\**')

// Should match the directory itself and descendants
expect(pattern.match('C:\\foo')).toBe(MatchKind.All)
expect(pattern.match('C:\\foo\\bar')).toBe(MatchKind.All)
expect(pattern.match('C:\\foo\\bar\\baz.txt')).toBe(MatchKind.All)
})

it('matches wildcard patterns with mixed separators on Windows', () => {
if (!IS_WINDOWS) {
return
}

// Pattern might be specified with forward slashes by user
const pattern = new Pattern('C:/Users/*/file.txt')

// But the actual path from filesystem will have backslashes
expect(pattern.match('C:\\Users\\test\\file.txt')).toBe(MatchKind.All)
})

it('handles complex patterns with backslashes on Windows', () => {
if (!IS_WINDOWS) {
return
}

const currentDrive = process.cwd().substring(0, 2)
const pattern = new Pattern(`${currentDrive}\\**\\*.txt`)

// Should match .txt files at any depth
expect(pattern.match(`${currentDrive}\\file.txt`)).toBe(MatchKind.All)
expect(pattern.match(`${currentDrive}\\foo\\bar\\test.txt`)).toBe(
MatchKind.All
)
expect(pattern.match(`${currentDrive}\\foo\\bar\\test.js`)).toBe(
MatchKind.None
)
})
})
21 changes: 20 additions & 1 deletion packages/glob/src/internal-pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ export class Pattern {
itemPath = pathHelper.safeTrimTrailingSeparator(itemPath)
}

// Convert to forward slashes on Windows before matching with minimatch
// since the pattern was converted to forward slashes in the constructor
itemPath = Pattern.convertToMinimatchPath(itemPath)

// Match
if (this.minimatch.match(itemPath)) {
return this.trailingSeparator ? MatchKind.Directory : MatchKind.All
Expand All @@ -176,8 +180,11 @@ export class Pattern {
return this.rootRegExp.test(itemPath)
}

// Convert to forward slashes on Windows to match the pattern format used by minimatch
itemPath = Pattern.convertToMinimatchPath(itemPath)

return this.minimatch.matchOne(
itemPath.split(IS_WINDOWS ? /\\+/ : /\/+/),
itemPath.split(/\/+/),
this.minimatch.set[0],
true
)
Expand All @@ -193,6 +200,18 @@ export class Pattern {
.replace(/\*/g, '[*]') // escape '*'
}

/**
* Converts path to forward slashes on Windows for compatibility with minimatch.
* On Windows, minimatch patterns use forward slashes, so paths must be converted
* to match the same format.
*/
private static convertToMinimatchPath(itemPath: string): string {
if (IS_WINDOWS) {
return itemPath.replace(/\\/g, '/')
}
return itemPath
}

/**
* Normalizes slashes and ensures absolute root
*/
Expand Down