diff --git a/.gitignore b/.gitignore index 8ad66d40..8327fc64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea/ /node_modules/ /*.log +/lib-es5/ diff --git a/lib/cli.js b/lib/cli.js index 61dc6832..cead6073 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -26,6 +26,7 @@ const cli = meow({ -u, --update Interactive update. -g, --global Look at global modules. -s, --skip-unused Skip check for unused packages. + -r, --remove-unused Automatically remove unused dependencies. -p, --production Skip devDependencies. -i, --ignore Ignore dependencies based on succeeding glob. -E, --save-exact Save exact version (x.y.z) instead of caret (^x.y.z) in package.json. @@ -43,6 +44,7 @@ const cli = meow({ u: 'update', g: 'global', s: 'skip-unused', + r: 'remove-unused', p: 'production', E: 'save-exact', i: 'ignore' @@ -56,6 +58,7 @@ const cli = meow({ 'update', 'global', 'skip-unused', + 'remove-unused', 'production', 'save-exact', 'color', @@ -72,6 +75,7 @@ const options = { update: cli.flags.update, global: cli.flags.global, skipUnused: cli.flags.skipUnused, + removeUnused: cli.flags.removeUnused, ignoreDev: cli.flags.production, saveExact: cli.flags.saveExact, emoji: cli.flags.emoji, diff --git a/lib/in/create-package-summary.js b/lib/in/create-package-summary.js index 37266df4..1bf017ad 100644 --- a/lib/in/create-package-summary.js +++ b/lib/in/create-package-summary.js @@ -104,7 +104,8 @@ function createPackageSummary(moduleName, currentState) { bump !== 'major', bump: bump, - unused: unused + unused: unused, + removed: unused && currentState.get('removeUnused') }; }); } diff --git a/lib/in/get-unused-packages.js b/lib/in/get-unused-packages.js index 09a5fc1d..87dbcaf2 100644 --- a/lib/in/get-unused-packages.js +++ b/lib/in/get-unused-packages.js @@ -2,15 +2,9 @@ const depcheck = require('depcheck'); const ora = require('ora'); +const skipUnused = require('./skip-unused-packages'); const _ = require('lodash'); -function skipUnused(currentState) { - return currentState.get('skipUnused') || // manual option to ignore this - currentState.get('global') || // global modules - currentState.get('update') || // in the process of doing an update - !currentState.get('cwdPackageJson').name; // there's no package.json -} - function checkUnused(currentState) { const spinner = ora(`Checking for unused packages. --skip-unused if you don't want this.`); spinner.enabled = spinner.enabled && currentState.get('spinner'); diff --git a/lib/in/index.js b/lib/in/index.js index f459118d..3aa4693a 100644 --- a/lib/in/index.js +++ b/lib/in/index.js @@ -3,11 +3,13 @@ const co = require('co'); const merge = require('merge-options'); const ora = require('ora'); const getUnusedPackages = require('./get-unused-packages'); +const removeUnusedPackages = require('./remove-unused-packages'); const createPackageSummary = require('./create-package-summary'); module.exports = function (currentState) { return co(function *() { yield getUnusedPackages(currentState); + yield removeUnusedPackages(currentState); const spinner = ora(`Checking npm registries for updated packages.`); spinner.enabled = spinner.enabled && currentState.get('spinner'); diff --git a/lib/in/remove-unused-packages.js b/lib/in/remove-unused-packages.js new file mode 100644 index 00000000..fbd78e0f --- /dev/null +++ b/lib/in/remove-unused-packages.js @@ -0,0 +1,36 @@ +'use strict'; + +const exec = require('child_process').exec; +const ora = require('ora'); +const skipUnused = require('./skip-unused-packages'); +const _ = require('lodash'); + +function skipAutoRemove(currentState) { + return skipUnused(currentState) || !currentState.get('removeUnused'); +} + +function removeUnused(currentState) { + const dependenciesToRemove = currentState.get('unusedDependencies'); + if (skipAutoRemove(currentState) || _.isEmpty(dependenciesToRemove)) { + return Promise.resolve(currentState); + } + + const spinner = ora(`Remove unused dependencies. --skip-unused if you don't want this.`); + spinner.enabled = spinner.enabled && currentState.get('spinner'); + spinner.start(); + + return new Promise(resolve => { + const removeCommand = 'npm remove --save ' + dependenciesToRemove.join(' '); + exec(removeCommand, {cwd: currentState.get('cwd')}, (error, stdout, stderr) => { + if (error || !_.isEmpty(stderr)) { + currentState.set('removeUnusedError', error || new Error(stderr)); + } + resolve(currentState); + }); + }).then(result => { + spinner.stop(); + return currentState; + }); +} + +module.exports = removeUnused; diff --git a/lib/in/skip-unused-packages.js b/lib/in/skip-unused-packages.js new file mode 100644 index 00000000..e1af2242 --- /dev/null +++ b/lib/in/skip-unused-packages.js @@ -0,0 +1,10 @@ +'use strict'; + +function skipUnused(currentState) { + return currentState.get('skipUnused') || // manual option to ignore this + currentState.get('global') || // global modules + currentState.get('update') || // in the process of doing an update + !currentState.get('cwdPackageJson').name; // there's no package.json +} + +module.exports = skipUnused; diff --git a/lib/out/static-output.js b/lib/out/static-output.js index 7eee0f69..4e60e750 100644 --- a/lib/out/static-output.js +++ b/lib/out/static-output.js @@ -17,9 +17,10 @@ function render(pkg, currentState) { const flags = currentState.get('global') ? '--global' : `--save${pkg.devDependency ? '-dev' : ''}`; const upgradeCommand = `npm install ${flags} ${packageName}@${pkg.latest}`; const upgradeMessage = `${chalk.green(upgradeCommand)} to go from ${pkg.installed} to ${pkg.latest}`; + // DYLAN: clean this up const status = _([ - pkg.notInstalled ? chalk.bgRed.white.bold(emoji(' :worried: ') + ' MISSING! ') + ' Not installed.' : '', + pkg.notInstalled && !pkg.removed ? chalk.bgRed.white.bold(emoji(' :worried: ') + ' MISSING! ') + ' Not installed.' : '', pkg.notInPackageJson ? chalk.bgRed.white.bold(emoji(' :worried: ') + ' PKG ERR! ') + ' Not in the package.json. ' + pkg.notInPackageJson : '', pkg.pkgError && !pkg.notInstalled ? chalk.bgGreen.white.bold(emoji(' :worried: ') + ' PKG ERR! ') + ' ' + chalk.red(pkg.pkgError.message) : '', pkg.bump && pkg.easyUpgrade ? [ @@ -30,12 +31,16 @@ function render(pkg, currentState) { chalk.white.bold.bgGreen((pkg.bump === 'nonSemver' ? emoji(' :sunglasses: ') + ' new ver! '.toUpperCase() : emoji(' :sunglasses: ') + ' ' + pkg.bump.toUpperCase() + ' UP ')) + ' ' + uppercaseFirstLetter(pkg.bump) + ' update available. ' + chalk.blue.underline(pkg.homepage || ''), indent + upgradeMessage ] : '', - pkg.unused ? [ + pkg.unused && !pkg.removed ? [ chalk.black.bold.bgWhite(emoji(' :confused: ') + ' NOTUSED? ') + ` ${chalk.yellow(`Still using ${packageName}?`)}`, indent + `Depcheck did not find code similar to ${chalk.green(`require('${packageName}')`)} or ${chalk.green(`import from '${packageName}'`)}.`, indent + `Check your code before removing as depcheck isn't able to foresee all ways dependencies can be used.`, indent + `Use ${chalk.green('--skip-unused')} to skip this check.`, - indent + `To remove this package: ${chalk.green(`npm uninstall --save${pkg.devDependency ? '-dev' : ''} ${packageName}`)}` + indent + `To remove this package: ${chalk.green(`npm uninstall --save${pkg.devDependency ? '-dev' : ''} ${packageName}`)} or add ${chalk.green(`--remove-unused`)} option for auto removing unused dependencies.` + ] : '', + pkg.removed ? [ + chalk.bgGreen.white.bold(emoji(' :thumbsup: ') + ' RM UNUSED ') + `The package was removed as unused.`, + indent + `Don't use ${chalk.green('--remove-unused')} or add ${chalk.green('--skip-unused')} option to skip this check.` ] : '', pkg.mismatch && !pkg.bump ? chalk.bgRed.yellow.bold(emoji(' :interrobang: ') + ' MISMATCH ') + ' Installed version does not match package.json. ' + pkg.installed + ' ≠ ' + pkg.packageJson : '', pkg.regError ? chalk.bgRed.white.bold(emoji(' :no_entry: ') + ' NPM ERR! ') + ' ' + chalk.red(pkg.regError) : '' @@ -85,6 +90,13 @@ function outputConsole(currentState) { console.log(''); console.log(renderedTable); console.log(`Use ${chalk.green(`npm-check -${currentState.get('global') ? 'g' : ''}u`)} for interactive update.`); + + const removeError = currentState.get('removeUnusedError'); + if (removeError) { + console.log("Auto removing dependencies have crashed with following error: ", + chalk.red(removeError)); + } + process.exitCode = 1; } else { console.log(`${emoji(':heart: ')}Your modules look ${chalk.bold('amazing')}. Keep up the great work.${emoji(' :heart:')}`); diff --git a/lib/state/state.js b/lib/state/state.js index 335a72fe..50821b67 100644 --- a/lib/state/state.js +++ b/lib/state/state.js @@ -9,6 +9,8 @@ const defaultOptions = { cwd: process.cwd(), nodeModulesPath: false, skipUnused: false, + removeUnused: false, + removeUnusedError: null, ignoreDev: false, forceColor: false, diff --git a/templates/readme/api.md b/templates/readme/api.md index c25cfe46..33f6b739 100644 --- a/templates/readme/api.md +++ b/templates/readme/api.md @@ -25,6 +25,11 @@ npmCheck(options) * Skip checking for unused packages. * default is `false` +### `removeUnused` + +* Automatically remove all unused packages. +* default is `false` + ### `ignoreDev` * Ignore `devDependencies`. diff --git a/templates/readme/cli.md b/templates/readme/cli.md index 908b684b..4e5fc9a4 100644 --- a/templates/readme/cli.md +++ b/templates/readme/cli.md @@ -33,6 +33,7 @@ Options -u, --update Interactive update. -g, --global Look at global modules. -s, --skip-unused Skip check for unused packages. + -r, --remove-unused Automatically remove unused dependencies. -p, --production Skip devDependencies. -i, --ignore Ignore dependencies based on succeeding glob. -E, --save-exact Save exact version (x.y.z) instead of caret (^x.y.z) in package.json. @@ -90,6 +91,14 @@ This option will skip that check. This is enabled by default when using `global` or `update`. +### `-r, --remove-unused` + +You can automatically remove all unused dependencies that `npm-check` marked as unused. You must also +be worried about false positive tests, so it's highly recommended to make a backup of your `package.json` +before running `npm-check` with this option. + +This option is disabled by default. + ### `-p, --production` By default `npm-check` will look at packages listed as `dependencies` and `devDependencies`. diff --git a/templates/readme/features.md b/templates/readme/features.md index cd56e13b..6e23f730 100644 --- a/templates/readme/features.md +++ b/templates/readme/features.md @@ -3,6 +3,7 @@ * Tells you what's out of date. * Provides a link to the package's documentation so you can decide if you want the update. * Kindly informs you if a dependency is not being used in your code. +* Allows to automatically remove all unused dependencies via `--remove-unused` option. * Works on your globally installed packages too, via `-g`. * **Interactive Update** for less typing and fewer typos, via `-u`. * Supports public and private [@scoped/packages](https://docs.npmjs.com/getting-started/scoped-packages).