From 9e7550be045525afbd45bd7d591f3f539d48ca50 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 5 Jul 2019 11:15:23 +0700 Subject: [PATCH 01/12] Add note about backward-slashes Fixes #93 --- index.d.ts | 4 ++++ readme.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/index.d.ts b/index.d.ts index 2ef233d..f6366c3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,6 +40,8 @@ declare const del: { /** Delete files and directories using glob patterns. + Note that glob patterns can only contain forward-slashes, not backward-slashes, so if you want to construct a glob pattern from path components, you need to use `path.posix.join()` instead of `path.join()`. + @param patterns - See the supported [glob patterns](https://github.com/sindresorhus/globby#globbing-patterns). - [Pattern examples with expected matches](https://github.com/sindresorhus/multimatch/blob/master/test/test.js) - [Quick globbing pattern overview](https://github.com/sindresorhus/multimatch#globbing-patterns) @@ -65,6 +67,8 @@ declare const del: { /** Synchronously delete files and directories using glob patterns. + Note that glob patterns can only contain forward-slashes, not backward-slashes, so if you want to construct a glob pattern from path components, you need to use `path.posix.join()` instead of `path.join()`. + @param patterns - See the supported [glob patterns](https://github.com/sindresorhus/globby#globbing-patterns). - [Pattern examples with expected matches](https://github.com/sindresorhus/multimatch/blob/master/test/test.js) - [Quick globbing pattern overview](https://github.com/sindresorhus/multimatch#globbing-patterns) diff --git a/readme.md b/readme.md index 59f83a1..79d5280 100644 --- a/readme.md +++ b/readme.md @@ -46,6 +46,8 @@ Suggestions on how to improve this welcome! ## API +Note that glob patterns can only contain forward-slashes, not backward-slashes, so if you want to construct a glob pattern from path components, you need to use `path.posix.join()` instead of `path.join()`. + ### del(patterns, options?) Returns `Promise` with the deleted paths. From 8efdbcdaed7f8eeafa93372fcb3e64edc20b7703 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Fri, 12 Jul 2019 11:04:58 -0700 Subject: [PATCH 02/12] Prevent race condition on macOS when deleting files (#95) --- index.js | 8 ++++-- test.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 1d15158..408a9a6 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,8 @@ module.exports = async (patterns, {force, dryRun, ...options} = {}) => { ...options }; - const files = await globby(patterns, options); + const files = (await globby(patterns, options)) + .sort((a, b) => b.localeCompare(a)); const mapper = async file => { if (!force) { @@ -54,7 +55,10 @@ module.exports.sync = (patterns, {force, dryRun, ...options} = {}) => { ...options }; - return globby.sync(patterns, options).map(file => { + const files = globby.sync(patterns, options) + .sort((a, b) => b.localeCompare(a)); + + return files.map(file => { if (!force) { safeCheck(file); } diff --git a/test.js b/test.js index 036cd32..6d6fee5 100644 --- a/test.js +++ b/test.js @@ -88,9 +88,9 @@ test('don\'t delete files, but return them - async', async t => { }); exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); t.deepEqual(deletedFiles, [ - path.join(t.context.tmp, '2.tmp'), + path.join(t.context.tmp, '4.tmp'), path.join(t.context.tmp, '3.tmp'), - path.join(t.context.tmp, '4.tmp') + path.join(t.context.tmp, '2.tmp') ]); }); @@ -101,8 +101,79 @@ test('don\'t delete files, but return them - sync', t => { }); exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); t.deepEqual(deletedFiles, [ - path.join(t.context.tmp, '2.tmp'), + path.join(t.context.tmp, '4.tmp'), path.join(t.context.tmp, '3.tmp'), - path.join(t.context.tmp, '4.tmp') + path.join(t.context.tmp, '2.tmp') ]); }); + +// Currently this only testable locally on an osx machine. +// https://github.com/sindresorhus/del/issues/68 +test.serial('does not throw EINVAL - async', async t => { + await del('**/*', { + cwd: t.context.tmp, + dot: true + }); + + const nestedFile = path.resolve(t.context.tmp, 'a/b/c/nested.js'); + const totalAttempts = 200; + + let count = 0; + while (count !== totalAttempts) { + makeDir.sync(nestedFile); + + // eslint-disable-next-line no-await-in-loop + const removed = await del('**/*', { + cwd: t.context.tmp, + dot: true + }); + + const expected = [ + path.resolve(t.context.tmp, 'a/b/c/nested.js'), + path.resolve(t.context.tmp, 'a/b/c'), + path.resolve(t.context.tmp, 'a/b'), + path.resolve(t.context.tmp, 'a') + ]; + + t.deepEqual(removed, expected); + + count += 1; + } + + notExists(t, [...fixtures, 'a']); + t.is(count, totalAttempts); +}); + +test.serial('does not throw EINVAL - sync', t => { + del.sync('**/*', { + cwd: t.context.tmp, + dot: true + }); + + const nestedFile = path.resolve(t.context.tmp, 'a/b/c/nested.js'); + const totalAttempts = 200; + + let count = 0; + while (count !== totalAttempts) { + makeDir.sync(nestedFile); + + const removed = del.sync('**/*', { + cwd: t.context.tmp, + dot: true + }); + + const expected = [ + path.resolve(t.context.tmp, 'a/b/c/nested.js'), + path.resolve(t.context.tmp, 'a/b/c'), + path.resolve(t.context.tmp, 'a/b'), + path.resolve(t.context.tmp, 'a') + ]; + + t.deepEqual(removed, expected); + + count += 1; + } + + notExists(t, [...fixtures, 'a']); + t.is(count, totalAttempts); +}); From ffbf4c423c2c6305e0a98aa5445d1767bca8d9d8 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Fri, 12 Jul 2019 11:44:35 -0700 Subject: [PATCH 03/12] Fix the `cwd` option (#96) --- index.js | 24 +++++---- package.json | 2 +- test.js | 138 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 148 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 408a9a6..9d8251c 100644 --- a/index.js +++ b/index.js @@ -3,27 +3,28 @@ const {promisify} = require('util'); const path = require('path'); const globby = require('globby'); const isPathCwd = require('is-path-cwd'); -const isPathInCwd = require('is-path-in-cwd'); +const isPathInside = require('is-path-inside'); const rimraf = require('rimraf'); const pMap = require('p-map'); const rimrafP = promisify(rimraf); -function safeCheck(file) { +function safeCheck(file, cwd) { if (isPathCwd(file)) { throw new Error('Cannot delete the current working directory. Can be overridden with the `force` option.'); } - if (!isPathInCwd(file)) { + if (!isPathInside(file, cwd)) { throw new Error('Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.'); } } -module.exports = async (patterns, {force, dryRun, ...options} = {}) => { +module.exports = async (patterns, {force, dryRun, cwd = process.cwd(), ...options} = {}) => { options = { expandDirectories: false, onlyFiles: false, followSymbolicLinks: false, + cwd, ...options }; @@ -31,12 +32,12 @@ module.exports = async (patterns, {force, dryRun, ...options} = {}) => { .sort((a, b) => b.localeCompare(a)); const mapper = async file => { + file = path.resolve(cwd, file); + if (!force) { - safeCheck(file); + safeCheck(file, cwd); } - file = path.resolve(options.cwd || '', file); - if (!dryRun) { await rimrafP(file, {glob: false}); } @@ -47,11 +48,12 @@ module.exports = async (patterns, {force, dryRun, ...options} = {}) => { return pMap(files, mapper, options); }; -module.exports.sync = (patterns, {force, dryRun, ...options} = {}) => { +module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options} = {}) => { options = { expandDirectories: false, onlyFiles: false, followSymbolicLinks: false, + cwd, ...options }; @@ -59,12 +61,12 @@ module.exports.sync = (patterns, {force, dryRun, ...options} = {}) => { .sort((a, b) => b.localeCompare(a)); return files.map(file => { + file = path.resolve(cwd, file); + if (!force) { - safeCheck(file); + safeCheck(file, cwd); } - file = path.resolve(options.cwd || '', file); - if (!dryRun) { rimraf.sync(file, {glob: false}); } diff --git a/package.json b/package.json index c4bbb12..054bfb2 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "dependencies": { "globby": "^10.0.0", "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", + "is-path-inside": "^3.0.1", "p-map": "^2.0.0", "rimraf": "^2.6.3" }, diff --git a/test.js b/test.js index 6d6fee5..a106823 100644 --- a/test.js +++ b/test.js @@ -1,10 +1,12 @@ import path from 'path'; import fs from 'fs'; -import test from 'ava'; +import {serial as test} from 'ava'; import tempy from 'tempy'; import makeDir from 'make-dir'; import del from '.'; +const processCwd = process.cwd(); + function exists(t, files) { for (const file of files) { t.true(fs.existsSync(path.join(t.context.tmp, file))); @@ -67,7 +69,7 @@ test('take options into account - sync', t => { notExists(t, ['2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); }); -test.serial('return deleted files - async', async t => { +test('return deleted files - async', async t => { t.deepEqual( await del('1.tmp', {cwd: t.context.tmp}), [path.join(t.context.tmp, '1.tmp')] @@ -109,7 +111,7 @@ test('don\'t delete files, but return them - sync', t => { // Currently this only testable locally on an osx machine. // https://github.com/sindresorhus/del/issues/68 -test.serial('does not throw EINVAL - async', async t => { +test('does not throw EINVAL - async', async t => { await del('**/*', { cwd: t.context.tmp, dot: true @@ -144,7 +146,7 @@ test.serial('does not throw EINVAL - async', async t => { t.is(count, totalAttempts); }); -test.serial('does not throw EINVAL - sync', t => { +test('does not throw EINVAL - sync', t => { del.sync('**/*', { cwd: t.context.tmp, dot: true @@ -177,3 +179,131 @@ test.serial('does not throw EINVAL - sync', t => { notExists(t, [...fixtures, 'a']); t.is(count, totalAttempts); }); + +test('delete relative files outside of process.cwd using cwd - async', async t => { + await del(['1.tmp'], {cwd: t.context.tmp}); + + exists(t, ['2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + notExists(t, ['1.tmp']); +}); + +test('delete relative files outside of process.cwd using cwd - sync', t => { + del.sync(['1.tmp'], {cwd: t.context.tmp}); + + exists(t, ['2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + notExists(t, ['1.tmp']); +}); + +test('delete absolute files outside of process.cwd using cwd - async', async t => { + const absolutePath = path.resolve(t.context.tmp, '1.tmp'); + await del([absolutePath], {cwd: t.context.tmp}); + + exists(t, ['2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + notExists(t, ['1.tmp']); +}); + +test('delete absolute files outside of process.cwd using cwd - sync', t => { + const absolutePath = path.resolve(t.context.tmp, '1.tmp'); + del.sync([absolutePath], {cwd: t.context.tmp}); + + exists(t, ['2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + notExists(t, ['1.tmp']); +}); + +test('cannot delete actual working directory without force: true - async', async t => { + process.chdir(t.context.tmp); + + await t.throwsAsync(() => del([t.context.tmp]), { + instanceOf: Error, + message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['', '1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + process.chdir(processCwd); +}); + +test('cannot delete actual working directory without force: true - sync', t => { + process.chdir(t.context.tmp); + + t.throws(() => del.sync([t.context.tmp]), { + instanceOf: Error, + message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['', '1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + process.chdir(processCwd); +}); + +test('cannot delete actual working directory with cwd option without force: true - async', async t => { + process.chdir(t.context.tmp); + + await t.throwsAsync(() => del([t.context.tmp], {cwd: __dirname}), { + instanceOf: Error, + message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['', '1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + process.chdir(processCwd); +}); + +test('cannot delete actual working directory with cwd option without force: true - sync', t => { + process.chdir(t.context.tmp); + + t.throws(() => del.sync([t.context.tmp], {cwd: __dirname}), { + instanceOf: Error, + message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['', '1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + process.chdir(processCwd); +}); + +test('cannot delete files outside cwd without force: true - async', async t => { + const absolutePath = path.resolve(t.context.tmp, '1.tmp'); + + await t.throwsAsync(() => del([absolutePath]), { + instanceOf: Error, + message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); +}); + +test('cannot delete files outside cwd without force: true - sync', t => { + const absolutePath = path.resolve(t.context.tmp, '1.tmp'); + + t.throws(() => del.sync([absolutePath]), { + instanceOf: Error, + message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['', '1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); +}); + +test('cannot delete files inside process.cwd when outside cwd without force: true - async', async t => { + process.chdir(t.context.tmp); + const removeFile = path.resolve(t.context.tmp, '2.tmp'); + const cwd = path.resolve(t.context.tmp, '1.tmp'); + + await t.throwsAsync(() => del([removeFile], {cwd}), { + instanceOf: Error, + message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + process.chdir(processCwd); +}); + +test('cannot delete files inside process.cwd when outside cwd without force: true - sync', t => { + process.chdir(t.context.tmp); + const removeFile = path.resolve(t.context.tmp, '2.tmp'); + const cwd = path.resolve(t.context.tmp, '1.tmp'); + + t.throws(() => del.sync([removeFile], {cwd}), { + instanceOf: Error, + message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' + }); + + exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); + process.chdir(processCwd); +}); From 902b594236001cabb737e731bdb04f8093b0bc61 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 13 Jul 2019 01:47:14 +0700 Subject: [PATCH 04/12] Meta tweaks --- test.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/test.js b/test.js index a106823..8b8632a 100644 --- a/test.js +++ b/test.js @@ -109,7 +109,7 @@ test('don\'t delete files, but return them - sync', t => { ]); }); -// Currently this only testable locally on an osx machine. +// Currently this is only testable locally on macOS. // https://github.com/sindresorhus/del/issues/68 test('does not throw EINVAL - async', async t => { await del('**/*', { @@ -213,7 +213,7 @@ test('delete absolute files outside of process.cwd using cwd - sync', t => { test('cannot delete actual working directory without force: true - async', async t => { process.chdir(t.context.tmp); - await t.throwsAsync(() => del([t.context.tmp]), { + await t.throwsAsync(del([t.context.tmp]), { instanceOf: Error, message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' }); @@ -225,7 +225,9 @@ test('cannot delete actual working directory without force: true - async', async test('cannot delete actual working directory without force: true - sync', t => { process.chdir(t.context.tmp); - t.throws(() => del.sync([t.context.tmp]), { + t.throws(() => { + del.sync([t.context.tmp]); + }, { instanceOf: Error, message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' }); @@ -237,7 +239,7 @@ test('cannot delete actual working directory without force: true - sync', t => { test('cannot delete actual working directory with cwd option without force: true - async', async t => { process.chdir(t.context.tmp); - await t.throwsAsync(() => del([t.context.tmp], {cwd: __dirname}), { + await t.throwsAsync(del([t.context.tmp], {cwd: __dirname}), { instanceOf: Error, message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' }); @@ -249,7 +251,9 @@ test('cannot delete actual working directory with cwd option without force: true test('cannot delete actual working directory with cwd option without force: true - sync', t => { process.chdir(t.context.tmp); - t.throws(() => del.sync([t.context.tmp], {cwd: __dirname}), { + t.throws(() => { + del.sync([t.context.tmp], {cwd: __dirname}); + }, { instanceOf: Error, message: 'Cannot delete the current working directory. Can be overridden with the `force` option.' }); @@ -261,7 +265,7 @@ test('cannot delete actual working directory with cwd option without force: true test('cannot delete files outside cwd without force: true - async', async t => { const absolutePath = path.resolve(t.context.tmp, '1.tmp'); - await t.throwsAsync(() => del([absolutePath]), { + await t.throwsAsync(del([absolutePath]), { instanceOf: Error, message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' }); @@ -272,7 +276,9 @@ test('cannot delete files outside cwd without force: true - async', async t => { test('cannot delete files outside cwd without force: true - sync', t => { const absolutePath = path.resolve(t.context.tmp, '1.tmp'); - t.throws(() => del.sync([absolutePath]), { + t.throws(() => { + del.sync([absolutePath]); + }, { instanceOf: Error, message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' }); @@ -285,7 +291,7 @@ test('cannot delete files inside process.cwd when outside cwd without force: tru const removeFile = path.resolve(t.context.tmp, '2.tmp'); const cwd = path.resolve(t.context.tmp, '1.tmp'); - await t.throwsAsync(() => del([removeFile], {cwd}), { + await t.throwsAsync(del([removeFile], {cwd}), { instanceOf: Error, message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' }); @@ -299,7 +305,9 @@ test('cannot delete files inside process.cwd when outside cwd without force: tru const removeFile = path.resolve(t.context.tmp, '2.tmp'); const cwd = path.resolve(t.context.tmp, '1.tmp'); - t.throws(() => del.sync([removeFile], {cwd}), { + t.throws(() => { + del.sync([removeFile], {cwd}); + }, { instanceOf: Error, message: 'Cannot delete files/directories outside the current working directory. Can be overridden with the `force` option.' }); From 51662acf8c0b0632ed577fc2bb64a65a677ead9b Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Fri, 12 Jul 2019 22:53:01 -0700 Subject: [PATCH 05/12] Reverse order back for the returned paths (#99) --- index.js | 12 ++++++++++-- test.js | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 9d8251c..953359b 100644 --- a/index.js +++ b/index.js @@ -45,7 +45,11 @@ module.exports = async (patterns, {force, dryRun, cwd = process.cwd(), ...option return file; }; - return pMap(files, mapper, options); + const removedFiles = await pMap(files, mapper, options); + + removedFiles.reverse(); + + return removedFiles; }; module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options} = {}) => { @@ -60,7 +64,7 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options const files = globby.sync(patterns, options) .sort((a, b) => b.localeCompare(a)); - return files.map(file => { + const removedFiles = files.map(file => { file = path.resolve(cwd, file); if (!force) { @@ -73,4 +77,8 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options return file; }); + + removedFiles.reverse(); + + return removedFiles; }; diff --git a/test.js b/test.js index 8b8632a..edf47c1 100644 --- a/test.js +++ b/test.js @@ -90,9 +90,9 @@ test('don\'t delete files, but return them - async', async t => { }); exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); t.deepEqual(deletedFiles, [ - path.join(t.context.tmp, '4.tmp'), + path.join(t.context.tmp, '2.tmp'), path.join(t.context.tmp, '3.tmp'), - path.join(t.context.tmp, '2.tmp') + path.join(t.context.tmp, '4.tmp') ]); }); @@ -103,9 +103,9 @@ test('don\'t delete files, but return them - sync', t => { }); exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); t.deepEqual(deletedFiles, [ - path.join(t.context.tmp, '4.tmp'), + path.join(t.context.tmp, '2.tmp'), path.join(t.context.tmp, '3.tmp'), - path.join(t.context.tmp, '2.tmp') + path.join(t.context.tmp, '4.tmp') ]); }); @@ -131,10 +131,10 @@ test('does not throw EINVAL - async', async t => { }); const expected = [ - path.resolve(t.context.tmp, 'a/b/c/nested.js'), - path.resolve(t.context.tmp, 'a/b/c'), + path.resolve(t.context.tmp, 'a'), path.resolve(t.context.tmp, 'a/b'), - path.resolve(t.context.tmp, 'a') + path.resolve(t.context.tmp, 'a/b/c'), + path.resolve(t.context.tmp, 'a/b/c/nested.js') ]; t.deepEqual(removed, expected); @@ -165,10 +165,10 @@ test('does not throw EINVAL - sync', t => { }); const expected = [ - path.resolve(t.context.tmp, 'a/b/c/nested.js'), - path.resolve(t.context.tmp, 'a/b/c'), + path.resolve(t.context.tmp, 'a'), path.resolve(t.context.tmp, 'a/b'), - path.resolve(t.context.tmp, 'a') + path.resolve(t.context.tmp, 'a/b/c'), + path.resolve(t.context.tmp, 'a/b/c/nested.js') ]; t.deepEqual(removed, expected); From ca05c65ca582b47a20e1b64539dd5ef45a0e8419 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Tue, 16 Jul 2019 10:46:59 -0700 Subject: [PATCH 06/12] Sort removed files (#102) --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 953359b..f9637f0 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ module.exports = async (patterns, {force, dryRun, cwd = process.cwd(), ...option const removedFiles = await pMap(files, mapper, options); - removedFiles.reverse(); + removedFiles.sort((a, b) => a.localeCompare(b)); return removedFiles; }; @@ -78,7 +78,7 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options return file; }); - removedFiles.reverse(); + removedFiles.sort((a, b) => a.localeCompare(b)); return removedFiles; }; From f509a8904330d23703a5f0f3d81081ab40aef2cb Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Thu, 22 Aug 2019 02:49:19 -0700 Subject: [PATCH 07/12] Update dependencies (#109) --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 054bfb2..fb2fb5e 100644 --- a/package.json +++ b/package.json @@ -47,11 +47,11 @@ "globby": "^10.0.0", "is-path-cwd": "^2.0.0", "is-path-inside": "^3.0.1", - "p-map": "^2.0.0", - "rimraf": "^2.6.3" + "p-map": "^3.0.0", + "rimraf": "^3.0.0" }, "devDependencies": { - "ava": "^2.1.0", + "ava": "^2.3.0", "make-dir": "^3.0.0", "tempy": "^0.3.0", "tsd": "^0.7.3", From 1299747b7524b8390d1187d4de23d200d214a901 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Thu, 22 Aug 2019 02:50:47 -0700 Subject: [PATCH 08/12] Use graceful-fs (#108) --- index.js | 21 +++++++++++++++++++-- package.json | 1 + 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index f9637f0..cbdde5a 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const {promisify} = require('util'); const path = require('path'); const globby = require('globby'); +const gracefulFs = require('graceful-fs'); const isPathCwd = require('is-path-cwd'); const isPathInside = require('is-path-inside'); const rimraf = require('rimraf'); @@ -9,6 +10,22 @@ const pMap = require('p-map'); const rimrafP = promisify(rimraf); +const rimrafOptions = { + glob: false, + unlink: gracefulFs.unlink, + unlinkSync: gracefulFs.unlinkSync, + chmod: gracefulFs.chmod, + chmodSync: gracefulFs.chmodSync, + stat: gracefulFs.stat, + statSync: gracefulFs.statSync, + lstat: gracefulFs.lstat, + lstatSync: gracefulFs.lstatSync, + rmdir: gracefulFs.rmdir, + rmdirSync: gracefulFs.rmdirSync, + readdir: gracefulFs.readdir, + readdirSync: gracefulFs.readdirSync +}; + function safeCheck(file, cwd) { if (isPathCwd(file)) { throw new Error('Cannot delete the current working directory. Can be overridden with the `force` option.'); @@ -39,7 +56,7 @@ module.exports = async (patterns, {force, dryRun, cwd = process.cwd(), ...option } if (!dryRun) { - await rimrafP(file, {glob: false}); + await rimrafP(file, rimrafOptions); } return file; @@ -72,7 +89,7 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options } if (!dryRun) { - rimraf.sync(file, {glob: false}); + rimraf.sync(file, rimrafOptions); } return file; diff --git a/package.json b/package.json index fb2fb5e..6b4ac47 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ ], "dependencies": { "globby": "^10.0.0", + "graceful-fs": "^4.2.2", "is-path-cwd": "^2.0.0", "is-path-inside": "^3.0.1", "p-map": "^3.0.0", From 9c722703afe32ab41a3bc5a77934d56976e75e64 Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Fri, 23 Aug 2019 03:59:57 -0700 Subject: [PATCH 09/12] Add benchmarks (#101) --- benchmark.js | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +++- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 benchmark.js diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 0000000..41cb7b3 --- /dev/null +++ b/benchmark.js @@ -0,0 +1,68 @@ +'use strict'; +const path = require('path'); +const Benchmark = require('benchmark'); +const makeDir = require('make-dir'); +const tempy = require('tempy'); +const del = require('.'); + +const suite = new Benchmark.Suite('concurrency'); + +const tempDir = tempy.directory(); + +const fixtures = Array.from({length: 2000}, (x, index) => { + return path.resolve(tempDir, (index + 1).toString()); +}); + +function createFixtures() { + for (const fixture of fixtures) { + makeDir.sync(path.resolve(tempDir, fixture)); + } +} + +const concurrencies = [1, 3, 5, 10, 15, 20, 50, 100, 200, 300, 400, 500, 1000, Infinity]; + +for (const concurrency of concurrencies) { + const name = `concurrency: ${concurrency.toString()}`; + + suite.add({ + name, + defer: true, + setup() {}, // This line breaks async await + async fn(deferred) { + // Can't use setup because it isn't called after every defer + // https://github.com/bestiejs/benchmark.js/issues/136 + createFixtures(); + + const removedFiles = await del(['**/*'], { + cwd: tempDir, + concurrency + }); + + if (removedFiles.length !== fixtures.length) { + const error = new Error( + `"${name}": files removed: ${removedFiles.length}, expected: ${fixtures.length}`, + ); + + console.error(error); + + del.sync(tempDir, {cwd: tempDir, force: true}); + + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); + } + + deferred.resolve(); + } + }); +} + +suite + .on('cycle', event => { + console.log(String(event.target)); + }) + .on('complete', function () { + console.log(`Fastest is ${this.filter('fastest').map('name')}`); + + del.sync(tempDir, {cwd: tempDir, force: true}); + }) + .run({async: true}); diff --git a/package.json b/package.json index 6b4ac47..df551d9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "node": ">=8" }, "scripts": { - "test": "xo && ava && tsd" + "test": "xo && ava && tsd", + "bench": "node benchmark.js" }, "files": [ "index.js", @@ -53,6 +54,7 @@ }, "devDependencies": { "ava": "^2.3.0", + "benchmark": "^2.1.4", "make-dir": "^3.0.0", "tempy": "^0.3.0", "tsd": "^0.7.3", From 01da91f548a318f33a82e9f09eb27f4e5c6cb94b Mon Sep 17 00:00:00 2001 From: Chris Blossom Date: Fri, 23 Aug 2019 04:00:59 -0700 Subject: [PATCH 10/12] Allow non-glob patterns with backslash on Windows (#100) This reverts commit 60e9d54446c2be7a718209f8a1e5bf92fa2dc99c --- .travis.yml | 4 ++++ index.d.ts | 8 ++++---- index.js | 20 ++++++++++++++++++++ package.json | 2 ++ readme.md | 4 ++-- test.js | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f98fed0..29ef4bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +os: + - linux + - osx + - windows language: node_js node_js: - '12' diff --git a/index.d.ts b/index.d.ts index f6366c3..b4d9767 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,12 +40,12 @@ declare const del: { /** Delete files and directories using glob patterns. - Note that glob patterns can only contain forward-slashes, not backward-slashes, so if you want to construct a glob pattern from path components, you need to use `path.posix.join()` instead of `path.join()`. + Note that glob patterns can only contain forward-slashes, not backward-slashes. Windows file paths can use backward-slashes as long as the path does not contain any glob-like characters, otherwise use `path.posix.join()` instead of `path.join()`. @param patterns - See the supported [glob patterns](https://github.com/sindresorhus/globby#globbing-patterns). - [Pattern examples with expected matches](https://github.com/sindresorhus/multimatch/blob/master/test/test.js) - [Quick globbing pattern overview](https://github.com/sindresorhus/multimatch#globbing-patterns) - @param options - You can specify any of the [`globby` options](https://github.com/sindresorhus/globby#options) in addition to the `del` options. In constrast to the `globby` defaults, `expandDirectories`, `onlyFiles`, and `followSymbolicLinks` are `false` by default. + @param options - You can specify any of the [`globby` options](https://github.com/sindresorhus/globby#options) in addition to the `del` options. In contrast to the `globby` defaults, `expandDirectories`, `onlyFiles`, and `followSymbolicLinks` are `false` by default. @returns The deleted paths. @example @@ -67,12 +67,12 @@ declare const del: { /** Synchronously delete files and directories using glob patterns. - Note that glob patterns can only contain forward-slashes, not backward-slashes, so if you want to construct a glob pattern from path components, you need to use `path.posix.join()` instead of `path.join()`. + Note that glob patterns can only contain forward-slashes, not backward-slashes. Windows file paths can use backward-slashes as long as the path does not contain any glob-like characters, otherwise use `path.posix.join()` instead of `path.join()`. @param patterns - See the supported [glob patterns](https://github.com/sindresorhus/globby#globbing-patterns). - [Pattern examples with expected matches](https://github.com/sindresorhus/multimatch/blob/master/test/test.js) - [Quick globbing pattern overview](https://github.com/sindresorhus/multimatch#globbing-patterns) - @param options - You can specify any of the [`globby` options](https://github.com/sindresorhus/globby#options) in addition to the `del` options. In constrast to the `globby` defaults, `expandDirectories`, `onlyFiles`, and `followSymbolicLinks` are `false` by default. + @param options - You can specify any of the [`globby` options](https://github.com/sindresorhus/globby#options) in addition to the `del` options. In contrast to the `globby` defaults, `expandDirectories`, `onlyFiles`, and `followSymbolicLinks` are `false` by default. @returns The deleted paths. */ sync( diff --git a/index.js b/index.js index cbdde5a..e1956b8 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,8 @@ const {promisify} = require('util'); const path = require('path'); const globby = require('globby'); +const isGlob = require('is-glob'); +const slash = require('slash'); const gracefulFs = require('graceful-fs'); const isPathCwd = require('is-path-cwd'); const isPathInside = require('is-path-inside'); @@ -36,6 +38,20 @@ function safeCheck(file, cwd) { } } +function normalizePatterns(patterns) { + patterns = Array.isArray(patterns) ? patterns : [patterns]; + + patterns = patterns.map(pattern => { + if (process.platform === 'win32' && isGlob(pattern) === false) { + return slash(pattern); + } + + return pattern; + }); + + return patterns; +} + module.exports = async (patterns, {force, dryRun, cwd = process.cwd(), ...options} = {}) => { options = { expandDirectories: false, @@ -45,6 +61,8 @@ module.exports = async (patterns, {force, dryRun, cwd = process.cwd(), ...option ...options }; + patterns = normalizePatterns(patterns); + const files = (await globby(patterns, options)) .sort((a, b) => b.localeCompare(a)); @@ -78,6 +96,8 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options ...options }; + patterns = normalizePatterns(patterns); + const files = globby.sync(patterns, options) .sort((a, b) => b.localeCompare(a)); diff --git a/package.json b/package.json index df551d9..51b594f 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,10 @@ "dependencies": { "globby": "^10.0.0", "graceful-fs": "^4.2.2", + "is-glob": "^4.0.1", "is-path-cwd": "^2.0.0", "is-path-inside": "^3.0.1", + "slash": "^3.0.0", "p-map": "^3.0.0", "rimraf": "^3.0.0" }, diff --git a/readme.md b/readme.md index 79d5280..36c0f06 100644 --- a/readme.md +++ b/readme.md @@ -46,7 +46,7 @@ Suggestions on how to improve this welcome! ## API -Note that glob patterns can only contain forward-slashes, not backward-slashes, so if you want to construct a glob pattern from path components, you need to use `path.posix.join()` instead of `path.join()`. +Note that glob patterns can only contain forward-slashes, not backward-slashes. Windows file paths can use backward-slashes as long as the path does not contain any glob-like characters, otherwise use `path.posix.join()` instead of `path.join()`. ### del(patterns, options?) @@ -69,7 +69,7 @@ See the supported [glob patterns](https://github.com/sindresorhus/globby#globbin Type: `object` -You can specify any of the [`globby` options](https://github.com/sindresorhus/globby#options) in addition to the below options. In constrast to the `globby` defaults, `expandDirectories`, `onlyFiles`, and `followSymbolicLinks` are `false` by default. +You can specify any of the [`globby` options](https://github.com/sindresorhus/globby#options) in addition to the below options. In contrast to the `globby` defaults, `expandDirectories`, `onlyFiles`, and `followSymbolicLinks` are `false` by default. ##### force diff --git a/test.js b/test.js index edf47c1..d5a4fcf 100644 --- a/test.js +++ b/test.js @@ -315,3 +315,37 @@ test('cannot delete files inside process.cwd when outside cwd without force: tru exists(t, ['1.tmp', '2.tmp', '3.tmp', '4.tmp', '.dot.tmp']); process.chdir(processCwd); }); + +test('windows can pass absolute paths with "\\" - async', async t => { + const filePath = path.resolve(t.context.tmp, '1.tmp'); + + const removeFiles = await del([filePath], {cwd: t.context.tmp, dryRun: true}); + + t.deepEqual(removeFiles, [filePath]); +}); + +test('windows can pass absolute paths with "\\" - sync', t => { + const filePath = path.resolve(t.context.tmp, '1.tmp'); + + const removeFiles = del.sync([filePath], {cwd: t.context.tmp, dryRun: true}); + + t.deepEqual(removeFiles, [filePath]); +}); + +test('windows can pass relative paths with "\\" - async', async t => { + const nestedFile = path.resolve(t.context.tmp, 'a/b/c/nested.js'); + makeDir.sync(nestedFile); + + const removeFiles = await del([nestedFile], {cwd: t.context.tmp, dryRun: true}); + + t.deepEqual(removeFiles, [nestedFile]); +}); + +test('windows can pass relative paths with "\\" - sync', t => { + const nestedFile = path.resolve(t.context.tmp, 'a/b/c/nested.js'); + makeDir.sync(nestedFile); + + const removeFiles = del.sync([nestedFile], {cwd: t.context.tmp, dryRun: true}); + + t.deepEqual(removeFiles, [nestedFile]); +}); From 12c443df8ece0eb57c641d3d67e999457ac76995 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 23 Aug 2019 13:03:47 +0200 Subject: [PATCH 11/12] Meta tweaks --- benchmark.js | 22 +++++++++++++++++++--- package.json | 10 +++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/benchmark.js b/benchmark.js index 41cb7b3..9cfd6db 100644 --- a/benchmark.js +++ b/benchmark.js @@ -9,7 +9,7 @@ const suite = new Benchmark.Suite('concurrency'); const tempDir = tempy.directory(); -const fixtures = Array.from({length: 2000}, (x, index) => { +const fixtures = Array.from({length: 2000}, (_, index) => { return path.resolve(tempDir, (index + 1).toString()); }); @@ -19,7 +19,22 @@ function createFixtures() { } } -const concurrencies = [1, 3, 5, 10, 15, 20, 50, 100, 200, 300, 400, 500, 1000, Infinity]; +const concurrencies = [ + 1, + 3, + 5, + 10, + 15, + 20, + 50, + 100, + 200, + 300, + 400, + 500, + 1000, + Infinity +]; for (const concurrency of concurrencies) { const name = `concurrency: ${concurrency.toString()}`; @@ -29,7 +44,8 @@ for (const concurrency of concurrencies) { defer: true, setup() {}, // This line breaks async await async fn(deferred) { - // Can't use setup because it isn't called after every defer + // Can't use `setup()` because it isn't called after every + // defer and it breaks using `async` keyword here. // https://github.com/bestiejs/benchmark.js/issues/136 createFixtures(); diff --git a/package.json b/package.json index 51b594f..68acb84 100644 --- a/package.json +++ b/package.json @@ -45,21 +45,21 @@ "filesystem" ], "dependencies": { - "globby": "^10.0.0", + "globby": "^10.0.1", "graceful-fs": "^4.2.2", "is-glob": "^4.0.1", - "is-path-cwd": "^2.0.0", + "is-path-cwd": "^2.2.0", "is-path-inside": "^3.0.1", - "slash": "^3.0.0", "p-map": "^3.0.0", - "rimraf": "^3.0.0" + "rimraf": "^3.0.0", + "slash": "^3.0.0" }, "devDependencies": { "ava": "^2.3.0", "benchmark": "^2.1.4", "make-dir": "^3.0.0", "tempy": "^0.3.0", - "tsd": "^0.7.3", + "tsd": "^0.7.4", "xo": "^0.24.0" } } From 557c1fad9651405992b8358035eda504973a1124 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 23 Aug 2019 13:04:59 +0200 Subject: [PATCH 12/12] 5.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68acb84..ad41ccf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "del", - "version": "5.0.0", + "version": "5.1.0", "description": "Delete files and directories", "license": "MIT", "repository": "sindresorhus/del",