@@ -7,7 +7,14 @@ import gitIgnore from 'ignore';
77import isPathInside from 'is-path-inside' ;
88import slash from 'slash' ;
99import { toPath } from 'unicorn-magic' ;
10- import { isNegativePattern , bindFsMethod } from './utilities.js' ;
10+ import {
11+ isNegativePattern ,
12+ bindFsMethod ,
13+ promisifyFsMethod ,
14+ findGitRoot ,
15+ findGitRootSync ,
16+ getParentGitignorePaths ,
17+ } from './utilities.js' ;
1118
1219const defaultIgnoredDirectories = [
1320'**/node_modules' ,
@@ -24,13 +31,105 @@ export const GITIGNORE_FILES_PATTERN = '**/.gitignore';
2431
2532const getReadFileMethod = fsImplementation =>
2633bindFsMethod ( fsImplementation ?. promises , 'readFile' )
27- ?? bindFsMethod ( fsImplementation , 'readFile' )
28- ?? bindFsMethod ( fsPromises , 'readFile' ) ;
34+ ?? bindFsMethod ( fsPromises , 'readFile' )
35+ ?? promisifyFsMethod ( fsImplementation , 'readFile' ) ;
2936
3037const getReadFileSyncMethod = fsImplementation =>
3138bindFsMethod ( fsImplementation , 'readFileSync' )
3239?? bindFsMethod ( fs , 'readFileSync' ) ;
3340
41+ const shouldSkipIgnoreFileError = ( error , suppressErrors ) => {
42+ if ( ! error ) {
43+ return Boolean ( suppressErrors ) ;
44+ }
45+
46+ if ( error . code === 'ENOENT' || error . code === 'ENOTDIR' ) {
47+ return true ;
48+ }
49+
50+ return Boolean ( suppressErrors ) ;
51+ } ;
52+
53+ const createIgnoreFileReadError = ( filePath , error ) => {
54+ if ( error instanceof Error ) {
55+ error . message = `Failed to read ignore file at ${ filePath } : ${ error . message } ` ;
56+ return error ;
57+ }
58+
59+ return new Error ( `Failed to read ignore file at ${ filePath } : ${ String ( error ) } ` ) ;
60+ } ;
61+
62+ const processIgnoreFileCore = ( filePath , readMethod , suppressErrors ) => {
63+ try {
64+ const content = readMethod ( filePath , 'utf8' ) ;
65+ return { filePath, content} ;
66+ } catch ( error ) {
67+ if ( shouldSkipIgnoreFileError ( error , suppressErrors ) ) {
68+ return undefined ;
69+ }
70+
71+ throw createIgnoreFileReadError ( filePath , error ) ;
72+ }
73+ } ;
74+
75+ const readIgnoreFilesSafely = async ( paths , readFileMethod , suppressErrors ) => {
76+ const fileResults = await Promise . all ( paths . map ( async filePath => {
77+ try {
78+ const content = await readFileMethod ( filePath , 'utf8' ) ;
79+ return { filePath, content} ;
80+ } catch ( error ) {
81+ if ( shouldSkipIgnoreFileError ( error , suppressErrors ) ) {
82+ return undefined ;
83+ }
84+
85+ throw createIgnoreFileReadError ( filePath , error ) ;
86+ }
87+ } ) ) ;
88+
89+ return fileResults . filter ( Boolean ) ;
90+ } ;
91+
92+ const readIgnoreFilesSafelySync = ( paths , readFileSyncMethod , suppressErrors ) => paths
93+ . map ( filePath => processIgnoreFileCore ( filePath , readFileSyncMethod , suppressErrors ) )
94+ . filter ( Boolean ) ;
95+
96+ const dedupePaths = paths => {
97+ const seen = new Set ( ) ;
98+ return paths . filter ( filePath => {
99+ if ( seen . has ( filePath ) ) {
100+ return false ;
101+ }
102+
103+ seen . add ( filePath ) ;
104+ return true ;
105+ } ) ;
106+ } ;
107+
108+ const globIgnoreFiles = ( globFunction , patterns , normalizedOptions ) => globFunction ( patterns , {
109+ ...normalizedOptions ,
110+ ...ignoreFilesGlobOptions , // Must be last to ensure absolute/dot flags stick
111+ } ) ;
112+
113+ const getParentIgnorePaths = ( gitRoot , normalizedOptions ) => gitRoot
114+ ? getParentGitignorePaths ( gitRoot , normalizedOptions . cwd )
115+ : [ ] ;
116+
117+ const combineIgnoreFilePaths = ( gitRoot , normalizedOptions , childPaths ) => dedupePaths ( [
118+ ...getParentIgnorePaths ( gitRoot , normalizedOptions ) ,
119+ ...childPaths ,
120+ ] ) ;
121+
122+ const buildIgnoreResult = ( files , normalizedOptions , gitRoot ) => {
123+ const baseDir = gitRoot || normalizedOptions . cwd ;
124+ const patterns = getPatternsFromIgnoreFiles ( files , baseDir ) ;
125+
126+ return {
127+ patterns,
128+ predicate : createIgnorePredicate ( patterns , normalizedOptions . cwd , baseDir ) ,
129+ usingGitRoot : Boolean ( gitRoot && gitRoot !== normalizedOptions . cwd ) ,
130+ } ;
131+ } ;
132+
34133// Apply base path to gitignore patterns based on .gitignore spec 2.22.1
35134// https://git-scm.com/docs/gitignore#_pattern_format
36135// See also https://github.com/sindresorhus/globby/issues/146
@@ -107,19 +206,30 @@ const toRelativePath = (fileOrDirectory, cwd) => {
107206return fileOrDirectory ;
108207} ;
109208
110- const getIsIgnoredPredicate = ( files , cwd ) => {
111- const patterns = files . flatMap ( file => parseIgnoreFile ( file , cwd ) ) ;
209+ const createIgnorePredicate = ( patterns , cwd , baseDir ) => {
112210const ignores = gitIgnore ( ) . add ( patterns ) ;
211+ // Normalize to handle path separator and . / .. components consistently
212+ const resolvedCwd = path . normalize ( path . resolve ( cwd ) ) ;
213+ const resolvedBaseDir = path . normalize ( path . resolve ( baseDir ) ) ;
113214
114215return fileOrDirectory => {
115216fileOrDirectory = toPath ( fileOrDirectory ) ;
116- fileOrDirectory = toRelativePath ( fileOrDirectory , cwd ) ;
117- // If path is outside cwd (undefined), it can't be ignored by patterns in cwd
118- if ( fileOrDirectory === undefined ) {
217+
218+ // Never ignore the cwd itself - use normalized comparison
219+ const normalizedPath = path . normalize ( path . resolve ( fileOrDirectory ) ) ;
220+ if ( normalizedPath === resolvedCwd ) {
221+ return false ;
222+ }
223+
224+ // Convert to relative path from baseDir (use normalized baseDir)
225+ const relativePath = toRelativePath ( fileOrDirectory , resolvedBaseDir ) ;
226+
227+ // If path is outside baseDir (undefined), it can't be ignored by patterns
228+ if ( relativePath === undefined ) {
119229return false ;
120230}
121231
122- return fileOrDirectory ? ignores . ignores ( slash ( fileOrDirectory ) ) : false ;
232+ return relativePath ? ignores . ignores ( slash ( relativePath ) ) : false ;
123233} ;
124234} ;
125235
@@ -148,91 +258,79 @@ const normalizeOptions = (options = {}) => {
148258} ;
149259} ;
150260
151- export const isIgnoredByIgnoreFiles = async ( patterns , options ) => {
261+ const collectIgnoreFileArtifactsAsync = async ( patterns , options , includeParentIgnoreFiles ) => {
152262const normalizedOptions = normalizeOptions ( options ) ;
153-
154- const paths = await fastGlob ( patterns , {
155- ...normalizedOptions ,
156- ...ignoreFilesGlobOptions , // Must be last to ensure absolute and dot are always set
157- } ) ;
158-
263+ const childPaths = await globIgnoreFiles ( fastGlob , patterns , normalizedOptions ) ;
264+ const gitRoot = includeParentIgnoreFiles
265+ ? await findGitRoot ( normalizedOptions . cwd , normalizedOptions . fs )
266+ : undefined ;
267+ const allPaths = combineIgnoreFilePaths ( gitRoot , normalizedOptions , childPaths ) ;
159268const readFileMethod = getReadFileMethod ( normalizedOptions . fs ) ;
160- const files = await Promise . all ( paths . map ( async filePath => ( {
161- filePath,
162- content : await readFileMethod ( filePath , 'utf8' ) ,
163- } ) ) ) ;
269+ const files = await readIgnoreFilesSafely ( allPaths , readFileMethod , normalizedOptions . suppressErrors ) ;
164270
165- return getIsIgnoredPredicate ( files , normalizedOptions . cwd ) ;
271+ return { files, normalizedOptions, gitRoot } ;
166272} ;
167273
168- export const isIgnoredByIgnoreFilesSync = ( patterns , options ) => {
274+ const collectIgnoreFileArtifactsSync = ( patterns , options , includeParentIgnoreFiles ) => {
169275const normalizedOptions = normalizeOptions ( options ) ;
276+ const childPaths = globIgnoreFiles ( fastGlob . sync , patterns , normalizedOptions ) ;
277+ const gitRoot = includeParentIgnoreFiles
278+ ? findGitRootSync ( normalizedOptions . cwd , normalizedOptions . fs )
279+ : undefined ;
280+ const allPaths = combineIgnoreFilePaths ( gitRoot , normalizedOptions , childPaths ) ;
281+ const readFileSyncMethod = getReadFileSyncMethod ( normalizedOptions . fs ) ;
282+ const files = readIgnoreFilesSafelySync ( allPaths , readFileSyncMethod , normalizedOptions . suppressErrors ) ;
170283
171- const paths = fastGlob . sync ( patterns , {
172- ...normalizedOptions ,
173- ...ignoreFilesGlobOptions , // Must be last to ensure absolute and dot are always set
174- } ) ;
284+ return { files, normalizedOptions, gitRoot} ;
285+ } ;
175286
176- const readFileSyncMethod = getReadFileSyncMethod ( normalizedOptions . fs ) ;
177- const files = paths . map ( filePath => ( {
178- filePath,
179- content : readFileSyncMethod ( filePath , 'utf8' ) ,
180- } ) ) ;
287+ export const isIgnoredByIgnoreFiles = async ( patterns , options ) => {
288+ const { files, normalizedOptions, gitRoot} = await collectIgnoreFileArtifactsAsync ( patterns , options , false ) ;
289+ return buildIgnoreResult ( files , normalizedOptions , gitRoot ) . predicate ;
290+ } ;
181291
182- return getIsIgnoredPredicate ( files , normalizedOptions . cwd ) ;
292+ export const isIgnoredByIgnoreFilesSync = ( patterns , options ) => {
293+ const { files, normalizedOptions, gitRoot} = collectIgnoreFileArtifactsSync ( patterns , options , false ) ;
294+ return buildIgnoreResult ( files , normalizedOptions , gitRoot ) . predicate ;
183295} ;
184296
185- const getPatternsFromIgnoreFiles = ( files , cwd ) => files . flatMap ( file => parseIgnoreFile ( file , cwd ) ) ;
297+ const getPatternsFromIgnoreFiles = ( files , baseDir ) => files . flatMap ( file => parseIgnoreFile ( file , baseDir ) ) ;
186298
187299/**
188300Read ignore files and return both patterns and predicate.
189301This avoids reading the same files twice (once for patterns, once for filtering).
190302
191- @returns {Promise<{patterns: string[], predicate: Function}> }
303+ @param {string[] } patterns - Patterns to find ignore files
304+ @param {Object } options - Options object
305+ @param {boolean } [includeParentIgnoreFiles=false] - Whether to search for parent .gitignore files
306+ @returns {Promise<{patterns: string[], predicate: Function, usingGitRoot: boolean}> }
192307*/
193- export const getIgnorePatternsAndPredicate = async ( patterns , options ) => {
194- const normalizedOptions = normalizeOptions ( options ) ;
195-
196- const paths = await fastGlob ( patterns , {
197- ...normalizedOptions ,
198- ...ignoreFilesGlobOptions , // Must be last to ensure absolute and dot are always set
199- } ) ;
200-
201- const readFileMethod = getReadFileMethod ( normalizedOptions . fs ) ;
202- const files = await Promise . all ( paths . map ( async filePath => ( {
203- filePath,
204- content : await readFileMethod ( filePath , 'utf8' ) ,
205- } ) ) ) ;
206-
207- return {
208- patterns : getPatternsFromIgnoreFiles ( files , normalizedOptions . cwd ) ,
209- predicate : getIsIgnoredPredicate ( files , normalizedOptions . cwd ) ,
210- } ;
308+ export const getIgnorePatternsAndPredicate = async ( patterns , options , includeParentIgnoreFiles = false ) => {
309+ const { files, normalizedOptions, gitRoot} = await collectIgnoreFileArtifactsAsync (
310+ patterns ,
311+ options ,
312+ includeParentIgnoreFiles ,
313+ ) ;
314+
315+ return buildIgnoreResult ( files , normalizedOptions , gitRoot ) ;
211316} ;
212317
213318/**
214319Read ignore files and return both patterns and predicate (sync version).
215320
216- @returns {{patterns: string[], predicate: Function} }
321+ @param {string[] } patterns - Patterns to find ignore files
322+ @param {Object } options - Options object
323+ @param {boolean } [includeParentIgnoreFiles=false] - Whether to search for parent .gitignore files
324+ @returns {{patterns: string[], predicate: Function, usingGitRoot: boolean} }
217325*/
218- export const getIgnorePatternsAndPredicateSync = ( patterns , options ) => {
219- const normalizedOptions = normalizeOptions ( options ) ;
220-
221- const paths = fastGlob . sync ( patterns , {
222- ...normalizedOptions ,
223- ...ignoreFilesGlobOptions , // Must be last to ensure absolute and dot are always set
224- } ) ;
225-
226- const readFileSyncMethod = getReadFileSyncMethod ( normalizedOptions . fs ) ;
227- const files = paths . map ( filePath => ( {
228- filePath,
229- content : readFileSyncMethod ( filePath , 'utf8' ) ,
230- } ) ) ;
231-
232- return {
233- patterns : getPatternsFromIgnoreFiles ( files , normalizedOptions . cwd ) ,
234- predicate : getIsIgnoredPredicate ( files , normalizedOptions . cwd ) ,
235- } ;
326+ export const getIgnorePatternsAndPredicateSync = ( patterns , options , includeParentIgnoreFiles = false ) => {
327+ const { files, normalizedOptions, gitRoot} = collectIgnoreFileArtifactsSync (
328+ patterns ,
329+ options ,
330+ includeParentIgnoreFiles ,
331+ ) ;
332+
333+ return buildIgnoreResult ( files , normalizedOptions , gitRoot ) ;
236334} ;
237335
238336export const isGitIgnored = options => isIgnoredByIgnoreFiles ( GITIGNORE_FILES_PATTERN , options ) ;
0 commit comments