@cjser/globby 16.2.1-cjser.2 → 16.2.3-cjser.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ignore.js CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  findGitRoot,
16
16
  findGitRootSync,
17
17
  getParentGitignorePaths,
18
+ buildPrunePatternsAndGuards,
19
+ negationsCouldRescue,
18
20
  } from './utilities.js';
19
21
 
20
22
  const defaultIgnoredDirectories = [
@@ -117,14 +119,43 @@ const globIgnoreFiles = (globFunction, patterns, normalizedOptions) => globFunct
117
119
  ...ignoreFilesGlobOptions, // Must be last to ensure absolute/dot flags stick
118
120
  });
119
121
 
120
- const getParentIgnorePaths = (gitRoot, normalizedOptions) => gitRoot
121
- ? getParentGitignorePaths(gitRoot, normalizedOptions.cwd)
122
- : [];
122
+ // Normalize a raw ignore-file line the way git does: strip a byte order mark and trailing whitespace. "Trailing spaces are ignored unless they are quoted with backslash" - a backslash-escaped trailing space is kept, still escaped, so the line can be re-fed to the `ignore` package without being stripped again.
123
+ const normalizeIgnoreFileLine = line => {
124
+ line = line.replace(/^\uFEFF/u, '');
123
125
 
124
- const combineIgnoreFilePaths = (gitRoot, normalizedOptions, childPaths) => dedupePaths([
125
- ...getParentIgnorePaths(gitRoot, normalizedOptions),
126
- ...childPaths,
127
- ]);
126
+ let whitespaceStart = line.length;
127
+ while (whitespaceStart > 0 && /\s/u.test(line[whitespaceStart - 1])) {
128
+ whitespaceStart--;
129
+ }
130
+
131
+ if (whitespaceStart === line.length) {
132
+ return line;
133
+ }
134
+
135
+ let backslashCount = 0;
136
+ for (let index = whitespaceStart - 1; index >= 0 && line[index] === '\\'; index--) {
137
+ backslashCount++;
138
+ }
139
+
140
+ return backslashCount % 2 === 1
141
+ ? line.slice(0, whitespaceStart) + ' '
142
+ : line.slice(0, whitespaceStart);
143
+ };
144
+
145
+ const readIgnoreFileLines = content => content
146
+ .split(/\r?\n/)
147
+ .map(line => normalizeIgnoreFileLine(line))
148
+ .filter(line => line && !line.startsWith('#'));
149
+
150
+ /**
151
+ Get the lines of every ignore file, as git reads them, each paired with the directory of the file that declared them.
152
+
153
+ Anchoring is only meaningful relative to that directory, and the rebased patterns have already lost it, so keep the originals for anything that has to reason about which paths a rule covers.
154
+ */
155
+ const getIgnoreRules = files => files.flatMap(file => {
156
+ const directory = path.dirname(file.filePath);
157
+ return readIgnoreFileLines(file.content).map(pattern => ({pattern, directory}));
158
+ });
128
159
 
