83 lines
1.8 KiB
TypeScript
83 lines
1.8 KiB
TypeScript
import { readdirSync, readFileSync, statSync } from 'fs'
|
|
import { createHash } from 'crypto'
|
|
import { join, relative } from 'path'
|
|
|
|
export interface FileInfo {
|
|
hash: string
|
|
mtime: string
|
|
size: number
|
|
}
|
|
|
|
export interface Manifest {
|
|
files: Record<string, FileInfo>
|
|
name: string
|
|
}
|
|
|
|
const EXCLUDE_PATTERNS = [
|
|
'node_modules',
|
|
'.DS_Store',
|
|
'*.log',
|
|
'dist',
|
|
'build',
|
|
'.env.local',
|
|
'.git',
|
|
'bun.lockb',
|
|
]
|
|
|
|
export function computeHash(content: Buffer | string): string {
|
|
return createHash('sha256').update(content).digest('hex')
|
|
}
|
|
|
|
export function generateManifest(appPath: string, appName: string): Manifest {
|
|
const files: Record<string, FileInfo> = {}
|
|
|
|
function walkDir(dir: string) {
|
|
const entries = readdirSync(dir, { withFileTypes: true })
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = join(dir, entry.name)
|
|
const relativePath = relative(appPath, fullPath)
|
|
|
|
// Check exclusions
|
|
if (shouldExclude(relativePath)) continue
|
|
|
|
if (entry.isDirectory()) {
|
|
walkDir(fullPath)
|
|
} else if (entry.isFile()) {
|
|
const content = readFileSync(fullPath)
|
|
const stats = statSync(fullPath)
|
|
|
|
files[relativePath] = {
|
|
hash: computeHash(content),
|
|
mtime: stats.mtime.toISOString(),
|
|
size: stats.size,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walkDir(appPath)
|
|
|
|
return {
|
|
name: appName,
|
|
files,
|
|
}
|
|
}
|
|
|
|
function shouldExclude(path: string): boolean {
|
|
const parts = path.split('/')
|
|
|
|
for (const pattern of EXCLUDE_PATTERNS) {
|
|
// Exact match or starts with (for directories)
|
|
if (parts.includes(pattern)) return true
|
|
|
|
// Wildcard patterns
|
|
if (pattern.startsWith('*')) {
|
|
const ext = pattern.slice(1)
|
|
if (path.endsWith(ext)) return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|