toes/src/shared/gitignore.ts
2026-01-29 23:29:47 -08:00

83 lines
2.3 KiB
TypeScript

import { existsSync, readFileSync } from 'fs'
import { join } from 'path'
const ALWAYS_EXCLUDE = [
'node_modules',
'.DS_Store',
'.git',
'*~',
]
const DEFAULT_PATTERNS = [
...ALWAYS_EXCLUDE,
'*.log',
'dist',
'build',
'.env.local',
'bun.lockb',
]
export interface GitignoreChecker {
shouldExclude: (path: string) => boolean
}
export function loadGitignore(appPath: string): GitignoreChecker {
const gitignorePath = join(appPath, '.gitignore')
const patterns = existsSync(gitignorePath)
? [...ALWAYS_EXCLUDE, ...parseGitignore(readFileSync(gitignorePath, 'utf-8'))]
: DEFAULT_PATTERNS
return {
shouldExclude: (path: string) => matchesPattern(path, patterns),
}
}
function parseGitignore(content: string): string[] {
return content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'))
}
function matchesPattern(path: string, patterns: string[]): boolean {
const parts = path.split('/')
const filename = parts[parts.length - 1]!
for (const pattern of patterns) {
// Patterns with wildcards or globs
if (pattern.includes('*') || pattern.includes('?')) {
const regex = globToRegex(pattern)
// Try matching against full path and just filename
if (regex.test(path) || regex.test(filename)) return true
continue
}
// Directory match: "node_modules" or "node_modules/"
const dirPattern = pattern.endsWith('/') ? pattern.slice(0, -1) : pattern
if (parts.includes(dirPattern)) return true
// Prefix patterns: "dist/" or "build/"
if (pattern.endsWith('/') && path.startsWith(pattern)) return true
// Exact match
if (path === pattern || filename === pattern) return true
}
return false
}
function globToRegex(glob: string): RegExp {
// Handle ** specially - it can match zero or more path segments
// Use placeholders to avoid conflicts with regex escaping
let pattern = glob
.replace(/\*\*\//g, '\x00') // **/ placeholder
.replace(/\*\*/g, '\x01') // ** placeholder
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '[^/]*') // Single * matches within path segment
.replace(/\?/g, '[^/]') // ? matches single char
.replace(/\x00/g, '(.*/)?') // **/ matches zero or more dirs
.replace(/\x01/g, '.*') // ** matches anything
return new RegExp(`^${pattern}$`)
}