del vs fs-extra vs remove vs rimraf
Node.js におけるファイルとディレクトリの削除戦略
delfs-extraremoverimraf類似パッケージ:

Node.js におけるファイルとディレクトリの削除戦略

delfs-extraremoverimraf は、すべて Node.js 環境でファイルやディレクトリを削除するためのツールですが、それぞれ役割と設計思想が異なります。rimrafrm -rf コマンドの Node 版として最も標準的な再帰的削除ツールです。fs-extra はネイティブの fs モジュールを拡張し、削除機能も含む包括的なファイル操作ライブラリです。delrimraf をベースにしつつ、グロブパターン(例:*.log)を使った柔軟なファイル指定に特化しています。一方、remove パッケージは歴史的に存在しますが、現在は rimraffs-extra に機能が統合されており、新規プロジェクトでの採用は推奨されません。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
del01,34412.7 kB176ヶ月前MIT
fs-extra09,62257.7 kB1223日前MIT
remove011-314年前MIT
rimraf05,842262 kB81ヶ月前BlueOak-1.0.0

Node.js におけるファイル削除ツールの徹底比較:del vs fs-extra vs remove vs rimraf

Node.js でファイルやディレクトリを削除する際、ネイティブの fs.unlinkfs.rmdir だけでは不十分なケースが多々あります。特に、中身が入ったディレクトリを再帰的に削除する場合や、複数のファイルをパターン指定で削除する場合、標準モジュールだけではコードが煩雑になります。

そこで登場するのが delfs-extraremoverimraf といったライブラリです。これらはすべて「削除」という共通の目的を持ちますが、設計思想、機能範囲、そしてメンテナンス状況に大きな違いがあります。本稿では、実務経験に基づき、どの場面でどのツールを選ぶべきかを技術的に深掘りします。

🗑️ 再帰的削除の仕組み:ネイティブ vs ライブラリ

Node.js v14.14.0 以降、ネイティブの fs モジュールにも fs.rm(path, { recursive: true }) が追加されました。しかし、古い Node バージョンをサポートする必要がある場合や、エラーハンドリングを統一したい場合には、ライブラリに頼る価値があります。

rimraf は、この「再帰的削除」に特化したデファクトスタンダードです。UNIX コマンドの rm -rf と同等の動作を Node.js で実現します。

// rimraf: 再帰的削除の標準
const rimraf = require('rimraf');

// ディレクトリごと削除
rimraf('/path/to/dir', () => {
  console.log('deleted');
});

fs-extra は、この rimraf の機能を fs.remove として取り込んでいます。ネイティブの fs モジュールの代わりに使うことで、削除だけでなくコピーや移動も統一された API で扱えます。

// fs-extra: 包括的なファイル操作の一部として削除
const fs = require('fs-extra');

// rimraf と同じく再帰的に削除される
await fs.remove('/path/to/dir');

del は、内部で rimraf を使用していますが、単なる削除ではなく「パターンマッチング」に重点を置いています。

// del: グロブパターンによる削除
const del = require('del');

// 複数のパターンに一致するファイルを削除
await del(['dist/*.js', 'dist/*.map']);

remove パッケージも同様の機能を提供しますが、現在は rimraf ほどの信頼性や更新頻度がありません。

// remove: 歴史的なパッケージ(非推奨)
const remove = require('remove');

// 機能は似ているが、エコシステムとしての支持は低い
remove.dir('/path/to/dir', function(err){ 
  // ...
});

🔍 グロブパターンサポートの有無

ファイル削除において、特定の拡張子を持つファイルだけを対象にしたいケースは頻繁にあります。この際、グロブパターン(*.txt など)を扱えるかが重要な分岐点になります。

del は、最初から globby を内包しており、パターン指定が第一級の機能として提供されています。ビルドツールのクリーンアップタスクなどに最適です。

// del: パターン指定がネイティブサポート
await del(['temp/*.tmp', '!temp/important.tmp']);
// 注意:ignore パターンも使用可能

rimraffs-extraremove は、基本的にパスを直接指定します。グロブパターンを使いたい場合は、別途 glob パッケージなどを組み合わせてパスを解決する必要があります。

// rimraf + glob: 組み合わせが必要
const glob = require('glob');
const rimraf = require('rimraf');

