del、fs-extra、remove、rimraf は、すべて Node.js 環境でファイルやディレクトリを削除するためのツールですが、それぞれ役割と設計思想が異なります。rimraf は rm -rf コマンドの Node 版として最も標準的な再帰的削除ツールです。fs-extra はネイティブの fs モジュールを拡張し、削除機能も含む包括的なファイル操作ライブラリです。del は rimraf をベースにしつつ、グロブパターン(例:*.log)を使った柔軟なファイル指定に特化しています。一方、remove パッケージは歴史的に存在しますが、現在は rimraf や fs-extra に機能が統合されており、新規プロジェクトでの採用は推奨されません。
Node.js でファイルやディレクトリを削除する際、ネイティブの fs.unlink や fs.rmdir だけでは不十分なケースが多々あります。特に、中身が入ったディレクトリを再帰的に削除する場合や、複数のファイルをパターン指定で削除する場合、標準モジュールだけではコードが煩雑になります。
そこで登場するのが del、fs-extra、remove、rimraf といったライブラリです。これらはすべて「削除」という共通の目的を持ちますが、設計思想、機能範囲、そしてメンテナンス状況に大きな違いがあります。本稿では、実務経験に基づき、どの場面でどのツールを選ぶべきかを技術的に深掘りします。
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 パターンも使用可能
rimraf、fs-extra、remove は、基本的にパスを直接指定します。グロブパターンを使いたい場合は、別途 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 を別途入れる必要があります。del は globby と rimraf を内部に持っているため、これら 2 つを個別にインストールするよりシンプルになる場合があります。remove は、機能面で rimraf に劣るため、依存として追加するメリットがほとんどありません。ビルド前に dist フォルダを空にしたい場合、del が最も適しています。
// 推奨: del
const del = require('del');
async function clean() {
await del(['dist']);
}
アプリケーション実行中に生成された一時フォルダを削除する場合、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');
もし既存プロジェクトで remove パッケージを使っている場合、それは rimraf への移行を検討すべきタイミングです。
// 非推奨: remove
// const remove = require('remove');
// 推奨: rimraf へ置換
const rimraf = require('rimraf');
| 機能 | del | fs-extra | remove | rimraf |
|---|---|---|---|---|
| 再帰的削除 | ✅ (内部で利用) | ✅ (fs.remove) | ✅ | ✅ |
| グロブパターン | ✅ (ネイティブ) | ❌ (別途必要) | ❌ | ❌ (別途必要) |
| Promise 対応 | ✅ | ✅ | ⚠️ (要確認) | ✅ (v3+) |
| fs 互換性 | ❌ | ✅ (拡張) | ❌ | ❌ |
| メンテナンス | 🟢 活発 | 🟢 活発 | 🔴 停滞気味 | 🟢 活発 |
| 主な用途 | クリーンアップ | 汎用ファイル操作 | (非推奨) | 削除特化 |
現代の Node.js 開発において、remove パッケージを選ぶ理由はほぼありません。機能は rimraf に含まれており、メンテナンス状況も rimraf や fs-extra の方が圧倒的に優れています。
fs-extra は、ファイル操作全般を統一したいプロジェクトにとって最強の選択肢です。既に fs-extra を導入しているなら、追加で rimraf を入れる必要はなく、fs.remove を使うだけで十分です。
rimraf は、余計な機能はいらない、ただ確実に削除したいという場合に最適です。CLI ツールの開発や、内部モジュールとして使用する場合に適しています。
del は、Grunt や Gulp、あるいは npm スクリプトなどで「特定のファイルパターンを削除したい」という明確なニーズがある場合にのみ選択します。グロブパターンの扱いが非常にスムーズです。
最終的には、「汎用性なら fs-extra、削除特化なら rimraf、パターン削除なら del」 という基準で選定するのが、技術的負債を増やさない賢明な判断です。
ビルドスクリプトなどで、特定のパターンに一致するファイル(例:dist/**/*.js)をまとめて削除したい場合に選択します。グロブパターンサポートが組み込まれているため、設定ファイルの整理やキャッシュクリアに適しています。
ファイル削除だけでなく、コピー、移動、JSON 読み書きなど、ファイルシステム操作全般を一元管理したい場合に最適です。fs.remove() メソッドは rimraf と同等の機能を提供し、既存の fs コードを置き換えるだけで導入できます。
歴史的なパッケージであり、現在はメンテナンスが停滞しているか、機能が rimraf に統合されています。新規プロジェクトでの使用は避け、代わりに rimraf または fs-extra の使用を強く推奨します。
純粋に「ディレクトリを再帰的に削除する」ことだけに特化したツールが必要な場合に選択します。他の依存関係を増やしたくない場合や、ビルドツールの内部ロジックとして軽量な削除機能が必要な時に最適です。
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.
npm install del
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'));
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!
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().
Returns Promise<string[]> with the deleted paths.
Returns string[] with the deleted paths.
Type: string | string[]
See the supported glob patterns.
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.
Type: boolean
Default: false
Allow deleting the current working directory and outside.
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'));
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']
Type: number
Default: Infinity
Minimum: 1
Concurrency limit.
Type: (progress: ProgressData) => void
Called after each file or directory is deleted.
import {deleteAsync} from 'del';
await deleteAsync(patterns, {
onProgress: progress => {
// …
}});
{
totalCount: number,
deletedCount: number,
percent: number,
path?: string
}
percent is a value between 0 and 1path is the absolute path of the deleted file or directory. It will not be present if nothing was deleted.See del-cli for a CLI for this module and trash-cli for a safe version that is suitable for running by hand.