├── benchmark ├── .gitignore ├── index.js ├── old-rimraf │ ├── LICENSE │ ├── package.json │ ├── CHANGELOG.md │ ├── bin.js │ ├── README.md │ └── rimraf.js ├── create-fixture.js ├── rimrafs.js ├── print-results.js └── run-test.js ├── map.js ├── src ├── platform.ts ├── rimraf-manual.ts ├── ignore-enoent.ts ├── readdir-or-error.ts ├── rimraf-native.ts ├── use-native.ts ├── fix-eperm.ts ├── path-arg.ts ├── retry-busy.ts ├── default-tmp.ts ├── opt-arg.ts ├── fs.ts ├── index.ts ├── rimraf-posix.ts ├── rimraf-windows.ts ├── rimraf-move-remove.ts └── bin.mts ├── .gitignore ├── .prettierignore ├── typedoc.json ├── .tshy ├── build.json ├── esm.json └── commonjs.json ├── libtap-settings.js ├── .github └── workflows │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── package-json-repo.js │ ├── benchmark.yml │ ├── ci.yml │ ├── isaacs-makework.yml │ ├── static.yml │ └── typedoc.yml ├── test ├── platform.ts ├── ignore-enoent.ts ├── rimraf-native.ts ├── rimraf-manual.ts ├── readdir-or-error.ts ├── default-tmp.ts ├── use-native.ts ├── delete-many-files.ts ├── path-arg.ts ├── fs.ts ├── retry-busy.ts ├── opt-arg.ts ├── fix-eperm.ts ├── index.ts ├── bin.ts ├── rimraf-posix.ts └── rimraf-windows.ts ├── tsconfig.json ├── tap-snapshots └── test │ ├── retry-busy.ts.test.cjs │ ├── rimraf-native.ts.test.cjs │ ├── bin.ts.test.cjs │ ├── index.ts.test.cjs │ ├── rimraf-posix.ts.test.cjs │ ├── rimraf-windows.ts.test.cjs │ └── rimraf-move-remove.ts.test.cjs ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /fixtures 3 | -------------------------------------------------------------------------------- /map.js: -------------------------------------------------------------------------------- 1 | module.exports = test => test.replace(/^test/, 'lib') 2 | -------------------------------------------------------------------------------- /src/platform.ts: -------------------------------------------------------------------------------- 1 | export default process.env.__TESTING_RIMRAF_PLATFORM__ || process.platform 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.nyc_output 3 | /coverage 4 | /dist 5 | !/typedoc.json 6 | !/tsconfig-*.json 7 | !/.prettierignore 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /example 3 | /.github 4 | /dist 5 | .env 6 | /tap-snapshots 7 | /.nyc_output 8 | /coverage 9 | /benchmark 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationLinks": { 3 | "GitHub": "https://github.com/isaacs/rimraf", 4 | "isaacs projects": "https://isaacs.github.io/" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.tshy/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "../src", 5 | "target": "es2022", 6 | "module": "nodenext", 7 | "moduleResolution": "nodenext" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.tshy/esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build.json", 3 | "include": [ 4 | "../src/**/*.ts", 5 | "../src/**/*.mts", 6 | "../src/**/*.tsx" 7 | ], 8 | "exclude": [], 9 | "compilerOptions": { 10 | "outDir": "../.tshy-build-tmp/esm" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.tshy/commonjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build.json", 3 | "include": [ 4 | "../src/**/*.ts", 5 | "../src/**/*.cts", 6 | "../src/**/*.tsx" 7 | ], 8 | "exclude": [ 9 | "../src/**/*.mts" 10 | ], 11 | "compilerOptions": { 12 | "outDir": "../.tshy-build-tmp/commonjs" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /libtap-settings.js: -------------------------------------------------------------------------------- 1 | // use this module for tap's recursive directory removal, so that 2 | // the windows tests don't fail with EBUSY. 3 | const { rimraf } = require('./') 4 | module.exports = { 5 | rmdirRecursiveSync: path => rimraf.sync(path), 6 | rmdirRecursive(path, cb) { 7 | rimraf(path, {}).then(() => cb(), cb) 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/commit-if-modified.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git config --global user.email "$1" 3 | shift 4 | git config --global user.name "$1" 5 | shift 6 | message="$1" 7 | shift 8 | if [ $(git status --porcelain "$@" | egrep '^ M' | wc -l) -gt 0 ]; then 9 | git add "$@" 10 | git commit -m "$message" 11 | git push || git pull --rebase 12 | git push 13 | fi 14 | -------------------------------------------------------------------------------- /src/rimraf-manual.ts: -------------------------------------------------------------------------------- 1 | import platform from './platform.js' 2 | 3 | import { rimrafPosix, rimrafPosixSync } from './rimraf-posix.js' 4 | import { rimrafWindows, rimrafWindowsSync } from './rimraf-windows.js' 5 | 6 | export const rimrafManual = platform === 'win32' ? rimrafWindows : rimrafPosix 7 | export const rimrafManualSync = 8 | platform === 'win32' ? rimrafWindowsSync : rimrafPosixSync 9 | -------------------------------------------------------------------------------- /src/ignore-enoent.ts: -------------------------------------------------------------------------------- 1 | export const ignoreENOENT = async (p: Promise) => 2 | p.catch(er => { 3 | if (er.code !== 'ENOENT') { 4 | throw er 5 | } 6 | }) 7 | 8 | export const ignoreENOENTSync = (fn: () => any) => { 9 | try { 10 | return fn() 11 | } catch (er) { 12 | if ((er as NodeJS.ErrnoException)?.code !== 'ENOENT') { 13 | throw er 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/platform.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import actual from '../dist/esm/platform.js' 3 | t.test('actual platform', t => { 4 | t.equal(actual, process.platform) 5 | t.end() 6 | }) 7 | t.test('fake platform', async t => { 8 | process.env.__TESTING_RIMRAF_PLATFORM__ = 'not actual platform' 9 | t.equal( 10 | (await t.mockImport('../dist/esm/platform.js')).default, 11 | 'not actual platform' 12 | ) 13 | t.end() 14 | }) 15 | -------------------------------------------------------------------------------- /src/readdir-or-error.ts: -------------------------------------------------------------------------------- 1 | // returns an array of entries if readdir() works, 2 | // or the error that readdir() raised if not. 3 | import { promises, readdirSync } from './fs.js' 4 | const { readdir } = promises 5 | export const readdirOrError = (path: string) => 6 | readdir(path).catch(er => er as NodeJS.ErrnoException) 7 | export const readdirOrErrorSync = (path: string) => { 8 | try { 9 | return readdirSync(path) 10 | } catch (er) { 11 | return er as NodeJS.ErrnoException 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "inlineSources": true, 8 | "jsx": "react", 9 | "module": "nodenext", 10 | "moduleResolution": "nodenext", 11 | "noUncheckedIndexedAccess": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "target": "es2022" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tap-snapshots/test/retry-busy.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/retry-busy.ts > TAP > default settings 1`] = ` 9 | Object { 10 | "codes": Set { 11 | "EMFILE", 12 | "ENFILE", 13 | "EBUSY", 14 | }, 15 | "MAXBACKOFF": 200, 16 | "MAXRETRIES": 10, 17 | "RATE": 1.2, 18 | } 19 | ` 20 | -------------------------------------------------------------------------------- /.github/workflows/copyright-year.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=${1:-$PWD} 3 | dates=($(git log --date=format:%Y --pretty=format:'%ad' --reverse | sort | uniq)) 4 | if [ "${#dates[@]}" -eq 1 ]; then 5 | datestr="${dates}" 6 | else 7 | datestr="${dates}-${dates[${#dates[@]}-1]}" 8 | fi 9 | 10 | stripDate='s/^((.*)Copyright\b(.*?))((?:,\s*)?(([0-9]{4}\s*-\s*[0-9]{4})|(([0-9]{4},\s*)*[0-9]{4})))(?:,)?\s*(.*)\n$/$1$9\n/g' 11 | addDate='s/^.*Copyright(?:\s*\(c\))? /Copyright \(c\) '$datestr' /g' 12 | for l in $dir/LICENSE*; do 13 | perl -pi -e "$stripDate" $l 14 | perl -pi -e "$addDate" $l 15 | done 16 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | const cases = require('./rimrafs.js') 2 | const runTest = require('./run-test.js') 3 | const print = require('./print-results.js') 4 | 5 | const rimraf = require('../') 6 | const main = async () => { 7 | // cleanup first. since the windows impl works on all platforms, 8 | // use that. it's only relevant if the folder exists anyway. 9 | rimraf.sync(__dirname + '/fixtures') 10 | const results = {} 11 | for (const name of Object.keys(cases)) { 12 | results[name] = await runTest(name) 13 | } 14 | rimraf.sync(__dirname + '/fixtures') 15 | return results 16 | } 17 | 18 | main().then(print) 19 | -------------------------------------------------------------------------------- /src/rimraf-native.ts: -------------------------------------------------------------------------------- 1 | import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js' 2 | import { promises, rmSync } from './fs.js' 3 | const { rm } = promises 4 | 5 | export const rimrafNative = async ( 6 | path: string, 7 | opt: RimrafAsyncOptions 8 | ): Promise => { 9 | await rm(path, { 10 | ...opt, 11 | force: true, 12 | recursive: true, 13 | }) 14 | return true 15 | } 16 | 17 | export const rimrafNativeSync = ( 18 | path: string, 19 | opt: RimrafSyncOptions 20 | ): boolean => { 21 | rmSync(path, { 22 | ...opt, 23 | force: true, 24 | recursive: true, 25 | }) 26 | return true 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/package-json-repo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pf = require.resolve(`${process.cwd()}/package.json`) 4 | const pj = require(pf) 5 | 6 | if (!pj.repository && process.env.GITHUB_REPOSITORY) { 7 | const fs = require('fs') 8 | const server = process.env.GITHUB_SERVER_URL || 'https://github.com' 9 | const repo = `${server}/${process.env.GITHUB_REPOSITORY}` 10 | pj.repository = repo 11 | const json = fs.readFileSync(pf, 'utf8') 12 | const match = json.match(/^\s*\{[\r\n]+([ \t]*)"/) 13 | const indent = match[1] 14 | const output = JSON.stringify(pj, null, indent || 2) + '\n' 15 | fs.writeFileSync(pf, output) 16 | } 17 | -------------------------------------------------------------------------------- /test/ignore-enoent.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { ignoreENOENT, ignoreENOENTSync } from '../dist/esm/ignore-enoent.js' 3 | 4 | const enoent = Object.assign(new Error('no ent'), { code: 'ENOENT' }) 5 | const eperm = Object.assign(new Error('eperm'), { code: 'EPERM' }) 6 | 7 | const throwEnoent = () => { 8 | throw enoent 9 | } 10 | const throwEperm = () => { 11 | throw eperm 12 | } 13 | 14 | t.resolves(ignoreENOENT(Promise.reject(enoent)), 'enoent is fine') 15 | t.rejects( 16 | ignoreENOENT(Promise.reject(eperm)), 17 | { code: 'EPERM' }, 18 | 'eperm is not' 19 | ) 20 | t.doesNotThrow(() => ignoreENOENTSync(throwEnoent), 'enoent is fine sync') 21 | t.throws( 22 | () => ignoreENOENTSync(throwEperm), 23 | { code: 'EPERM' }, 24 | 'eperm is not fine sync' 25 | ) 26 | -------------------------------------------------------------------------------- /tap-snapshots/test/rimraf-native.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/rimraf-native.ts > TAP > calls the right node function > must match snapshot 1`] = ` 9 | Array [ 10 | Array [ 11 | "rm", 12 | "path", 13 | Object { 14 | "force": true, 15 | "recursive": true, 16 | "x": "y", 17 | }, 18 | ], 19 | Array [ 20 | "rmSync", 21 | "path", 22 | Object { 23 | "a": "b", 24 | "force": true, 25 | "recursive": true, 26 | }, 27 | ], 28 | ] 29 | ` 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /benchmark/old-rimraf/LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/rimraf-native.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { RimrafAsyncOptions, RimrafSyncOptions } from '../src/index.js' 3 | 4 | const CALLS: any[] = [] 5 | const fs = { 6 | rmSync: (path: string, options: any) => { 7 | CALLS.push(['rmSync', path, options]) 8 | }, 9 | promises: { 10 | rm: async (path: string, options: any) => { 11 | CALLS.push(['rm', path, options]) 12 | }, 13 | }, 14 | } 15 | 16 | const { rimrafNative, rimrafNativeSync } = (await t.mockImport( 17 | '../dist/esm/rimraf-native.js', 18 | { 19 | '../dist/esm/fs.js': fs, 20 | } 21 | )) as typeof import('../dist/esm/rimraf-native.js') 22 | 23 | t.test('calls the right node function', async t => { 24 | await rimrafNative('path', { x: 'y' } as unknown as RimrafAsyncOptions) 25 | rimrafNativeSync('path', { a: 'b' } as unknown as RimrafSyncOptions) 26 | t.matchSnapshot(CALLS) 27 | }) 28 | -------------------------------------------------------------------------------- /benchmark/old-rimraf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rimraf", 3 | "version": "3.0.2", 4 | "main": "rimraf.js", 5 | "description": "A deep deletion module for node (like `rm -rf`)", 6 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 7 | "license": "ISC", 8 | "repository": "git://github.com/isaacs/rimraf.git", 9 | "scripts": { 10 | "preversion": "npm test", 11 | "postversion": "npm publish", 12 | "postpublish": "git push origin --follow-tags", 13 | "test": "tap test/*.js" 14 | }, 15 | "bin": "./bin.js", 16 | "dependencies": { 17 | "glob": "^7.1.3" 18 | }, 19 | "files": [ 20 | "LICENSE", 21 | "README.md", 22 | "bin.js", 23 | "rimraf.js" 24 | ], 25 | "devDependencies": { 26 | "mkdirp": "^0.5.1", 27 | "tap": "^12.1.1" 28 | }, 29 | "funding": { 30 | "url": "https://github.com/sponsors/isaacs" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/use-native.ts: -------------------------------------------------------------------------------- 1 | import { RimrafAsyncOptions, RimrafOptions } from './index.js' 2 | import platform from './platform.js' 3 | 4 | const version = process.env.__TESTING_RIMRAF_NODE_VERSION__ || process.version 5 | const versArr = version.replace(/^v/, '').split('.') 6 | 7 | /* c8 ignore start */ 8 | const [major = 0, minor = 0] = versArr.map(v => parseInt(v, 10)) 9 | /* c8 ignore stop */ 10 | const hasNative = major > 14 || (major === 14 && minor >= 14) 11 | 12 | // we do NOT use native by default on Windows, because Node's native 13 | // rm implementation is less advanced. Change this code if that changes. 14 | export const useNative: (opt?: RimrafAsyncOptions) => boolean = 15 | !hasNative || platform === 'win32' 16 | ? () => false 17 | : opt => !opt?.signal && !opt?.filter 18 | export const useNativeSync: (opt?: RimrafOptions) => boolean = 19 | !hasNative || platform === 'win32' 20 | ? () => false 21 | : opt => !opt?.signal && !opt?.filter 22 | -------------------------------------------------------------------------------- /benchmark/create-fixture.js: -------------------------------------------------------------------------------- 1 | const { writeFile: writeFile_ } = require('fs') 2 | const writeFile = async (path, data) => new Promise((res, rej) => 3 | writeFile_(path, data, er => er ? rej(er) : res())) 4 | const { mkdirp } = require('mkdirp') 5 | const { resolve } = require('path') 6 | 7 | const create = async (path, start, end, maxDepth, depth = 0) => { 8 | await mkdirp(path) 9 | const promises = [] 10 | for (let i = start; i <= end; i++) { 11 | const c = String.fromCharCode(i) 12 | if (depth < maxDepth && (i-start >= depth)) 13 | await create(resolve(path, c), start, end, maxDepth, depth + 1) 14 | else 15 | promises.push(writeFile(resolve(path, c), c)) 16 | } 17 | await Promise.all(promises) 18 | return path 19 | } 20 | 21 | module.exports = async ({ start, end, depth, name }) => { 22 | const path = resolve(__dirname, 'fixtures', name, 'test') 23 | return await create(path, start.charCodeAt(0), end.charCodeAt(0), depth) 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: 'benchmarks' 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | benchmark: 7 | strategy: 8 | matrix: 9 | node-version: [16.x, 19.x] 10 | platform: 11 | - os: ubuntu-latest 12 | - os: macos-latest 13 | - os: windows-latest 14 | 15 | runs-on: ${{ matrix.platform.os }} 16 | 17 | defaults: 18 | run: 19 | shell: bash 20 | 21 | steps: 22 | - name: Checkout Repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Use Nodejs ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install dependencies 31 | run: npm install 32 | 33 | - name: benchmark 34 | run: node benchmark/index.js 35 | env: 36 | RIMRAF_TEST_START_CHAR: a 37 | RIMRAF_TEST_END_CHAR: f 38 | RIMRAF_TEST_DEPTH: 5 39 | -------------------------------------------------------------------------------- /test/rimraf-manual.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { fileURLToPath } from 'url' 3 | import { rimrafManual, rimrafManualSync } from '../dist/esm/rimraf-manual.js' 4 | import { rimrafPosix, rimrafPosixSync } from '../dist/esm/rimraf-posix.js' 5 | import { rimrafWindows, rimrafWindowsSync } from '../dist/esm/rimraf-windows.js' 6 | 7 | if (!process.env.__TESTING_RIMRAF_PLATFORM__) { 8 | const otherPlatform = process.platform !== 'win32' ? 'win32' : 'posix' 9 | t.spawn( 10 | process.execPath, 11 | [...process.execArgv, fileURLToPath(import.meta.url)], 12 | { 13 | name: otherPlatform, 14 | env: { 15 | ...process.env, 16 | __TESTING_RIMRAF_PLATFORM__: otherPlatform, 17 | }, 18 | } 19 | ) 20 | } 21 | 22 | const platform = process.env.__TESTING_RIMRAF_PLATFORM__ || process.platform 23 | 24 | const [expectManual, expectManualSync] = 25 | platform === 'win32' 26 | ? [rimrafWindows, rimrafWindowsSync] 27 | : [rimrafPosix, rimrafPosixSync] 28 | t.equal(rimrafManual, expectManual, 'got expected implementation') 29 | t.equal(rimrafManualSync, expectManualSync, 'got expected implementation') 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [16.x, 18.x, 20.x, 21.x] 10 | platform: 11 | - os: ubuntu-latest 12 | shell: bash 13 | - os: macos-latest 14 | shell: bash 15 | - os: windows-latest 16 | shell: bash 17 | - os: windows-latest 18 | shell: powershell 19 | fail-fast: false 20 | 21 | runs-on: ${{ matrix.platform.os }} 22 | defaults: 23 | run: 24 | shell: ${{ matrix.platform.shell }} 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Use Nodejs ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Run Tests 39 | run: npm test -- -t0 -c 40 | env: 41 | RIMRAF_TEST_START_CHAR: a 42 | RIMRAF_TEST_END_CHAR: f 43 | RIMRAF_TEST_DEPTH: 5 44 | -------------------------------------------------------------------------------- /tap-snapshots/test/bin.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/bin.ts > TAP > interactive deletes > -v > a > had any leftover 1`] = ` 9 | false 10 | ` 11 | 12 | exports[`test/bin.ts > TAP > interactive deletes > -V > a > had any leftover 1`] = ` 13 | false 14 | ` 15 | 16 | exports[`test/bin.ts > TAP > interactive deletes > -v > hehaha, yes i think so, , A > had any leftover 1`] = ` 17 | false 18 | ` 19 | 20 | exports[`test/bin.ts > TAP > interactive deletes > -V > hehaha, yes i think so, , A > had any leftover 1`] = ` 21 | false 22 | ` 23 | 24 | exports[`test/bin.ts > TAP > interactive deletes > -v > no, n, N, N, Q > had any leftover 1`] = ` 25 | true 26 | ` 27 | 28 | exports[`test/bin.ts > TAP > interactive deletes > -V > no, n, N, N, Q > had any leftover 1`] = ` 29 | true 30 | ` 31 | 32 | exports[`test/bin.ts > TAP > interactive deletes > -v > y, YOLO, no, quit > had any leftover 1`] = ` 33 | true 34 | ` 35 | 36 | exports[`test/bin.ts > TAP > interactive deletes > -V > y, YOLO, no, quit > had any leftover 1`] = ` 37 | true 38 | ` 39 | -------------------------------------------------------------------------------- /test/readdir-or-error.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { 3 | readdirOrError, 4 | readdirOrErrorSync, 5 | } from '../dist/esm/readdir-or-error.js' 6 | 7 | const path = t.testdir({ 8 | file: 'file', 9 | empty: {}, 10 | full: { 11 | x: 'x', 12 | y: 'y', 13 | z: 'z', 14 | }, 15 | }) 16 | 17 | // [path, expected] 18 | const cases: [string, string[] | { code: string }][] = [ 19 | ['file', { code: 'ENOTDIR' }], 20 | ['empty', []], 21 | ['full', ['x', 'y', 'z']], 22 | ] 23 | 24 | for (const [c, expect] of cases) { 25 | t.test(c, async t => { 26 | const p = `${path}/${c}` 27 | const resAsync = await readdirOrError(p) 28 | const resSync = readdirOrErrorSync(p) 29 | if (Array.isArray(expect)) { 30 | if (!Array.isArray(resAsync)) { 31 | throw new Error('expected array async result') 32 | } 33 | if (!Array.isArray(resSync)) { 34 | throw new Error('expected array sync result') 35 | } 36 | t.same( 37 | resAsync.map(e => e.name).sort(), 38 | expect.sort(), 39 | 'got async result' 40 | ) 41 | t.same(resSync.map(e => e.name).sort(), expect.sort(), 'got sync result') 42 | } else { 43 | t.match(resAsync, expect, 'got async result') 44 | t.match(resSync, expect, 'got sync result') 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /benchmark/rimrafs.js: -------------------------------------------------------------------------------- 1 | // just disable the glob option, and promisify it, for apples-to-apples comp 2 | const oldRimraf = () => { 3 | const {promisify} = require('util') 4 | const oldRimraf = require('./old-rimraf') 5 | const pOldRimraf = promisify(oldRimraf) 6 | const rimraf = path => pOldRimraf(path, { disableGlob: true }) 7 | const sync = path => oldRimraf.sync(path, { disableGlob: true }) 8 | return Object.assign(rimraf, { sync }) 9 | } 10 | 11 | const { spawn, spawnSync } = require('child_process') 12 | const systemRmRf = () => { 13 | const rimraf = path => new Promise((res, rej) => { 14 | const proc = spawn('rm', ['-rf', path]) 15 | proc.on('close', (code, signal) => { 16 | if (code || signal) 17 | rej(Object.assign(new Error('command failed'), { code, signal })) 18 | else 19 | res() 20 | }) 21 | }) 22 | rimraf.sync = path => { 23 | const result = spawnSync('rm', ['-rf', path]) 24 | if (result.status || result.signal) { 25 | throw Object.assign(new Error('command failed'), { 26 | code: result.status, 27 | signal: result.signal, 28 | }) 29 | } 30 | } 31 | return rimraf 32 | } 33 | 34 | module.exports = { 35 | native: require('../').native, 36 | posix: require('../').posix, 37 | windows: require('../').windows, 38 | old: oldRimraf(), 39 | system: systemRmRf(), 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/isaacs-makework.yml: -------------------------------------------------------------------------------- 1 | name: "various tidying up tasks to silence nagging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write # for commit-if-modified.sh to push 11 | 12 | jobs: 13 | makework: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Use Node.js 20 | uses: actions/setup-node@v2.1.4 21 | with: 22 | node-version: 16.x 23 | - name: put repo in package.json 24 | run: node .github/workflows/package-json-repo.js 25 | - name: check in package.json if modified 26 | run: | 27 | bash -x .github/workflows/commit-if-modified.sh \ 28 | "package-json-repo-bot@example.com" \ 29 | "package.json Repo Bot" \ 30 | "chore: add repo to package.json" \ 31 | package.json package-lock.json 32 | - name: put all dates in license copyright line 33 | run: bash .github/workflows/copyright-year.sh 34 | - name: check in licenses if modified 35 | run: | 36 | bash .github/workflows/commit-if-modified.sh \ 37 | "license-year-bot@example.com" \ 38 | "License Year Bot" \ 39 | "chore: add copyright year to license" \ 40 | LICENSE* 41 | -------------------------------------------------------------------------------- /src/fix-eperm.ts: -------------------------------------------------------------------------------- 1 | import { chmodSync, promises } from './fs.js' 2 | const { chmod } = promises 3 | 4 | export const fixEPERM = 5 | (fn: (path: string) => Promise) => async (path: string) => { 6 | try { 7 | return await fn(path) 8 | } catch (er) { 9 | const fer = er as NodeJS.ErrnoException 10 | if (fer?.code === 'ENOENT') { 11 | return 12 | } 13 | if (fer?.code === 'EPERM') { 14 | try { 15 | await chmod(path, 0o666) 16 | } catch (er2) { 17 | const fer2 = er2 as NodeJS.ErrnoException 18 | if (fer2?.code === 'ENOENT') { 19 | return 20 | } 21 | throw er 22 | } 23 | return await fn(path) 24 | } 25 | throw er 26 | } 27 | } 28 | 29 | export const fixEPERMSync = (fn: (path: string) => any) => (path: string) => { 30 | try { 31 | return fn(path) 32 | } catch (er) { 33 | const fer = er as NodeJS.ErrnoException 34 | if (fer?.code === 'ENOENT') { 35 | return 36 | } 37 | if (fer?.code === 'EPERM') { 38 | try { 39 | chmodSync(path, 0o666) 40 | } catch (er2) { 41 | const fer2 = er2 as NodeJS.ErrnoException 42 | if (fer2?.code === 'ENOENT') { 43 | return 44 | } 45 | throw er 46 | } 47 | return fn(path) 48 | } 49 | throw er 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Use Nodejs ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18.x 37 | - name: Install dependencies 38 | run: npm install 39 | - name: Generate typedocs 40 | run: npm run typedoc 41 | - name: Setup Pages 42 | uses: actions/configure-pages@v3 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v1 45 | with: 46 | path: './docs' 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v1 50 | -------------------------------------------------------------------------------- /.github/workflows/typedoc.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Use Nodejs ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18.x 37 | - name: Install dependencies 38 | run: npm install 39 | - name: Generate typedocs 40 | run: npm run typedoc 41 | 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v3 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v1 46 | with: 47 | path: './docs' 48 | - name: Deploy to GitHub Pages 49 | id: deployment 50 | uses: actions/deploy-pages@v1 51 | -------------------------------------------------------------------------------- /benchmark/old-rimraf/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v3.0 2 | 3 | - Add `--preserve-root` option to executable (default true) 4 | - Drop support for Node.js below version 6 5 | 6 | # v2.7 7 | 8 | - Make `glob` an optional dependency 9 | 10 | # 2.6 11 | 12 | - Retry on EBUSY on non-windows platforms as well 13 | - Make `rimraf.sync` 10000% more reliable on Windows 14 | 15 | # 2.5 16 | 17 | - Handle Windows EPERM when lstat-ing read-only dirs 18 | - Add glob option to pass options to glob 19 | 20 | # 2.4 21 | 22 | - Add EPERM to delay/retry loop 23 | - Add `disableGlob` option 24 | 25 | # 2.3 26 | 27 | - Make maxBusyTries and emfileWait configurable 28 | - Handle weird SunOS unlink-dir issue 29 | - Glob the CLI arg for better Windows support 30 | 31 | # 2.2 32 | 33 | - Handle ENOENT properly on Windows 34 | - Allow overriding fs methods 35 | - Treat EPERM as indicative of non-empty dir 36 | - Remove optional graceful-fs dep 37 | - Consistently return null error instead of undefined on success 38 | - win32: Treat ENOTEMPTY the same as EBUSY 39 | - Add `rimraf` binary 40 | 41 | # 2.1 42 | 43 | - Fix SunOS error code for a non-empty directory 44 | - Try rmdir before readdir 45 | - Treat EISDIR like EPERM 46 | - Remove chmod 47 | - Remove lstat polyfill, node 0.7 is not supported 48 | 49 | # 2.0 50 | 51 | - Fix myGid call to check process.getgid 52 | - Simplify the EBUSY backoff logic. 53 | - Use fs.lstat in node >= 0.7.9 54 | - Remove gently option 55 | - remove fiber implementation 56 | - Delete files that are marked read-only 57 | 58 | # 1.0 59 | 60 | - Allow ENOENT in sync method 61 | - Throw when no callback is provided 62 | - Make opts.gently an absolute path 63 | - use 'stat' if 'lstat' is not available 64 | - Consistent error naming, and rethrow non-ENOENT stat errors 65 | - add fiber implementation 66 | -------------------------------------------------------------------------------- /src/path-arg.ts: -------------------------------------------------------------------------------- 1 | import { parse, resolve } from 'path' 2 | import { inspect } from 'util' 3 | import { RimrafAsyncOptions } from './index.js' 4 | import platform from './platform.js' 5 | 6 | const pathArg = (path: string, opt: RimrafAsyncOptions = {}) => { 7 | const type = typeof path 8 | if (type !== 'string') { 9 | const ctor = path && type === 'object' && path.constructor 10 | const received = 11 | ctor && ctor.name 12 | ? `an instance of ${ctor.name}` 13 | : type === 'object' 14 | ? inspect(path) 15 | : `type ${type} ${path}` 16 | const msg = 17 | 'The "path" argument must be of type string. ' + `Received ${received}` 18 | throw Object.assign(new TypeError(msg), { 19 | path, 20 | code: 'ERR_INVALID_ARG_TYPE', 21 | }) 22 | } 23 | 24 | if (/\0/.test(path)) { 25 | // simulate same failure that node raises 26 | const msg = 'path must be a string without null bytes' 27 | throw Object.assign(new TypeError(msg), { 28 | path, 29 | code: 'ERR_INVALID_ARG_VALUE', 30 | }) 31 | } 32 | 33 | path = resolve(path) 34 | const { root } = parse(path) 35 | 36 | if (path === root && opt.preserveRoot !== false) { 37 | const msg = 'refusing to remove root directory without preserveRoot:false' 38 | throw Object.assign(new Error(msg), { 39 | path, 40 | code: 'ERR_PRESERVE_ROOT', 41 | }) 42 | } 43 | 44 | if (platform === 'win32') { 45 | const badWinChars = /[*|"<>?:]/ 46 | const { root } = parse(path) 47 | if (badWinChars.test(path.substring(root.length))) { 48 | throw Object.assign(new Error('Illegal characters in path.'), { 49 | path, 50 | code: 'EINVAL', 51 | }) 52 | } 53 | } 54 | 55 | return path 56 | } 57 | 58 | export default pathArg 59 | -------------------------------------------------------------------------------- /benchmark/print-results.js: -------------------------------------------------------------------------------- 1 | const sum = list => list.reduce((a, b) => a + b) 2 | const mean = list => sum(list) / list.length 3 | const median = list => list.sort()[Math.floor(list.length / 2)] 4 | const max = list => list.sort()[list.length - 1] 5 | const min = list => list.sort()[0] 6 | const sqrt = n => Math.pow(n, 0.5) 7 | const variance = list => { 8 | const m = mean(list) 9 | return mean(list.map(n => Math.pow(n - m, 2))) 10 | } 11 | const stddev = list => { 12 | const v = variance(list) 13 | if (isNaN(v)) { 14 | console.error({list, v}) 15 | throw new Error('wat?') 16 | } 17 | return sqrt(variance(list)) 18 | } 19 | 20 | const round = n => Math.round(n * 1e3) / 1e3 21 | 22 | const nums = list => ({ 23 | mean: round(mean(list)), 24 | median: round(median(list)), 25 | stddev: round(stddev(list)), 26 | max: round(max(list)), 27 | min: round(min(list)), 28 | }) 29 | 30 | const printEr = er => `${er.code ? er.code + ': ' : ''}${er.message}` 31 | const failures = list => list.length === 0 ? {} 32 | : { failures: list.map(er => printEr(er)).join('\n') } 33 | 34 | const table = results => { 35 | const table = {} 36 | for (const [type, data] of Object.entries(results)) { 37 | table[`${type} sync`] = { 38 | ...nums(data.syncTimes), 39 | ...failures(data.syncFails), 40 | } 41 | table[`${type} async`] = { 42 | ...nums(data.asyncTimes), 43 | ...failures(data.asyncFails), 44 | } 45 | table[`${type} parallel`] = { 46 | ...nums(data.paraTimes), 47 | ...failures(data.paraFails), 48 | } 49 | } 50 | // sort by mean time 51 | return Object.entries(table) 52 | .sort(([, {mean:a}], [, {mean:b}]) => a - b) 53 | .reduce((set, [key, val]) => { 54 | set[key] = val 55 | return set 56 | }, {}) 57 | } 58 | 59 | const print = results => { 60 | console.log(JSON.stringify(results, 0, 2)) 61 | console.log('Results sorted by fastest mean value') 62 | console.table(table(results)) 63 | } 64 | 65 | module.exports = print 66 | -------------------------------------------------------------------------------- /test/default-tmp.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | 3 | import { tmpdir } from 'os' 4 | import { win32 } from 'path' 5 | 6 | t.test('posix platform', async t => { 7 | const { defaultTmp, defaultTmpSync } = (await t.mockImport( 8 | '../dist/esm/default-tmp.js', 9 | { 10 | '../dist/esm/platform.js': 'posix', 11 | } 12 | )) as typeof import('../dist/esm/default-tmp.js') 13 | t.equal(defaultTmpSync('anything'), tmpdir()) 14 | t.equal(await defaultTmp('anything').then(t => t), tmpdir()) 15 | }) 16 | 17 | t.test('windows', async t => { 18 | const tempDirCheck = (path: string) => { 19 | switch (path.toLowerCase()) { 20 | case 'd:\\temp': 21 | return { isDirectory: () => true } 22 | case 'e:\\temp': 23 | return { isDirectory: () => false } 24 | default: 25 | throw Object.assign(new Error('no ents here'), { code: 'ENOENT' }) 26 | } 27 | } 28 | const { defaultTmp, defaultTmpSync } = (await t.mockImport( 29 | '../dist/esm/default-tmp.js', 30 | { 31 | os: { 32 | tmpdir: () => 'C:\\Windows\\Temp', 33 | }, 34 | path: win32, 35 | '../dist/esm/platform.js': 'win32', 36 | '../dist/esm/fs.js': { 37 | statSync: tempDirCheck, 38 | promises: { 39 | stat: async (path: string) => tempDirCheck(path), 40 | }, 41 | }, 42 | } 43 | )) as typeof import('../dist/esm/default-tmp.js') 44 | 45 | const expect = { 46 | 'c:\\some\\path': 'C:\\Windows\\Temp', 47 | 'C:\\some\\path': 'C:\\Windows\\Temp', 48 | 'd:\\some\\path': 'd:\\temp', 49 | 'D:\\some\\path': 'D:\\temp', 50 | 'e:\\some\\path': 'e:\\', 51 | 'E:\\some\\path': 'E:\\', 52 | 'f:\\some\\path': 'f:\\', 53 | 'F:\\some\\path': 'F:\\', 54 | } 55 | 56 | for (const [path, tmp] of Object.entries(expect)) { 57 | t.test(`${path} -> ${tmp}`, async t => { 58 | t.equal(defaultTmpSync(path), tmp, 'sync') 59 | t.equal(await defaultTmp(path), tmp, 'async') 60 | }) 61 | } 62 | 63 | t.end() 64 | }) 65 | -------------------------------------------------------------------------------- /src/retry-busy.ts: -------------------------------------------------------------------------------- 1 | // note: max backoff is the maximum that any *single* backoff will do 2 | 3 | import { RimrafAsyncOptions, RimrafOptions } from './index.js' 4 | 5 | export const MAXBACKOFF = 200 6 | export const RATE = 1.2 7 | export const MAXRETRIES = 10 8 | export const codes = new Set(['EMFILE', 'ENFILE', 'EBUSY']) 9 | 10 | export const retryBusy = (fn: (path: string) => Promise) => { 11 | const method = async ( 12 | path: string, 13 | opt: RimrafAsyncOptions, 14 | backoff = 1, 15 | total = 0 16 | ) => { 17 | const mbo = opt.maxBackoff || MAXBACKOFF 18 | const rate = opt.backoff || RATE 19 | const max = opt.maxRetries || MAXRETRIES 20 | let retries = 0 21 | while (true) { 22 | try { 23 | return await fn(path) 24 | } catch (er) { 25 | const fer = er as NodeJS.ErrnoException 26 | if (fer?.path === path && fer?.code && codes.has(fer.code)) { 27 | backoff = Math.ceil(backoff * rate) 28 | total = backoff + total 29 | if (total < mbo) { 30 | return new Promise((res, rej) => { 31 | setTimeout(() => { 32 | method(path, opt, backoff, total).then(res, rej) 33 | }, backoff) 34 | }) 35 | } 36 | if (retries < max) { 37 | retries++ 38 | continue 39 | } 40 | } 41 | throw er 42 | } 43 | } 44 | } 45 | 46 | return method 47 | } 48 | 49 | // just retries, no async so no backoff 50 | export const retryBusySync = (fn: (path: string) => any) => { 51 | const method = (path: string, opt: RimrafOptions) => { 52 | const max = opt.maxRetries || MAXRETRIES 53 | let retries = 0 54 | while (true) { 55 | try { 56 | return fn(path) 57 | } catch (er) { 58 | const fer = er as NodeJS.ErrnoException 59 | if ( 60 | fer?.path === path && 61 | fer?.code && 62 | codes.has(fer.code) && 63 | retries < max 64 | ) { 65 | retries++ 66 | continue 67 | } 68 | throw er 69 | } 70 | } 71 | } 72 | return method 73 | } 74 | -------------------------------------------------------------------------------- /benchmark/old-rimraf/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const rimraf = require('./') 4 | 5 | const path = require('path') 6 | 7 | const isRoot = arg => /^(\/|[a-zA-Z]:\\)$/.test(path.resolve(arg)) 8 | const filterOutRoot = arg => { 9 | const ok = preserveRoot === false || !isRoot(arg) 10 | if (!ok) { 11 | console.error(`refusing to remove ${arg}`) 12 | console.error('Set --no-preserve-root to allow this') 13 | } 14 | return ok 15 | } 16 | 17 | let help = false 18 | let dashdash = false 19 | let noglob = false 20 | let preserveRoot = true 21 | const args = process.argv.slice(2).filter(arg => { 22 | if (dashdash) 23 | return !!arg 24 | else if (arg === '--') 25 | dashdash = true 26 | else if (arg === '--no-glob' || arg === '-G') 27 | noglob = true 28 | else if (arg === '--glob' || arg === '-g') 29 | noglob = false 30 | else if (arg.match(/^(-+|\/)(h(elp)?|\?)$/)) 31 | help = true 32 | else if (arg === '--preserve-root') 33 | preserveRoot = true 34 | else if (arg === '--no-preserve-root') 35 | preserveRoot = false 36 | else 37 | return !!arg 38 | }).filter(arg => !preserveRoot || filterOutRoot(arg)) 39 | 40 | const go = n => { 41 | if (n >= args.length) 42 | return 43 | const options = noglob ? { glob: false } : {} 44 | rimraf(args[n], options, er => { 45 | if (er) 46 | throw er 47 | go(n+1) 48 | }) 49 | } 50 | 51 | if (help || args.length === 0) { 52 | // If they didn't ask for help, then this is not a "success" 53 | const log = help ? console.log : console.error 54 | log('Usage: rimraf [ ...]') 55 | log('') 56 | log(' Deletes all files and folders at "path" recursively.') 57 | log('') 58 | log('Options:') 59 | log('') 60 | log(' -h, --help Display this usage info') 61 | log(' -G, --no-glob Do not expand glob patterns in arguments') 62 | log(' -g, --glob Expand glob patterns in arguments (default)') 63 | log(' --preserve-root Do not remove \'/\' (default)') 64 | log(' --no-preserve-root Do not treat \'/\' specially') 65 | log(' -- Stop parsing flags') 66 | process.exit(help ? 0 : 1) 67 | } else 68 | go(0) 69 | -------------------------------------------------------------------------------- /src/default-tmp.ts: -------------------------------------------------------------------------------- 1 | // The default temporary folder location for use in the windows algorithm. 2 | // It's TEMPting to use dirname(path), since that's guaranteed to be on the 3 | // same device. However, this means that: 4 | // rimraf(path).then(() => rimraf(dirname(path))) 5 | // will often fail with EBUSY, because the parent dir contains 6 | // marked-for-deletion directory entries (which do not show up in readdir). 7 | // The approach here is to use os.tmpdir() if it's on the same drive letter, 8 | // or resolve(path, '\\temp') if it exists, or the root of the drive if not. 9 | // On Posix (not that you'd be likely to use the windows algorithm there), 10 | // it uses os.tmpdir() always. 11 | import { tmpdir } from 'os' 12 | import { parse, resolve } from 'path' 13 | import { promises, statSync } from './fs.js' 14 | import platform from './platform.js' 15 | const { stat } = promises 16 | 17 | const isDirSync = (path: string) => { 18 | try { 19 | return statSync(path).isDirectory() 20 | } catch (er) { 21 | return false 22 | } 23 | } 24 | 25 | const isDir = (path: string) => 26 | stat(path).then( 27 | st => st.isDirectory(), 28 | () => false 29 | ) 30 | 31 | const win32DefaultTmp = async (path: string) => { 32 | const { root } = parse(path) 33 | const tmp = tmpdir() 34 | const { root: tmpRoot } = parse(tmp) 35 | if (root.toLowerCase() === tmpRoot.toLowerCase()) { 36 | return tmp 37 | } 38 | 39 | const driveTmp = resolve(root, '/temp') 40 | if (await isDir(driveTmp)) { 41 | return driveTmp 42 | } 43 | 44 | return root 45 | } 46 | 47 | const win32DefaultTmpSync = (path: string) => { 48 | const { root } = parse(path) 49 | const tmp = tmpdir() 50 | const { root: tmpRoot } = parse(tmp) 51 | if (root.toLowerCase() === tmpRoot.toLowerCase()) { 52 | return tmp 53 | } 54 | 55 | const driveTmp = resolve(root, '/temp') 56 | if (isDirSync(driveTmp)) { 57 | return driveTmp 58 | } 59 | 60 | return root 61 | } 62 | 63 | const posixDefaultTmp = async () => tmpdir() 64 | const posixDefaultTmpSync = () => tmpdir() 65 | 66 | export const defaultTmp = 67 | platform === 'win32' ? win32DefaultTmp : posixDefaultTmp 68 | export const defaultTmpSync = 69 | platform === 'win32' ? win32DefaultTmpSync : posixDefaultTmpSync 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rimraf", 3 | "version": "5.0.5", 4 | "type": "module", 5 | "tshy": { 6 | "main": true, 7 | "exports": { 8 | "./package.json": "./package.json", 9 | ".": "./src/index.ts" 10 | } 11 | }, 12 | "bin": "./dist/esm/bin.mjs", 13 | "main": "./dist/commonjs/index.js", 14 | "types": "./dist/commonjs/index.d.ts", 15 | "exports": { 16 | "./package.json": "./package.json", 17 | ".": { 18 | "import": { 19 | "types": "./dist/esm/index.d.ts", 20 | "default": "./dist/esm/index.js" 21 | }, 22 | "require": { 23 | "types": "./dist/commonjs/index.d.ts", 24 | "default": "./dist/commonjs/index.js" 25 | } 26 | } 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "description": "A deep deletion module for node (like `rm -rf`)", 32 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 33 | "license": "ISC", 34 | "repository": "git://github.com/isaacs/rimraf.git", 35 | "scripts": { 36 | "preversion": "npm test", 37 | "postversion": "npm publish", 38 | "prepublishOnly": "git push origin --follow-tags", 39 | "prepare": "tshy", 40 | "pretest": "npm run prepare", 41 | "presnap": "npm run prepare", 42 | "test": "tap", 43 | "snap": "tap", 44 | "format": "prettier --write . --loglevel warn", 45 | "benchmark": "node benchmark/index.js", 46 | "typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts" 47 | }, 48 | "prettier": { 49 | "semi": false, 50 | "printWidth": 80, 51 | "tabWidth": 2, 52 | "useTabs": false, 53 | "singleQuote": true, 54 | "jsxSingleQuote": false, 55 | "bracketSameLine": true, 56 | "arrowParens": "avoid", 57 | "endOfLine": "lf" 58 | }, 59 | "devDependencies": { 60 | "@types/node": "^20.6.5", 61 | "mkdirp": "^3.0.1", 62 | "prettier": "^2.8.2", 63 | "tap": "^18.5.4", 64 | "tshy": "^1.7.0", 65 | "typedoc": "^0.25.1", 66 | "typescript": "^5.2" 67 | }, 68 | "funding": { 69 | "url": "https://github.com/sponsors/isaacs" 70 | }, 71 | "engines": { 72 | "node": ">=14" 73 | }, 74 | "dependencies": { 75 | "glob": "^10.3.7" 76 | }, 77 | "keywords": [ 78 | "rm", 79 | "rm -rf", 80 | "rm -fr", 81 | "remove", 82 | "directory", 83 | "cli", 84 | "rmdir", 85 | "recursive" 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /test/use-native.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | 3 | // node before 14.14 didn't native recursive fs.rm 4 | if (/^v([0-8]\.|1[0-3]\.|14\.[0-9]\.|14\.1[1-3]\.)/.test(process.version)) { 5 | t.plan(0, 'no native recursive fs.rm in this node version') 6 | process.exit(0) 7 | } 8 | 9 | import { fileURLToPath } from 'url' 10 | import { useNative, useNativeSync } from '../dist/esm/use-native.js' 11 | 12 | const args = [...process.execArgv, fileURLToPath(import.meta.url)] 13 | 14 | if (!process.env.__TESTING_RIMRAF_EXPECT_USE_NATIVE__) { 15 | t.spawn( 16 | process.execPath, 17 | args, 18 | { 19 | env: { 20 | ...process.env, 21 | __TESTING_RIMRAF_PLATFORM__: 'darwin', 22 | __TESTING_RIMRAF_NODE_VERSION__: 'v18.0.0', 23 | __TESTING_RIMRAF_EXPECT_USE_NATIVE__: '1', 24 | }, 25 | }, 26 | 'darwin v18' 27 | ) 28 | 29 | t.spawn( 30 | process.execPath, 31 | args, 32 | { 33 | env: { 34 | ...process.env, 35 | __TESTING_RIMRAF_PLATFORM__: 'win32', 36 | __TESTING_RIMRAF_NODE_VERSION__: 'v18.0.0', 37 | __TESTING_RIMRAF_EXPECT_USE_NATIVE__: '0', 38 | }, 39 | }, 40 | 'win32 v18' 41 | ) 42 | 43 | t.spawn( 44 | process.execPath, 45 | args, 46 | { 47 | env: { 48 | ...process.env, 49 | __TESTING_RIMRAF_NODE_VERSION__: 'v8.9.10', 50 | __TESTING_RIMRAF_PLATFORM__: 'darwin', 51 | __TESTING_RIMRAF_EXPECT_USE_NATIVE__: '0', 52 | }, 53 | }, 54 | 'darwin v8' 55 | ) 56 | 57 | t.spawn( 58 | process.execPath, 59 | args, 60 | { 61 | env: { 62 | ...process.env, 63 | __TESTING_RIMRAF_NODE_VERSION__: 'v14.13.12', 64 | __TESTING_RIMRAF_PLATFORM__: 'darwin', 65 | __TESTING_RIMRAF_EXPECT_USE_NATIVE__: '0', 66 | }, 67 | }, 68 | 'darwin v14.13.12' 69 | ) 70 | } else { 71 | const expect = process.env.__TESTING_RIMRAF_EXPECT_USE_NATIVE__ === '1' 72 | if (expect) { 73 | // always need manual if a signal is passed in 74 | const signal = 75 | typeof AbortController !== 'undefined' ? new AbortController().signal : {} 76 | //@ts-ignore 77 | t.equal(useNative({ signal }), false) 78 | //@ts-ignore 79 | t.equal(useNativeSync({ signal }), false) 80 | 81 | // always need manual if a filter is provided 82 | t.equal(useNative({ filter: () => true }), false) 83 | t.equal(useNativeSync({ filter: () => true }), false) 84 | } 85 | t.equal(useNative(), expect) 86 | t.equal(useNativeSync(), expect) 87 | } 88 | -------------------------------------------------------------------------------- /src/opt-arg.ts: -------------------------------------------------------------------------------- 1 | import { Dirent, Stats } from 'fs' 2 | import { GlobOptions } from 'glob' 3 | 4 | const typeOrUndef = (val: any, t: string) => 5 | typeof val === 'undefined' || typeof val === t 6 | 7 | export const isRimrafOptions = (o: any): o is RimrafOptions => 8 | !!o && 9 | typeof o === 'object' && 10 | typeOrUndef(o.preserveRoot, 'boolean') && 11 | typeOrUndef(o.tmp, 'string') && 12 | typeOrUndef(o.maxRetries, 'number') && 13 | typeOrUndef(o.retryDelay, 'number') && 14 | typeOrUndef(o.backoff, 'number') && 15 | typeOrUndef(o.maxBackoff, 'number') && 16 | (typeOrUndef(o.glob, 'boolean') || (o.glob && typeof o.glob === 'object')) && 17 | typeOrUndef(o.filter, 'function') 18 | 19 | export const assertRimrafOptions: (o: any) => void = ( 20 | o: any 21 | ): asserts o is RimrafOptions => { 22 | if (!isRimrafOptions(o)) { 23 | throw new Error('invalid rimraf options') 24 | } 25 | } 26 | 27 | export interface RimrafAsyncOptions { 28 | preserveRoot?: boolean 29 | tmp?: string 30 | maxRetries?: number 31 | retryDelay?: number 32 | backoff?: number 33 | maxBackoff?: number 34 | signal?: AbortSignal 35 | glob?: boolean | GlobOptions 36 | filter?: 37 | | ((path: string, ent: Dirent | Stats) => boolean) 38 | | ((path: string, ent: Dirent | Stats) => Promise) 39 | } 40 | 41 | export interface RimrafSyncOptions extends RimrafAsyncOptions { 42 | filter?: (path: string, ent: Dirent | Stats) => boolean 43 | } 44 | 45 | export type RimrafOptions = RimrafSyncOptions | RimrafAsyncOptions 46 | 47 | const optArgT = ( 48 | opt: T 49 | ): 50 | | (T & { 51 | glob: GlobOptions & { withFileTypes: false } 52 | }) 53 | | (T & { glob: undefined }) => { 54 | assertRimrafOptions(opt) 55 | const { glob, ...options } = opt 56 | if (!glob) { 57 | return options as T & { glob: undefined } 58 | } 59 | const globOpt = 60 | glob === true 61 | ? opt.signal 62 | ? { signal: opt.signal } 63 | : {} 64 | : opt.signal 65 | ? { 66 | signal: opt.signal, 67 | ...glob, 68 | } 69 | : glob 70 | return { 71 | ...options, 72 | glob: { 73 | ...globOpt, 74 | // always get absolute paths from glob, to ensure 75 | // that we are referencing the correct thing. 76 | absolute: true, 77 | withFileTypes: false, 78 | }, 79 | } as T & { glob: GlobOptions & { withFileTypes: false } } 80 | } 81 | 82 | export const optArg = (opt: RimrafAsyncOptions = {}) => optArgT(opt) 83 | export const optArgSync = (opt: RimrafSyncOptions = {}) => optArgT(opt) 84 | -------------------------------------------------------------------------------- /test/delete-many-files.ts: -------------------------------------------------------------------------------- 1 | // this isn't for coverage. it's basically a smoke test, to ensure that 2 | // we can delete a lot of files on CI in multiple platforms and node versions. 3 | import t from 'tap' 4 | 5 | if (/^v10\./.test(process.version)) { 6 | t.plan(0, 'skip this on node 10, it runs out of memory') 7 | process.exit(0) 8 | } 9 | 10 | // run with RIMRAF_TEST_START_CHAR/_END_CHAR/_DEPTH environs to 11 | // make this more or less aggressive. 12 | const START = (process.env.RIMRAF_TEST_START_CHAR || 'a').charCodeAt(0) 13 | const END = (process.env.RIMRAF_TEST_END_CHAR || 'f').charCodeAt(0) 14 | const DEPTH = +(process.env.RIMRAF_TEST_DEPTH || '') || 4 15 | 16 | import { statSync, mkdirSync, readdirSync } from '../dist/esm/fs.js' 17 | import { writeFileSync } from 'fs' 18 | import { resolve, dirname } from 'path' 19 | 20 | const create = (path: string, depth = 0) => { 21 | mkdirSync(path) 22 | for (let i = START; i <= END; i++) { 23 | const c = String.fromCharCode(i) 24 | if (depth < DEPTH && i - START >= depth) { 25 | create(resolve(path, c), depth + 1) 26 | } else { 27 | writeFileSync(resolve(path, c), c) 28 | } 29 | } 30 | return path 31 | } 32 | 33 | import { manual } from '../dist/esm/index.js' 34 | const cases = { manual } 35 | 36 | const base = t.testdir(Object.fromEntries( 37 | Object.entries(cases).map(([name]) => [name, { 38 | sync: {}, 39 | async: {}, 40 | }]) 41 | )) 42 | 43 | t.test('create all fixtures', t => { 44 | for (const name of Object.keys(cases)) { 45 | for (const type of ['sync', 'async']) { 46 | const path = `${base}/${name}/${type}/test` 47 | create(path) 48 | t.equal(statSync(path).isDirectory(), true, `${name}/${type} created`) 49 | } 50 | } 51 | setTimeout(() => t.end(), 3000) 52 | }) 53 | 54 | t.test('delete all fixtures', t => { 55 | for (const [name, rimraf] of Object.entries(cases)) { 56 | t.test(name, t => { 57 | t.test('async', async t => { 58 | const path = `${base}/${name}/async/test` 59 | await rimraf(path, {}) 60 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'fully removed') 61 | t.same(readdirSync(dirname(path)), [], 'no temp entries left behind') 62 | }) 63 | 64 | t.test('sync', t => { 65 | const path = `${base}/${name}/sync/test` 66 | rimraf.sync(path, {}) 67 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'fully removed') 68 | t.same(readdirSync(dirname(path)), [], 'no temp entries left behind') 69 | t.end() 70 | }) 71 | 72 | t.end() 73 | }) 74 | } 75 | t.end() 76 | }) 77 | -------------------------------------------------------------------------------- /src/fs.ts: -------------------------------------------------------------------------------- 1 | // promisify ourselves, because older nodes don't have fs.promises 2 | 3 | import fs, { Dirent } from 'fs' 4 | 5 | // sync ones just take the sync version from node 6 | export { 7 | chmodSync, 8 | mkdirSync, 9 | renameSync, 10 | rmdirSync, 11 | rmSync, 12 | statSync, 13 | lstatSync, 14 | unlinkSync, 15 | } from 'fs' 16 | 17 | import { readdirSync as rdSync } from 'fs' 18 | export const readdirSync = (path: fs.PathLike): Dirent[] => 19 | rdSync(path, { withFileTypes: true }) 20 | 21 | // unrolled for better inlining, this seems to get better performance 22 | // than something like: 23 | // const makeCb = (res, rej) => (er, ...d) => er ? rej(er) : res(...d) 24 | // which would be a bit cleaner. 25 | 26 | const chmod = (path: fs.PathLike, mode: fs.Mode): Promise => 27 | new Promise((res, rej) => 28 | fs.chmod(path, mode, (er, ...d: any[]) => (er ? rej(er) : res(...d))) 29 | ) 30 | 31 | const mkdir = ( 32 | path: fs.PathLike, 33 | options?: 34 | | fs.Mode 35 | | (fs.MakeDirectoryOptions & { recursive?: boolean | null }) 36 | | undefined 37 | | null 38 | ): Promise => 39 | new Promise((res, rej) => 40 | fs.mkdir(path, options, (er, made) => (er ? rej(er) : res(made))) 41 | ) 42 | 43 | const readdir = (path: fs.PathLike): Promise => 44 | new Promise((res, rej) => 45 | fs.readdir(path, { withFileTypes: true }, (er, data) => 46 | er ? rej(er) : res(data) 47 | ) 48 | ) 49 | 50 | const rename = (oldPath: fs.PathLike, newPath: fs.PathLike): Promise => 51 | new Promise((res, rej) => 52 | fs.rename(oldPath, newPath, (er, ...d: any[]) => (er ? rej(er) : res(...d))) 53 | ) 54 | 55 | const rm = (path: fs.PathLike, options: fs.RmOptions): Promise => 56 | new Promise((res, rej) => 57 | fs.rm(path, options, (er, ...d: any[]) => (er ? rej(er) : res(...d))) 58 | ) 59 | 60 | const rmdir = (path: fs.PathLike): Promise => 61 | new Promise((res, rej) => 62 | fs.rmdir(path, (er, ...d: any[]) => (er ? rej(er) : res(...d))) 63 | ) 64 | 65 | const stat = (path: fs.PathLike): Promise => 66 | new Promise((res, rej) => 67 | fs.stat(path, (er, data) => (er ? rej(er) : res(data))) 68 | ) 69 | 70 | const lstat = (path: fs.PathLike): Promise => 71 | new Promise((res, rej) => 72 | fs.lstat(path, (er, data) => (er ? rej(er) : res(data))) 73 | ) 74 | 75 | const unlink = (path: fs.PathLike): Promise => 76 | new Promise((res, rej) => 77 | fs.unlink(path, (er, ...d: any[]) => (er ? rej(er) : res(...d))) 78 | ) 79 | 80 | export const promises = { 81 | chmod, 82 | mkdir, 83 | readdir, 84 | rename, 85 | rm, 86 | rmdir, 87 | stat, 88 | lstat, 89 | unlink, 90 | } 91 | -------------------------------------------------------------------------------- /test/path-arg.ts: -------------------------------------------------------------------------------- 1 | import * as PATH from 'path' 2 | import t from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import { inspect } from 'util' 5 | 6 | if (!process.env.__TESTING_RIMRAF_PLATFORM__) { 7 | const fake = process.platform === 'win32' ? 'posix' : 'win32' 8 | t.spawn( 9 | process.execPath, 10 | [...process.execArgv, fileURLToPath(import.meta.url)], 11 | { 12 | name: fake, 13 | env: { 14 | ...process.env, 15 | __TESTING_RIMRAF_PLATFORM__: fake, 16 | }, 17 | } 18 | ) 19 | } 20 | 21 | const platform = process.env.__TESTING_RIMRAF_PLATFORM__ || process.platform 22 | const path = PATH[platform as 'win32' | 'posix'] || PATH 23 | const { default: pathArg } = (await t.mockImport('../dist/esm/path-arg.js', { 24 | path, 25 | })) as typeof import('../dist/esm/path-arg.js') 26 | 27 | const { resolve } = path 28 | 29 | t.equal(pathArg('a/b/c'), resolve('a/b/c')) 30 | t.throws( 31 | () => pathArg('a\0b'), 32 | Error('path must be a string without null bytes') 33 | ) 34 | if (platform === 'win32') { 35 | const badPaths = [ 36 | 'c:\\a\\b:c', 37 | 'c:\\a\\b*c', 38 | 'c:\\a\\b?c', 39 | 'c:\\a\\bc', 41 | 'c:\\a\\b|c', 42 | 'c:\\a\\b"c', 43 | ] 44 | for (const path of badPaths) { 45 | const er = Object.assign(new Error('Illegal characters in path'), { 46 | path, 47 | code: 'EINVAL', 48 | }) 49 | t.throws(() => pathArg(path), er) 50 | } 51 | } 52 | 53 | t.throws(() => pathArg('/'), { code: 'ERR_PRESERVE_ROOT' }) 54 | 55 | t.throws(() => pathArg('/', { preserveRoot: undefined }), { 56 | code: 'ERR_PRESERVE_ROOT', 57 | }) 58 | t.equal(pathArg('/', { preserveRoot: false }), resolve('/')) 59 | 60 | //@ts-expect-error 61 | t.throws(() => pathArg({}), { 62 | code: 'ERR_INVALID_ARG_TYPE', 63 | path: {}, 64 | message: 65 | 'The "path" argument must be of type string. ' + 66 | 'Received an instance of Object', 67 | name: 'TypeError', 68 | }) 69 | //@ts-expect-error 70 | t.throws(() => pathArg([]), { 71 | code: 'ERR_INVALID_ARG_TYPE', 72 | path: [], 73 | message: 74 | 'The "path" argument must be of type string. ' + 75 | 'Received an instance of Array', 76 | name: 'TypeError', 77 | }) 78 | //@ts-expect-error 79 | t.throws(() => pathArg(Object.create(null) as {}), { 80 | code: 'ERR_INVALID_ARG_TYPE', 81 | path: Object.create(null), 82 | message: 83 | 'The "path" argument must be of type string. ' + 84 | `Received ${inspect(Object.create(null))}`, 85 | name: 'TypeError', 86 | }) 87 | //@ts-expect-error 88 | t.throws(() => pathArg(true), { 89 | code: 'ERR_INVALID_ARG_TYPE', 90 | path: true, 91 | message: 92 | 'The "path" argument must be of type string. ' + 93 | `Received type boolean true`, 94 | name: 'TypeError', 95 | }) 96 | -------------------------------------------------------------------------------- /test/fs.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | 3 | // verify that every function in the root is *Sync, and every 4 | // function is fs.promises is the promisified version of fs[method], 5 | // and that when the cb returns an error, the promised version fails, 6 | // and when the cb returns data, the promisified version resolves to it. 7 | import realFS from 'fs' 8 | import * as fs from '../dist/esm/fs.js' 9 | 10 | const mockFSMethodPass = 11 | (method: string) => 12 | (...args: any[]) => { 13 | const cb = args.pop() 14 | process.nextTick(() => cb(null, method, 1, 2, 3)) 15 | } 16 | const mockFSMethodFail = 17 | (method: string) => 18 | (...args: any[]) => { 19 | const cb = args.pop() 20 | process.nextTick(() => cb(new Error('oops'), method, 1, 2, 3)) 21 | } 22 | 23 | import { useNative } from '../dist/esm/use-native.js' 24 | t.type(fs.promises, Object) 25 | const mockFSPass: Record any> = {} 26 | const mockFSFail: Record any> = {} 27 | 28 | for (const method of Object.keys( 29 | fs.promises as Record any> 30 | )) { 31 | // of course fs.rm is missing when we shouldn't use native :) 32 | // also, readdirSync is clubbed to always return file types 33 | if (method !== 'rm' || useNative()) { 34 | t.type( 35 | (realFS as { [k: string]: any })[method], 36 | Function, 37 | `real fs.${method} is a function` 38 | ) 39 | if (method !== 'readdir') { 40 | t.equal( 41 | (fs as { [k: string]: any })[`${method}Sync`], 42 | (realFS as unknown as { [k: string]: (...a: any[]) => any })[ 43 | `${method}Sync` 44 | ], 45 | `has ${method}Sync` 46 | ) 47 | } 48 | } 49 | 50 | // set up our pass/fails for the next tests 51 | mockFSPass[method] = mockFSMethodPass(method) 52 | mockFSFail[method] = mockFSMethodFail(method) 53 | } 54 | 55 | // doesn't have any sync versions that aren't promisified 56 | for (const method of Object.keys(fs)) { 57 | if (method === 'promises') { 58 | continue 59 | } 60 | const m = method.replace(/Sync$/, '') 61 | t.type( 62 | (fs.promises as { [k: string]: (...a: any[]) => any })[m], 63 | Function, 64 | `fs.promises.${m} is a function` 65 | ) 66 | } 67 | 68 | t.test('passing resolves promise', async t => { 69 | const fs = (await t.mockImport('../src/fs.js', { 70 | fs: { ...realFS, ...mockFSPass }, 71 | })) as typeof import('../src/fs.js') 72 | for (const [m, fn] of Object.entries( 73 | fs.promises as { [k: string]: (...a: any) => Promise } 74 | )) { 75 | t.same(await fn(), m, `got expected value for ${m}`) 76 | } 77 | }) 78 | 79 | t.test('failing rejects promise', async t => { 80 | const fs = (await t.mockImport('../src/fs.js', { 81 | fs: { ...realFS, ...mockFSFail }, 82 | })) as typeof import('../src/fs.js') 83 | for (const [m, fn] of Object.entries( 84 | fs.promises as { [k: string]: (...a: any[]) => Promise } 85 | )) { 86 | t.rejects(fn(), { message: 'oops' }, `got expected value for ${m}`) 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 5.0 2 | 3 | - No default export, only named exports 4 | 5 | # 4.4 6 | 7 | - Provide Dirent or Stats object as second argument to filter 8 | 9 | # 4.3 10 | 11 | - Return boolean indicating whether the path was fully removed 12 | - Add filter option 13 | - bin: add --verbose, -v to print files as they are deleted 14 | - bin: add --no-verbose, -V to not print files as they are deleted 15 | - bin: add -i --interactive to be prompted on each deletion 16 | - bin: add -I --no-interactive to not be prompted on each 17 | deletion 18 | - **4.3.1** Fixed inappropriately following symbolic links to 19 | directories 20 | 21 | # v4.2 22 | 23 | - Brought back `glob` support, using the new and improved glob v9 24 | 25 | # v4.1 26 | 27 | - Improved hybrid module with no need to look at the `.default` 28 | dangly bit. `.default` preserved as a reference to `rimraf` 29 | for compatibility with anyone who came to rely on it in v4.0. 30 | - Accept and ignore `-rf` and `-fr` arguments to the bin. 31 | 32 | # v4.0 33 | 34 | - Remove `glob` dependency entirely. This library now only 35 | accepts actual file and folder names to delete. 36 | - Accept array of paths or single path. 37 | - Windows performance and reliability improved. 38 | - All strategies separated into explicitly exported methods. 39 | - Drop support for Node.js below version 14 40 | - rewrite in TypeScript 41 | - ship CJS/ESM hybrid module 42 | - Error on unknown arguments to the bin. (Previously they were 43 | silently ignored.) 44 | 45 | # v3.0 46 | 47 | - Add `--preserve-root` option to executable (default true) 48 | - Drop support for Node.js below version 6 49 | 50 | # v2.7 51 | 52 | - Make `glob` an optional dependency 53 | 54 | # 2.6 55 | 56 | - Retry on EBUSY on non-windows platforms as well 57 | - Make `rimraf.sync` 10000% more reliable on Windows 58 | 59 | # 2.5 60 | 61 | - Handle Windows EPERM when lstat-ing read-only dirs 62 | - Add glob option to pass options to glob 63 | 64 | # 2.4 65 | 66 | - Add EPERM to delay/retry loop 67 | - Add `disableGlob` option 68 | 69 | # 2.3 70 | 71 | - Make maxBusyTries and emfileWait configurable 72 | - Handle weird SunOS unlink-dir issue 73 | - Glob the CLI arg for better Windows support 74 | 75 | # 2.2 76 | 77 | - Handle ENOENT properly on Windows 78 | - Allow overriding fs methods 79 | - Treat EPERM as indicative of non-empty dir 80 | - Remove optional graceful-fs dep 81 | - Consistently return null error instead of undefined on success 82 | - win32: Treat ENOTEMPTY the same as EBUSY 83 | - Add `rimraf` binary 84 | 85 | # 2.1 86 | 87 | - Fix SunOS error code for a non-empty directory 88 | - Try rmdir before readdir 89 | - Treat EISDIR like EPERM 90 | - Remove chmod 91 | - Remove lstat polyfill, node 0.7 is not supported 92 | 93 | # 2.0 94 | 95 | - Fix myGid call to check process.getgid 96 | - Simplify the EBUSY backoff logic. 97 | - Use fs.lstat in node >= 0.7.9 98 | - Remove gently option 99 | - remove fiber implementation 100 | - Delete files that are marked read-only 101 | 102 | # 1.0 103 | 104 | - Allow ENOENT in sync method 105 | - Throw when no callback is provided 106 | - Make opts.gently an absolute path 107 | - use 'stat' if 'lstat' is not available 108 | - Consistent error naming, and rethrow non-ENOENT stat errors 109 | - add fiber implementation 110 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { glob, globSync } from 'glob' 2 | import { 3 | optArg, 4 | optArgSync, 5 | RimrafAsyncOptions, 6 | RimrafSyncOptions, 7 | } from './opt-arg.js' 8 | import pathArg from './path-arg.js' 9 | import { rimrafManual, rimrafManualSync } from './rimraf-manual.js' 10 | import { rimrafMoveRemove, rimrafMoveRemoveSync } from './rimraf-move-remove.js' 11 | import { rimrafNative, rimrafNativeSync } from './rimraf-native.js' 12 | import { rimrafPosix, rimrafPosixSync } from './rimraf-posix.js' 13 | import { rimrafWindows, rimrafWindowsSync } from './rimraf-windows.js' 14 | import { useNative, useNativeSync } from './use-native.js' 15 | 16 | export { 17 | assertRimrafOptions, 18 | isRimrafOptions, 19 | RimrafAsyncOptions, 20 | RimrafOptions, 21 | RimrafSyncOptions, 22 | } from './opt-arg.js' 23 | 24 | const wrap = 25 | (fn: (p: string, o: RimrafAsyncOptions) => Promise) => 26 | async ( 27 | path: string | string[], 28 | opt?: RimrafAsyncOptions 29 | ): Promise => { 30 | const options = optArg(opt) 31 | if (options.glob) { 32 | path = await glob(path, options.glob) 33 | } 34 | if (Array.isArray(path)) { 35 | return !!( 36 | await Promise.all(path.map(p => fn(pathArg(p, options), options))) 37 | ).reduce((a, b) => a && b, true) 38 | } else { 39 | return !!(await fn(pathArg(path, options), options)) 40 | } 41 | } 42 | 43 | const wrapSync = 44 | (fn: (p: string, o: RimrafSyncOptions) => boolean) => 45 | (path: string | string[], opt?: RimrafSyncOptions): boolean => { 46 | const options = optArgSync(opt) 47 | if (options.glob) { 48 | path = globSync(path, options.glob) 49 | } 50 | if (Array.isArray(path)) { 51 | return !!path 52 | .map(p => fn(pathArg(p, options), options)) 53 | .reduce((a, b) => a && b, true) 54 | } else { 55 | return !!fn(pathArg(path, options), options) 56 | } 57 | } 58 | 59 | export const nativeSync = wrapSync(rimrafNativeSync) 60 | export const native = Object.assign(wrap(rimrafNative), { sync: nativeSync }) 61 | 62 | export const manualSync = wrapSync(rimrafManualSync) 63 | export const manual = Object.assign(wrap(rimrafManual), { sync: manualSync }) 64 | 65 | export const windowsSync = wrapSync(rimrafWindowsSync) 66 | export const windows = Object.assign(wrap(rimrafWindows), { sync: windowsSync }) 67 | 68 | export const posixSync = wrapSync(rimrafPosixSync) 69 | export const posix = Object.assign(wrap(rimrafPosix), { sync: posixSync }) 70 | 71 | export const moveRemoveSync = wrapSync(rimrafMoveRemoveSync) 72 | export const moveRemove = Object.assign(wrap(rimrafMoveRemove), { 73 | sync: moveRemoveSync, 74 | }) 75 | 76 | export const rimrafSync = wrapSync((path, opt) => 77 | useNativeSync(opt) ? rimrafNativeSync(path, opt) : rimrafManualSync(path, opt) 78 | ) 79 | export const sync = rimrafSync 80 | 81 | const rimraf_ = wrap((path, opt) => 82 | useNative(opt) ? rimrafNative(path, opt) : rimrafManual(path, opt) 83 | ) 84 | export const rimraf = Object.assign(rimraf_, { 85 | rimraf: rimraf_, 86 | sync: rimrafSync, 87 | rimrafSync: rimrafSync, 88 | manual, 89 | manualSync, 90 | native, 91 | nativeSync, 92 | posix, 93 | posixSync, 94 | windows, 95 | windowsSync, 96 | moveRemove, 97 | moveRemoveSync, 98 | }) 99 | rimraf.rimraf = rimraf 100 | -------------------------------------------------------------------------------- /benchmark/run-test.js: -------------------------------------------------------------------------------- 1 | const START = process.env.RIMRAF_TEST_START_CHAR || 'a' 2 | const END = process.env.RIMRAF_TEST_END_CHAR || 'f' 3 | const DEPTH = +process.env.RIMRAF_TEST_DEPTH || 5 4 | const N = +process.env.RIMRAF_TEST_ITERATIONS || 7 5 | 6 | const cases = require('./rimrafs.js') 7 | 8 | const create = require('./create-fixture.js') 9 | 10 | const hrToMS = hr => Math.round(hr[0]*1e9 + hr[1]) / 1e6 11 | 12 | const runTest = async (type) => { 13 | const rimraf = cases[type] 14 | if (!rimraf) 15 | throw new Error('unknown rimraf type: ' + type) 16 | 17 | const opt = { 18 | start: START, 19 | end: END, 20 | depth: DEPTH, 21 | } 22 | console.error(`\nrunning test for ${type}, iterations=${N} %j...`, opt) 23 | 24 | // first, create all fixtures 25 | const syncPaths = [] 26 | const asyncPaths = [] 27 | const paraPaths = [] 28 | process.stderr.write('creating fixtures...') 29 | for (let i = 0; i < N; i++) { 30 | const [syncPath, asyncPath, paraPath] = await Promise.all([ 31 | create({ name: `${type}/sync/${i}`, ...opt }), 32 | create({ name: `${type}/async/${i}`, ...opt }), 33 | create({ name: `${type}/para/${i}`, ...opt }), 34 | ]) 35 | syncPaths.push(syncPath) 36 | asyncPaths.push(asyncPath) 37 | paraPaths.push(paraPath) 38 | process.stderr.write('.') 39 | } 40 | console.error('done!') 41 | 42 | const syncTimes = [] 43 | const syncFails = [] 44 | process.stderr.write('running sync tests...') 45 | const startSync = process.hrtime() 46 | for (const path of syncPaths) { 47 | const start = process.hrtime() 48 | try { 49 | rimraf.sync(path) 50 | syncTimes.push(hrToMS(process.hrtime(start))) 51 | } catch (er) { 52 | syncFails.push(er) 53 | } 54 | process.stderr.write('.') 55 | } 56 | const syncTotal = hrToMS(process.hrtime(startSync)) 57 | console.error('done! (%j ms, %j failed)', syncTotal, syncFails.length) 58 | 59 | const asyncTimes = [] 60 | const asyncFails = [] 61 | process.stderr.write('running async tests...') 62 | const startAsync = process.hrtime() 63 | for (const path of asyncPaths) { 64 | const start = process.hrtime() 65 | await rimraf(path).then( 66 | () => asyncTimes.push(hrToMS(process.hrtime(start))), 67 | er => asyncFails.push(er) 68 | ).then(() => process.stderr.write('.')) 69 | } 70 | const asyncTotal = hrToMS(process.hrtime(startAsync)) 71 | console.error('done! (%j ms, %j failed)', asyncTotal, asyncFails.length) 72 | 73 | const paraTimes = [] 74 | const paraFails = [] 75 | process.stderr.write('running parallel tests...') 76 | const startPara = process.hrtime() 77 | const paraRuns = [] 78 | for (const path of paraPaths) { 79 | const start = process.hrtime() 80 | paraRuns.push(rimraf(path).then( 81 | () => paraTimes.push(hrToMS(process.hrtime(start))), 82 | er => paraFails.push(er) 83 | ).then(() => process.stderr.write('.'))) 84 | } 85 | await Promise.all(paraRuns) 86 | const paraTotal = hrToMS(process.hrtime(startPara)) 87 | console.error('done! (%j ms, %j failed)', paraTotal, paraFails.length) 88 | 89 | // wait a tick to let stderr to clear 90 | return Promise.resolve().then(() => ({ 91 | syncTimes, 92 | syncFails, 93 | asyncTimes, 94 | asyncFails, 95 | paraTimes, 96 | paraFails, 97 | })) 98 | } 99 | 100 | module.exports = runTest 101 | -------------------------------------------------------------------------------- /test/retry-busy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | codes, 3 | MAXBACKOFF, 4 | MAXRETRIES, 5 | RATE, 6 | retryBusy, 7 | retryBusySync, 8 | } from '../dist/esm/retry-busy.js' 9 | 10 | import t from 'tap' 11 | 12 | t.matchSnapshot( 13 | { 14 | MAXBACKOFF, 15 | RATE, 16 | MAXRETRIES, 17 | codes, 18 | }, 19 | 'default settings' 20 | ) 21 | 22 | t.test('basic working operation when no errors happen', async t => { 23 | let calls = 0 24 | const arg = {} as unknown as string 25 | const opt = {} 26 | const method = (a: typeof arg, b?: any) => { 27 | t.equal(a, arg, 'got first argument') 28 | t.equal(b, undefined, 'did not get another argument') 29 | calls++ 30 | } 31 | const asyncMethod = async (a: typeof arg, b?: any) => method(a, b) 32 | const rBS = retryBusySync(method) 33 | rBS(arg, opt) 34 | t.equal(calls, 1) 35 | const rB = retryBusy(asyncMethod) 36 | await rB(arg, opt).then(() => t.equal(calls, 2)) 37 | }) 38 | 39 | t.test('retry when known error code thrown', t => { 40 | t.plan(codes.size) 41 | 42 | for (const code of codes) { 43 | t.test(code, async t => { 44 | let thrown = false 45 | let calls = 0 46 | const arg = {} as unknown as string 47 | const opt = {} 48 | const method = (a: string, b?: any) => { 49 | t.equal(a, arg, 'got first argument') 50 | t.equal(b, undefined, 'did not get another argument') 51 | if (!thrown) { 52 | thrown = true 53 | t.equal(calls, 0, 'first call') 54 | calls++ 55 | throw Object.assign(new Error(code), { path: a, code }) 56 | } else { 57 | t.equal(calls, 1, 'second call') 58 | calls++ 59 | thrown = false 60 | } 61 | } 62 | const asyncMethod = async (a: string, b?: any) => method(a, b) 63 | const rBS = retryBusySync(method) 64 | rBS(arg, opt) 65 | t.equal(calls, 2) 66 | calls = 0 67 | const rB = retryBusy(asyncMethod) 68 | await rB(arg, opt).then(() => t.equal(calls, 2)) 69 | }) 70 | } 71 | }) 72 | 73 | t.test('retry and eventually give up', t => { 74 | t.plan(codes.size) 75 | const opt = { 76 | maxBackoff: 2, 77 | maxRetries: 2, 78 | } 79 | 80 | for (const code of codes) { 81 | t.test(code, async t => { 82 | let calls = 0 83 | const arg = {} as unknown as string 84 | const method = (a: string, b?: any) => { 85 | t.equal(a, arg, 'got first argument') 86 | t.equal(b, undefined, 'did not get another argument') 87 | calls++ 88 | throw Object.assign(new Error(code), { path: a, code }) 89 | } 90 | const asyncMethod = async (a: string, b?: any) => method(a, b) 91 | const rBS = retryBusySync(method) 92 | t.throws(() => rBS(arg, opt), { path: arg, code }) 93 | t.equal(calls, 3) 94 | calls = 0 95 | const rB = retryBusy(asyncMethod) 96 | await t.rejects(rB(arg, opt)).then(() => t.equal(calls, 3)) 97 | }) 98 | } 99 | }) 100 | 101 | t.test('throw unknown error gives up right away', async t => { 102 | const arg = {} as unknown as string 103 | const opt = {} 104 | const method = (a: string, b?: any) => { 105 | t.equal(a, arg, 'got first argument') 106 | t.equal(b, undefined, 'did not get another argument') 107 | throw Object.assign(new Error('nope'), { path: a, code: 'nope' }) 108 | } 109 | const asyncMethod = async (a: string, b?: any) => method(a, b) 110 | const rBS = retryBusySync(method) 111 | t.throws(() => rBS(arg, opt), { code: 'nope' }) 112 | const rB = retryBusy(asyncMethod) 113 | await t.rejects(rB(arg, opt), { code: 'nope' }) 114 | }) 115 | -------------------------------------------------------------------------------- /benchmark/old-rimraf/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/isaacs/rimraf.svg?branch=master)](https://travis-ci.org/isaacs/rimraf) [![Dependency Status](https://david-dm.org/isaacs/rimraf.svg)](https://david-dm.org/isaacs/rimraf) [![devDependency Status](https://david-dm.org/isaacs/rimraf/dev-status.svg)](https://david-dm.org/isaacs/rimraf#info=devDependencies) 2 | 3 | The [UNIX command](http://en.wikipedia.org/wiki/Rm_(Unix)) `rm -rf` for node. 4 | 5 | Install with `npm install rimraf`, or just drop rimraf.js somewhere. 6 | 7 | ## API 8 | 9 | `rimraf(f, [opts], callback)` 10 | 11 | The first parameter will be interpreted as a globbing pattern for files. If you 12 | want to disable globbing you can do so with `opts.disableGlob` (defaults to 13 | `false`). This might be handy, for instance, if you have filenames that contain 14 | globbing wildcard characters. 15 | 16 | The callback will be called with an error if there is one. Certain 17 | errors are handled for you: 18 | 19 | * Windows: `EBUSY` and `ENOTEMPTY` - rimraf will back off a maximum of 20 | `opts.maxBusyTries` times before giving up, adding 100ms of wait 21 | between each attempt. The default `maxBusyTries` is 3. 22 | * `ENOENT` - If the file doesn't exist, rimraf will return 23 | successfully, since your desired outcome is already the case. 24 | * `EMFILE` - Since `readdir` requires opening a file descriptor, it's 25 | possible to hit `EMFILE` if too many file descriptors are in use. 26 | In the sync case, there's nothing to be done for this. But in the 27 | async case, rimraf will gradually back off with timeouts up to 28 | `opts.emfileWait` ms, which defaults to 1000. 29 | 30 | ## options 31 | 32 | * unlink, chmod, stat, lstat, rmdir, readdir, 33 | unlinkSync, chmodSync, statSync, lstatSync, rmdirSync, readdirSync 34 | 35 | In order to use a custom file system library, you can override 36 | specific fs functions on the options object. 37 | 38 | If any of these functions are present on the options object, then 39 | the supplied function will be used instead of the default fs 40 | method. 41 | 42 | Sync methods are only relevant for `rimraf.sync()`, of course. 43 | 44 | For example: 45 | 46 | ```javascript 47 | var myCustomFS = require('some-custom-fs') 48 | 49 | rimraf('some-thing', myCustomFS, callback) 50 | ``` 51 | 52 | * maxBusyTries 53 | 54 | If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error code is encountered 55 | on Windows systems, then rimraf will retry with a linear backoff 56 | wait of 100ms longer on each try. The default maxBusyTries is 3. 57 | 58 | Only relevant for async usage. 59 | 60 | * emfileWait 61 | 62 | If an `EMFILE` error is encountered, then rimraf will retry 63 | repeatedly with a linear backoff of 1ms longer on each try, until 64 | the timeout counter hits this max. The default limit is 1000. 65 | 66 | If you repeatedly encounter `EMFILE` errors, then consider using 67 | [graceful-fs](http://npm.im/graceful-fs) in your program. 68 | 69 | Only relevant for async usage. 70 | 71 | * glob 72 | 73 | Set to `false` to disable [glob](http://npm.im/glob) pattern 74 | matching. 75 | 76 | Set to an object to pass options to the glob module. The default 77 | glob options are `{ nosort: true, silent: true }`. 78 | 79 | Glob version 6 is used in this module. 80 | 81 | Relevant for both sync and async usage. 82 | 83 | * disableGlob 84 | 85 | Set to any non-falsey value to disable globbing entirely. 86 | (Equivalent to setting `glob: false`.) 87 | 88 | ## rimraf.sync 89 | 90 | It can remove stuff synchronously, too. But that's not so good. Use 91 | the async API. It's better. 92 | 93 | ## CLI 94 | 95 | If installed with `npm install rimraf -g` it can be used as a global 96 | command `rimraf [ ...]` which is useful for cross platform support. 97 | 98 | ## mkdirp 99 | 100 | If you need to create a directory recursively, check out 101 | [mkdirp](https://github.com/substack/node-mkdirp). 102 | -------------------------------------------------------------------------------- /src/rimraf-posix.ts: -------------------------------------------------------------------------------- 1 | // the simple recursive removal, where unlink and rmdir are atomic 2 | // Note that this approach does NOT work on Windows! 3 | // We stat first and only unlink if the Dirent isn't a directory, 4 | // because sunos will let root unlink a directory, and some 5 | // SUPER weird breakage happens as a result. 6 | 7 | import { lstatSync, promises, rmdirSync, unlinkSync } from './fs.js' 8 | const { lstat, rmdir, unlink } = promises 9 | 10 | import { parse, resolve } from 'path' 11 | 12 | import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' 13 | 14 | import { Dirent, Stats } from 'fs' 15 | import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js' 16 | import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' 17 | 18 | export const rimrafPosix = async (path: string, opt: RimrafAsyncOptions) => { 19 | if (opt?.signal?.aborted) { 20 | throw opt.signal.reason 21 | } 22 | try { 23 | return await rimrafPosixDir(path, opt, await lstat(path)) 24 | } catch (er) { 25 | if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true 26 | throw er 27 | } 28 | } 29 | 30 | export const rimrafPosixSync = (path: string, opt: RimrafSyncOptions) => { 31 | if (opt?.signal?.aborted) { 32 | throw opt.signal.reason 33 | } 34 | try { 35 | return rimrafPosixDirSync(path, opt, lstatSync(path)) 36 | } catch (er) { 37 | if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true 38 | throw er 39 | } 40 | } 41 | 42 | const rimrafPosixDir = async ( 43 | path: string, 44 | opt: RimrafAsyncOptions, 45 | ent: Dirent | Stats 46 | ): Promise => { 47 | if (opt?.signal?.aborted) { 48 | throw opt.signal.reason 49 | } 50 | const entries = ent.isDirectory() ? await readdirOrError(path) : null 51 | if (!Array.isArray(entries)) { 52 | // this can only happen if lstat/readdir lied, or if the dir was 53 | // swapped out with a file at just the right moment. 54 | /* c8 ignore start */ 55 | if (entries) { 56 | if (entries.code === 'ENOENT') { 57 | return true 58 | } 59 | if (entries.code !== 'ENOTDIR') { 60 | throw entries 61 | } 62 | } 63 | /* c8 ignore stop */ 64 | if (opt.filter && !(await opt.filter(path, ent))) { 65 | return false 66 | } 67 | await ignoreENOENT(unlink(path)) 68 | return true 69 | } 70 | 71 | const removedAll = ( 72 | await Promise.all( 73 | entries.map(ent => rimrafPosixDir(resolve(path, ent.name), opt, ent)) 74 | ) 75 | ).reduce((a, b) => a && b, true) 76 | 77 | if (!removedAll) { 78 | return false 79 | } 80 | 81 | // we don't ever ACTUALLY try to unlink /, because that can never work 82 | // but when preserveRoot is false, we could be operating on it. 83 | // No need to check if preserveRoot is not false. 84 | if (opt.preserveRoot === false && path === parse(path).root) { 85 | return false 86 | } 87 | 88 | if (opt.filter && !(await opt.filter(path, ent))) { 89 | return false 90 | } 91 | 92 | await ignoreENOENT(rmdir(path)) 93 | return true 94 | } 95 | 96 | const rimrafPosixDirSync = ( 97 | path: string, 98 | opt: RimrafSyncOptions, 99 | ent: Dirent | Stats 100 | ): boolean => { 101 | if (opt?.signal?.aborted) { 102 | throw opt.signal.reason 103 | } 104 | const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null 105 | if (!Array.isArray(entries)) { 106 | // this can only happen if lstat/readdir lied, or if the dir was 107 | // swapped out with a file at just the right moment. 108 | /* c8 ignore start */ 109 | if (entries) { 110 | if (entries.code === 'ENOENT') { 111 | return true 112 | } 113 | if (entries.code !== 'ENOTDIR') { 114 | throw entries 115 | } 116 | } 117 | /* c8 ignore stop */ 118 | if (opt.filter && !opt.filter(path, ent)) { 119 | return false 120 | } 121 | ignoreENOENTSync(() => unlinkSync(path)) 122 | return true 123 | } 124 | let removedAll: boolean = true 125 | for (const ent of entries) { 126 | const p = resolve(path, ent.name) 127 | removedAll = rimrafPosixDirSync(p, opt, ent) && removedAll 128 | } 129 | if (opt.preserveRoot === false && path === parse(path).root) { 130 | return false 131 | } 132 | 133 | if (!removedAll) { 134 | return false 135 | } 136 | 137 | if (opt.filter && !opt.filter(path, ent)) { 138 | return false 139 | } 140 | 141 | ignoreENOENTSync(() => rmdirSync(path)) 142 | return true 143 | } 144 | -------------------------------------------------------------------------------- /test/opt-arg.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { optArg as oa, optArgSync as oas } from '../dist/esm/opt-arg.js' 3 | import { RimrafAsyncOptions, RimrafSyncOptions } from '../src/index.js' 4 | 5 | const asyncOpt = { a: 1 } as unknown as RimrafAsyncOptions 6 | const syncOpt = { s: 1 } as unknown as RimrafSyncOptions 7 | 8 | t.same(oa(asyncOpt), asyncOpt, 'returns equivalent object if provided') 9 | t.same(oas(syncOpt), oa(syncOpt), 'optArgSync does the same thing') 10 | t.same(oa(), {}, 'returns new object otherwise') 11 | t.same(oas(), {}, 'returns new object otherwise, sync') 12 | 13 | //@ts-expect-error 14 | t.throws(() => oa(true)) 15 | //@ts-expect-error 16 | t.throws(() => oa(null)) 17 | //@ts-expect-error 18 | t.throws(() => oa('hello')) 19 | //@ts-expect-error 20 | t.throws(() => oa({ maxRetries: 'banana' })) 21 | 22 | t.test('every kind of invalid option value', t => { 23 | // skip them when it's undefined, and skip the case 24 | // where they're all undefined, otherwise try every 25 | // possible combination of the values here. 26 | const badBool = [undefined, 1, null, 'x', {}] 27 | const badNum = [undefined, true, false, null, 'x', '1', {}] 28 | const badStr = [undefined, { toString: () => 'hi' }, /hi/, Symbol.for('hi')] 29 | for (const preserveRoot of badBool) { 30 | for (const tmp of badStr) { 31 | for (const maxRetries of badNum) { 32 | for (const retryDelay of badNum) { 33 | for (const backoff of badNum) { 34 | for (const maxBackoff of badNum) { 35 | if ( 36 | preserveRoot === undefined && 37 | maxRetries === undefined && 38 | retryDelay === undefined && 39 | backoff === undefined && 40 | maxBackoff === undefined && 41 | tmp === undefined 42 | ) { 43 | continue 44 | } 45 | t.throws(() => 46 | oa({ 47 | //@ts-expect-error 48 | preserveRoot, 49 | //@ts-expect-error 50 | maxRetries, 51 | //@ts-expect-error 52 | retryDelay, 53 | //@ts-expect-error 54 | backoff, 55 | //@ts-expect-error 56 | maxBackoff, 57 | //@ts-expect-error 58 | tmp, 59 | }) 60 | ) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | t.end() 68 | }) 69 | 70 | t.test('test every allowed combination', t => { 71 | const goodBool = [undefined, true, false] 72 | // note that a few of these actually aren't *valid*, 73 | // but it's verifying what the initial opt checker does. 74 | const goodNum = [undefined, 1, Math.pow(2, 32), -1] 75 | const goodStr = [undefined, 'hi'] 76 | for (const preserveRoot of goodBool) { 77 | for (const tmp of goodStr) { 78 | for (const maxRetries of goodNum) { 79 | for (const retryDelay of goodNum) { 80 | for (const backoff of goodNum) { 81 | for (const maxBackoff of goodNum) { 82 | t.ok( 83 | oa({ 84 | preserveRoot, 85 | maxRetries, 86 | retryDelay, 87 | backoff, 88 | maxBackoff, 89 | tmp, 90 | }) 91 | ) 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | t.end() 99 | }) 100 | 101 | t.test('glob option handling', t => { 102 | t.same(oa({ glob: true }), { 103 | glob: { absolute: true, withFileTypes: false }, 104 | }) 105 | const gws = oa({ signal: { x: 1 } as unknown as AbortSignal, glob: true }) 106 | t.same(gws, { 107 | signal: { x: 1 }, 108 | glob: { absolute: true, signal: { x: 1 }, withFileTypes: false }, 109 | }) 110 | t.equal(gws.signal, gws.glob?.signal) 111 | t.same(oa({ glob: { nodir: true } }), { 112 | glob: { absolute: true, nodir: true, withFileTypes: false }, 113 | }) 114 | const gwsg = oa({ 115 | signal: { x: 1 } as unknown as AbortSignal, 116 | glob: { nodir: true }, 117 | }) 118 | t.same(gwsg, { 119 | signal: { x: 1 }, 120 | glob: { 121 | absolute: true, 122 | nodir: true, 123 | withFileTypes: false, 124 | signal: { x: 1 }, 125 | }, 126 | }) 127 | t.equal(gwsg.signal, gwsg.glob?.signal) 128 | t.same( 129 | oa({ 130 | signal: { x: 1 } as unknown as AbortSignal, 131 | glob: { nodir: true, signal: { y: 1 } as unknown as AbortSignal }, 132 | }), 133 | { 134 | signal: { x: 1 }, 135 | glob: { 136 | absolute: true, 137 | nodir: true, 138 | signal: { y: 1 }, 139 | withFileTypes: false, 140 | }, 141 | } 142 | ) 143 | t.end() 144 | }) 145 | -------------------------------------------------------------------------------- /tap-snapshots/test/index.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/index.ts > TAP > mocky unit tests to select the correct function > main function, useNative=false > must match snapshot 1`] = ` 9 | Array [ 10 | Array [ 11 | "optArg", 12 | Object { 13 | "a": 1, 14 | }, 15 | ], 16 | Array [ 17 | "pathArg", 18 | "path", 19 | ], 20 | Array [ 21 | "useNative", 22 | Object { 23 | "a": 1, 24 | }, 25 | ], 26 | Array [ 27 | "rimrafPosix", 28 | "path", 29 | Object { 30 | "a": 1, 31 | }, 32 | ], 33 | Array [ 34 | "optArg", 35 | Object { 36 | "a": 2, 37 | }, 38 | ], 39 | Array [ 40 | "pathArg", 41 | "path", 42 | ], 43 | Array [ 44 | "useNativeSync", 45 | Object { 46 | "a": 2, 47 | }, 48 | ], 49 | Array [ 50 | "rimrafPosixSync", 51 | "path", 52 | Object { 53 | "a": 2, 54 | }, 55 | ], 56 | ] 57 | ` 58 | 59 | exports[`test/index.ts > TAP > mocky unit tests to select the correct function > main function, useNative=true > must match snapshot 1`] = ` 60 | Array [ 61 | Array [ 62 | "optArg", 63 | Object { 64 | "a": 1, 65 | }, 66 | ], 67 | Array [ 68 | "pathArg", 69 | "path", 70 | ], 71 | Array [ 72 | "useNative", 73 | Object { 74 | "a": 1, 75 | }, 76 | ], 77 | Array [ 78 | "rimrafNative", 79 | "path", 80 | Object { 81 | "a": 1, 82 | }, 83 | ], 84 | Array [ 85 | "optArg", 86 | Object { 87 | "a": 2, 88 | }, 89 | ], 90 | Array [ 91 | "pathArg", 92 | "path", 93 | ], 94 | Array [ 95 | "useNativeSync", 96 | Object { 97 | "a": 2, 98 | }, 99 | ], 100 | Array [ 101 | "rimrafNativeSync", 102 | "path", 103 | Object { 104 | "a": 2, 105 | }, 106 | ], 107 | ] 108 | ` 109 | 110 | exports[`test/index.ts > TAP > mocky unit tests to select the correct function > manual > must match snapshot 1`] = ` 111 | Array [ 112 | Array [ 113 | "optArg", 114 | Object { 115 | "a": 3, 116 | }, 117 | ], 118 | Array [ 119 | "pathArg", 120 | "path", 121 | ], 122 | Array [ 123 | "rimrafPosix", 124 | "path", 125 | Object { 126 | "a": 3, 127 | }, 128 | ], 129 | Array [ 130 | "optArg", 131 | Object { 132 | "a": 4, 133 | }, 134 | ], 135 | Array [ 136 | "pathArg", 137 | "path", 138 | ], 139 | Array [ 140 | "rimrafPosixSync", 141 | "path", 142 | Object { 143 | "a": 4, 144 | }, 145 | ], 146 | ] 147 | ` 148 | 149 | exports[`test/index.ts > TAP > mocky unit tests to select the correct function > native > must match snapshot 1`] = ` 150 | Array [ 151 | Array [ 152 | "optArg", 153 | Object { 154 | "a": 5, 155 | }, 156 | ], 157 | Array [ 158 | "pathArg", 159 | "path", 160 | ], 161 | Array [ 162 | "rimrafNative", 163 | "path", 164 | Object { 165 | "a": 5, 166 | }, 167 | ], 168 | Array [ 169 | "optArg", 170 | Object { 171 | "a": 6, 172 | }, 173 | ], 174 | Array [ 175 | "pathArg", 176 | "path", 177 | ], 178 | Array [ 179 | "rimrafNativeSync", 180 | "path", 181 | Object { 182 | "a": 6, 183 | }, 184 | ], 185 | ] 186 | ` 187 | 188 | exports[`test/index.ts > TAP > mocky unit tests to select the correct function > posix > must match snapshot 1`] = ` 189 | Array [ 190 | Array [ 191 | "optArg", 192 | Object { 193 | "a": 7, 194 | }, 195 | ], 196 | Array [ 197 | "pathArg", 198 | "path", 199 | ], 200 | Array [ 201 | "rimrafPosix", 202 | "path", 203 | Object { 204 | "a": 7, 205 | }, 206 | ], 207 | Array [ 208 | "optArg", 209 | Object { 210 | "a": 8, 211 | }, 212 | ], 213 | Array [ 214 | "pathArg", 215 | "path", 216 | ], 217 | Array [ 218 | "rimrafPosixSync", 219 | "path", 220 | Object { 221 | "a": 8, 222 | }, 223 | ], 224 | ] 225 | ` 226 | 227 | exports[`test/index.ts > TAP > mocky unit tests to select the correct function > windows > must match snapshot 1`] = ` 228 | Array [ 229 | Array [ 230 | "optArg", 231 | Object { 232 | "a": 9, 233 | }, 234 | ], 235 | Array [ 236 | "pathArg", 237 | "path", 238 | ], 239 | Array [ 240 | "rimrafWindows", 241 | "path", 242 | Object { 243 | "a": 9, 244 | }, 245 | ], 246 | Array [ 247 | "optArg", 248 | Object { 249 | "a": 10, 250 | }, 251 | ], 252 | Array [ 253 | "pathArg", 254 | "path", 255 | ], 256 | Array [ 257 | "rimrafWindowsSync", 258 | "path", 259 | Object { 260 | "a": 10, 261 | }, 262 | ], 263 | ] 264 | ` 265 | -------------------------------------------------------------------------------- /test/fix-eperm.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | 3 | t.test('works if it works', async t => { 4 | const { fixEPERM, fixEPERMSync } = await t.mockImport('../dist/esm/fix-eperm.js', {}) as typeof import('../dist/esm/fix-eperm.js') 5 | const fixed = fixEPERM(async () => 1) 6 | await fixed('x').then(n => t.equal(n, 1)) 7 | const fixedSync = fixEPERMSync(() => 1) 8 | t.equal(fixedSync('x'), 1) 9 | }) 10 | 11 | t.test('throw non-EPERM just throws', async t => { 12 | const { fixEPERM, fixEPERMSync } = await t.mockImport('../dist/esm/fix-eperm.js', {}) as typeof import('../dist/esm/fix-eperm.js') 13 | const fixed = fixEPERM(() => { 14 | throw new Error('oops') 15 | }) 16 | await t.rejects(fixed('x'), new Error('oops')) 17 | const fixedSync = fixEPERMSync(() => { 18 | throw new Error('oops') 19 | }) 20 | t.throws(() => fixedSync('x'), new Error('oops')) 21 | }) 22 | 23 | t.test('throw ENOENT returns void', async t => { 24 | const er = Object.assign(new Error('no ents'), { code: 'ENOENT' }) 25 | const { fixEPERM, fixEPERMSync } = await t.mockImport('../dist/esm/fix-eperm.js', {}) as typeof import('../dist/esm/fix-eperm.js') 26 | const fixed = fixEPERM(() => { 27 | throw er 28 | }) 29 | await fixed('x').then(n => t.equal(n, undefined)) 30 | const fixedSync = fixEPERMSync(() => { 31 | throw er 32 | }) 33 | t.equal(fixedSync('x'), undefined) 34 | }) 35 | 36 | t.test('chmod and try again', async t => { 37 | const seen = new Set() 38 | const finished = new Set() 39 | const eperm = Object.assign(new Error('perm'), { code: 'EPERM' }) 40 | const method = (p: string) => { 41 | if (!seen.has(p)) { 42 | seen.add(p) 43 | throw eperm 44 | } else { 45 | t.equal(chmods.has(p), true) 46 | t.equal(finished.has(p), false) 47 | finished.add(p) 48 | } 49 | } 50 | const asyncMethod = async (p: string) => method(p) 51 | const chmods = new Set() 52 | const chmodSync = (p: string, mode: number) => { 53 | t.equal(chmods.has(p), false) 54 | chmods.add(p) 55 | t.equal(mode, 0o666) 56 | } 57 | const chmod = async (p: string, mode: number) => chmodSync(p, mode) 58 | const { fixEPERM, fixEPERMSync } = await t.mockImport('../dist/esm/fix-eperm.js', { 59 | '../dist/esm/fs.js': { 60 | promises: { chmod }, 61 | chmodSync, 62 | }, 63 | }) as typeof import('../dist/esm/fix-eperm.js') 64 | const fixed = fixEPERM(asyncMethod) 65 | const fixedSync = fixEPERMSync(method) 66 | await fixed('async').then(n => t.equal(n, undefined)) 67 | t.equal(fixedSync('sync'), undefined) 68 | t.equal(chmods.size, 2) 69 | t.equal(seen.size, 2) 70 | t.equal(finished.size, 2) 71 | }) 72 | 73 | t.test('chmod ENOENT is fine, abort', async t => { 74 | const seen = new Set() 75 | const finished = new Set() 76 | const eperm = Object.assign(new Error('perm'), { code: 'EPERM' }) 77 | const method = (p: string) => { 78 | if (!seen.has(p)) { 79 | seen.add(p) 80 | throw eperm 81 | } else { 82 | t.equal(chmods.has(p), true) 83 | t.equal(finished.has(p), false) 84 | finished.add(p) 85 | } 86 | } 87 | const asyncMethod = async (p: string) => method(p) 88 | const chmods = new Set() 89 | const chmodSync = (p: string, mode: number) => { 90 | t.equal(chmods.has(p), false) 91 | chmods.add(p) 92 | t.equal(mode, 0o666) 93 | throw Object.assign(new Error('no ent'), { code: 'ENOENT' }) 94 | } 95 | const chmod = async (p:string, mode:number) => chmodSync(p, mode) 96 | const { fixEPERM, fixEPERMSync } = await t.mockImport('../dist/esm/fix-eperm.js', { 97 | '../dist/esm/fs.js': { 98 | promises: { chmod }, 99 | chmodSync, 100 | }, 101 | }) as typeof import('../dist/esm/fix-eperm.js') 102 | const fixed = fixEPERM(asyncMethod) 103 | const fixedSync = fixEPERMSync(method) 104 | await fixed('async').then(n => t.equal(n, undefined)) 105 | t.equal(fixedSync('sync'), undefined) 106 | t.equal(chmods.size, 2) 107 | t.equal(seen.size, 2) 108 | t.equal(finished.size, 0) 109 | }) 110 | 111 | t.test('chmod other than ENOENT is failure', async t => { 112 | const seen = new Set() 113 | const finished = new Set() 114 | const eperm = Object.assign(new Error('perm'), { code: 'EPERM' }) 115 | const method = (p: string) => { 116 | if (!seen.has(p)) { 117 | seen.add(p) 118 | throw eperm 119 | } else { 120 | t.equal(chmods.has(p), true) 121 | t.equal(finished.has(p), false) 122 | finished.add(p) 123 | } 124 | } 125 | const asyncMethod = async (p: string) => method(p) 126 | const chmods = new Set() 127 | const chmodSync = (p:string, mode:number) => { 128 | t.equal(chmods.has(p), false) 129 | chmods.add(p) 130 | t.equal(mode, 0o666) 131 | throw Object.assign(new Error('ent bro'), { code: 'OHNO' }) 132 | } 133 | const chmod = async (p:string, mode:number) => chmodSync(p, mode) 134 | const { fixEPERM, fixEPERMSync } = await t.mockImport('../dist/esm/fix-eperm.js', { 135 | '../dist/esm/fs.js': { 136 | promises: { chmod }, 137 | chmodSync, 138 | }, 139 | }) as typeof import('../dist/esm/fix-eperm.js') 140 | const fixed = fixEPERM(asyncMethod) 141 | const fixedSync = fixEPERMSync(method) 142 | await t.rejects(fixed('async'), { code: 'EPERM' }) 143 | t.throws(() => fixedSync('sync'), { code: 'EPERM' }) 144 | t.equal(chmods.size, 2) 145 | t.equal(seen.size, 2) 146 | t.equal(finished.size, 0) 147 | }) 148 | -------------------------------------------------------------------------------- /src/rimraf-windows.ts: -------------------------------------------------------------------------------- 1 | // This is the same as rimrafPosix, with the following changes: 2 | // 3 | // 1. EBUSY, ENFILE, EMFILE trigger retries and/or exponential backoff 4 | // 2. All non-directories are removed first and then all directories are 5 | // removed in a second sweep. 6 | // 3. If we hit ENOTEMPTY in the second sweep, fall back to move-remove on 7 | // the that folder. 8 | // 9 | // Note: "move then remove" is 2-10 times slower, and just as unreliable. 10 | 11 | import { Dirent, Stats } from 'fs' 12 | import { parse, resolve } from 'path' 13 | import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js' 14 | import { fixEPERM, fixEPERMSync } from './fix-eperm.js' 15 | import { lstatSync, promises, rmdirSync, unlinkSync } from './fs.js' 16 | import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' 17 | import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' 18 | import { retryBusy, retryBusySync } from './retry-busy.js' 19 | import { rimrafMoveRemove, rimrafMoveRemoveSync } from './rimraf-move-remove.js' 20 | const { unlink, rmdir, lstat } = promises 21 | 22 | const rimrafWindowsFile = retryBusy(fixEPERM(unlink)) 23 | const rimrafWindowsFileSync = retryBusySync(fixEPERMSync(unlinkSync)) 24 | const rimrafWindowsDirRetry = retryBusy(fixEPERM(rmdir)) 25 | const rimrafWindowsDirRetrySync = retryBusySync(fixEPERMSync(rmdirSync)) 26 | 27 | const rimrafWindowsDirMoveRemoveFallback = async ( 28 | path: string, 29 | opt: RimrafAsyncOptions 30 | ): Promise => { 31 | /* c8 ignore start */ 32 | if (opt?.signal?.aborted) { 33 | throw opt.signal.reason 34 | } 35 | /* c8 ignore stop */ 36 | // already filtered, remove from options so we don't call unnecessarily 37 | const { filter, ...options } = opt 38 | try { 39 | return await rimrafWindowsDirRetry(path, options) 40 | } catch (er) { 41 | if ((er as NodeJS.ErrnoException)?.code === 'ENOTEMPTY') { 42 | return await rimrafMoveRemove(path, options) 43 | } 44 | throw er 45 | } 46 | } 47 | 48 | const rimrafWindowsDirMoveRemoveFallbackSync = ( 49 | path: string, 50 | opt: RimrafSyncOptions 51 | ): boolean => { 52 | if (opt?.signal?.aborted) { 53 | throw opt.signal.reason 54 | } 55 | // already filtered, remove from options so we don't call unnecessarily 56 | const { filter, ...options } = opt 57 | try { 58 | return rimrafWindowsDirRetrySync(path, options) 59 | } catch (er) { 60 | const fer = er as NodeJS.ErrnoException 61 | if (fer?.code === 'ENOTEMPTY') { 62 | return rimrafMoveRemoveSync(path, options) 63 | } 64 | throw er 65 | } 66 | } 67 | 68 | const START = Symbol('start') 69 | const CHILD = Symbol('child') 70 | const FINISH = Symbol('finish') 71 | 72 | export const rimrafWindows = async (path: string, opt: RimrafAsyncOptions) => { 73 | if (opt?.signal?.aborted) { 74 | throw opt.signal.reason 75 | } 76 | try { 77 | return await rimrafWindowsDir(path, opt, await lstat(path), START) 78 | } catch (er) { 79 | if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true 80 | throw er 81 | } 82 | } 83 | 84 | export const rimrafWindowsSync = (path: string, opt: RimrafSyncOptions) => { 85 | if (opt?.signal?.aborted) { 86 | throw opt.signal.reason 87 | } 88 | try { 89 | return rimrafWindowsDirSync(path, opt, lstatSync(path), START) 90 | } catch (er) { 91 | if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true 92 | throw er 93 | } 94 | } 95 | 96 | const rimrafWindowsDir = async ( 97 | path: string, 98 | opt: RimrafAsyncOptions, 99 | ent: Dirent | Stats, 100 | state = START 101 | ): Promise => { 102 | if (opt?.signal?.aborted) { 103 | throw opt.signal.reason 104 | } 105 | 106 | const entries = ent.isDirectory() ? await readdirOrError(path) : null 107 | if (!Array.isArray(entries)) { 108 | // this can only happen if lstat/readdir lied, or if the dir was 109 | // swapped out with a file at just the right moment. 110 | /* c8 ignore start */ 111 | if (entries) { 112 | if (entries.code === 'ENOENT') { 113 | return true 114 | } 115 | if (entries.code !== 'ENOTDIR') { 116 | throw entries 117 | } 118 | } 119 | /* c8 ignore stop */ 120 | if (opt.filter && !(await opt.filter(path, ent))) { 121 | return false 122 | } 123 | // is a file 124 | await ignoreENOENT(rimrafWindowsFile(path, opt)) 125 | return true 126 | } 127 | 128 | const s = state === START ? CHILD : state 129 | const removedAll = ( 130 | await Promise.all( 131 | entries.map(ent => rimrafWindowsDir(resolve(path, ent.name), opt, ent, s)) 132 | ) 133 | ).reduce((a, b) => a && b, true) 134 | 135 | if (state === START) { 136 | return rimrafWindowsDir(path, opt, ent, FINISH) 137 | } else if (state === FINISH) { 138 | if (opt.preserveRoot === false && path === parse(path).root) { 139 | return false 140 | } 141 | if (!removedAll) { 142 | return false 143 | } 144 | if (opt.filter && !(await opt.filter(path, ent))) { 145 | return false 146 | } 147 | await ignoreENOENT(rimrafWindowsDirMoveRemoveFallback(path, opt)) 148 | } 149 | return true 150 | } 151 | 152 | const rimrafWindowsDirSync = ( 153 | path: string, 154 | opt: RimrafSyncOptions, 155 | ent: Dirent | Stats, 156 | state = START 157 | ): boolean => { 158 | const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null 159 | if (!Array.isArray(entries)) { 160 | // this can only happen if lstat/readdir lied, or if the dir was 161 | // swapped out with a file at just the right moment. 162 | /* c8 ignore start */ 163 | if (entries) { 164 | if (entries.code === 'ENOENT') { 165 | return true 166 | } 167 | if (entries.code !== 'ENOTDIR') { 168 | throw entries 169 | } 170 | } 171 | /* c8 ignore stop */ 172 | if (opt.filter && !opt.filter(path, ent)) { 173 | return false 174 | } 175 | // is a file 176 | ignoreENOENTSync(() => rimrafWindowsFileSync(path, opt)) 177 | return true 178 | } 179 | 180 | let removedAll = true 181 | for (const ent of entries) { 182 | const s = state === START ? CHILD : state 183 | const p = resolve(path, ent.name) 184 | removedAll = rimrafWindowsDirSync(p, opt, ent, s) && removedAll 185 | } 186 | 187 | if (state === START) { 188 | return rimrafWindowsDirSync(path, opt, ent, FINISH) 189 | } else if (state === FINISH) { 190 | if (opt.preserveRoot === false && path === parse(path).root) { 191 | return false 192 | } 193 | if (!removedAll) { 194 | return false 195 | } 196 | if (opt.filter && !opt.filter(path, ent)) { 197 | return false 198 | } 199 | ignoreENOENTSync(() => { 200 | rimrafWindowsDirMoveRemoveFallbackSync(path, opt) 201 | }) 202 | } 203 | return true 204 | } 205 | -------------------------------------------------------------------------------- /tap-snapshots/test/rimraf-posix.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/rimraf-posix.ts > TAP > filter function > filter=i > async > paths seen 1`] = ` 9 | Array [ 10 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/a", 11 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/b", 12 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/d", 13 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/e", 14 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/g", 15 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/h", 16 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/i", 17 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/i/j", 18 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/i/k", 19 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/i/l", 20 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/i/m", 21 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/i/m/n", 22 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async/c/f/i/m/o", 23 | ] 24 | ` 25 | 26 | exports[`test/rimraf-posix.ts > TAP > filter function > filter=i > async filter > paths seen 1`] = ` 27 | Array [ 28 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/a", 29 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/b", 30 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/d", 31 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/e", 32 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/g", 33 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/h", 34 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/i", 35 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/i/j", 36 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/i/k", 37 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/i/l", 38 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/i/m", 39 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/i/m/n", 40 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-async-filter/c/f/i/m/o", 41 | ] 42 | ` 43 | 44 | exports[`test/rimraf-posix.ts > TAP > filter function > filter=i > sync > paths seen 1`] = ` 45 | Array [ 46 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/a", 47 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/b", 48 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/d", 49 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/e", 50 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/g", 51 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/h", 52 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/i", 53 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/i/j", 54 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/i/k", 55 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/i/l", 56 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/i/m", 57 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/i/m/n", 58 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-i-sync/c/f/i/m/o", 59 | ] 60 | ` 61 | 62 | exports[`test/rimraf-posix.ts > TAP > filter function > filter=j > async > paths seen 1`] = ` 63 | Array [ 64 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/a", 65 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/b", 66 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/d", 67 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/e", 68 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/g", 69 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/h", 70 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/i/j", 71 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/i/k", 72 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/i/l", 73 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/i/m", 74 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/i/m/n", 75 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async/c/f/i/m/o", 76 | ] 77 | ` 78 | 79 | exports[`test/rimraf-posix.ts > TAP > filter function > filter=j > async filter > paths seen 1`] = ` 80 | Array [ 81 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/a", 82 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/b", 83 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/d", 84 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/e", 85 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/g", 86 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/h", 87 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/i/j", 88 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/i/k", 89 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/i/l", 90 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/i/m", 91 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/i/m/n", 92 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-async-filter/c/f/i/m/o", 93 | ] 94 | ` 95 | 96 | exports[`test/rimraf-posix.ts > TAP > filter function > filter=j > sync > paths seen 1`] = ` 97 | Array [ 98 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/a", 99 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/b", 100 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/d", 101 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/e", 102 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/g", 103 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/h", 104 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/i/j", 105 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/i/k", 106 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/i/l", 107 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/i/m", 108 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/i/m/n", 109 | ".tap/fixtures/test-rimraf-posix.ts-filter-function-filter-j-sync/c/f/i/m/o", 110 | ] 111 | ` 112 | -------------------------------------------------------------------------------- /src/rimraf-move-remove.ts: -------------------------------------------------------------------------------- 1 | // https://youtu.be/uhRWMGBjlO8?t=537 2 | // 3 | // 1. readdir 4 | // 2. for each entry 5 | // a. if a non-empty directory, recurse 6 | // b. if an empty directory, move to random hidden file name in $TEMP 7 | // c. unlink/rmdir $TEMP 8 | // 9 | // This works around the fact that unlink/rmdir is non-atomic and takes 10 | // a non-deterministic amount of time to complete. 11 | // 12 | // However, it is HELLA SLOW, like 2-10x slower than a naive recursive rm. 13 | 14 | import { basename, parse, resolve } from 'path' 15 | import { defaultTmp, defaultTmpSync } from './default-tmp.js' 16 | 17 | import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' 18 | 19 | import { 20 | chmodSync, 21 | lstatSync, 22 | promises as fsPromises, 23 | renameSync, 24 | rmdirSync, 25 | unlinkSync, 26 | } from './fs.js' 27 | const { lstat, rename, unlink, rmdir, chmod } = fsPromises 28 | 29 | import { Dirent, Stats } from 'fs' 30 | import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js' 31 | import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' 32 | 33 | // crypto.randomBytes is much slower, and Math.random() is enough here 34 | const uniqueFilename = (path: string) => `.${basename(path)}.${Math.random()}` 35 | 36 | const unlinkFixEPERM = async (path: string) => 37 | unlink(path).catch((er: Error & { code?: string }) => { 38 | if (er.code === 'EPERM') { 39 | return chmod(path, 0o666).then( 40 | () => unlink(path), 41 | er2 => { 42 | if (er2.code === 'ENOENT') { 43 | return 44 | } 45 | throw er 46 | } 47 | ) 48 | } else if (er.code === 'ENOENT') { 49 | return 50 | } 51 | throw er 52 | }) 53 | 54 | const unlinkFixEPERMSync = (path: string) => { 55 | try { 56 | unlinkSync(path) 57 | } catch (er) { 58 | if ((er as NodeJS.ErrnoException)?.code === 'EPERM') { 59 | try { 60 | return chmodSync(path, 0o666) 61 | } catch (er2) { 62 | if ((er2 as NodeJS.ErrnoException)?.code === 'ENOENT') { 63 | return 64 | } 65 | throw er 66 | } 67 | } else if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') { 68 | return 69 | } 70 | throw er 71 | } 72 | } 73 | 74 | export const rimrafMoveRemove = async ( 75 | path: string, 76 | opt: RimrafAsyncOptions 77 | ) => { 78 | if (opt?.signal?.aborted) { 79 | throw opt.signal.reason 80 | } 81 | try { 82 | return await rimrafMoveRemoveDir(path, opt, await lstat(path)) 83 | } catch (er) { 84 | if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true 85 | throw er 86 | } 87 | } 88 | 89 | const rimrafMoveRemoveDir = async ( 90 | path: string, 91 | opt: RimrafAsyncOptions, 92 | ent: Dirent | Stats 93 | ): Promise => { 94 | if (opt?.signal?.aborted) { 95 | throw opt.signal.reason 96 | } 97 | if (!opt.tmp) { 98 | return rimrafMoveRemoveDir( 99 | path, 100 | { ...opt, tmp: await defaultTmp(path) }, 101 | ent 102 | ) 103 | } 104 | if (path === opt.tmp && parse(path).root !== path) { 105 | throw new Error('cannot delete temp directory used for deletion') 106 | } 107 | 108 | const entries = ent.isDirectory() ? await readdirOrError(path) : null 109 | if (!Array.isArray(entries)) { 110 | // this can only happen if lstat/readdir lied, or if the dir was 111 | // swapped out with a file at just the right moment. 112 | /* c8 ignore start */ 113 | if (entries) { 114 | if (entries.code === 'ENOENT') { 115 | return true 116 | } 117 | if (entries.code !== 'ENOTDIR') { 118 | throw entries 119 | } 120 | } 121 | /* c8 ignore stop */ 122 | if (opt.filter && !(await opt.filter(path, ent))) { 123 | return false 124 | } 125 | await ignoreENOENT(tmpUnlink(path, opt.tmp, unlinkFixEPERM)) 126 | return true 127 | } 128 | 129 | const removedAll = ( 130 | await Promise.all( 131 | entries.map(ent => rimrafMoveRemoveDir(resolve(path, ent.name), opt, ent)) 132 | ) 133 | ).reduce((a, b) => a && b, true) 134 | if (!removedAll) { 135 | return false 136 | } 137 | 138 | // we don't ever ACTUALLY try to unlink /, because that can never work 139 | // but when preserveRoot is false, we could be operating on it. 140 | // No need to check if preserveRoot is not false. 141 | if (opt.preserveRoot === false && path === parse(path).root) { 142 | return false 143 | } 144 | if (opt.filter && !(await opt.filter(path, ent))) { 145 | return false 146 | } 147 | await ignoreENOENT(tmpUnlink(path, opt.tmp, rmdir)) 148 | return true 149 | } 150 | 151 | const tmpUnlink = async ( 152 | path: string, 153 | tmp: string, 154 | rm: (p: string) => Promise 155 | ) => { 156 | const tmpFile = resolve(tmp, uniqueFilename(path)) 157 | await rename(path, tmpFile) 158 | return await rm(tmpFile) 159 | } 160 | 161 | export const rimrafMoveRemoveSync = (path: string, opt: RimrafSyncOptions) => { 162 | if (opt?.signal?.aborted) { 163 | throw opt.signal.reason 164 | } 165 | try { 166 | return rimrafMoveRemoveDirSync(path, opt, lstatSync(path)) 167 | } catch (er) { 168 | if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true 169 | throw er 170 | } 171 | } 172 | 173 | const rimrafMoveRemoveDirSync = ( 174 | path: string, 175 | opt: RimrafSyncOptions, 176 | ent: Dirent | Stats 177 | ): boolean => { 178 | if (opt?.signal?.aborted) { 179 | throw opt.signal.reason 180 | } 181 | if (!opt.tmp) { 182 | return rimrafMoveRemoveDirSync( 183 | path, 184 | { ...opt, tmp: defaultTmpSync(path) }, 185 | ent 186 | ) 187 | } 188 | const tmp: string = opt.tmp 189 | 190 | if (path === opt.tmp && parse(path).root !== path) { 191 | throw new Error('cannot delete temp directory used for deletion') 192 | } 193 | 194 | const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null 195 | if (!Array.isArray(entries)) { 196 | // this can only happen if lstat/readdir lied, or if the dir was 197 | // swapped out with a file at just the right moment. 198 | /* c8 ignore start */ 199 | if (entries) { 200 | if (entries.code === 'ENOENT') { 201 | return true 202 | } 203 | if (entries.code !== 'ENOTDIR') { 204 | throw entries 205 | } 206 | } 207 | /* c8 ignore stop */ 208 | if (opt.filter && !opt.filter(path, ent)) { 209 | return false 210 | } 211 | ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, unlinkFixEPERMSync)) 212 | return true 213 | } 214 | 215 | let removedAll = true 216 | for (const ent of entries) { 217 | const p = resolve(path, ent.name) 218 | removedAll = rimrafMoveRemoveDirSync(p, opt, ent) && removedAll 219 | } 220 | if (!removedAll) { 221 | return false 222 | } 223 | if (opt.preserveRoot === false && path === parse(path).root) { 224 | return false 225 | } 226 | if (opt.filter && !opt.filter(path, ent)) { 227 | return false 228 | } 229 | ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, rmdirSync)) 230 | return true 231 | } 232 | 233 | const tmpUnlinkSync = ( 234 | path: string, 235 | tmp: string, 236 | rmSync: (p: string) => void 237 | ) => { 238 | const tmpFile = resolve(tmp, uniqueFilename(path)) 239 | renameSync(path, tmpFile) 240 | return rmSync(tmpFile) 241 | } 242 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { statSync } from 'fs' 2 | import {resolve} from 'path' 3 | import t from 'tap' 4 | import { 5 | RimrafAsyncOptions, 6 | RimrafOptions, 7 | RimrafSyncOptions, 8 | rimraf, 9 | rimrafSync 10 | } from '../src/index.js' 11 | 12 | import * as OPTARG from '../dist/esm/opt-arg.js' 13 | 14 | t.test('mocky unit tests to select the correct function', async t => { 15 | // don't mock rimrafManual, so we can test the platform switch 16 | const CALLS: any[] = [] 17 | let USE_NATIVE = true 18 | const mocks = { 19 | '../dist/esm/use-native.js': { 20 | useNative: (opt: RimrafOptions) => { 21 | CALLS.push(['useNative', opt]) 22 | return USE_NATIVE 23 | }, 24 | useNativeSync: (opt: RimrafOptions) => { 25 | CALLS.push(['useNativeSync', opt]) 26 | return USE_NATIVE 27 | }, 28 | }, 29 | '../dist/esm/path-arg.js': (path: string) => { 30 | CALLS.push(['pathArg', path]) 31 | return path 32 | }, 33 | '../dist/esm/opt-arg.js': { 34 | ...OPTARG, 35 | optArg: (opt: RimrafOptions) => { 36 | CALLS.push(['optArg', opt]) 37 | return opt 38 | }, 39 | optArgSync: (opt: RimrafOptions) => { 40 | CALLS.push(['optArg', opt]) 41 | return opt 42 | }, 43 | }, 44 | '../dist/esm/rimraf-posix.js': { 45 | rimrafPosix: async (path: string, opt: RimrafOptions) => { 46 | CALLS.push(['rimrafPosix', path, opt]) 47 | }, 48 | rimrafPosixSync: async (path: string, opt: RimrafOptions) => { 49 | CALLS.push(['rimrafPosixSync', path, opt]) 50 | }, 51 | }, 52 | '../dist/esm/rimraf-windows.js': { 53 | rimrafWindows: async (path: string, opt: RimrafOptions) => { 54 | CALLS.push(['rimrafWindows', path, opt]) 55 | }, 56 | rimrafWindowsSync: async (path: string, opt: RimrafOptions) => { 57 | CALLS.push(['rimrafWindowsSync', path, opt]) 58 | }, 59 | }, 60 | '../dist/esm/rimraf-native.js': { 61 | rimrafNative: async (path: string, opt: RimrafOptions) => { 62 | CALLS.push(['rimrafNative', path, opt]) 63 | }, 64 | rimrafNativeSync: async (path: string, opt: RimrafOptions) => { 65 | CALLS.push(['rimrafNativeSync', path, opt]) 66 | }, 67 | }, 68 | } 69 | process.env.__TESTING_RIMRAF_PLATFORM__ = 'posix' 70 | const { rimraf } = (await t.mockImport( 71 | '../dist/esm/index.js', 72 | mocks 73 | )) as typeof import('../dist/esm/index.js') 74 | 75 | t.afterEach(() => (CALLS.length = 0)) 76 | for (const useNative of [true, false]) { 77 | t.test(`main function, useNative=${useNative}`, t => { 78 | USE_NATIVE = useNative 79 | rimraf('path', { a: 1 } as unknown as RimrafAsyncOptions) 80 | rimraf.sync('path', { a: 2 } as unknown as RimrafSyncOptions) 81 | t.equal(rimraf.rimraf, rimraf) 82 | t.equal(rimraf.rimrafSync, rimraf.sync) 83 | t.matchSnapshot(CALLS) 84 | t.end() 85 | }) 86 | } 87 | 88 | t.test('manual', t => { 89 | rimraf.manual('path', { a: 3 } as unknown as RimrafAsyncOptions) 90 | rimraf.manual.sync('path', { a: 4 } as unknown as RimrafSyncOptions) 91 | t.equal(rimraf.manualSync, rimraf.manual.sync) 92 | t.matchSnapshot(CALLS) 93 | t.end() 94 | }) 95 | 96 | t.test('native', t => { 97 | rimraf.native('path', { a: 5 } as unknown as RimrafAsyncOptions) 98 | rimraf.native.sync('path', { a: 6 } as unknown as RimrafSyncOptions) 99 | t.equal(rimraf.nativeSync, rimraf.native.sync) 100 | t.matchSnapshot(CALLS) 101 | t.end() 102 | }) 103 | 104 | t.test('posix', t => { 105 | rimraf.posix('path', { a: 7 } as unknown as RimrafAsyncOptions) 106 | rimraf.posix.sync('path', { a: 8 } as unknown as RimrafSyncOptions) 107 | t.equal(rimraf.posixSync, rimraf.posix.sync) 108 | t.matchSnapshot(CALLS) 109 | t.end() 110 | }) 111 | 112 | t.test('windows', t => { 113 | rimraf.windows('path', { a: 9 } as unknown as RimrafAsyncOptions) 114 | rimraf.windows.sync('path', { a: 10 } as unknown as RimrafSyncOptions) 115 | t.equal(rimraf.windowsSync, rimraf.windows.sync) 116 | t.matchSnapshot(CALLS) 117 | t.end() 118 | }) 119 | 120 | t.end() 121 | }) 122 | 123 | t.test('actually delete some stuff', t => { 124 | const fixture = { 125 | a: 'a', 126 | b: 'b', 127 | c: { 128 | d: 'd', 129 | e: 'e', 130 | f: { 131 | g: 'g', 132 | h: 'h', 133 | i: { 134 | j: 'j', 135 | k: 'k', 136 | l: 'l', 137 | m: { 138 | n: 'n', 139 | o: 'o', 140 | }, 141 | }, 142 | }, 143 | }, 144 | } 145 | t.test('sync', t => { 146 | const path = t.testdir(fixture) 147 | rimraf.sync(path) 148 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'deleted') 149 | t.end() 150 | }) 151 | t.test('async', async t => { 152 | const path = t.testdir(fixture) 153 | await rimraf(path) 154 | t.throws(() => statSync(path), { code: 'ENOENT' }) 155 | }) 156 | t.end() 157 | }) 158 | 159 | t.test('accept array of paths as first arg', async t => { 160 | const ASYNC_CALLS: any[] = [] 161 | const SYNC_CALLS: any[] = [] 162 | const { rimraf, rimrafSync } = (await t.mockImport('../dist/esm/index.js', { 163 | '../dist/esm/use-native.js': { 164 | useNative: () => true, 165 | useNativeSync: () => true, 166 | }, 167 | '../dist/esm/rimraf-native.js': { 168 | rimrafNative: async (path: string, opt: RimrafOptions) => 169 | ASYNC_CALLS.push([path, opt]), 170 | rimrafNativeSync: (path: string, opt: RimrafOptions) => 171 | SYNC_CALLS.push([path, opt]), 172 | }, 173 | })) as typeof import('../dist/esm/index.js') 174 | t.equal(await rimraf(['a', 'b', 'c']), true) 175 | t.equal( 176 | await rimraf(['i', 'j', 'k'], { x: 'ya' } as unknown as RimrafOptions), 177 | true 178 | ) 179 | t.same(ASYNC_CALLS, [ 180 | [resolve('a'), {}], 181 | [resolve('b'), {}], 182 | [resolve('c'), {}], 183 | [resolve('i'), { x: 'ya' }], 184 | [resolve('j'), { x: 'ya' }], 185 | [resolve('k'), { x: 'ya' }], 186 | ]) 187 | 188 | t.equal(rimrafSync(['x', 'y', 'z']), true) 189 | t.equal( 190 | rimrafSync(['m', 'n', 'o'], { 191 | cat: 'chai', 192 | } as unknown as RimrafSyncOptions), 193 | true 194 | ) 195 | t.same(SYNC_CALLS, [ 196 | [resolve('x'), {}], 197 | [resolve('y'), {}], 198 | [resolve('z'), {}], 199 | [resolve('m'), { cat: 'chai' }], 200 | [resolve('n'), { cat: 'chai' }], 201 | [resolve('o'), { cat: 'chai' }], 202 | ]) 203 | }) 204 | 205 | t.test('deleting globs', async t => { 206 | 207 | const fixture = { 208 | a: 'a', 209 | b: 'b', 210 | c: { 211 | d: 'd', 212 | e: 'e', 213 | f: { 214 | g: 'g', 215 | h: 'h', 216 | i: { 217 | j: 'j', 218 | k: 'k', 219 | l: 'l', 220 | m: { 221 | n: 'n', 222 | o: 'o', 223 | }, 224 | }, 225 | }, 226 | }, 227 | } 228 | 229 | t.test('sync', t => { 230 | const cwd = t.testdir(fixture) 231 | rimrafSync('**/f/**/m', { glob: { cwd } }) 232 | t.throws(() => statSync(cwd + '/c/f/i/m')) 233 | statSync(cwd + '/c/f/i/l') 234 | t.end() 235 | }) 236 | t.test('async', async t => { 237 | const cwd = t.testdir(fixture) 238 | await rimraf('**/f/**/m', { glob: { cwd } }) 239 | t.throws(() => statSync(cwd + '/c/f/i/m')) 240 | statSync(cwd + '/c/f/i/l') 241 | }) 242 | 243 | t.end() 244 | }) 245 | -------------------------------------------------------------------------------- /tap-snapshots/test/rimraf-windows.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/rimraf-windows.ts > TAP > filter function > filter=i > async > paths seen 1`] = ` 9 | Array [ 10 | Array [ 11 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/a", 12 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/b", 13 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/d", 14 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/e", 15 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/g", 16 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/h", 17 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/i", 18 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/i/j", 19 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/i/k", 20 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/i/l", 21 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/i/m", 22 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/i/m/n", 23 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async/c/f/i/m/o", 24 | ], 25 | ] 26 | ` 27 | 28 | exports[`test/rimraf-windows.ts > TAP > filter function > filter=i > async filter > paths seen 1`] = ` 29 | Array [ 30 | Array [ 31 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/a", 32 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/b", 33 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/d", 34 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/e", 35 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/g", 36 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/h", 37 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/i", 38 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/i/j", 39 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/i/k", 40 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/i/l", 41 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/i/m", 42 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/i/m/n", 43 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-async-filter/c/f/i/m/o", 44 | ], 45 | ] 46 | ` 47 | 48 | exports[`test/rimraf-windows.ts > TAP > filter function > filter=i > sync > paths seen 1`] = ` 49 | Array [ 50 | Array [ 51 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/a", 52 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/b", 53 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/d", 54 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/e", 55 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/g", 56 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/h", 57 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/i", 58 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/i/j", 59 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/i/k", 60 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/i/l", 61 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/i/m", 62 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/i/m/n", 63 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-i-sync/c/f/i/m/o", 64 | ], 65 | ] 66 | ` 67 | 68 | exports[`test/rimraf-windows.ts > TAP > filter function > filter=j > async > paths seen 1`] = ` 69 | Array [ 70 | Array [ 71 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/a", 72 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/b", 73 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/d", 74 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/e", 75 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/g", 76 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/h", 77 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/i/j", 78 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/i/j", 79 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/i/k", 80 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/i/l", 81 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/i/m", 82 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/i/m/n", 83 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async/c/f/i/m/o", 84 | ], 85 | ] 86 | ` 87 | 88 | exports[`test/rimraf-windows.ts > TAP > filter function > filter=j > async filter > paths seen 1`] = ` 89 | Array [ 90 | Array [ 91 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/a", 92 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/b", 93 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/d", 94 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/e", 95 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/g", 96 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/h", 97 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/i/j", 98 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/i/j", 99 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/i/k", 100 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/i/l", 101 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/i/m", 102 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/i/m/n", 103 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-async-filter/c/f/i/m/o", 104 | ], 105 | ] 106 | ` 107 | 108 | exports[`test/rimraf-windows.ts > TAP > filter function > filter=j > sync > paths seen 1`] = ` 109 | Array [ 110 | Array [ 111 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/a", 112 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/b", 113 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/d", 114 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/e", 115 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/g", 116 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/h", 117 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/i/j", 118 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/i/j", 119 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/i/k", 120 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/i/l", 121 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/i/m", 122 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/i/m/n", 123 | ".tap/fixtures/test-rimraf-windows.ts-filter-function-filter-j-sync/c/f/i/m/o", 124 | ], 125 | ] 126 | ` 127 | 128 | exports[`test/rimraf-windows.ts > TAP > handle EPERMs on unlink by trying to chmod 0o666 > async > chmods 1`] = ` 129 | 1 130 | ` 131 | 132 | exports[`test/rimraf-windows.ts > TAP > handle EPERMs on unlink by trying to chmod 0o666 > sync > chmods 1`] = ` 133 | 1 134 | ` 135 | 136 | exports[`test/rimraf-windows.ts > TAP > handle EPERMs, chmod raises something other than ENOENT > async > chmods 1`] = ` 137 | 0 138 | ` 139 | 140 | exports[`test/rimraf-windows.ts > TAP > handle EPERMs, chmod raises something other than ENOENT > sync > chmods 1`] = ` 141 | 1 142 | ` 143 | 144 | exports[`test/rimraf-windows.ts > TAP > handle EPERMs, chmod returns ENOENT > async > chmods 1`] = ` 145 | 1 146 | ` 147 | 148 | exports[`test/rimraf-windows.ts > TAP > handle EPERMs, chmod returns ENOENT > sync > chmods 1`] = ` 149 | 1 150 | ` 151 | -------------------------------------------------------------------------------- /tap-snapshots/test/rimraf-move-remove.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/rimraf-move-remove.ts > TAP > filter function > filter=i > async > paths seen 1`] = ` 9 | Array [ 10 | Array [ 11 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/a", 12 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/b", 13 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/d", 14 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/e", 15 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/g", 16 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/h", 17 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/i", 18 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/i/j", 19 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/i/k", 20 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/i/l", 21 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/i/m", 22 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/i/m/n", 23 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async/c/f/i/m/o", 24 | ], 25 | ] 26 | ` 27 | 28 | exports[`test/rimraf-move-remove.ts > TAP > filter function > filter=i > async filter > paths seen 1`] = ` 29 | Array [ 30 | Array [ 31 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/a", 32 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/b", 33 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/d", 34 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/e", 35 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/g", 36 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/h", 37 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/i", 38 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/i/j", 39 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/i/k", 40 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/i/l", 41 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/i/m", 42 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/i/m/n", 43 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-async-filter/c/f/i/m/o", 44 | ], 45 | ] 46 | ` 47 | 48 | exports[`test/rimraf-move-remove.ts > TAP > filter function > filter=i > sync > paths seen 1`] = ` 49 | Array [ 50 | Array [ 51 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/a", 52 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/b", 53 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/d", 54 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/e", 55 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/g", 56 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/h", 57 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/i", 58 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/i/j", 59 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/i/k", 60 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/i/l", 61 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/i/m", 62 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/i/m/n", 63 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-i-sync/c/f/i/m/o", 64 | ], 65 | ] 66 | ` 67 | 68 | exports[`test/rimraf-move-remove.ts > TAP > filter function > filter=j > async > paths seen 1`] = ` 69 | Array [ 70 | Array [ 71 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/a", 72 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/b", 73 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/d", 74 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/e", 75 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/g", 76 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/h", 77 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/i/j", 78 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/i/k", 79 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/i/l", 80 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/i/m", 81 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/i/m/n", 82 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async/c/f/i/m/o", 83 | ], 84 | ] 85 | ` 86 | 87 | exports[`test/rimraf-move-remove.ts > TAP > filter function > filter=j > async filter > paths seen 1`] = ` 88 | Array [ 89 | Array [ 90 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/a", 91 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/b", 92 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/d", 93 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/e", 94 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/g", 95 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/h", 96 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/i/j", 97 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/i/k", 98 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/i/l", 99 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/i/m", 100 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/i/m/n", 101 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-async-filter/c/f/i/m/o", 102 | ], 103 | ] 104 | ` 105 | 106 | exports[`test/rimraf-move-remove.ts > TAP > filter function > filter=j > sync > paths seen 1`] = ` 107 | Array [ 108 | Array [ 109 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/a", 110 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/b", 111 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/d", 112 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/e", 113 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/g", 114 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/h", 115 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/i/j", 116 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/i/k", 117 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/i/l", 118 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/i/m", 119 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/i/m/n", 120 | ".tap/fixtures/test-rimraf-move-remove.ts-filter-function-filter-j-sync/c/f/i/m/o", 121 | ], 122 | ] 123 | ` 124 | 125 | exports[`test/rimraf-move-remove.ts > TAP > handle EPERMs on unlink by trying to chmod 0o666 > async > must match snapshot 1`] = ` 126 | Array [ 127 | Array [ 128 | "chmod", 129 | "{tmpfile}", 130 | "438", 131 | ], 132 | ] 133 | ` 134 | 135 | exports[`test/rimraf-move-remove.ts > TAP > handle EPERMs on unlink by trying to chmod 0o666 > sync > must match snapshot 1`] = ` 136 | Array [ 137 | Array [ 138 | "chmodSync", 139 | "{tmpfile}", 140 | "438", 141 | ], 142 | ] 143 | ` 144 | 145 | exports[`test/rimraf-move-remove.ts > TAP > handle EPERMs, chmod raises something other than ENOENT > async > must match snapshot 1`] = ` 146 | Array [] 147 | ` 148 | 149 | exports[`test/rimraf-move-remove.ts > TAP > handle EPERMs, chmod raises something other than ENOENT > sync > must match snapshot 1`] = ` 150 | Array [ 151 | Array [ 152 | "chmodSync", 153 | "{tmpfile}", 154 | "438", 155 | ], 156 | ] 157 | ` 158 | 159 | exports[`test/rimraf-move-remove.ts > TAP > handle EPERMs, chmod returns ENOENT > async > must match snapshot 1`] = ` 160 | Array [ 161 | Array [ 162 | "chmod", 163 | "{tmpfile}", 164 | "438", 165 | ], 166 | ] 167 | ` 168 | 169 | exports[`test/rimraf-move-remove.ts > TAP > handle EPERMs, chmod returns ENOENT > sync > must match snapshot 1`] = ` 170 | Array [ 171 | Array [ 172 | "chmodSync", 173 | "{tmpfile}", 174 | "438", 175 | ], 176 | ] 177 | ` 178 | -------------------------------------------------------------------------------- /src/bin.mts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { readFile } from 'fs/promises' 3 | import type { RimrafAsyncOptions } from './index.js' 4 | import { rimraf } from './index.js' 5 | 6 | const pj = fileURLToPath(new URL('../package.json', import.meta.url)) 7 | const pjDist = fileURLToPath(new URL('../../package.json', import.meta.url)) 8 | const { version } = JSON.parse( 9 | await readFile(pjDist, 'utf8').catch(() => readFile(pj, 'utf8')) 10 | ) as { version: string } 11 | 12 | const runHelpForUsage = () => 13 | console.error('run `rimraf --help` for usage information') 14 | 15 | export const help = `rimraf version ${version} 16 | 17 | Usage: rimraf [ ...] 18 | Deletes all files and folders at "path", recursively. 19 | 20 | Options: 21 | -- Treat all subsequent arguments as paths 22 | -h --help Display this usage info 23 | --preserve-root Do not remove '/' recursively (default) 24 | --no-preserve-root Do not treat '/' specially 25 | -G --no-glob Treat arguments as literal paths, not globs (default) 26 | -g --glob Treat arguments as glob patterns 27 | -v --verbose Be verbose when deleting files, showing them as 28 | they are removed. Not compatible with --impl=native 29 | -V --no-verbose Be silent when deleting files, showing nothing as 30 | they are removed (default) 31 | -i --interactive Ask for confirmation before deleting anything 32 | Not compatible with --impl=native 33 | -I --no-interactive Do not ask for confirmation before deleting 34 | 35 | --impl= Specify the implementation to use: 36 | rimraf: choose the best option (default) 37 | native: the built-in implementation in Node.js 38 | manual: the platform-specific JS implementation 39 | posix: the Posix JS implementation 40 | windows: the Windows JS implementation (falls back to 41 | move-remove on ENOTEMPTY) 42 | move-remove: a slow reliable Windows fallback 43 | 44 | Implementation-specific options: 45 | --tmp= Temp file folder for 'move-remove' implementation 46 | --max-retries= maxRetries for 'native' and 'windows' implementations 47 | --retry-delay= retryDelay for 'native' implementation, default 100 48 | --backoff= Exponential backoff factor for retries (default: 1.2) 49 | ` 50 | 51 | import { parse, relative, resolve } from 'path' 52 | const cwd = process.cwd() 53 | 54 | import { Dirent, Stats } from 'fs' 55 | import { createInterface, Interface } from 'readline' 56 | import { fileURLToPath } from 'url' 57 | 58 | const prompt = async (rl: Interface, q: string) => 59 | new Promise(res => rl.question(q, res)) 60 | 61 | const interactiveRimraf = async ( 62 | impl: (path: string | string[], opt?: RimrafAsyncOptions) => Promise, 63 | paths: string[], 64 | opt: RimrafAsyncOptions 65 | ) => { 66 | const existingFilter = opt.filter || (() => true) 67 | let allRemaining = false 68 | let noneRemaining = false 69 | const queue: (() => Promise)[] = [] 70 | let processing = false 71 | const processQueue = async () => { 72 | if (processing) return 73 | processing = true 74 | let next: (() => Promise) | undefined 75 | while ((next = queue.shift())) { 76 | await next() 77 | } 78 | processing = false 79 | } 80 | const oneAtATime = 81 | (fn: (s: string, e: Dirent | Stats) => Promise) => 82 | async (s: string, e: Dirent | Stats): Promise => { 83 | const p = new Promise(res => { 84 | queue.push(async () => { 85 | const result = await fn(s, e) 86 | res(result) 87 | return result 88 | }) 89 | }) 90 | processQueue() 91 | return p 92 | } 93 | const rl = createInterface({ 94 | input: process.stdin, 95 | output: process.stdout, 96 | }) 97 | opt.filter = oneAtATime( 98 | async (path: string, ent: Dirent | Stats): Promise => { 99 | if (noneRemaining) { 100 | return false 101 | } 102 | while (!allRemaining) { 103 | const a = ( 104 | await prompt(rl, `rm? ${relative(cwd, path)}\n[(Yes)/No/All/Quit] > `) 105 | ).trim() 106 | if (/^n/i.test(a)) { 107 | return false 108 | } else if (/^a/i.test(a)) { 109 | allRemaining = true 110 | break 111 | } else if (/^q/i.test(a)) { 112 | noneRemaining = true 113 | return false 114 | } else if (a === '' || /^y/i.test(a)) { 115 | break 116 | } else { 117 | continue 118 | } 119 | } 120 | return existingFilter(path, ent) 121 | } 122 | ) 123 | await impl(paths, opt) 124 | rl.close() 125 | } 126 | 127 | const main = async (...args: string[]) => { 128 | const verboseFilter = (s: string) => { 129 | console.log(relative(cwd, s)) 130 | return true 131 | } 132 | 133 | if (process.env.__RIMRAF_TESTING_BIN_FAIL__ === '1') { 134 | throw new Error('simulated rimraf failure') 135 | } 136 | 137 | const opt: RimrafAsyncOptions = {} 138 | const paths: string[] = [] 139 | let dashdash = false 140 | let impl: ( 141 | path: string | string[], 142 | opt?: RimrafAsyncOptions 143 | ) => Promise = rimraf 144 | 145 | let interactive = false 146 | 147 | for (const arg of args) { 148 | if (dashdash) { 149 | paths.push(arg) 150 | continue 151 | } 152 | if (arg === '--') { 153 | dashdash = true 154 | continue 155 | } else if (arg === '-rf' || arg === '-fr') { 156 | // this never did anything, but people put it there I guess 157 | continue 158 | } else if (arg === '-h' || arg === '--help') { 159 | console.log(help) 160 | return 0 161 | } else if (arg === '--interactive' || arg === '-i') { 162 | interactive = true 163 | continue 164 | } else if (arg === '--no-interactive' || arg === '-I') { 165 | interactive = false 166 | continue 167 | } else if (arg === '--verbose' || arg === '-v') { 168 | opt.filter = verboseFilter 169 | continue 170 | } else if (arg === '--no-verbose' || arg === '-V') { 171 | opt.filter = undefined 172 | continue 173 | } else if (arg === '-g' || arg === '--glob') { 174 | opt.glob = true 175 | continue 176 | } else if (arg === '-G' || arg === '--no-glob') { 177 | opt.glob = false 178 | continue 179 | } else if (arg === '--preserve-root') { 180 | opt.preserveRoot = true 181 | continue 182 | } else if (arg === '--no-preserve-root') { 183 | opt.preserveRoot = false 184 | continue 185 | } else if (/^--tmp=/.test(arg)) { 186 | const val = arg.substring('--tmp='.length) 187 | opt.tmp = val 188 | continue 189 | } else if (/^--max-retries=/.test(arg)) { 190 | const val = +arg.substring('--max-retries='.length) 191 | opt.maxRetries = val 192 | continue 193 | } else if (/^--retry-delay=/.test(arg)) { 194 | const val = +arg.substring('--retry-delay='.length) 195 | opt.retryDelay = val 196 | continue 197 | } else if (/^--backoff=/.test(arg)) { 198 | const val = +arg.substring('--backoff='.length) 199 | opt.backoff = val 200 | continue 201 | } else if (/^--impl=/.test(arg)) { 202 | const val = arg.substring('--impl='.length) 203 | switch (val) { 204 | case 'rimraf': 205 | impl = rimraf 206 | continue 207 | case 'native': 208 | case 'manual': 209 | case 'posix': 210 | case 'windows': 211 | impl = rimraf[val] 212 | continue 213 | case 'move-remove': 214 | impl = rimraf.moveRemove 215 | continue 216 | default: 217 | console.error(`unknown implementation: ${val}`) 218 | runHelpForUsage() 219 | return 1 220 | } 221 | } else if (/^-/.test(arg)) { 222 | console.error(`unknown option: ${arg}`) 223 | runHelpForUsage() 224 | return 1 225 | } else { 226 | paths.push(arg) 227 | } 228 | } 229 | 230 | if (opt.preserveRoot !== false) { 231 | for (const path of paths.map(p => resolve(p))) { 232 | if (path === parse(path).root) { 233 | console.error(`rimraf: it is dangerous to operate recursively on '/'`) 234 | console.error('use --no-preserve-root to override this failsafe') 235 | return 1 236 | } 237 | } 238 | } 239 | 240 | if (!paths.length) { 241 | console.error('rimraf: must provide a path to remove') 242 | runHelpForUsage() 243 | return 1 244 | } 245 | 246 | if (impl === rimraf.native && (interactive || opt.filter)) { 247 | console.error('native implementation does not support -v or -i') 248 | runHelpForUsage() 249 | return 1 250 | } 251 | 252 | if (interactive) { 253 | await interactiveRimraf(impl, paths, opt) 254 | } else { 255 | await impl(paths, opt) 256 | } 257 | 258 | return 0 259 | } 260 | main.help = help 261 | 262 | export default main 263 | 264 | if (process.env.__TESTING_RIMRAF_BIN__ !== '1') { 265 | const args = process.argv.slice(2) 266 | main(...args).then( 267 | code => process.exit(code), 268 | er => { 269 | console.error(er) 270 | process.exit(1) 271 | } 272 | ) 273 | } 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The [UNIX command]() `rm -rf` for node. 2 | 3 | Install with `npm install rimraf`. 4 | 5 | ## Major Changes 6 | 7 | ### v4 to v5 8 | 9 | - There is no default export anymore. Import the functions directly 10 | using, e.g., `import { rimrafSync } from 'rimraf`. 11 | 12 | ### v3 to v4 13 | 14 | - The function returns a `Promise` instead of taking a callback. 15 | - Globbing requires the `--glob` CLI option or `glob` option property 16 | to be set. (Removed in 4.0 and 4.1, opt-in support added in 4.2.) 17 | - Functions take arrays of paths, as well as a single path. 18 | - Native implementation used by default when available, except on 19 | Windows, where this implementation is faster and more reliable. 20 | - New implementation on Windows, falling back to "move then 21 | remove" strategy when exponential backoff for `EBUSY` fails to 22 | resolve the situation. 23 | - Simplified implementation on Posix, since the Windows 24 | affordances are not necessary there. 25 | - As of 4.3, return/resolve value is boolean instead of undefined 26 | 27 | ## API 28 | 29 | Hybrid module, load either with `import` or `require()`. 30 | 31 | ```js 32 | // 'rimraf' export is the one you probably want, but other 33 | // strategies exported as well. 34 | import { rimraf, rimrafSync, native, nativeSync } from 'rimraf' 35 | // or 36 | const { rimraf, rimrafSync, native, nativeSync } = require('rimraf') 37 | ``` 38 | 39 | All removal functions return a boolean indicating that all 40 | entries were successfully removed. 41 | 42 | The only case in which this will not return `true` is if 43 | something was omitted from the removal via a `filter` option. 44 | 45 | ### `rimraf(f, [opts]) -> Promise` 46 | 47 | This first parameter is a path or array of paths. The second 48 | argument is an options object. 49 | 50 | Options: 51 | 52 | - `preserveRoot`: If set to boolean `false`, then allow the 53 | recursive removal of the root directory. Otherwise, this is 54 | not allowed. 55 | - `tmp`: Windows only. Temp folder to use to place files and 56 | folders for the "move then remove" fallback. Must be on the 57 | same physical device as the path being deleted. Defaults to 58 | `os.tmpdir()` when that is on the same drive letter as the path 59 | being deleted, or `${drive}:\temp` if present, or `${drive}:\` 60 | if not. 61 | - `maxRetries`: Windows and Native only. Maximum number of 62 | retry attempts in case of `EBUSY`, `EMFILE`, and `ENFILE` 63 | errors. Default `10` for Windows implementation, `0` for Native 64 | implementation. 65 | - `backoff`: Windows only. Rate of exponential backoff for async 66 | removal in case of `EBUSY`, `EMFILE`, and `ENFILE` errors. 67 | Should be a number greater than 1. Default `1.2` 68 | - `maxBackoff`: Windows only. Maximum total backoff time in ms to 69 | attempt asynchronous retries in case of `EBUSY`, `EMFILE`, and 70 | `ENFILE` errors. Default `200`. With the default `1.2` backoff 71 | rate, this results in 14 retries, with the final retry being 72 | delayed 33ms. 73 | - `retryDelay`: Native only. Time to wait between retries, using 74 | linear backoff. Default `100`. 75 | - `signal` Pass in an AbortSignal to cancel the directory 76 | removal. This is useful when removing large folder structures, 77 | if you'd like to limit the amount of time spent. 78 | 79 | Using a `signal` option prevents the use of Node's built-in 80 | `fs.rm` because that implementation does not support abort 81 | signals. 82 | 83 | - `glob` Boolean flag to treat path as glob pattern, or an object 84 | specifying [`glob` options](https://github.com/isaacs/node-glob). 85 | - `filter` Method that returns a boolean indicating whether that 86 | path should be deleted. With async rimraf methods, this may 87 | return a Promise that resolves to a boolean. (Since Promises 88 | are truthy, returning a Promise from a sync filter is the same 89 | as just not filtering anything.) 90 | 91 | The first argument to the filter is the path string. The 92 | second argument is either a `Dirent` or `Stats` object for that 93 | path. (The first path explored will be a `Stats`, the rest 94 | will be `Dirent`.) 95 | 96 | If a filter method is provided, it will _only_ remove entries 97 | if the filter returns (or resolves to) a truthy value. Omitting 98 | a directory will still allow its children to be removed, unless 99 | they are also filtered out, but any parents of a filtered entry 100 | will not be removed, since the directory would not be empty in 101 | that case. 102 | 103 | Using a filter method prevents the use of Node's built-in 104 | `fs.rm` because that implementation does not support filtering. 105 | 106 | Any other options are provided to the native Node.js `fs.rm` implementation 107 | when that is used. 108 | 109 | This will attempt to choose the best implementation, based on Node.js 110 | version and `process.platform`. To force a specific implementation, use 111 | one of the other functions provided. 112 | 113 | ### `rimraf.sync(f, [opts])` `rimraf.rimrafSync(f, [opts])` 114 | 115 | Synchronous form of `rimraf()` 116 | 117 | Note that, unlike many file system operations, the synchronous form will 118 | typically be significantly _slower_ than the async form, because recursive 119 | deletion is extremely parallelizable. 120 | 121 | ### `rimraf.native(f, [opts])` 122 | 123 | Uses the built-in `fs.rm` implementation that Node.js provides. This is 124 | used by default on Node.js versions greater than or equal to `14.14.0`. 125 | 126 | ### `rimraf.nativeSync(f, [opts])` `rimraf.native.sync(f, [opts])` 127 | 128 | Synchronous form of `rimraf.native` 129 | 130 | ### `rimraf.manual(f, [opts])` 131 | 132 | Use the JavaScript implementation appropriate for your operating system. 133 | 134 | ### `rimraf.manualSync(f, [opts])` `rimraf.manualSync(f, opts)` 135 | 136 | Synchronous form of `rimraf.manual()` 137 | 138 | ### `rimraf.windows(f, [opts])` 139 | 140 | JavaScript implementation of file removal appropriate for Windows 141 | platforms. Works around `unlink` and `rmdir` not being atomic 142 | operations, and `EPERM` when deleting files with certain 143 | permission modes. 144 | 145 | First deletes all non-directory files within the tree, and then 146 | removes all directories, which should ideally be empty by that 147 | time. When an `ENOTEMPTY` is raised in the second pass, falls 148 | back to the `rimraf.moveRemove` strategy as needed. 149 | 150 | ### `rimraf.windows.sync(path, [opts])` `rimraf.windowsSync(path, [opts])` 151 | 152 | Synchronous form of `rimraf.windows()` 153 | 154 | ### `rimraf.moveRemove(path, [opts])` 155 | 156 | Moves all files and folders to the parent directory of `path` 157 | with a temporary filename prior to attempting to remove them. 158 | 159 | Note that, in cases where the operation fails, this _may_ leave 160 | files lying around in the parent directory with names like 161 | `.file-basename.txt.0.123412341`. Until the Windows kernel 162 | provides a way to perform atomic `unlink` and `rmdir` operations, 163 | this is unfortunately unavoidable. 164 | 165 | To move files to a different temporary directory other than the 166 | parent, provide `opts.tmp`. Note that this _must_ be on the same 167 | physical device as the folder being deleted, or else the 168 | operation will fail. 169 | 170 | This is the slowest strategy, but most reliable on Windows 171 | platforms. Used as a last-ditch fallback by `rimraf.windows()`. 172 | 173 | ### `rimraf.moveRemove.sync(path, [opts])` `rimraf.moveRemoveSync(path, [opts])` 174 | 175 | Synchronous form of `rimraf.moveRemove()` 176 | 177 | ### Command Line Interface 178 | 179 | ``` 180 | rimraf version 4.3.0 181 | 182 | Usage: rimraf [ ...] 183 | Deletes all files and folders at "path", recursively. 184 | 185 | Options: 186 | -- Treat all subsequent arguments as paths 187 | -h --help Display this usage info 188 | --preserve-root Do not remove '/' recursively (default) 189 | --no-preserve-root Do not treat '/' specially 190 | -G --no-glob Treat arguments as literal paths, not globs (default) 191 | -g --glob Treat arguments as glob patterns 192 | -v --verbose Be verbose when deleting files, showing them as 193 | they are removed. Not compatible with --impl=native 194 | -V --no-verbose Be silent when deleting files, showing nothing as 195 | they are removed (default) 196 | -i --interactive Ask for confirmation before deleting anything 197 | Not compatible with --impl=native 198 | -I --no-interactive Do not ask for confirmation before deleting 199 | 200 | --impl= Specify the implementation to use: 201 | rimraf: choose the best option (default) 202 | native: the built-in implementation in Node.js 203 | manual: the platform-specific JS implementation 204 | posix: the Posix JS implementation 205 | windows: the Windows JS implementation (falls back to 206 | move-remove on ENOTEMPTY) 207 | move-remove: a slow reliable Windows fallback 208 | 209 | Implementation-specific options: 210 | --tmp= Temp file folder for 'move-remove' implementation 211 | --max-retries= maxRetries for 'native' and 'windows' implementations 212 | --retry-delay= retryDelay for 'native' implementation, default 100 213 | --backoff= Exponential backoff factor for retries (default: 1.2) 214 | ``` 215 | 216 | ## mkdirp 217 | 218 | If you need to _create_ a directory recursively, check out 219 | [mkdirp](https://github.com/isaacs/node-mkdirp). 220 | -------------------------------------------------------------------------------- /benchmark/old-rimraf/rimraf.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert") 2 | const path = require("path") 3 | const fs = require("fs") 4 | let glob = undefined 5 | try { 6 | glob = require("glob") 7 | } catch (_err) { 8 | // treat glob as optional. 9 | } 10 | 11 | const defaultGlobOpts = { 12 | nosort: true, 13 | silent: true 14 | } 15 | 16 | // for EMFILE handling 17 | let timeout = 0 18 | 19 | const isWindows = (process.platform === "win32") 20 | 21 | const defaults = options => { 22 | const methods = [ 23 | 'unlink', 24 | 'chmod', 25 | 'stat', 26 | 'lstat', 27 | 'rmdir', 28 | 'readdir' 29 | ] 30 | methods.forEach(m => { 31 | options[m] = options[m] || fs[m] 32 | m = m + 'Sync' 33 | options[m] = options[m] || fs[m] 34 | }) 35 | 36 | options.maxBusyTries = options.maxBusyTries || 3 37 | options.emfileWait = options.emfileWait || 1000 38 | if (options.glob === false) { 39 | options.disableGlob = true 40 | } 41 | if (options.disableGlob !== true && glob === undefined) { 42 | throw Error('glob dependency not found, set `options.disableGlob = true` if intentional') 43 | } 44 | options.disableGlob = options.disableGlob || false 45 | options.glob = options.glob || defaultGlobOpts 46 | } 47 | 48 | const rimraf = (p, options, cb) => { 49 | if (typeof options === 'function') { 50 | cb = options 51 | options = {} 52 | } 53 | 54 | assert(p, 'rimraf: missing path') 55 | assert.equal(typeof p, 'string', 'rimraf: path should be a string') 56 | assert.equal(typeof cb, 'function', 'rimraf: callback function required') 57 | assert(options, 'rimraf: invalid options argument provided') 58 | assert.equal(typeof options, 'object', 'rimraf: options should be object') 59 | 60 | defaults(options) 61 | 62 | let busyTries = 0 63 | let errState = null 64 | let n = 0 65 | 66 | const next = (er) => { 67 | errState = errState || er 68 | if (--n === 0) 69 | cb(errState) 70 | } 71 | 72 | const afterGlob = (er, results) => { 73 | if (er) 74 | return cb(er) 75 | 76 | n = results.length 77 | if (n === 0) 78 | return cb() 79 | 80 | results.forEach(p => { 81 | const CB = (er) => { 82 | if (er) { 83 | if ((er.code === "EBUSY" || er.code === "ENOTEMPTY" || er.code === "EPERM") && 84 | busyTries < options.maxBusyTries) { 85 | busyTries ++ 86 | // try again, with the same exact callback as this one. 87 | return setTimeout(() => rimraf_(p, options, CB), busyTries * 100) 88 | } 89 | 90 | // this one won't happen if graceful-fs is used. 91 | if (er.code === "EMFILE" && timeout < options.emfileWait) { 92 | return setTimeout(() => rimraf_(p, options, CB), timeout ++) 93 | } 94 | 95 | // already gone 96 | if (er.code === "ENOENT") er = null 97 | } 98 | 99 | timeout = 0 100 | next(er) 101 | } 102 | rimraf_(p, options, CB) 103 | }) 104 | } 105 | 106 | if (options.disableGlob || !glob.hasMagic(p)) 107 | return afterGlob(null, [p]) 108 | 109 | options.lstat(p, (er, stat) => { 110 | if (!er) 111 | return afterGlob(null, [p]) 112 | 113 | glob(p, options.glob, afterGlob) 114 | }) 115 | 116 | } 117 | 118 | // Two possible strategies. 119 | // 1. Assume it's a file. unlink it, then do the dir stuff on EPERM or EISDIR 120 | // 2. Assume it's a directory. readdir, then do the file stuff on ENOTDIR 121 | // 122 | // Both result in an extra syscall when you guess wrong. However, there 123 | // are likely far more normal files in the world than directories. This 124 | // is based on the assumption that a the average number of files per 125 | // directory is >= 1. 126 | // 127 | // If anyone ever complains about this, then I guess the strategy could 128 | // be made configurable somehow. But until then, YAGNI. 129 | const rimraf_ = (p, options, cb) => { 130 | assert(p) 131 | assert(options) 132 | assert(typeof cb === 'function') 133 | 134 | // sunos lets the root user unlink directories, which is... weird. 135 | // so we have to lstat here and make sure it's not a dir. 136 | options.lstat(p, (er, st) => { 137 | if (er && er.code === "ENOENT") 138 | return cb(null) 139 | 140 | // Windows can EPERM on stat. Life is suffering. 141 | if (er && er.code === "EPERM" && isWindows) 142 | fixWinEPERM(p, options, er, cb) 143 | 144 | if (st && st.isDirectory()) 145 | return rmdir(p, options, er, cb) 146 | 147 | options.unlink(p, er => { 148 | if (er) { 149 | if (er.code === "ENOENT") 150 | return cb(null) 151 | if (er.code === "EPERM") 152 | return (isWindows) 153 | ? fixWinEPERM(p, options, er, cb) 154 | : rmdir(p, options, er, cb) 155 | if (er.code === "EISDIR") 156 | return rmdir(p, options, er, cb) 157 | } 158 | return cb(er) 159 | }) 160 | }) 161 | } 162 | 163 | const fixWinEPERM = (p, options, er, cb) => { 164 | assert(p) 165 | assert(options) 166 | assert(typeof cb === 'function') 167 | 168 | options.chmod(p, 0o666, er2 => { 169 | if (er2) 170 | cb(er2.code === "ENOENT" ? null : er) 171 | else 172 | options.stat(p, (er3, stats) => { 173 | if (er3) 174 | cb(er3.code === "ENOENT" ? null : er) 175 | else if (stats.isDirectory()) 176 | rmdir(p, options, er, cb) 177 | else 178 | options.unlink(p, cb) 179 | }) 180 | }) 181 | } 182 | 183 | const fixWinEPERMSync = (p, options, er) => { 184 | assert(p) 185 | assert(options) 186 | 187 | try { 188 | options.chmodSync(p, 0o666) 189 | } catch (er2) { 190 | if (er2.code === "ENOENT") 191 | return 192 | else 193 | throw er 194 | } 195 | 196 | let stats 197 | try { 198 | stats = options.statSync(p) 199 | } catch (er3) { 200 | if (er3.code === "ENOENT") 201 | return 202 | else 203 | throw er 204 | } 205 | 206 | if (stats.isDirectory()) 207 | rmdirSync(p, options, er) 208 | else 209 | options.unlinkSync(p) 210 | } 211 | 212 | const rmdir = (p, options, originalEr, cb) => { 213 | assert(p) 214 | assert(options) 215 | assert(typeof cb === 'function') 216 | 217 | // try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS) 218 | // if we guessed wrong, and it's not a directory, then 219 | // raise the original error. 220 | options.rmdir(p, er => { 221 | if (er && (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM")) 222 | rmkids(p, options, cb) 223 | else if (er && er.code === "ENOTDIR") 224 | cb(originalEr) 225 | else 226 | cb(er) 227 | }) 228 | } 229 | 230 | const rmkids = (p, options, cb) => { 231 | assert(p) 232 | assert(options) 233 | assert(typeof cb === 'function') 234 | 235 | options.readdir(p, (er, files) => { 236 | if (er) 237 | return cb(er) 238 | let n = files.length 239 | if (n === 0) 240 | return options.rmdir(p, cb) 241 | let errState 242 | files.forEach(f => { 243 | rimraf(path.join(p, f), options, er => { 244 | if (errState) 245 | return 246 | if (er) 247 | return cb(errState = er) 248 | if (--n === 0) 249 | options.rmdir(p, cb) 250 | }) 251 | }) 252 | }) 253 | } 254 | 255 | // this looks simpler, and is strictly *faster*, but will 256 | // tie up the JavaScript thread and fail on excessively 257 | // deep directory trees. 258 | const rimrafSync = (p, options) => { 259 | options = options || {} 260 | defaults(options) 261 | 262 | assert(p, 'rimraf: missing path') 263 | assert.equal(typeof p, 'string', 'rimraf: path should be a string') 264 | assert(options, 'rimraf: missing options') 265 | assert.equal(typeof options, 'object', 'rimraf: options should be object') 266 | 267 | let results 268 | 269 | if (options.disableGlob || !glob.hasMagic(p)) { 270 | results = [p] 271 | } else { 272 | try { 273 | options.lstatSync(p) 274 | results = [p] 275 | } catch (er) { 276 | results = glob.sync(p, options.glob) 277 | } 278 | } 279 | 280 | if (!results.length) 281 | return 282 | 283 | for (let i = 0; i < results.length; i++) { 284 | const p = results[i] 285 | 286 | let st 287 | try { 288 | st = options.lstatSync(p) 289 | } catch (er) { 290 | if (er.code === "ENOENT") 291 | return 292 | 293 | // Windows can EPERM on stat. Life is suffering. 294 | if (er.code === "EPERM" && isWindows) 295 | fixWinEPERMSync(p, options, er) 296 | } 297 | 298 | try { 299 | // sunos lets the root user unlink directories, which is... weird. 300 | if (st && st.isDirectory()) 301 | rmdirSync(p, options, null) 302 | else 303 | options.unlinkSync(p) 304 | } catch (er) { 305 | if (er.code === "ENOENT") 306 | return 307 | if (er.code === "EPERM") 308 | return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er) 309 | if (er.code !== "EISDIR") 310 | throw er 311 | 312 | rmdirSync(p, options, er) 313 | } 314 | } 315 | } 316 | 317 | const rmdirSync = (p, options, originalEr) => { 318 | assert(p) 319 | assert(options) 320 | 321 | try { 322 | options.rmdirSync(p) 323 | } catch (er) { 324 | if (er.code === "ENOENT") 325 | return 326 | if (er.code === "ENOTDIR") 327 | throw originalEr 328 | if (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM") 329 | rmkidsSync(p, options) 330 | } 331 | } 332 | 333 | const rmkidsSync = (p, options) => { 334 | assert(p) 335 | assert(options) 336 | options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options)) 337 | 338 | // We only end up here once we got ENOTEMPTY at least once, and 339 | // at this point, we are guaranteed to have removed all the kids. 340 | // So, we know that it won't be ENOENT or ENOTDIR or anything else. 341 | // try really hard to delete stuff on windows, because it has a 342 | // PROFOUNDLY annoying habit of not closing handles promptly when 343 | // files are deleted, resulting in spurious ENOTEMPTY errors. 344 | const retries = isWindows ? 100 : 1 345 | let i = 0 346 | do { 347 | let threw = true 348 | try { 349 | const ret = options.rmdirSync(p, options) 350 | threw = false 351 | return ret 352 | } finally { 353 | if (++i < retries && threw) 354 | continue 355 | } 356 | } while (true) 357 | } 358 | 359 | module.exports = rimraf 360 | rimraf.sync = rimrafSync 361 | -------------------------------------------------------------------------------- /test/bin.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync } from 'child_process' 2 | import { readdirSync, statSync } from 'fs' 3 | import t from 'tap' 4 | import { fileURLToPath } from 'url' 5 | import { RimrafOptions } from '../src/index.js' 6 | 7 | const binModule = fileURLToPath(new URL('../dist/esm/bin.mjs', import.meta.url)) 8 | 9 | t.test('basic arg parsing stuff', async t => { 10 | const LOGS: any[] = [] 11 | const ERRS: any[] = [] 12 | const { log: consoleLog, error: consoleError } = console 13 | process.env.__TESTING_RIMRAF_BIN__ = '1' 14 | t.teardown(() => { 15 | console.log = consoleLog 16 | console.error = consoleError 17 | delete process.env.__TESTING_RIMRAF_BIN__ 18 | }) 19 | console.log = (...msg) => LOGS.push(msg) 20 | console.error = (...msg) => ERRS.push(msg) 21 | 22 | const CALLS: any[] = [] 23 | const rimraf = Object.assign( 24 | async (path: string, opt: RimrafOptions) => 25 | CALLS.push(['rimraf', path, opt]), 26 | { 27 | native: async (path: string, opt: RimrafOptions) => 28 | CALLS.push(['native', path, opt]), 29 | manual: async (path: string, opt: RimrafOptions) => 30 | CALLS.push(['manual', path, opt]), 31 | posix: async (path: string, opt: RimrafOptions) => 32 | CALLS.push(['posix', path, opt]), 33 | windows: async (path: string, opt: RimrafOptions) => 34 | CALLS.push(['windows', path, opt]), 35 | moveRemove: async (path: string, opt: RimrafOptions) => 36 | CALLS.push(['move-remove', path, opt]), 37 | } 38 | ) 39 | 40 | const { default: bin } = await t.mockImport('../dist/esm/bin.mjs', { 41 | '../dist/esm/index.js': { 42 | rimraf, 43 | ...rimraf, 44 | }, 45 | }) 46 | 47 | t.afterEach(() => { 48 | LOGS.length = 0 49 | ERRS.length = 0 50 | CALLS.length = 0 51 | }) 52 | 53 | t.test('helpful output', t => { 54 | const cases = [['-h'], ['--help'], ['a', 'b', '--help', 'c']] 55 | for (const c of cases) { 56 | t.test(c.join(' '), async t => { 57 | t.equal(await bin(...c), 0) 58 | t.same(LOGS, [[bin.help]]) 59 | t.same(ERRS, []) 60 | t.same(CALLS, []) 61 | }) 62 | } 63 | t.end() 64 | }) 65 | 66 | t.test('no paths', async t => { 67 | t.equal(await bin(), 1) 68 | t.same(LOGS, []) 69 | t.same(ERRS, [ 70 | ['rimraf: must provide a path to remove'], 71 | ['run `rimraf --help` for usage information'], 72 | ]) 73 | }) 74 | 75 | t.test('unnecessary -rf', async t => { 76 | t.equal(await bin('-rf', 'foo'), 0) 77 | t.equal(await bin('-fr', 'foo'), 0) 78 | t.equal(await bin('foo', '-rf'), 0) 79 | t.equal(await bin('foo', '-fr'), 0) 80 | t.same(LOGS, []) 81 | t.same(ERRS, []) 82 | t.same(CALLS, [ 83 | ['rimraf', ['foo'], {}], 84 | ['rimraf', ['foo'], {}], 85 | ['rimraf', ['foo'], {}], 86 | ['rimraf', ['foo'], {}], 87 | ]) 88 | }) 89 | 90 | t.test('verbose', async t => { 91 | t.equal(await bin('-v', 'foo'), 0) 92 | t.equal(await bin('--verbose', 'foo'), 0) 93 | t.equal(await bin('-v', '-V', '--verbose', 'foo'), 0) 94 | t.same(LOGS, []) 95 | t.same(ERRS, []) 96 | const { log } = console 97 | t.teardown(() => { 98 | console.log = log 99 | }) 100 | const logs: any[] = [] 101 | console.log = s => logs.push(s) 102 | for (const c of CALLS) { 103 | t.equal(c[0], 'rimraf') 104 | t.same(c[1], ['foo']) 105 | t.type(c[2].filter, 'function') 106 | t.equal(c[2].filter('x'), true) 107 | t.same(logs, ['x']) 108 | logs.length = 0 109 | } 110 | }) 111 | 112 | t.test('silent', async t => { 113 | t.equal(await bin('-V', 'foo'), 0) 114 | t.equal(await bin('--no-verbose', 'foo'), 0) 115 | t.equal(await bin('-V', '-v', '--no-verbose', 'foo'), 0) 116 | t.same(LOGS, []) 117 | t.same(ERRS, []) 118 | const { log } = console 119 | t.teardown(() => { 120 | console.log = log 121 | }) 122 | const logs: any[] = [] 123 | console.log = s => logs.push(s) 124 | for (const c of CALLS) { 125 | t.equal(c[0], 'rimraf') 126 | t.same(c[1], ['foo']) 127 | t.type(c[2].filter, 'undefined') 128 | t.same(logs, []) 129 | } 130 | }) 131 | 132 | t.test('glob true', async t => { 133 | t.equal(await bin('-g', 'foo'), 0) 134 | t.equal(await bin('--glob', 'foo'), 0) 135 | t.equal(await bin('-G', '-g', 'foo'), 0) 136 | t.equal(await bin('-g', '-G', 'foo'), 0) 137 | t.equal(await bin('-G', 'foo'), 0) 138 | t.equal(await bin('--no-glob', 'foo'), 0) 139 | t.same(LOGS, []) 140 | t.same(ERRS, []) 141 | t.same(CALLS, [ 142 | ['rimraf', ['foo'], { glob: true }], 143 | ['rimraf', ['foo'], { glob: true }], 144 | ['rimraf', ['foo'], { glob: true }], 145 | ['rimraf', ['foo'], { glob: false }], 146 | ['rimraf', ['foo'], { glob: false }], 147 | ['rimraf', ['foo'], { glob: false }], 148 | ]) 149 | }) 150 | 151 | t.test('dashdash', async t => { 152 | t.equal(await bin('--', '-h'), 0) 153 | t.same(LOGS, []) 154 | t.same(ERRS, []) 155 | t.same(CALLS, [['rimraf', ['-h'], {}]]) 156 | }) 157 | 158 | t.test('no preserve root', async t => { 159 | t.equal(await bin('--no-preserve-root', 'foo'), 0) 160 | t.same(LOGS, []) 161 | t.same(ERRS, []) 162 | t.same(CALLS, [['rimraf', ['foo'], { preserveRoot: false }]]) 163 | }) 164 | t.test('yes preserve root', async t => { 165 | t.equal(await bin('--preserve-root', 'foo'), 0) 166 | t.same(LOGS, []) 167 | t.same(ERRS, []) 168 | t.same(CALLS, [['rimraf', ['foo'], { preserveRoot: true }]]) 169 | }) 170 | t.test('yes preserve root, remove root', async t => { 171 | t.equal(await bin('/'), 1) 172 | t.same(LOGS, []) 173 | t.same(ERRS, [ 174 | [`rimraf: it is dangerous to operate recursively on '/'`], 175 | ['use --no-preserve-root to override this failsafe'], 176 | ]) 177 | t.same(CALLS, []) 178 | }) 179 | t.test('no preserve root, remove root', async t => { 180 | t.equal(await bin('/', '--no-preserve-root'), 0) 181 | t.same(LOGS, []) 182 | t.same(ERRS, []) 183 | t.same(CALLS, [['rimraf', ['/'], { preserveRoot: false }]]) 184 | }) 185 | 186 | t.test('--tmp=', async t => { 187 | t.equal(await bin('--tmp=some-path', 'foo'), 0) 188 | t.same(LOGS, []) 189 | t.same(ERRS, []) 190 | t.same(CALLS, [['rimraf', ['foo'], { tmp: 'some-path' }]]) 191 | }) 192 | 193 | t.test('--tmp=', async t => { 194 | t.equal(await bin('--backoff=1.3', 'foo'), 0) 195 | t.same(LOGS, []) 196 | t.same(ERRS, []) 197 | t.same(CALLS, [['rimraf', ['foo'], { backoff: 1.3 }]]) 198 | }) 199 | 200 | t.test('--max-retries=n', async t => { 201 | t.equal(await bin('--max-retries=100', 'foo'), 0) 202 | t.same(LOGS, []) 203 | t.same(ERRS, []) 204 | t.same(CALLS, [['rimraf', ['foo'], { maxRetries: 100 }]]) 205 | }) 206 | 207 | t.test('--retry-delay=n', async t => { 208 | t.equal(await bin('--retry-delay=100', 'foo'), 0) 209 | t.same(LOGS, []) 210 | t.same(ERRS, []) 211 | t.same(CALLS, [['rimraf', ['foo'], { retryDelay: 100 }]]) 212 | }) 213 | 214 | t.test('--uknown-option', async t => { 215 | t.equal(await bin('--unknown-option=100', 'foo'), 1) 216 | t.same(LOGS, []) 217 | t.same(ERRS, [ 218 | ['unknown option: --unknown-option=100'], 219 | ['run `rimraf --help` for usage information'], 220 | ]) 221 | t.same(CALLS, []) 222 | }) 223 | 224 | t.test('--impl=asdf', async t => { 225 | t.equal(await bin('--impl=asdf', 'foo'), 1) 226 | t.same(LOGS, []) 227 | t.same(ERRS, [ 228 | ['unknown implementation: asdf'], 229 | ['run `rimraf --help` for usage information'], 230 | ]) 231 | t.same(CALLS, []) 232 | }) 233 | 234 | t.test('native cannot do filters', async t => { 235 | t.equal(await bin('--impl=native', '-v', 'foo'), 1) 236 | t.same(ERRS, [ 237 | ['native implementation does not support -v or -i'], 238 | ['run `rimraf --help` for usage information'], 239 | ]) 240 | ERRS.length = 0 241 | t.equal(await bin('--impl=native', '-i', 'foo'), 1) 242 | t.same(ERRS, [ 243 | ['native implementation does not support -v or -i'], 244 | ['run `rimraf --help` for usage information'], 245 | ]) 246 | ERRS.length = 0 247 | t.same(CALLS, []) 248 | t.same(LOGS, []) 249 | // ok to turn it on and back off though 250 | t.equal(await bin('--impl=native', '-i', '-I', 'foo'), 0) 251 | t.same(CALLS, [['native', ['foo'], {}]]) 252 | }) 253 | 254 | const impls = [ 255 | 'rimraf', 256 | 'native', 257 | 'manual', 258 | 'posix', 259 | 'windows', 260 | 'move-remove', 261 | ] 262 | for (const impl of impls) { 263 | t.test(`--impl=${impl}`, async t => { 264 | t.equal(await bin('foo', `--impl=${impl}`), 0) 265 | t.same(LOGS, []) 266 | t.same(ERRS, []) 267 | t.same(CALLS, [[impl, ['foo'], {}]]) 268 | }) 269 | } 270 | 271 | t.end() 272 | }) 273 | 274 | t.test('actually delete something with it', async t => { 275 | const path = t.testdir({ 276 | a: { 277 | b: { 278 | c: '1', 279 | }, 280 | }, 281 | }) 282 | 283 | const res = spawnSync(process.execPath, [binModule, path], { 284 | encoding: 'utf8' 285 | }) 286 | t.throws(() => statSync(path)) 287 | t.equal(res.status, 0) 288 | }) 289 | 290 | t.test('print failure when impl throws', async t => { 291 | const path = t.testdir({ 292 | a: { 293 | b: { 294 | c: '1', 295 | }, 296 | }, 297 | }) 298 | 299 | const res = spawnSync(process.execPath, [binModule, path], { 300 | env: { 301 | ...process.env, 302 | __RIMRAF_TESTING_BIN_FAIL__: '1', 303 | }, 304 | }) 305 | t.equal(statSync(path).isDirectory(), true) 306 | t.equal(res.status, 1) 307 | t.match(res.stderr.toString(), /^Error: simulated rimraf failure/) 308 | }) 309 | 310 | t.test('interactive deletes', t => { 311 | const scripts = [ 312 | ['a'], 313 | ['y', 'YOLO', 'no', 'quit'], 314 | ['hehaha', 'yes i think so', '', 'A'], 315 | ['no', 'n', 'N', 'N', 'Q'], 316 | ] 317 | const fixture = { 318 | a: { b: '', c: '', d: '' }, 319 | b: { c: '', d: '', e: '' }, 320 | c: { d: '', e: '', f: '' }, 321 | } 322 | const verboseOpt = ['-v', '-V'] 323 | 324 | // t.jobs = scripts.length * verboseOpt.length 325 | 326 | const node = process.execPath 327 | 328 | const leftovers = (d: string) => { 329 | try { 330 | readdirSync(d) 331 | return true 332 | } catch (_) { 333 | return false 334 | } 335 | } 336 | 337 | for (const verbose of verboseOpt) { 338 | t.test(verbose, async t => { 339 | for (const s of scripts) { 340 | const script = s.slice() 341 | t.test(script.join(', '), async t => { 342 | const d = t.testdir(fixture) 343 | const args = [binModule, '-i', verbose, d] 344 | const child = spawn(node, args, { 345 | stdio: 'pipe', 346 | }) 347 | const out: string[] = [] 348 | const err: string[] = [] 349 | const timer = setTimeout(() => { 350 | t.fail('timed out') 351 | child.kill('SIGKILL') 352 | }, 10000) 353 | child.stdout.setEncoding('utf8') 354 | child.stderr.setEncoding('utf8') 355 | let last = '' 356 | child.stdout.on('data', async (c: string) => { 357 | // await new Promise(r => setTimeout(r, 50)) 358 | out.push(c.trim()) 359 | const s = script.shift() 360 | if (s !== undefined) { 361 | last === s 362 | out.push(s.trim()) 363 | child.stdin.write(s + '\n') 364 | } else { 365 | // keep writing whatever the last option was 366 | child.stdin.write(last + '\n') 367 | } 368 | }) 369 | child.stderr.on('data', (c: string) => { 370 | err.push(c) 371 | }) 372 | return new Promise(res => { 373 | child.on( 374 | 'close', 375 | (code: number | null, signal: NodeJS.Signals | null) => { 376 | clearTimeout(timer) 377 | t.same(err, [], 'should not see any stderr') 378 | t.equal(code, 0, 'code') 379 | t.equal(signal, null, 'signal') 380 | t.matchSnapshot(leftovers(d), 'had any leftover') 381 | res() 382 | } 383 | ) 384 | }) 385 | }) 386 | } 387 | t.end() 388 | }) 389 | } 390 | t.end() 391 | }) 392 | -------------------------------------------------------------------------------- /test/rimraf-posix.ts: -------------------------------------------------------------------------------- 1 | // have to do this *before* loading tap, or else the fact that we 2 | // load rimraf-posix.js for tap's fixture cleanup will cause it to 3 | // have some coverage, but not 100%, failing the coverage check. 4 | // if (process.platform === 'win32') { 5 | // console.log('TAP version 13') 6 | // console.log('1..0 # this test does not work reliably on windows') 7 | // process.exit(0) 8 | // } 9 | 10 | import { Dirent, Stats, statSync } from 'fs' 11 | import * as PATH from 'path' 12 | import { basename, parse, relative } from 'path' 13 | import t from 'tap' 14 | import { rimrafPosix, rimrafPosixSync } from '../dist/esm/rimraf-posix.js' 15 | 16 | import * as fs from '../dist/esm/fs.js' 17 | 18 | const fixture = { 19 | a: 'a', 20 | b: 'b', 21 | c: { 22 | d: 'd', 23 | e: 'e', 24 | f: { 25 | g: 'g', 26 | h: 'h', 27 | i: { 28 | j: 'j', 29 | k: 'k', 30 | l: 'l', 31 | m: { 32 | n: 'n', 33 | o: 'o', 34 | }, 35 | }, 36 | }, 37 | }, 38 | } 39 | 40 | t.test('actually delete some stuff', t => { 41 | const { statSync } = fs 42 | t.test('sync', t => { 43 | const path = t.testdir(fixture) 44 | rimrafPosixSync(path, {}) 45 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'deleted') 46 | t.doesNotThrow( 47 | () => rimrafPosixSync(path, {}), 48 | 'deleting a second time is OK' 49 | ) 50 | t.end() 51 | }) 52 | t.test('async', async t => { 53 | const path = t.testdir(fixture) 54 | await rimrafPosix(path, {}) 55 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'deleted') 56 | t.resolves(rimrafPosix(path, {}), 'deleting a second time is OK') 57 | }) 58 | t.end() 59 | }) 60 | 61 | t.test('throw unlink errors', async t => { 62 | const { rimrafPosix, rimrafPosixSync } = (await t.mockImport( 63 | '../dist/esm/rimraf-posix.js', 64 | { 65 | '../dist/esm/fs.js': { 66 | ...fs, 67 | unlinkSync: () => { 68 | throw Object.assign(new Error('cannot unlink'), { code: 'FOO' }) 69 | }, 70 | promises: { 71 | ...fs.promises, 72 | unlink: async () => { 73 | throw Object.assign(new Error('cannot unlink'), { code: 'FOO' }) 74 | }, 75 | }, 76 | }, 77 | } 78 | )) as typeof import('../dist/esm/rimraf-posix.js') 79 | const path = t.testdir(fixture) 80 | t.throws(() => rimrafPosixSync(path, {}), { code: 'FOO' }) 81 | t.rejects(rimrafPosix(path, {}), { code: 'FOO' }) 82 | }) 83 | 84 | t.test('throw rmdir errors', async t => { 85 | const { rimrafPosix, rimrafPosixSync } = (await t.mockImport( 86 | '../dist/esm/rimraf-posix.js', 87 | { 88 | '../dist/esm/fs.js': { 89 | ...fs, 90 | rmdirSync: () => { 91 | throw Object.assign(new Error('cannot rmdir'), { code: 'FOO' }) 92 | }, 93 | promises: { 94 | ...fs.promises, 95 | rmdir: async () => { 96 | throw Object.assign(new Error('cannot rmdir'), { code: 'FOO' }) 97 | }, 98 | }, 99 | }, 100 | } 101 | )) as typeof import('../dist/esm/rimraf-posix.js') 102 | const path = t.testdir(fixture) 103 | t.throws(() => rimrafPosixSync(path, {}), { code: 'FOO' }) 104 | t.rejects(rimrafPosix(path, {}), { code: 'FOO' }) 105 | }) 106 | 107 | t.test('throw unexpected readdir errors', async t => { 108 | const { rimrafPosix, rimrafPosixSync } = (await t.mockImport( 109 | '../dist/esm/rimraf-posix.js', 110 | { 111 | '../dist/esm/fs.js': { 112 | ...fs, 113 | readdirSync: () => { 114 | throw Object.assign(new Error('cannot readdir'), { code: 'FOO' }) 115 | }, 116 | promises: { 117 | ...fs.promises, 118 | readdir: async () => { 119 | throw Object.assign(new Error('cannot readdir'), { code: 'FOO' }) 120 | }, 121 | }, 122 | }, 123 | } 124 | )) as typeof import('../dist/esm/rimraf-posix.js') 125 | const path = t.testdir(fixture) 126 | t.throws(() => rimrafPosixSync(path, {}), { code: 'FOO' }) 127 | t.rejects(rimrafPosix(path, {}), { code: 'FOO' }) 128 | }) 129 | 130 | t.test('ignore ENOENTs from unlink/rmdir', async t => { 131 | const { rimrafPosix, rimrafPosixSync } = (await t.mockImport( 132 | '../dist/esm/rimraf-posix.js', 133 | { 134 | '../dist/esm/fs.js': { 135 | ...fs, 136 | // simulate a case where two rimrafs are happening in parallel, 137 | // so the deletion happens AFTER the readdir, but before ours. 138 | rmdirSync: (path: string) => { 139 | fs.rmdirSync(path) 140 | fs.rmdirSync(path) 141 | }, 142 | unlinkSync: (path: string) => { 143 | fs.unlinkSync(path) 144 | fs.unlinkSync(path) 145 | }, 146 | promises: { 147 | ...fs.promises, 148 | rmdir: async (path: string) => { 149 | fs.rmdirSync(path) 150 | return fs.promises.rmdir(path) 151 | }, 152 | unlink: async (path: string) => { 153 | fs.unlinkSync(path) 154 | return fs.promises.unlink(path) 155 | }, 156 | }, 157 | }, 158 | } 159 | )) as typeof import('../dist/esm/rimraf-posix.js') 160 | const { statSync } = fs 161 | t.test('sync', t => { 162 | const path = t.testdir(fixture) 163 | rimrafPosixSync(path, {}) 164 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'deleted') 165 | t.end() 166 | }) 167 | t.test('async', async t => { 168 | const path = t.testdir(fixture) 169 | await rimrafPosix(path, {}) 170 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'deleted') 171 | }) 172 | 173 | t.end() 174 | }) 175 | 176 | t.test('rimraffing root, do not actually rmdir root', async t => { 177 | let ROOT: undefined | string = undefined 178 | const { rimrafPosix, rimrafPosixSync } = (await t.mockImport( 179 | '../dist/esm/rimraf-posix.js', 180 | { 181 | path: { 182 | ...PATH, 183 | parse: (path: string) => { 184 | const p = parse(path) 185 | if (path === ROOT) { 186 | p.root = path 187 | } 188 | return p 189 | }, 190 | }, 191 | } 192 | )) as typeof import('../dist/esm/rimraf-posix.js') 193 | t.test('async', async t => { 194 | ROOT = t.testdir(fixture) 195 | await rimrafPosix(ROOT, { preserveRoot: false }) 196 | t.equal(fs.statSync(ROOT).isDirectory(), true, 'root still present') 197 | t.same(fs.readdirSync(ROOT), [], 'entries all gone') 198 | }) 199 | t.test('sync', async t => { 200 | ROOT = t.testdir(fixture) 201 | rimrafPosixSync(ROOT, { preserveRoot: false }) 202 | t.equal(fs.statSync(ROOT).isDirectory(), true, 'root still present') 203 | t.same(fs.readdirSync(ROOT), [], 'entries all gone') 204 | }) 205 | t.end() 206 | }) 207 | 208 | t.test( 209 | 'abort on signal', 210 | { skip: typeof AbortController === 'undefined' }, 211 | t => { 212 | t.test('sync', t => { 213 | const d = t.testdir(fixture) 214 | const ac = new AbortController() 215 | const { signal } = ac 216 | ac.abort(new Error('aborted rimraf')) 217 | t.throws(() => rimrafPosixSync(d, { signal })) 218 | t.end() 219 | }) 220 | t.test('sync abort in filter', t => { 221 | const d = t.testdir(fixture) 222 | const ac = new AbortController() 223 | const { signal } = ac 224 | const opt = { 225 | signal, 226 | filter: (p: string, st: Stats | Dirent) => { 227 | if (basename(p) === 'g' && st.isFile()) { 228 | ac.abort(new Error('done')) 229 | } 230 | return true 231 | }, 232 | } 233 | t.throws(() => rimrafPosixSync(d, opt), { message: 'done' }) 234 | t.end() 235 | }) 236 | t.test('async', async t => { 237 | const d = t.testdir(fixture) 238 | const ac = new AbortController() 239 | const { signal } = ac 240 | const p = t.rejects(() => rimrafPosix(d, { signal })) 241 | ac.abort(new Error('aborted rimraf')) 242 | await p 243 | }) 244 | t.test('async preaborted', async t => { 245 | const d = t.testdir(fixture) 246 | const ac = new AbortController() 247 | ac.abort(new Error('aborted rimraf')) 248 | const { signal } = ac 249 | await t.rejects(() => rimrafPosix(d, { signal })) 250 | }) 251 | t.end() 252 | } 253 | ) 254 | 255 | t.test('filter function', t => { 256 | t.formatSnapshot = undefined 257 | 258 | for (const f of ['i', 'j']) { 259 | t.test(`filter=${f}`, t => { 260 | t.test('sync', t => { 261 | const dir = t.testdir(fixture) 262 | const saw: string[] = [] 263 | const filter = (p: string) => { 264 | saw.push(relative(process.cwd(), p).replace(/\\/g, '/')) 265 | return basename(p) !== f 266 | } 267 | rimrafPosixSync(dir, { filter }) 268 | t.matchSnapshot( 269 | saw.sort((a, b) => a.localeCompare(b, 'en')), 270 | 'paths seen' 271 | ) 272 | statSync(dir) 273 | statSync(dir + '/c') 274 | statSync(dir + '/c/f') 275 | statSync(dir + '/c/f/i') 276 | if (f === 'j') { 277 | statSync(dir + '/c/f/i/j') 278 | } else { 279 | t.throws(() => statSync(dir + '/c/f/i/j')) 280 | } 281 | t.throws(() => statSync(dir + '/a')) 282 | t.throws(() => statSync(dir + '/b')) 283 | t.throws(() => statSync(dir + '/c/d')) 284 | t.throws(() => statSync(dir + '/c/e')) 285 | t.throws(() => statSync(dir + '/c/f/g')) 286 | t.throws(() => statSync(dir + '/c/f/h')) 287 | t.throws(() => statSync(dir + '/c/f/i/k')) 288 | t.throws(() => statSync(dir + '/c/f/i/l')) 289 | t.throws(() => statSync(dir + '/c/f/i/m')) 290 | t.end() 291 | }) 292 | 293 | t.test('async', async t => { 294 | const dir = t.testdir(fixture) 295 | const saw: string[] = [] 296 | const filter = (p: string) => { 297 | saw.push(relative(process.cwd(), p).replace(/\\/g, '/')) 298 | return basename(p) !== f 299 | } 300 | await rimrafPosix(dir, { filter }) 301 | t.matchSnapshot( 302 | saw.sort((a, b) => a.localeCompare(b, 'en')), 303 | 'paths seen' 304 | ) 305 | statSync(dir) 306 | statSync(dir + '/c') 307 | statSync(dir + '/c/f') 308 | statSync(dir + '/c/f/i') 309 | if (f === 'j') { 310 | statSync(dir + '/c/f/i/j') 311 | } else { 312 | t.throws(() => statSync(dir + '/c/f/i/j')) 313 | } 314 | t.throws(() => statSync(dir + '/a')) 315 | t.throws(() => statSync(dir + '/b')) 316 | t.throws(() => statSync(dir + '/c/d')) 317 | t.throws(() => statSync(dir + '/c/e')) 318 | t.throws(() => statSync(dir + '/c/f/g')) 319 | t.throws(() => statSync(dir + '/c/f/h')) 320 | t.throws(() => statSync(dir + '/c/f/i/k')) 321 | t.throws(() => statSync(dir + '/c/f/i/l')) 322 | t.throws(() => statSync(dir + '/c/f/i/m')) 323 | }) 324 | 325 | t.test('async filter', async t => { 326 | const dir = t.testdir(fixture) 327 | const saw: string[] = [] 328 | const filter = async (p: string) => { 329 | saw.push(relative(process.cwd(), p).replace(/\\/g, '/')) 330 | await new Promise(setImmediate) 331 | return basename(p) !== f 332 | } 333 | await rimrafPosix(dir, { filter }) 334 | t.matchSnapshot( 335 | saw.sort((a, b) => a.localeCompare(b, 'en')), 336 | 'paths seen' 337 | ) 338 | statSync(dir) 339 | statSync(dir + '/c') 340 | statSync(dir + '/c/f') 341 | statSync(dir + '/c/f/i') 342 | if (f === 'j') { 343 | statSync(dir + '/c/f/i/j') 344 | } else { 345 | t.throws(() => statSync(dir + '/c/f/i/j')) 346 | } 347 | t.throws(() => statSync(dir + '/a')) 348 | t.throws(() => statSync(dir + '/b')) 349 | t.throws(() => statSync(dir + '/c/d')) 350 | t.throws(() => statSync(dir + '/c/e')) 351 | t.throws(() => statSync(dir + '/c/f/g')) 352 | t.throws(() => statSync(dir + '/c/f/h')) 353 | t.throws(() => statSync(dir + '/c/f/i/k')) 354 | t.throws(() => statSync(dir + '/c/f/i/l')) 355 | t.throws(() => statSync(dir + '/c/f/i/m')) 356 | }) 357 | t.end() 358 | }) 359 | } 360 | t.end() 361 | }) 362 | 363 | t.test('do not follow symlinks', t => { 364 | const fixture = { 365 | x: { 366 | y: t.fixture('symlink', '../z'), 367 | z: '', 368 | }, 369 | z: { 370 | a: '', 371 | b: { c: '' }, 372 | }, 373 | } 374 | t.test('sync', t => { 375 | const d = t.testdir(fixture) 376 | t.equal(rimrafPosixSync(d + '/x', {}), true) 377 | statSync(d + '/z') 378 | statSync(d + '/z/a') 379 | statSync(d + '/z/b/c') 380 | t.end() 381 | }) 382 | t.test('async', async t => { 383 | const d = t.testdir(fixture) 384 | t.equal(await rimrafPosix(d + '/x', {}), true) 385 | statSync(d + '/z') 386 | statSync(d + '/z/a') 387 | statSync(d + '/z/b/c') 388 | }) 389 | t.end() 390 | }) 391 | -------------------------------------------------------------------------------- /test/rimraf-windows.ts: -------------------------------------------------------------------------------- 1 | import { Dirent, PathLike, Stats, statSync } from 'fs' 2 | import * as PATH from 'path' 3 | import { basename, parse, relative } from 'path' 4 | import t from 'tap' 5 | import * as FS from '../dist/esm/fs.js' 6 | import { rimrafWindows, rimrafWindowsSync } from '../dist/esm/rimraf-windows.js' 7 | 8 | t.formatSnapshot = (calls: string[][]) => 9 | Array.isArray(calls) 10 | ? calls.map(args => 11 | args.map(arg => 12 | String(arg) 13 | .split(process.cwd()) 14 | .join('{CWD}') 15 | .replace(/\\/g, '/') 16 | .replace(/.*\/(\.[a-z]\.)[^/]*$/, '{tmpfile}') 17 | ) 18 | ) 19 | : calls 20 | 21 | const fixture = { 22 | a: 'a', 23 | b: 'b', 24 | c: { 25 | d: 'd', 26 | e: 'e', 27 | f: { 28 | g: 'g', 29 | h: 'h', 30 | i: { 31 | j: 'j', 32 | k: 'k', 33 | l: 'l', 34 | m: { 35 | n: 'n', 36 | o: 'o', 37 | }, 38 | }, 39 | }, 40 | }, 41 | } 42 | 43 | t.test('actually delete some stuff', async t => { 44 | const fsMock: Record = { ...FS, promises: { ...FS.promises } } 45 | 46 | // simulate annoying windows semantics, where an unlink or rmdir 47 | // may take an arbitrary amount of time. we only delay unlinks, 48 | // to ensure that we will get an error when we try to rmdir. 49 | const { 50 | statSync, 51 | promises: { unlink }, 52 | } = FS 53 | 54 | const danglers: Promise[] = [] 55 | const unlinkLater = (path: string) => { 56 | const p = new Promise(res => { 57 | setTimeout(() => unlink(path).then(res, res), 100) 58 | }) 59 | danglers.push(p) 60 | } 61 | fsMock.unlinkSync = (path: string) => unlinkLater(path) 62 | fsMock.promises.unlink = async (path: string) => unlinkLater(path) 63 | 64 | // but actually do wait to clean them up, though 65 | t.teardown(async () => { 66 | await Promise.all(danglers) 67 | }) 68 | 69 | const { rimrafPosix, rimrafPosixSync } = (await t.mockImport( 70 | '../dist/esm/rimraf-posix.js', 71 | { 72 | '../dist/esm/fs.js': fsMock, 73 | } 74 | )) as typeof import('../dist/esm/rimraf-posix.js') 75 | 76 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 77 | '../dist/esm/rimraf-windows.js', 78 | { '../dist/esm/fs.js': fsMock } 79 | )) as typeof import('../dist/esm/rimraf-windows.js') 80 | 81 | t.test('posix does not work here', t => { 82 | t.test('sync', t => { 83 | const path = t.testdir(fixture) 84 | t.throws(() => rimrafPosixSync(path, {})) 85 | t.end() 86 | }) 87 | t.test('async', async t => { 88 | const path = t.testdir(fixture) 89 | await t.rejects(() => rimrafPosix(path, {})) 90 | t.end() 91 | }) 92 | t.end() 93 | }) 94 | 95 | t.test('sync', t => { 96 | const path = t.testdir(fixture) 97 | rimrafWindowsSync(path, {}) 98 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'deleted') 99 | t.doesNotThrow( 100 | () => rimrafWindowsSync(path, {}), 101 | 'deleting a second time is OK' 102 | ) 103 | t.end() 104 | }) 105 | 106 | t.test('async', async t => { 107 | const path = t.testdir(fixture) 108 | await rimrafWindows(path, {}) 109 | t.throws(() => statSync(path), { code: 'ENOENT' }, 'deleted') 110 | await t.resolves(rimrafWindows(path, {}), 'deleting a second time is OK') 111 | }) 112 | t.end() 113 | }) 114 | 115 | t.test('throw unlink errors', async t => { 116 | // only throw once here, or else it messes with tap's fixture cleanup 117 | // that's probably a bug in t.mock? 118 | let threwAsync = false 119 | let threwSync = false 120 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 121 | '../dist/esm/rimraf-windows.js', 122 | { 123 | '../dist/esm/fs.js': { 124 | ...FS, 125 | unlinkSync: (path: string) => { 126 | if (threwSync) { 127 | return FS.unlinkSync(path) 128 | } 129 | threwSync = true 130 | throw Object.assign(new Error('cannot unlink'), { code: 'FOO' }) 131 | }, 132 | promises: { 133 | ...FS.promises, 134 | unlink: async (path: string) => { 135 | if (threwAsync) { 136 | return FS.promises.unlink(path) 137 | } 138 | threwAsync = true 139 | throw Object.assign(new Error('cannot unlink'), { code: 'FOO' }) 140 | }, 141 | }, 142 | }, 143 | } 144 | )) as typeof import('../dist/esm/rimraf-windows.js') 145 | // nest to clean up the mess 146 | t.test('sync', t => { 147 | const path = t.testdir({ test: fixture }) + '/test' 148 | t.throws(() => rimrafWindowsSync(path, {}), { code: 'FOO' }) 149 | t.end() 150 | }) 151 | t.test('async', t => { 152 | const path = t.testdir({ test: fixture }) + '/test' 153 | t.rejects(rimrafWindows(path, {}), { code: 'FOO' }) 154 | t.end() 155 | }) 156 | t.end() 157 | }) 158 | 159 | t.test('ignore ENOENT unlink errors', async t => { 160 | const threwAsync = false 161 | let threwSync = false 162 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 163 | '../dist/esm/rimraf-windows.js', 164 | { 165 | '../dist/esm/fs.js': { 166 | ...FS, 167 | unlinkSync: (path: string) => { 168 | FS.unlinkSync(path) 169 | if (threwSync) { 170 | return 171 | } 172 | threwSync = true 173 | FS.unlinkSync(path) 174 | }, 175 | promises: { 176 | ...FS.promises, 177 | unlink: async (path: string) => { 178 | FS.unlinkSync(path) 179 | if (threwAsync) { 180 | return 181 | } 182 | threwSync = true 183 | FS.unlinkSync(path) 184 | }, 185 | }, 186 | }, 187 | } 188 | )) as typeof import('../dist/esm/rimraf-windows.js') 189 | // nest to clean up the mess 190 | t.test('sync', t => { 191 | const path = t.testdir({ test: fixture }) + '/test' 192 | t.doesNotThrow(() => rimrafWindowsSync(path, {}), 'enoent no problems') 193 | t.end() 194 | }) 195 | t.test('async', t => { 196 | const path = t.testdir({ test: fixture }) + '/test' 197 | t.resolves(() => rimrafWindows(path, {}), 'enoent no problems') 198 | t.end() 199 | }) 200 | t.end() 201 | }) 202 | 203 | t.test('throw rmdir errors', async t => { 204 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 205 | '../dist/esm/rimraf-windows.js', 206 | { 207 | '../dist/esm/fs.js': { 208 | ...FS, 209 | rmdirSync: () => { 210 | throw Object.assign(new Error('cannot rmdir'), { code: 'FOO' }) 211 | }, 212 | promises: { 213 | ...FS.promises, 214 | rmdir: async () => { 215 | throw Object.assign(new Error('cannot rmdir'), { code: 'FOO' }) 216 | }, 217 | }, 218 | }, 219 | } 220 | )) as typeof import('../dist/esm/rimraf-windows.js') 221 | t.test('sync', t => { 222 | // nest it so that we clean up the mess 223 | const path = t.testdir({ test: fixture }) + '/test' 224 | t.throws(() => rimrafWindowsSync(path, {}), { code: 'FOO' }) 225 | t.end() 226 | }) 227 | t.test('async', t => { 228 | // nest it so that we clean up the mess 229 | const path = t.testdir({ test: fixture }) + '/test' 230 | t.rejects(rimrafWindows(path, {}), { code: 'FOO' }) 231 | t.end() 232 | }) 233 | t.end() 234 | }) 235 | 236 | t.test('throw unexpected readdir errors', async t => { 237 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 238 | '../dist/esm/rimraf-windows.js', 239 | { 240 | '../dist/esm/fs.js': { 241 | ...FS, 242 | readdirSync: () => { 243 | throw Object.assign(new Error('cannot readdir'), { code: 'FOO' }) 244 | }, 245 | promises: { 246 | ...FS.promises, 247 | readdir: async () => { 248 | throw Object.assign(new Error('cannot readdir'), { code: 'FOO' }) 249 | }, 250 | }, 251 | }, 252 | } 253 | )) as typeof import('../dist/esm/rimraf-windows.js') 254 | t.test('sync', t => { 255 | // nest to clean up the mess 256 | const path = t.testdir({ test: fixture }) + '/test' 257 | t.throws(() => rimrafWindowsSync(path, {}), { code: 'FOO' }) 258 | t.end() 259 | }) 260 | t.test('async', t => { 261 | // nest to clean up the mess 262 | const path = t.testdir({ test: fixture }) + '/test' 263 | t.rejects(rimrafWindows(path, {}), { code: 'FOO' }) 264 | t.end() 265 | }) 266 | t.end() 267 | }) 268 | 269 | t.test('handle EPERMs on unlink by trying to chmod 0o666', async t => { 270 | const CHMODS = [] 271 | let threwAsync = false 272 | let threwSync = false 273 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 274 | '../dist/esm/rimraf-windows.js', 275 | { 276 | '../dist/esm/fs.js': { 277 | ...FS, 278 | chmodSync: (...args: any[]) => { 279 | CHMODS.push(['chmodSync', ...args]) 280 | //@ts-ignore 281 | return FS.chmodSync(...args) 282 | }, 283 | unlinkSync: (path: string) => { 284 | if (threwSync) { 285 | return FS.unlinkSync(path) 286 | } 287 | threwSync = true 288 | throw Object.assign(new Error('cannot unlink'), { code: 'EPERM' }) 289 | }, 290 | promises: { 291 | ...FS.promises, 292 | unlink: async (path: String) => { 293 | if (threwAsync) { 294 | return FS.promises.unlink(path as PathLike) 295 | } 296 | threwAsync = true 297 | throw Object.assign(new Error('cannot unlink'), { code: 'EPERM' }) 298 | }, 299 | chmod: async (...args: any[]) => { 300 | CHMODS.push(['chmod', ...args]) 301 | //@ts-ignore 302 | return FS.promises.chmod(...args) 303 | }, 304 | }, 305 | }, 306 | } 307 | )) as typeof import('../dist/esm/rimraf-windows.js') 308 | 309 | t.afterEach(() => (CHMODS.length = 0)) 310 | 311 | t.test('sync', t => { 312 | // nest it so that we clean up the mess 313 | const path = t.testdir({ test: fixture }) + '/test' 314 | rimrafWindowsSync(path, {}) 315 | t.matchSnapshot(CHMODS.length, 'chmods') 316 | t.end() 317 | }) 318 | t.test('async', async t => { 319 | // nest it so that we clean up the mess 320 | const path = t.testdir({ test: fixture }) + '/test' 321 | await rimrafWindows(path, {}) 322 | t.matchSnapshot(CHMODS.length, 'chmods') 323 | t.end() 324 | }) 325 | t.end() 326 | }) 327 | 328 | t.test('handle EPERMs, chmod returns ENOENT', async t => { 329 | const CHMODS = [] 330 | let threwAsync = false 331 | let threwSync = false 332 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 333 | '../dist/esm/rimraf-windows.js', 334 | { 335 | '../dist/esm/fs.js': { 336 | ...FS, 337 | chmodSync: (...args: any[]) => { 338 | CHMODS.push(['chmodSync', ...args]) 339 | try { 340 | FS.unlinkSync(args[0]) 341 | } catch (_) {} 342 | //@ts-ignore 343 | return FS.chmodSync(...args) 344 | }, 345 | unlinkSync: (path: string) => { 346 | if (threwSync) { 347 | return FS.unlinkSync(path) 348 | } 349 | threwSync = true 350 | throw Object.assign(new Error('cannot unlink'), { code: 'EPERM' }) 351 | }, 352 | promises: { 353 | ...FS.promises, 354 | unlink: async (path: string) => { 355 | if (threwAsync) { 356 | return FS.promises.unlink(path) 357 | } 358 | threwAsync = true 359 | throw Object.assign(new Error('cannot unlink'), { code: 'EPERM' }) 360 | }, 361 | chmod: async (...args: any[]) => { 362 | CHMODS.push(['chmod', ...args]) 363 | try { 364 | FS.unlinkSync(args[0]) 365 | } catch (_) {} 366 | //@ts-ignore 367 | return FS.promises.chmod(...args) 368 | }, 369 | }, 370 | }, 371 | } 372 | )) as typeof import('../dist/esm/rimraf-windows.js') 373 | 374 | t.afterEach(() => (CHMODS.length = 0)) 375 | 376 | t.test('sync', t => { 377 | // nest it so that we clean up the mess 378 | const path = t.testdir({ test: fixture }) + '/test' 379 | rimrafWindowsSync(path, {}) 380 | t.matchSnapshot(CHMODS.length, 'chmods') 381 | t.end() 382 | }) 383 | t.test('async', async t => { 384 | // nest it so that we clean up the mess 385 | const path = t.testdir({ test: fixture }) + '/test' 386 | await rimrafWindows(path, {}) 387 | t.matchSnapshot(CHMODS.length, 'chmods') 388 | t.end() 389 | }) 390 | t.end() 391 | }) 392 | 393 | t.test('handle EPERMs, chmod raises something other than ENOENT', async t => { 394 | const CHMODS = [] 395 | let threwAsync = false 396 | let threwSync = false 397 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 398 | '../dist/esm/rimraf-windows.js', 399 | { 400 | '../dist/esm/fs.js': { 401 | ...FS, 402 | chmodSync: (...args: any[]) => { 403 | CHMODS.push(['chmodSync', ...args]) 404 | try { 405 | FS.unlinkSync(args[0]) 406 | } catch (_) {} 407 | throw Object.assign(new Error('cannot chmod'), { code: 'FOO' }) 408 | }, 409 | unlinkSync: (path: string) => { 410 | if (threwSync) { 411 | return FS.unlinkSync(path) 412 | } 413 | threwSync = true 414 | throw Object.assign(new Error('cannot unlink'), { code: 'EPERM' }) 415 | }, 416 | promises: { 417 | ...FS.promises, 418 | unlink: async (path: string) => { 419 | if (threwAsync) { 420 | return FS.promises.unlink(path) 421 | } 422 | threwAsync = true 423 | throw Object.assign(new Error('cannot unlink'), { code: 'EPERM' }) 424 | }, 425 | chmod: async (...args: any[]) => { 426 | CHMODS.push(['chmod', ...args]) 427 | try { 428 | FS.unlinkSync(args[0]) 429 | } catch (_) {} 430 | throw Object.assign(new Error('cannot chmod'), { code: 'FOO' }) 431 | }, 432 | }, 433 | }, 434 | } 435 | )) as typeof import('../dist/esm/rimraf-windows.js') 436 | 437 | t.afterEach(() => (CHMODS.length = 0)) 438 | 439 | t.test('sync', t => { 440 | // nest it so that we clean up the mess 441 | const path = t.testdir({ test: fixture }) + '/test' 442 | t.throws(() => rimrafWindowsSync(path, {}), { code: 'EPERM' }) 443 | t.matchSnapshot(CHMODS.length, 'chmods') 444 | t.end() 445 | }) 446 | t.test('async', async t => { 447 | // nest it so that we clean up the mess 448 | const path = t.testdir({ test: fixture }) + '/test' 449 | t.rejects(rimrafWindows(path, {}), { code: 'EPERM' }) 450 | t.matchSnapshot(CHMODS.length, 'chmods') 451 | t.end() 452 | }) 453 | t.end() 454 | }) 455 | 456 | t.test('rimraffing root, do not actually rmdir root', async t => { 457 | let ROOT: string | undefined = undefined 458 | const { rimrafWindows, rimrafWindowsSync } = (await t.mockImport( 459 | '../dist/esm/rimraf-windows.js', 460 | { 461 | path: { 462 | ...PATH, 463 | parse: (path: string) => { 464 | const p = parse(path) 465 | if (path === ROOT) { 466 | p.root = path 467 | } 468 | return p 469 | }, 470 | }, 471 | } 472 | )) as typeof import('../dist/esm/rimraf-windows.js') 473 | t.test('async', async t => { 474 | ROOT = t.testdir(fixture) 475 | await rimrafWindows(ROOT, { preserveRoot: false }) 476 | t.equal(FS.statSync(ROOT).isDirectory(), true, 'root still present') 477 | t.same(FS.readdirSync(ROOT), [], 'entries all gone') 478 | }) 479 | t.test('sync', async t => { 480 | ROOT = t.testdir(fixture) 481 | rimrafWindowsSync(ROOT, { preserveRoot: false }) 482 | t.equal(FS.statSync(ROOT).isDirectory(), true, 'root still present') 483 | t.same(FS.readdirSync(ROOT), [], 'entries all gone') 484 | }) 485 | t.end() 486 | }) 487 | 488 | t.test( 489 | 'abort on signal', 490 | { skip: typeof AbortController === 'undefined' }, 491 | t => { 492 | t.test('sync', t => { 493 | const d = t.testdir(fixture) 494 | const ac = new AbortController() 495 | const { signal } = ac 496 | ac.abort(new Error('aborted rimraf')) 497 | t.throws(() => rimrafWindowsSync(d, { signal })) 498 | t.end() 499 | }) 500 | t.test('sync abort in filter', t => { 501 | const d = t.testdir(fixture) 502 | const ac = new AbortController() 503 | const { signal } = ac 504 | const opt = { 505 | signal, 506 | filter: (p: string, st: Stats | Dirent) => { 507 | if (basename(p) === 'g' && st.isFile()) { 508 | ac.abort(new Error('done')) 509 | } 510 | return true 511 | }, 512 | } 513 | t.throws(() => rimrafWindowsSync(d, opt), { message: 'done' }) 514 | t.end() 515 | }) 516 | t.test('async', async t => { 517 | const d = t.testdir(fixture) 518 | const ac = new AbortController() 519 | const { signal } = ac 520 | const p = t.rejects(() => rimrafWindows(d, { signal })) 521 | ac.abort(new Error('aborted rimraf')) 522 | await p 523 | }) 524 | t.test('async, pre-aborted', async t => { 525 | const ac = new AbortController() 526 | const { signal } = ac 527 | const d = t.testdir(fixture) 528 | ac.abort(new Error('aborted rimraf')) 529 | await t.rejects(() => rimrafWindows(d, { signal })) 530 | }) 531 | t.end() 532 | } 533 | ) 534 | 535 | t.test('filter function', t => { 536 | for (const f of ['i', 'j']) { 537 | t.test(`filter=${f}`, t => { 538 | t.test('sync', t => { 539 | const dir = t.testdir(fixture) 540 | const saw: string[] = [] 541 | const filter = (p: string) => { 542 | saw.push(relative(process.cwd(), p).replace(/\\/g, '/')) 543 | return basename(p) !== f 544 | } 545 | rimrafWindowsSync(dir, { filter }) 546 | t.matchSnapshot( 547 | [saw.sort((a, b) => a.localeCompare(b, 'en'))], 548 | 'paths seen' 549 | ) 550 | statSync(dir) 551 | statSync(dir + '/c') 552 | statSync(dir + '/c/f') 553 | statSync(dir + '/c/f/i') 554 | if (f === 'j') { 555 | statSync(dir + '/c/f/i/j') 556 | } else { 557 | t.throws(() => statSync(dir + '/c/f/i/j')) 558 | } 559 | t.throws(() => statSync(dir + '/a')) 560 | t.throws(() => statSync(dir + '/b')) 561 | t.throws(() => statSync(dir + '/c/d')) 562 | t.throws(() => statSync(dir + '/c/e')) 563 | t.throws(() => statSync(dir + '/c/f/g')) 564 | t.throws(() => statSync(dir + '/c/f/h')) 565 | t.throws(() => statSync(dir + '/c/f/i/k')) 566 | t.throws(() => statSync(dir + '/c/f/i/l')) 567 | t.throws(() => statSync(dir + '/c/f/i/m')) 568 | t.end() 569 | }) 570 | 571 | t.test('async', async t => { 572 | const dir = t.testdir(fixture) 573 | const saw: string[] = [] 574 | const filter = (p: string) => { 575 | saw.push(relative(process.cwd(), p).replace(/\\/g, '/')) 576 | return basename(p) !== f 577 | } 578 | await rimrafWindows(dir, { filter }) 579 | t.matchSnapshot( 580 | [saw.sort((a, b) => a.localeCompare(b, 'en'))], 581 | 'paths seen' 582 | ) 583 | statSync(dir) 584 | statSync(dir + '/c') 585 | statSync(dir + '/c/f') 586 | statSync(dir + '/c/f/i') 587 | if (f === 'j') { 588 | statSync(dir + '/c/f/i/j') 589 | } else { 590 | t.throws(() => statSync(dir + '/c/f/i/j')) 591 | } 592 | t.throws(() => statSync(dir + '/a')) 593 | t.throws(() => statSync(dir + '/b')) 594 | t.throws(() => statSync(dir + '/c/d')) 595 | t.throws(() => statSync(dir + '/c/e')) 596 | t.throws(() => statSync(dir + '/c/f/g')) 597 | t.throws(() => statSync(dir + '/c/f/h')) 598 | t.throws(() => statSync(dir + '/c/f/i/k')) 599 | t.throws(() => statSync(dir + '/c/f/i/l')) 600 | t.throws(() => statSync(dir + '/c/f/i/m')) 601 | }) 602 | 603 | t.test('async filter', async t => { 604 | const dir = t.testdir(fixture) 605 | const saw: string[] = [] 606 | const filter = async (p: string) => { 607 | saw.push(relative(process.cwd(), p).replace(/\\/g, '/')) 608 | await new Promise(setImmediate) 609 | return basename(p) !== f 610 | } 611 | await rimrafWindows(dir, { filter }) 612 | t.matchSnapshot( 613 | [saw.sort((a, b) => a.localeCompare(b, 'en'))], 614 | 'paths seen' 615 | ) 616 | statSync(dir) 617 | statSync(dir + '/c') 618 | statSync(dir + '/c/f') 619 | statSync(dir + '/c/f/i') 620 | if (f === 'j') { 621 | statSync(dir + '/c/f/i/j') 622 | } else { 623 | t.throws(() => statSync(dir + '/c/f/i/j')) 624 | } 625 | t.throws(() => statSync(dir + '/a')) 626 | t.throws(() => statSync(dir + '/b')) 627 | t.throws(() => statSync(dir + '/c/d')) 628 | t.throws(() => statSync(dir + '/c/e')) 629 | t.throws(() => statSync(dir + '/c/f/g')) 630 | t.throws(() => statSync(dir + '/c/f/h')) 631 | t.throws(() => statSync(dir + '/c/f/i/k')) 632 | t.throws(() => statSync(dir + '/c/f/i/l')) 633 | t.throws(() => statSync(dir + '/c/f/i/m')) 634 | }) 635 | t.end() 636 | }) 637 | } 638 | t.end() 639 | }) 640 | 641 | t.test('do not follow symlinks', t => { 642 | const fixture = { 643 | x: { 644 | y: t.fixture('symlink', '../z'), 645 | z: '', 646 | }, 647 | z: { 648 | a: '', 649 | b: { c: '' }, 650 | }, 651 | } 652 | t.test('sync', t => { 653 | const d = t.testdir(fixture) 654 | t.equal(rimrafWindowsSync(d + '/x', {}), true) 655 | statSync(d + '/z') 656 | statSync(d + '/z/a') 657 | statSync(d + '/z/b/c') 658 | t.end() 659 | }) 660 | t.test('async', async t => { 661 | const d = t.testdir(fixture) 662 | t.equal(await rimrafWindows(d + '/x', {}), true) 663 | statSync(d + '/z') 664 | statSync(d + '/z/a') 665 | statSync(d + '/z/b/c') 666 | }) 667 | t.end() 668 | }) 669 | --------------------------------------------------------------------------------