From c6645ffc1c79994ffa0bc20e02c76a9214adaf59 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 17:12:50 -0400 Subject: [PATCH 01/66] Bump ecmascript version to 8 for async/await --- .eslintrc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 04c34f1..eea21a9 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -2,7 +2,7 @@ root: true parserOptions: - ecmaVersion: 7 + ecmaVersion: 8 env: node: true From 05825886ede33bc14c7218caadf5886f9f3a9c92 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 17:13:23 -0400 Subject: [PATCH 02/66] Major version - 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9fce442..0b9ce5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "2.1.2", + "version": "3.0.0", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From 456dfe2b41992807ec63e3b0657bf5e1e922a6aa Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 17:13:50 -0400 Subject: [PATCH 03/66] Force node engine to >=8.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b9ce5e..18a92cc 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "update-notifier": "^1.0.3" }, "engines": { - "node": ">=6.9.0" + "node": ">=8.0.0" }, "engineStrict": true } From e859cff6011be96d87b32946e1505ab037fc4d64 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 17:18:59 -0400 Subject: [PATCH 04/66] Remove old heavy dependencies #23 * async-each-series - Removed in favor of using async/await with promises. * clui - Removed in favor of ora and progress. This module was too big and modclean was only using the progress bar and spinner. * update-notifier - While it was convient, it's a lot of extra bloat. We may investigate adding something like this again in the future. --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 18a92cc..7439cf4 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,7 @@ }, "homepage": "https://github.com/ModClean/modclean", "dependencies": { - "async-each-series": "^1.1.0", "chalk": "^1.1.3", - "clui": "^0.3.1", "commander": "^2.9.0", "empty-dir": "^0.2.1", "glob": "^7.1.1", @@ -47,7 +45,6 @@ "modclean-patterns-default": "latest", "rimraf": "^2.5.4", "subdirs": "^1.0.0", - "update-notifier": "^1.0.3" }, "engines": { "node": ">=8.0.0" From 5c3bc5aabbbb3a8fac648865c3836c6443515327 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 17:20:44 -0400 Subject: [PATCH 05/66] Add smaller dependencies #23 * await-handler - Better error handling for async/await. * ora - CLI spinners. * progress - CLI progress bar. --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 7439cf4..ed0d12a 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,14 @@ "homepage": "https://github.com/ModClean/modclean", "dependencies": { "chalk": "^1.1.3", + "await-handler": "^1.1.0", "commander": "^2.9.0", "empty-dir": "^0.2.1", "glob": "^7.1.1", "lodash.uniq": "^4.5.0", "modclean-patterns-default": "latest", + "ora": "^2.1.0", + "progress": "^2.0.0", "rimraf": "^2.5.4", "subdirs": "^1.0.0", }, From a201ceecfcf05304ee9de8e2aee79277e66e9a20 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 17:58:25 -0400 Subject: [PATCH 06/66] Update dependencies #23 --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ed0d12a..8283def 100644 --- a/package.json +++ b/package.json @@ -37,17 +37,17 @@ }, "homepage": "https://github.com/ModClean/modclean", "dependencies": { - "chalk": "^1.1.3", "await-handler": "^1.1.0", + "chalk": "^2.4.1", "commander": "^2.9.0", - "empty-dir": "^0.2.1", - "glob": "^7.1.1", + "empty-dir": "^1.0.0", + "glob": "^7.1.2", "lodash.uniq": "^4.5.0", "modclean-patterns-default": "latest", "ora": "^2.1.0", "progress": "^2.0.0", "rimraf": "^2.5.4", - "subdirs": "^1.0.0", + "subdirs": "^1.0.1" }, "engines": { "node": ">=8.0.0" From c29b2679f039ac8d489cca132a9c95a8ebd08fa3 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:00:38 -0400 Subject: [PATCH 07/66] Adding package-lock.json --- package-lock.json | 275 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..81acc96 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,275 @@ +{ + "name": "modclean", + "version": "3.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "await-handler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/await-handler/-/await-handler-1.1.0.tgz", + "integrity": "sha512-AYY1IVVWacfpAOzdWs2vrHzavhnZa2C+lRfiAiNWf73/XmUDz5AMKrQl50GXcIhA0R+Q5U67TRlXQCqvccr5BA==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "^1.1.1" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "empty-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/empty-dir/-/empty-dir-1.0.0.tgz", + "integrity": "sha512-97qcDM6mUA1jAeX6cktw7akc5awIGA+VIkA5MygKOKA+c2Vseo/xwKN0JNJTUhZUtPwZboKVD2p1xu+sV/F4xA==" + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "ora": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-2.1.0.tgz", + "integrity": "sha512-hNNlAd3gfv/iPmsNxYoAPLvxg7HuPozww7fFonMZvL84tP6Ox5igfk5j/+a9rtJJwqMgKK+JgWsAQik5o0HTLA==", + "requires": { + "chalk": "^2.3.1", + "cli-cursor": "^2.1.0", + "cli-spinners": "^1.1.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^4.0.0", + "wcwidth": "^1.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "subdirs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/subdirs/-/subdirs-1.0.1.tgz", + "integrity": "sha1-1lJkeHR25Mr378VJj7dAxp9ibUg=", + "requires": { + "es6-promise": "^3.0.2" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} From af4ad4573389e3f14196ceaf32cb72d9f17f0e8c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:01:07 -0400 Subject: [PATCH 08/66] Adding package-lock.json --- package-lock.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package-lock.json b/package-lock.json index 81acc96..963b3a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -173,6 +173,11 @@ "brace-expansion": "^1.1.7" } }, + "modclean-patterns-default": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/modclean-patterns-default/-/modclean-patterns-default-1.1.1.tgz", + "integrity": "sha1-UvCFIbosOVPt9XEdKEeYipRuHGQ=" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", From a791895b5ff37a9a25b5c7997e5655ae7f86fecc Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:03:41 -0400 Subject: [PATCH 09/66] :boom: Replace `process` option with `filter` --- lib/modclean.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 0b70aef..1da0075 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -51,18 +51,17 @@ let defaults = { * @type {Boolean} */ dotFiles: true, - /** - * Function (async or sync) to call before each file is deleted to give ability to prevent deletion. (Optional, default `null`) - * If called with 3 arguments (file, files, cb) it's async and `cb` must be called with a result (`false` skips file). - * If called with 1 or 2 arguments (file, files) it's sync and `return false` will skip the file. - * @type {Function|null} - */ - process: null, /** * The folder name used for storing modules. Used to append to `options.cwd` if running in parent directory (default `"node_modules"`) * @type {String} */ modulesDir: 'node_modules', + /** + * Filter function to run for each file/directory found. File object is passed in as a single argument. Return `true` to mark file for removal, + * `false` to keep it. Optionally may return `Promise` for async support. + * @type {?Function} + */ + filter: null, /** * Remove empty directories as part of the cleanup process (default `true`) * @type {Boolean} From 85a4bdf2cb7949d9c6ec80f12cf1418dcb4a996e Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:04:18 -0400 Subject: [PATCH 10/66] :sparkles: Add `skipModules` option --- lib/modclean.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/modclean.js b/lib/modclean.js index 1da0075..d8ea91b 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -62,6 +62,11 @@ let defaults = { * @type {?Function} */ filter: null, + /** + * If the directory being deleted is a module, skip it's deletion. + * @type {Boolean} + */ + skipModules: true, /** * Remove empty directories as part of the cleanup process (default `true`) * @type {Boolean} From 877cebb4b02430d737ee490a4de802da1a39f2b2 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:04:38 -0400 Subject: [PATCH 11/66] :sparkles: Adding `globOptions` option --- lib/modclean.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/modclean.js b/lib/modclean.js index d8ea91b..32e7966 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -82,6 +82,11 @@ let defaults = { return true; }, + /** + * Custom options to pass into `glob` to further customize file searching. Will override existing options. + * @type {Object} + */ + globOptions: {}, /** * Whether file deletion errors should halt the module from running and return the error to the callback (default `false`) * @type {Boolean} From 7f9048e5cf3b959ff04ad83d6b25cf971bee6cf1 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:05:39 -0400 Subject: [PATCH 12/66] :zap: Update default empty directory filter --- lib/modclean.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 32e7966..d6a4b5e 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -78,9 +78,7 @@ let defaults = { * @return {Boolean} `true` if is a valid file, `false` if invalid file */ emptyDirFilter: function(file) { - if(/Thumbs\.db$/i.test(file) || /\.DS_Store$/i.test(file)) return false; - - return true; + return !/(Thumbs\.db|\.DS_Store)$/i.test(file); }, /** * Custom options to pass into `glob` to further customize file searching. Will override existing options. From 104d1dbe1f67e21c98cb54c86f2baa8be0978a65 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:08:14 -0400 Subject: [PATCH 13/66] :recycle: Refactoring for promises + async/await --- lib/modclean.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index d6a4b5e..8f0f7dc 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -4,13 +4,18 @@ * @author Kyle Ross */ "use strict"; -const glob = require('glob'); -const rimraf = require('rimraf'); const path = require('path'); +const util = require('util'); + +const on = require('await-handler'); const subdirs = require('subdirs'); -const emptyDir = require('empty-dir'); -const each = require('async-each-series'); const fs = require('fs'); +const glob = util.promisify(require('glob')); +const rm = util.promisify(require('rimraf')); +const emptyDir = util.promisify(require('empty-dir')); + +const stat = util.promisify(fs.stat); +const readdir = util.promisify(fs.readdir); const Utils = require('./utils.js'); const EventEmitter = require('events').EventEmitter; From e05b77cda04df532fea2e1b6deaf49a8e90ae5d5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:10:20 -0400 Subject: [PATCH 14/66] :recycle: Make utils a subclass instead --- lib/modclean.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 8f0f7dc..289ee9c 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -17,8 +17,7 @@ const emptyDir = util.promisify(require('empty-dir')); const stat = util.promisify(fs.stat); const readdir = util.promisify(fs.readdir); -const Utils = require('./utils.js'); -const EventEmitter = require('events').EventEmitter; +const ModClean_Utils = require('./utils.js'); let defaults = { /** @@ -109,9 +108,9 @@ let defaults = { /** * @class ModClean - * @extends {EventEmitter} + * @extends {ModClean_Utils} */ -class ModClean extends EventEmitter { +class ModClean extends ModClean_Utils { /** * Initalizes ModClean class with provided options. If `cb` is provided, it will start `clean()`. * @param {Object} options Options to configure ModClean From 93d217d1cffd92b13b7d61b01cf6ba19bc25b644 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:10:34 -0400 Subject: [PATCH 15/66] :recycle: Organization --- lib/modclean.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modclean.js b/lib/modclean.js index 289ee9c..a706d8c 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -4,12 +4,12 @@ * @author Kyle Ross */ "use strict"; +const fs = require('fs'); const path = require('path'); const util = require('util'); const on = require('await-handler'); const subdirs = require('subdirs'); -const fs = require('fs'); const glob = util.promisify(require('glob')); const rm = util.promisify(require('rimraf')); const emptyDir = util.promisify(require('empty-dir')); From e71f4ba510868db6dd9128abebf937e864215263 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:27:09 -0400 Subject: [PATCH 16/66] :recycle: :boom: Refactor constructor function * :boom: Remove `cb` parameter. * :recycle: Cleanup of options initialization. * :boom: No longer calls `this.clean()` by default. --- lib/modclean.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index a706d8c..a67d66f 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -116,21 +116,14 @@ class ModClean extends ModClean_Utils { * @param {Object} options Options to configure ModClean * @param {Function} cb Optional callback function, if provided, `clean()` is automatically called. */ - constructor(options, cb) { + constructor(options={}) { super(); - this.utils = new Utils(this); - this.errors = []; - - if(typeof options === 'function') cb = options; - this.options = Object.assign({}, modclean.defaults, options && typeof options === 'object' ? options : {}); - - this._patterns = this.utils.initPatterns(this.options); + this.options = Object.assign({}, modclean.defaults, options || {}); + this._patterns = this.initPatterns(); if(this.options.modulesDir !== false && path.basename(this.options.cwd) !== this.options.modulesDir) this.options.cwd = path.join(this.options.cwd, this.options.modulesDir); - - if(cb) this.clean(cb); } /** From f7b5d0f18a9be2410d131a3c48c37e70e0a55deb Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:30:42 -0400 Subject: [PATCH 17/66] :recycle: :boom: Refactor clean function * :boom: Remove callback parameter. Returns `Promise` instead. * :boom: Rename `start` event to `clean:start`. * :recycle: Switch to async/await instead of nested callbacks. * :boom: Rename `complete` event to `clean:done`. * :boom: No longer returns `this`, but a promise that resolves with a results object. --- lib/modclean.js | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index a67d66f..c1a3ccb 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -131,29 +131,26 @@ class ModClean extends ModClean_Utils { * @param {Function} cb Callback to call once process is complete with `err` and `results`. * @return {ModClean} Instance of ModClean */ - clean(cb) { - let opts = this.options; - - this.emit('start', this); - - let done = (err, results) => { - this.emit('complete', err, results); - if(typeof cb === 'function') cb(err, results); - }; - - this._find((err, files) => { - if(err) return done(err); - - this._process(files, (err, results) => { - if(err || !opts.removeEmptyDirs) return done(err, results); - - this.cleanEmptyDirs((err, dirs) => { - return done(err, Array.isArray(dirs)? results.concat(dirs) : results); - }); - }); - }); - - return this; + async clean() { + let files, dirs, results; + this.emit('clean:start', this); + + try { + files = await this._find(); + results = await this._process(files); + dirs = await this.cleanEmptyDirs(); + + if(dirs) { + results.deleted = results.deleted.concat(dirs.deleted); + results.empty = dirs.empty; + } + } catch(err) { + throw err; + } + + this.emit('clean:complete', results); + + return results; } /** From f8e238c3de7af284aeee107accb691d35ab3ac1e Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:38:44 -0400 Subject: [PATCH 18/66] :recycle: :boom: Refactor _find() function * :boom: Remove callback parameter in favor of promises. * :sparkles: Added ability to override and pass in custom options to `glob`. * :sparkles: Added `follow` option for glob to follow symlinks. * :boom: Rename `beforeFind` event to `file:find`. * :sparkles: Files are now stat'ed to gather more information. * :boom: Array of files returned is now an array of file objects with various properties to describe the file. * :sparkles: #19 Now determines if the file is a directory and if it's a module. * :boom: Renamed `files` event to `file:list`. * :boom: Returns promises with array of file objects. --- lib/modclean.js | 65 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index c1a3ccb..4a2a5b7 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -158,23 +158,56 @@ class ModClean extends ModClean_Utils { * @private * @param {Function} cb Callback function to call once complete. */ - _find(cb) { - let opts = this.options, - globOpts = { - cwd: opts.cwd, - dot: opts.dotFiles, - nocase: opts.ignoreCase, - ignore: this._patterns.ignore, - nodir: opts.noDirs - }; - - this.emit('beforeFind', this._patterns.allow, globOpts); + async _find() { + let defaultGlobOpts = { + cwd: this.options.cwd, + dot: this.options.dotFiles, + nocase: this.options.ignoreCase, + ignore: this._patterns.ignore, + nodir: this.options.noDirs, + follow: this.options.followSymlink + }; + + let globOpts = Object.assign({}, defaultGlobOpts, this.options.globOptions); + + this.emit('file:find', this._patterns.allow, globOpts); - glob(`**/@(${this._patterns.allow.join('|')})`, globOpts, (err, files) => { - if(err) this.utils.error(err, '_find'); - else this.emit('files', files); - cb(err, files); - }); + let [err, results] = await on(glob(`**/@(${this._patterns.allow.join('|')})`, globOpts)); + if(err) throw this.error(err, '_find'); + + let [e, files] = await on( + Promise.all(results.map(async file => { + let obj = { + path: file, + fullPath: path.join(this.options.cwd, file), + dir: path.join(this.options.cwd, path.parse(file).dir), + name: path.basename(file), + isModule: false + }; + + try { + let stats = await stat(obj.fullPath); + if(stats.isDirectory()) { + let list = await readdir(obj.fullPath); + if(list.indexOf('package.json')) obj.isModule = true; + } + + obj.stat = stats; + } catch(error) { + this.error(error, '_find'); + return null; + } + + return obj; + })) + ); + + if(e) throw e; + + files = files.filter(obj => !!obj); + + this.emit('file:list', files); + return files; } /** From 01eefb43cc62b7b8d9f77e7562118e7208ebed0a Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 11 May 2018 18:44:20 -0400 Subject: [PATCH 19/66] :recycle: :boom: Refactor _process() function * :boom: Removed callback parameter. * Removed callack style code in favor for promises. * :boom: Renamed `process` event to `process:start`. * :boom: Removed code for `process` option. * :zap: Files are now deleted in parallel instead of in series. * :boom: Renamed `finish` event to `process:done`. * :boom: Resolves with object containing list of file objects and array of paths that were deleted successfully. --- lib/modclean.js | 73 ++++++++++++++++--------------------------------- 1 file changed, 24 insertions(+), 49 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 4a2a5b7..af4054e 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -216,56 +216,31 @@ class ModClean extends ModClean_Utils { * @param {Array} files List of file paths to process. * @param {Function} cb Callback function to call once complete. */ - _process(files, cb) { - let self = this, - opts = this.options, - processFn = typeof opts.process === 'function'? opts.process : function() { return true; }, - results = []; + async _process(files) { + let results = { + files, + deleted: [] + }; - if(!files.length) return cb(null, []); - - this.emit('process', files); - - each(files, function(file, callback) { - let next = () => { - if(processFn.length <= 1) { - // If processFn has 0 or 1 argument (file), then assume sync - if(processFn(file) !== false) self._deleteFile(file, (err) => { - if(!err) results.push(file); - callback(err); - }); - else callback(); - } else { - // If processFn more than 1 argument (file, cb), then assume async - processFn(file, function(result) { - if(result !== false) self._deleteFile(file, (err) => { - if(!err) results.push(file); - callback(err); - }); - else callback(); - }); - } - }; - - if(opts.followSymlink) { - next(); - } else { - fs.lstat(path.join(opts.cwd, path.parse(file).dir), (err, stat) => { - if(err || !stat.isSymbolicLink()) { - next(); - return; - } - callback(); - }); - } - }, function(err) { - /** - * @event finish - * @property {Array} results List of files successfully deleted - */ - self.emit('finish', results); - cb(err, results); - }); + if(!files.length) return results; + + this.emit('process:start', files); + + let [err] = await on( + Promise.all(files.map(async file => { + let [e, res] = await on(this._deleteFile(file)); + if(e) throw e; + + if(res) results.deleted.push(file.fullPath); + return res; + })) + ); + + if(err) throw err; + + this.emit('process:done', results.deleted); + + return results; } /** From 3199764cf3faa5fd48fb37dbe3711e50380ec908 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:43:34 -0400 Subject: [PATCH 20/66] :recycle: :boom: Refactor _deleteFile() function * :boom: Removed callback function in favor of promises. * :boom: Moved filtering functionality into _deleteFile() instead of when the files are processed. * :zap: Added functionality for new `filter` option. * :zap: Skips all directories that are classified as a module if `skipModules` option is enabled. * :zap: `file:deleted` event is additionally emitted when test mode is on. * :zap: Errors will no longer be emitted for files/folders missing from the filesystem. * :boom: Returns promise that resolves with a Boolean instead of the file path. --- lib/modclean.js | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index af4054e..d58cc62 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -249,26 +249,36 @@ class ModClean extends ModClean_Utils { * @param {String} file File path to delete. * @param {Function} cb Callback function to call once complete. */ - _deleteFile(file, cb) { - let self = this, - opts = this.options; - - function done() { - self.emit('deleted', file); - return cb(null, file); + async _deleteFile(file) { + let shouldDelete = true; + + if(typeof this.options.filter === 'function') { + let res = this.options.filter(file); + if(res instanceof Promise) shouldDelete = await res; + else shouldDelete = !!res; } - - // If test mode is enabled, just return the file. - if(opts.test) return done(); - - rimraf(path.join(opts.cwd, file), (err) => { - if(err) { - this.utils.error(err, '_deleteFile', { file }, 'fileError'); - return opts.errorHalt? cb(err, file) : cb(); - } - - return done(); - }); + + if(this.options.skipModules && file.isModule) shouldDelete = false; + + if(!shouldDelete) { + this.emit('file:skipped', file); + return false; + } + + if(this.options.test) { + this.emit('file:deleted', file); + return true; + } + + let [err] = await on(rm(file.fullPath)); + if(err && err.code !== 'ENOENT') { + err = this.error(err, '_deleteFile', { file }); + if(this.options.errorHalt) throw err; + } else { + this.emit('file:deleted', file); + } + + return !err; } /** From 6d9f89f4873c0a50b54afcd4ffc431a6a6a2408b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:48:47 -0400 Subject: [PATCH 21/66] :recycle: :boom: Refactored cleanEmptyDirs() * :boom: Removed callback parameter in favor of promises. * :boom: Moved check for test mode into `_removeEmptyDirs()` instead. * :boom: Renamed `beforeEmptyDirs` event to `emptydir:start`. * :boom: Renamed `afterEmptyDirs` event to `emptydir:done`. * :boom: Returns Promise that resolves with object instead of array. --- lib/modclean.js | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index d58cc62..70906fa 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -285,27 +285,23 @@ class ModClean extends ModClean_Utils { * Finds and removes all empty directories. * @param {Function} cb Callback to call once complete. */ - cleanEmptyDirs(cb) { - let self = this, - opts = this.options, - results = []; - // If test mode is enabled or removeEmptyDirs is disabled, just return. - if(opts.test || !opts.removeEmptyDirs) return cb(); - - this.emit('beforeEmptyDirs'); - - function done(err, res) { - self.emit('afterEmptyDirs', results); - return cb(err, res); - } - - this._findEmptyDirs((err, dirs) => { - if(err) return done(err); - - this._removeEmptyDirs(dirs, (err, res) => { - return done(err, res); - }); - }); + async cleanEmptyDirs() { + if(!this.options.removeEmptyDirs) return false; + + this.emit('emptydir:start'); + + + let [error, dirs] = await on(this._findEmptyDirs()); + if(error) throw error; + + let results = await this._removeEmptyDirs(dirs); + + this.emit('emptydir:done', results); + + return { + empty: dirs, + deleted: results + }; } /** From 9d08a5045b923611bda2f045a11845623958fe13 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:50:52 -0400 Subject: [PATCH 22/66] :recycle: :boom: Refactored _findEmptyDirs() * :boom: Removed callback parameter in favor of Promises. * :boom: Renamed `emptyDirs` event to `emptydir:list`. --- lib/modclean.js | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 70906fa..99bbca9 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -309,26 +309,21 @@ class ModClean extends ModClean_Utils { * @private * @param {Function} cb Callback to call once complete. */ - _findEmptyDirs(cb) { - let self = this, - results = []; - - subdirs(this.options.cwd, function(err, dirs) { - if(err) self.utils.error(err, '_findEmptyDirs'); - if(err || !Array.isArray(dirs)) return cb(err, []); - - each(dirs, (dir, dCb) => { - emptyDir(dir, self.options.emptyDirFilter || function() { return true; }, (err, isEmpty) => { - if(err) self.utils.error(err, '_findEmptyDirs'); - if(err || !isEmpty) return dCb(); - results.push(dir); - dCb(); - }); - }, (err) => { - self.emit('emptyDirs', results); - cb(err, results); - }); - }); + async _findEmptyDirs() { + let results = []; + + let [err, dirs] = await on(subdirs(this.options.cwd)); + if(err) throw this.error(err, '_findEmptyDirs'); + if(!Array.isArray(dirs)) return []; + + for(let dir of dirs) { + let [e, isEmpty] = await on(emptyDir(dir, this.options.emptyDirFilter)); + if(e) this.error(err, '_findEmptyDirs', { dir }); + else if(isEmpty) results.push(dir); + } + + this.emit('emptydir:list', results); + return results; } /** From 772ecdc051622bd72aec62c9518137d8c90784ac Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:53:54 -0400 Subject: [PATCH 23/66] :recycle: :boom: Refactored _removeEmptyDirs() * :boom: Removed callback parameter in favor of promises. * :boom: Errors are no longer emitted under the event name `emptyDirError` but as a standard error instead. * :boom: Renamed `deletedEmptyDir` event to `emptydir:deleted`. --- lib/modclean.js | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 99bbca9..6483b1a 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -332,23 +332,22 @@ class ModClean extends ModClean_Utils { * @param {Array} dirs List of empty directories to remove. * @param {Function} cb Callback function to call once complete. */ - _removeEmptyDirs(dirs, cb) { - let self = this, - results = []; - - each(dirs, (dir, dCb) => { - rimraf(dir, (err) => { - if(err) self.utils.error(err, '_removeEmptyDirs', { dir }, 'emptyDirError'); - else { - results.push(dir); - self.emit('deletedEmptyDir', dir); - } - - dCb(); - }); - }, (err) => { - cb(err, results); - }); + async _removeEmptyDirs(dirs) { + let results = []; + + // Return all empty directories if in test mode + if(this.options.test) return dirs; + + for(let dir in dirs) { + let [err] = await on(rm(dir)); + if(err) this.error(err, '_removeEmptyDirs', { dir }); + else { + results.push(dir); + this.emit('emptydir:deleted', dir); + } + } + + return results; } } From 473bef0c2ab4237ebe5a3b2ffe8c0fa52a64f068 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:54:38 -0400 Subject: [PATCH 24/66] :boom: Removed callback parmeter from shortcut --- lib/modclean.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/modclean.js b/lib/modclean.js index 6483b1a..5d1d851 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -362,6 +362,8 @@ module.exports = modclean; */ function modclean(options, cb) { return new ModClean(options, cb); +function modclean(options) { + return new ModClean(options); } /** From d9a0a80c5aacf93abd0f0dd9ca610fcff8ef44d2 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:55:03 -0400 Subject: [PATCH 25/66] :boom: Removed callback parmeter from shortcut --- lib/modclean.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 5d1d851..bb9835f 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -360,8 +360,6 @@ module.exports = modclean; * @param {Function} cb Callback function to call once completed or if error * @return {Object} New ModClean instance */ -function modclean(options, cb) { - return new ModClean(options, cb); function modclean(options) { return new ModClean(options); } From 8a13ef1a4c0e6fb8b9ab800302090b880582e987 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:56:12 -0400 Subject: [PATCH 26/66] :bulb: Updating inline docs --- lib/modclean.js | 65 +++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index bb9835f..0156e7b 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -37,7 +37,7 @@ let defaults = { additionalPatterns: [], /** * Ignore the provided glob patterns - * @type {Array|null} + * @type {?Array} */ ignorePatterns: null, /** @@ -78,7 +78,7 @@ let defaults = { removeEmptyDirs: true, /** * Filter function used when checking if a directory is empty - * @param {String} file File name to filter against + * @param {String} file File name in directory to filter against * @return {Boolean} `true` if is a valid file, `false` if invalid file */ emptyDirFilter: function(file) { @@ -86,7 +86,7 @@ let defaults = { }, /** * Custom options to pass into `glob` to further customize file searching. Will override existing options. - * @type {Object} + * @type {?Object} */ globOptions: {}, /** @@ -112,9 +112,9 @@ let defaults = { */ class ModClean extends ModClean_Utils { /** - * Initalizes ModClean class with provided options. If `cb` is provided, it will start `clean()`. - * @param {Object} options Options to configure ModClean - * @param {Function} cb Optional callback function, if provided, `clean()` is automatically called. + * Initalizes ModClean class with provided options. + * @constructor + * @param {?Object} [options={}] Options to configure ModClean */ constructor(options={}) { super(); @@ -128,8 +128,9 @@ class ModClean extends ModClean_Utils { /** * Automated clean process that finds and deletes items based on the ModClean options. - * @param {Function} cb Callback to call once process is complete with `err` and `results`. - * @return {ModClean} Instance of ModClean + * @async + * @public + * @return {Promise} Promise that resolves an `results` object or rejects with an `Error`. */ async clean() { let files, dirs, results; @@ -155,8 +156,9 @@ class ModClean extends ModClean_Utils { /** * Finds files/folders based on the ModClean options. + * @async * @private - * @param {Function} cb Callback function to call once complete. + * @return {Promise} Resolves with array of file objects found. */ async _find() { let defaultGlobOpts = { @@ -211,10 +213,11 @@ class ModClean extends ModClean_Utils { } /** - * Processes the found files and deletes the ones that pass `options.process`. + * Processes the found files and deletes them in parallel. + * @async * @private - * @param {Array} files List of file paths to process. - * @param {Function} cb Callback function to call once complete. + * @param {Array} files List of file paths to process and delete. + * @return {Promise} Resolves with `results` object. */ async _process(files) { let results = { @@ -244,10 +247,11 @@ class ModClean extends ModClean_Utils { } /** - * Deletes a single file/folder from the filesystem. + * Deletes a single provided file/folder from the filesystem. + * @async * @private - * @param {String} file File path to delete. - * @param {Function} cb Callback function to call once complete. + * @param {Object} file File object to delete. + * @return {Promise} Resolves with boolean if file was deleted. */ async _deleteFile(file) { let shouldDelete = true; @@ -282,8 +286,10 @@ class ModClean extends ModClean_Utils { } /** - * Finds and removes all empty directories. - * @param {Function} cb Callback to call once complete. + * Finds and removes all empty directories automatically. + * @async + * @public + * @return {Promise} Resolves with `results` object. */ async cleanEmptyDirs() { if(!this.options.removeEmptyDirs) return false; @@ -306,8 +312,9 @@ class ModClean extends ModClean_Utils { /** * Finds all empty directories within `options.cwd`. + * @async * @private - * @param {Function} cb Callback to call once complete. + * @return {Promise} Resolves with array of empty directory paths found. */ async _findEmptyDirs() { let results = []; @@ -328,9 +335,10 @@ class ModClean extends ModClean_Utils { /** * Removes all empty directories provided in `dirs`. + * @async * @private - * @param {Array} dirs List of empty directories to remove. - * @param {Function} cb Callback function to call once complete. + * @param {Array} dirs List of empty directories to remove. + * @return {Promise} Resolves with array of empty directories in which were successfully removed. */ async _removeEmptyDirs(dirs) { let results = []; @@ -351,14 +359,16 @@ class ModClean extends ModClean_Utils { } } -// export modclean +/** + * Exports a wrapper for the ModClean @class . + * @type {Function} + */ module.exports = modclean; /** - * Shortcut for calling `new ModClean(options, cb).clean()` - * @param {Object} options Options to set for ModClean (Optional) - * @param {Function} cb Callback function to call once completed or if error - * @return {Object} New ModClean instance + * Shortcut for calling `new ModClean(options, cb)` + * @param {?Object} options Options to set for ModClean + * @return {ModClean} New ModClean instance */ function modclean(options) { return new ModClean(options); @@ -370,5 +380,8 @@ function modclean(options) { */ modclean.defaults = defaults; -// Export ModClean class +/** + * Static property with access to the ModClean class. + * @type {Class} + */ modclean.ModClean = ModClean; From bb41689f417e71866e113e06f41d90c94b8cfb51 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:57:14 -0400 Subject: [PATCH 27/66] :recycle: Inject eventemitter in utils class --- lib/utils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utils.js b/lib/utils.js index e72360b..9fec688 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,10 +2,12 @@ const uniq = require('lodash.uniq'); const path = require('path'); +const EventEmitter = require('events').EventEmitter; -class ModClean_Utils { constructor(inst) { this._inst = inst; +class ModClean_Utils extends EventEmitter { + super(); } /** From a657e7cb300009781ee382912f7b5707480da0b3 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 11:58:55 -0400 Subject: [PATCH 28/66] :recycle: Updating constructor * :boom: No longer uses `inst` parameter and instead will be used as a subclass of ModClean. * :recycle: Moved `errors` array here instead of in the base ModClean class. --- lib/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 9fec688..b6effa5 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,10 +4,10 @@ const path = require('path'); const EventEmitter = require('events').EventEmitter; - constructor(inst) { - this._inst = inst; class ModClean_Utils extends EventEmitter { + constructor() { super(); + this.errors = []; } /** From 1f3db6450b17ca81bfdb9f9ff6fa357c001b0dbd Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:00:55 -0400 Subject: [PATCH 29/66] :recycle: Refactor initPatterns() * :boom: Remove the `opts` parameter as options are now in scope. * :recycle: Updated all use of `opts` to `this.options`. --- lib/utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index b6effa5..dbf0eb2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -15,8 +15,8 @@ class ModClean_Utils extends EventEmitter { * @param {Object} opts Options object for ModClean * @return {Object} The compiled and loaded patterns */ - initPatterns(opts) { - let patDefs = opts.patterns, + initPatterns() { + let patDefs = this.options.patterns, patterns = [], ignore = []; @@ -58,8 +58,8 @@ class ModClean_Utils extends EventEmitter { }); }); - let addlPats = opts.additionalPatterns, - addlIgnore = opts.ignorePatterns; + let addlPats = this.options.additionalPatterns, + addlIgnore = this.options.ignorePatterns; if(Array.isArray(addlPats) && addlPats.length) patterns = patterns.concat(addlPats); if(Array.isArray(addlIgnore) && addlIgnore.length) ignore = ignore.concat(addlIgnore); From 342aa61cd0e35e240fbf32412c8f2e70fc2deb33 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:01:37 -0400 Subject: [PATCH 30/66] :recycle: Remove extra backslashes from regex --- lib/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils.js b/lib/utils.js index dbf0eb2..5ba1e0a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -91,7 +91,7 @@ class ModClean_Utils extends EventEmitter { if(ext === '.js' || ext === '.json') patterns = require(module); else throw new Error(`Invalid pattern module "${def}" provided`); } else { - if(module.match(/modclean\-patterns\-/) === null) module = 'modclean-patterns-' + module; + if(module.match(/modclean-patterns-/) === null) module = 'modclean-patterns-' + module; try { patterns = require(module); From 9ca59a4b280bc9d80609b7674f327c07d27d4cbc Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:03:37 -0400 Subject: [PATCH 31/66] :recycle: :boom: Refactor error() function * :boom: Instead of creating a new object containing all error and extra info, the error object is now used and the properties are set on the prototype. * :zap: If error is not an object, it will create a new error. --- lib/utils.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 5ba1e0a..020b069 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -118,15 +118,14 @@ class ModClean_Utils extends EventEmitter { * @return {Object} The compiled error object */ error(err, method, obj={}, event='error') { - let errObj = Object.assign({ - error: err, - method: method - }, obj || {}); + if(typeof err !== 'object') err = new Error(err); + err.method = method; + err.payload = obj; - this._inst.errors.push(errObj); - if(event !== false) this._inst.emit(event, errObj); + this.errors.push(err); + if(event !== false) this.emit(event, err); - return errObj; + return err; } } From 81ba943e639254e30134d0a9dba2cffd87d3f337 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:03:59 -0400 Subject: [PATCH 32/66] :bulb: Updating inline docs --- lib/utils.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 020b069..b5183e9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,15 @@ const path = require('path'); const EventEmitter = require('events').EventEmitter; +/** + * @class ModClean_Utils + * @extends {EventEmitter} + */ class ModClean_Utils extends EventEmitter { + /** + * Constructor function + * @constructor + */ constructor() { super(); this.errors = []; @@ -12,7 +20,6 @@ class ModClean_Utils extends EventEmitter { /** * Initializes patterns based on the configuration - * @param {Object} opts Options object for ModClean * @return {Object} The compiled and loaded patterns */ initPatterns() { @@ -111,11 +118,11 @@ class ModClean_Utils extends EventEmitter { /** * Stores error details and emits error event - * @param {Error} err Error object - * @param {String} method Method in which the error occurred - * @param {Object} obj Optional object to combine into the stored error object - * @param {String} event Event name to emit, `false` disables - * @return {Object} The compiled error object + * @param {Error} err Error object + * @param {String} method Method in which the error occurred + * @param {?Object} obj Optional object to combine into the stored error object + * @param {?String} event Event name to emit, `false` disables + * @return {Error} The compiled error object */ error(err, method, obj={}, event='error') { if(typeof err !== 'object') err = new Error(err); From 78c4642221547fd5a7f221eb3914dd796ac5fdb2 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:05:34 -0400 Subject: [PATCH 33/66] :heavy_minus_sign: Removing dependencies * :heavy_minus_sign: Removing `update-notifier`. * :heavy_minus_sign: Removing `clui`. * :heavy_minus_sign: Removing `os`. --- bin/modclean.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/modclean.js b/bin/modclean.js index 2bb1242..cd339a4 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -3,10 +3,7 @@ const chalk = require('chalk'); const program = require('commander'); -const notifier = require('update-notifier'); -const clui = require('clui'); const path = require('path'); -const os = require('os'); const pkg = require('../package.json'); const utils = require('./utils'); From 69f8f8bbe12b0614ca48f9d8dc9a6dd064a6725c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:06:18 -0400 Subject: [PATCH 34/66] :heavy_plus_sign: Adding dependencies Added built-in `util` and `fs` packages. --- bin/modclean.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/modclean.js b/bin/modclean.js index cd339a4..56a57d1 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -3,7 +3,9 @@ const chalk = require('chalk'); const program = require('commander'); +const util = require('util'); const path = require('path'); +const fs = require('fs'); const pkg = require('../package.json'); const utils = require('./utils'); From ce3b57c3d51a8fbb219091d19d7a6f237266f015 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:06:46 -0400 Subject: [PATCH 35/66] :boom: Removing code for update-notifier --- bin/modclean.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/modclean.js b/bin/modclean.js index 56a57d1..9aee107 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -12,8 +12,6 @@ const utils = require('./utils'); const modclean = require('../lib/modclean'); const ModClean = modclean.ModClean; -notifier({ pkg }).notify(); - function list(val) { return val.split(','); } From b402f1a97cb2bee787710208f34b42c005d37208 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:07:49 -0400 Subject: [PATCH 36/66] :recycle: Refactoring new line code Moved new line to top of the process --- bin/modclean.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/modclean.js b/bin/modclean.js index 9aee107..f37a2d0 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -21,6 +21,9 @@ process.on('SIGINT', function() { process.exit(); }); +// Add a space at the top +console.log("\n"); + program .version(pkg.version) .description('Remove unwanted files and directories from your node_modules folder') @@ -49,7 +52,6 @@ class ModClean_CLI { // Display CLI header console.log( - "\n" + chalk.yellow.bold('MODCLEAN ') + chalk.gray(' Version ' + pkg.version) + "\n" ); From e2fc96f9316303ca891ef9cc3368269227ffeaeb Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:22:24 -0400 Subject: [PATCH 37/66] :boom: Removing interactive flag Removed interactive flag and functionality as it is a bit tedius and not really useful in a properly setup environment. --- bin/modclean.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/modclean.js b/bin/modclean.js index f37a2d0..d03a805 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -32,7 +32,6 @@ program .option('-p, --path ', 'Path to run modclean on (defaults to current directory)') .option('-D, --modules-dir ', 'Modules directory name (defaults to "node_modules")') .option('-s, --case-sensitive', 'Matches are case sensitive') - .option('-i, --interactive', 'Each deleted file/folder requires confirmation') .option('-P, --no-progress', 'Hide progress bar') .option('-e, --error-halt', 'Halt script on error') .option('-v, --verbose', 'Run in verbose mode') From f1992fea39d76e5ef1e856b54cb408761998924f Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:34:48 -0400 Subject: [PATCH 38/66] :zap: Added flag to enable module deletion By default, modclean will not delete matched folders that are determined to be a module. With this new flag, it will disable that functionality, allowing matched modules to be deleted. --- bin/modclean.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/modclean.js b/bin/modclean.js index d03a805..9289bc6 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -43,6 +43,7 @@ program .option('--no-dirs', 'Exclude directories from being removed') .option('--no-dotfiles', 'Exclude dot files from being removed') .option('-k, --keep-empty', 'Keep empty directories') + .option('-m, --modules', 'Delete modules that are matched by patterns') .parse(process.argv); class ModClean_CLI { @@ -122,6 +123,7 @@ class ModClean_CLI { cb(res); }); } + skipModules: !program.modules }; this.modclean = new ModClean(options); From d914f7606b0ad08e872a8e0ae05bb41b8ccd51af Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:38:45 -0400 Subject: [PATCH 39/66] :zap: Added new log file functionality and flag Added new flag in which will allow output of log files at the end of the process. Log files are created for deleted files/folders, skipped files, and errors in the current working directory. --- bin/modclean.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/bin/modclean.js b/bin/modclean.js index 9289bc6..99414e1 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -44,6 +44,7 @@ program .option('--no-dotfiles', 'Exclude dot files from being removed') .option('-k, --keep-empty', 'Keep empty directories') .option('-m, --modules', 'Delete modules that are matched by patterns') + .option('-l, --log', 'Output log files') .parse(process.argv); class ModClean_CLI { @@ -94,6 +95,12 @@ class ModClean_CLI { // Disable progress bar in interactive mode if(program.interactive) program.progress = false; + + this.logs = { + deleted: [], + skipped: [], + errors: [] + }; let options = { cwd: program.path || process.cwd(), @@ -189,6 +196,7 @@ class ModClean_CLI { inst.on('deleted', (file) => { updateProgress(this.stats.current, this.stats.total); + this.logs.skipped.push(file.fullPath); this.log( 'verbose', `${chalk.yellow.bold('DELETED')} (${this.stats.current}/${this.stats.total}) ${chalk.gray(file)}` @@ -199,6 +207,7 @@ class ModClean_CLI { this.log('event', 'beforeEmptyDirs'); showSpinner(`Searching for empty directories in ${inst.options.cwd}...`); + this.logs.deleted.push(file.fullPath); }); inst.on('emptyDirs', (dirs) => { @@ -221,6 +230,7 @@ class ModClean_CLI { this.stats.currentEmpty += 1; updateProgress(this.stats.currentEmpty, this.stats.totalEmpty); + this.logs.deleted.push(dir); this.log( 'verbose', `${chalk.yellow.bold('DELETED EMPTY DIR')} (${this.stats.currentEmpty}/${this.stats.totalEmpty}) ${chalk.gray(dir)}` @@ -242,6 +252,11 @@ class ModClean_CLI { inst.on('fileError', (err) => { this.log('event', 'fileError'); this.log('error', `${chalk.red.bold('FILE ERROR:')} ${err.error}\n${chalk.gray(err.file)}`); + this.logs.errors.push({ + message: err.message, + method: err.method, + payload: err.payload + }); }); // Finish Event (once processing/deleting all files is complete) @@ -262,6 +277,30 @@ class ModClean_CLI { } done(err, results) { + + async writeLogs() { + if(!program.log) return; + let writeFile = util.promisify(fs.writeFile); + + try { + await writeFile(path.join(process.cwd(), 'modclean-deleted.log'), JSON.stringify(this.logs.deleted)); + await writeFile(path.join(process.cwd(), 'modclean-skipped.log'), JSON.stringify(this.logs.skipped)); + await writeFile(path.join(process.cwd(), 'modclean-errors.log'), JSON.stringify(this.logs.errors)); + } catch(e) { + console.error('Error while creating log files:'); + console.error(e); + } + + return true; + } + + async fail(err) { + this.log('error', err); + + await this.writeLogs(); + process.stdin.destroy(); + return process.exit(1); + } console.log( "\n" + chalk.green.bold('FILES/FOLDERS DELETED') + "\n" + ` ${chalk.yellow.bold('Total: ')} ${results.length}\n` + @@ -279,6 +318,7 @@ class ModClean_CLI { process.exit(0); }, 500); + await this.writeLogs(); } } From 88db443db0493a59ae2863732b59c31a00c83dcd Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:40:34 -0400 Subject: [PATCH 40/66] :recycle: Refactor of CLI Refactored CLI to match the new functionality and changes in ModClean. --- bin/modclean.js | 238 ++++++++++++++++++++---------------------------- 1 file changed, 97 insertions(+), 141 deletions(-) diff --git a/bin/modclean.js b/bin/modclean.js index 99414e1..30a5464 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -50,7 +50,7 @@ program class ModClean_CLI { constructor() { this.log = utils.initLog(program.verbose); - + // Display CLI header console.log( chalk.yellow.bold('MODCLEAN ') + @@ -71,7 +71,7 @@ class ModClean_CLI { chalk.red.bold('WARNING:') + "\n" + chalk.gray(utils.warningMsg) + "\n" ); - + return utils.confirm('Are you sure you want to continue?', (res) => { if(!res) return process.exit(0); this.start(); @@ -82,19 +82,13 @@ class ModClean_CLI { } start() { - let self = this, - platform = os.platform(); - this.stats = { - current: 0, total: 0, - skipped: [], - currentEmpty: 0, + deleted: 0, + skipped: 0, + deletedEmpty: 0, totalEmpty: 0 }; - - // Disable progress bar in interactive mode - if(program.interactive) program.progress = false; this.logs = { deleted: [], @@ -115,168 +109,136 @@ class ModClean_CLI { ignoreCase: !program.caseSensitive, test: !!program.test, followSymlink: !!program.followSymlink, - process: function(file, cb) { - self.stats.current += 1; - if(!program.interactive) return cb(true); - - let name = path.relative(options.cwd, file); - if(platform === 'win32') name = name.replace(/\\+/g, '/'); - - utils.confirm( - chalk.gray(`(${self.stats.current}/${self.stats.total})`) + - `${name} ${chalk.gray(' - ')} ${chalk.yellow.bold('Delete File?')}`, - function(res) { - if(!res) self.stats.skipped.push(file); - cb(res); - }); - } skipModules: !program.modules }; this.modclean = new ModClean(options); - + this.initEvents(); - this.modclean.clean(this.done.bind(this)); + + this.modclean.clean() + .then(this.done.bind(this)) + .catch(this.fail.bind(this)); } initEvents() { - var inst = this.modclean; - - let progressBar = new clui.Progress(40), - spinner = new clui.Spinner('Loading...'), - showProgress = true; - - if(!process.stdout.isTTY || program.interactive || !program.progress || program.verbose) showProgress = false; - - function updateProgress(current, total) { - if(showProgress) { - process.stdout.cursorTo(0); - process.stdout.write(progressBar.update(current, total)); - process.stdout.clearLine(1); - } - } - - function showSpinner(msg) { - if(!process.stdout.isTTY || program.verbose || !program.progress) { - console.log(msg); - } else { - spinner.message(msg); - spinner.start(); - } - } + var inst = this.modclean, + showProgress = true, + spinner, progress; - // Start Event (searching for files) - inst.on('start', () => { - this.log('event', 'start'); + if(!process.stdout.isTTY || !program.progress || program.verbose) showProgress = false; + + inst.on('clean:start', () => { + this.log('event', 'clean:start'); this.log('verbose', '\n' + JSON.stringify(inst.options, null, 4)); - - showSpinner(`Searching for files in ${inst.options.cwd}...`); + + spinner = utils.spinner(`Searching for files in ${inst.options.cwd}...`); }); - - // Files Event (file list after searching complete) - inst.on('files', (files) => { - this.log('event', 'files'); - if(process.stdout.isTTY) spinner.stop(); - + + inst.on('file:find', () => { + this.log('event', 'file:find'); + }); + + inst.on('file:list', files => { + this.log('event', 'file:list'); this.stats.total = files.length; - - console.log(`Found ${chalk.green.bold(files.length)} files/folders to remove\n`); + + spinner.succeed(`Found ${files.length} files to remove`); }); - - inst.on('process', (files) => { - this.log('event', 'process'); - - if(!showProgress && !program.interactive && !program.verbose) - console.log('Deleting files, please wait...'); - - updateProgress(0, files.length); + + inst.on('process:start', files => { + this.log('event', 'process:start'); + + if(!showProgress) console.log('Deleting files, please wait...'); + if(showProgress) progress = utils.progressBar(files.length); }); - - // Deleted Event (called for each file deleted) - inst.on('deleted', (file) => { - updateProgress(this.stats.current, this.stats.total); - + + inst.on('file:skipped', file => { + this.log('event', 'file:skipped'); + this.stats.skipped += 1; this.logs.skipped.push(file.fullPath); + this.log( 'verbose', - `${chalk.yellow.bold('DELETED')} (${this.stats.current}/${this.stats.total}) ${chalk.gray(file)}` + `${chalk.magenta.bold('SKIPPED')} ${chalk.gray(file.path)}` ); + + if(showProgress) progress.tick(1); }); - - inst.on('beforeEmptyDirs', () => { - this.log('event', 'beforeEmptyDirs'); - - showSpinner(`Searching for empty directories in ${inst.options.cwd}...`); + + inst.on('file:deleted', file => { + this.log('event', 'file:deleted'); + this.stats.deleted += 1; this.logs.deleted.push(file.fullPath); + + this.log( + 'verbose', + `${chalk.yellow.bold('DELETED')} ${chalk.gray(file.path)}` + ); + + if(showProgress) progress.tick(1); }); - - inst.on('emptyDirs', (dirs) => { - this.log('event', 'emptyDirs'); - - if(process.stdout.isTTY) spinner.stop(); - + + inst.on('process:done', () => { + this.log('event', 'process:done'); + if(showProgress) progress.terminate(); + }); + + inst.on('emptydir:start', () => { + this.log('event', 'emptydir:start'); + spinner = utils.spinner(`Searching for empty directories in ${inst.options.cwd}...`); + }); + + inst.on('emptydir:list', dirs => { + this.log('event', 'emptydir:list'); + spinner.succeed(`Found ${dirs.length} empty directories to remove`); + this.stats.totalEmpty = dirs.length; - console.log(`\nFound ${chalk.green.bold(dirs.length)} empty directories to remove\n`); if(!dirs.length) return; - if(!showProgress && !program.interactive && !program.verbose) - console.log('Deleting empty directories, please wait...'); - - updateProgress(0, dirs.length); + if(!showProgress) console.log('Deleting empty directories, please wait...'); + if(showProgress) progress = utils.progressBar(dirs.length); }); - - inst.on('deletedEmptyDir', (dir) => { - this.stats.currentEmpty += 1; - updateProgress(this.stats.currentEmpty, this.stats.totalEmpty); - + + inst.on('emptydir:deleted', dir => { + this.log('event', 'emptydir:deleted'); + this.stats.deletedEmpty += 1; this.logs.deleted.push(dir); + this.log( 'verbose', - `${chalk.yellow.bold('DELETED EMPTY DIR')} (${this.stats.currentEmpty}/${this.stats.totalEmpty}) ${chalk.gray(dir)}` + `${chalk.yellow.bold('DELETED EMPTY DIR')} ${chalk.gray(dir)}` ); + + if(showProgress) progress.tick(1); }); - - inst.on('afterEmptyDirs', () => { - if(showProgress) process.stdout.write('\n'); - this.log('event', 'afterEmptyDirs'); + + inst.on('emptydir:done', () => { + this.log('event', 'emptydir:done'); + if(showProgress) progress.terminate(); }); - - // Error Event (called as soon as an error is encountered) + inst.on('error', (err) => { this.log('event', 'error'); - this.log('error', err.error); - }); - - // FileError Event (called when there was an error deleting a file) - inst.on('fileError', (err) => { - this.log('event', 'fileError'); - this.log('error', `${chalk.red.bold('FILE ERROR:')} ${err.error}\n${chalk.gray(err.file)}`); + this.log('error', err.message); + this.logs.errors.push({ message: err.message, method: err.method, payload: err.payload }); }); - - // Finish Event (once processing/deleting all files is complete) - inst.on('finish', (results) => { - if(showProgress) process.stdout.write('\n'); - this.log('event', 'finish'); - + + inst.on('clean:complete', res => { + this.log('event', 'clean:complete'); + this.log( 'verbose', - `${chalk.green('FINISH')} Deleted ${chalk.yellow.bold(results.length)} files/folders of ${chalk.yellow.bold(this.stats.total)}` + `${chalk.green('FINISH')} Deleted ${chalk.yellow.bold(res.deleted.length)} files/folders of ${chalk.yellow.bold(this.stats.total + this.stats.totalEmpty)}` ); }); - - // Complete Event (once everything has completed) - inst.on('complete', () => { - this.log('event', 'complete'); - }); } - - done(err, results) { async writeLogs() { if(!program.log) return; @@ -301,24 +263,18 @@ class ModClean_CLI { process.stdin.destroy(); return process.exit(1); } + + async done(results) { console.log( "\n" + chalk.green.bold('FILES/FOLDERS DELETED') + "\n" + - ` ${chalk.yellow.bold('Total: ')} ${results.length}\n` + - ` ${chalk.yellow.bold('Skipped: ')} ${this.stats.skipped.length}\n` + - ` ${chalk.yellow.bold('Empty: ')} ${this.stats.totalEmpty}\n` + ` ${chalk.yellow.bold('Total: ')} ${results.deleted.length}\n` + + ` ${chalk.yellow.bold('Skipped: ')} ${this.stats.skipped}\n` + + ` ${chalk.yellow.bold('Empty: ')} ${this.stats.deletedEmpty}\n` ); - setTimeout(() => { - process.stdin.destroy(); - - if(err) { - this.log('error', err); - return process.exit(1); - } - - process.exit(0); - }, 500); await this.writeLogs(); + process.stdin.destroy(); + return process.exit(0); } } From 232c4c1f38d5109e8fdebc34b8acbd93f891d25b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:41:58 -0400 Subject: [PATCH 41/66] :recycle: Progress bars and spinners After switching to new lighter-weight dependencies for progress bars and spinners, the functionality has been moved to utility functions for easier use. --- bin/utils.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bin/utils.js b/bin/utils.js index 1c0f787..568e970 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -3,7 +3,9 @@ */ "use strict"; const readline = require('readline'); -const chalk = require('chalk'); +const chalk = require('chalk'); +const ora = require('ora'); +const ProgressBar = require('progress'); exports.warningMsg = ` This module deletes files from the filesystem and cannot be recovered. @@ -30,6 +32,17 @@ exports.confirm = function(msg, cb) { }); }; +exports.progressBar = function(total) { + return new ProgressBar('[:bar] :percent (:current/:total) :etas', { + total: total, + incomplete: ' ', + width: 30 + }); +}; + +exports.spinner = function(msg) { + return ora(msg).start(); +}; exports.initLog = function(verbose) { let types = { From 13a2400bc51a1ec331ec8e45d9402855d5baa900 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 14 May 2018 12:44:57 -0400 Subject: [PATCH 42/66] :bulb: Updating changelog --- HISTORY.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 4547cf3..bc3700e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,55 @@ # ModClean History +## 3.0.0-alpha.1 (5/10/2018) +* Updated dependencies: + - `chalk` @ 2.4.1 + - `empty-dir` @ 1.0.0 + - `glob` @ 7.1.2 + - `subdirs` @ 1.0.1 +* Removed dependencies: + - `async-each-series` + - `clui` + - `modclean-patterns-default` + - `update-notifier` +* Added dependencies: + - `await-handler` + - `ora` + - `progress` + +#### ModClean API Changes +* **BREAKING:** Switch from callbacks to promises. +* **BREAKING:** Utilizes async/await which will require Node 8+. +* **BREAKING:** ModClean constructor no longer calls `clean()` by default, it must be manually called. +* **BREAKING:** Stored errors in `errors` is now the pure `Error` with additional properties attached. +* **BREAKING:** Renamed and namespaced most emitted events for clarity: + - `start` is now `clean:start` + - `complete` is now `clean:complete` + - `deleted` is now `file:deleted` + - `process` is now `file:list` + - `finish` is now `process:done` + - `beforeFind` is now `file:find` + - `beforeEmptyDirs` is now `emptydir:start` + - `afterEmptyDirs` is now `emptydir:done` + - `emptyDirs` is now `emptydir:list` + - `deletedEmptyDir` is now `emptydir:deleted` +* **BREAKING:** Removed `finish` event. +* **BREAKING:** Empty directory errors no longer emit event `emptyDirError`, but emit standard `error` event instead. +* **BREAKING:** File deletion errors no longer emit event `fileError`, but emit standard `error` event instead. +* **BREAKING:** `cleanEmptyDirs()` results are now an object containing `empty` (array of all empty directories) and `deleted` (array of all removed directories). +* **BREAKING:** Files are now objects instead of strings. The new file objects contain more information about a file/directory being removed. _This does not apply to empty directories._ +* **BREAKING:** Removed `process` option in favor for new `filter` option. +* **NEW:** Added `globOptions` to options object. +* **NEW:** Added `filter` function to options object. +* **NEW:** Added `skipModules` to options object. Will skip deletion of a directory if determined it is a module. +* **NEW:** Added `file:skipped` event when a file/directory is skipped due to `filter` result. +* **NEW:** Added `process:start` event when file processing starts. +* **PERFORMANCE:** Reduced old and outdated dependencies to bring ModCleans dependency weight from over 9MB down to 1.2MB. +* **PERFORMANCE:** Files are now deleted in parallel instead of series. + +#### ModClean CLI Changes +* **BREAKING:** Removed `-i, --interactive` option. This option provided no real gain and added overhead which slows down the entire process. You should run in test mode if you want to see the files/folders being deleted prior. +* **NEW:** Added `-l, --log` option. Outputs log files for deleted and skipped files along with errors in the current working directory on completion. + ## 2.1.2 (11/6/2017) * Update package.json to use latest version of `modclean-patterns-default` From f16f7bcbe1b10d99a77fc6df923c9c0a7fe7d25b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 11:42:21 -0400 Subject: [PATCH 43/66] :pencil2: Removing note about -i flag --- bin/utils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/utils.js b/bin/utils.js index 568e970..3bdc82f 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -14,9 +14,8 @@ exports.warningMsg = work correctly after running this script. You can easily restore your modules by deleting your "node_modules" folder and running "npm install" again. If you have any concerns with deleting files, you should run this utility with the - "--test" flag first or with the "-i" flag to make the process interactive. The - author or contributors shall not be held responsible for damages this module - might cause. Please see the README for more information.\n\n`; + "--test" flag first. The author or contributors shall not be held responsible + for damages this module might cause. Please see the README for more information.\n\n`; exports.confirm = function(msg, cb) { process.stdin.setEncoding('utf8'); From ed385efc348542b3811836b2311f5a31b3becebb Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 11:53:03 -0400 Subject: [PATCH 44/66] :recycle: Move extra space to constructor --- bin/modclean.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/modclean.js b/bin/modclean.js index 30a5464..6c09682 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -21,9 +21,6 @@ process.on('SIGINT', function() { process.exit(); }); -// Add a space at the top -console.log("\n"); - program .version(pkg.version) .description('Remove unwanted files and directories from your node_modules folder') @@ -51,6 +48,9 @@ class ModClean_CLI { constructor() { this.log = utils.initLog(program.verbose); + // Add a space at the top + console.log("\n"); + // Display CLI header console.log( chalk.yellow.bold('MODCLEAN ') + From d17678db3c9eb579ac3dc883648f1e4e0681934b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 11:53:31 -0400 Subject: [PATCH 45/66] :memo: Start updating the docs --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6bd6aa..76f71e5 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,22 @@ [![npm version](https://img.shields.io/npm/v/modclean.svg)](https://www.npmjs.com/package/modclean) ![NPM Dependencies](https://david-dm.org/ModClean/modclean.svg) [![NPM Downloads](https://img.shields.io/npm/dm/modclean.svg)](https://www.npmjs.com/package/modclean) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/ModClean/modclean/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/ModClean/modclean.svg)](https://github.com/ModClean/modclean/issues) [![Package Quality](http://npm.packagequality.com/shield/modclean.svg)](http://packagequality.com/#?package=modclean) -### This documentation is for ModClean 2.x which requires Node v6.9+, if you need to support older versions, use [ModClean 1.3.0](https://github.com/ModClean/modclean/tree/1.x) instead. +### This documentation is for ModClean 3.x which requires Node v6.9+, if you need to support older versions, use: +**Node v6.9+:** [ModClean 2.x](https://github.com/ModClean/modclean/tree/2.x) +**Older:** [ModClean 1.x](https://github.com/ModClean/modclean/tree/1.x) + ModClean is a utility that finds and removes unnecessary files and folders from your `node_modules` directory based on [predefined](https://github.com/ModClean/modclean-patterns-default) and [custom](https://github.com/ModClean/modclean/wiki/Custom-Pattern-Plugins) [glob](https://github.com/isaacs/node-glob) patterns. This utility comes with both a CLI and a programmatic API to provide customization for your environment. ModClean is used and tested in an Enterprise environment on a daily basis. +## What's New in ModClean 3? +This has been a complete overhaul of ModClean and the CLI to provide more performance and new features. With any major change, there have been a lot of breaking changes as well (see the [Migration Guide]() for more info). Here are some of the new notable features: + +1. **Moved away from callbacks to Promises.** With Promise support, ModClean utilizes async/await in order to have cleaner and more performant code. +2. **New options for more customization.** Now you can customize ModClean more including options passed into `glob`. +3. **NPM Module detection support.** ModClean now has the ability to detect if a matched directory is a NPM module and prevent deleting it. This allows stricter patterns without the worry of potentially deleting a module. +4. **New event names.** Events are now properly named and namespaced for easier use and understanding. +5. **Dependency reduction.** Reduced the dependencies and brought in newer and more usable dependencies that brought ModClean from over 9MBs to 1.2MB. + ## Why? There are a few different reasons why you would want to use ModClean: @@ -20,7 +32,7 @@ There are a few different reasons why you would want to use ModClean: The :cake: is a lie, but the [Benchmarks](https://github.com/ModClean/modclean/wiki/Benchmarks) are not. ## How? -**New!** In ModClean 2.0.0, patterns are now provided by plugins instead of a static `patterns.json` file as part of the module. By default, ModClean comes with [modclean-patterns-default](https://github.com/ModClean/modclean-patterns-default) installed, providing the same patterns as before. You now have the ability to create your own patterns plugins and use multiple plugins to clean your modules. This allows flexibility with both the programmatic API and CLI. +**Note:** In ModClean 2+, patterns are now provided by plugins instead of a static `patterns.json` file as part of the module. By default, ModClean comes with [modclean-patterns-default](https://github.com/ModClean/modclean-patterns-default) installed, providing the same patterns as before. You now have the ability to create your own patterns plugins and use multiple plugins to clean your modules. This allows flexibility with both the programmatic API and CLI. ModClean scans the `node_modules` directory of your choosing, finding all files and folders that match the defined patterns and deleting them. Both the CLI and the programmatic API provides all the options needed to customize this process to your requirements. Depending on the number of modules your app requires, files can be reduced anywhere from hundreds to thousands and disk space can be reduced considerably. @@ -29,6 +41,8 @@ _(File and disk space reduction can also be different between the version of NPM **IMPORTANT** This module has been heavily tested in an enterprise environment used for large enterprise applications. The provided patterns in [modclean-patterns-default](https://github.com/ModClean/modclean-patterns-default) have worked very well when cleaning up useless files in many popular modules. There are hundreds of thousands of modules in NPM and I cannot simply cover them all. If you are using ModClean for the first time on your application, you should create a copy of the application so you can ensure it still runs properly after running ModClean. The patterns are set in a way to ensure no crutial module files are removed, although there could be one-off cases where a module could be affected and that's why I am stressing that testing and backups are important. If you find any files that should be removed, please create a pull request to [modclean-patterns-default](https://github.com/ModClean/modclean-patterns-default) or create your own patterns plugin to share with the community. +In ModClean 3+, module detection has been added. This will help prevent modules that have names matched by the patterns from being removed. + ## Removal Benchmark So how well does this module work? If we `npm install sails` and run ModClean on it, here are the results: From 22c21c1644b2c0ff630109a0897925e95a501878 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 11:54:54 -0400 Subject: [PATCH 46/66] Updating repo in package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8283def..ec93d60 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/ModClean/modclean" + "url": "https://github.com/ModClean/modclean/tree/3.0.0-dev" }, "keywords": [ "module", @@ -35,7 +35,7 @@ "bugs": { "url": "https://github.com/ModClean/modclean/issues" }, - "homepage": "https://github.com/ModClean/modclean", + "homepage": "https://github.com/ModClean/modclean/tree/3.0.0-dev", "dependencies": { "await-handler": "^1.1.0", "chalk": "^2.4.1", From ce4e2278ccd0e95d45424cafc9038cc235ef9165 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 11:56:25 -0400 Subject: [PATCH 47/66] :bookmark: Setting version to 3.0.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec93d60..2218e7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0", + "version": "3.0.0-alpha.1", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From 28f8645923be26fe707b45179a9e45de2a2b870b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 13:45:34 -0400 Subject: [PATCH 48/66] :zap: Added isDirectory to file objects --- lib/modclean.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 0156e7b..5fb4cc8 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -184,12 +184,17 @@ class ModClean extends ModClean_Utils { fullPath: path.join(this.options.cwd, file), dir: path.join(this.options.cwd, path.parse(file).dir), name: path.basename(file), - isModule: false + isModule: false, + isDirectory: null }; try { - let stats = await stat(obj.fullPath); - if(stats.isDirectory()) { + let stats = await stat(obj.fullPath), + isDir = stats.isDirectory(); + + obj.isDirectory = isDir; + + if(isDir) { let list = await readdir(obj.fullPath); if(list.indexOf('package.json')) obj.isModule = true; } From d0d89c0cfe7a40e195959cd0aa4f3956836a9bb6 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 13:45:47 -0400 Subject: [PATCH 49/66] v3.0.0-alpha.2 --- HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index bc3700e..dcee337 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,8 @@ # ModClean History +## 3.0.0-alpha.2 (5/16/2018) +* **NEW:** Added `isDirectory` as a property of file objects. + ## 3.0.0-alpha.1 (5/10/2018) * Updated dependencies: - `chalk` @ 2.4.1 diff --git a/package.json b/package.json index 2218e7e..8c4824b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0-alpha.1", + "version": "3.0.0-alpha.2", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From 5ac724a4aa935bbdb426f113b929743d69ef73fd Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 15:31:32 -0400 Subject: [PATCH 50/66] :zap: Added version properties --- HISTORY.md | 1 + lib/modclean.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index dcee337..fb820e2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,7 @@ ## 3.0.0-alpha.2 (5/16/2018) * **NEW:** Added `isDirectory` as a property of file objects. +* **NEW:** Added `modclean.version` and `ModClean.version`. ## 3.0.0-alpha.1 (5/10/2018) * Updated dependencies: diff --git a/lib/modclean.js b/lib/modclean.js index 5fb4cc8..851f2ad 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -4,6 +4,8 @@ * @author Kyle Ross */ "use strict"; +const pkg = require('../package.json'); + const fs = require('fs'); const path = require('path'); const util = require('util'); @@ -118,7 +120,8 @@ class ModClean extends ModClean_Utils { */ constructor(options={}) { super(); - + this.version = pkg.version; + this.options = Object.assign({}, modclean.defaults, options || {}); this._patterns = this.initPatterns(); @@ -390,3 +393,9 @@ modclean.defaults = defaults; * @type {Class} */ modclean.ModClean = ModClean; + +/** + * The ModClean version number. + * @type {String} + */ +modclean.version = pkg.version; From ae51857e3ec5551d01831426e5390f9e4b035c28 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 16 May 2018 15:33:24 -0400 Subject: [PATCH 51/66] v3.0.0-alpha.3 --- HISTORY.md | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index fb820e2..b1bfb98 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,10 @@ # ModClean History +## 3.0.0-alpha.3 (5/16/2018) +* **NEW:** Added `modclean.version` and `ModClean.version`. + ## 3.0.0-alpha.2 (5/16/2018) * **NEW:** Added `isDirectory` as a property of file objects. -* **NEW:** Added `modclean.version` and `ModClean.version`. ## 3.0.0-alpha.1 (5/10/2018) * Updated dependencies: diff --git a/package.json b/package.json index 8c4824b..364fb9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0-alpha.2", + "version": "3.0.0-alpha.3", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From ec5e67d7ef9b9ab455f6e99fe64194d9c190b418 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 17 May 2018 13:26:28 -0400 Subject: [PATCH 52/66] :recycle: Get modclean version from library --- bin/modclean.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/modclean.js b/bin/modclean.js index 6c09682..8728523 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -6,7 +6,6 @@ const program = require('commander'); const util = require('util'); const path = require('path'); const fs = require('fs'); -const pkg = require('../package.json'); const utils = require('./utils'); const modclean = require('../lib/modclean'); @@ -22,7 +21,7 @@ process.on('SIGINT', function() { }); program - .version(pkg.version) + .version(modclean.version) .description('Remove unwanted files and directories from your node_modules folder') .usage('modclean [options]') .option('-t, --test', 'Run modclean and return results without deleting files') From 66433c3d759d4ae0051b237b0a36ccc207095df7 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 17 May 2018 13:27:08 -0400 Subject: [PATCH 53/66] Adding more keywords --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 364fb9b..a6da1e6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,11 @@ "limit", "less", "files", - "folders" + "folders", + "prune", + "cruft", + "delete", + "npm" ], "author": "Kyle Ross ", "contributors": [ From b5544d48a84ac3d50db26fb35bcbe9b252cacced Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 17 May 2018 13:27:35 -0400 Subject: [PATCH 54/66] :bug: Typo causing all dirs to be modules --- lib/modclean.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modclean.js b/lib/modclean.js index 851f2ad..2aa5faf 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -199,7 +199,7 @@ class ModClean extends ModClean_Utils { if(isDir) { let list = await readdir(obj.fullPath); - if(list.indexOf('package.json')) obj.isModule = true; + if(list.indexOf('package.json') !== -1) obj.isModule = true; } obj.stat = stats; From f87797cb9c346b743c40418fdbdcf36f51176392 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 17 May 2018 13:27:48 -0400 Subject: [PATCH 55/66] 3.0.0-alpha.4 --- HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index b1bfb98..cd46f67 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,8 @@ # ModClean History +## 3.0.0-alpha.4 (5/17/2018) +* **FIX:** Fix typo that caused all directories to be marked as a module. + ## 3.0.0-alpha.3 (5/16/2018) * **NEW:** Added `modclean.version` and `ModClean.version`. diff --git a/package.json b/package.json index a6da1e6..0e29d34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0-alpha.3", + "version": "3.0.0-alpha.4", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From e2cb0276c02980d1f4bc7f01bd3e4a746068a544 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 17 May 2018 16:47:29 -0400 Subject: [PATCH 56/66] :zap: Improvement when matching modules --- lib/modclean.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 2aa5faf..e9a5310 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -198,8 +198,10 @@ class ModClean extends ModClean_Utils { obj.isDirectory = isDir; if(isDir) { - let list = await readdir(obj.fullPath); - if(list.indexOf('package.json') !== -1) obj.isModule = true; + let list = await readdir(obj.fullPath), + parent = path.basename(path.join(obj.fullPath, '../')); + + if(list.indexOf('package.json') !== -1 && parent === this.options.modulesDir) obj.isModule = true; } obj.stat = stats; From 4775049fc9ac57efed7f264673af4eb6264bc559 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 17 May 2018 16:47:49 -0400 Subject: [PATCH 57/66] 3.0.0-alpha.5 --- HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index cd46f67..b86832a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,8 @@ # ModClean History +## 3.0.0-alpha.5 (5/17/2018) +* **IMPROVEMENT:** Better checking whether a directory is a module by ensuring the parent directory is equal to `options.modulesDir`. + ## 3.0.0-alpha.4 (5/17/2018) * **FIX:** Fix typo that caused all directories to be marked as a module. diff --git a/package.json b/package.json index 0e29d34..4cec988 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From 01ecfb5c48b229762f2dc7fe8abbbf376ed3dc42 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 18 May 2018 11:46:41 -0400 Subject: [PATCH 58/66] :bug: Fix old variable reference in CLI (#24) --- bin/modclean.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/modclean.js b/bin/modclean.js index 8728523..686f475 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -53,7 +53,7 @@ class ModClean_CLI { // Display CLI header console.log( chalk.yellow.bold('MODCLEAN ') + - chalk.gray(' Version ' + pkg.version) + "\n" + chalk.gray(' Version ' + modclean.version) + "\n" ); // Display "running in test mode" message From 20f3da70b97865ff0cb748f73315df80d6ccda3d Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 18 May 2018 11:46:53 -0400 Subject: [PATCH 59/66] 3.0.0-alpha.6 --- HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index b86832a..092dca5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,8 @@ # ModClean History +## 3.0.0-alpha.6 (5/18/2018) +* **FIX:** Fixed old variable reference in CLI (#24) + ## 3.0.0-alpha.5 (5/17/2018) * **IMPROVEMENT:** Better checking whether a directory is a module by ensuring the parent directory is equal to `options.modulesDir`. diff --git a/package.json b/package.json index 4cec988..fc20790 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From bb5995dd5196e35e2746720813dd120b0fdf4b0c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 18 May 2018 12:18:05 -0400 Subject: [PATCH 60/66] :sparkles: Remove patterns that are ignored (#19) In order to prevent race conditions in node-glob, this will now remove matching patterns that exist in the ignore patterns. --- lib/utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index b5183e9..2e57f8b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -74,6 +74,8 @@ class ModClean_Utils extends EventEmitter { patterns = uniq(patterns); ignore = uniq(ignore); + patterns = patterns.filter(pat => ignore.indexOf(pat) === -1); + if(!patterns.length) throw new Error('No patterns have been loaded, nothing to check against'); return { From 7669cd644034ca4e4f6be39bc484cf7089b337a0 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 18 May 2018 12:20:02 -0400 Subject: [PATCH 61/66] :recycle: Make additionalPatterns null by default --- lib/modclean.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index e9a5310..350feb2 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -34,9 +34,9 @@ let defaults = { patterns: ['default:safe'], /** * Array of additional patterns to use. - * @type {Array} + * @type {?Array} */ - additionalPatterns: [], + additionalPatterns: null, /** * Ignore the provided glob patterns * @type {?Array} From f15f7587084c3befdb3ba965077c7ba173f10f28 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 21 May 2018 11:56:22 -0400 Subject: [PATCH 62/66] :recycle: Added function to create file object --- lib/modclean.js | 73 ++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/lib/modclean.js b/lib/modclean.js index 350feb2..246a95c 100644 --- a/lib/modclean.js +++ b/lib/modclean.js @@ -181,37 +181,9 @@ class ModClean extends ModClean_Utils { if(err) throw this.error(err, '_find'); let [e, files] = await on( - Promise.all(results.map(async file => { - let obj = { - path: file, - fullPath: path.join(this.options.cwd, file), - dir: path.join(this.options.cwd, path.parse(file).dir), - name: path.basename(file), - isModule: false, - isDirectory: null - }; - - try { - let stats = await stat(obj.fullPath), - isDir = stats.isDirectory(); - - obj.isDirectory = isDir; - - if(isDir) { - let list = await readdir(obj.fullPath), - parent = path.basename(path.join(obj.fullPath, '../')); - - if(list.indexOf('package.json') !== -1 && parent === this.options.modulesDir) obj.isModule = true; - } - - obj.stat = stats; - } catch(error) { - this.error(error, '_find'); - return null; - } - - return obj; - })) + Promise.all( + results.map(async file => await this._buildFileObject(file)) + ) ); if(e) throw e; @@ -221,6 +193,45 @@ class ModClean extends ModClean_Utils { this.emit('file:list', files); return files; } + + /** + * Creates file object for the specified `file` path + * @async + * @private + * @param {String} file File path to create object for + * @return {Object} Object with all file properties + */ + async _buildFileObject(file) { + let obj = { + path: file, + fullPath: path.join(this.options.cwd, file), + dir: path.join(this.options.cwd, path.parse(file).dir), + name: path.basename(file), + isModule: false, + isDirectory: null + }; + + try { + let stats = await stat(obj.fullPath), + isDir = stats.isDirectory(); + + obj.isDirectory = isDir; + + if(isDir) { + let list = await readdir(obj.fullPath), + parent = path.basename(path.join(obj.fullPath, '../')); + + if(list.indexOf('package.json') !== -1 && parent === (this.options.modulesDir || 'node_modules')) obj.isModule = true; + } + + obj.stat = stats; + } catch(error) { + this.error(error, '_buildFileObject'); + return null; + } + + return obj; + } /** * Processes the found files and deletes them in parallel. From a15989d56275c57fc4a9620e3c4f99da2bb05299 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 21 May 2018 12:00:20 -0400 Subject: [PATCH 63/66] :sparkles: Allow no pattern defs to be sent in --- lib/utils.js | 66 +++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 2e57f8b..9efcd02 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -27,43 +27,45 @@ class ModClean_Utils extends EventEmitter { patterns = [], ignore = []; - if(!Array.isArray(patDefs)) patDefs = [patDefs]; - - patDefs.forEach((def) => { - def = def.split(':'); - let mod = def[0], - name = def[1], - loader = this._loadPatterns(mod), - results; - - mod = loader.module; - results = loader.patterns; - - let all = Object.keys(results).filter(function(val) { - return val[0] !== '$'; - }); - - if(!name) { - if(results.$default) name = results.$default; - else name = all[0]; - } - - if(name === '*') name = all; + if(patDefs) { + if(!Array.isArray(patDefs)) patDefs = [patDefs]; - let rules = Array.isArray(name)? name : name.split(','); - - rules.forEach(function(rule) { - if(!results.hasOwnProperty(rule)) throw new Error(`Module "${mod}" does not contain rule "${rule}"`); - let obj = results[rule]; + patDefs.forEach((def) => { + def = def.split(':'); + let mod = def[0], + name = def[1], + loader = this._loadPatterns(mod), + results; + + mod = loader.module; + results = loader.patterns; - if(Array.isArray(obj)) return patterns = patterns.concat(obj); + let all = Object.keys(results).filter(function(val) { + return val[0] !== '$'; + }); - if(typeof obj === 'object') { - if(obj.hasOwnProperty('patterns')) patterns = patterns.concat(obj.patterns); - if(obj.hasOwnProperty('ignore')) ignore = ignore.concat(obj.ignore); + if(!name) { + if(results.$default) name = results.$default; + else name = all[0]; } + + if(name === '*') name = all; + + let rules = Array.isArray(name)? name : name.split(','); + + rules.forEach(function(rule) { + if(!results.hasOwnProperty(rule)) throw new Error(`Module "${mod}" does not contain rule "${rule}"`); + let obj = results[rule]; + + if(Array.isArray(obj)) return patterns = patterns.concat(obj); + + if(typeof obj === 'object') { + if(obj.hasOwnProperty('patterns')) patterns = patterns.concat(obj.patterns); + if(obj.hasOwnProperty('ignore')) ignore = ignore.concat(obj.ignore); + } + }); }); - }); + } let addlPats = this.options.additionalPatterns, addlIgnore = this.options.ignorePatterns; From 9a181486804e8b6c8ff6ac6babf11cb524534fd5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 21 May 2018 12:02:31 -0400 Subject: [PATCH 64/66] :recycle: Misc. cleanup/refactor --- bin/modclean.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/modclean.js b/bin/modclean.js index 686f475..f809a08 100755 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -23,19 +23,19 @@ process.on('SIGINT', function() { program .version(modclean.version) .description('Remove unwanted files and directories from your node_modules folder') - .usage('modclean [options]') - .option('-t, --test', 'Run modclean and return results without deleting files') + .usage('[options]') .option('-p, --path ', 'Path to run modclean on (defaults to current directory)') .option('-D, --modules-dir ', 'Modules directory name (defaults to "node_modules")') + .option('-n, --patterns ', 'Patterns plugins/rules to use (defaults to "default:safe")', list) + .option('-a, --additional-patterns ', 'Additional glob patterns to search for', list) + .option('-I, --ignore ', 'Comma separated list of patterns to ignore', list) + .option('-t, --test', 'Run modclean and return results without deleting files') .option('-s, --case-sensitive', 'Matches are case sensitive') .option('-P, --no-progress', 'Hide progress bar') .option('-e, --error-halt', 'Halt script on error') .option('-v, --verbose', 'Run in verbose mode') .option('-f, --follow-symlink', 'Clean symlinked packages as well') .option('-r, --run', 'Run immediately without warning') - .option('-n, --patterns ', 'Patterns plugins/rules to use (defaults to "default:safe")', list) - .option('-a, --additional-patterns ', 'Additional glob patterns to search for', list) - .option('-I, --ignore ', 'Comma separated list of patterns to ignore', list) .option('--no-dirs', 'Exclude directories from being removed') .option('--no-dotfiles', 'Exclude dot files from being removed') .option('-k, --keep-empty', 'Keep empty directories') From 0b86c799eee083c39d6a771c527450cf3fda6d96 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Mon, 21 May 2018 12:03:05 -0400 Subject: [PATCH 65/66] 3.0.0-beta.1 --- HISTORY.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 092dca5..9e03796 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,14 @@ # ModClean History +## 3.0.0-beta.1 (5/21/2018) +First beta release of ModClean 3.0.0. + +* **IMPROVEMENT:** Remove "ignore" patterns from that matching patterns array to prevent race condition in node-glob (#19). +* **NEW:** Added ability to send in no pattern definitions. This will allow custom patterns to be quickly used (without a definition module) by using `options.additionalPatterns`. +* Make `additionalPatterns` option `null` by default to match other options. +* Refactored how file objects are generated by moving functionality to its own function. +* Misc. cleanup and refactoring. + ## 3.0.0-alpha.6 (5/18/2018) * **FIX:** Fixed old variable reference in CLI (#24) diff --git a/package.json b/package.json index fc20790..80b407a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0-alpha.6", + "version": "3.0.0-beta.1", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { From 9f8aa0b162c18a7763566967436bc75a9c02f91d Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Tue, 30 Oct 2018 13:04:46 -0400 Subject: [PATCH 66/66] Adding tests --- README.md | 32 +++---- package-lock.json | 188 ++++++++++++++++++++++++++++++++++++++- package.json | 11 ++- test/0-exports.js | 29 ++++++ test/1-defaults.js | 123 +++++++++++++++++++++++++ test/2-class.js | 82 +++++++++++++++++ test/support/fixtures.js | 42 +++++++++ 7 files changed, 488 insertions(+), 19 deletions(-) create mode 100644 test/0-exports.js create mode 100644 test/1-defaults.js create mode 100644 test/2-class.js create mode 100644 test/support/fixtures.js diff --git a/README.md b/README.md index 76f71e5..45948a8 100644 --- a/README.md +++ b/README.md @@ -46,34 +46,34 @@ In ModClean 3+, module detection has been added. This will help prevent modules ## Removal Benchmark So how well does this module work? If we `npm install sails` and run ModClean on it, here are the results: -_All tests ran on macOS 10.12.3 with Node v6.9.1 and NPM v4.0.5_ +_All tests ran on macOS 10.13.5 with Node v8.7.0 and NPM v6.0.1_ #### Using Default Safe Patterns `modclean -n default:safe` or `modclean` -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ------------ | -| Before ModClean | 16,179 | 1,941 | 71.24 MB | -| After ModClean | 12,192 | 1,503 | 59.35 MB | -| Reduced | **3,987** | **438** | **11.88 MB** | +| | Total Files | Total Folders | Total Size | +| --------------- | ----------- | ------------- | ----------- | +| Before ModClean | 11,112 | 857 | 32.40 MB | +| After ModClean | 9,721 | 705 | 27.62 MB | +| Reduction | **1,391** | **152** | **4.77 MB** | #### Using Safe and Caution Patterns `modclean -n default:safe,default:caution` -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ------------ | -| Before ModClean | 16,179 | 1,941 | 71.24 MB | -| After ModClean | 11,941 | 1,473 | 55.28 MB | -| Reduced | **4,238** | **468** | **15.95 MB** | +| | Total Files | Total Folders | Total Size | +| --------------- | ----------- | ------------- | ----------- | +| Before ModClean | 11,112 | 857 | 32.40 MB | +| After ModClean | 9,689 | 705 | 26.78 MB | +| Reduction | **1,423** | **152** | **5.62 MB** | #### Using Safe, Caution and Danger Patterns `modclean --patterns="default:*"` -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ------------ | -| Before ModClean | 16,179 | 1,941 | 71.24 MB | -| After ModClean | 11,684 | 1,444 | 51.76 MB | -| Reduced | **4,495** | **497** | **19.47 MB** | +| | Total Files | Total Folders | Total Size | +| --------------- | ----------- | ------------- | ----------- | +| Before ModClean | 11,112 | 857 | 32.40 MB | +| After ModClean | 9,628 | 701 | 25.89 MB | +| Reduction | **1,484** | **156** | **6.50 MB** | That makes a huge difference in the amount of files and disk space. diff --git a/package-lock.json b/package-lock.json index 963b3a8..b248b17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "3.0.0", + "version": "3.0.0-beta.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -17,6 +17,12 @@ "color-convert": "^1.9.0" } }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "await-handler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/await-handler/-/await-handler-1.1.0.tgz", @@ -36,6 +42,26 @@ "concat-map": "0.0.1" } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" + } + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -46,6 +72,12 @@ "supports-color": "^5.3.0" } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -87,6 +119,24 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "defaults": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", @@ -95,6 +145,12 @@ "clone": "^1.0.2" } }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "empty-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/empty-dir/-/empty-dir-1.0.0.tgz", @@ -110,11 +166,44 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "gar": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/gar/-/gar-1.0.3.tgz", + "integrity": "sha512-zDpwk/l3HbhjVAvdxNUTJFzgXiNy0a7EmE/50XT38o1z+7NJbFhp+8CDsv1Qgy2adBAwUVYlMpIX2fZUbmlUJw==", + "dev": true + }, + "get-folder-size": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-2.0.0.tgz", + "integrity": "sha512-5h4efQY/sHvf9ZuwOan1HgNaRyApKnJjZ1ZdTOPkpTjIHZNqeMTabBU/LLN6lU9jncBwxJKFcG9cuqiGhu47uQ==", + "dev": true, + "requires": { + "gar": "^1.0.2", + "tiny-each-async": "2.0.3" + } + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -128,11 +217,29 @@ "path-is-absolute": "^1.0.0" } }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -147,6 +254,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -173,11 +289,51 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, "modclean-patterns-default": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/modclean-patterns-default/-/modclean-patterns-default-1.1.1.tgz", "integrity": "sha1-UvCFIbosOVPt9XEdKEeYipRuHGQ=" }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -212,6 +368,12 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "progress": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", @@ -234,6 +396,12 @@ "glob": "^7.0.5" } }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -263,6 +431,24 @@ "has-flag": "^3.0.0" } }, + "tiny-each-async": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", + "integrity": "sha1-jru/1tYpXxNwAD+7NxYq/loKUdE=", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 80b407a..9629f33 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "modclean": "./bin/modclean.js" }, "scripts": { - "test": "npm install && mocha test.js" + "test": "./node_modules/.bin/mocha -S" }, "repository": { "type": "git", @@ -56,5 +56,12 @@ "engines": { "node": ">=8.0.0" }, - "engineStrict": true + "engineStrict": true, + "devDependencies": { + "chai": "^4.1.2", + "fs-extra": "^6.0.1", + "get-folder-size": "^2.0.0", + "mocha": "^5.2.0", + "semver-regex": "^2.0.0" + } } diff --git a/test/0-exports.js b/test/0-exports.js new file mode 100644 index 0000000..2e4edae --- /dev/null +++ b/test/0-exports.js @@ -0,0 +1,29 @@ +const assert = require('chai').assert; +const semverRegex = require('semver-regex'); +const modclean = require('../'); + +describe ('Exports', () => { + it('should export a function', () => { + assert.isFunction(modclean); + }); + + it('should export default options', () => { + assert.isObject(modclean.defaults); + }); + + it ('should export access to ModClean class', () => { + assert.isFunction(modclean.ModClean); + }); + + it ('should export the version', () => { + assert.isString(modclean.version); + assert.match(modclean.version, semverRegex()); + }); + + describe ('Constructor Shortcut', () => { + it('should return new instance of ModClean', () => { + let mc = modclean(); + assert.instanceOf(mc, modclean.ModClean); + }); + }); +}); diff --git a/test/1-defaults.js b/test/1-defaults.js new file mode 100644 index 0000000..f7b8952 --- /dev/null +++ b/test/1-defaults.js @@ -0,0 +1,123 @@ +const assert = require('chai').assert; +const modclean = require('../'); + +describe ('Default Options', () => { + let def = modclean.defaults; + + it('should have all of the options', () => { + assert.hasAllKeys(modclean.defaults, [ + "cwd", + "patterns", + "additionalPatterns", + "ignorePatterns", + "noDirs", + "ignoreCase", + "dotFiles", + "modulesDir", + "filter", + "skipModules", + "removeEmptyDirs", + "emptyDirFilter", + "globOptions", + "errorHalt", + "test", + "followSymlink" + ]); + }); + + it ('should have defaults.cwd', () => { + assert.isString(def.cwd); + assert.equal(def.cwd, process.cwd()); + }); + + it ('should have defaults.patterns', () => { + assert.isArray(def.patterns); + assert.deepEqual(def.patterns, ['default:safe']); + }); + + it ('should have defaults.additionalPatterns', () => { + assert.isNull(def.additionalPatterns); + }); + + it ('should have defaults.ignorePatterns', () => { + assert.isNull(def.ignorePatterns); + }); + + it ('should have defaults.noDirs', () => { + assert.isBoolean(def.noDirs); + assert.isFalse(def.noDirs); + }); + + it ('should have defaults.ignoreCase', () => { + assert.isBoolean(def.ignoreCase); + assert.isTrue(def.ignoreCase); + }); + + it ('should have defaults.dotFiles', () => { + assert.isBoolean(def.dotFiles); + assert.isTrue(def.dotFiles); + }); + + it ('should have defaults.modulesDir', () => { + assert.isString(def.modulesDir); + assert.equal(def.modulesDir, 'node_modules'); + }); + + it ('should have defaults.filter', () => { + assert.isNull(def.filter); + }); + + it ('should have defaults.skipModules', () => { + assert.isBoolean(def.skipModules); + assert.isTrue(def.skipModules); + }); + + it ('should have defaults.removeEmptyDirs', () => { + assert.isBoolean(def.removeEmptyDirs); + assert.isTrue(def.removeEmptyDirs); + }); + + it ('should have defaults.emptyDirFilter', () => { + assert.isFunction(def.emptyDirFilter); + }); + + it ('should have defaults.globOptions', () => { + assert.isObject(def.globOptions); + assert.isEmpty(def.globOptions); + }); + + it ('should have defaults.errorHalt', () => { + assert.isBoolean(def.errorHalt); + assert.isFalse(def.errorHalt); + }); + + it ('should have defaults.test', () => { + assert.isBoolean(def.test); + assert.isFalse(def.test); + }); + + it ('should have defaults.followSymlink', () => { + assert.isBoolean(def.followSymlink); + assert.isFalse(def.followSymlink); + }); + + describe ('defaults.emptyDirFilter', () => { + it('should return true for valid file', () => { + assert.isTrue( + def.emptyDirFilter('/path/to/valid.txt') + ); + }); + + it ('should return false for invalid file (.DS_Store)', () => { + assert.isFalse( + def.emptyDirFilter('/path/to/.DS_Store') + ); + }); + + it('should return false for invalid file (Thumbs.db)', () => { + assert.isFalse( + def.emptyDirFilter('/path/to/Thumbs.db') + ); + }); + }); +}); diff --git a/test/2-class.js b/test/2-class.js new file mode 100644 index 0000000..6a4afa0 --- /dev/null +++ b/test/2-class.js @@ -0,0 +1,82 @@ +const assert = require('chai').assert; +const path = require('path'); +const semverRegex = require('semver-regex'); +const modclean = require('../'); + +describe ('ModClean Class', () => { + let mc = modclean(); + + it ('should have version property', () => { + assert.isString(mc.version); + assert.match(mc.version, semverRegex()); + }); + + it ('should have EventEmitter', () => { + assert.isFunction(mc.emit); + assert.isFunction(mc.on); + }); + + describe ('Default Options', () => { + let mc = modclean(); + + it('should set options.cwd', () => { + assert.equal(mc.options.cwd, path.join(process.cwd(), mc.options.modulesDir)); + }); + + it ('should set _patterns', () => { + assert.isObject(mc._patterns); + assert.hasAllKeys(mc._patterns, ['allow', 'ignore']); + + assert.isArray(mc._patterns.allow); + assert.isArray(mc._patterns.ignore); + }); + }); + + + describe ('Custom Options', () => { + let mc = modclean({ + patterns: ['default:*'], + additionalPatterns: ['custom.file'], + ignorePatterns: ['ignore.file', 'cname'], + modulesDir: false, + filter: function() { + return true; + }, + globOptions: { + dot: true + } + }); + + it('should just use options.cwd without modulesDir', () => { + assert.equal(mc.options.cwd, process.cwd()); + }); + + it('should set _patterns', () => { + assert.isObject(mc._patterns); + assert.hasAllKeys(mc._patterns, ['allow', 'ignore']); + + assert.isArray(mc._patterns.allow); + assert.isArray(mc._patterns.ignore); + }); + + it ('should add allowed patterns', () => { + assert.include(mc._patterns.allow, 'custom.file'); + }); + + it ('should add ignored patterns', () => { + assert.include(mc._patterns.ignore, 'ignore.file'); + }); + + it ('should remove ignored patterns from allowed patterns', () => { + assert.notInclude(mc._patterns.allow, 'cname'); + }); + + it ('should have a filter function', () => { + assert.isFunction(mc.options.filter); + }); + + it ('should have globOptions', () => { + assert.deepEqual(mc.options.globOptions, { dot: true }); + }); + }); +}); diff --git a/test/support/fixtures.js b/test/support/fixtures.js new file mode 100644 index 0000000..d969480 --- /dev/null +++ b/test/support/fixtures.js @@ -0,0 +1,42 @@ +const fs = require('fs-extra'); +const cp = require('child_process'); +const util = require('util'); +const path = require('path'); + +const execFile = util.promisify(cp.execFile); + +class ModClean_Fixtures { + constructor(opts = {}) { + this.opts = Object.assign({ + path: path.join(process.cwd(), 'test/fixtures'), + modules: ['express', 'lodash'] + }, opts); + + this.fixtures = this.opts.path; + } + + async setupDir() { + await fs.mkdirp(this.fixtures).catch(e => { throw e; }); + return this.fixtures; + } + + async execNPM(args) { + let options = { + cwd: this.fixtures + }; + + return await execFile('npm', args, options); + } + + async installModules(modules) { + await this.execNPM(['install'].concat(modules)).catch(e => { throw e; }); + return true; + } + + async cleanup() { + await fs.remove(this.fixtures).catch(e => { throw e; }); + return true; + } +} + +module.exports = ModClean_Fixtures;