Skip to content
Open
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
7 changes: 6 additions & 1 deletion benchmark/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { fileURLToPath } from 'node:url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

/**
* Discovers and runs benchmark files in the current directory, optionally filtered by the first command-line argument.
*
* Selects files ending with `.ts` (excluding `bench.ts` and declaration files ending with `.d.ts`). If a filter string is provided as the first CLI argument, only files whose names include that substring (case-insensitive) are selected. Logs a message and exits if no files match, logs the number of files found, and imports each matched file to execute its benchmark; errors importing individual files are logged.
*/
async function runBenchmarks() {
const args = process.argv.slice(2)
const filter = args[0]
Expand Down Expand Up @@ -38,4 +43,4 @@ async function runBenchmarks() {
}
}

runBenchmarks()
runBenchmarks()
34 changes: 33 additions & 1 deletion benchmark/rm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,31 @@ if (fs.existsSync(baseDir)) {
}
fs.mkdirSync(baseDir)

/**
* Create a flat directory containing a specified number of files.
*
* Ensures the target directory exists and writes `count` files named
* `file-0.txt` through `file-(count-1).txt`, each containing the string "content".
*
* @param dir - The directory path where files will be created
* @param count - The number of files to create (non-negative integer)
*/
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')
}
}

/**
* Creates a chain of nested subdirectories under the given root and places a file in each level.
*
* Ensures the root directory exists, then creates `depth` nested directories named `depth-0`, `depth-1`, ...,
* and writes a file named `file.txt` containing "content" into each created subdirectory.
*
* @param dir - The root directory under which the nested structure will be created
* @param depth - The number of nested subdirectory levels to create
*/
function createDeepStructure(dir: string, depth: number) {
let current = dir
if (!fs.existsSync(current)) fs.mkdirSync(current, { recursive: true })
Expand All @@ -29,6 +47,15 @@ function createDeepStructure(dir: string, depth: number) {
}
}

/**
* Runs a benchmark group that compares multiple rmSync implementations and prints a Mitata-like comparison table.
*
* Executes a warmup run then performs 10 timed iterations per implementation using the provided setup function to create
* each test directory (setup time is excluded from measurements). For each implementation it computes the average time
* in milliseconds and prints each implementation's average alongside a ratio compared to the first (baseline).
*
* @param setupFn - A function that creates the test directory structure at the given path before removal
*/
async function runGroup(groupName: string, setupFn: (dir: string) => void) {
console.log(`\n${groupName}`)

Expand Down Expand Up @@ -84,6 +111,11 @@ async function runGroup(groupName: string, setupFn: (dir: string) => void) {
})
}

/**
* Execute the benchmark suite for the two test scenarios and remove temporary data.
*
* Runs the "Flat directory (2000 files)" and "Deep nested directory (depth 100)" benchmark groups sequentially, then deletes the temporary base directory if it exists.
*/
async function run() {
await runGroup('Flat directory (2000 files)', (dir) => createFlatStructure(dir, 2000))
await runGroup('Deep nested directory (depth 100)', (dir) => createDeepStructure(dir, 100))
Expand All @@ -94,4 +126,4 @@ async function run() {
}
}

run()
run()
27 changes: 26 additions & 1 deletion src/readdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,31 @@ pub struct Dirent {
}

