( root: string, maxDepth = 10, ignorePatterns: string[] = [] )
| 120 | } |
| 121 | |
| 122 | export async function collectFiles( |
| 123 | root: string, |
| 124 | maxDepth = 10, |
| 125 | ignorePatterns: string[] = [] |
| 126 | ): Promise<string[]> { |
| 127 | const files: string[] = []; |
| 128 | |
| 129 | // Split positive ignores from gitignore-style negations (`!pattern`). A |
| 130 | // negation explicitly re-includes an entry that would otherwise be skipped |
| 131 | // — including dot-folders, which are skipped by default. |
| 132 | const positivePatterns: string[] = []; |
| 133 | const negationPatterns: string[] = []; |
| 134 | for (const raw of ignorePatterns) { |
| 135 | if (raw.startsWith("!")) { |
| 136 | negationPatterns.push(raw.slice(1)); |
| 137 | } else { |
| 138 | positivePatterns.push(raw); |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | const normalize = (p: string) => p.replace(/\/\*\*?$/, "").replace(/^\//, ""); |
| 143 | const extraIgnore = new Set(positivePatterns.map(normalize)); |
| 144 | const negationNames = new Set(negationPatterns.map(normalize)); |
| 145 | |
| 146 | function isExplicitlyIncluded(name: string, fullPath: string): boolean { |
| 147 | if (negationNames.has(name)) return true; |
| 148 | const rel = fullPath.replace(root, "").replace(/^[/\\]/, ""); |
| 149 | for (const pattern of negationPatterns) { |
| 150 | const clean = normalize(pattern); |
| 151 | // Exact match by name or relative path only. A negation like `!.source` |
| 152 | // re-includes the `.source` entry itself; it does NOT auto-re-include |
| 153 | // arbitrary descendants. Otherwise nested dot-folders (.source/.git) |
| 154 | // and explicit positive ignores (.source/testfolder) would be silently |
| 155 | // overridden by any parent-level negation. (gitignore semantics: a |
| 156 | // child can only be re-included by an explicit `!child` pattern.) |
| 157 | if (rel === clean) return true; |
| 158 | // Explicit recursive negation `!path/**` re-includes path and descendants. |
| 159 | if (pattern.endsWith("/**") || pattern.endsWith("/*")) { |
| 160 | if (rel.startsWith(clean + "/") || rel.startsWith(clean + "\\")) return true; |
| 161 | } |
| 162 | } |
| 163 | return false; |
| 164 | } |
| 165 | |
| 166 | function shouldIgnoreDir(name: string, fullPath: string): boolean { |
| 167 | if (isExplicitlyIncluded(name, fullPath)) return false; |
| 168 | if (IGNORE_DIRS.has(name)) return true; |
| 169 | if (extraIgnore.has(name)) return true; |
| 170 | // Check if any pattern matches a path segment |
| 171 | const rel = fullPath.replace(root, "").replace(/^[/\\]/, ""); |
| 172 | for (const pattern of positivePatterns) { |
| 173 | const clean = normalize(pattern); |
| 174 | if (rel === clean || rel.startsWith(clean + "/") || rel.startsWith(clean + "\\")) return true; |
| 175 | } |
| 176 | return false; |
| 177 | } |
| 178 | |
| 179 | async function walk(dir: string, depth: number) { |
no test coverage detected