Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ version = "0.1.0"
crate-type = ["cdylib"]

[dependencies]
jwalk = "0.8.1"
napi = "3.0.0"
napi-derive = "3.4"
serde = "1.0.228"
walkdir = "2.5.0"
jwalk = "0.8.1"
napi = "3.0.0"
napi-derive = "3.4"
rayon = "1.11.0"
remove_dir_all = "1.0.0"
serde = "1.0.228"
walkdir = "2.5.0"

[build-dependencies]
napi-build = "2"
Expand Down
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@ We are rewriting `fs` APIs one by one.
encoding?: string; // ❌
withFileTypes?: boolean; // ✅
recursive?: boolean; // ✅
concurrency?: number; // ✨
};
```
- **Return Type Diff**: `Buffer` return not supported yet.
- **Performance**: TBD
- **Supported Version**: TBD
- **Notes**:
- ✨ Supports `options.concurrency` to control parallelism.
- **Return Type**:
```ts
string[]
| {
name: string, // ✅
parentPath: string, // ✅
isDir: boolean // ✅
}[]
```

### `readFile`

Expand All @@ -64,7 +69,21 @@ We are rewriting `fs` APIs one by one.

### `rm`

- **Status**: ❌
- **Node.js Arguments**:
```ts
path: string; // ✅
options?: {
force?: boolean; // ✅
maxRetries?: number; // ❌
recursive?: boolean; // ✅
retryDelay?: number; // ❌
concurrency?: number; // ✨
};
```
- **Return Type**:
```ts
void
```

### `rmdir`

Expand Down
217 changes: 217 additions & 0 deletions __test__/rm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import test from 'ava'
import { rmSync, rm } from '../index.js'
import { mkdirSync, writeFileSync, existsSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'

// Helper function to create a temporary directory
function createTempDir(): string {
const tempDir = join(tmpdir(), `hyper-fs-test-${Date.now()}-${Math.random().toString(36).substring(7)}`)
mkdirSync(tempDir, { recursive: true })
return tempDir
}

test('sync: should remove a file', (t) => {
const tempDir = createTempDir()
const testFile = join(tempDir, 'test.txt')
writeFileSync(testFile, 'test content')

t.true(existsSync(testFile), 'File should exist before removal')
rmSync(testFile)
t.false(existsSync(testFile), 'File should not exist after removal')
})

test('async: should remove a file', async (t) => {
const tempDir = createTempDir()
const testFile = join(tempDir, 'test.txt')
writeFileSync(testFile, 'test content')

t.true(existsSync(testFile), 'File should exist before removal')
await rm(testFile)
t.false(existsSync(testFile), 'File should not exist after removal')
})

test('sync: should remove an empty directory', (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'empty-dir')
mkdirSync(testDir)

t.true(existsSync(testDir), 'Directory should exist before removal')
rmSync(testDir)
t.false(existsSync(testDir), 'Directory should not exist after removal')
})

test('async: should remove an empty directory', async (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'empty-dir')
mkdirSync(testDir)

t.true(existsSync(testDir), 'Directory should exist before removal')
await rm(testDir)
t.false(existsSync(testDir), 'Directory should not exist after removal')
})

test('sync: should remove a directory recursively when recursive=true', (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'nested-dir')
const nestedDir = join(testDir, 'nested')
const testFile = join(nestedDir, 'file.txt')

mkdirSync(nestedDir, { recursive: true })
writeFileSync(testFile, 'content')

t.true(existsSync(testDir), 'Directory should exist before removal')
t.true(existsSync(testFile), 'Nested file should exist before removal')

rmSync(testDir, { recursive: true })

t.false(existsSync(testDir), 'Directory should not exist after removal')
t.false(existsSync(testFile), 'Nested file should not exist after removal')
})

test('async: should remove a directory recursively when recursive=true', async (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'nested-dir')
const nestedDir = join(testDir, 'nested')
const testFile = join(nestedDir, 'file.txt')

mkdirSync(nestedDir, { recursive: true })
writeFileSync(testFile, 'content')

t.true(existsSync(testDir), 'Directory should exist before removal')
t.true(existsSync(testFile), 'Nested file should exist before removal')

await rm(testDir, { recursive: true })

t.false(existsSync(testDir), 'Directory should not exist after removal')
t.false(existsSync(testFile), 'Nested file should not exist after removal')
})

test('sync: should throw error when removing non-empty directory without recursive', (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'non-empty-dir')
const testFile = join(testDir, 'file.txt')

mkdirSync(testDir)
writeFileSync(testFile, 'content')

t.true(existsSync(testDir), 'Directory should exist')
t.throws(() => rmSync(testDir), { message: /ENOTEMPTY|EEXIST/ })
})

test('async: should throw error when removing non-empty directory without recursive', async (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'non-empty-dir')
const testFile = join(testDir, 'file.txt')

mkdirSync(testDir)
writeFileSync(testFile, 'content')

t.true(existsSync(testDir), 'Directory should exist')
await t.throwsAsync(async () => await rm(testDir), { message: /ENOTEMPTY|EEXIST/ })
})

test('sync: should throw error when file does not exist and force=false', (t) => {
const tempDir = createTempDir()
const nonExistentFile = join(tempDir, 'non-existent.txt')

t.false(existsSync(nonExistentFile), 'File should not exist')
t.throws(() => rmSync(nonExistentFile), { message: /ENOENT/ })
})

test('async: should throw error when file does not exist and force=false', async (t) => {
const tempDir = createTempDir()
const nonExistentFile = join(tempDir, 'non-existent.txt')

t.false(existsSync(nonExistentFile), 'File should not exist')
await t.throwsAsync(async () => await rm(nonExistentFile), { message: /ENOENT/ })
})

test('sync: should not throw error when file does not exist and force=true', (t) => {
const tempDir = createTempDir()
const nonExistentFile = join(tempDir, 'non-existent.txt')

t.false(existsSync(nonExistentFile), 'File should not exist')
// Should not throw
t.notThrows(() => rmSync(nonExistentFile, { force: true }))
})

test('async: should not throw error when file does not exist and force=true', async (t) => {
const tempDir = createTempDir()
const nonExistentFile = join(tempDir, 'non-existent.txt')

t.false(existsSync(nonExistentFile), 'File should not exist')
// Should not throw
await t.notThrowsAsync(async () => await rm(nonExistentFile, { force: true }))
})

test('sync: should remove file when force=true (even if file exists)', (t) => {
const tempDir = createTempDir()
const testFile = join(tempDir, 'test.txt')
writeFileSync(testFile, 'content')

t.true(existsSync(testFile), 'File should exist before removal')
rmSync(testFile, { force: true })
t.false(existsSync(testFile), 'File should not exist after removal')
})

test('async: should remove file when force=true (even if file exists)', async (t) => {
const tempDir = createTempDir()
const testFile = join(tempDir, 'test.txt')
writeFileSync(testFile, 'content')

t.true(existsSync(testFile), 'File should exist before removal')
await rm(testFile, { force: true })
t.false(existsSync(testFile), 'File should not exist after removal')
})

test('sync: should work with recursive=false explicitly', (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'empty-dir')
mkdirSync(testDir)

t.true(existsSync(testDir), 'Directory should exist before removal')
rmSync(testDir, { recursive: false })
t.false(existsSync(testDir), 'Directory should not exist after removal')
})

test('async: should work with recursive=false explicitly', async (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'empty-dir')
mkdirSync(testDir)

t.true(existsSync(testDir), 'Directory should exist before removal')
await rm(testDir, { recursive: false })
t.false(existsSync(testDir), 'Directory should not exist after removal')
})

test('sync: should remove deep nested directory with concurrency', (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'nested-dir-concurrency')
// Create a structure: nested-dir/subdir1/file, nested-dir/subdir2/file, ...
mkdirSync(testDir)
for (let i = 0; i < 10; i++) {
const subDir = join(testDir, `sub-${i}`)
mkdirSync(subDir)
writeFileSync(join(subDir, 'file.txt'), 'content')
}

t.true(existsSync(testDir))
rmSync(testDir, { recursive: true, concurrency: 4 })
t.false(existsSync(testDir))
})

test('async: should remove deep nested directory with concurrency', async (t) => {
const tempDir = createTempDir()
const testDir = join(tempDir, 'nested-dir-async-concurrency')
mkdirSync(testDir)
for (let i = 0; i < 10; i++) {
const subDir = join(testDir, `sub-${i}`)
mkdirSync(subDir)
writeFileSync(join(subDir, 'file.txt'), 'content')
}

t.true(existsSync(testDir))
await rm(testDir, { recursive: true, concurrency: 4 })
t.false(existsSync(testDir))
})
16 changes: 15 additions & 1 deletion benchmark/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,24 @@ import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))

async function runBenchmarks() {
const args = process.argv.slice(2)
const filter = args[0]

const files = fs.readdirSync(__dirname).filter((file) => {
return file.endsWith('.ts') && file !== 'bench.ts' && !file.endsWith('.d.ts')
const isBenchFile = file.endsWith('.ts') && file !== 'bench.ts' && !file.endsWith('.d.ts')
if (!isBenchFile) return false

if (filter) {
return file.toLowerCase().includes(filter.toLowerCase())
}
return true
})

if (files.length === 0) {
console.log(`No benchmark files found matching filter "${filter}"`)
return
}

console.log(`Found ${files.length} benchmark files to run...`)

for (const file of files) {
Expand Down
66 changes: 33 additions & 33 deletions benchmark/readdir.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import { Bench } from 'tinybench'
import { run, bench, group } from 'mitata'
import * as fs from 'node:fs'
import { readdirSync } from '../index.js'
import * as path from 'node:path'
import { readdirSync } from '../index.js'

const bench = new Bench({ time: 1000 })
const targetDir = path.resolve(process.cwd(), 'node_modules')
// Fallback to current directory if node_modules doesn't exist
const dir = fs.existsSync(targetDir) ? targetDir : process.cwd()

console.log(`Benchmarking readdir on: ${dir}`)
try {
const count = fs.readdirSync(dir).length
console.log(`File count in target dir: ${count}`)
} catch {}

// 1. Basic readdir
group('Readdir (names only)', () => {
bench('Node.js', () => fs.readdirSync(dir)).baseline()
bench('Hyper-FS', () => readdirSync(dir))
})

// 2. With File Types
group('Readdir (withFileTypes)', () => {
bench('Node.js', () => fs.readdirSync(dir, { withFileTypes: true })).baseline()
bench('Hyper-FS', () => readdirSync(dir, { withFileTypes: true }))
})

// 3. Recursive + withFileTypes
group('Readdir (recursive + withFileTypes)', () => {
bench('Node.js', () => fs.readdirSync(dir, { recursive: true, withFileTypes: true })).baseline()
bench('Hyper-FS', () => readdirSync(dir, { recursive: true, withFileTypes: true }))
})

bench
.add('Node.js fs.readdirSync', () => {
fs.readdirSync(dir)
})
.add('Node.js fs.readdirSync (withFileTypes)', () => {
fs.readdirSync(dir, { withFileTypes: true })
})
.add('Node.js fs.readdirSync (recursive, withFileTypes)', () => {
fs.readdirSync(dir, { recursive: true, withFileTypes: true })
})
.add('hyper-fs readdirSync (default)', () => {
readdirSync(dir)
})
.add('hyper-fs readdirSync (withFileTypes)', () => {
readdirSync(dir, { withFileTypes: true })
})
.add('hyper-fs readdirSync (recursive)', () => {
readdirSync(dir, { recursive: true })
})
.add('hyper-fs readdirSync (recursive, withFileTypes)', () => {
readdirSync(dir, { recursive: true, withFileTypes: true })
})
.add('hyper-fs readdirSync (4 threads, recursive)', () => {
readdirSync(dir, { concurrency: 4, recursive: true })
})
.add('hyper-fs readdirSync (4 threads, recursive, withFileTypes)', () => {
readdirSync(dir, { concurrency: 4, recursive: true, withFileTypes: true })
})
await bench.run()
// 4. Concurrency (Hyper-FS only comparison)
group('Hyper-FS Concurrency', () => {
bench('Default (Auto)', () => readdirSync(dir, { recursive: true })).baseline()
bench('4 Threads', () => readdirSync(dir, { recursive: true, concurrency: 4 }))
})

console.table(bench.table())
await run({
colors: true,
})
Loading