Skip to content

Commit cf97abe

Browse files
authored
Merge pull request #2 from CoderSerio/feat/carbon-dev
The potential issues reported above are tracked and scheduled for resolution before the 1.0 release.
2 parents a4f5e6c + ae72083 commit cf97abe

File tree

13 files changed

+591
-56
lines changed

13 files changed

+591
-56
lines changed

Cargo.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ version = "0.1.0"
99
crate-type = ["cdylib"]
1010

1111
[dependencies]
12-
jwalk = "0.8.1"
13-
napi = "3.0.0"
14-
napi-derive = "3.4"
15-
serde = "1.0.228"
16-
walkdir = "2.5.0"
12+
jwalk = "0.8.1"
13+
napi = "3.0.0"
14+
napi-derive = "3.4"
15+
rayon = "1.11.0"
16+
remove_dir_all = "1.0.0"
17+
serde = "1.0.228"
18+
walkdir = "2.5.0"
1719

1820
[build-dependencies]
1921
napi-build = "2"

README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,18 @@ We are rewriting `fs` APIs one by one.
3838
encoding?: string; //
3939
withFileTypes?: boolean; //
4040
recursive?: boolean; //
41+
concurrency?: number; //
4142
};
4243
```
43-
- **Return Type Diff**: `Buffer` return not supported yet.
44-
- **Performance**: TBD
45-
- **Supported Version**: TBD
46-
- **Notes**:
47-
- ✨ Supports `options.concurrency` to control parallelism.
44+
- **Return Type**:
45+
```ts
46+
string[]
47+
| {
48+
name: string, //
49+
parentPath: string, //
50+
isDir: boolean //
51+
}[]
52+
```
4853

4954
### `readFile`
5055

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

6570
### `rm`
6671

67-
- **Status**: ❌
72+
- **Node.js Arguments**:
73+
```ts
74+
path: string; //
75+
options?: {
76+
force?: boolean; //
77+
maxRetries?: number; //
78+
recursive?: boolean; //
79+
retryDelay?: number; //
80+
concurrency?: number; //
81+
};
82+
```
83+
- **Return Type**:
84+
```ts
85+
void
86+
```
6887

6988
### `rmdir`
7089

__test__/rm.spec.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import test from 'ava'
2+
import { rmSync, rm } from '../index.js'
3+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs'
4+
import { join } from 'node:path'
5+
import { tmpdir } from 'node:os'
6+
7+
// Helper function to create a temporary directory
8+
function createTempDir(): string {
9+
const tempDir = join(tmpdir(), `hyper-fs-test-${Date.now()}-${Math.random().toString(36).substring(7)}`)
10+
mkdirSync(tempDir, { recursive: true })
11+
return tempDir
12+
}
13+
14+
test('sync: should remove a file', (t) => {
15+
const tempDir = createTempDir()
16+
const testFile = join(tempDir, 'test.txt')
17+
writeFileSync(testFile, 'test content')
18+
19+
t.true(existsSync(testFile), 'File should exist before removal')
20+
rmSync(testFile)
21+
t.false(existsSync(testFile), 'File should not exist after removal')
22+
})
23+
24+
test('async: should remove a file', async (t) => {
25+
const tempDir = createTempDir()
26+
const testFile = join(tempDir, 'test.txt')
27+
writeFileSync(testFile, 'test content')
28+
29+
t.true(existsSync(testFile), 'File should exist before removal')
30+
await rm(testFile)
31+
t.false(existsSync(testFile), 'File should not exist after removal')
32+
})
33+
34+
test('sync: should remove an empty directory', (t) => {
35+
const tempDir = createTempDir()
36+
const testDir = join(tempDir, 'empty-dir')
37+
mkdirSync(testDir)
38+
39+
t.true(existsSync(testDir), 'Directory should exist before removal')
40+
rmSync(testDir)
41+
t.false(existsSync(testDir), 'Directory should not exist after removal')
42+
})
43+
44+
test('async: should remove an empty directory', async (t) => {
45+
const tempDir = createTempDir()
46+
const testDir = join(tempDir, 'empty-dir')
47+
mkdirSync(testDir)
48+
49+
t.true(existsSync(testDir), 'Directory should exist before removal')
50+
await rm(testDir)
51+
t.false(existsSync(testDir), 'Directory should not exist after removal')
52+
})
53+
54+
test('sync: should remove a directory recursively when recursive=true', (t) => {
55+
const tempDir = createTempDir()
56+
const testDir = join(tempDir, 'nested-dir')
57+
const nestedDir = join(testDir, 'nested')
58+
const testFile = join(nestedDir, 'file.txt')
59+
60+
mkdirSync(nestedDir, { recursive: true })
61+
writeFileSync(testFile, 'content')
62+
63+
t.true(existsSync(testDir), 'Directory should exist before removal')
64+
t.true(existsSync(testFile), 'Nested file should exist before removal')
65+
66+
rmSync(testDir, { recursive: true })
67+
68+
t.false(existsSync(testDir), 'Directory should not exist after removal')
69+
t.false(existsSync(testFile), 'Nested file should not exist after removal')
70+
})
71+
72+
test('async: should remove a directory recursively when recursive=true', async (t) => {
73+
const tempDir = createTempDir()
74+
const testDir = join(tempDir, 'nested-dir')
75+
const nestedDir = join(testDir, 'nested')
76+
const testFile = join(nestedDir, 'file.txt')
77+
78+
mkdirSync(nestedDir, { recursive: true })
79+
writeFileSync(testFile, 'content')
80+
81+
t.true(existsSync(testDir), 'Directory should exist before removal')
82+
t.true(existsSync(testFile), 'Nested file should exist before removal')
83+
84+
await rm(testDir, { recursive: true })
85+
86+
t.false(existsSync(testDir), 'Directory should not exist after removal')
87+
t.false(existsSync(testFile), 'Nested file should not exist after removal')
88+
})
89+
90+
test('sync: should throw error when removing non-empty directory without recursive', (t) => {
91+
const tempDir = createTempDir()
92+
const testDir = join(tempDir, 'non-empty-dir')
93+
const testFile = join(testDir, 'file.txt')
94+
95+
mkdirSync(testDir)
96+
writeFileSync(testFile, 'content')
97+
98+
t.true(existsSync(testDir), 'Directory should exist')
99+
t.throws(() => rmSync(testDir), { message: /ENOTEMPTY|EEXIST/ })
100+
})
101+
102+
test('async: should throw error when removing non-empty directory without recursive', async (t) => {
103+
const tempDir = createTempDir()
104+
const testDir = join(tempDir, 'non-empty-dir')
105+
const testFile = join(testDir, 'file.txt')
106+
107+
mkdirSync(testDir)
108+
writeFileSync(testFile, 'content')
109+
110+
t.true(existsSync(testDir), 'Directory should exist')
111+
await t.throwsAsync(async () => await rm(testDir), { message: /ENOTEMPTY|EEXIST/ })
112+
})
113+
114+
test('sync: should throw error when file does not exist and force=false', (t) => {
115+
const tempDir = createTempDir()
116+
const nonExistentFile = join(tempDir, 'non-existent.txt')
117+
118+
t.false(existsSync(nonExistentFile), 'File should not exist')
119+
t.throws(() => rmSync(nonExistentFile), { message: /ENOENT/ })
120+
})
121+
122+
test('async: should throw error when file does not exist and force=false', async (t) => {
123+
const tempDir = createTempDir()
124+
const nonExistentFile = join(tempDir, 'non-existent.txt')
125+
126+
t.false(existsSync(nonExistentFile), 'File should not exist')
127+
await t.throwsAsync(async () => await rm(nonExistentFile), { message: /ENOENT/ })
128+
})
129+
130+
test('sync: should not throw error when file does not exist and force=true', (t) => {
131+
const tempDir = createTempDir()
132+
const nonExistentFile = join(tempDir, 'non-existent.txt')
133+
134+
t.false(existsSync(nonExistentFile), 'File should not exist')
135+
// Should not throw
136+
t.notThrows(() => rmSync(nonExistentFile, { force: true }))
137+
})
138+
139+
test('async: should not throw error when file does not exist and force=true', async (t) => {
140+
const tempDir = createTempDir()
141+
const nonExistentFile = join(tempDir, 'non-existent.txt')
142+
143+
t.false(existsSync(nonExistentFile), 'File should not exist')
144+
// Should not throw
145+
await t.notThrowsAsync(async () => await rm(nonExistentFile, { force: true }))
146+
})
147+
148+
test('sync: should remove file when force=true (even if file exists)', (t) => {
149+
const tempDir = createTempDir()
150+
const testFile = join(tempDir, 'test.txt')
151+
writeFileSync(testFile, 'content')
152+
153+
t.true(existsSync(testFile), 'File should exist before removal')
154+
rmSync(testFile, { force: true })
155+
t.false(existsSync(testFile), 'File should not exist after removal')
156+
})
157+
158+
test('async: should remove file when force=true (even if file exists)', async (t) => {
159+
const tempDir = createTempDir()
160+
const testFile = join(tempDir, 'test.txt')
161+
writeFileSync(testFile, 'content')
162+
163+
t.true(existsSync(testFile), 'File should exist before removal')
164+
await rm(testFile, { force: true })
165+
t.false(existsSync(testFile), 'File should not exist after removal')
166+
})
167+
168+
test('sync: should work with recursive=false explicitly', (t) => {
169+
const tempDir = createTempDir()
170+
const testDir = join(tempDir, 'empty-dir')
171+
mkdirSync(testDir)
172+
173+
t.true(existsSync(testDir), 'Directory should exist before removal')
174+
rmSync(testDir, { recursive: false })
175+
t.false(existsSync(testDir), 'Directory should not exist after removal')
176+
})
177+
178+
test('async: should work with recursive=false explicitly', async (t) => {
179+
const tempDir = createTempDir()
180+
const testDir = join(tempDir, 'empty-dir')
181+
mkdirSync(testDir)
182+
183+
t.true(existsSync(testDir), 'Directory should exist before removal')
184+
await rm(testDir, { recursive: false })
185+
t.false(existsSync(testDir), 'Directory should not exist after removal')
186+
})
187+
188+
test('sync: should remove deep nested directory with concurrency', (t) => {
189+
const tempDir = createTempDir()
190+
const testDir = join(tempDir, 'nested-dir-concurrency')
191+
// Create a structure: nested-dir/subdir1/file, nested-dir/subdir2/file, ...
192+
mkdirSync(testDir)
193+
for (let i = 0; i < 10; i++) {
194+
const subDir = join(testDir, `sub-${i}`)
195+
mkdirSync(subDir)
196+
writeFileSync(join(subDir, 'file.txt'), 'content')
197+
}
198+
199+
t.true(existsSync(testDir))
200+
rmSync(testDir, { recursive: true, concurrency: 4 })
201+
t.false(existsSync(testDir))
202+
})
203+
204+
test('async: should remove deep nested directory with concurrency', async (t) => {
205+
const tempDir = createTempDir()
206+
const testDir = join(tempDir, 'nested-dir-async-concurrency')
207+
mkdirSync(testDir)
208+
for (let i = 0; i < 10; i++) {
209+
const subDir = join(testDir, `sub-${i}`)
210+
mkdirSync(subDir)
211+
writeFileSync(join(subDir, 'file.txt'), 'content')
212+
}
213+
214+
t.true(existsSync(testDir))
215+
await rm(testDir, { recursive: true, concurrency: 4 })
216+
t.false(existsSync(testDir))
217+
})

benchmark/bench.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@ import { fileURLToPath } from 'node:url'
55
const __dirname = path.dirname(fileURLToPath(import.meta.url))
66

77
async function runBenchmarks() {
8+
const args = process.argv.slice(2)
9+
const filter = args[0]
10+
811
const files = fs.readdirSync(__dirname).filter((file) => {
9-
return file.endsWith('.ts') && file !== 'bench.ts' && !file.endsWith('.d.ts')
12+
const isBenchFile = file.endsWith('.ts') && file !== 'bench.ts' && !file.endsWith('.d.ts')
13+
if (!isBenchFile) return false
14+
15+
if (filter) {
16+
return file.toLowerCase().includes(filter.toLowerCase())
17+
}
18+
return true
1019
})
1120

21+
if (files.length === 0) {
22+
console.log(`No benchmark files found matching filter "${filter}"`)
23+
return
24+
}
25+
1226
console.log(`Found ${files.length} benchmark files to run...`)
1327

1428
for (const file of files) {

benchmark/readdir.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,42 @@
1-
import { Bench } from 'tinybench'
1+
import { run, bench, group } from 'mitata'
22
import * as fs from 'node:fs'
3-
import { readdirSync } from '../index.js'
43
import * as path from 'node:path'
4+
import { readdirSync } from '../index.js'
55

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

1010
console.log(`Benchmarking readdir on: ${dir}`)
11+
try {
12+
const count = fs.readdirSync(dir).length
13+
console.log(`File count in target dir: ${count}`)
14+
} catch {}
15+
16+
// 1. Basic readdir
17+
group('Readdir (names only)', () => {
18+
bench('Node.js', () => fs.readdirSync(dir)).baseline()
19+
bench('Hyper-FS', () => readdirSync(dir))
20+
})
21+
22+
// 2. With File Types
23+
group('Readdir (withFileTypes)', () => {
24+
bench('Node.js', () => fs.readdirSync(dir, { withFileTypes: true })).baseline()
25+
bench('Hyper-FS', () => readdirSync(dir, { withFileTypes: true }))
26+
})
27+
28+
// 3. Recursive + withFileTypes
29+
group('Readdir (recursive + withFileTypes)', () => {
30+
bench('Node.js', () => fs.readdirSync(dir, { recursive: true, withFileTypes: true })).baseline()
31+
bench('Hyper-FS', () => readdirSync(dir, { recursive: true, withFileTypes: true }))
32+
})
1133

12-
bench
13-
.add('Node.js fs.readdirSync', () => {
14-
fs.readdirSync(dir)
15-
})
16-
.add('Node.js fs.readdirSync (withFileTypes)', () => {
17-
fs.readdirSync(dir, { withFileTypes: true })
18-
})
19-
.add('Node.js fs.readdirSync (recursive, withFileTypes)', () => {
20-
fs.readdirSync(dir, { recursive: true, withFileTypes: true })
21-
})
22-
.add('hyper-fs readdirSync (default)', () => {
23-
readdirSync(dir)
24-
})
25-
.add('hyper-fs readdirSync (withFileTypes)', () => {
26-
readdirSync(dir, { withFileTypes: true })
27-
})
28-
.add('hyper-fs readdirSync (recursive)', () => {
29-
readdirSync(dir, { recursive: true })
30-
})
31-
.add('hyper-fs readdirSync (recursive, withFileTypes)', () => {
32-
readdirSync(dir, { recursive: true, withFileTypes: true })
33-
})
34-
.add('hyper-fs readdirSync (4 threads, recursive)', () => {
35-
readdirSync(dir, { concurrency: 4, recursive: true })
36-
})
37-
.add('hyper-fs readdirSync (4 threads, recursive, withFileTypes)', () => {
38-
readdirSync(dir, { concurrency: 4, recursive: true, withFileTypes: true })
39-
})
40-
await bench.run()
34+
// 4. Concurrency (Hyper-FS only comparison)
35+
group('Hyper-FS Concurrency', () => {
36+
bench('Default (Auto)', () => readdirSync(dir, { recursive: true })).baseline()
37+
bench('4 Threads', () => readdirSync(dir, { recursive: true, concurrency: 4 }))
38+
})
4139

42-
console.table(bench.table())
40+
await run({
41+
colors: true,
42+
})

0 commit comments

Comments
 (0)