const files = glob.sync('*.log');
files.forEach(file => rimraf.sync(file));

🛡️ 安全性とエラーハンドリング

削除操作は破壊的であるため、エラーハンドリングと安全性が重要です。

fs-extra は、Promise ベースの API が標準で提供されており、async/await との相性が抜群です。また、ネイティブ fs と互換性があるため、既存コードへの導入コストが低いです。

// fs-extra: Promise 対応で扱いやすい
try {
  await fs.remove('/secure/data');
} catch (err) {
  console.error('削除に失敗しました', err);
}

rimraf も Promise をサポートしていますが、バージョンによって API が異なります(v3 以降は Promise 対応)。コールバックスタイルとの互換性も残っています。

// rimraf: Promise スタイル(v3+)
await rimraf('/path/to/dir');

del は、存在しないファイルを削除しようとしてもエラーを出さず、黙って完了する設計になっています。これはクリーンアップスクリプトにおいて、毎回存在チェックを書かなくて済むため非常に便利です。

// del: 存在しないファイルでもエラーにならない
await del(['non-existent-file.txt']); 
// エラーにならず解決される

remove パッケージは、エラーハンドリングの挙動がバージョンや実装によってばらつきがあり、現代的な async/await 環境ではラップが必要になることが多いです。

📦 依存関係とバンドルサイズへの影響

フロントエンドのビルドツール(Webpack や Vite など)を設定する際、依存パッケージの数は重要です。

  • fs-extra を導入すると、それ単体でファイル操作のほとんどが賄えるため、追加のユーティリティが減ります。
  • rimraf は単一機能に特化しているため、軽量ですが、グロブ機能が必要な場合は glob を別途入れる必要があります。
  • delglobbyrimraf を内部に持っているため、これら 2 つを個別にインストールするよりシンプルになる場合があります。
  • remove は、機能面で rimraf に劣るため、依存として追加するメリットがほとんどありません。

🚀 実戦シナリオでの使い分け

シナリオ 1: ビルドディレクトリのクリーンアップ

ビルド前に dist フォルダを空にしたい場合、del が最も適しています。

// 推奨: del
const del = require('del');

async function clean() {
  await del(['dist']);
}

シナリオ 2: 一時ファイルの管理

アプリケーション実行中に生成された一時フォルダを削除する場合、fs-extra または rimraf が適しています。

// 推奨: fs-extra (既に導入している場合)
const fs = require('fs-extra');
await fs.remove('./temp/session_123');

// 推奨: rimraf (軽量さを重視する場合)
const rimraf = require('rimraf');
await rimraf('./temp/session_123');

シナリオ 3: 古いパッケージの移行

もし既存プロジェクトで remove パッケージを使っている場合、それは rimraf への移行を検討すべきタイミングです。

// 非推奨: remove
// const remove = require('remove');

// 推奨: rimraf へ置換
const rimraf = require('rimraf');

📊 機能比較サマリー

機能delfs-extraremoverimraf
再帰的削除✅ (内部で利用)✅ (fs.remove)
グロブパターン✅ (ネイティブ)❌ (別途必要)❌ (別途必要)
Promise 対応⚠️ (要確認)✅ (v3+)
fs 互換性✅ (拡張)
メンテナンス🟢 活発🟢 活発🔴 停滞気味🟢 活発
主な用途クリーンアップ汎用ファイル操作(非推奨)削除特化

💡 結論:どれを選ぶべきか

現代の Node.js 開発において、remove パッケージを選ぶ理由はほぼありません。機能は rimraf に含まれており、メンテナンス状況も rimraffs-extra の方が圧倒的に優れています。

fs-extra は、ファイル操作全般を統一したいプロジェクトにとって最強の選択肢です。既に fs-extra を導入しているなら、追加で rimraf を入れる必要はなく、fs.remove を使うだけで十分です。

rimraf は、余計な機能はいらない、ただ確実に削除したいという場合に最適です。CLI ツールの開発や、内部モジュールとして使用する場合に適しています。

del は、Grunt や Gulp、あるいは npm スクリプトなどで「特定のファイルパターンを削除したい」という明確なニーズがある場合にのみ選択します。グロブパターンの扱いが非常にスムーズです。

