fs: move rmdir recursive option to end-of-life

Has been runtime deprecated for ~ 5 years now. It's time.

PR-URL: https://github.com/nodejs/node/pull/58616
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Dario Piotrowicz <dario.piotrowicz@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: LiviaMedeiros <livia@cirno.name>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
James M Snell 2025-06-21 09:20:38 -07:00 committed by GitHub
parent b04c4a44a5
commit eec0302088
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 105 additions and 434 deletions

View File

@ -3057,6 +3057,9 @@ The [`crypto.Certificate()` constructor][] is deprecated. Use
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58616
description: End-of-Life.
- version: v16.0.0
pr-url: https://github.com/nodejs/node/pull/37302
description: Runtime deprecation.
@ -3068,10 +3071,10 @@ changes:
description: Documentation-only deprecation.
-->
Type: Runtime
Type: End-of-Life
In future versions of Node.js, `recursive` option will be ignored for
`fs.rmdir`, `fs.rmdirSync`, and `fs.promises.rmdir`.
The `fs.rmdir`, `fs.rmdirSync`, and `fs.promises.rmdir` methods used
to support a `recursive` option. That option has been removed.
Use `fs.rm(path, { recursive: true, force: true })`,
`fs.rmSync(path, { recursive: true, force: true })` or

View File

@ -1589,6 +1589,9 @@ Renames `oldPath` to `newPath`.
<!-- YAML
added: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58616
description: Remove `recursive` option.
- version: v16.0.0
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fsPromises.rmdir(path, { recursive: true })` on a `path`
@ -1622,18 +1625,10 @@ changes:
-->
* `path` {string|Buffer|URL}
* `options` {Object}
* `maxRetries` {integer} If an `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY`, or
`EPERM` error is encountered, Node.js retries the operation with a linear
backoff wait of `retryDelay` milliseconds longer on each try. This option
represents the number of retries. This option is ignored if the `recursive`
option is not `true`. **Default:** `0`.
* `recursive` {boolean} If `true`, perform a recursive directory removal. In
recursive mode, operations are retried on failure. **Default:** `false`.
**Deprecated.**
* `retryDelay` {integer} The amount of time in milliseconds to wait between
retries. This option is ignored if the `recursive` option is not `true`.
**Default:** `100`.
* `options` {Object} There are currently no options exposed. There used to
be options for `recursive`, `maxBusyTries`, and `emfileWait` but they were
deprecated and removed. The `options` argument is still accepted for
backwards compatibility but it is not used.
* Returns: {Promise} Fulfills with `undefined` upon success.
Removes the directory identified by `path`.
@ -4255,6 +4250,9 @@ rename('oldFile.txt', 'newFile.txt', (err) => {
<!-- YAML
added: v0.0.2
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58616
description: Remove `recursive` option.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
@ -4305,18 +4303,10 @@ changes:
-->
* `path` {string|Buffer|URL}
* `options` {Object}
* `maxRetries` {integer} If an `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY`, or
`EPERM` error is encountered, Node.js retries the operation with a linear
backoff wait of `retryDelay` milliseconds longer on each try. This option
represents the number of retries. This option is ignored if the `recursive`
option is not `true`. **Default:** `0`.
* `recursive` {boolean} If `true`, perform a recursive directory removal. In
recursive mode, operations are retried on failure. **Default:** `false`.
**Deprecated.**
* `retryDelay` {integer} The amount of time in milliseconds to wait between
retries. This option is ignored if the `recursive` option is not `true`.
**Default:** `100`.
* `options` {Object} There are currently no options exposed. There used to
be options for `recursive`, `maxBusyTries`, and `emfileWait` but they were
deprecated and removed. The `options` argument is still accepted for
backwards compatibility but it is not used.
* `callback` {Function}
* `err` {Error}
@ -6234,6 +6224,9 @@ See the POSIX rename(2) documentation for more details.
<!-- YAML
added: v0.1.21
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58616
description: Remove `recursive` option.
- version: v16.0.0
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fs.rmdirSync(path, { recursive: true })` on a `path`
@ -6271,18 +6264,10 @@ changes:
-->
* `path` {string|Buffer|URL}
* `options` {Object}
* `maxRetries` {integer} If an `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY`, or
`EPERM` error is encountered, Node.js retries the operation with a linear
backoff wait of `retryDelay` milliseconds longer on each try. This option
represents the number of retries. This option is ignored if the `recursive`
option is not `true`. **Default:** `0`.
* `recursive` {boolean} If `true`, perform a recursive directory removal. In
recursive mode, operations are retried on failure. **Default:** `false`.
**Deprecated.**
* `retryDelay` {integer} The amount of time in milliseconds to wait between
retries. This option is ignored if the `recursive` option is not `true`.
**Default:** `100`.
* `options` {Object} There are currently no options exposed. There used to
be options for `recursive`, `maxBusyTries`, and `emfileWait` but they were
deprecated and removed. The `options` argument is still accepted for
backwards compatibility but it is not used.
Synchronous rmdir(2). Returns `undefined`.