// #[napi] // marco: expose the function to Node
/// Read directory entries from `path_str` according to the provided `options`.
///
/// The function performs a non-recursive or recursive directory listing based on `options.recursive`.
/// Hidden entries may be skipped when `options.skip_hidden` is true. When `options.with_file_types`
/// is true the results include `Dirent` objects with `name`, `parent_path`, and `is_dir`; otherwise
/// the results are plain file path strings (non-recursive: entry names; recursive: paths relative
/// to the provided root).
///
/// # Returns
///
/// `Ok(Either::A(Vec<String>))` with entry names or relative paths when `with_file_types` is false,
/// or `Ok(Either::B(Vec<Dirent>))` with `Dirent` objects when `with_file_types` is true. Returns
/// `Err` when the path does not exist or an underlying IO error occurs (error message contains
/// the underlying reason).
///
/// # Examples
///
/// ```
/// // Non-recursive list of names
/// let res = ls(".".to_string(), None).unwrap();
/// match res {
/// Either::A(names) => println!("names: {:?}", names),
/// Either::B(dirents) => println!("dirents: {:?}", dirents),
/// }
/// ```
fn ls(
path_str: String,
options: Option<ReaddirOptions>,
Expand Down Expand Up @@ -184,4 +209,4 @@ impl Task for ReaddirTask {
#[napi(js_name = "readdir")]
pub fn readdir(path: String, options: Option<ReaddirOptions>) -> AsyncTask<ReaddirTask> {
AsyncTask::new(ReaddirTask { path, options })
}
}
101 changes: 100 additions & 1 deletion src/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,43 @@ pub struct RmOptions {
pub concurrency: Option<u32>,
}

/// Recursively removes the file or directory at `path` according to `opts`.
///
/// If `path` is a directory and `opts.recursive` is `true`, the directory's
/// contents are removed first (optionally in parallel when `opts.concurrency`
/// is greater than 1) and then the directory itself is removed. If the path
/// is a directory and `opts.recursive` is `false`, the function attempts to
/// remove the directory and maps "directory not empty" conditions to an
/// `ENOTEMPTY`-style error message. If the path is not a directory, the file
/// is removed.
///
/// # Parameters
///
/// - `path`: filesystem path to remove.
/// - `opts`: removal options; `recursive` controls directory recursion and
/// `concurrency` (when > 1) enables parallel traversal.
///
/// # Returns
///
/// `Ok(())` on successful removal, or an `napi::Error` created from the
/// underlying I/O error on failure.
///
/// # Examples
///
/// ```no_run
/// use std::path::Path;
///
/// let opts = RmOptions {
/// force: None,
/// max_retries: None,
/// recursive: Some(true),
/// retry_delay: None,
/// concurrency: None,
/// };
///
/// // Remove the current directory contents (for demonstration; be careful)
/// let _ = remove_recursive(Path::new("."), &opts).unwrap();
/// ```
fn remove_recursive(path: &Path, opts: &RmOptions) -> Result<()> {
let meta = fs::symlink_metadata(path).map_err(|e| Error::from_reason(e.to_string()))?;

Expand Down Expand Up @@ -72,6 +109,31 @@ fn remove_recursive(path: &Path, opts: &RmOptions) -> Result<()> {
Ok(())
}

/// Remove the filesystem entry at `path_str` using the provided rm-style options.
///
/// The empty string for `path_str` is treated as `"."`. When `options` is `None`,
/// defaults are used (force = false, recursive = false, other fields unset).
/// If `options.force` is true and the path does not exist, the call succeeds silently.
///
/// # Parameters
///
/// - `path_str` — Path to remove; `""` is interpreted as the current directory (`"."`).
/// - `options` — Optional `RmOptions` controlling behavior (e.g. `force`, `recursive`, `concurrency`).
///
/// # Returns
///
/// `Ok(())` on successful removal, or an `Err` containing a `napi::Error` describing the failure.
///
/// # Examples
///
/// ```
/// // remove a single file (non-recursive)
/// let _ = remove("tmp/file.txt".to_string(), None);
///
/// // remove or ignore missing path
/// let opts = RmOptions { force: Some(true), recursive: Some(false), max_retries: None, retry_delay: None, concurrency: None };
/// let _ = remove("tmp/missing".to_string(), Some(opts));
/// ```
fn remove(path_str: String, options: Option<RmOptions>) -> Result<()> {
let search_path_str = if path_str.is_empty() { "." } else { &path_str };
let path = Path::new(search_path_str);
Expand Down Expand Up @@ -110,21 +172,58 @@ impl Task for RmTask {
type Output = ();
type JsValue = ();

/// Performs the removal operation described by this task.
///
/// # Examples
///
/// ```no_run
/// let mut task = RmTask { path: ".".to_string(), options: None };
/// let result = task.compute();
/// assert!(result.is_ok());
/// ```
fn compute(&mut self) -> Result<Self::Output> {
remove(self.path.clone(), self.options.clone())
}

/// Convert the completed task result into a JavaScript value for the N-API environment.
///
/// This implementation produces no JavaScript value and signals successful resolution.
///
/// # Returns
///
/// `Ok(())` indicating the task resolved with no value to return to JavaScript.
fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result<Self::JsValue> {
Ok(())
}
}

/// Creates an asynchronous remove task for the given filesystem path using the provided options.
///
/// The returned task, when scheduled by the N-API runtime, will perform the removal work off the
/// main thread and resolve with no value.
///
/// # Examples
///
/// ```
/// let task = rm("some/path".to_string(), None);
/// // `task` can be returned to JavaScript or scheduled with the napi runtime.
/// ```
#[napi(js_name = "rm")]
pub fn rm(path: String, options: Option<RmOptions>) -> AsyncTask<RmTask> {
AsyncTask::new(RmTask { path, options })
}

/// Synchronously removes the filesystem entry at the given path using the provided options.
///
/// Returns `Ok(())` on success or an error describing the failure.
///
/// # Examples
///
/// ```
/// // Remove a file or directory at "./tmp" using default options.
/// rm_sync("./tmp".to_string(), None).unwrap();
/// ```
#[napi(js_name = "rmSync")]
pub fn rm_sync(path: String, options: Option<RmOptions>) -> Result<()> {
remove(path, options)
}
}
Loading