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

    Fields

    No fields configured for Feature.

    Projects

    Status
    🔖 Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions