diff --git a/src/bin.ts b/src/bin.ts index 73e64dc9..7aeffca9 100755 --- a/src/bin.ts +++ b/src/bin.ts @@ -91,10 +91,9 @@ const interactiveRimraf = async ( return false } while (!allRemaining) { - const a = (await prompt( - rl, - `rm? ${relative(cwd, path)}\n[(Yes)/No/All/Quit] > ` - )).trim() + const a = ( + await prompt(rl, `rm? ${relative(cwd, path)}\n[(Yes)/No/All/Quit] > `) + ).trim() if (/^n/i.test(a)) { return false } else if (/^a/i.test(a)) { diff --git a/src/fs.ts b/src/fs.ts index 3cafb66e..fe2a52ad 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -10,6 +10,7 @@ export { rmdirSync, rmSync, statSync, + lstatSync, unlinkSync, } from 'fs' @@ -66,6 +67,11 @@ const stat = (path: fs.PathLike): Promise => fs.stat(path, (er, data) => (er ? rej(er) : res(data))) ) +const lstat = (path: fs.PathLike): Promise => + new Promise((res, rej) => + fs.lstat(path, (er, data) => (er ? rej(er) : res(data))) + ) + const unlink = (path: fs.PathLike): Promise => new Promise((res, rej) => fs.unlink(path, (er, ...d: any[]) => (er ? rej(er) : res(...d))) @@ -79,5 +85,6 @@ export const promises = { rm, rmdir, stat, + lstat, unlink, } diff --git a/src/ignore-enoent.ts b/src/ignore-enoent.ts index 076f31c4..c0667912 100644 --- a/src/ignore-enoent.ts +++ b/src/ignore-enoent.ts @@ -1,4 +1,3 @@ - export const ignoreENOENT = async (p: Promise) => p.catch(er => { if (er.code !== 'ENOENT') { diff --git a/src/readdir-or-error.ts b/src/readdir-or-error.ts index 693365f8..79970df1 100644 --- a/src/readdir-or-error.ts +++ b/src/readdir-or-error.ts @@ -1,6 +1,6 @@ // returns an array of entries if readdir() works, // or the error that readdir() raised if not. -import { promises, readdirSync } from './fs.js' +import { promises, readdirSync } from './fs.js' const { readdir } = promises export const readdirOrError = (path: string) => readdir(path).catch(er => er as NodeJS.ErrnoException) diff --git a/src/rimraf-move-remove.ts b/src/rimraf-move-remove.ts index 73f08de7..c64cc224 100644 --- a/src/rimraf-move-remove.ts +++ b/src/rimraf-move-remove.ts @@ -18,13 +18,15 @@ import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' import { chmodSync, + lstatSync, promises as fsPromises, renameSync, rmdirSync, unlinkSync, } from './fs.js' -const { rename, unlink, rmdir, chmod } = fsPromises +const { lstat, rename, unlink, rmdir, chmod } = fsPromises +import { Dirent, Stats } from 'fs' import { RimrafAsyncOptions, RimrafSyncOptions } from '.' import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' @@ -72,25 +74,51 @@ const unlinkFixEPERMSync = (path: string) => { export const rimrafMoveRemove = async ( path: string, opt: RimrafAsyncOptions +) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return await rimrafMoveRemoveDir(path, opt, await lstat(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafMoveRemoveDir = async ( + path: string, + opt: RimrafAsyncOptions, + ent: Dirent | Stats ): Promise => { if (opt?.signal?.aborted) { throw opt.signal.reason } if (!opt.tmp) { - return rimrafMoveRemove(path, { ...opt, tmp: await defaultTmp(path) }) + return rimrafMoveRemoveDir( + path, + { ...opt, tmp: await defaultTmp(path) }, + ent + ) } if (path === opt.tmp && parse(path).root !== path) { throw new Error('cannot delete temp directory used for deletion') } - const entries = await readdirOrError(path) + const entries = ent.isDirectory() ? await readdirOrError(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !(await opt.filter(path))) { return false } @@ -100,7 +128,7 @@ export const rimrafMoveRemove = async ( const removedAll = ( await Promise.all( - entries.map(entry => rimrafMoveRemove(resolve(path, entry.name), opt)) + entries.map(ent => rimrafMoveRemoveDir(resolve(path, ent.name), opt, ent)) ) ).reduce((a, b) => a && b, true) if (!removedAll) { @@ -130,15 +158,32 @@ const tmpUnlink = async ( return await rm(tmpFile) } -export const rimrafMoveRemoveSync = ( +export const rimrafMoveRemoveSync = (path: string, opt: RimrafSyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return rimrafMoveRemoveDirSync(path, opt, lstatSync(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafMoveRemoveDirSync = ( path: string, - opt: RimrafSyncOptions + opt: RimrafSyncOptions, + ent: Dirent | Stats ): boolean => { if (opt?.signal?.aborted) { throw opt.signal.reason } if (!opt.tmp) { - return rimrafMoveRemoveSync(path, { ...opt, tmp: defaultTmpSync(path) }) + return rimrafMoveRemoveDirSync( + path, + { ...opt, tmp: defaultTmpSync(path) }, + ent + ) } const tmp: string = opt.tmp @@ -146,14 +191,20 @@ export const rimrafMoveRemoveSync = ( throw new Error('cannot delete temp directory used for deletion') } - const entries = readdirOrErrorSync(path) + const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !opt.filter(path)) { return false } @@ -162,9 +213,9 @@ export const rimrafMoveRemoveSync = ( } let removedAll = true - for (const entry of entries) { - removedAll = - rimrafMoveRemoveSync(resolve(path, entry.name), opt) && removedAll + for (const ent of entries) { + const p = resolve(path, ent.name) + removedAll = rimrafMoveRemoveDirSync(p, opt, ent) && removedAll } if (!removedAll) { return false diff --git a/src/rimraf-posix.ts b/src/rimraf-posix.ts index cb109037..c26b4884 100644 --- a/src/rimraf-posix.ts +++ b/src/rimraf-posix.ts @@ -1,35 +1,66 @@ // the simple recursive removal, where unlink and rmdir are atomic // Note that this approach does NOT work on Windows! -// We rmdir before unlink even though that is arguably less efficient -// (since the average folder contains >1 file, it means more system -// calls), because sunos will let root unlink a directory, and some +// We stat first and only unlink if the Dirent isn't a directory, +// because sunos will let root unlink a directory, and some // SUPER weird breakage happens as a result. -import { promises, rmdirSync, unlinkSync } from './fs.js' -const { rmdir, unlink } = promises +import { lstatSync, promises, rmdirSync, unlinkSync } from './fs.js' +const { lstat, rmdir, unlink } = promises import { parse, resolve } from 'path' import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' +import { Dirent, Stats } from 'fs' import { RimrafAsyncOptions, RimrafSyncOptions } from '.' import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' -export const rimrafPosix = async ( +export const rimrafPosix = async (path: string, opt: RimrafAsyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return await rimrafPosixDir(path, opt, await lstat(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +export const rimrafPosixSync = (path: string, opt: RimrafSyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return rimrafPosixDirSync(path, opt, lstatSync(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafPosixDir = async ( path: string, - opt: RimrafAsyncOptions + opt: RimrafAsyncOptions, + ent: Dirent | Stats ): Promise => { if (opt?.signal?.aborted) { throw opt.signal.reason } - const entries = await readdirOrError(path) + const entries = ent.isDirectory() ? await readdirOrError(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !(await opt.filter(path))) { return false } @@ -39,7 +70,7 @@ export const rimrafPosix = async ( const removedAll = ( await Promise.all( - entries.map(entry => rimrafPosix(resolve(path, entry.name), opt)) + entries.map(ent => rimrafPosixDir(resolve(path, ent.name), opt, ent)) ) ).reduce((a, b) => a && b, true) @@ -62,21 +93,28 @@ export const rimrafPosix = async ( return true } -export const rimrafPosixSync = ( +const rimrafPosixDirSync = ( path: string, - opt: RimrafSyncOptions + opt: RimrafSyncOptions, + ent: Dirent | Stats ): boolean => { if (opt?.signal?.aborted) { throw opt.signal.reason } - const entries = readdirOrErrorSync(path) + const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !opt.filter(path)) { return false } @@ -84,8 +122,9 @@ export const rimrafPosixSync = ( return true } let removedAll: boolean = true - for (const entry of entries) { - removedAll = rimrafPosixSync(resolve(path, entry.name), opt) && removedAll + for (const ent of entries) { + const p = resolve(path, ent.name) + removedAll = rimrafPosixDirSync(p, opt, ent) && removedAll } if (opt.preserveRoot === false && path === parse(path).root) { return false diff --git a/src/rimraf-windows.ts b/src/rimraf-windows.ts index 2b9b1296..7ee088e9 100644 --- a/src/rimraf-windows.ts +++ b/src/rimraf-windows.ts @@ -8,20 +8,21 @@ // // Note: "move then remove" is 2-10 times slower, and just as unreliable. +import { Dirent, Stats } from 'fs' import { parse, resolve } from 'path' import { RimrafAsyncOptions, RimrafSyncOptions } from '.' import { fixEPERM, fixEPERMSync } from './fix-eperm.js' -import { promises, rmdirSync, unlinkSync } from './fs.js' +import { lstatSync, promises, rmdirSync, unlinkSync } from './fs.js' import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' import { retryBusy, retryBusySync } from './retry-busy.js' import { rimrafMoveRemove, rimrafMoveRemoveSync } from './rimraf-move-remove.js' -const { unlink, rmdir } = promises +const { unlink, rmdir, lstat } = promises const rimrafWindowsFile = retryBusy(fixEPERM(unlink)) const rimrafWindowsFileSync = retryBusySync(fixEPERMSync(unlinkSync)) -const rimrafWindowsDir = retryBusy(fixEPERM(rmdir)) -const rimrafWindowsDirSync = retryBusySync(fixEPERMSync(rmdirSync)) +const rimrafWindowsDirRetry = retryBusy(fixEPERM(rmdir)) +const rimrafWindowsDirRetrySync = retryBusySync(fixEPERMSync(rmdirSync)) const rimrafWindowsDirMoveRemoveFallback = async ( path: string, @@ -35,7 +36,7 @@ const rimrafWindowsDirMoveRemoveFallback = async ( // already filtered, remove from options so we don't call unnecessarily const { filter, ...options } = opt try { - return await rimrafWindowsDir(path, options) + return await rimrafWindowsDirRetry(path, options) } catch (er) { if ((er as NodeJS.ErrnoException)?.code === 'ENOTEMPTY') { return await rimrafMoveRemove(path, options) @@ -54,7 +55,7 @@ const rimrafWindowsDirMoveRemoveFallbackSync = ( // already filtered, remove from options so we don't call unnecessarily const { filter, ...options } = opt try { - return rimrafWindowsDirSync(path, options) + return rimrafWindowsDirRetrySync(path, options) } catch (er) { const fer = er as NodeJS.ErrnoException if (fer?.code === 'ENOTEMPTY') { @@ -67,28 +68,55 @@ const rimrafWindowsDirMoveRemoveFallbackSync = ( const START = Symbol('start') const CHILD = Symbol('child') const FINISH = Symbol('finish') -const states = new Set([START, CHILD, FINISH]) -export const rimrafWindows = async ( +export const rimrafWindows = async (path: string, opt: RimrafAsyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return await rimrafWindowsDir(path, opt, await lstat(path), START) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +export const rimrafWindowsSync = (path: string, opt: RimrafSyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return rimrafWindowsDirSync(path, opt, lstatSync(path), START) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafWindowsDir = async ( path: string, opt: RimrafAsyncOptions, + ent: Dirent | Stats, state = START ): Promise => { if (opt?.signal?.aborted) { throw opt.signal.reason } - if (!states.has(state)) { - throw new TypeError('invalid third argument passed to rimraf') - } - const entries = await readdirOrError(path) + const entries = ent.isDirectory() ? await readdirOrError(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !(await opt.filter(path))) { return false } @@ -100,12 +128,12 @@ export const rimrafWindows = async ( const s = state === START ? CHILD : state const removedAll = ( await Promise.all( - entries.map(entry => rimrafWindows(resolve(path, entry.name), opt, s)) + entries.map(ent => rimrafWindowsDir(resolve(path, ent.name), opt, ent, s)) ) ).reduce((a, b) => a && b, true) if (state === START) { - return rimrafWindows(path, opt, FINISH) + return rimrafWindowsDir(path, opt, ent, FINISH) } else if (state === FINISH) { if (opt.preserveRoot === false && path === parse(path).root) { return false @@ -121,23 +149,26 @@ export const rimrafWindows = async ( return true } -export const rimrafWindowsSync = ( +const rimrafWindowsDirSync = ( path: string, opt: RimrafSyncOptions, + ent: Dirent | Stats, state = START ): boolean => { - if (!states.has(state)) { - throw new TypeError('invalid third argument passed to rimraf') - } - - const entries = readdirOrErrorSync(path) + const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !opt.filter(path)) { return false } @@ -147,13 +178,14 @@ export const rimrafWindowsSync = ( } let removedAll = true - for (const entry of entries) { + for (const ent of entries) { const s = state === START ? CHILD : state - removedAll = rimrafWindowsSync(resolve(path, entry.name), opt, s) && removedAll + const p = resolve(path, ent.name) + removedAll = rimrafWindowsDirSync(p, opt, ent, s) && removedAll } if (state === START) { - return rimrafWindowsSync(path, opt, FINISH) + return rimrafWindowsDirSync(path, opt, ent, FINISH) } else if (state === FINISH) { if (opt.preserveRoot === false && path === parse(path).root) { return false diff --git a/test/index.js b/test/index.js index ca23438f..ee379bf4 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,4 @@ -const {statSync} = require('fs') +const { statSync } = require('fs') const t = require('tap') t.same( @@ -225,14 +225,14 @@ t.test('deleting globs', t => { t.test('sync', t => { const cwd = t.testdir(fixture) - rimrafSync('**/f/**/m', { glob: { cwd }}) + rimrafSync('**/f/**/m', { glob: { cwd } }) t.throws(() => statSync(cwd + '/c/f/i/m')) statSync(cwd + '/c/f/i/l') t.end() }) t.test('async', async t => { const cwd = t.testdir(fixture) - await rimraf('**/f/**/m', { glob: { cwd }}) + await rimraf('**/f/**/m', { glob: { cwd } }) t.throws(() => statSync(cwd + '/c/f/i/m')) statSync(cwd + '/c/f/i/l') }) diff --git a/test/rimraf-move-remove.js b/test/rimraf-move-remove.js index 1c98e56e..78055b6d 100644 --- a/test/rimraf-move-remove.js +++ b/test/rimraf-move-remove.js @@ -275,11 +275,13 @@ t.test('refuse to delete the root dir', async t => { } ) + const d = t.testdir({}) + // not brave enough to pass the actual c:\\ here... - t.throws(() => rimrafMoveRemoveSync('some-path', { tmp: 'some-path' }), { + t.throws(() => rimrafMoveRemoveSync(d, { tmp: d }), { message: 'cannot delete temp directory used for deletion', }) - t.rejects(() => rimrafMoveRemove('some-path', { tmp: 'some-path' }), { + t.rejects(() => rimrafMoveRemove(d, { tmp: d }), { message: 'cannot delete temp directory used for deletion', }) }) @@ -520,6 +522,22 @@ t.test( t.throws(() => rimrafMoveRemoveSync(d, { signal })) t.end() }) + t.test('sync abort in filter', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const opt = { + signal, + filter: p => { + if (basename(p) === 'g') { + ac.abort(new Error('done')) + } + return true + }, + } + t.throws(() => rimrafMoveRemoveSync(d, opt), { message: 'done' }) + t.end() + }) t.test('async', async t => { const ac = new AbortController() const { signal } = ac @@ -650,3 +668,36 @@ t.test('filter function', t => { } t.end() }) + +t.test('do not follow symlinks', t => { + const { + rimrafMoveRemove, + rimrafMoveRemoveSync, + } = require('../dist/cjs/src/rimraf-move-remove.js') + const fixture = { + x: { + y: t.fixture('symlink', '../z'), + z: '', + }, + z: { + a: '', + b: { c: '' }, + }, + } + t.test('sync', t => { + const d = t.testdir(fixture) + t.equal(rimrafMoveRemoveSync(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + t.equal(await rimrafMoveRemove(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + }) + t.end() +}) diff --git a/test/rimraf-posix.js b/test/rimraf-posix.js index 7c7e0b28..897129bb 100644 --- a/test/rimraf-posix.js +++ b/test/rimraf-posix.js @@ -223,6 +223,22 @@ t.test( t.throws(() => rimrafPosixSync(d, { signal })) t.end() }) + t.test('sync abort in filter', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const opt = { + signal, + filter: p => { + if (basename(p) === 'g') { + ac.abort(new Error('done')) + } + return true + }, + } + t.throws(() => rimrafPosixSync(d, opt), { message: 'done' }) + t.end() + }) t.test('async', async t => { const d = t.testdir(fixture) const ac = new AbortController() @@ -231,6 +247,13 @@ t.test( ac.abort(new Error('aborted rimraf')) await p }) + t.test('async preaborted', async t => { + const d = t.testdir(fixture) + const ac = new AbortController() + ac.abort(new Error('aborted rimraf')) + const { signal } = ac + await t.rejects(() => rimrafPosix(d, { signal })) + }) t.end() } ) @@ -346,3 +369,32 @@ t.test('filter function', t => { } t.end() }) + +t.test('do not follow symlinks', t => { + const fixture = { + x: { + y: t.fixture('symlink', '../z'), + z: '', + }, + z: { + a: '', + b: { c: '' }, + }, + } + t.test('sync', t => { + const d = t.testdir(fixture) + t.equal(rimrafPosixSync(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + t.equal(await rimrafPosix(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + }) + t.end() +}) diff --git a/test/rimraf-windows.js b/test/rimraf-windows.js index 6e4722e6..34ed7d77 100644 --- a/test/rimraf-windows.js +++ b/test/rimraf-windows.js @@ -486,16 +486,6 @@ t.test('rimraffing root, do not actually rmdir root', async t => { t.end() }) -t.test('do not allow third arg', async t => { - const ROOT = t.testdir(fixture) - const { - rimrafWindows, - rimrafWindowsSync, - } = require('../dist/cjs/src/rimraf-windows.js') - t.rejects(rimrafWindows(ROOT, {}, true)) - t.throws(() => rimrafWindowsSync(ROOT, {}, true)) -}) - t.test( 'abort on signal', { skip: typeof AbortController === 'undefined' }, @@ -512,6 +502,22 @@ t.test( t.throws(() => rimrafWindowsSync(d, { signal })) t.end() }) + t.test('sync abort in filter', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const opt = { + signal, + filter: p => { + if (basename(p) === 'g') { + ac.abort(new Error('done')) + } + return true + }, + } + t.throws(() => rimrafWindowsSync(d, opt), { message: 'done' }) + t.end() + }) t.test('async', async t => { const d = t.testdir(fixture) const ac = new AbortController() @@ -642,3 +648,36 @@ t.test('filter function', t => { } t.end() }) + +t.test('do not follow symlinks', t => { + const { + rimrafWindows, + rimrafWindowsSync, + } = require('../dist/cjs/src/rimraf-windows.js') + const fixture = { + x: { + y: t.fixture('symlink', '../z'), + z: '', + }, + z: { + a: '', + b: { c: '' }, + }, + } + t.test('sync', t => { + const d = t.testdir(fixture) + t.equal(rimrafWindowsSync(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + t.equal(await rimrafWindows(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + }) + t.end() +})