summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE19
-rw-r--r--doc/api/fs.md55
-rw-r--r--lib/fs.js36
-rw-r--r--lib/internal/fs/promises.js14
-rw-r--r--lib/internal/fs/rimraf.js252
-rw-r--r--lib/internal/fs/utils.js29
-rw-r--r--node.gyp1
-rw-r--r--test/parallel/test-fs-rmdir-recursive.js152
-rwxr-xr-xtools/license-builder.sh3
9 files changed, 550 insertions, 11 deletions
diff --git a/LICENSE b/LICENSE
index 1f2435a722..f57c3dc0c3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1507,3 +1507,22 @@ The externally maintained libraries used by Node.js are:
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
+
+- rimraf, located at lib/internal/fs/rimraf.js, is licensed as follows:
+ """
+ The ISC License
+
+ Copyright (c) Isaac Z. Schlueter and Contributors
+
+ 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.
+
+ 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.
+ """
diff --git a/doc/api/fs.md b/doc/api/fs.md
index 5e2923a0ff..7e8266a90e 100644
--- a/doc/api/fs.md
+++ b/doc/api/fs.md
@@ -3017,10 +3017,14 @@ changes:
Synchronous rename(2). Returns `undefined`.
-## fs.rmdir(path, callback)
+## fs.rmdir(path[, options], callback)
<!-- YAML
added: v0.0.2
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/29168
+ description: The `recursive`, `maxBusyTries`, and `emfileWait` options are
+ now supported.
- version: v10.0.0
pr-url: https://github.com/nodejs/node/pull/12562
description: The `callback` parameter is no longer optional. Not passing
@@ -3035,7 +3039,21 @@ changes:
it will emit a deprecation warning with id DEP0013.
-->
+> Stability: 1 - Recursive removal is experimental.
+
* `path` {string|Buffer|URL}
+* `options` {Object}
+ * `emfileWait` {integer} If an `EMFILE` error is encountered, Node.js will
+ retry the operation with a linear backoff of 1ms longer on each try until the
+ timeout duration passes this limit. This option is ignored if the `recursive`
+ option is not `true`. **Default:** `1000`.
+ * `maxBusyTries` {integer} If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error is
+ encountered, Node.js will retry the operation with a linear backoff wait of
+ 100ms longer on each try. This option represents the number of retries. This
+ option is ignored if the `recursive` option is not `true`. **Default:** `3`.
+ * `recursive` {boolean} If `true`, perform a recursive directory removal. In
+ recursive mode, errors are not reported if `path` does not exist, and
+ operations are retried on failure. **Default:** `false`.
* `callback` {Function}
* `err` {Error}
@@ -3045,17 +3063,27 @@ to the completion callback.
Using `fs.rmdir()` on a file (not a directory) results in an `ENOENT` error on
Windows and an `ENOTDIR` error on POSIX.
-## fs.rmdirSync(path)
+## fs.rmdirSync(path[, options])
<!-- YAML
added: v0.1.21
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/29168
+ description: The `recursive`, `maxBusyTries`, and `emfileWait` options are
+ now supported.
- version: v7.6.0
pr-url: https://github.com/nodejs/node/pull/10739
description: The `path` parameters can be a WHATWG `URL` object using
`file:` protocol. Support is currently still *experimental*.
-->
+> Stability: 1 - Recursive removal is experimental.
+
* `path` {string|Buffer|URL}
+* `options` {Object}
+ * `recursive` {boolean} If `true`, perform a recursive directory removal. In
+ recursive mode, errors are not reported if `path` does not exist, and
+ operations are retried on failure. **Default:** `false`.
Synchronous rmdir(2). Returns `undefined`.
@@ -4694,12 +4722,31 @@ added: v10.0.0
Renames `oldPath` to `newPath` and resolves the `Promise` with no arguments
upon success.
-### fsPromises.rmdir(path)
+### fsPromises.rmdir(path[, options])
<!-- YAML
added: v10.0.0
+changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/29168
+ description: The `recursive`, `maxBusyTries`, and `emfileWait` options are
+ now supported.
-->
+> Stability: 1 - Recursive removal is experimental.
+
* `path` {string|Buffer|URL}
+* `options` {Object}
+ * `emfileWait` {integer} If an `EMFILE` error is encountered, Node.js will
+ retry the operation with a linear backoff of 1ms longer on each try until the
+ timeout duration passes this limit. This option is ignored if the `recursive`
+ option is not `true`. **Default:** `1000`.
+ * `maxBusyTries` {integer} If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error is
+ encountered, Node.js will retry the operation with a linear backoff wait of
+ 100ms longer on each try. This option represents the number of retries. This
+ option is ignored if the `recursive` option is not `true`. **Default:** `3`.
+ * `recursive` {boolean} If `true`, perform a recursive directory removal. In
+ recursive mode, errors are not reported if `path` does not exist, and
+ operations are retried on failure. **Default:** `false`.
* Returns: {Promise}
Removes the directory identified by `path` then resolves the `Promise` with
@@ -5193,7 +5240,7 @@ the file contents.
[`fs.readdir()`]: #fs_fs_readdir_path_options_callback
[`fs.readdirSync()`]: #fs_fs_readdirsync_path_options
[`fs.realpath()`]: #fs_fs_realpath_path_options_callback
-[`fs.rmdir()`]: #fs_fs_rmdir_path_callback
+[`fs.rmdir()`]: #fs_fs_rmdir_path_options_callback
[`fs.stat()`]: #fs_fs_stat_path_options_callback
[`fs.symlink()`]: #fs_fs_symlink_target_path_type_callback
[`fs.utimes()`]: #fs_fs_utimes_path_atime_mtime_callback
diff --git a/lib/fs.js b/lib/fs.js
index 37d1eb7917..5c7d907f5e 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -76,6 +76,7 @@ const {
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
+ validateRmdirOptions,
warnOnNonPortableTemplate
} = require('internal/fs/utils');
const {
@@ -100,6 +101,8 @@ let watchers;
let ReadFileContext;
let ReadStream;
let WriteStream;
+let rimraf;
+let rimrafSync;
// These have to be separate because of how graceful-fs happens to do it's
// monkeypatching.
@@ -736,16 +739,41 @@ function ftruncateSync(fd, len = 0) {
handleErrorFromBinding(ctx);
}
-function rmdir(path, callback) {
+
+function lazyLoadRimraf() {
+ if (rimraf === undefined)
+ ({ rimraf, rimrafSync } = require('internal/fs/rimraf'));
+}
+
+function rmdir(path, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+
callback = makeCallback(callback);
- path = getValidatedPath(path);
+ path = pathModule.toNamespacedPath(getValidatedPath(path));
+ options = validateRmdirOptions(options);
+
+ if (options.recursive) {
+ lazyLoadRimraf();
+ return rimraf(path, options, callback);
+ }
+
const req = new FSReqCallback();
req.oncomplete = callback;
- binding.rmdir(pathModule.toNamespacedPath(path), req);
+ binding.rmdir(path, req);
}
-function rmdirSync(path) {
+function rmdirSync(path, options) {
path = getValidatedPath(path);
+ options = validateRmdirOptions(options);
+
+ if (options.recursive) {
+ lazyLoadRimraf();
+ return rimrafSync(pathModule.toNamespacedPath(path), options);
+ }
+
const ctx = { path };
binding.rmdir(pathModule.toNamespacedPath(path), undefined, ctx);
handleErrorFromBinding(ctx);
diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index 7e6fae9273..5af2ffa763 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -18,6 +18,7 @@ const {
ERR_METHOD_NOT_IMPLEMENTED
} = require('internal/errors').codes;
const { isUint8Array } = require('internal/util/types');
+const { rimrafPromises } = require('internal/fs/rimraf');
const {
copyObject,
getDirents,
@@ -32,6 +33,7 @@ const {
validateBufferArray,
validateOffsetLengthRead,
validateOffsetLengthWrite,
+ validateRmdirOptions,
warnOnNonPortableTemplate
} = require('internal/fs/utils');
const {
@@ -300,9 +302,15 @@ async function ftruncate(handle, len = 0) {
return binding.ftruncate(handle.fd, len, kUsePromises);
}
-async function rmdir(path) {
- path = getValidatedPath(path);
- return binding.rmdir(pathModule.toNamespacedPath(path), kUsePromises);
+async function rmdir(path, options) {
+ path = pathModule.toNamespacedPath(getValidatedPath(path));
+ options = validateRmdirOptions(options);
+
+ if (options.recursive) {
+ return rimrafPromises(path, options);
+ }
+
+ return binding.rmdir(path, kUsePromises);
}
async function fdatasync(handle) {
diff --git a/lib/internal/fs/rimraf.js b/lib/internal/fs/rimraf.js
new file mode 100644
index 0000000000..73f783d1d2
--- /dev/null
+++ b/lib/internal/fs/rimraf.js
@@ -0,0 +1,252 @@
+// This file is a modified version of the rimraf module on npm. It has been
+// modified in the following ways:
+// - Use of the assert module has been replaced with core's error system.
+// - All code related to the glob dependency has been removed.
+// - Bring your own custom fs module is not currently supported.
+// - Some basic code cleanup.
+'use strict';
+const {
+ chmod,
+ chmodSync,
+ lstat,
+ lstatSync,
+ readdir,
+ readdirSync,
+ rmdir,
+ rmdirSync,
+ stat,
+ statSync,
+ unlink,
+ unlinkSync
+} = require('fs');
+const { join } = require('path');
+const { setTimeout } = require('timers');
+const notEmptyErrorCodes = new Set(['ENOTEMPTY', 'EEXIST', 'EPERM']);
+const isWindows = process.platform === 'win32';
+const epermHandler = isWindows ? fixWinEPERM : _rmdir;
+const epermHandlerSync = isWindows ? fixWinEPERMSync : _rmdirSync;
+const numRetries = isWindows ? 100 : 1;
+
+
+function rimraf(path, options, callback) {
+ let timeout = 0; // For EMFILE handling.
+ let busyTries = 0;
+
+ _rimraf(path, options, function CB(err) {
+ if (err) {
+ if ((err.code === 'EBUSY' || err.code === 'ENOTEMPTY' ||
+ err.code === 'EPERM') && busyTries < options.maxBusyTries) {
+ busyTries++;
+ return setTimeout(_rimraf, busyTries * 100, path, options, CB);
+ }
+
+ if (err.code === 'EMFILE' && timeout < options.emfileWait)
+ return setTimeout(_rimraf, timeout++, path, options, CB);
+
+ // The file is already gone.
+ if (err.code === 'ENOENT')
+ err = null;
+ }
+
+ callback(err);
+ });
+}
+
+
+function _rimraf(path, options, callback) {
+ // SunOS lets the root user unlink directories. Use lstat here to make sure
+ // it's not a directory.
+ lstat(path, (err, stats) => {
+ if (err) {
+ if (err.code === 'ENOENT')
+ return callback(null);
+
+ // Windows can EPERM on stat.
+ if (isWindows && err.code === 'EPERM')
+ return fixWinEPERM(path, options, err, callback);
+ } else if (stats.isDirectory()) {
+ return _rmdir(path, options, err, callback);
+ }
+
+ unlink(path, (err) => {
+ if (err) {
+ if (err.code === 'ENOENT')
+ return callback(null);
+ if (err.code === 'EISDIR')
+ return _rmdir(path, options, err, callback);
+ if (err.code === 'EPERM') {
+ return epermHandler(path, options, err, callback);
+ }
+ }
+
+ return callback(err);
+ });
+ });
+}
+
+
+function fixWinEPERM(path, options, originalErr, callback) {
+ chmod(path, 0o666, (err) => {
+ if (err)
+ return callback(err.code === 'ENOENT' ? null : originalErr);
+
+ stat(path, (err, stats) => {
+ if (err)
+ return callback(err.code === 'ENOENT' ? null : originalErr);
+
+ if (stats.isDirectory())
+ _rmdir(path, options, originalErr, callback);
+ else
+ unlink(path, callback);
+ });
+ });
+}
+
+
+function _rmdir(path, options, originalErr, callback) {
+ rmdir(path, (err) => {
+ if (err) {
+ if (notEmptyErrorCodes.has(err.code))
+ return _rmchildren(path, options, callback);
+ if (err.code === 'ENOTDIR')
+ return callback(originalErr);
+ }
+
+ callback(err);
+ });
+}
+
+
+function _rmchildren(path, options, callback) {
+ readdir(path, (err, files) => {
+ if (err)
+ return callback(err);
+
+ let numFiles = files.length;
+
+ if (numFiles === 0)
+ return rmdir(path, callback);
+
+ let done = false;
+
+ files.forEach((child) => {
+ rimraf(join(path, child), options, (err) => {
+ if (done)
+ return;
+
+ if (err) {
+ done = true;
+ return callback(err);
+ }
+
+ numFiles--;
+ if (numFiles === 0)
+ rmdir(path, callback);
+ });
+ });
+ });
+}
+
+
+function rimrafPromises(path, options) {
+ return new Promise((resolve, reject) => {
+ rimraf(path, options, (err) => {
+ if (err)
+ return reject(err);
+
+ resolve();
+ });
+ });
+}
+
+
+function rimrafSync(path, options) {
+ let stats;
+
+ try {
+ stats = lstatSync(path);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+
+ // Windows can EPERM on stat.
+ if (isWindows && err.code === 'EPERM')
+ fixWinEPERMSync(path, options, err);
+ }
+
+ try {
+ // SunOS lets the root user unlink directories.
+ if (stats !== undefined && stats.isDirectory())
+ _rmdirSync(path, options, null);
+ else
+ unlinkSync(path);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+ if (err.code === 'EPERM')
+ return epermHandlerSync(path, options, err);
+ if (err.code !== 'EISDIR')
+ throw err;
+
+ _rmdirSync(path, options, err);
+ }
+}
+
+
+function _rmdirSync(path, options, originalErr) {
+ try {
+ rmdirSync(path);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+ if (err.code === 'ENOTDIR')
+ throw originalErr;
+
+ if (notEmptyErrorCodes.has(err.code)) {
+ // Removing failed. Try removing all children and then retrying the
+ // original removal. Windows has a habit of not closing handles promptly
+ // when files are deleted, resulting in spurious ENOTEMPTY failures. Work
+ // around that issue by retrying on Windows.
+ readdirSync(path).forEach((child) => {
+ rimrafSync(join(path, child), options);
+ });
+
+ for (let i = 0; i < numRetries; i++) {
+ try {
+ return rmdirSync(path, options);
+ } catch {} // Ignore errors.
+ }
+ }
+ }
+}
+
+
+function fixWinEPERMSync(path, options, originalErr) {
+ try {
+ chmodSync(path, 0o666);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+
+ throw originalErr;
+ }
+
+ let stats;
+
+ try {
+ stats = statSync(path);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+
+ throw originalErr;
+ }
+
+ if (stats.isDirectory())
+ _rmdirSync(path, options, originalErr);
+ else
+ unlinkSync(path);
+}
+
+
+module.exports = { rimraf, rimrafPromises, rimrafSync };
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js
index 600d118db3..fb060d23e6 100644
--- a/lib/internal/fs/utils.js
+++ b/lib/internal/fs/utils.js
@@ -22,6 +22,10 @@ const {
} = require('internal/util/types');
const { once } = require('internal/util');
const { toPathIfFileURL } = require('internal/url');
+const {
+ validateInt32,
+ validateUint32
+} = require('internal/validators');
const pathModule = require('path');
const kType = Symbol('type');
const kStats = Symbol('stats');
@@ -525,6 +529,30 @@ function warnOnNonPortableTemplate(template) {
}
}
+const defaultRmdirOptions = {
+ emfileWait: 1000,
+ maxBusyTries: 3,
+ recursive: false,
+};
+
+const validateRmdirOptions = hideStackFrames((options) => {
+ if (options === undefined)
+ return defaultRmdirOptions;
+ if (options === null || typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
+
+ options = { ...defaultRmdirOptions, ...options };
+
+ if (typeof options.recursive !== 'boolean')
+ throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', options.recursive);
+
+ validateInt32(options.emfileWait, 'emfileWait', 0);
+ validateUint32(options.maxBusyTries, 'maxBusyTries');
+
+ return options;
+});
+
+
module.exports = {
assertEncoding,
BigIntStats, // for testing
@@ -545,5 +573,6 @@ module.exports = {
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
+ validateRmdirOptions,
warnOnNonPortableTemplate
};
diff --git a/node.gyp b/node.gyp
index 4576e5a335..1d45f51171 100644
--- a/node.gyp
+++ b/node.gyp
@@ -124,6 +124,7 @@
'lib/internal/freeze_intrinsics.js',
'lib/internal/fs/promises.js',
'lib/internal/fs/read_file_context.js',
+ 'lib/internal/fs/rimraf.js',
'lib/internal/fs/streams.js',
'lib/internal/fs/sync_write_stream.js',
'lib/internal/fs/utils.js',
diff --git a/test/parallel/test-fs-rmdir-recursive.js b/test/parallel/test-fs-rmdir-recursive.js
new file mode 100644
index 0000000000..b020221b27
--- /dev/null
+++ b/test/parallel/test-fs-rmdir-recursive.js
@@ -0,0 +1,152 @@
+// Flags: --expose-internals
+'use strict';
+const common = require('../common');
+const tmpdir = require('../common/tmpdir');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { validateRmdirOptions } = require('internal/fs/utils');
+let count = 0;
+
+tmpdir.refresh();
+
+function makeNonEmptyDirectory() {
+ const dirname = `rmdir-recursive-${count}`;
+ fs.mkdirSync(path.join(dirname, 'foo', 'bar', 'baz'), { recursive: true });
+ fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8');
+ count++;
+ return dirname;
+}
+
+// Test the asynchronous version.
+{
+ const dir = makeNonEmptyDirectory();
+
+ // Removal should fail without the recursive option.
+ fs.rmdir(dir, common.mustCall((err) => {
+ assert.strictEqual(err.syscall, 'rmdir');
+
+ // Removal should fail without the recursive option set to true.
+ fs.rmdir(dir, { recursive: false }, common.mustCall((err) => {
+ assert.strictEqual(err.syscall, 'rmdir');
+
+ // Recursive removal should succeed.
+ fs.rmdir(dir, { recursive: true }, common.mustCall((err) => {
+ assert.ifError(err);
+
+ // No error should occur if recursive and the directory does not exist.
+ fs.rmdir(dir, { recursive: true }, common.mustCall((err) => {
+ assert.ifError(err);
+
+ // Attempted removal should fail now because the directory is gone.
+ fs.rmdir(dir, common.mustCall((err) => {
+ assert.strictEqual(err.syscall, 'rmdir');
+ }));
+ }));
+ }));
+ }));
+ }));
+}
+
+// Test the synchronous version.
+{
+ const dir = makeNonEmptyDirectory();
+
+ // Removal should fail without the recursive option set to true.
+ common.expectsError(() => {
+ fs.rmdirSync(dir);
+ }, { syscall: 'rmdir' });
+ common.expectsError(() => {
+ fs.rmdirSync(dir, { recursive: false });
+ }, { syscall: 'rmdir' });
+
+ // Recursive removal should succeed.
+ fs.rmdirSync(dir, { recursive: true });
+
+ // No error should occur if recursive and the directory does not exist.
+ fs.rmdirSync(dir, { recursive: true });
+
+ // Attempted removal should fail now because the directory is gone.
+ common.expectsError(() => fs.rmdirSync(dir), { syscall: 'rmdir' });
+}
+
+// Test the Promises based version.
+(async () => {
+ const dir = makeNonEmptyDirectory();
+
+ // Removal should fail without the recursive option set to true.
+ assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
+ assert.rejects(fs.promises.rmdir(dir, { recursive: false }), {
+ syscall: 'rmdir'
+ });
+
+ // Recursive removal should succeed.
+ await fs.promises.rmdir(dir, { recursive: true });
+
+ // No error should occur if recursive and the directory does not exist.
+ await fs.promises.rmdir(dir, { recursive: true });
+
+ // Attempted removal should fail now because the directory is gone.
+ assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
+})();
+
+// Test input validation.
+{
+ const defaults = {
+ emfileWait: 1000,
+ maxBusyTries: 3,
+ recursive: false
+ };
+ const modified = {
+ emfileWait: 953,
+ maxBusyTries: 5,
+ recursive: true
+ };
+
+ assert.deepStrictEqual(validateRmdirOptions(), defaults);
+ assert.deepStrictEqual(validateRmdirOptions({}), defaults);
+ assert.deepStrictEqual(validateRmdirOptions(modified), modified);
+ assert.deepStrictEqual(validateRmdirOptions({
+ maxBusyTries: 99
+ }), {
+ emfileWait: 1000,
+ maxBusyTries: 99,
+ recursive: false
+ });
+
+ [null, 'foo', 5, NaN].forEach((bad) => {
+ common.expectsError(() => {
+ validateRmdirOptions(bad);
+ }, {
+ code: 'ERR_INVALID_ARG_TYPE',
+ type: TypeError,
+ message: /^The "options" argument must be of type object\./
+ });
+ });
+
+ [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => {
+ common.expectsError(() => {
+ validateRmdirOptions({ recursive: bad });
+ }, {
+ code: 'ERR_INVALID_ARG_TYPE',
+ type: TypeError,
+ message: /^The "recursive" argument must be of type boolean\./
+ });
+ });
+
+ common.expectsError(() => {
+ validateRmdirOptions({ emfileWait: -1 });
+ }, {
+ code: 'ERR_OUT_OF_RANGE',
+ type: RangeError,
+ message: /^The value of "emfileWait" is out of range\./
+ });
+
+ common.expectsError(() => {
+ validateRmdirOptions({ maxBusyTries: -1 });
+ }, {
+ code: 'ERR_OUT_OF_RANGE',
+ type: RangeError,
+ message: /^The value of "maxBusyTries" is out of range\./
+ });
+}
diff --git a/tools/license-builder.sh b/tools/license-builder.sh
index 7875a0cd24..c46b18845f 100755
--- a/tools/license-builder.sh
+++ b/tools/license-builder.sh
@@ -106,4 +106,7 @@ addlicense "HdrHistogram" "deps/histogram" "$(cat ${rootdir}/deps/histogram/LICE
addlicense "node-heapdump" "src/heap_utils.cc" \
"$(curl -sL https://raw.githubusercontent.com/bnoordhuis/node-heapdump/0ca52441e46241ffbea56a389e2856ec01c48c97/LICENSE)"
+addlicense "rimraf" "lib/internal/fs/rimraf.js" \
+ "$(curl -sL https://raw.githubusercontent.com/isaacs/rimraf/0e365ac4e4d64a25aa2a3cc026348f13410210e1/LICENSE)"
+
mv $tmplicense $licensefile