Skip to content

Spec: Rimraf to Native fs.rm() Comparison Suite #472

Description

@AugustinMauroy

Description

We should document and codemod the migration from rimraf v3/v4/v5 to node:fs.rm() in Node.js v24.

  • The migration should be divided by rimraf v3, v4, and v5.
  • The migration should separate import replacement from runtime behavior differences.
  • The migration should call out observed output gaps so version-specific behavior is explicit.
  • The migration should keep the deletion wiring and example structure aligned across all baselines.
  • The migration should include compensating patterns for code that depends on legacy glob handling or retry behavior.

Important points

  • rimraf v3 and v4 expose the older package-style recursive delete APIs, while v5 uses named exports.
  • rimraf glob patterns do not map directly to fs.rm(), but they can be migrated with fs.globSync() plus fs.rm() or fs.rmSync().
  • Most cases are a straight source-import migration, but a few cases need behavior notes because glob handling or missing-path behavior differs at runtime.
  • The codemod should prefer built-in node:fs imports for Node.js-only code.
  • fs.rmdir({ recursive: true }) is deprecated and should not be used as a replacement.

Examples

rimraf v3 -> node:fs v24

- import rimraf from "rimraf-v3";
+ import { globSync, rmSync } from "node:fs";
+ import { rm as rmPromise } from "node:fs/promises";

Literal recursive delete:

- rimraf("dist", (error) => {
-   if (error) throw error;
- });
+ await rmPromise("dist", {
+   recursive: true,
+   force: true,
+ });

Observed runtime behavior:

--- rimraf v3
+++ node:fs v24
@@
 (no changes)

Example of glob migration:

- rimraf("dist/**/*.js", (error) => {
-   if (error) throw error;
- });
+ for (const filePath of globSync("dist/**/*.js")) {
+   rmSync(filePath, { recursive: true, force: true });
+ }

rimraf v4 -> node:fs v24

- import rimraf from "rimraf-v4";
+ import { globSync, rmSync } from "node:fs";
+ import { rm as rmPromise } from "node:fs/promises";

Observed runtime behavior:

--- rimraf v4
+++ node:fs v24
@@
 (no changes)

Example of the same migration pattern with no compensation needed:

- await rimraf("dist", {
-   glob: false,
- });
+ await rmPromise("dist", {
+   recursive: true,
+   force: true,
+ });

rimraf v5 -> node:fs v24

- import { rimraf, rimrafSync } from "rimraf-v5";
+ import { globSync, rmSync } from "node:fs";
+ import { rm as rmPromise } from "node:fs/promises";

Observed runtime behavior:

--- rimraf v5
+++ node:fs v24
@@
 (no changes)

Example of sync migration:

- rimrafSync("dist");
+ rmSync("dist", {
+   recursive: true,
+   force: true,
+ });

Caveats

  • These are migration differences, not stream-style output gaps.
  • The version split matters when code depends on glob expansion, missing-path behavior, or Windows retry handling.
  • If the code relies on package-specific retry semantics, keep the behavior explicit instead of assuming parity with native deletion.
  • If the code only needs literal recursive deletion, prefer the Node core behavior and remove the rimraf dependency.
  • If the code depends on glob expansion, keep the globbing step separate and use fs.globSync() before deletion.

Refs

Metadata

Metadata

Assignees

No one assigned
    No fields configured for Feature.

    Projects

    Status
    🔖 Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions