init helder
This commit is contained in:
117
src/main/fs-service.ts
Normal file
117
src/main/fs-service.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { readdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { join, relative, sep } from 'node:path'
|
||||
|
||||
export interface FileNode {
|
||||
name: string
|
||||
type: 'dir' | 'file'
|
||||
path: string
|
||||
open?: boolean
|
||||
children?: FileNode[]
|
||||
}
|
||||
|
||||
/** Directories never walked — noise or huge, and not part of "the project". */
|
||||
const IGNORE_DIRS = new Set([
|
||||
'node_modules', '.git', 'out', 'dist', 'build', '.next', '.nuxt',
|
||||
'.cache', 'coverage', 'vendor', '.idea', '.vscode', '.helder', '.turbo',
|
||||
])
|
||||
|
||||
const MAX_FILE_BYTES = 300_000
|
||||
const MAX_INDEXED_FILES = 6000
|
||||
|
||||
function ignored(name: string): boolean {
|
||||
return IGNORE_DIRS.has(name) || name === '.DS_Store'
|
||||
}
|
||||
|
||||
/** Recursive project tree, dirs first then files, alphabetical. */
|
||||
export async function readTree(root: string): Promise<FileNode> {
|
||||
const name = root.split(sep).filter(Boolean).pop() || root
|
||||
return { name, type: 'dir', path: '', open: true, children: await readDir(root, root, 0) }
|
||||
}
|
||||
|
||||
async function readDir(abs: string, root: string, depth: number): Promise<FileNode[]> {
|
||||
let entries: import('node:fs').Dirent[]
|
||||
try {
|
||||
entries = await readdir(abs, { withFileTypes: true })
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
const dirs: FileNode[] = []
|
||||
const files: FileNode[] = []
|
||||
for (const e of entries) {
|
||||
if (ignored(e.name)) continue
|
||||
const childAbs = join(abs, e.name)
|
||||
const rel = relative(root, childAbs).split(sep).join('/')
|
||||
if (e.isDirectory()) {
|
||||
dirs.push({
|
||||
name: e.name, type: 'dir', path: rel, open: depth < 1,
|
||||
children: depth < 12 ? await readDir(childAbs, root, depth + 1) : [],
|
||||
})
|
||||
} else if (e.isFile()) {
|
||||
files.push({ name: e.name, type: 'file', path: rel })
|
||||
}
|
||||
}
|
||||
dirs.sort((a, b) => a.name.localeCompare(b.name))
|
||||
files.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return [...dirs, ...files]
|
||||
}
|
||||
|
||||
function looksBinary(buf: Buffer): boolean {
|
||||
const n = Math.min(buf.length, 8000)
|
||||
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/** Read a single text file (relative path) → string. */
|
||||
export async function readProjectFile(root: string, rel: string): Promise<string> {
|
||||
const buf = await readFile(join(root, rel))
|
||||
if (looksBinary(buf)) return ''
|
||||
return buf.toString('utf8')
|
||||
}
|
||||
|
||||
/** Write a text file (relative path). Used by the editable buffer's save. */
|
||||
export async function writeProjectFile(root: string, rel: string, content: string): Promise<void> {
|
||||
await writeFile(join(root, rel), content, 'utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an in-memory content index of all (small, text) files — powers content
|
||||
* search and plain-file viewing without touching disk per keystroke. Capped to
|
||||
* keep large repos sane. PHASE: swap content search to ripgrep when scaling up.
|
||||
*/
|
||||
export async function readAll(root: string): Promise<Record<string, string>> {
|
||||
const out: Record<string, string> = {}
|
||||
let count = 0
|
||||
|
||||
async function walk(abs: string): Promise<void> {
|
||||
if (count >= MAX_INDEXED_FILES) return
|
||||
let entries: import('node:fs').Dirent[]
|
||||
try {
|
||||
entries = await readdir(abs, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (count >= MAX_INDEXED_FILES) return
|
||||
if (ignored(e.name)) continue
|
||||
const childAbs = join(abs, e.name)
|
||||
if (e.isDirectory()) {
|
||||
await walk(childAbs)
|
||||
} else if (e.isFile()) {
|
||||
try {
|
||||
const s = await stat(childAbs)
|
||||
if (s.size > MAX_FILE_BYTES) continue
|
||||
const buf = await readFile(childAbs)
|
||||
if (looksBinary(buf)) continue
|
||||
const rel = relative(root, childAbs).split(sep).join('/')
|
||||
out[rel] = buf.toString('utf8')
|
||||
count++
|
||||
} catch {
|
||||
/* skip unreadable */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(root)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user