diff --git a/Cargo.toml b/Cargo.toml index 984af5d..8d235ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index c7faefc..009cf62 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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` diff --git a/__test__/rm.spec.ts b/__test__/rm.spec.ts new file mode 100644 index 0000000..04d3aef --- /dev/null +++ b/__test__/rm.spec.ts @@ -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)) +}) diff --git a/benchmark/bench.ts b/benchmark/bench.ts index 74aaaa7..f222877 100644 --- a/benchmark/bench.ts +++ b/benchmark/bench.ts @@ -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) { diff --git a/benchmark/readdir.ts b/benchmark/readdir.ts index 26797dc..da1797e 100644 --- a/benchmark/readdir.ts +++ b/benchmark/readdir.ts @@ -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, +}) diff --git a/benchmark/rm.ts b/benchmark/rm.ts new file mode 100644 index 0000000..7251394 --- /dev/null +++ b/benchmark/rm.ts @@ -0,0 +1,97 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' +import * as os from 'node:os' +import { rmSync as hyperRmSync } from '../index.js' + +const tmpDir = os.tmpdir() +const baseDir = path.join(tmpDir, 'hyper-fs-bench-rm') + +// Clean up previous runs +if (fs.existsSync(baseDir)) { + fs.rmSync(baseDir, { recursive: true, force: true }) +} +fs.mkdirSync(baseDir) + +function createFlatStructure(dir: string, count: number) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) + for (let i = 0; i < count; i++) { + fs.writeFileSync(path.join(dir, `file-${i}.txt`), 'content') + } +} + +function createDeepStructure(dir: string, depth: number) { + let current = dir + if (!fs.existsSync(current)) fs.mkdirSync(current, { recursive: true }) + for (let i = 0; i < depth; i++) { + current = path.join(current, `depth-${i}`) + fs.mkdirSync(current) + fs.writeFileSync(path.join(current, 'file.txt'), 'content') + } +} + +async function runGroup(groupName: string, setupFn: (dir: string) => void) { + console.log(`\n${groupName}`) + + const implementations = [ + { name: 'Node.js fs.rmSync', fn: (p: string) => fs.rmSync(p, { recursive: true, force: true }) }, + { name: 'hyper-fs rmSync', fn: (p: string) => hyperRmSync(p, { recursive: true, force: true }) }, + { + name: 'hyper-fs rmSync (4 threads)', + fn: (p: string) => hyperRmSync(p, { recursive: true, force: true, concurrency: 4 }), + }, + ] + + const results: { name: string; time: number }[] = [] + + for (const impl of implementations) { + const times: number[] = [] + const iterations = 10 + + // Warmup (1 run) + const warmupDir = path.join(baseDir, `warmup-${impl.name.replace(/[^a-zA-Z0-9]/g, '')}`) + setupFn(warmupDir) + impl.fn(warmupDir) + + for (let i = 0; i < iterations; i++) { + const testDir = path.join(baseDir, `${impl.name.replace(/[^a-zA-Z0-9]/g, '-')}-${i}`) + setupFn(testDir) // Setup time NOT included + + const start = process.hrtime.bigint() + impl.fn(testDir) // Measured time + const end = process.hrtime.bigint() + + const ms = Number(end - start) / 1_000_000 + times.push(ms) + } + + const avg = times.reduce((a, b) => a + b, 0) / times.length + results.push({ name: impl.name, time: avg }) + } + + // Render Mitata-like output + // Example: + // Node.js fs.rmSync 10.50 ms (baseline) + // hyper-fs rmSync 12.00 ms 1.14x (slower) + + const baseline = results[0] + + results.forEach((res) => { + const isBaseline = res === baseline + const ratio = res.time / baseline.time + const diffStr = isBaseline ? '(baseline)' : `${ratio.toFixed(2)}x ${ratio > 1 ? '(slower)' : '(faster)'}` + + console.log(` ${res.name.padEnd(25)} ${res.time.toFixed(2)} ms ${diffStr}`) + }) +} + +async function run() { + await runGroup('Flat directory (2000 files)', (dir) => createFlatStructure(dir, 2000)) + await runGroup('Deep nested directory (depth 100)', (dir) => createDeepStructure(dir, 100)) + + // Clean up + if (fs.existsSync(baseDir)) { + fs.rmSync(baseDir, { recursive: true, force: true }) + } +} + +run() diff --git a/index.d.ts b/index.d.ts index b56ade1..9e51a57 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,19 @@ export interface Dirent { export declare function readdir(path: string, options?: ReaddirOptions | undefined | null): Promise +/** * Reads the contents of a directory. + * @param {string | Buffer | URL} path + * @param {string | { + * encoding?: string; + * withFileTypes?: boolean; + * recursive?: boolean; + * }} [options] + * @param {( + * err?: Error, + * files?: string[] | Buffer[] | Dirent[] + * ) => any} callback + * @returns {void} + */ export interface ReaddirOptions { skipHidden?: boolean concurrency?: number @@ -19,3 +32,27 @@ export declare function readdirSync( path: string, options?: ReaddirOptions | undefined | null, ): Array | Array + +export declare function rm(path: string, options?: RmOptions | undefined | null): Promise + +/** * Asynchronously removes files and + * directories (modeled on the standard POSIX `rm` utility). + * @param {string | Buffer | URL} path + * @param {{ + * force?: boolean; + * maxRetries?: number; + * recursive?: boolean; + * retryDelay?: number; + * }} [options] + * @param {(err?: Error) => any} callback + * @returns {void} + */ +export interface RmOptions { + force?: boolean + maxRetries?: number + recursive?: boolean + retryDelay?: number + concurrency?: number +} + +export declare function rmSync(path: string, options?: RmOptions | undefined | null): void diff --git a/index.js b/index.js index 25d517c..8e0cb2d 100644 --- a/index.js +++ b/index.js @@ -574,3 +574,5 @@ if (!nativeBinding) { module.exports = nativeBinding module.exports.readdir = nativeBinding.readdir module.exports.readdirSync = nativeBinding.readdirSync +module.exports.rm = nativeBinding.rm +module.exports.rmSync = nativeBinding.rmSync diff --git a/package.json b/package.json index b5cb0a7..d5af444 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "oxlint": "^1.14.0", "prettier": "^3.6.2", "tinybench": "^5.0.1", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "mitata": "^1.0.34" }, "lint-staged": { "*.@(js|ts|tsx)": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81d8584..11f820c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: lint-staged: specifier: ^16.1.6 version: 16.2.7 + mitata: + specifier: ^1.0.34 + version: 1.0.34 npm-run-all2: specifier: ^8.0.4 version: 8.0.4 @@ -1491,6 +1494,10 @@ packages: { integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== } engines: { node: '>= 18' } + mitata@1.0.34: + resolution: + { integrity: sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA== } + ms@2.1.3: resolution: { integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== } @@ -2952,6 +2959,8 @@ snapshots: dependencies: minipass: 7.1.2 + mitata@1.0.34: {} + ms@2.1.3: {} mute-stream@3.0.0: {} diff --git a/src/lib.rs b/src/lib.rs index d055b2c..8757011 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,8 @@ // define modules pub mod readdir; +pub mod rm; //export modules pub use readdir::*; +pub use rm::*; diff --git a/src/readdir.rs b/src/readdir.rs index 2c9722a..0089e15 100644 --- a/src/readdir.rs +++ b/src/readdir.rs @@ -5,15 +5,21 @@ use napi_derive::napi; use std::fs; use std::path::Path; -// basic usage -// ls('./node_modules') - -// advanced usage -// readdirSync('./src', { -// recursive: true, -// concurrency: 8, -// ignore: ['.git'], -// }); +// # nodejs readdir jsdoc: +/** + * Reads the contents of a directory. + * @param {string | Buffer | URL} path + * @param {string | { + * encoding?: string; + * withFileTypes?: boolean; + * recursive?: boolean; + * }} [options] + * @param {( + * err?: Error, + * files?: string[] | Buffer[] | Dirent[] + * ) => any} callback + * @returns {void} + */ #[napi(object)] #[derive(Clone)] @@ -41,7 +47,7 @@ fn ls( let path = Path::new(search_path_str); if !Path::new(&path).exists() { return Err(Error::from_reason(format!( - "ENOENT: no such file or directory, scandir '{}'", + "ENOENT: no such file or directory, readdir '{}'", path.to_string_lossy() ))); } diff --git a/src/rm.rs b/src/rm.rs new file mode 100644 index 0000000..44d3dff --- /dev/null +++ b/src/rm.rs @@ -0,0 +1,129 @@ +use napi::bindgen_prelude::*; +use napi::Task; +use napi_derive::napi; +use rayon::prelude::*; +use std::fs; +use std::path::Path; + +// nodejs rm jsdoc: +/** + * Asynchronously removes files and + * directories (modeled on the standard POSIX `rm` utility). + * @param {string | Buffer | URL} path + * @param {{ + * force?: boolean; + * maxRetries?: number; + * recursive?: boolean; + * retryDelay?: number; + * }} [options] + * @param {(err?: Error) => any} callback + * @returns {void} + */ + +#[napi(object)] +#[derive(Clone)] +pub struct RmOptions { + pub force: Option, + pub max_retries: Option, + pub recursive: Option, + pub retry_delay: Option, + pub concurrency: Option, +} + +fn remove_recursive(path: &Path, opts: &RmOptions) -> Result<()> { + let meta = fs::symlink_metadata(path).map_err(|e| Error::from_reason(e.to_string()))?; + + if meta.is_dir() { + if opts.recursive.unwrap_or(false) { + let entries_iter = fs::read_dir(path).map_err(|e| Error::from_reason(e.to_string()))?; + + let concurrency = opts.concurrency.unwrap_or(0); + if concurrency > 1 { + let entries: Vec<_> = entries_iter + .collect::>() + .map_err(|e| Error::from_reason(e.to_string()))?; + + entries + .par_iter() + .try_for_each(|entry| -> Result<()> { remove_recursive(&entry.path(), opts) })?; + } else { + for entry in entries_iter { + let entry = entry.map_err(|e| Error::from_reason(e.to_string()))?; + remove_recursive(&entry.path(), opts)?; + } + } + + fs::remove_dir(path).map_err(|e| Error::from_reason(e.to_string()))?; + } else { + fs::remove_dir(path).map_err(|e| { + if e.kind() == std::io::ErrorKind::AlreadyExists || e.to_string().contains("not empty") { + Error::from_reason(format!( + "ENOTEMPTY: directory not empty, rm '{}'", + path.to_string_lossy() + )) + } else { + Error::from_reason(e.to_string()) + } + })?; + } + } else { + fs::remove_file(path).map_err(|e| Error::from_reason(e.to_string()))?; + } + Ok(()) +} + +fn remove(path_str: String, options: Option) -> Result<()> { + let path = Path::new(&path_str); + + let opts = options.unwrap_or(RmOptions { + force: Some(false), + recursive: Some(false), + max_retries: None, + retry_delay: None, + concurrency: None, + }); + let force = opts.force.unwrap_or(false); + + if !path.exists() { + if force { + // If force is true, silently succeed when path doesn't exist + return Ok(()); + } + return Err(Error::from_reason(format!( + "ENOENT: no such file or directory, rm '{}'", + path.to_string_lossy() + ))); + } + + remove_recursive(path, &opts) +} + +// ========= async version ========= + +pub struct RmTask { + pub path: String, + pub options: Option, +} + +impl Task for RmTask { + type Output = (); + type JsValue = (); + + fn compute(&mut self) -> Result { + remove(self.path.clone(), self.options.clone()) + } + + fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result { + Ok(()) + } +} + +#[napi(js_name = "rm")] +pub fn rm(path: String, options: Option) -> AsyncTask { + AsyncTask::new(RmTask { path, options }) +} + +#[napi(js_name = "rmSync")] +pub fn rm_sync(path: String, options: Option) -> Result<()> { + remove(path, options) +}