129
160
  const buildIgnoreResult = (files, normalizedOptions, gitRoot) => {
130
161
  const baseDir = gitRoot || normalizedOptions.cwd;
@@ -133,6 +164,7 @@ const buildIgnoreResult = (files, normalizedOptions, gitRoot) => {
133
164
 
134
165
  return {
135
166
  patterns,
167
+ rules: getIgnoreRules(files),
136
168
  matcher,
137
169
  predicate: fileOrDirectory => matcher(fileOrDirectory).ignored,
138
170
  usingGitRoot: Boolean(gitRoot && gitRoot !== normalizedOptions.cwd),
@@ -177,11 +209,7 @@ const applyBaseToPattern = (pattern, base) => {
177
209
 
178
210
  const parseIgnoreFile = (file, cwd) => {
179
211
  const base = slash(path.relative(cwd, path.dirname(file.filePath)));
180
-
181
- return file.content
182
- .split(/\r?\n/)
183
- .filter(line => line && !line.startsWith('#'))
184
- .map(pattern => applyBaseToPattern(pattern, base));
212
+ return readIgnoreFileLines(file.content).map(pattern => applyBaseToPattern(pattern, base));
185
213
  };
186
214
 
187
215
  const toRelativePath = (fileOrDirectory, cwd) => {
@@ -515,6 +543,7 @@ const createExcludesFileValue = (value, declaringFilePath) => ({
515
543
 
516
544
  /**
517
545
  Parse git config content and return the excludesFile value and any include paths to recurse into.
546
+
518
547
  The caller is responsible for reading files and recursing (sync or async).
519
548
  */
520
549
  const parseGitConfigForExcludesFile = (content, normalizedPath, gitDirectory) => {
@@ -789,30 +818,149 @@ export const buildGlobalPredicate = (globalIgnoreFile, cwd, rootDirectory = cwd)
789
818
  return fileOrDirectory => matcher(fileOrDirectory).ignored;
790
819
  };
791
820
 
821
+ /**
822
+ Ignore files at or above the cwd can be located without traversing anything.
823
+
824
+ Reading them before searching for the nested ones lets the search itself skip whole ignored directories. Otherwise the recursive search for ignore files walks even the directories that the same `.gitignore` excludes, so a large ignored directory (a mounted share, a build output) is enumerated on every call.
825
+ */
826
+ const getKnownIgnoreFilePaths = (patterns, normalizedOptions, gitRoot) => {
827
+ const searchPatterns = [patterns].flat();
828
+ const isGitignoreSearch = searchPatterns.includes(GITIGNORE_FILES_PATTERN);
829
+ if (!isGitignoreSearch) {
830
+ return [];
831
+ }
832
+
833
+ return gitRoot
834
+ ? getParentGitignorePaths(gitRoot, normalizedOptions.cwd)
835
+ : [path.join(normalizedOptions.cwd, '.gitignore')];
836
+ };
837
+
838
+ const getKnownIgnoreFileSearchOptions = (patterns, normalizedOptions) => ({
839
+ ...normalizedOptions,
840
+ ignore: [
841
+ ...normalizedOptions.ignore,
842
+ // Keep negative search patterns active when the known candidates are matched independently.
843
+ ...[patterns].flat()
844
+ .filter(pattern => isNegativePattern(pattern))
845
+ .map(pattern => pattern.slice(1)),
846
+ ],
847
+ });
848
+
849
+ const getKnownIgnoreFilePattern = (filePath, cwd) => {
850
+ // Relative candidates keep cwd-relative exclusions such as `.gitignore` meaningful; parent candidates must remain absolute because they are outside cwd.
851
+ const pattern = isPathInside(filePath, cwd) ? path.relative(cwd, filePath) : filePath;
852
+ return fastGlob.convertPathToPattern(pattern);
853
+ };
854
+
855
+ const getMatchingKnownIgnoreFilePaths = (knownPaths, matchingPaths) => {
856
+ // Fast-glob normalizes its results, so compare resolved paths before returning the original paths for reading.
857
+ const matchingPathSet = new Set(matchingPaths.map(filePath => path.resolve(filePath)));
858
+
859
+ return knownPaths.filter(filePath => matchingPathSet.has(path.resolve(filePath)));
860
+ };
861
+
862
+ const globKnownIgnoreFilePaths = (globFunction, knownPaths, patterns, normalizedOptions) => {
863
+ if (knownPaths.length === 0) {
864
+ return [];
865
+ }
866
+
867
+ return globIgnoreFiles(
868
+ globFunction,
869
+ knownPaths.map(filePath => getKnownIgnoreFilePattern(filePath, normalizedOptions.cwd)),
870
+ getKnownIgnoreFileSearchOptions(patterns, normalizedOptions),
871
+ );
872
+ };
873
+
874
+ const filterKnownIgnoreFilePathsAsync = async (knownPaths, patterns, normalizedOptions) => {
875
+ const matchingPaths = await globKnownIgnoreFilePaths(fastGlob, knownPaths, patterns, normalizedOptions);
876
+ return getMatchingKnownIgnoreFilePaths(knownPaths, matchingPaths);
877
+ };
878
+
879
+ const filterKnownIgnoreFilePathsSync = (knownPaths, patterns, normalizedOptions) => {
880
+ const matchingPaths = globKnownIgnoreFilePaths(fastGlob.sync, knownPaths, patterns, normalizedOptions);
881
+ return getMatchingKnownIgnoreFilePaths(knownPaths, matchingPaths);
882
+ };
883
+
884
+ const getIgnoreFileSearchPrune = (searchPatterns, files, normalizedOptions, gitRoot) => {
885
+ if (files.length === 0) {
886
+ return {patterns: [], guardNames: []};
887
+ }
888
+
889
+ const {cwd} = normalizedOptions;
890
+ const baseDir = gitRoot || cwd;
891
+ const ignorePatterns = getPatternsFromIgnoreFiles(files, baseDir);
892
+ const matcher = createIgnoreMatcher(ignorePatterns, cwd, baseDir);
893
+
894
+ // Custom ignore files can live anywhere, including beside the cwd, so only a pure `.gitignore` search may treat the rules at or above the cwd as complete.
895
+ const searchPatternsArray = [searchPatterns].flat();
896
+ const gitignoreOnlySearch = searchPatternsArray.every(pattern => pattern === GITIGNORE_FILES_PATTERN);
897
+ const searchesForGitignoreFiles = searchPatternsArray.includes(GITIGNORE_FILES_PATTERN);
898
+
899
+ return buildPrunePatternsAndGuards(getIgnoreRules(files), matcher, cwd, {gitignoreOnlySearch, searchesForGitignoreFiles});
900
+ };
901
+
902
+ const withPrunedSearch = (normalizedOptions, prunePatterns) => prunePatterns.length === 0
903
+ ? normalizedOptions
904
+ : {...normalizedOptions, ignore: [...normalizedOptions.ignore, ...prunePatterns]};
905
+
906
+ // The known ignore files have already been read; only the newly discovered ones are left.
907
+ const getUnreadPaths = (childPaths, knownPaths) => {
908
+ const alreadyRead = new Set(knownPaths.map(filePath => path.resolve(filePath)));
909
+ return dedupePaths(childPaths).filter(filePath => !alreadyRead.has(path.resolve(filePath)));
910
+ };
911
+
792
912
  const collectIgnoreFileArtifactsAsync = async (patterns, options, includeParentIgnoreFiles) => {
793
913
  const normalizedOptions = normalizeOptions(options);
794
- const childPaths = await globIgnoreFiles(fastGlob, patterns, normalizedOptions);
914
+ const readFileMethod = getReadFileMethod(normalizedOptions.fs);
795
915
  const gitRoot = includeParentIgnoreFiles
796
916
  ? await findGitRoot(normalizedOptions.cwd, normalizedOptions.fs)
797
917
  : undefined;
798
- const allPaths = combineIgnoreFilePaths(gitRoot, normalizedOptions, childPaths);
799
- const readFileMethod = getReadFileMethod(normalizedOptions.fs);
800
- const files = await readIgnoreFilesSafely(allPaths, readFileMethod, normalizedOptions.suppressErrors);
801
918
 
802
- return {files, normalizedOptions, gitRoot};
919
+ const knownPaths = await filterKnownIgnoreFilePathsAsync(
920
+ getKnownIgnoreFilePaths(patterns, normalizedOptions, gitRoot),
921
+ patterns,
922
+ normalizedOptions,
923
+ );
924
+ const knownFiles = await readIgnoreFilesSafely(knownPaths, readFileMethod, normalizedOptions.suppressErrors);
925
+ const {patterns: prunePatterns, guardNames} = getIgnoreFileSearchPrune(patterns, knownFiles, normalizedOptions, gitRoot);
926
+
927
+ const childPaths = await globIgnoreFiles(fastGlob, patterns, withPrunedSearch(normalizedOptions, prunePatterns));
928
+ let childFiles = await readIgnoreFilesSafely(getUnreadPaths(childPaths, knownPaths), readFileMethod, normalizedOptions.suppressErrors);
929
+
930
+ // A negation found by the pruned search can re-include a directory the prune patterns skipped, hiding the ignore files inside it. Repeat the search without pruning when that happens; a single unpruned pass finds everything, so once is enough.
931
+ if (negationsCouldRescue(getIgnoreRules(childFiles), guardNames)) {
932
+ const allPaths = await globIgnoreFiles(fastGlob, patterns, normalizedOptions);
933
+ childFiles = await readIgnoreFilesSafely(getUnreadPaths(allPaths, knownPaths), readFileMethod, normalizedOptions.suppressErrors);
934
+ }
935
+
936
+ return {files: [...knownFiles, ...childFiles], normalizedOptions, gitRoot};
803
937
  };
804
938
 
805
939
  const collectIgnoreFileArtifactsSync = (patterns, options, includeParentIgnoreFiles) => {
806
940
  const normalizedOptions = normalizeOptions(options);
807
- const childPaths = globIgnoreFiles(fastGlob.sync, patterns, normalizedOptions);
941
+ const readFileSyncMethod = getReadFileSyncMethod(normalizedOptions.fs);
808
942
  const gitRoot = includeParentIgnoreFiles
809
943
  ? findGitRootSync(normalizedOptions.cwd, normalizedOptions.fs)
810
944
  : undefined;
811
- const allPaths = combineIgnoreFilePaths(gitRoot, normalizedOptions, childPaths);
812
- const readFileSyncMethod = getReadFileSyncMethod(normalizedOptions.fs);
813
- const files = readIgnoreFilesSafelySync(allPaths, readFileSyncMethod, normalizedOptions.suppressErrors);
814
945
 
815
- return {files, normalizedOptions, gitRoot};
946
+ const knownPaths = filterKnownIgnoreFilePathsSync(
947
+ getKnownIgnoreFilePaths(patterns, normalizedOptions, gitRoot),
948
+ patterns,
949
+ normalizedOptions,
950
+ );
951
+ const knownFiles = readIgnoreFilesSafelySync(knownPaths, readFileSyncMethod, normalizedOptions.suppressErrors);
952
+ const {patterns: prunePatterns, guardNames} = getIgnoreFileSearchPrune(patterns, knownFiles, normalizedOptions, gitRoot);
953
+
954
+ const childPaths = globIgnoreFiles(fastGlob.sync, patterns, withPrunedSearch(normalizedOptions, prunePatterns));
955
+ let childFiles = readIgnoreFilesSafelySync(getUnreadPaths(childPaths, knownPaths), readFileSyncMethod, normalizedOptions.suppressErrors);
956
+
957
+ // See `collectIgnoreFileArtifactsAsync`: a rescuing negation means the pruned search may have missed files.
958
+ if (negationsCouldRescue(getIgnoreRules(childFiles), guardNames)) {
959
+ const allPaths = globIgnoreFiles(fastGlob.sync, patterns, normalizedOptions);
960
+ childFiles = readIgnoreFilesSafelySync(getUnreadPaths(allPaths, knownPaths), readFileSyncMethod, normalizedOptions.suppressErrors);
961
+ }
962
+
963
+ return {files: [...knownFiles, ...childFiles], normalizedOptions, gitRoot};
816
964
  };
817
965
 
818
966
  export const isIgnoredByIgnoreFiles = async (patterns, options) => {
@@ -834,7 +982,7 @@ This avoids reading the same files twice (once for patterns, once for filtering)
834
982
  @param {string[]} patterns - Patterns to find ignore files
835
983
  @param {Object} options - Options object
836
984
  @param {boolean} [includeParentIgnoreFiles=false] - Whether to search for parent .gitignore files
837
- @returns {Promise<{patterns: string[], matcher: Function, predicate: Function, usingGitRoot: boolean}>}
985
+ @returns {Promise<{patterns: string[], rules: Array<{pattern: string, directory: string}>, matcher: Function, predicate: Function, usingGitRoot: boolean}>}
838
986
  */
839
987
  export const getIgnorePatternsAndPredicate = async (patterns, options, includeParentIgnoreFiles = false) => {
840
988
  const {files, normalizedOptions, gitRoot} = await collectIgnoreFileArtifactsAsync(
@@ -852,7 +1000,7 @@ Read ignore files and return both patterns and predicate (sync version).
852
1000
  @param {string[]} patterns - Patterns to find ignore files
853
1001
  @param {Object} options - Options object
854
1002
  @param {boolean} [includeParentIgnoreFiles=false] - Whether to search for parent .gitignore files
855
- @returns {{patterns: string[], matcher: Function, predicate: Function, usingGitRoot: boolean}}
1003
+ @returns {{patterns: string[], rules: Array<{pattern: string, directory: string}>, matcher: Function, predicate: Function, usingGitRoot: boolean}}
856
1004
  */
857
1005
  export const getIgnorePatternsAndPredicateSync = (patterns, options, includeParentIgnoreFiles = false) => {
858
1006
  const {files, normalizedOptions, gitRoot} = collectIgnoreFileArtifactsSync(
package/index.js CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  isNegativePattern,
20
20
  getStaticAbsolutePathPrefix,
21
21
  normalizeNegativePattern,
22
- normalizeDirectoryPatternForFastGlob,
23
22
  adjustIgnorePatternsForParentDirectories,
24
23
  convertPatternsForFastGlob,
25
24
  findGitRoot,
@@ -233,31 +232,28 @@ const combinePredicate = (matcher, globalMatcher) => {
233
232
  };
234
233
  };
235
234
 
236
- const buildIgnoreFilterResult = (options, cwd, {patterns, matcher, usingGitRoot}, globalMatcher, createFilter) => {
235
+ const buildIgnoreFilterResult = ({options, cwd, ignoreResult: {rules, matcher}, globalMatcher, createFilter}) => {
237
236
  const finalPredicate = combinePredicate(matcher, globalMatcher);
238
237
 
239
- // Convert patterns to fast-glob format (may return empty array if predicate should handle everything)
240
- const patternsForFastGlob = convertPatternsForFastGlob(patterns, usingGitRoot, normalizeDirectoryPatternForFastGlob);
238
+ // Patterns fast-glob can use to skip ignored directories while traversing. The predicate below stays authoritative, so this only ever needs to be safe, never exhaustive. They are returned separately from `options.ignore` so they skip directory expansion and the parent-directory adjustment, which would rebase them outside the scope their ignore files govern.
239
+ const pruneIgnorePatterns = convertPatternsForFastGlob(rules, matcher, cwd);
241
240
 
242
241
  return {
243
- options: {
244
- ...options,
245
- ignore: [...options.ignore, ...patternsForFastGlob],
246
- },
242
+ options,
243
+ pruneIgnorePatterns,
247
244
  filter: createFilter(finalPredicate, cwd, options.fs),
248
245
  };
249
246
  };
250
247
 
251
248
  /**
252
- Apply gitignore patterns to options and return filter predicate.
253
-
254
- When negation patterns are present (e.g., '!important.log'), we cannot pass positive patterns to fast-glob because it would filter out files before our predicate can re-include them. In this case, we rely entirely on the predicate for filtering, which handles negations correctly.
249
+ Apply ignore files to options and return the filter predicate.
255
250
 
256
- When there are no negations, we optimize by passing patterns to fast-glob's ignore option to skip directories during traversal (performance optimization).
251
+ The predicate handles every rule, including negations, and is the authoritative filter applied
252
+ to fast-glob's results. Rules that provably cannot be affected by negations are additionally
253
+ translated into fast-glob `ignore` patterns, so whole ignored directories are skipped during
254
+ traversal; see `convertPatternsForFastGlob`.
257
255
 
258
- All patterns (including negated) are always used in the filter predicate to ensure correct Git-compatible behavior.
259
-
260
- @returns {Promise<{options: Object, filter: Function}>}
256
+ @returns {Promise<{options: Object, pruneIgnorePatterns: string[], filter: Function}>}
261
257
  */
262
258
  const applyIgnoreFilesAndGetFilter = async options => {
263
259
  const cwd = options.cwd ?? process.cwd();
@@ -267,6 +263,7 @@ const applyIgnoreFilesAndGetFilter = async options => {
267
263
  if (ignoreFilesPatterns.length === 0 && !globalIgnoreFile) {
268
264
  return {
269
265
  options,
266
+ pruneIgnorePatterns: [],
270
267
  filter: createFilterFunctionAsync(false, cwd, options.fs),
271
268
  };
272
269
  }
@@ -276,18 +273,24 @@ const applyIgnoreFilesAndGetFilter = async options => {
276
273
  const includeParentIgnoreFiles = options.gitignore === true;
277
274
  const ignoreResult = ignoreFilesPatterns.length > 0
278
275
  ? await getIgnorePatternsAndPredicate(ignoreFilesPatterns, options, includeParentIgnoreFiles)
279
- : {patterns: [], matcher: false, usingGitRoot: false};
276
+ : {rules: [], matcher: false};
280
277
 
281
278
  const globalGitRoot = globalIgnoreFile ? await findGitRoot(cwd, options.fs) : undefined;
282
279
  const globalMatcher = globalIgnoreFile ? buildGlobalMatcher(globalIgnoreFile, cwd, globalGitRoot ?? cwd) : undefined;
283
280
 
284
- return buildIgnoreFilterResult(options, cwd, ignoreResult, globalMatcher, createFilterFunctionAsync);
281
+ return buildIgnoreFilterResult({
282
+ options,
283
+ cwd,
284
+ ignoreResult,
285
+ globalMatcher,
286
+ createFilter: createFilterFunctionAsync,
287
+ });
285
288
  };
286
289
 
287
290
  /**
288
- Apply gitignore patterns to options and return filter predicate (sync version).
291
+ Apply ignore files to options and return the filter predicate (sync version).
289
292
 
290
- @returns {{options: Object, filter: Function}}
293
+ @returns {{options: Object, pruneIgnorePatterns: string[], filter: Function}}
291
294
  */
292
295
  const applyIgnoreFilesAndGetFilterSync = options => {
293
296
  const cwd = options.cwd ?? process.cwd();
@@ -297,6 +300,7 @@ const applyIgnoreFilesAndGetFilterSync = options => {
297
300
  if (ignoreFilesPatterns.length === 0 && !globalIgnoreFile) {
298
301
  return {
299
302
  options,
303
+ pruneIgnorePatterns: [],
300
304
  filter: createFilterFunction(false, cwd, options.fs),
301
305
  };
302
306
  }
@@ -306,12 +310,18 @@ const applyIgnoreFilesAndGetFilterSync = options => {
306
310
  const includeParentIgnoreFiles = options.gitignore === true;
307
311
  const ignoreResult = ignoreFilesPatterns.length > 0
308
312
  ? getIgnorePatternsAndPredicateSync(ignoreFilesPatterns, options, includeParentIgnoreFiles)
309
- : {patterns: [], matcher: false, usingGitRoot: false};
313
+ : {rules: [], matcher: false};
310
314
 
311
315
  const globalGitRoot = globalIgnoreFile ? findGitRootSync(cwd, options.fs) : undefined;
312
316
  const globalMatcher = globalIgnoreFile ? buildGlobalMatcher(globalIgnoreFile, cwd, globalGitRoot ?? cwd) : undefined;
313
317
 
314
- return buildIgnoreFilterResult(options, cwd, ignoreResult, globalMatcher, createFilterFunction);
318
+ return buildIgnoreFilterResult({
319
+ options,
320
+ cwd,
321
+ ignoreResult,
322
+ globalMatcher,
323
+ createFilter: createFilterFunction,
324
+ });
315
325
  };
316
326
 
317
327
  const assertGlobalGitignoreSyncSupport = options => {
@@ -545,18 +555,31 @@ const applyParentDirectoryIgnoreAdjustments = tasks => tasks.map(task => ({
545
555
  },
546
556
  }));
547
557
 
558
+ // Prune patterns are appended after directory expansion and the parent-directory adjustment on
559
+ // purpose: they are already glob-shaped, and rebasing them onto a `../` prefix would let them
560
+ // ignore paths outside the scope of the ignore files they came from.
561
+ const appendPruneIgnorePatterns = (tasks, pruneIgnorePatterns) => pruneIgnorePatterns.length === 0
562
+ ? tasks
563
+ : tasks.map(task => ({
564
+ patterns: task.patterns,
565
+ options: {
566
+ ...task.options,
567
+ ignore: [...task.options.ignore, ...pruneIgnorePatterns],
568
+ },
569
+ }));
570
+
548
571
  const normalizeExpandDirectoriesOption = (options, cwd) => ({
549
572
  ...(cwd ? {cwd} : {}),
550
573
  ...(Array.isArray(options) ? {files: options} : options),
551
574
  });
552
575
 
553
- const generateTasks = async (patterns, options) => {
576
+ const generateTasks = async (patterns, options, pruneIgnorePatterns = []) => {
554
577
  const globTasks = convertNegativePatterns(patterns, options);
555
578
 
556
579
  const {cwd, expandDirectories, fs: fsImplementation} = options;
557
580
 
558
581
  if (!expandDirectories) {
559
- return applyParentDirectoryIgnoreAdjustments(globTasks);
582
+ return appendPruneIgnorePatterns(applyParentDirectoryIgnoreAdjustments(globTasks), pruneIgnorePatterns);
560
583
  }
561
584
 
562
585
  const directoryToGlobOptions = {
@@ -564,7 +587,7 @@ const generateTasks = async (patterns, options) => {
564
587
  fs: fsImplementation,
565
588
  };
566
589
 
567
- return Promise.all(globTasks.map(async task => {
590
+ const tasks = await Promise.all(globTasks.map(async task => {
568
591
  let {patterns, options} = task;
569
592
 
570
593
  [
@@ -580,14 +603,16 @@ const generateTasks = async (patterns, options) => {
580
603
 
581
604
  return {patterns, options};
582
605
  }));
606
+
607
+ return appendPruneIgnorePatterns(tasks, pruneIgnorePatterns);
583
608
  };
584
609
 
585
- const generateTasksSync = (patterns, options) => {
610
+ const generateTasksSync = (patterns, options, pruneIgnorePatterns = []) => {
586
611
  const globTasks = convertNegativePatterns(patterns, options);
587
612
  const {cwd, expandDirectories, fs: fsImplementation} = options;
588
613
 
589
614
  if (!expandDirectories) {
590
- return applyParentDirectoryIgnoreAdjustments(globTasks);
615
+ return appendPruneIgnorePatterns(applyParentDirectoryIgnoreAdjustments(globTasks), pruneIgnorePatterns);
591
616
  }
592
617
 
593
618
  const directoryToGlobSyncOptions = {
@@ -595,7 +620,7 @@ const generateTasksSync = (patterns, options) => {
595
620
  fs: fsImplementation,
596
621
  };
597
622
 
598
- return globTasks.map(task => {
623
+ const tasks = globTasks.map(task => {
599
624
  let {patterns, options} = task;
600
625
  patterns = directoryToGlobSync(patterns, directoryToGlobSyncOptions);
601
626
  options.ignore = directoryToGlobSync(options.ignore, {cwd, fs: fsImplementation});
@@ -605,16 +630,18 @@ const generateTasksSync = (patterns, options) => {
605
630
 
606
631
  return {patterns, options};
607
632
  });
633
+
634
+ return appendPruneIgnorePatterns(tasks, pruneIgnorePatterns);
608
635
  };
609
636
 
610
637
  export const globby = normalizeArguments(async (patterns, options) => {
611
638
  assertGlobalGitignoreAsyncSupport(options);
612
639
 
613
640
  // Apply ignore files and get filter (reads .gitignore files once)
614
- const {options: modifiedOptions, filter} = await applyIgnoreFilesAndGetFilter(options);
641
+ const {options: modifiedOptions, pruneIgnorePatterns, filter} = await applyIgnoreFilesAndGetFilter(options);
615
642
 
616
- // Generate tasks with modified options (includes gitignore patterns in ignore option)
617
- const tasks = await generateTasks(patterns, modifiedOptions);
643
+ // Generate tasks, attaching the prune patterns so fast-glob skips ignored directories
644
+ const tasks = await generateTasks(patterns, modifiedOptions, pruneIgnorePatterns);
618
645
 
619
646
  const results = await Promise.all(tasks.map(task => fastGlob(task.patterns, task.options)));
620
647
  return unionFastGlobResultsAsync(results, filter);
@@ -624,10 +651,10 @@ export const globbySync = normalizeArgumentsSync((patterns, options) => {
624
651
  assertGlobalGitignoreSyncSupport(options);
625
652
 
626
653
  // Apply ignore files and get filter (reads .gitignore files once)
627
- const {options: modifiedOptions, filter} = applyIgnoreFilesAndGetFilterSync(options);
654
+ const {options: modifiedOptions, pruneIgnorePatterns, filter} = applyIgnoreFilesAndGetFilterSync(options);
628
655
 
629
- // Generate tasks with modified options (includes gitignore patterns in ignore option)
630
- const tasks = generateTasksSync(patterns, modifiedOptions);
656
+ // Generate tasks, attaching the prune patterns so fast-glob skips ignored directories
657
+ const tasks = generateTasksSync(patterns, modifiedOptions, pruneIgnorePatterns);
631
658
 
632
659
  const results = tasks.map(task => fastGlob.sync(task.patterns, task.options));
633
660
  return unionFastGlobResults(results, filter);
@@ -639,10 +666,10 @@ export const globbyStream = normalizeArgumentsSync((patterns, options) => {
639
666
  const seen = new Set();
640
667
  const stream = Readable.from((async function * () {
641
668
  // Apply ignore files and get filter (reads .gitignore files once)
642
- const {options: modifiedOptions, filter} = await applyIgnoreFilesAndGetFilter(options);
669
+ const {options: modifiedOptions, pruneIgnorePatterns, filter} = await applyIgnoreFilesAndGetFilter(options);
643
670
 
644
- // Generate tasks with modified options (includes gitignore patterns in ignore option)
645
- const tasks = await generateTasks(patterns, modifiedOptions);
671
+ // Generate tasks, attaching the prune patterns so fast-glob skips ignored directories
672
+ const tasks = await generateTasks(patterns, modifiedOptions, pruneIgnorePatterns);
646
673
 
647
674
  if (tasks.length === 0) {
648
675
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjser/globby",
3
- "version": "16.2.1-cjser.2",
3
+ "version": "16.2.3-cjser.2",
4
4
  "description": "User-friendly glob matching",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -99,11 +99,11 @@
99
99
  "types": "./index.d.ts",
100
100
  "main": "./dist-cjser/index.cjs",
101
101
  "cjser": {
102
- "sourceVersion": "16.2.1",
102
+ "sourceVersion": "16.2.3",
103
103
  "cjserVersion": 2,
104
104
  "original": {
105
105
  "name": "globby",
106
- "version": "16.2.1",
106
+ "version": "16.2.3",
107
107
  "exports": {
108
108
  "types": "./index.d.ts",
109
109
  "default": "./index.js"
package/readme.md CHANGED
@@ -93,7 +93,7 @@ When enabled, globby searches for `.gitignore` files from the current working di
93
93
 
94
94
  Gitignore patterns take priority over user patterns, matching Git's behavior. To include gitignored files, set this to `false`.
95
95
 
96
- **Performance:** Globby reads `.gitignore` files before globbing. When there are no negation patterns (like `!important.log`) and no parent `.gitignore` files are found, it passes ignore patterns to fast-glob to skip traversing ignored directories entirely, which significantly improves performance for large `node_modules` or build directories. When negation patterns or parent `.gitignore` files are present, all filtering is done after traversal to ensure correct Git-compatible behavior. For optimal performance, prefer specific `.gitignore` patterns without negations, or use `ignoreFiles: '.gitignore'` to target only the root ignore file.
96
+ **Performance:** Globby reads `.gitignore` files before globbing and hands fast-glob patterns for the directories it can prove are ignored, so whole ignored directories (like large `node_modules` or build outputs) are skipped during traversal instead of being enumerated and filtered afterwards. This holds even when negation patterns (like `!important.log`) or parent `.gitignore` files are present: only the rules that provably cannot be re-included by a negation are used to skip directories, while the final filtering always matches Git's behavior. To read fewer ignore files, use `ignoreFiles: '.gitignore'` to target only the root ignore file.
97
97
 
98
98
  ##### globalGitignore
99
99