diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..04c34f1 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,60 @@ +--- +root: true + +parserOptions: + ecmaVersion: 7 + +env: + node: true + es6: true + mocha: true + jasmine: true + +extends: 'eslint:recommended' + +rules: + indent: [2, 4, { "SwitchCase": 1 }] + linebreak-style: [2, "unix"] + semi: [2, "always"] + comma-dangle: [2, "never"] + consistent-return: 0 + no-plusplus: [2, { "allowForLoopAfterthoughts": true }] + eqeqeq: [2, "smart"] + wrap-iife: [2, "any"] + no-empty-function: 2 + no-console: 0 + no-mixed-spaces-and-tabs: 2 + no-whitespace-before-property: 2 + space-before-function-paren: [2, "never"] + space-in-parens: [2, "never"] + array-callback-return: 2 + class-methods-use-this: 0 + dot-notation: [2, { "allowKeywords": true }] + no-alert: 1 + no-caller: 2 + no-else-return: 2 + no-eval: 2 + no-extend-native: 2 + no-extra-bind: 2 + no-floating-decimal: 2 + no-implied-eval: 2 + no-iterator: 2 + no-lone-blocks: 2 + no-multi-spaces: 2 + no-new-wrappers: 2 + no-octal: 2 + no-proto: 2 + no-redeclare: 2 + no-self-assign: 2 + no-self-compare: 2 + no-throw-literal: 2 + no-useless-concat: 2 + no-useless-escape: 2 + no-useless-return: 2 + no-with: 2 + yoda: 2 + no-dupe-keys: 2 + use-isnan: 2 + no-unreachable: 2 + no-tabs: 2 + object-curly-spacing: [2, "always"] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 01d7c04..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: node_js -node_js: - - "4.1" - - "4.0" - - "1.0" - - "1.6" - - "0.12" - - "0.11" - - "0.10" - - "0.8" -before_install: - - npm install -g npm -before_script: - - npm install -script: 'mocha test.js' -notifications: - email: false diff --git a/BENCHMARK.md b/BENCHMARK.md deleted file mode 100644 index a4fc83b..0000000 --- a/BENCHMARK.md +++ /dev/null @@ -1,53 +0,0 @@ -# ModClean Benchmarks -This is an additional list of benchmarks to show other common modules/frameworks. All benchmark results are from [modclean-benchmark](https://github.com/KyleRoss/modclean-benchmark). Feel free to create your own benchmarks and submit them in this file through a pull request. - -### `npm install express lodash moment async` -Example of standard modules used to create basic web applications. - - modclean-benchmark -m express,lodash,moment,async -n safe - -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ------------- | -| Before ModClean | 753 | 81 | 5.13 MB | -| After ModClean | 612 | 78 | 4.76 MB | -| Reduced | **141** | **3** | **382.85 KB** | - - -### `npm install grunt grunt-contrib-clean grunt-contrib-concat grunt-contrib-copy grunt-contrib-uglify` -Example of a standard grunt installation. - - modclean-benchmark -m grunt,grunt-contrib-clean,grunt-contrib-concat,grunt-contrib-copy,grunt-contrib-uglify -n safe - -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ------------- | -| Before ModClean | 1,402 | 291 | 8.99 MB | -| After ModClean | 859 | 226 | 6.28 MB | -| Reduced | **543** | **65** | **2.71 MB** | - - -### `npm install lodash commander colors async express q underscore debug coffee-script request chalk mkdirp` -Example of the most depended-upon modules from [npmjs.com](https://www.npmjs.com/) (4/28/2015). - - modclean-benchmark -m lodash,commander,colors,async,express,q,underscore,debug,coffee-script,request,chalk,mkdirp -n safe - -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ------------- | -| Before ModClean | 1,285 | 222 | 5.09 MB | -| After ModClean | 819 | 190 | 3.62 MB | -| Reduced | **466** | **32** | **1.48 MB** | - - ---- - -## Large Application Examples - -### Application Framework Example -At the company I work for, our homegrown application framework uses a module set similar to the below: - - modclean-benchmark -m async,body-parser,colors,compression,cookie-parser,edge,express,express-debug,express-session,fs-extra,glob,humanize,jade,js-yaml,less,mime,moment,mongoose,ms,multer,node-uuid,numeral,on-finished,serve-favicon,lodash -n safe - -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ------------- | -| Before ModClean | 5,905 | 1,265 | 84.53 MB | -| After ModClean | 2,730 | 824 | 36.76 MB | -| Reduced | **3,175** | **441** | **47.76 MB** | diff --git a/HISTORY.md b/HISTORY.md index 70571b3..4547cf3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,69 @@ # ModClean History +## 2.1.2 (11/6/2017) +* Update package.json to use latest version of `modclean-patterns-default` + +## 2.1.1 (11/6/2017) +* **New!** Added options to ignore symlinked modules (@ben8p) #15 + - Added `followSymlink` configuration option. + - Added `-f`, `--follow-symlink` CLI option. +* Added `.eslintrc.yml` file to help enforce rules. +* Minor cleanup + +## 2.1.0 (1/17/2017) +* Added `emptyDirFilter` configuration option. +* Use `Object.assign` instead of `extend` module. + - Removed `extend` dependency. +* Removed unused `pad` dependency. +* Removed duplicate call to `clean()` in shortcut method. +* Misc. cleanup +* **Breaking Change:** All error events (`error`, `fileError` and `emptyDirsError`) now return error object. +* **New!** Added `errors` property on the ModClean class which contains all errors that occurred. +* Moved documentation to the Wiki. + +## 2.0.0 (1/11/2017) +### ModClean API Changes +* Complete rewrite using ES6 and some breaking changes (now requires Node v6.9+) +* No longer includes `patterns.json` file, instead uses plugins to allow further customization. +* **Breaking Change:** `patterns` option now takes array of pattern plugins instead of patterns. See README for more information. +* **Breaking Change:** `ignore` option has been renamed to `ignorePatterns`. +* **Breaking Change:** `process` option now is sync with only 1 argument and async with 2. +* **New!** Added `additionalPatterns` option that takes array of glob patterns to use with any provided pattern plugins. +* **New!** Added `dotFiles` option that allows disabling of removing dot files. +* **New!** Added `process` event which will be fired before the files start processing. +* **New!** When finding empty directories, it will now ignore `.DS_Store` and `Thumbs.db` files. +* **New!** Added `beforeFind` event. +* **New!** Added `beforeEmptyDirs` event. +* **New!** Added `emptyDirs` event. +* **New!** Added `emptyDirError` event. +* **New!** Added `afterEmptyDirs` event. +* **New!** Added `deletedEmptyDir` event. +* Removed utility functions in favor for micro-packages. + * Removed unused dependencies. + * Replaced `node-dir` dependency with `subdirs`. + * Replaced `colors` dependency with `chalk`. + * Added `empty-dir` dependency. + * Added `async-each-series` dependency. + * Added `extend` dependency. + * Added `lodash.uniq` dependency. +* Updated all dependcies to their latest versions. +* Benchmarks updated for 2017. + +### ModClean CLI Changes +* Rewrite using ES6 with new features and bug fixes. +* **Breaking Change:** `-n, --patterns` option now takes list of plugins instead of a pattern rule name. +* **Removed:** `-d, --empty` option. +* **New!** Added `-D, --modules-dir` option. +* **New!** Added `-a, --additional-patterns` option. +* **New!** Added `-k, --keep-empty` option. +* **New!** Added `--no-dotfiles` option. +* Removed `inquirer` dependency in favor of `clui` and utility functions. +* Reduced memory usage of the CLI. +* Logging has been rewritten to handle stack overflow errors. +* Updated some text. + +_If you still need to support Node < v6.9, use ModClean 1.3.0 instead. For more information about these changes, refer to the README._ + ## 1.3.0 (1/6/2017) * Added `noDirs` option to exclude directories from being removed (#8) - Added `--no-dirs` option to CLI (#8) diff --git a/LICENSE b/LICENSE index 2398d1c..6f8e1fa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Kyle Ross +Copyright (c) 2017 Kyle Ross Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b9d61c2..e6bd6aa 100644 --- a/README.md +++ b/README.md @@ -1,346 +1,97 @@ # ModClean *Remove unwanted files and directories from your node_modules folder* -[![npm version](https://img.shields.io/npm/v/modclean.svg)](https://www.npmjs.com/package/modclean) [![Build Status](https://img.shields.io/travis/KyleRoss/modclean.svg)](https://travis-ci.org/KyleRoss/modclean) ![NPM Dependencies](https://david-dm.org/KyleRoss/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/KyleRoss/modclean/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/KyleRoss/modclean.svg)](https://github.com/KyleRoss/modclean/issues) +[![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) -In some environments (especially Enterprise), it's required to commit the `node_modules` folder into version control due to compatibility and vetting open source code. One of the major issues with this is the sheer amount of useless files that are littered through the node_modules folder; taking up space, causing long commit/checkout times, increasing latency on the network, causing additional stress on a CI server, etc. If you think about it, do you really need to deploy tests, examples, build files, attribute files, etc? ModClean is a simple utility that provides a full API and CLI utility to reduce the number of useless files. Even if you do not commit your node_modules folder, this utility is still useful when the application is deployed since you do not need these useless files wasting precious disk space on your server. +### 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. -Depending on the number of modules you are using, file reduction can be anywhere from hundreds to thousands. I work for a Fortune 500 company and we use this to reduce the amount of useless files and typically we remove over 500 files (roughly 100MB total) from our ~20 modules we use in our applications. It's a huge improvement in deployment time and commit/checkout time. +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. -This module comes with a JSON file (patterns.json) that outlines file patterns that searched for through the node_modules folder recursively. This list is a basic list of commonly found files/folders within various node_modules that are junk, although it's by no means a complete list. Even though this is the default list, you can provide your own list of patterns to use instead. +## Why? +There are a few different reasons why you would want to use ModClean: + +* **Commiting Modules.** Some environments (especially Enterprise), it's required to commit the `node_modules` directory with your application into version control. This is due to compatibility, vetting and vunerability scanning rules for open source software. This can lead to issues with project size, checking out/pulling changes and the infamous 255 character path limit if you're unlucky enough to be on Windows or SVN. +* **Wasted space on your server.** Why waste space on your server with files not needed by you or the modules? +* **Packaged applications.** If you're required to package your application, you can reduce the size of the package quickly by removing unneeded files. +* **Compiled applications.** Other tools like, [NW.js](https://nwjs.io/) and [Electron](http://electron.atom.io/) make it easy to create cross-platform desktop apps, but depending on the modules, your app can become huge. Reduce down the size of the compiled application before shipping and make it faster for users to download. +* **Save space on your machine.** Depending on the amount of global modules you have installed, you can reduce their space by removing those gremlin files. +* **and much more!** + +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. + +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. + +_(File and disk space reduction can also be different between the version of NPM and Operating System)_ **IMPORTANT** -This module has been heavily tested in an enterprise environment used for large enterprise applications. The provided patterns in this module (see patterns.json) have worked very well when cleaning up useless files in many popular modules. There are hundreds of thousands of modules in NPM and we 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. There could still be many useless files left after the cleanup process since we cannot cover them all. If you find any files that should be removed, please create a pull request using the contributing guidelines at the bottom of this file. +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. -### Removal Benchmark +## Removal Benchmark So how well does this module work? If we `npm install sails` and run ModClean on it, here are the results: -#### Using Safe Patterns -`modclean -n safe` +_All tests ran on macOS 10.12.3 with Node v6.9.1 and NPM v4.0.5_ -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ----------- | -| Before ModClean | 7,461 | 1,915 | 72.5 MB | -| After ModClean | 3,335 | 1,393 | 43.5 MB | -| Reduced | **4,126** | **522** | **29.0 MB** | +#### 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** | #### Using Safe and Caution Patterns -`modclean -n safe,caution` +`modclean -n default:safe,default:caution` -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ----------- | -| Before ModClean | 7,461 | 1,915 | 72.5 MB | -| After ModClean | 3,029 | 1,393 | 38.8 MB | -| Reduced | **4,432** | **522** | **33.7 MB** | +| | 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** | #### Using Safe, Caution and Danger Patterns -`modclean -n safe,caution,danger` +`modclean --patterns="default:*"` -| | Total Files | Total Folders | Total Size | -| --------------- | ----------- | ------------- | ----------- | -| Before ModClean | 7,461 | 1,915 | 72.5 MB | -| After ModClean | 2,957 | 1,393 | 35.9 MB | -| Reduced | **4,504** | **522** | **36.6 MB** | +| | 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** | That makes a huge difference in the amount of files and disk space. -View additional benchmarks in [BENCHMARK.md](https://github.com/KyleRoss/modclean/blob/master/BENCHMARK.md). If you would like to run some of your own benchmarks, you can use [modclean-benchmark](https://github.com/KyleRoss/modclean-benchmark). +View additional benchmarks on the Wiki: [Benchmarks](https://github.com/ModClean/modclean/wiki/Benchmarks). If you would like to run some of your own benchmarks, you can use [modclean-benchmark](https://github.com/ModClean/modclean-benchmark). ## Install Install locally - npm install modclean --save +```bash +npm install modclean --save +``` Install globally (CLI) - npm install modclean -g - - -## CLI Usage -If you want to use this module as a tool, you can use the provided CLI utility. After installing globally, you will now have access to the command `modclean`. There are several options available to customize how it should run. All options listed below are optional. - -### Usage - - modclean [-tsievrhV] [-p path] - -#### -p [path], --path [path] -Provide a different path to run ModClean in. By default, it uses `process.cwd()`. The path **must** either be inside a `node_modules` directory or in a directory that contains a `node_modules` folder. - -#### -n, --patterns [patterns] -Specify which group(s) of patterns to use. Can be `safe`, `caution` or `danger`. Separate multiple groups by a single comma (no spaces). Default is `safe`. -Example: `modclean -n safe,caution` - -#### -t, --test -Run in test mode which will do everything ModClean does except delete the files. It's good practice to run this first to analyze the files that will be deleted. - -#### -s, --case-sensitive -When files are searched, they are searched using case sensitive matching. (ex. `README.md` pattern would not match `readme.md` files) - -#### -i, --interactive -Run in interactive mode. For each file found, you will be prompted whether you want to delete or skip. - -#### -P, --no-progress -Turns off the progress bar when files are being deleted. - -#### -I, --ignore -Comma-separated list of glob patterns to ignore during cleaning. - -#### --no-dirs -Do not delete directories, only files. - -#### -d, --empty -Delete all empty directories after the cleanup process. Does not prompt for deletion when in `--interactive` mode. - -#### -e, --error-halt -Whether to halt the process when an error is encountered. The process is only halted when there is an issue deleting a file due to permissions or some other catastrophic issue. +```bash +npm install modclean -g +``` -#### -v, --verbose -Runs in verbose mode. This will display much more information during the process. - -#### -r, --run -Run the utility immediately without displaying the warning and having to confirm. - -#### -h, --help -Show help/usage screen. - -#### -V, --version -Display the version of ModClean that is installed. --- -## API Documentation -You can also use ModClean programmically so you can include it into your own utilities and customize how it works. Just install ModClean locally to your project. - -### Examples - - // Require modclean module - var modclean = require('modclean'); - -Run the basic ModClean process with a callback function when completed. - - modclean(function(err, results) { - if(err) return console.error(err); - - console.log('Deleted Files Total:', results.length); - }); - -Run the basic ModClean process with conditional file skipping. - - modclean({ - process: function(file, files) { - // Skip .gitignore files - if(file.match(/\.gitignore/i)) { - return false; - } - - return true; - } - }).clean(function(err, results) { - if(err) return console.error(err); - - console.log('Deleted Files Total:', results.length); - }); - -More advanced usage. - - var path = require('path'); - - var MC = new modclean.ModClean({ - // Define a custom path - cwd: path.join(process.cwd(), 'myApp/node/node_modules'), - // Only delete patterns.safe patterns along with html and png files - patterns: [modclean.patterns.safe, '*.html', '*.png'], - // Run in test mode so no files are deleted - test: true - }); - - MC.on('deleted', function(file) { - // For every file deleted, log it - console.log((MC.options.test? 'TEST' : ''), file, 'deleted from filesystem'); - }); - - // Run the cleanup process without using the 'clean' function - MC._find(null, function(err, files) { - if(err) return console.error('Error while searching for files', err); - - MC._process(files, function(err, results) { - if(err) return console.error('Error while processing files', err); - - console.log('Deleted Files Total:', results.length); - }); - }); - -### Options -The options below can be used to modify how ModClean works. - -#### cwd -*(String)* **Default:** `process.cwd()` -The path in which ModClean should recursively search through to find files to remove. If the path does not end with `options.modulesDir`, it will be appended to the path, allowing this script to run in the parent directory. - -#### patterns -*(Array)* **Default** `modclean.patterns.safe` (see patterns.json file) -Patterns to use as part of the search. These patterns are concatenated into a regex string and passed into `glob`. Anything allowed in `glob` can be used in the patterns. This option can also be an array of arrays in which will be flattened. - -#### ignoreCase -*(Boolean)* **Default** `true` -Whether `glob` should ignore the case of the file names when searching. If you need to do strict searching, set this to `false`. - -#### process -*(Function)* **Default:** `null` -Optional function to call before each file is deleted. This function can be used asynchronously or synchronously depending on the number of parameters provided. If the provided function has 1 or 2 parameters `function(file, files)`, it is synchronous, if it has 3 parameters `function(file, files, cb)`, it is asynchronous. When sync, you can `return false` to skip the current file being processed, otherwise when async, you can call the callback function `cb(false)` to skip the file. The **file** parameter is the current path with the filename appened of the file being processed. The **files** parameter is the full array of all the files. - -#### modulesDir -*(String|Boolean)* **Default:** `"node_modules"` -The modules directory name to use when looking for modules. This is only used when setting the correct `options.cwd` path. If you do not want the modules directory to be appended to `options.cwd`, set this option to `false`. If `options.cwd` already ends with the value of this option, it will not be appended to the path. - -#### removeEmptyDirs -*(Boolean)* **Default:** `true` -Whether to remove empty directories after the cleanup process. This is usually a safe option to use. - -#### ignore -*([String])* **Default:** `null` -Array of glob patterns (strings) to ignore while running the deletion process. - -#### noDirs -*(Boolean) **Default:** `false` -Set to `true` to skip directories from being deleted during the cleaning process. - -#### errorHalt -*(Boolean)* **Default:** `false` -Whether the script should exit with a filesystem error if one is encountered. This really only pertains to systems with complex permissions or Windows filesystems. The `rimraf` module will only throw an error if there is actually an issue deleting an existing file. If the file doesn't exist, it does not throw an error. - -#### test -*(Boolean)* **Default:** `false` -Whether to run in test mode. If set to `true` everything will run (including all events), although the files will not actually be deleted from the filesystem. This is useful when you need to analyze the files to be deleted before actually deleting them. - - -### Methods and Properties -These are the methods and properties returned when calling `var modclean = require('modclean');`. - -#### modclean([options][,cb]) -Create a new `ModClean` instance. It's the same as calling `new modclean.ModClean()`. If a callback function is provided, it will automatically call the `clean()` method and therefore `clean()` should not be called manually. If you need to set event listeners, set the callback function in the `clean()` method instead. - -**options** *(Object)* - Options to configure how ModClean works. (Optional) -**cb** *(Function)* - Callback function to call once the process is completed `function(err, results)`. The `results` parameter contains an array of all the files that were successfully remove from the filesystem. - -#### modclean.defaults -*(Object)* - The default options used in all created ModClean instances. You may change the defaults at anytime if you will be creating multiple instances that need to use the same options. - -#### modclean.patterns -*(Object)* - The full list of patterns provided in `patterns.json`. This returns 3 properties (`safe`, `caution`, `danger`) which determines the level of file removal. +### [Read the CLI Documentation](https://github.com/ModClean/modclean/wiki/CLI) -#### modclean.ModClean([options][,cb]) -Create instance of the `ModClean` class. The parameters are the same as `modclean()`. The only difference between this and `modclean()` is that this must be called with `new`. +### [Read the API Documentation](https://github.com/ModClean/modclean/wiki/API) - var modclean = require('modclean'); - - // Create new instance - var MC = new modclean.ModClean(); - -#### modclean.ModClean().clean([cb]) -Runs the ModClean process. Only needs to be called if a callback function is not provided to `modclean.ModClean()`. - -**cb** *(Function)* - Callback function to call once the process is completed `function(err, results)`. The `results` parameter contains an array of all the files that were successfully remove from the filesystem. - -#### modclean.ModClean()._find(patterns, cb) -Internally used by ModClean to search for files based on the provided patterns. - -**patterns** *(Array|null)* - Patterns to use for the search process. If set to `null`, it will default to `options.patterns`. -**cb** *(Function)* - Callback function to call once the search process is completed with an array of file paths `function(err, files)`. - -#### modclean.ModClean()._process(files, cb) -Internally used by ModClean to process each of the files. The processing includes running `options.process` and then calling `ModClean()._deleteFile()`. - -**files** *(Array)* - Array of file paths to process and send for deletion. -**cb** *(Function)* - Callback function to call once processing and deletion is complete `function(err, results)`. The results parameter contains an array of files that were successfully deleted (does not include skipped files). - -#### modclean.ModClean()._deleteFile(file, cb) -Internally used by ModClean to delete a file at the given path. - -**file** *(String)* - File path to be deleted. Should not include `options.cwd` as it will be prepended. -**cb** *(Function)* - Callback function to be called once the file is deleted `function(err, file)`. The callback will not receive an error if `options.errorHalt = false`. - -#### modclean.ModClean()._removeEmpty(cb) -Internally used by ModClean to delete all empty directories within `options.cwd`. - -**cb** *(Function)* - Callback function to be called once all empty directories have been deleted `function(err, results)`. - -#### modclean.ModClean().options -Compiled options object used by the ModClean instance. - -#### modclean.ModClean().on(event, fn) -Creates an event handler on the ModClean instance. - -**event** *(String)* - Any of the event names the are listed in the events section below. -**fn** *(Function)* - Function to call when the specified event is emitted. - - -### Events -The following events are emitted from the `ModClean` instance. - -#### start -Emitted at the beginning of `clean()`. - -**inst** *(Object)* - Provides access to the current ModClean instance. - -#### files -Emitted once a list of all found files has been compiled from the `_find()` method. - -**files** *(Array)* - Array of file paths found. - -#### deleted -Emitted each time a file has been deleted from the file system by the `_deleteFile()` method. - -**file** *(String)* - The file path that has been deleted. - -#### finish -Emitted once processing and deletion of files has completed by the `_process()` method. - -**results** *(Array)* - List of file paths that were successfully deleted from the file system (not including skipped files). - -#### complete -Emitted once the entire ModClean process has completed before calling the main callback function. - -**err** *(Object|String|null)* - Error (if any) that was thrown during the process. -**results** *(Array)* - List of file paths that were successfully deleted from the file system (not including skipped files). - -#### fileError -Emitted if there was an error thrown while deleting a file/folder. Will emit even if `options.errorHalt = false`. - -**err** *(Object|String)* - Error thrown by `rimraf`. -**file** *(String)* - File path of the file/folder that caused the error. - -#### error -Emitted if there was an error thrown somewhere in the module. - -**err** *(Object|String)* - The error that was thrown. - ---- - -## Tests -This module has a good number of tests written for it that should cover most (if not all cases). If you would like to run the tests, please install `mocha` globally and install the `devDependencies` for this module. You can run the tests by calling `npm test`. The tests are ran using this modules own `node_modules` folder. +### [Read the Custom Patterns Plugin Documentation](https://github.com/ModClean/modclean/wiki/Custom-Pattern-Plugins) --- ## Issues If you find any bugs with either ModClean or the CLI Utility, please feel free to open an issue. Any feature requests may also be poseted in the issues. ---- - -## Contributing -If you would like to contribute to this project, please ensure you follow the guidelines below: - -### Code Style -I'm not very picky on the code style as long as it roughly follows what is currently written in the modules. Just ensure that lines that should end with semi-colons do end with them. Comments are also very helpful for future contributors to know what's going on. - -### Run Tests -If you are making a code change, please run the tests and ensure they pass before submitting a pull request. If you are adding new functionality, please ensure to write the tests for it. - -### Patterns.json Changes -In case there are file patterns that were missed and this module could clean up additional files, feel free to submit a pull request adding the pattern. I will not accept pull reuqests that use wildcards on `.js` or `.json` files. If you notice a pattern that is causing issues with a particular module, submit a pull request or issue. There are 3 sections to the `patterns.json` file: (`safe`, `caution` and `danger`). Each of these sections determine the level of files to remove which includes additional patterns that match file/folder names. Safe patterns contain absolutely useless files that can be safely removed whereas caution and danger patterns are ones in which could cause issues with certain modules but will help significantly clean up more files. - ---- - ## License ModClean is licensed under the MIT license. Please see LICENSE in the repository for the full text. diff --git a/bin/modclean.js b/bin/modclean.js old mode 100644 new mode 100755 index cac387e..2bb1242 --- a/bin/modclean.js +++ b/bin/modclean.js @@ -1,302 +1,285 @@ #!/usr/bin/env node -require('colors'); +"use strict"; -var program = require('commander'), - inquirer = require('inquirer'), - pkg = require('../package.json'), - notifier = require('update-notifier')({ pkg: pkg }), - path = require('path'), - os = require('os'), - modclean = require('../index.js'), - ModClean = modclean.ModClean, - pkg = require('../package.json'); +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'); -if(notifier.update) notifier.notify(); +const utils = require('./utils'); +const modclean = require('../lib/modclean'); +const ModClean = modclean.ModClean; + +notifier({ pkg }).notify(); function list(val) { return val.split(','); } +process.on('SIGINT', function() { + process.stdin.destroy(); + process.exit(); +}); + program .version(pkg.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') .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') + .option('-f, --follow-symlink', 'Clean symlinked packages as well') .option('-r, --run', 'Run immediately without warning') - .option('-n, --patterns [patterns]', 'Patterns type(s) to remove (safe, caution, or danger)') + .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('-d, --empty', 'Remove empty directories') + .option('--no-dotfiles', 'Exclude dot files from being removed') + .option('-k, --keep-empty', 'Keep empty directories') .parse(process.argv); -var ui = new inquirer.ui.BottomBar(); +class ModClean_CLI { + constructor() { + this.log = utils.initLog(program.verbose); -newLine(); -ui.log.write('MODCLEAN'.yellow.bold + (' v' + pkg.version).grey); + // Display CLI header + console.log( + "\n" + + chalk.yellow.bold('MODCLEAN ') + + chalk.gray(' Version ' + pkg.version) + "\n" + ); -if(program.test) { - newLine(); - ui.log.write('RUNNING IN TEST MODE'.red.bold); - ui.log.write('When running in test mode, files will not be deleted from the file system.'. grey); -} + // Display "running in test mode" message + if(program.test) { + console.log( + chalk.cyan.bold('RUNNING IN TEST MODE') + "\n" + + chalk.gray('When running in test mode, files will not be deleted from the file system.') + "\n" + ); + } -if(!program.run && !program.test) { - ui.log.write('\n' + (' WARNING:'.red.bold)); - ui.log.write([ - ' This module deletes files from the filesystem that cannot be retreived. Although what', - ' is being deleted are not required by the modules and/or your application, there is no', - ' guarantee that the modules will work after this script runs. The patterns.json file', - ' included with this module shows a listing of files and folders that will be searched', - ' for and removed from the file system. If you have any concerns with deleting files,', - ' you should run this utility using the "--test" flag first or with the "-i" flag to', - ' make the deletion process interactive. The author or contributors shall not be held', - ' responsible for and damages this module may cause. Please see the README.\n\n' - ].join(' \n')); - - confirm('Are you sure you want to continue?', function(result) { - if(!result) return process.exit(0); - new ModCleanCLI(program); - }); -} else { - newLine(); - process.nextTick(function() { - new ModCleanCLI(program); - }); -} + // Display warning message and confirmation prompt + if(!program.run && !program.test) { + console.log( + chalk.red.bold('WARNING:') + "\n" + + chalk.gray(utils.warningMsg) + "\n" + ); -function ModCleanCLI(opts) { - if(!(this instanceof ModCleanCLI)) return new ModCleanCLI(opts); - - var self = this, - _patterns = []; - - this.current = 0; - this.skipped = []; - this.total = 0; - this.argv = opts; - this.os = os.platform(); - - if(opts.patterns) { - var patts = opts.patterns.split(','); - - for(var i = 0; i < patts.length; i++) { - if(modclean.patterns.hasOwnProperty(patts[i].toLowerCase())) - _patterns.push(modclean.patterns[patts[i].toLowerCase()]); + return utils.confirm('Are you sure you want to continue?', (res) => { + if(!res) return process.exit(0); + this.start(); + }); } + + this.start(); } - - this.options = { - cwd: opts.path || process.cwd(), - patterns: _patterns.length? _patterns : modclean.patterns.safe, - noDirs: !!opts.dirs, - errorHalt: !!opts.errorHalt, - removeEmptyDirs: !!opts.empty, - test: !!opts.test, - process: function(file, files, cb) { - self.current += 1; - if(!opts.interactive) return cb(true); - - var fn = path.relative(self.options.cwd, file); - if(self.os === 'win32') fn = fn.replace(/\\+/g, '/'); - - confirm(('(%/%) '.fmt(self.current, files.length)).grey + fn + ' - ' + 'Delete file?'.yellow.bold, function(res) { - if(!res) self.skipped.push(file); - cb(res); - }); + + start() { + let self = this, + platform = os.platform(); + + this.stats = { + current: 0, + total: 0, + skipped: [], + currentEmpty: 0, + totalEmpty: 0 + }; + + // Disable progress bar in interactive mode + if(program.interactive) program.progress = false; + + let options = { + cwd: program.path || process.cwd(), + modulesDir: program.modulesDir || 'node_modules', + patterns: program.patterns && program.patterns.length? program.patterns : ['default:safe'], + additionalPatterns: program.additionalPatterns || [], + ignorePatterns: program.ignore || [], + noDirs: !program.dirs, + dotFiles: !!program.dotfiles, + errorHalt: !!program.errorHalt, + removeEmptyDirs: !program.keepEmpty, + 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); + }); + } + }; + + this.modclean = new ModClean(options); + + this.initEvents(); + this.modclean.clean(this.done.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); + } } - }; - - if(opts.ignore && opts.ignore.length) this.options.ignore = opts.ignore; - - if(program.caseSensitive) this.options.ignoreCase = false; - - this.inst = new ModClean(this.options); - this.initEvents(); - this.inst.clean(this.done.bind(this)); -} -ModCleanCLI.prototype = { - done: function(err, results) { - var pb = ui.bottomBar; - // DISPLAY RESULTS - newLine(); - ui.updateBottomBar(''); - console.log(pb); - newLine(); - ui.log.write('FILES/FOLDERS DELETED'.green.bold); - ui.log.write(' Total: '.yellow.bold + results.length); - ui.log.write(' Skipped: '.yellow.bold + this.skipped.length); - newLine(); - - log('verbose', '\n' + results.join('\n')); - - if(err) { - newLine(); - log('error', err); - return process.exit(1); + function showSpinner(msg) { + if(!process.stdout.isTTY || program.verbose || !program.progress) { + console.log(msg); + } else { + spinner.message(msg); + spinner.start(); + } } - - setTimeout(function() { - process.exit(0); - }, 500); - }, - - initEvents: function() { - var self = this, - inst = self.inst; - - this.progressBar = new Progress(40); - + // Start Event (searching for files) - inst.on('start', function() { - log('event', 'start'); - log('verbose', '\n' + JSON.stringify(inst.options, null, 4)); - self.spin('Searching for files in '+ self.options.cwd); + inst.on('start', () => { + this.log('event', 'start'); + this.log('verbose', '\n' + JSON.stringify(inst.options, null, 4)); + + showSpinner(`Searching for files in ${inst.options.cwd}...`); }); - + // Files Event (file list after searching complete) - inst.on('files', function(files) { - log('event', 'files'); - self.spinStop(); - - self.total = files.length; - log(null, 'Found', (files.length.toString()).green.bold, 'files/folders to remove'); - newLine(); + inst.on('files', (files) => { + this.log('event', 'files'); + if(process.stdout.isTTY) spinner.stop(); + + this.stats.total = files.length; + + console.log(`Found ${chalk.green.bold(files.length)} files/folders to remove\n`); }); - + + inst.on('process', (files) => { + this.log('event', 'process'); + + if(!showProgress && !program.interactive && !program.verbose) + console.log('Deleting files, please wait...'); + + updateProgress(0, files.length); + }); + // Deleted Event (called for each file deleted) - inst.on('deleted', function(file) { - log('event', 'deleted'); - - if(!self.argv.interactive && self.argv.progress) ui.updateBottomBar(self.getProgress()); - - log('verbose', 'DELETED'.yellow.bold, '(%/%) %'.fmt(self.current, self.total, file.grey)); + inst.on('deleted', (file) => { + updateProgress(this.stats.current, this.stats.total); + + this.log( + 'verbose', + `${chalk.yellow.bold('DELETED')} (${this.stats.current}/${this.stats.total}) ${chalk.gray(file)}` + ); + }); + + inst.on('beforeEmptyDirs', () => { + this.log('event', 'beforeEmptyDirs'); + + showSpinner(`Searching for empty directories in ${inst.options.cwd}...`); + }); + + inst.on('emptyDirs', (dirs) => { + this.log('event', 'emptyDirs'); + + if(process.stdout.isTTY) spinner.stop(); + + 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); + }); + + inst.on('deletedEmptyDir', (dir) => { + this.stats.currentEmpty += 1; + updateProgress(this.stats.currentEmpty, this.stats.totalEmpty); + + this.log( + 'verbose', + `${chalk.yellow.bold('DELETED EMPTY DIR')} (${this.stats.currentEmpty}/${this.stats.totalEmpty}) ${chalk.gray(dir)}` + ); }); - + + inst.on('afterEmptyDirs', () => { + if(showProgress) process.stdout.write('\n'); + this.log('event', 'afterEmptyDirs'); + }); + // Error Event (called as soon as an error is encountered) - inst.on('error', function(err) { - log('event', 'error'); - if(self.argv.verbose) log('error', err); + 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', function(err, file) { - log('event', 'fileError'); - if(self.argv.verbose) log('error', 'FILE ERROR:'.red, err, file.grey); + inst.on('fileError', (err) => { + this.log('event', 'fileError'); + this.log('error', `${chalk.red.bold('FILE ERROR:')} ${err.error}\n${chalk.gray(err.file)}`); }); - + // Finish Event (once processing/deleting all files is complete) - inst.on('finish', function(results) { - log('event', 'finish'); - log('verbose', 'FINISH'.green, 'Deleted '+ (results.length.toString()).yellow.bold, 'files/folders of '+ (self.total.toString()).yellow.bold); + inst.on('finish', (results) => { + if(showProgress) process.stdout.write('\n'); + this.log('event', 'finish'); + + this.log( + 'verbose', + `${chalk.green('FINISH')} Deleted ${chalk.yellow.bold(results.length)} files/folders of ${chalk.yellow.bold(this.stats.total)}` + ); }); - + // Complete Event (once everything has completed) - inst.on('complete', function() { - log('event', 'complete'); + inst.on('complete', () => { + this.log('event', 'complete'); }); - }, - - getProgress: function() { - if(!this.argv.progress) return ''; - return 'Cleanup Progress '.grey + this.progressBar.update(this.current, this.total) + ' (%/%)'.fmt(this.current, this.total); - }, - - spin: function(str) { - str = str.grey; - - var loader = [ - '/ '.cyan.bold + str, - '| '.cyan.bold + str, - '\\ '.cyan.bold + str, - '- '.cyan.bold + str - ]; - - var i = 4; - this._spin = setInterval(function() { - ui.updateBottomBar(loader[i++ % 4]); - }, 300); - }, - - spinStop: function() { - clearInterval(this._spin); - ui.updateBottomBar(''); - } -}; - - -function log(type) { - var prefix; - if((type === 'verbose' || type === 'event') && !program.verbose) return; - - switch(type) { - case 'event': - prefix = 'EVENT'.magenta.bold; break; - - case 'error': - prefix = 'ERROR'.red.bold; break; - - case 'verbose': - prefix = 'VERBOSE'.green.bold; break; - - default: prefix = ''; } - - var args = Array.prototype.slice.call(arguments, 1); - args.unshift(prefix + '>'.grey); - - ui.log.write(args.join(' ')); -} -function newLine() { - ui.log.write('\n'); -} + done(err, 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` + ); -function confirm(str, fn) { - ui.updateBottomBar(str + (' [y|N] ').grey); - process.stdin.setEncoding('utf8'); - process.stdin.once('data', function(val) { - fn(/^y|yes|ok|true$/i.test(val.trim())); - }).resume(); -} + setTimeout(() => { + process.stdin.destroy(); + + if(err) { + this.log('error', err); + return process.exit(1); + } -// Modified from clui (https://github.com/nathanpeck/clui) -function Progress(width) { - var currentValue = 0, - maxValue = 0; - - this.update = function (currentValue, maxValue) { - if(maxValue === 0) return '[]'; - - var barLength = Math.ceil(currentValue / maxValue * width); - if(barLength > width) barLength = width; - - return '[' + - (Array(barLength).join("|")).green + - Array(width + 1 - barLength).join("-") + - '] '+ (Math.ceil(currentValue / maxValue * 100) + '%').grey; - }; + process.exit(0); + }, 500); + } } -// String Interpolation since I don't care for the built in console one -// Adding this since messing with the String prototype isn't a big deal in CLI apps -String.prototype.fmt = function stringProtoFmtFn() { - var args = (arguments.length === 1 && Array.isArray(arguments[0])) ? arguments[0] : arguments, - index = 0; - return this.replace(/(%)/g, function stringProtoFmtReplaceCB(m) { - var val = args[index]; - - if(Array.isArray(val)) val = val.join(', '); - if(typeof val === 'object') val = JSON.stringify(val); - if(typeof val === 'function') val = val.toString(); - - index++; - return val || m; - }); -}; +new ModClean_CLI(); diff --git a/bin/utils.js b/bin/utils.js new file mode 100644 index 0000000..1c0f787 --- /dev/null +++ b/bin/utils.js @@ -0,0 +1,53 @@ +/* + Misc. utils for the CLI program + */ +"use strict"; +const readline = require('readline'); +const chalk = require('chalk'); + +exports.warningMsg = +` This module deletes files from the filesystem and cannot be recovered. + Depending on the patterns plugins, the files being deleted typically are not + required by modules, although there is no guarantee that the modules will + 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`; + +exports.confirm = function(msg, cb) { + process.stdin.setEncoding('utf8'); + + let rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.question(msg + chalk.gray(' [y/N] '), function(answer) { + rl.close(); + cb(/^y|yes|ok|true$/i.test(answer.trim())); + }); +}; + + +exports.initLog = function(verbose) { + let types = { + error: chalk.red.bold('ERROR'), + info: '' + }; + + if(verbose) { + types.event = chalk.magenta.bold('EVENT'); + types.verbose = chalk.green.bold('VERBOSE'); + } + + return function log(type, ...args) { + if(!type) type = 'info'; + if(!types.hasOwnProperty(type)) return; + let method = type === 'error'? 'error' : 'log'; + + args.unshift(types[type] + chalk.gray('>')); + console[method](args.join(' ')); + }; +}; diff --git a/index.js b/index.js index 7915ef6..a62afd1 100644 --- a/index.js +++ b/index.js @@ -1,366 +1,3 @@ -/** - * modclean - * Remove unwanted files and directories from your node_modules folder - * @version 1.2.1 (4/29/2015) - * @author Kyle Ross - */ +"use strict"; -var glob = require('glob'), - rimraf = require('rimraf'), - util = require("util"), - fs = require('fs'), - EventEmitter = require("events").EventEmitter, - path = require('path'), - dir = require('node-dir'), - patterns = require('./patterns.json'); - -var defaults = { - /** - * The directory to search in, default is `process.cwd()` - * @type {String} - */ - cwd: process.cwd(), - /** - * Patterns to search for, default is an array loaded from `patterns.json` - * @type {Array} - */ - patterns: patterns.safe, - /** - * Exclude directories from being removed - * @type {Boolean} - */ - noDirs: false, - /** - * Ignore the provided glob patterns - * @type {Array|null} - */ - ignore: null, - /** - * Ignore the case of the file names when searching by patterns (default `true`) - * @type {Boolean} - */ - ignoreCase: 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', - /** - * Remove empty directories as part of the cleanup process (default `true`) - * @type {Boolean} - */ - removeEmptyDirs: true, - /** - * Whether file deletion errors should halt the module from running and return the error to the callback (default `false`) - * @type {Boolean} - */ - errorHalt: false, - /** - * Use test mode which will get the list of files and run the process without actually deleting the files (default `false`) - * @type {Boolean} - */ - test: false -}; - -// export modclean -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 - */ -function modclean(options, cb) { - return new ModClean(options, cb).clean(); -} - -/** - * The default options for ModClean that can be overridden. - * @property {Object} options Default options for ModClean. - */ -modclean.defaults = defaults; - -/** - * The full list of patterns from patterns.json - * @property {Object} patterns `safe`, `caution` and `danger` patterns from patterns.json - */ -modclean.patterns = patterns; - -// Export ModClean class -modclean.ModClean = ModClean; - -/** - * Main ModClean class - * @class ModClean - * @param {Object} options ModClean options (optional) - * @param {Function} cb Callback function to call if once completed or if error - `function(error, files)` - */ -function ModClean(options, cb) { - if(!(this instanceof ModClean)) return new ModClean(options); - EventEmitter.call(this); - - if(typeof options === 'function') cb = options; - if(!options || typeof options !== 'object') options = {}; - - this.options = options = extend(Object.create(modclean.defaults), options || {}); - if(typeof this.options.patterns === 'string') this.options.patterns = [this.options.patterns]; - - if(Array.isArray(this.options.patterns)) { - var _patterns = []; - for(var i = 0; i < this.options.patterns.length; i++) { - if(Array.isArray(this.options.patterns[i])) { - _patterns = _patterns.concat(this.options.patterns[i]); - } else { - _patterns.push(this.options.patterns[i]); - } - } - - this.options.patterns = _patterns; - } - - 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); -} - -// Inherit EventEmitter.prototype -util.inherits(ModClean, EventEmitter); - -/** - * Start the clean process - * @public - * @param {Function} cb Callback function to call if once completed or if error - `function(error, files)` - */ -ModClean.prototype.clean = function(cb) { - var self = this; - - this.finalCallback = function(err, results) { - /** - * @event complete - * @property {Mixed} err An error string/object if error was thrown - * @property {Array} results List of files successfully deleted - */ - this.emit('complete', err, results); - if(typeof cb === 'function') cb(err, results); - }; - - /** - * @event start - * @property {Object} this ModClean instance - */ - self.emit('start', this); - - self._find(null, function(err, files) { - if(err) return self.finalCallback(err); - - self._process(files, function(err, results) { - if(err) return self.finalCallback(err); - - if(self.options.removeEmptyDirs) { - self._removeEmpty(function(e, res) { - self.finalCallback(e, results.concat(res)); - }); - } else self.finalCallback(err, results); - }); - }); -}; - -/** - * Find patterns using glob and return file list - * @protected - * @param {Array} patterns Array of patterns to find (defaults to `options.patterns`) - * @param {Function} cb Callback to call with `error` and `files` when complete - */ -ModClean.prototype._find = function(patterns, cb) { - var self = this, - opts = self.options, - globOpts = { - cwd: opts.cwd, - dot: true, - nocase: opts.ignoreCase, - ignore: opts.ignore, - nodir: opts.noDirs - }; - - glob('**/@('+ (patterns || opts.patterns).join('|') +')', globOpts, function(err, files) { - if(err) self.emit('error', err); - /** - * @event files - * @property {Array} files List of files found based on `options.patterns` in `options.cwd` - */ - else self.emit('files', files); - cb(err, files); - }); -}; - -/** - * Processes a list of files - * @protected - * @param {Array} files List of files to process - * @param {Function} cb Callback function to call once completed or if error - */ -ModClean.prototype._process = function(files, cb) { - var self = this, - opts = this.options, - processFn = typeof opts.process === 'function'? opts.process : function() { return true; }; - - if(!files.length) return cb(null, []); - - eachSeries(files, function(file, callback) { - if(processFn.length === 3) { - // If processFn has 3 arguments (file, files, cb), then assume async - processFn(file, files, function(result) { - if(result !== false) self._deleteFile(file, callback); - else callback(); - }); - } else { - // If processFn has any other number of arguments (default: file, files), then assume sync - if(processFn(file, files) !== false) self._deleteFile(file, callback); - else callback(); - } - }, function(err, results, errFile) { - /** - * @event finish - * @property {Array} results List of files successfully deleted - */ - self.emit('finish', results); - cb(err, results); - }); -}; - -/** - * Deletes file/folder at given path - * @protected - * @param {String} file The file/folder path to delete - * @param {Function} cb Callback function to call when complete or error - */ -ModClean.prototype._deleteFile = function(file, cb) { - var self = this, - opts = self.options; - // If test mode is enabled, just return the file. - if(opts.test) { - self.emit('deleted', file); - return cb(null, file); - } - - rimraf(path.join(opts.cwd, file), function(err) { - if(err) { - /** - * @event fileError - * @property {Mixed} err The error object/string - * @property {String} file The file that caused the error - */ - self.emit('fileError', err, file); - return opts.errorHalt? cb(err, file) : cb(); - } - - /** - * @event deleted - * @property {String} file The deleted file name - */ - self.emit('deleted', file); - cb(null, file); - }); -}; - -/** - * Remove empty directories from `options.cwd`. - * @protected - * @param {Function} cb Callback function to call when completed `function(err)`. - */ -ModClean.prototype._removeEmpty = function(cb) { - var self = this, - opts = self.options; - // If test mode is enabled or removeEmptyDirs is disabled, just return. - if(opts.test || !opts.removeEmptyDirs) return cb(null); - - dir.subdirs(opts.cwd, function(err, dirs) { - if(err) return cb(err); - if(!Array.isArray(dirs)) return cb(); - - eachSeries(dirs, function(dp, icb) { - isEmptyDir(dp, function(err, empty) { - if(err || !empty) return icb(); - - rimraf(dp, function(err) { - if(err) return icb(); - icb(null, dp); - }); - }); - }, function(err, results) { - cb(err, results); - }); - }); -}; - - -/** - * Determines if provided path is empty. - * @private - * @param {String} path The path to check if is empty - * @param {Function} cb Callback function to call once completed - */ -function isEmptyDir(path, cb) { - fs.readdir(path, function (err, files) { - if(err === null) return cb(null, !!!files.length); - cb(err); - }); -} - -/** - * Extends object with properties from another object - * @private - * @param {Object} obj The destination object - * @param {Object} ... The object in which to copy the properties from - * @return {Object} Destination object with properties copied - */ -function extend(obj) { - var arr = []; - - arr.forEach.call(arr.slice.call(arguments, 1), function(source) { - if(source) { - for(var prop in source) { - obj[prop] = source[prop]; - } - } - }); - return obj; -} - -/** - * Iterate through array asynchronously one item at a given time. - * Modified from `node-async` - https://github.com/caolan/async/blob/master/lib/async.js - * @private - * @param {Array} arr The array to iterate through - * @param {Function} iterator The function to run for each iteration - * @param {Function} callback Final callback function to call once all iterations have completed or if error - */ -function eachSeries(arr, iterator, callback) { - var results = []; - callback = callback || function() {}; - if(!arr.length) return callback(); - var completed = 0; - var iterate = function() { - iterator(arr[completed], function(err, result) { - if(err) { - callback(err, results, result); - callback = function() {}; - } else { - if(result) results.push(result); - completed += 1; - if (completed >= arr.length) callback(null, results); - else iterate(); - } - }); - }; - iterate(); -} +module.exports = require('./lib/modclean'); diff --git a/lib/modclean.js b/lib/modclean.js new file mode 100644 index 0000000..0b70aef --- /dev/null +++ b/lib/modclean.js @@ -0,0 +1,365 @@ +/*! + * modclean + * Remove unwanted files and directories from your node_modules folder + * @author Kyle Ross + */ +"use strict"; +const glob = require('glob'); +const rimraf = require('rimraf'); +const path = require('path'); +const subdirs = require('subdirs'); +const emptyDir = require('empty-dir'); +const each = require('async-each-series'); +const fs = require('fs'); + +const Utils = require('./utils.js'); +const EventEmitter = require('events').EventEmitter; + +let defaults = { + /** + * The directory to search in, default is `process.cwd()` + * @type {String} + */ + cwd: process.cwd(), + /** + * Array of patterns plugins to use. Default is `['default:safe']`. + * @type {Array} + */ + patterns: ['default:safe'], + /** + * Array of additional patterns to use. + * @type {Array} + */ + additionalPatterns: [], + /** + * Ignore the provided glob patterns + * @type {Array|null} + */ + ignorePatterns: null, + /** + * Exclude directories from being removed + * @type {Boolean} + */ + noDirs: false, + /** + * Ignore the case of the file names when searching by patterns (default `true`) + * @type {Boolean} + */ + ignoreCase: true, + /** + * Include dot files in the search (default `true`) + * @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', + /** + * Remove empty directories as part of the cleanup process (default `true`) + * @type {Boolean} + */ + removeEmptyDirs: true, + /** + * Filter function used when checking if a directory is empty + * @param {String} file File name to filter against + * @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; + }, + /** + * Whether file deletion errors should halt the module from running and return the error to the callback (default `false`) + * @type {Boolean} + */ + errorHalt: false, + /** + * Use test mode which will get the list of files and run the process without actually deleting the files (default `false`) + * @type {Boolean} + */ + test: false, + /** + * Force deletion to be done also in symlinked packages (when using npm link) (default `false`) + * @type {Boolean} + */ + followSymlink: false +}; + +/** + * @class ModClean + * @extends {EventEmitter} + */ +class ModClean extends EventEmitter { + /** + * 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. + */ + constructor(options, cb) { + 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); + + 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); + } + + /** + * 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 + */ + 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; + } + + /** + * Finds files/folders based on the ModClean options. + * @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); + + glob(`**/@(${this._patterns.allow.join('|')})`, globOpts, (err, files) => { + if(err) this.utils.error(err, '_find'); + else this.emit('files', files); + cb(err, files); + }); + } + + /** + * Processes the found files and deletes the ones that pass `options.process`. + * @private + * @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 = []; + + 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); + }); + } + + /** + * Deletes a single file/folder from the filesystem. + * @private + * @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); + } + + // 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(); + }); + } + + /** + * 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); + }); + }); + } + + /** + * Finds all empty directories within `options.cwd`. + * @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); + }); + }); + } + + /** + * Removes all empty directories provided in `dirs`. + * @private + * @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); + }); + } +} + +// export modclean +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 + */ +function modclean(options, cb) { + return new ModClean(options, cb); +} + +/** + * The default options for ModClean that can be overridden. + * @property {Object} options Default options for ModClean. + */ +modclean.defaults = defaults; + +// Export ModClean class +modclean.ModClean = ModClean; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..e72360b --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,131 @@ +"use strict"; +const uniq = require('lodash.uniq'); +const path = require('path'); + + +class ModClean_Utils { + constructor(inst) { + this._inst = inst; + } + + /** + * Initializes patterns based on the configuration + * @param {Object} opts Options object for ModClean + * @return {Object} The compiled and loaded patterns + */ + initPatterns(opts) { + let patDefs = opts.patterns, + 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; + + 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 = opts.additionalPatterns, + addlIgnore = opts.ignorePatterns; + + if(Array.isArray(addlPats) && addlPats.length) patterns = patterns.concat(addlPats); + if(Array.isArray(addlIgnore) && addlIgnore.length) ignore = ignore.concat(addlIgnore); + + patterns = uniq(patterns); + ignore = uniq(ignore); + + if(!patterns.length) throw new Error('No patterns have been loaded, nothing to check against'); + + return { + allow: patterns, + ignore + }; + } + + /** + * Parses pattern configuration item and attempts to load it + * @param {String} module Module name or path to load + * @param {String} def Raw definition provided in the configuration + * @return {Object} Object containing the found module name and the loaded patterns + */ + _loadPatterns(module, def) { + let patterns; + + if(module.indexOf('/') !== -1) { + let ext = path.extname(module); + if(!path.isAbsolute(module)) module = path.resolve(process.cwd(), module); + + 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; + + try { + patterns = require(module); + } catch(e) { + throw new Error(`Unable to find patterns plugin "${module}", is it installed?`); + } + } + + if(patterns === null || typeof patterns !== 'object') + throw new Error(`Patterns "${module}" did not return an object`); + + return { + module, + patterns + }; + } + + /** + * 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 + */ + error(err, method, obj={}, event='error') { + let errObj = Object.assign({ + error: err, + method: method + }, obj || {}); + + this._inst.errors.push(errObj); + if(event !== false) this._inst.emit(event, errObj); + + return errObj; + } +} + +module.exports = ModClean_Utils; diff --git a/package.json b/package.json index acd16ea..9fce442 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modclean", - "version": "1.3.0", + "version": "2.1.2", "description": "Remove unwanted files and directories from your node_modules folder", "main": "index.js", "bin": { @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/KyleRoss/modclean" + "url": "https://github.com/ModClean/modclean" }, "keywords": [ "module", @@ -20,26 +20,37 @@ "module-cleanup", "node_modules", "delete", + "flatten", + "path", + "limit", + "less", "files", "folders" ], - "author": "Kyle Ross", + "author": "Kyle Ross ", + "contributors": [ + "Benjamin Santalucia (https://github.com/ben8p)" + ], "license": "MIT", "bugs": { - "url": "https://github.com/KyleRoss/modclean/issues" + "url": "https://github.com/ModClean/modclean/issues" }, - "homepage": "https://github.com/KyleRoss/modclean", + "homepage": "https://github.com/ModClean/modclean", "dependencies": { - "colors": "^1.1.2", - "commander": "^2.8.1", - "glob": "^7.0.0", - "inquirer": "^0.12.0", - "node-dir": "^0.1.9", - "rimraf": "^2.4.2", - "update-notifier": "^0.6.1" + "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", + "lodash.uniq": "^4.5.0", + "modclean-patterns-default": "latest", + "rimraf": "^2.5.4", + "subdirs": "^1.0.0", + "update-notifier": "^1.0.3" + }, + "engines": { + "node": ">=6.9.0" }, - "devDependencies": { - "mocha": "^2.2.5", - "should": "^7.0.4" - } + "engineStrict": true } diff --git a/patterns.json b/patterns.json deleted file mode 100644 index 767db97..0000000 --- a/patterns.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "safe": [ - "readme*", - ".npmignore", - "*license*", - "*licence*", - "history.md", - "history.markdown", - "history", - ".gitattributes", - ".gitmodules", - ".travis.yml", - "binding.gyp", - "contributing*", - "component.json", - "composer.json", - "makefile*", - "gemfile*", - "rakefile*", - ".coveralls.yml", - "example*", - "changelog*", - "changes", - ".jshintrc", - "bower.json", - "*appveyor.yml", - "*.log", - "*.tlog", - "*.patch", - "*.sln", - "*.pdb", - "*.vcxproj*", - ".gitignore", - ".sauce-labs*", - ".vimrc*", - ".idea", - "examples", - "samples", - "test", - "tests", - "draft-00", - "draft-01", - "draft-02", - "draft-03", - "draft-04", - ".eslintrc", - ".jamignore", - ".jscsrc", - "*.todo", - "*.md", - "*.markdown", - "*.js.map", - "contributors", - "*.orig", - "*.rej", - ".zuul.yml", - ".editorconfig", - ".npmrc", - ".jshintignore", - ".eslintignore", - ".lint", - ".lintignore", - "cakefile", - ".istanbul.yml", - "authors", - "hyper-schema", - "mocha.opts", - ".gradle", - ".tern-port", - ".gitkeep", - ".dntrc", - "*.watchr", - ".jsbeautifyrc", - "cname", - "screenshots", - ".dir-locals.el", - "jsl.conf", - "jsstyle", - "benchmark", - "dockerfile", - "*.nuspec", - "*.csproj", - "thumbs.db", - ".ds_store", - "desktop.ini", - "npm-debug.log", - "wercker.yml" - ], - - "caution": [ - "*.png", - "*.gif", - "*.jpg", - "*.html", - "*.css", - "*.eot", - "*.ttf", - "*.woff", - "*.ico", - "*.js.gz", - "*.json~", - "*.txt", - "*.sh", - "*.gnu", - "*.jar", - "*.bat", - "*.gyp", - "*.gypi", - "*.h", - "*.c", - "*.cc", - "*.cpp" - ], - - "danger": [ - "*.coffee", - "*.ts", - ".bin", - "*.min.js", - "test.js", - "*.test.js", - "gruntfile.js", - "gulpfile.js", - "*.sh", - "*.bat", - "*.cmd", - "*.ps1", - "*.map", - "images", - "semver", - "link", - "links", - "uglifyjs" - ] -} diff --git a/test.js b/test.js deleted file mode 100644 index c762975..0000000 --- a/test.js +++ /dev/null @@ -1,395 +0,0 @@ -/** - * ModClean - Tests - * @requires mocha (npm install mocha -g) - * @author Kyle Ross - */ - -/*global describe, it, before, beforeEach */ - -var should = require('should'), - path = require('path'), - modclean = require('./index.js'); - -describe('modclean', function() { - it('should be a function', function() { - modclean.should.be.a.Function; - }); - - describe('defaults', function() { - var patterns; - - before('load patterns.json', function() { - patterns = require('./patterns.json'); - }); - - it('should be an object', function() { - modclean.defaults.should.be.an.instanceOf(Object); - }); - - it('should have correct cwd', function() { - modclean.defaults.cwd.should.equal(process.cwd()); - }); - - it('should contain patterns from patterns.json', function() { - modclean.defaults.patterns.should.match(patterns.safe); - }); - }); - - describe('patterns', function() { - var patterns; - - before('load patterns.json', function() { - patterns = require('./patterns.json'); - }); - - it('should be an object', function() { - modclean.patterns.should.be.an.instanceOf(Object); - }); - - it('should contain patterns from patterns.json', function() { - modclean.patterns.should.match(patterns); - }); - - describe('safe', function() { - it('should contain patterns', function() { - modclean.patterns.should.have.property('safe'); - modclean.patterns.safe.length.should.be.greaterThan(0); - }); - }); - - describe('caution', function() { - it('should contain patterns', function() { - modclean.patterns.should.have.property('caution'); - modclean.patterns.caution.length.should.be.greaterThan(0); - }); - }); - - describe('danger', function() { - it('should contain patterns', function() { - modclean.patterns.should.have.property('danger'); - modclean.patterns.danger.length.should.be.greaterThan(0); - }); - }); - }); - - describe('ModClean', function() { - it('should be a function', function() { - modclean.ModClean.should.be.a.Function; - }); - - it('should have EventEmitter', function() { - modclean.ModClean.super_.name.should.equal('EventEmitter'); - }); - - it('should have clean method', function() { - modclean.ModClean.prototype.clean.should.be.a.Function; - }); - - it('should have _find method', function() { - modclean.ModClean.prototype._find.should.be.a.Function; - }); - - it('should have _process method', function() { - modclean.ModClean.prototype._process.should.be.a.Function; - }); - - it('should have _deleteFile method', function() { - modclean.ModClean.prototype._deleteFile.should.be.a.Function; - }); - }); -}); - -describe('ModClean instance', function() { - this.timeout(3000); - - describe('options', function() { - it('should have default options', function() { - var opts = new modclean.ModClean().options; - opts.cwd = process.cwd(); - opts.should.match(modclean.defaults); - }); - - describe('custom', function() { - var MC, - customPatterns = ['a', 'b', 'c']; - - before('set custom options', function() { - MC = new modclean.ModClean({ - patterns: customPatterns, - ignoreCase: false, - process: function() {}, - modulesDir: 'nm', - errorHalt: true, - test: true - }); - }); - - it('should have custom patterns', function() { - MC.options.patterns.should.match(customPatterns); - }); - - it('should flatten array of arrays for patterns', function() { - var _patterns = []; - _patterns = _patterns.concat(modclean.patterns.safe, modclean.patterns.caution); - - var MC = new modclean.ModClean({ - patterns: [modclean.patterns.safe, modclean.patterns.caution] - }); - - MC.options.patterns.should.match(_patterns); - }); - - it('should ignore case', function() { - MC.options.ignoreCase.should.be.false; - }); - - it('should have process function', function() { - MC.options.process.should.be.a.Function; - }); - - it('should have modulesDir appended to cwd', function() { - MC.options.cwd.should.equal(path.join(process.cwd(), MC.options.modulesDir)); - }); - - it('should errorHalt', function() { - MC.options.errorHalt.should.be.true; - }); - - it('should be in test mode', function() { - MC.options.test.should.be.true; - }); - - it('should go back to defaults in a new instance', function() { - var NMC = new modclean.ModClean(); - NMC.options.should.not.match(MC.options); - }); - }); - }); - - describe('constructor', function() { - it('should run clean if callback provided', function(done) { - var MC = new modclean.ModClean({ test: true }, function(err, results) { - results.should.be.an.Array; - done(); - }); - }); - - it('should except callback as first parameter', function(done) { - modclean.defaults.test = true; - - var MC = new modclean.ModClean(function(err, results) { - results.should.be.an.Array; - modclean.defaults.test = false; - done(); - }); - }); - }); - - describe('complete event', function() { - it('should emit complete event', function(done) { - var MC = new modclean.ModClean({ test: true }); - - MC.on('complete', function(err, results) { - should.not.exist(err); - results.should.be.an.Array; - done(); - }); - - MC.clean(); - }); - }); - - describe('method', function() { - describe('clean', function() { - it('should run the clean method and complete', function(done) { - var MC = new modclean.ModClean({ test: true }); - MC.clean.should.be.a.Function; - - MC.clean(function(err, results) { - should.not.exist(err); - results.should.be.an.Array; - done(); - }); - }); - - it('should emit start event', function(done) { - var MC = new modclean.ModClean({ test: true }); - - MC.on('start', function(inst) { - inst.should.be.instanceOf(modclean.ModClean); - done(); - }); - - MC.clean(); - }); - }); - - describe('_find', function() { - it('should find all index.js files (custom patterns)', function(done) { - var MC = new modclean.ModClean({ test: true }); - MC._find.should.be.a.Function; - - MC._find(['index.js'], function(err, files) { - should.not.exist(err); - files.should.be.an.Array; - done(); - }); - }); - - it('should emit files event', function(done) { - var MC = new modclean.ModClean({ test: true }); - - MC.on('files', function(files) { - files.should.be.an.Array; - files.length.should.be.greaterThan(0); - done(); - }); - - MC._find(['index.js'], function() {}); - }); - }); - - describe('_process', function() { - it('should find all index.js files (custom patterns)', function(done) { - var MC = new modclean.ModClean({ test: true }); - MC._process.should.be.a.Function; - - MC._find(['index.js'], function(err, files) { - should.not.exist(err); - files.should.be.an.Array; - - MC._process(files, function(err, results) { - should.not.exist(err); - results.should.be.an.Array; - results.length.should.equal(files.length); - done(); - }); - }); - }); - - it('should emit finish event', function(done) { - var MC = new modclean.ModClean({ test: true }); - - MC.on('finish', function(results) { - results.should.be.an.Array; - results.length.should.be.greaterThan(0); - done(); - }); - - MC._find(['index.js'], function(err, files) { - MC._process(files, function() {}); - }); - }); - }); - - describe('_deleteFile', function() { - it('should delete a file', function(done) { - var MC = new modclean.ModClean({ test: true }); - - MC._deleteFile('someFile.txt', function(err, file) { - should.not.exist(err); - file.should.be.a.String; - done(); - }); - }); - - it('should emit deleted event', function(done) { - var MC = new modclean.ModClean({ test: true }); - - MC.on('deleted', function(file) { - file.should.be.a.String; - done(); - }); - - MC._deleteFile('someFile.txt', function() {}); - }); - }); - }); - - describe('process function', function() { - describe('async', function() { - it('should call the process function', function(done) { - var count = 0; - - new modclean.ModClean({ - patterns: ['index.js'], - removeEmptyDirs: false, - process: function(file, files, cb) { - file.should.be.a.String; - files.should.be.an.Array; - cb.should.be.a.Function; - count++; - cb(true); - }, - test: true - }, function(err, results) { - should.not.exist(err); - results.should.be.an.Array; - results.length.should.equal(count); - done(); - }); - }); - - it('should skip files', function(done) { - var count = 0; - - new modclean.ModClean({ - patterns: ['index.js'], - removeEmptyDirs: false, - process: function(file, files, cb) { - count++; - cb(false); - }, - test: true - }, function(err, results) { - should.not.exist(err); - results.should.be.an.Array; - results.length.should.not.equal(count); - done(); - }); - }); - }); - - describe('sync', function() { - it('should call the process function', function(done) { - var count = 0; - - new modclean.ModClean({ - patterns: ['index.js'], - removeEmptyDirs: false, - process: function(file, files) { - file.should.be.a.String; - files.should.be.an.Array; - count++; - return true; - }, - test: true - }, function(err, results) { - should.not.exist(err); - results.should.be.an.Array; - results.length.should.equal(count); - done(); - }); - }); - - it('should skip files', function(done) { - var count = 0; - - new modclean.ModClean({ - patterns: ['index.js'], - removeEmptyDirs: false, - process: function(file, files) { - count++; - return false; - }, - test: true - }, function(err, results) { - should.not.exist(err); - results.should.be.an.Array; - results.length.should.not.equal(count); - done(); - }); - }); - }); - }); -});