83 lines
2.3 KiB
TypeScript
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}$`)
|
|
}
|