From 5623ba3d5b30753d3afea4fc7cfa2c88cf2768ea Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Mon, 31 Oct 2022 14:19:52 -0400 Subject: [PATCH] BREAKING: Drop Node v12 support; require v14.14+ (#969) Resolves #968 --- .github/workflows/ci.yml | 2 +- lib/fs/__tests__/multi-param.test.js | 6 +- lib/fs/__tests__/rm.test.js | 6 +- lib/fs/index.js | 30 ++- lib/remove/index.js | 9 +- lib/remove/rimraf.js | 302 --------------------------- package.json | 3 +- 7 files changed, 19 insertions(+), 339 deletions(-) delete mode 100644 lib/remove/rimraf.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3321617ab..19e224605 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: test: strategy: matrix: - node: [12.x, 13.x, 14.x, 15.x, 16.x, 17.x] + node: [14.x, 16.x, 18.x, 19.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/lib/fs/__tests__/multi-param.test.js b/lib/fs/__tests__/multi-param.test.js index 6b0cccb48..a22046bf5 100644 --- a/lib/fs/__tests__/multi-param.test.js +++ b/lib/fs/__tests__/multi-param.test.js @@ -4,14 +4,10 @@ const assert = require('assert') const path = require('path') const crypto = require('crypto') const os = require('os') -const atLeastNode = require('at-least-node') const fs = require('../..') const SIZE = 1000 -// Used for tests on Node 12.9.0+ only -const describeNode12 = atLeastNode('12.9.0') ? describe : describe.skip - describe('fs.read()', () => { let TEST_FILE let TEST_DATA @@ -153,7 +149,7 @@ describe('fs.write()', () => { }) }) -describeNode12('fs.writev()', () => { +describe('fs.writev()', () => { let TEST_FILE let TEST_DATA let TEST_FD diff --git a/lib/fs/__tests__/rm.test.js b/lib/fs/__tests__/rm.test.js index 150cb6d6e..ae2bb6b92 100644 --- a/lib/fs/__tests__/rm.test.js +++ b/lib/fs/__tests__/rm.test.js @@ -4,14 +4,10 @@ const fse = require('../..') const os = require('os') const path = require('path') const assert = require('assert') -const atLeastNode = require('at-least-node') /* eslint-env mocha */ -// Used for tests on Node 14.14.0+ only -const describeNode14 = atLeastNode('14.14.0') ? describe : describe.skip - -describeNode14('fs.rm', () => { +describe('fs.rm', () => { let TEST_FILE beforeEach(done => { diff --git a/lib/fs/index.js b/lib/fs/index.js index 7b025e294..0bce0e8c6 100644 --- a/lib/fs/index.js +++ b/lib/fs/index.js @@ -41,8 +41,7 @@ const api = [ 'writeFile' ].filter(key => { // Some commands are not available on some systems. Ex: - // fs.opendir was added in Node.js v12.12.0 - // fs.rm was added in Node.js v14.14.0 + // fs.cp was added in Node.js v16.7.0 // fs.lchown is not available on at least some Linux return typeof fs[key] === 'function' }) @@ -98,23 +97,20 @@ exports.write = function (fd, buffer, ...args) { }) } -// fs.writev only available in Node v12.9.0+ -if (typeof fs.writev === 'function') { - // Function signature is - // s.writev(fd, buffers[, position], callback) - // We need to handle the optional arg, so we use ...args - exports.writev = function (fd, buffers, ...args) { - if (typeof args[args.length - 1] === 'function') { - return fs.writev(fd, buffers, ...args) - } +// Function signature is +// s.writev(fd, buffers[, position], callback) +// We need to handle the optional arg, so we use ...args +exports.writev = function (fd, buffers, ...args) { + if (typeof args[args.length - 1] === 'function') { + return fs.writev(fd, buffers, ...args) + } - return new Promise((resolve, reject) => { - fs.writev(fd, buffers, ...args, (err, bytesWritten, buffers) => { - if (err) return reject(err) - resolve({ bytesWritten, buffers }) - }) + return new Promise((resolve, reject) => { + fs.writev(fd, buffers, ...args, (err, bytesWritten, buffers) => { + if (err) return reject(err) + resolve({ bytesWritten, buffers }) }) - } + }) } // fs.realpath.native sometimes not available if fs is monkey-patched diff --git a/lib/remove/index.js b/lib/remove/index.js index 4428e59ad..da746c7be 100644 --- a/lib/remove/index.js +++ b/lib/remove/index.js @@ -2,18 +2,13 @@ const fs = require('graceful-fs') const u = require('universalify').fromCallback -const rimraf = require('./rimraf') function remove (path, callback) { - // Node 14.14.0+ - if (fs.rm) return fs.rm(path, { recursive: true, force: true }, callback) - rimraf(path, callback) + fs.rm(path, { recursive: true, force: true }, callback) } function removeSync (path) { - // Node 14.14.0+ - if (fs.rmSync) return fs.rmSync(path, { recursive: true, force: true }) - rimraf.sync(path) + fs.rmSync(path, { recursive: true, force: true }) } module.exports = { diff --git a/lib/remove/rimraf.js b/lib/remove/rimraf.js deleted file mode 100644 index 2c7710265..000000000 --- a/lib/remove/rimraf.js +++ /dev/null @@ -1,302 +0,0 @@ -'use strict' - -const fs = require('graceful-fs') -const path = require('path') -const assert = require('assert') - -const isWindows = (process.platform === 'win32') - -function defaults (options) { - const methods = [ - 'unlink', - 'chmod', - 'stat', - 'lstat', - 'rmdir', - 'readdir' - ] - methods.forEach(m => { - options[m] = options[m] || fs[m] - m = m + 'Sync' - options[m] = options[m] || fs[m] - }) - - options.maxBusyTries = options.maxBusyTries || 3 -} - -function rimraf (p, options, cb) { - let busyTries = 0 - - if (typeof options === 'function') { - cb = options - options = {} - } - - assert(p, 'rimraf: missing path') - assert.strictEqual(typeof p, 'string', 'rimraf: path should be a string') - assert.strictEqual(typeof cb, 'function', 'rimraf: callback function required') - assert(options, 'rimraf: invalid options argument provided') - assert.strictEqual(typeof options, 'object', 'rimraf: options should be object') - - defaults(options) - - rimraf_(p, options, function CB (er) { - if (er) { - if ((er.code === 'EBUSY' || er.code === 'ENOTEMPTY' || er.code === 'EPERM') && - busyTries < options.maxBusyTries) { - busyTries++ - const time = busyTries * 100 - // try again, with the same exact callback as this one. - return setTimeout(() => rimraf_(p, options, CB), time) - } - - // already gone - if (er.code === 'ENOENT') er = null - } - - cb(er) - }) -} - -// Two possible strategies. -// 1. Assume it's a file. unlink it, then do the dir stuff on EPERM or EISDIR -// 2. Assume it's a directory. readdir, then do the file stuff on ENOTDIR -// -// Both result in an extra syscall when you guess wrong. However, there -// are likely far more normal files in the world than directories. This -// is based on the assumption that a the average number of files per -// directory is >= 1. -// -// If anyone ever complains about this, then I guess the strategy could -// be made configurable somehow. But until then, YAGNI. -function rimraf_ (p, options, cb) { - assert(p) - assert(options) - assert(typeof cb === 'function') - - // sunos lets the root user unlink directories, which is... weird. - // so we have to lstat here and make sure it's not a dir. - options.lstat(p, (er, st) => { - if (er && er.code === 'ENOENT') { - return cb(null) - } - - // Windows can EPERM on stat. Life is suffering. - if (er && er.code === 'EPERM' && isWindows) { - return fixWinEPERM(p, options, er, cb) - } - - if (st && st.isDirectory()) { - return rmdir(p, options, er, cb) - } - - options.unlink(p, er => { - if (er) { - if (er.code === 'ENOENT') { - return cb(null) - } - if (er.code === 'EPERM') { - return (isWindows) - ? fixWinEPERM(p, options, er, cb) - : rmdir(p, options, er, cb) - } - if (er.code === 'EISDIR') { - return rmdir(p, options, er, cb) - } - } - return cb(er) - }) - }) -} - -function fixWinEPERM (p, options, er, cb) { - assert(p) - assert(options) - assert(typeof cb === 'function') - - options.chmod(p, 0o666, er2 => { - if (er2) { - cb(er2.code === 'ENOENT' ? null : er) - } else { - options.stat(p, (er3, stats) => { - if (er3) { - cb(er3.code === 'ENOENT' ? null : er) - } else if (stats.isDirectory()) { - rmdir(p, options, er, cb) - } else { - options.unlink(p, cb) - } - }) - } - }) -} - -function fixWinEPERMSync (p, options, er) { - let stats - - assert(p) - assert(options) - - try { - options.chmodSync(p, 0o666) - } catch (er2) { - if (er2.code === 'ENOENT') { - return - } else { - throw er - } - } - - try { - stats = options.statSync(p) - } catch (er3) { - if (er3.code === 'ENOENT') { - return - } else { - throw er - } - } - - if (stats.isDirectory()) { - rmdirSync(p, options, er) - } else { - options.unlinkSync(p) - } -} - -function rmdir (p, options, originalEr, cb) { - assert(p) - assert(options) - assert(typeof cb === 'function') - - // try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS) - // if we guessed wrong, and it's not a directory, then - // raise the original error. - options.rmdir(p, er => { - if (er && (er.code === 'ENOTEMPTY' || er.code === 'EEXIST' || er.code === 'EPERM')) { - rmkids(p, options, cb) - } else if (er && er.code === 'ENOTDIR') { - cb(originalEr) - } else { - cb(er) - } - }) -} - -function rmkids (p, options, cb) { - assert(p) - assert(options) - assert(typeof cb === 'function') - - options.readdir(p, (er, files) => { - if (er) return cb(er) - - let n = files.length - let errState - - if (n === 0) return options.rmdir(p, cb) - - files.forEach(f => { - rimraf(path.join(p, f), options, er => { - if (errState) { - return - } - if (er) return cb(errState = er) - if (--n === 0) { - options.rmdir(p, cb) - } - }) - }) - }) -} - -// this looks simpler, and is strictly *faster*, but will -// tie up the JavaScript thread and fail on excessively -// deep directory trees. -function rimrafSync (p, options) { - let st - - options = options || {} - defaults(options) - - assert(p, 'rimraf: missing path') - assert.strictEqual(typeof p, 'string', 'rimraf: path should be a string') - assert(options, 'rimraf: missing options') - assert.strictEqual(typeof options, 'object', 'rimraf: options should be object') - - try { - st = options.lstatSync(p) - } catch (er) { - if (er.code === 'ENOENT') { - return - } - - // Windows can EPERM on stat. Life is suffering. - if (er.code === 'EPERM' && isWindows) { - fixWinEPERMSync(p, options, er) - } - } - - try { - // sunos lets the root user unlink directories, which is... weird. - if (st && st.isDirectory()) { - rmdirSync(p, options, null) - } else { - options.unlinkSync(p) - } - } catch (er) { - if (er.code === 'ENOENT') { - return - } else if (er.code === 'EPERM') { - return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er) - } else if (er.code !== 'EISDIR') { - throw er - } - rmdirSync(p, options, er) - } -} - -function rmdirSync (p, options, originalEr) { - assert(p) - assert(options) - - try { - options.rmdirSync(p) - } catch (er) { - if (er.code === 'ENOTDIR') { - throw originalEr - } else if (er.code === 'ENOTEMPTY' || er.code === 'EEXIST' || er.code === 'EPERM') { - rmkidsSync(p, options) - } else if (er.code !== 'ENOENT') { - throw er - } - } -} - -function rmkidsSync (p, options) { - assert(p) - assert(options) - options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options)) - - if (isWindows) { - // We only end up here once we got ENOTEMPTY at least once, and - // at this point, we are guaranteed to have removed all the kids. - // So, we know that it won't be ENOENT or ENOTDIR or anything else. - // try really hard to delete stuff on windows, because it has a - // PROFOUNDLY annoying habit of not closing handles promptly when - // files are deleted, resulting in spurious ENOTEMPTY errors. - const startTime = Date.now() - do { - try { - const ret = options.rmdirSync(p, options) - return ret - } catch {} - } while (Date.now() - startTime < 500) // give up after 500ms - } else { - const ret = options.rmdirSync(p, options) - return ret - } -} - -module.exports = rimraf -rimraf.sync = rimrafSync diff --git a/package.json b/package.json index 059000e6d..c05dc2bfe 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "10.1.0", "description": "fs-extra contains methods that aren't included in the vanilla Node.js fs package. Such as recursive mkdir, copy, and remove.", "engines": { - "node": ">=12" + "node": ">=14.14" }, "homepage": "https://github.com/jprichardson/node-fs-extra", "repository": { @@ -42,7 +42,6 @@ "universalify": "^2.0.0" }, "devDependencies": { - "at-least-node": "^1.0.0", "klaw": "^2.1.1", "klaw-sync": "^3.0.2", "minimist": "^1.1.1",