最終的には、「汎用性なら fs-extra、削除特化なら rimraf、パターン削除なら del という基準で選定するのが、技術的負債を増やさない賢明な判断です。

選び方: del vs fs-extra vs remove vs rimraf

  • del:

    ビルドスクリプトなどで、特定のパターンに一致するファイル(例:dist/**/*.js)をまとめて削除したい場合に選択します。グロブパターンサポートが組み込まれているため、設定ファイルの整理やキャッシュクリアに適しています。

  • fs-extra:

    ファイル削除だけでなく、コピー、移動、JSON 読み書きなど、ファイルシステム操作全般を一元管理したい場合に最適です。fs.remove() メソッドは rimraf と同等の機能を提供し、既存の fs コードを置き換えるだけで導入できます。

  • remove:

    歴史的なパッケージであり、現在はメンテナンスが停滞しているか、機能が rimraf に統合されています。新規プロジェクトでの使用は避け、代わりに rimraf または fs-extra の使用を強く推奨します。

  • rimraf:

    純粋に「ディレクトリを再帰的に削除する」ことだけに特化したツールが必要な場合に選択します。他の依存関係を増やしたくない場合や、ビルドツールの内部ロジックとして軽量な削除機能が必要な時に最適です。

del のREADME

del

Delete files and directories using globs

Similar to rimraf, but with a Promise API and support for multiple files and globbing. It also protects you against deleting the current working directory and above.

Install

npm install del

Usage

import {deleteAsync} from 'del';

const deletedFilePaths = await deleteAsync(['temp/*.js', '!temp/unicorn.js']);
const deletedDirectoryPaths = await deleteAsync(['temp', 'public']);

console.log('Deleted files:\n', deletedFilePaths.join('\n'));
console.log('\n\n');
console.log('Deleted directories:\n', deletedDirectoryPaths.join('\n'));

Beware

The glob pattern ** matches all children and the parent.

So this won't work:

deleteSync(['public/assets/**', '!public/assets/goat.png']);

You have to explicitly ignore the parent directories too:

deleteSync(['public/assets/**', '!public/assets', '!public/assets/goat.png']);

To delete all subdirectories inside public/, you can do:

deleteSync(['public/*/']);

Suggestions on how to improve this welcome!

API

Note that glob patterns can only contain forward-slashes, not backward-slashes. Windows file paths can use backward-slashes as long as the path does not contain any glob-like characters, otherwise use path.posix.join() instead of path.join().

deleteAsync(patterns, options?)

Returns Promise<string[]> with the deleted paths.

deleteSync(patterns, options?)

Returns string[] with the deleted paths.

patterns

Type: string | string[]

See the supported glob patterns.

options

Type: object

You can specify any of the globby options in addition to the below options. In contrast to the globby defaults, expandDirectories, onlyFiles, and followSymbolicLinks are false by default.

force

Type: boolean
Default: false

Allow deleting the current working directory and outside.

dryRun

Type: boolean
Default: false

See what would be deleted.

import {deleteAsync} from 'del';

const deletedPaths = await deleteAsync(['temp/*.js'], {dryRun: true});

console.log('Files and directories that would be deleted:\n', deletedPaths.join('\n'));
dot

Type: boolean
Default: false

Allow patterns to match files/folders that start with a period (.).

This option is passed through to fast-glob.

Note that an explicit dot in a portion of the pattern will always match dot files.

Example

directory/
├── .editorconfig
└── package.json
import {deleteSync} from 'del';

deleteSync('*', {dot: false});
//=> ['package.json']
deleteSync('*', {dot: true});
//=> ['.editorconfig', 'package.json']
concurrency

Type: number
Default: Infinity
Minimum: 1

Concurrency limit.

onProgress

Type: (progress: ProgressData) => void

Called after each file or directory is deleted.

import {deleteAsync} from 'del';

await deleteAsync(patterns, {
	onProgress: progress => {
	// …
}});
ProgressData
{
	totalCount: number,
	deletedCount: number,
	percent: number,
	path?: string
}
  • percent is a value between 0 and 1
  • path is the absolute path of the deleted file or directory. It will not be present if nothing was deleted.

CLI

See del-cli for a CLI for this module and trash-cli for a safe version that is suitable for running by hand.

Related

  • make-dir - Make a directory and its parents if needed
  • globby - User-friendly glob matching