Skip to content

Conversation

@CoderSerio
Copy link
Owner

@CoderSerio CoderSerio commented Dec 7, 2025

Description

This PR optimizes the performance of readdir and readdirSync by using std::fs for simple, non-recursive cases, and aligns the API behavior with Node.js by introducing the withFileTypes option and returning an array of strings by default.

Changes

  • Performance Optimization:

    • For non-recursive calls without concurrency enabled, readdir now uses std::fs::read_dir directly instead of jwalk. This significantly reduces overhead for common use cases.
    • Implemented an "on-demand" property access strategy: file types are only queried from the filesystem when withFileTypes is explicitly set to true.
  • API Alignment:

    • Added support for withFileTypes: boolean option.
    • Breaking Change / Fix: readdir now returns string[] (file names) by default, matching Node.js fs.readdir behavior. Previously it returned Dirent[].
    • When withFileTypes: true is passed, it returns Dirent[] containing name, parentPath, and isDir.
  • Refactoring:

    • Refactored ls function in src/readdir.rs to handle both return types (Vec<String> and Vec<Dirent>) using Either.
    • Cleaned up loop logic to reduce code duplication between different return type modes.

Benchmarks

Benchmarks run on node_modules directory:

image

Checklist

  • Code compiles and passes linter (cargo check, npm run lint)
  • Added/Updated tests in __test__/readDir.spec.ts
  • Verified performance improvements with benchmark/readdir.ts
  • Updated type definitions

Summary by CodeRabbit

  • New Features

    • Added recursive option for directory listing to traverse subdirectories
    • Added withFileTypes option to return detailed file information (name and type) alongside directory names
    • Return type now adapts based on options: returns file names as strings by default, or file objects with metadata when withFileTypes is enabled
  • Tests

    • Expanded test coverage for new recursive and withFileTypes options

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 7, 2025

Walkthrough

This change refactors the directory-reading API to support both recursive and non-recursive listing modes, with optional Dirent objects instead of only strings. The TypeScript types, Rust implementation, and tests are updated accordingly, with field name changes and expanded option support.

Changes

Cohort / File(s) Summary
Type Definitions
index.d.ts
Updated ReaddirOptions with recursive and withFileTypes flags; changed Dirent field from path to parentPath; readdirSync return type now Array<string> | Array<Dirent>
Test Suite
__test__/readdir.spec.ts
Tests refactored to handle dual return types (strings and Dirent objects); added type guards and casts for withFileTypes mode; extended concurrency tests with recursive flag
Benchmark Suite
benchmark/readdir.ts
Expanded coverage with scenarios for withFileTypes, recursive, concurrency combinations across Node.js fs and hyper-fs implementations
Rust Module Rename
src/lib.rs
Public module declaration and re-export changed from read_dir to readdir
Rust Implementation
src/read_dir.rs (deleted), src/readdir.rs (added)
Complete rewrite: new ReaddirOptions with recursive and with_file_types fields; new Dirent with parent_path field; readdirSync returns Either<Vec, Vec>; async readdir API added via ReaddirTask

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Critical areas requiring attention:
    • Breaking API change: Dirent field rename (pathparentPath) and return type union (strings vs. Dirent objects)
    • Type definitions must align with Rust implementation and test expectations
    • Comprehensive test coverage of new mode combinations (recursive, withFileTypes, concurrency)
    • Rust implementation logic for recursive traversal and Dirent object construction
    • Verify async/await behavior in ReaddirTask implementation

Poem

🐰 A readdir hop, both swift and true,
With strings and Dirents, we've options new!
Recursive paths now dance with care,
From parent paths, we show the where.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically summarizes the main changes: optimizing readdir performance and aligning the API with Node.js conventions, which are the primary objectives of this pull request.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/readdir

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

9-9: Fix the return type for readdir.

The async readdir function returns Promise<unknown>, which loses type safety. It should return the same type as readdirSync: Promise<Array<string> | Array<Dirent>>.

Apply this diff:

-export declare function readdir(path: string, options?: ReaddirOptions | undefined | null): Promise<unknown>
+export declare function readdir(path: string, options?: ReaddirOptions | undefined | null): Promise<Array<string> | Array<Dirent>>
🧹 Nitpick comments (1)
src/readdir.rs (1)

130-148: Consider logging fallback path.

The fallback on line 143 silently handles strip_prefix failures by returning just the filename. While this prevents crashes, it could make debugging harder if the fallback is unexpectedly triggered. Consider logging or documenting when this case occurs.

Example enhancement:

         match p.strip_prefix(root) {
           Ok(relative) => relative.to_string_lossy().to_string(),
-          Err(_) => e.file_name().to_string_lossy().to_string(), // Fallback
+          Err(_) => {
+            // Fallback: this shouldn't normally happen
+            eprintln!("Warning: Failed to strip prefix for path: {:?}", p);
+            e.file_name().to_string_lossy().to_string()
+          }
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09f9a95 and 78010bc.

📒 Files selected for processing (6)
  • __test__/readdir.spec.ts (2 hunks)
  • benchmark/readdir.ts (1 hunks)
  • index.d.ts (2 hunks)
  • src/lib.rs (1 hunks)
  • src/read_dir.rs (0 hunks)
  • src/readdir.rs (1 hunks)
💤 Files with no reviewable changes (1)
  • src/read_dir.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/lib.rs (1)
src/readdir.rs (1)
  • readdir (179-181)
__test__/readdir.spec.ts (1)
src/readdir.rs (1)
  • readdir (179-181)
src/readdir.rs (1)
index.d.ts (2)
  • ReaddirOptions (11-16)
  • Dirent (3-7)
🔇 Additional comments (16)
src/readdir.rs (5)

18-33: LGTM!

The struct definitions are well-designed. Optional fields provide flexibility for default values, and the Clone derivation is necessary for the async Task implementation.


36-58: LGTM!

The options handling is sound. Empty path defaults to ".", existence check provides proper ENOENT error, and default values align with expected behavior.


151-181: LGTM!

The async implementation follows the standard napi-rs pattern. The synchronous wrapper and Task implementation are correctly structured. Cloning in compute() is necessary for the mutable borrow context.


59-98: Performance optimization correctly implemented.

The non-recursive path efficiently uses std::fs::read_dir instead of jwalk. The mutually exclusive Vec allocation pattern is correct and avoids unnecessary allocations. The file_type() call includes a safe fallback with unwrap_or(false).

The parent_path behavior matches Node.js semantics—it is set to the directory path passed to the function, so relative paths like "." and absolute paths are preserved consistently across all entries.


100-129: RayonNewPool(0) correctly uses automatic thread pool sizing.

Line 103–104: The code correctly uses Parallelism::RayonNewPool(0) when concurrency is None. According to jwalk's behavior, passing 0 to RayonNewPool instructs Rayon to automatically choose the thread pool size (respecting RAYON_NUM_THREADS if set, otherwise defaulting to the number of logical CPUs). This is the intended behavior and not a bug.

src/lib.rs (1)

13-16: LGTM!

The module rename from read_dir to readdir correctly aligns with the new file structure.

benchmark/readdir.ts (1)

12-39: Excellent benchmark coverage!

The expanded benchmarks comprehensively test the new API surface: default behavior, withFileTypes, recursive, and various concurrency combinations. This provides valuable performance comparison data against Node.js.

index.d.ts (3)

3-7: LGTM!

The path to parentPath rename improves clarity, making it explicit that this field represents the parent directory path, not the full file path.


11-16: LGTM!

The added recursive and withFileTypes options correctly align with the Rust implementation and Node.js API.


18-21: LGTM!

The return type correctly reflects the Either behavior: returning string arrays by default and Dirent arrays when withFileTypes: true.

__test__/readdir.spec.ts (6)

4-15: LGTM!

The test correctly verifies the default behavior returns strings. The type check on line 11 and the string equality check on line 13 properly validate the return type.


17-44: LGTM!

The test comprehensively validates Dirent objects when withFileTypes: true. The type guards (lines 26-31, 36-38, 41-43) properly handle the union type, and the checks for name and isDir properties verify the Dirent structure.


46-51: LGTM!

The async test correctly validates that readdir returns strings by default and includes expected files.


53-67: Good test coverage for concurrency.

The tests correctly verify that concurrency works with recursive mode. The comment on line 56 helpfully explains that concurrency requires recursive mode.


69-84: LGTM!

The skipHidden test correctly handles both string and Dirent return types using the ternary operator on lines 75 and 79. The test logic properly validates hidden file filtering.


86-89: LGTM!

Error handling tests correctly verify that both sync and async variants throw on non-existent directories.

Comment on lines +11 to +16
// advanced usage
// readdirSync('./src', {
// recursive: true,
// concurrency: 8,
// ignore: ['.git'],
// });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update or remove outdated comment.

The example comment references an ignore option that is not implemented in ReaddirOptions. This could mislead developers.

Apply this diff to remove the outdated reference:

 // advanced usage
 // readdirSync('./src', {
 //   recursive: true,
 //   concurrency: 8,
-//   ignore: ['.git'],
 // });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// advanced usage
// readdirSync('./src', {
// recursive: true,
// concurrency: 8,
// ignore: ['.git'],
// });
// advanced usage
// readdirSync('./src', {
// recursive: true,
// concurrency: 8,
// });
🤖 Prompt for AI Agents
In src/readdir.rs around lines 11 to 16, the commented example shows an `ignore`
option that no longer exists on ReaddirOptions; remove the `ignore: ['.git'],`
fragment from the example (or update it to a valid option if you prefer) so the
comment reflects the current API and cannot mislead developers.

@CoderSerio CoderSerio merged commit b2a79ec into main Dec 7, 2025
15 checks passed
@CoderSerio
Copy link
Owner Author

I plan to add a JS wrapper layer in a future PR to optimize the speed of scanning flat directory trees
(May it works 😆

This was referenced Dec 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants