diff --git a/src/utils/glob.mts b/src/utils/glob.mts index 7fa86042f..469fe74b4 100644 --- a/src/utils/glob.mts +++ b/src/utils/glob.mts @@ -135,6 +135,22 @@ function ignorePatternToMinimatch(pattern: string): string { return `${negatedPrefix}${matchEverywherePrefix}${escapedPatternWithoutLeadingSlash}${matchInsideSuffix}` } +// fast-glob silently discards `ignore` entries that end in `/` (it +// treats them as literal directory paths, not glob patterns). The +// gitignore convention of writing directory entries as `dist/` lands +// here as `**/dist/` after `ignorePatternToMinimatch`, which fast-glob +// then drops — defeating the entire ignore. Strip the trailing slash +// so fast-glob actually honors the pattern. +function stripTrailingSlash(pattern: string): string { + if ( + pattern.length > 1 && + pattern.charCodeAt(pattern.length - 1) === 47 /*'/'*/ + ) { + return pattern.slice(0, -1) + } + return pattern +} + function workspacePatternToGlobPattern(workspace: string): string { const { length } = workspace if (!length) { @@ -252,7 +268,9 @@ export async function globWithGitIgnore( absolute: true, cwd, dot: true, - ignore: hasNegatedPattern ? defaultIgnore : [...ignores], + ignore: hasNegatedPattern + ? defaultIgnore + : [...ignores].map(stripTrailingSlash), ...additionalOptions, } as GlobOptions diff --git a/src/utils/glob.test.mts b/src/utils/glob.test.mts index 111287576..7bc132a65 100644 --- a/src/utils/glob.test.mts +++ b/src/utils/glob.test.mts @@ -182,6 +182,25 @@ describe('glob utilities', () => { ]) }) + it('should skip directories ignored via trailing-slash patterns', async () => { + // fast-glob silently drops `ignore` entries that end in `/`, so + // the pattern produced for `dist/` (`**/dist/`) used to do + // nothing at the walk level and dist contents leaked through. + mockTestFs({ + [`${mockFixturePath}/.gitignore`]: 'dist/\n', + [`${mockFixturePath}/package.json`]: '{}', + [`${mockFixturePath}/dist/a.json`]: '{}', + }) + + const results = await globWithGitIgnore(['**/*.json'], { + cwd: mockFixturePath, + }) + + expect(results.map(normalizePath).sort()).toEqual([ + `${mockFixturePath}/package.json`, + ]) + }) + it('should combine filter with negated gitignore patterns', async () => { mockTestFs({ [`${mockFixturePath}/.gitignore`]: 'build/**\n!build/manifest.json',