diff --git a/packages/glob/__tests__/internal-pattern-helper.test.ts b/packages/glob/__tests__/internal-pattern-helper.test.ts index 1d60f6b3b4..d95aa25f65 100644 --- a/packages/glob/__tests__/internal-pattern-helper.test.ts +++ b/packages/glob/__tests__/internal-pattern-helper.test.ts @@ -128,6 +128,36 @@ describe('pattern-helper', () => { ]) }) + it('matches when itemPath uses Windows separators', () => { + if (!IS_WINDOWS) return + const root = 'C\\\\' + const patterns = [ + `${root}**/*.proj`, + `${root}**/README.txt`, + `!${root}**/solution2/**`, + `${root}**/*.sln`, + `!${root}**/proj2/README.txt` + ].map(x => new Pattern(x)) + expect( + patternHelper.match( + patterns, + path.join(root, 'solution1', 'proj1', 'proj1.proj') + ) + ).toBe(MatchKind.All) + expect( + patternHelper.match( + patterns, + path.join(root, 'solution1', 'proj2', 'README.txt') + ) + ).toBe(MatchKind.All) + expect( + patternHelper.match( + patterns, + path.join(root, 'solution2', 'proj1', 'README.txt') + ) + ).toBe(MatchKind.None) + }) + it('partialMatch skips negate patterns', () => { const root = IS_WINDOWS ? 'C:\\' : '/' const patterns = [ diff --git a/packages/glob/__tests__/internal-pattern.test.ts b/packages/glob/__tests__/internal-pattern.test.ts index 8a9ecc85cb..33325f0803 100644 --- a/packages/glob/__tests__/internal-pattern.test.ts +++ b/packages/glob/__tests__/internal-pattern.test.ts @@ -101,6 +101,17 @@ describe('pattern', () => { ) }) + it('matches when itemPath uses Windows separators', () => { + if (!IS_WINDOWS) return + const root = 'C\\\\' + const pattern = new Pattern(`${root}Foo/**/Baz`) + // itemPath uses '\\' separators; toMinimatchPath should normalize + expect(pattern.match(path.join(root, 'Foo', 'Baz'))).toBe(MatchKind.All) + expect(pattern.match(path.join(root, 'Foo', 'bar', 'bAZ'))).toBe( + MatchKind.All + ) + }) + it('is case insensitive partial match on Windows', () => { const root = IS_WINDOWS ? 'C:\\' : '/' const pattern = new Pattern(`${root}Foo/Bar/**/Baz`) diff --git a/packages/glob/src/internal-path-helper.ts b/packages/glob/src/internal-path-helper.ts index 0931bd2cd0..fe1bd18b06 100644 --- a/packages/glob/src/internal-path-helper.ts +++ b/packages/glob/src/internal-path-helper.ts @@ -204,3 +204,13 @@ export function safeTrimTrailingSeparator(p: string): string { // Otherwise trim trailing slash return p.substr(0, p.length - 1) } + +/** + * Converts a filesystem path to a Minimatch-friendly path. + * Minimatch operates on POSIX-style '/' separators across platforms. + * On Windows, convert '\\' to '/'. Otherwise, return the path unchanged. + */ +export function toMinimatchPath(p: string): string { + if (!p) return '' + return IS_WINDOWS ? p.replace(/\\/g, '/') : p +} diff --git a/packages/glob/src/internal-pattern.ts b/packages/glob/src/internal-pattern.ts index e1dbbda843..90a10e5e4d 100644 --- a/packages/glob/src/internal-pattern.ts +++ b/packages/glob/src/internal-pattern.ts @@ -130,8 +130,9 @@ export class Pattern { noext: true, nonegate: true } - pattern = IS_WINDOWS ? pattern.replace(/\\/g, '/') : pattern - this.minimatch = new Minimatch(pattern, minimatchOptions) + // Convert to a Minimatch-friendly form using POSIX separators + const minimatchPattern = pathHelper.toMinimatchPath(pattern) + this.minimatch = new Minimatch(minimatchPattern, minimatchOptions) } /** @@ -156,8 +157,11 @@ export class Pattern { itemPath = pathHelper.safeTrimTrailingSeparator(itemPath) } + // Convert to a Minimatch-friendly form using POSIX separators + const itemPathForMatch = pathHelper.toMinimatchPath(itemPath) + // Match - if (this.minimatch.match(itemPath)) { + if (this.minimatch.match(itemPathForMatch)) { return this.trailingSeparator ? MatchKind.Directory : MatchKind.All } @@ -176,8 +180,9 @@ export class Pattern { return this.rootRegExp.test(itemPath) } + const mmItem = pathHelper.toMinimatchPath(itemPath) return this.minimatch.matchOne( - itemPath.split(IS_WINDOWS ? /\\+/ : /\/+/), + mmItem.split(/\/+/), this.minimatch.set[0], true )