153 lines
6.6 KiB
TypeScript
153 lines
6.6 KiB
TypeScript
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { simpleGit } from 'simple-git'
|
|
import { buildTreeFromPaths, createProjectFile, readAll, readDirChildren, readProjectFile, readTree, writeProjectFile } from '../src/main/fs-service'
|
|
|
|
let dir = ''
|
|
afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }) })
|
|
|
|
async function fixture(): Promise<string> {
|
|
const d = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
|
await mkdir(join(d, 'src'), { recursive: true })
|
|
await mkdir(join(d, 'node_modules/pkg'), { recursive: true })
|
|
await mkdir(join(d, '.git'), { recursive: true })
|
|
await writeFile(join(d, 'src', 'a.ts'), 'export const a = 1\n')
|
|
await writeFile(join(d, 'README.md'), '# hi\n')
|
|
await writeFile(join(d, 'node_modules', 'pkg', 'index.js'), 'module.exports = 1\n')
|
|
await writeFile(join(d, '.git', 'HEAD'), 'ref: refs/heads/main\n')
|
|
await writeFile(join(d, 'logo.bin'), Buffer.from([0x00, 0x01, 0x02, 0x00, 0x99]))
|
|
return d
|
|
}
|
|
|
|
describe('readTree', () => {
|
|
it('lists dirs before files, ignoring node_modules and .git', async () => {
|
|
dir = await fixture()
|
|
const tree = await readTree(dir)
|
|
const top = (tree.children || []).map((c) => c.name)
|
|
expect(top).not.toContain('node_modules')
|
|
expect(top).not.toContain('.git')
|
|
expect(top).toContain('src')
|
|
// dirs first
|
|
expect(top.indexOf('src')).toBeLessThan(top.indexOf('README.md'))
|
|
expect(top.indexOf('src')).toBeLessThan(top.indexOf('logo.bin'))
|
|
})
|
|
|
|
it('shows an empty folder (no files yet) that rg --files can not emit', async () => {
|
|
dir = await fixture()
|
|
await mkdir(join(dir, 'empty'), { recursive: true })
|
|
await mkdir(join(dir, 'src', 'nested', 'deep'), { recursive: true }) // file-empty nested chain
|
|
const tree = await readTree(dir)
|
|
const top = (tree.children || []).map((c) => c.name)
|
|
expect(top).toContain('empty')
|
|
const empty = (tree.children || []).find((c) => c.name === 'empty')!
|
|
expect(empty.type).toBe('dir')
|
|
// the nested empty chain shows under the (file-bearing) src folder
|
|
const src = (tree.children || []).find((c) => c.name === 'src')!
|
|
const nested = (src.children || []).find((c) => c.name === 'nested')!
|
|
expect(nested.type).toBe('dir')
|
|
expect((nested.children || []).map((c) => c.name)).toEqual(['deep'])
|
|
})
|
|
})
|
|
|
|
describe('readDirChildren', () => {
|
|
it('returns a single folder\'s children (root and subdir), ignoring node_modules', async () => {
|
|
dir = await fixture()
|
|
const top = (await readDirChildren(dir, '')).map((c) => c.name)
|
|
expect(top).toContain('src')
|
|
expect(top).toContain('README.md')
|
|
expect(top).not.toContain('node_modules')
|
|
const src = await readDirChildren(dir, 'src')
|
|
expect(src.map((c) => c.name)).toEqual(['a.ts'])
|
|
expect(src[0].path).toBe('src/a.ts')
|
|
})
|
|
|
|
it('shows an empty subfolder created since the initial tree read', async () => {
|
|
dir = await fixture()
|
|
await mkdir(join(dir, 'src', 'fresh'), { recursive: true })
|
|
const src = await readDirChildren(dir, 'src')
|
|
const fresh = src.find((c) => c.name === 'fresh')
|
|
expect(fresh?.type).toBe('dir')
|
|
expect(fresh?.path).toBe('src/fresh')
|
|
})
|
|
|
|
it('reflects files added/removed since the initial tree read', async () => {
|
|
dir = await fixture()
|
|
await writeFile(join(dir, 'src', 'b.ts'), 'export const b = 2\n')
|
|
await rm(join(dir, 'src', 'a.ts'))
|
|
expect((await readDirChildren(dir, 'src')).map((c) => c.name)).toEqual(['b.ts'])
|
|
})
|
|
})
|
|
|
|
describe('readAll', () => {
|
|
it('indexes text files, skipping ignored dirs and binary files', async () => {
|
|
dir = await fixture()
|
|
const files = await readAll(dir)
|
|
const keys = Object.keys(files)
|
|
expect(keys).toContain('src/a.ts')
|
|
expect(keys).toContain('README.md')
|
|
expect(keys.some((k) => k.includes('node_modules'))).toBe(false)
|
|
expect(keys).not.toContain('logo.bin') // binary skipped
|
|
expect(files['src/a.ts']).toContain('export const a')
|
|
})
|
|
|
|
it('shows .gitignored files in a repo (files.followGitignore default off)', async () => {
|
|
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
|
await simpleGit(dir).init()
|
|
await writeFile(join(dir, '.gitignore'), 'secret.txt\n')
|
|
await writeFile(join(dir, 'secret.txt'), 'shh')
|
|
await writeFile(join(dir, 'keep.txt'), 'ok')
|
|
const keys = Object.keys(await readAll(dir))
|
|
expect(keys).toContain('keep.txt')
|
|
expect(keys).toContain('secret.txt') // gitignore not followed by default → shown
|
|
})
|
|
})
|
|
|
|
describe('buildTreeFromPaths', () => {
|
|
it('nests paths with dirs before files, alphabetical', () => {
|
|
const t = buildTreeFromPaths('proj', ['src/b.ts', 'src/a.ts', 'README.md', 'src/util/x.ts'])
|
|
expect((t.children || []).map((c) => c.name)).toEqual(['src', 'README.md'])
|
|
const src = (t.children || []).find((c) => c.name === 'src')!
|
|
expect((src.children || []).map((c) => c.name)).toEqual(['util', 'a.ts', 'b.ts'])
|
|
expect((src.children || []).find((c) => c.name === 'util')!.path).toBe('src/util')
|
|
})
|
|
|
|
it('forces dirPaths in as empty folders, deduped against file-derived dirs', () => {
|
|
const t = buildTreeFromPaths('proj', ['src/a.ts'], ['empty', 'src/sub', 'src'])
|
|
const top = (t.children || []).map((c) => c.name)
|
|
expect(top).toEqual(['empty', 'src']) // dirs alphabetical, both present once
|
|
const src = (t.children || []).find((c) => c.name === 'src')!
|
|
expect((src.children || []).map((c) => c.name)).toEqual(['sub', 'a.ts'])
|
|
expect((src.children || []).find((c) => c.name === 'sub')!.type).toBe('dir')
|
|
})
|
|
})
|
|
|
|
describe('read/write round-trip', () => {
|
|
it('writes then reads the same content', async () => {
|
|
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
|
await writeProjectFile(dir, 'note.txt', 'hello world\n')
|
|
expect(await readProjectFile(dir, 'note.txt')).toBe('hello world\n')
|
|
})
|
|
})
|
|
|
|
describe('createProjectFile', () => {
|
|
it('creates an empty file and makes missing parent folders', async () => {
|
|
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
|
await createProjectFile(dir, 'src/new/fresh.ts')
|
|
expect(await readProjectFile(dir, 'src/new/fresh.ts')).toBe('')
|
|
})
|
|
|
|
it('refuses to overwrite an existing file', async () => {
|
|
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
|
await writeProjectFile(dir, 'keep.txt', 'precious\n')
|
|
await expect(createProjectFile(dir, 'keep.txt')).rejects.toThrow()
|
|
expect(await readProjectFile(dir, 'keep.txt')).toBe('precious\n')
|
|
})
|
|
|
|
it('refuses to escape the project root', async () => {
|
|
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
|
await expect(createProjectFile(dir, '../escape.txt')).rejects.toThrow()
|
|
})
|
|
})
|