View File

@ -102,7 +102,6 @@ const {
},
copyObject,
Dirent,
emitRecursiveRmdirWarning,
getDirent,
getDirents,
getOptions,
@ -1109,11 +1108,7 @@ function lazyLoadRimraf() {
/**
* Asynchronously removes a directory.
* @param {string | Buffer | URL} path
* @param {{
* maxRetries?: number;
* recursive?: boolean;
* retryDelay?: number;
* }} [options]
* @param {{}} [options]
* @param {(err?: Error) => any} callback
* @returns {void}
*/
@ -1123,60 +1118,45 @@ function rmdir(path, options, callback) {
options = undefined;
}
if (options?.recursive !== undefined) {
// This API previously accepted a `recursive` option that was deprecated
// and removed. However, in order to make the change more visible, we
// opted to throw an error if recursive is specified rather than removing it
// entirely.
throw new ERR_INVALID_ARG_VALUE(
'options.recursive',
options.recursive,
'is no longer supported',
);
}
callback = makeCallback(callback);
path = getValidatedPath(path);
if (options?.recursive) {
emitRecursiveRmdirWarning();
validateRmOptions(
path,
{ ...options, force: false },
true,
(err, options) => {
if (err === false) {
const req = new FSReqCallback();
req.oncomplete = callback;
binding.rmdir(path, req);
return;
}
if (err) {
return callback(err);
}
lazyLoadRimraf();
rimraf(path, options, callback);
});
} else {
validateRmdirOptions(options);
const req = new FSReqCallback();
req.oncomplete = callback;
binding.rmdir(path, req);
}
}
/**
* Synchronously removes a directory.
* @param {string | Buffer | URL} path
* @param {{
* maxRetries?: number;
* recursive?: boolean;
* retryDelay?: number;
* }} [options]
* @param {{}} [options]
* @returns {void}
*/
function rmdirSync(path, options) {
path = getValidatedPath(path);
if (options?.recursive) {
emitRecursiveRmdirWarning();
options = validateRmOptionsSync(path, { ...options, force: false }, true);
if (options !== false) {
return binding.rmSync(path, options.maxRetries, options.recursive, options.retryDelay);
}
} else {
validateRmdirOptions(options);
if (options?.recursive !== undefined) {
throw new ERR_INVALID_ARG_VALUE(
'options.recursive',
options.recursive,
'is no longer supported',
);
}
validateRmdirOptions(options);
binding.rmdir(path);
}

View File

@ -56,7 +56,6 @@ const {
kWriteFileMaxChunkSize,
},
copyObject,
emitRecursiveRmdirWarning,
getDirents,
getOptions,
getStatFsFromBinding,
@ -812,16 +811,17 @@ async function rm(path, options) {
async function rmdir(path, options) {
path = getValidatedPath(path);
options = validateRmdirOptions(options);
if (options.recursive) {
emitRecursiveRmdirWarning();
const stats = await stat(path);
if (stats.isDirectory()) {
return lazyRimRaf()(path, options);
}
if (options?.recursive !== undefined) {
throw new ERR_INVALID_ARG_VALUE(
'options.recursive',
options.recursive,
'is no longer supported',
);
}
options = validateRmdirOptions(options);
return await PromisePrototypeThen(
binding.rmdir(path, kUsePromises),
undefined,

View File

@ -778,12 +778,6 @@ const defaultRmOptions = {
maxRetries: 0,
};
const defaultRmdirOptions = {
retryDelay: 100,
maxRetries: 0,
recursive: false,
};
const validateCpOptions = hideStackFrames((options) => {
if (options === undefined)
return { ...defaultCpOptions };
@ -807,7 +801,10 @@ const validateCpOptions = hideStackFrames((options) => {
const validateRmOptions = hideStackFrames((path, options, expectDir, cb) => {
options = validateRmdirOptions(options, defaultRmOptions);
validateBoolean(options.force, 'options.force');
validateBoolean.withoutStackTrace(options.force, 'options.force');
validateBoolean.withoutStackTrace(options.recursive, 'options.recursive');
validateInt32.withoutStackTrace(options.retryDelay, 'options.retryDelay', 0);
validateUint32.withoutStackTrace(options.maxRetries, 'options.maxRetries');
lazyLoadFs().lstat(path, (err, stats) => {
if (err) {
@ -839,6 +836,10 @@ const validateRmOptions = hideStackFrames((path, options, expectDir, cb) => {
const validateRmOptionsSync = hideStackFrames((path, options, expectDir) => {
options = validateRmdirOptions.withoutStackTrace(options, defaultRmOptions);
validateBoolean.withoutStackTrace(options.force, 'options.force');
validateBoolean.withoutStackTrace(options.recursive, 'options.recursive');
validateInt32.withoutStackTrace(options.retryDelay, 'options.retryDelay', 0);
validateUint32.withoutStackTrace(options.maxRetries, 'options.maxRetries');
if (!options.force || expectDir || !options.recursive) {
const isDirectory = lazyLoadFs()
@ -862,35 +863,14 @@ const validateRmOptionsSync = hideStackFrames((path, options, expectDir) => {
return options;
});
let recursiveRmdirWarned;
function emitRecursiveRmdirWarning() {
if (recursiveRmdirWarned === undefined) {
// TODO(joyeecheung): use getOptionValue('--no-deprecation') instead.
recursiveRmdirWarned = process.noDeprecation;
}
if (!recursiveRmdirWarned) {
process.emitWarning(
'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DeprecationWarning',
'DEP0147',
);
recursiveRmdirWarned = true;
}
}
const validateRmdirOptions = hideStackFrames(
(options, defaults = defaultRmdirOptions) => {
(options, defaults = { __proto__: null }) => {
if (options === undefined)
return defaults;
validateObject.withoutStackTrace(options, 'options');
options = { ...defaults, ...options };
validateBoolean.withoutStackTrace(options.recursive, 'options.recursive');
validateInt32.withoutStackTrace(options.retryDelay, 'options.retryDelay', 0);
validateUint32.withoutStackTrace(options.maxRetries, 'options.maxRetries');
return options;
});
@ -950,7 +930,6 @@ module.exports = {
copyObject,
Dirent,
DirentFromStats,
emitRecursiveRmdirWarning,
getDirent,
getDirents,
getOptions,

View File

@ -0,0 +1,31 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const {
rmdir,
rmdirSync,
promises: { rmdir: rmdirPromise }
} = require('fs');
assert.throws(() => {
rmdir('nonexistent', {
recursive: true,
}, common.mustNotCall());
}, {
code: 'ERR_INVALID_ARG_VALUE',
});
assert.throws(() => {
rmdirSync('nonexistent', {
recursive: true,
});
}, {
code: 'ERR_INVALID_ARG_VALUE',
});
rmdirPromise('nonexistent', {
recursive: true,
}).then(common.mustNotCall(), common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE');
}));

View File

@ -1,22 +0,0 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
tmpdir.refresh();
{
// Should warn when trying to delete a nonexistent path
common.expectWarning(
'DeprecationWarning',
'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DEP0147'
);
assert.throws(
() => fs.rmdirSync(tmpdir.resolve('noexist.txt'),
{ recursive: true }),
{ code: 'ENOENT' }
);
}

View File

@ -1,22 +0,0 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
tmpdir.refresh();
{
common.expectWarning(
'DeprecationWarning',
'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DEP0147'
);
const filePath = tmpdir.resolve('rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
assert.throws(
() => fs.rmdirSync(filePath, { recursive: true }),
{ code: common.isWindows ? 'ENOENT' : 'ENOTDIR' }
);
}

View File

@ -1,21 +0,0 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const fs = require('fs');
tmpdir.refresh();
{
// Should warn when trying to delete a nonexistent path
common.expectWarning(
'DeprecationWarning',
'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DEP0147'
);
fs.rmdir(
tmpdir.resolve('noexist.txt'),
{ recursive: true },
common.mustCall()
);
}

View File

@ -1,21 +0,0 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
tmpdir.refresh();
{
common.expectWarning(
'DeprecationWarning',
'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DEP0147'
);
const filePath = tmpdir.resolve('rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => {
assert.strictEqual(err.code, common.isWindows ? 'ENOENT' : 'ENOTDIR');
}));
}

View File

@ -1,219 +0,0 @@
// 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');
common.expectWarning(
'DeprecationWarning',
'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' +
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DEP0147'
);
tmpdir.refresh();
let count = 0;
const nextDirPath = (name = 'rmdir-recursive') =>
tmpdir.resolve(`${name}-${count++}`);
function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) {
fs.mkdirSync(dirname, { recursive: true });
fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8');
const options = { flag: 'wx' };
for (let f = files; f > 0; f--) {
fs.writeFileSync(path.join(dirname, `f-${depth}-${f}`), '', options);
}
if (createSymLinks) {
// Valid symlink
fs.symlinkSync(
`f-${depth}-1`,
path.join(dirname, `link-${depth}-good`),
'file'
);
// Invalid symlink
fs.symlinkSync(
'does-not-exist',
path.join(dirname, `link-${depth}-bad`),
'file'
);
}
// File with a name that looks like a glob
fs.writeFileSync(path.join(dirname, '[a-z0-9].txt'), '', options);
depth--;
if (depth <= 0) {
return;
}
for (let f = folders; f > 0; f--) {
fs.mkdirSync(
path.join(dirname, `folder-${depth}-${f}`),
{ recursive: true }
);
makeNonEmptyDirectory(
depth,
files,
folders,
path.join(dirname, `d-${depth}-${f}`),
createSymLinks
);
}
}
function removeAsync(dir) {
// 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.mustSucceed(() => {
// An error should occur if recursive and the directory does not exist.
fs.rmdir(dir, { recursive: true }, common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
// Attempted removal should fail now because the directory is gone.
fs.rmdir(dir, common.mustCall((err) => {
assert.strictEqual(err.syscall, 'rmdir');
}));
}));
}));
}));
}));
}
// Test the asynchronous version
{
// Create a 4-level folder hierarchy including symlinks
let dir = nextDirPath();
makeNonEmptyDirectory(4, 10, 2, dir, true);
removeAsync(dir);
// Create a 2-level folder hierarchy without symlinks
dir = nextDirPath();
makeNonEmptyDirectory(2, 10, 2, dir, false);
removeAsync(dir);
// Create a flat folder including symlinks
dir = nextDirPath();
makeNonEmptyDirectory(1, 10, 2, dir, true);
removeAsync(dir);
}
// Test the synchronous version.
{
const dir = nextDirPath();
makeNonEmptyDirectory(4, 10, 2, dir, true);
// Removal should fail without the recursive option set to true.
assert.throws(() => {
fs.rmdirSync(dir);
}, { syscall: 'rmdir' });
assert.throws(() => {
fs.rmdirSync(dir, { recursive: false });
}, { syscall: 'rmdir' });
// Recursive removal should succeed.
fs.rmdirSync(dir, { recursive: true });
// An error should occur if recursive and the directory does not exist.
assert.throws(() => fs.rmdirSync(dir, { recursive: true }),
{ code: 'ENOENT' });
// Attempted removal should fail now because the directory is gone.
assert.throws(() => fs.rmdirSync(dir), { syscall: 'rmdir' });
}
// Test the Promises based version.
(async () => {
const dir = nextDirPath();
makeNonEmptyDirectory(4, 10, 2, dir, true);
// Removal should fail without the recursive option set to true.
await assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
await assert.rejects(fs.promises.rmdir(dir, { recursive: false }), {
syscall: 'rmdir'
});
// Recursive removal should succeed.
await fs.promises.rmdir(dir, { recursive: true });
// An error should occur if recursive and the directory does not exist.
await assert.rejects(fs.promises.rmdir(dir, { recursive: true }),
{ code: 'ENOENT' });
// Attempted removal should fail now because the directory is gone.
await assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
})().then(common.mustCall());
// Test input validation.
{
const defaults = {
retryDelay: 100,
maxRetries: 0,
recursive: false
};
const modified = {
retryDelay: 953,
maxRetries: 5,
recursive: true
};
assert.deepStrictEqual(validateRmdirOptions(), defaults);
assert.deepStrictEqual(validateRmdirOptions({}), defaults);
assert.deepStrictEqual(validateRmdirOptions(modified), modified);
assert.deepStrictEqual(validateRmdirOptions({
maxRetries: 99
}), {
retryDelay: 100,
maxRetries: 99,
recursive: false
});
[null, 'foo', 5, NaN].forEach((bad) => {
assert.throws(() => {
validateRmdirOptions(bad);
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /^The "options" argument must be of type object\./
});
});
[undefined, null, 'foo', Infinity, function() {}].forEach((bad) => {
assert.throws(() => {
validateRmdirOptions({ recursive: bad });
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /^The "options\.recursive" property must be of type boolean\./
});
});
assert.throws(() => {
validateRmdirOptions({ retryDelay: -1 });
}, {
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
message: /^The value of "options\.retryDelay" is out of range\./
});
assert.throws(() => {
validateRmdirOptions({ maxRetries: -1 });
}, {
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
message: /^The value of "options\.maxRetries" is out of range\./
});
}

View File

@ -9,7 +9,7 @@ tmpdir.refresh();
{
assert.throws(
() =>
fs.rmdirSync(tmpdir.resolve('noexist.txt'), { recursive: true }),
fs.rmdirSync(tmpdir.resolve('noexist.txt')),
{
code: 'ENOENT',
}
@ -18,7 +18,6 @@ tmpdir.refresh();
{
fs.rmdir(
tmpdir.resolve('noexist.txt'),
{ recursive: true },
common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
})
@ -26,8 +25,7 @@ tmpdir.refresh();
}
{
assert.rejects(
() => fs.promises.rmdir(tmpdir.resolve('noexist.txt'),
{ recursive: true }),
() => fs.promises.rmdir(tmpdir.resolve('noexist.txt')),
{
code: 'ENOENT',
}

View File

@ -11,18 +11,18 @@ const code = common.isWindows ? 'ENOENT' : 'ENOTDIR';
{
const filePath = tmpdir.resolve('rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
assert.throws(() => fs.rmdirSync(filePath, { recursive: true }), { code });
assert.throws(() => fs.rmdirSync(filePath), { code });
}
{
const filePath = tmpdir.resolve('rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => {
fs.rmdir(filePath, common.mustCall((err) => {
assert.strictEqual(err.code, code);
}));
}
{
const filePath = tmpdir.resolve('rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
assert.rejects(() => fs.promises.rmdir(filePath, { recursive: true }),
assert.rejects(() => fs.promises.rmdir(filePath),
{ code }).then(common.mustCall());
}