├── .gitattributes ├── .gitignore ├── .travis.yml ├── appveyor.yml ├── .editorconfig ├── LICENSE ├── package.json ├── README.md ├── index.js └── test.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | except: /^v\d/ 3 | language: node_js 4 | node_js: node 5 | after_script: node_modules/.bin/nyc report --reporter=text-lcov | npx coveralls 6 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | platform: x64 3 | shallow_clone: true 4 | skip_tags: true 5 | install: 6 | - ps: Install-Product node Stable x64 7 | - npm install 8 | build: off 9 | test_script: node test.js 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | tab_width = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{md,yml}] 12 | indent_style = space 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | Copyright 2017 - 2018 Shinnosuke Watanabe 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rmfr", 3 | "version": "2.0.0", 4 | "description": "Node.js implementation of `rm -fr` – recursive removal of files and directories", 5 | "repository": "shinnn/rmfr", 6 | "author": "Shinnosuke Watanabe (https://github.com/shinnn)", 7 | "scripts": { 8 | "pretest": "eslint --fix --format=codeframe index.js test.js", 9 | "test": "nyc --reporter=html --reporter=text node test.js" 10 | }, 11 | "license": "ISC", 12 | "files": [ 13 | "index.js" 14 | ], 15 | "keywords": [ 16 | "rimraf", 17 | "rm", 18 | "rmdir", 19 | "unlink", 20 | "remove", 21 | "clean", 22 | "recursive", 23 | "glob", 24 | "file", 25 | "directory", 26 | "promise", 27 | "promisified", 28 | "then", 29 | "thenable" 30 | ], 31 | "dependencies": { 32 | "assert-valid-glob-opts": "^1.0.0", 33 | "glob": "^7.1.2", 34 | "graceful-fs": "^4.1.11", 35 | "inspect-with-kind": "^1.0.4", 36 | "rimraf": "^2.6.2" 37 | }, 38 | "devDependencies": { 39 | "@shinnn/eslint-config-node": "^5.0.0", 40 | "eslint": "^4.18.1", 41 | "nyc": "^11.5.0", 42 | "tape": "^4.9.0" 43 | }, 44 | "eslintConfig": { 45 | "extends": "@shinnn/node" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rmfr 2 | 3 | [![npm version](https://img.shields.io/npm/v/rmfr.svg)](https://www.npmjs.com/package/rmfr) 4 | [![Build Status](https://travis-ci.org/shinnn/rmfr.svg?branch=master)](https://travis-ci.org/shinnn/rmfr) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/afcmk50xuig9jfs7/branch/master?svg=true)](https://ci.appveyor.com/project/ShinnosukeWatanabe/rmfr/branch/master) 6 | [![Coverage Status](https://coveralls.io/repos/github/shinnn/rmfr/badge.svg?branch=master)](https://coveralls.io/github/shinnn/rmfr?branch=master) 7 | 8 | [Node.js](https://nodejs.org/) implementation of `rm -fr` – recursive removal of files and directories 9 | 10 | ```javascript 11 | const rmfr = require('rmfr'); 12 | 13 | (async () => await rmfr('path/to/target'))(); 14 | ``` 15 | 16 | ## Installation 17 | 18 | [Use](https://docs.npmjs.com/cli/install) [npm](https://docs.npmjs.com/getting-started/what-is-npm). 19 | 20 | ``` 21 | npm install rmfr 22 | ``` 23 | 24 | ## API 25 | 26 | ```javascript 27 | const rmfr = require('rmfr'); 28 | ``` 29 | 30 | ### rmfr(*path* [, *options*]) 31 | 32 | *path*: `string` (a file/directory path) 33 | *options*: `Object` 34 | Return: `Promise` 35 | 36 | When it finish removing a target, it will be [*fulfilled*](https://promisesaplus.com/#point-26) with no arguments. 37 | 38 | When it fails to remove a target, it will be [*rejected*](https://promisesaplus.com/#point-30) with an error as its first argument. 39 | 40 | #### Options 41 | 42 | All [`rimraf`](https://github.com/isaacs/rimraf) [options](https://github.com/isaacs/rimraf#options) except for `disableGlob` are available, with some differences: 43 | 44 | * `glob` option defaults to `false`. 45 | * If you want to specify targets using glob pattern, set `glob` option `true` or provide a [`node-glob` options object](https://github.com/isaacs/node-glob#options). 46 | * `unlink`, `chmod`, `rmdir` and `readdir` options default to the corresponding [`graceful-fs`](https://github.com/isaacs/node-graceful-fs) methods. 47 | 48 | ```javascript 49 | const rmfr = require('rmfr'); 50 | 51 | rmfr('inde*.js'); // doesn't remove `./index.js` 52 | rmfr('inde*.js', {glob: true}); // removes `./index.js` 53 | ``` 54 | 55 | ## License 56 | 57 | [ISC License](./LICENSE) © 2017 - 2018 Shinnosuke Watanabe 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {promisify} = require('util'); 4 | const {resolve} = require('path'); 5 | 6 | const assertValidGlobOpts = require('assert-valid-glob-opts'); 7 | const {chmod, readdir, rmdir, unlink} = require('graceful-fs'); 8 | const {hasMagic} = require('glob'); 9 | const inspectWithKind = require('inspect-with-kind'); 10 | const rimraf = require('rimraf'); 11 | 12 | const RIMRAF_DOC_URL = 'https://github.com/isaacs/rimraf#options'; 13 | const SUPPORTED_FS_METHODS = [ 14 | 'unlink', 15 | 'chmod', 16 | 'stat', 17 | 'lstat', 18 | 'rmdir', 19 | 'readdir' 20 | ]; 21 | const UNSUPPORTED_GLOB_OPTIONS = [ 22 | 'mark', 23 | 'stat' 24 | ]; 25 | const FORCED_GLOB_OPTIONS = [ 26 | 'nosort', 27 | 'nounique' 28 | ]; 29 | const promisifiedRimraf = promisify(rimraf); 30 | 31 | module.exports = async function rmfr(...args) { 32 | const argLen = args.length; 33 | 34 | if (argLen !== 1 && argLen !== 2) { 35 | throw new RangeError(`Expected 1 or 2 arguments ([, ]), but got ${ 36 | argLen === 0 ? 'no' : argLen 37 | } arguments.`); 38 | } 39 | 40 | const defaultOptions = { 41 | glob: false, 42 | chmod, 43 | readdir, 44 | rmdir, 45 | unlink 46 | }; 47 | 48 | if (argLen === 1) { 49 | return promisifiedRimraf(args[0], defaultOptions); 50 | } 51 | 52 | const [path] = args; 53 | 54 | if (typeof args[1] !== 'object') { 55 | throw new TypeError(`Expected an option object passed to rimraf (${RIMRAF_DOC_URL}), but got ${ 56 | inspectWithKind(args[1]) 57 | }.`); 58 | } 59 | 60 | const options = Object.assign(defaultOptions, args[1]); 61 | const errors = []; 62 | 63 | for (const method of SUPPORTED_FS_METHODS) { 64 | if (options[method] !== undefined && typeof options[method] !== 'function') { 65 | errors.push(`\`${method}\` option must be a function, but got ${ 66 | inspectWithKind(options[method]) 67 | }.`); 68 | } 69 | } 70 | 71 | if (options.maxBusyTries !== undefined && typeof options.maxBusyTries !== 'number') { 72 | errors.push(`\`maxBusyTries\` option must be a number, but got ${ 73 | inspectWithKind(options.maxBusyTries) 74 | }.`); 75 | } 76 | 77 | if (options.emfileWait !== undefined && typeof options.emfileWait !== 'number') { 78 | errors.push(`\`emfileWait\` option must be a number, but got ${ 79 | inspectWithKind(options.emfileWait) 80 | }.`); 81 | } 82 | 83 | if (options.disableGlob !== undefined) { 84 | errors.push(`rmfr doesn't support \`disableGlob\` option, but a value ${ 85 | inspectWithKind(options.disableGlob) 86 | } was provided. rmfr disables glob feature by default.`); 87 | } 88 | 89 | const defaultGlobOptions = { 90 | nosort: true, 91 | nounique: true, 92 | silent: true 93 | }; 94 | 95 | if (options.glob === true) { 96 | options.glob = defaultGlobOptions; 97 | } else if (typeof options.glob === 'object') { 98 | assertValidGlobOpts(options.glob); 99 | const hasCwdOption = options.glob.cwd !== undefined; 100 | 101 | for (const unsupportedGlobOption of UNSUPPORTED_GLOB_OPTIONS) { 102 | const val = options.glob[unsupportedGlobOption]; 103 | 104 | if (val) { 105 | errors.push(`rmfr doesn't support \`${unsupportedGlobOption}\` option in \`glob\` option, but got ${ 106 | inspectWithKind(val) 107 | }.`); 108 | } 109 | } 110 | 111 | for (const forcedGlobOption of FORCED_GLOB_OPTIONS) { 112 | const val = options.glob[forcedGlobOption]; 113 | 114 | if (val === false) { 115 | errors.push(`rmfr doesn't allow \`${forcedGlobOption}\` option in \`glob\` option to be disabled, but \`false\` was passed to it.`); 116 | } 117 | } 118 | 119 | options.glob = Object.assign(defaultOptions, options.glob, { 120 | // Remove this line when isaacs/rimraf#133 is merged 121 | absolute: hasCwdOption 122 | }); 123 | 124 | if (errors.length === 0 && hasCwdOption && !hasMagic(path, options.glob)) { 125 | // Bypass https://github.com/isaacs/rimraf/blob/v2.6.2/rimraf.js#L62 126 | return promisifiedRimraf(resolve(options.glob.cwd, path), Object.assign(options, { 127 | disableGlob: true 128 | })); 129 | } 130 | } else if (options.glob !== false) { 131 | errors.push(`\`glob\` option must be an object passed to \`glob\` or a Boolean value, but got ${ 132 | inspectWithKind(options.glob) 133 | }.`); 134 | } 135 | 136 | if (errors.length === 1) { 137 | throw new TypeError(errors[0]); 138 | } 139 | 140 | if (errors.length !== 0) { 141 | throw new TypeError(`There was ${errors.length} errors in rimraf options you provided: 142 | ${errors.map(error => ` * ${error}`).join('\n')} 143 | `); 144 | } 145 | 146 | return promisifiedRimraf(path, options); 147 | }; 148 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {promisify} = require('util'); 4 | 5 | const {lstat, mkdir, writeFile} = require('graceful-fs'); 6 | const rmfr = require('.'); 7 | const test = require('tape'); 8 | 9 | const promisifiedLstat = promisify(lstat); 10 | const promisifiedMkdir = promisify(mkdir); 11 | const promisifiedWriteFile = promisify(writeFile); 12 | 13 | test('rmfr()', async t => { 14 | await Promise.all([ 15 | promisifiedWriteFile('tmp_file', ''), 16 | promisifiedMkdir('tmp_dir') 17 | ]); 18 | 19 | await rmfr('tmp_file'); 20 | 21 | try { 22 | await promisifiedLstat('tmp_file'); 23 | t.fail('File not removed.'); 24 | } catch ({code}) { 25 | t.equal( 26 | code, 27 | 'ENOENT', 28 | 'should remove a file.' 29 | ); 30 | } 31 | 32 | await rmfr('tmp_d*', {}); 33 | 34 | t.ok( 35 | (await promisifiedLstat('tmp_dir')).isDirectory(), 36 | 'should disable `glob` option by default.' 37 | ); 38 | 39 | await rmfr('../{tmp_d*,test.js}', { 40 | glob: { 41 | cwd: 'node_modules', 42 | ignore: __filename 43 | } 44 | }); 45 | 46 | try { 47 | await promisifiedLstat('../{tmp_d*,package.json}'); 48 | t.fail('Directory not removed.'); 49 | } catch ({code}) { 50 | t.equal( 51 | code, 52 | 'ENOENT', 53 | 'should support glob.' 54 | ); 55 | } 56 | 57 | t.ok( 58 | (await promisifiedLstat(__filename)).isFile(), 59 | 'should support glob options.' 60 | ); 61 | 62 | await rmfr('test.js', { 63 | glob: { 64 | cwd: 'this/directory/does/not/exist' 65 | } 66 | }); 67 | 68 | t.ok( 69 | (await promisifiedLstat(__filename)).isFile(), 70 | 'should consider `cwd` even if the path contains no special characters.' 71 | ); 72 | 73 | const error = new Error('_'); 74 | 75 | try { 76 | await rmfr('.gitignore', {unlink: (path, cb) => cb(error)}); 77 | t.fail('Unexpectedly succeeded.'); 78 | } catch (err) { 79 | t.equal( 80 | err, 81 | error, 82 | 'should fail when an error occurs while calling rimraf.' 83 | ); 84 | } 85 | 86 | try { 87 | await rmfr(); 88 | t.fail('Unexpectedly succeeded.'); 89 | } catch ({message}) { 90 | t.equal( 91 | message, 92 | 'Expected 1 or 2 arguments ([, ]), but got no arguments.', 93 | 'should fail when it takes no arguments.' 94 | ); 95 | } 96 | 97 | try { 98 | await rmfr('<', {o: 'O'}, '/'); 99 | t.fail('Unexpectedly succeeded.'); 100 | } catch ({message}) { 101 | t.equal( 102 | message, 103 | 'Expected 1 or 2 arguments ([, ]), but got 3 arguments.', 104 | 'should fail when it takes too many arguments.' 105 | ); 106 | } 107 | 108 | try { 109 | await rmfr(['1'], {glob: true}); 110 | t.fail('Unexpectedly succeeded.'); 111 | } catch ({message}) { 112 | t.equal( 113 | message, 114 | 'rimraf: path should be a string', 115 | 'should fail when the first argument is not a string.' 116 | ); 117 | } 118 | 119 | try { 120 | await rmfr('foo', 1); 121 | t.fail('Unexpectedly succeeded.'); 122 | } catch ({message}) { 123 | t.ok( 124 | /^Expected an option object passed to rimraf.*, but got 1 \(number\)\./.test(message), 125 | 'should fail when the second argument is not an object.' 126 | ); 127 | } 128 | 129 | try { 130 | await rmfr('foo', {chmod: new Set(['a'])}); 131 | t.fail('Unexpectedly succeeded.'); 132 | } catch ({name}) { 133 | t.equal( 134 | name, 135 | 'TypeError', 136 | 'should fail when it takes an invalid option.' 137 | ); 138 | } 139 | 140 | try { 141 | await rmfr('foo', { 142 | maxBusyTries: 'foo', 143 | emfileWait: 'bar', 144 | glob: 'baz' 145 | }); 146 | t.fail('Unexpectedly succeeded.'); 147 | } catch ({name}) { 148 | t.equal( 149 | name, 150 | 'TypeError', 151 | 'should fail when it takes invalid options.' 152 | ); 153 | } 154 | 155 | try { 156 | await rmfr('foo', { 157 | glob: { 158 | stat: true 159 | } 160 | }); 161 | t.fail('Unexpectedly succeeded.'); 162 | } catch ({message}) { 163 | t.equal( 164 | message, 165 | 'rmfr doesn\'t support `stat` option in `glob` option, but got true (boolean).', 166 | 'should fail when it takes unsupported glob option.' 167 | ); 168 | } 169 | 170 | try { 171 | await rmfr('foo', { 172 | glob: { 173 | nosort: false 174 | } 175 | }); 176 | t.fail('Unexpectedly succeeded.'); 177 | } catch ({message}) { 178 | t.equal( 179 | message, 180 | 'rmfr doesn\'t allow `nosort` option in `glob` option to be disabled, but `false` was passed to it.', 181 | 'should fail when the forcedly enabled option is disabled.' 182 | ); 183 | } 184 | 185 | try { 186 | await rmfr('foo', {disableGlob: true}); 187 | t.fail('Unexpectedly succeeded.'); 188 | } catch ({message}) { 189 | t.equal( 190 | message, 191 | 'rmfr doesn\'t support `disableGlob` option, but a value true (boolean) was provided. rmfr disables glob feature by default.', 192 | 'should fail when `disableGlob` option is provided.' 193 | ); 194 | } 195 | 196 | try { 197 | await rmfr('foo', { 198 | glob: { 199 | ignore: new WeakSet() 200 | } 201 | }); 202 | t.fail('Unexpectedly succeeded.'); 203 | } catch ({message}) { 204 | t.equal( 205 | message, 206 | 'node-glob expected `ignore` option to be an array or string, but got WeakSet {}.', 207 | 'should fail when it takes invalid glob options.' 208 | ); 209 | } 210 | 211 | t.end(); 212 | }); 213 | --------------------------------------------------------------------------------