├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npm-upgrade.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── db └── changelogUrls.json ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── Config.js ├── askUser.js ├── bin │ └── cli.js ├── catchAsyncError.js ├── changelogUtils.js ├── cliStyles.js ├── cliTable.js ├── commands │ ├── changelog.js │ ├── check.js │ ├── ignore.js │ └── ignore │ │ ├── add.js │ │ ├── list.js │ │ └── reset.js ├── filterUtils.js ├── packageUtils.js ├── repositoryUtils.js └── stringUtils.js └── tools └── addModuleChangelogUrlToDb.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": "10.19" 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | "@babel/plugin-transform-runtime", 11 | "lodash" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | indent_style = space 7 | indent_size = 2 8 | 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | insert_final_newline = true 12 | 13 | [changelogUrls.json] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "th0r", 4 | "rules": { 5 | "array-element-newline": "off", 6 | "require-unicode-regexp": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /npm-debug.log 2 | /node_modules 3 | /lib 4 | -------------------------------------------------------------------------------- /.npm-upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "eslint": { 4 | "versions": ">=6.0.0", 5 | "reason": "^5 is required for 'eslint-config-th0r'" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > - [Breaking Change] 5 | > - [New Feature] 6 | > - [Improvement] 7 | > - [Bug Fix] 8 | > - [Internal] 9 | > - [Documentation] 10 | 11 | _Note: Gaps between patch versions are faulty, broken or test releases._ 12 | 13 | ## UNRELEASED 14 | 15 | ## 3.1.2 16 | * **Improvement** 17 | * Don't check for version updates for filtered out dependencies (fixes [#96](https://github.com/th0r/npm-upgrade/issues/96)) 18 | 19 | ## 3.1.1 20 | * **Improvement** 21 | * Disable timeout for fetching NPM repository (fixes [#74](https://github.com/th0r/npm-upgrade/issues/74)) 22 | * Add GitLab to the list of known repositories so `npm-upgrade` now searches for common changelog files there (fixes [#47](https://github.com/th0r/npm-upgrade/issues/47)) 23 | * Don't hardcode `master` branch and let GitHub decide the name of the main branch when searching for common changelog files (fixes [#73](https://github.com/th0r/npm-upgrade/issues/73)) 24 | 25 | ## 3.1.0 26 | * **New Feature** 27 | * `--global` flag that allows to upgrade global packages ([#70](https://github.com/th0r/npm-upgrade/pull/70), [@Medallyon](https://github.com/Medallyon)) 28 | 29 | ## 3.0.0 30 | * **Breaking Change** 31 | * Drop support for Node.js <= 10.19 32 | 33 | * **Internal** 34 | * Update deps 35 | 36 | ## 2.0.4 37 | * **Internal** 38 | * Use `open` package instead of `opener` ([#52](https://github.com/th0r/npm-upgrade/pull/52), [@cascornelissen](https://github.com/cascornelissen)) 39 | 40 | ## 2.0.3 41 | * **Internal** 42 | * Update deps 43 | 44 | ## 2.0.2 45 | * **Bug Fix** 46 | * Fix handling of dependency group flags (`--production`, `--development` etc.) 47 | 48 | ## 2.0.1 49 | * **Improvement** 50 | * Use indentation from `package.json` for `.npm-upgrade.json` ([#36](https://github.com/th0r/npm-upgrade/pull/36), [@cascornelissen](https://github.com/cascornelissen)) 51 | 52 | ## 2.0.0 53 | * **Breaking Change** 54 | * Drop support for Node.js <= 8.10 55 | 56 | * **Improvement** 57 | * Show `@types/*` packages right below their corresponding modules (closes #32) 58 | 59 | * **Internal** 60 | * Update deps 61 | 62 | ## 1.4.1 63 | * **Improvement** 64 | * Preserve indentation in `package.json` (#21, @cascornelissen) 65 | 66 | ## 1.4.0 67 | * **Internal** 68 | * Update deps 69 | 70 | ## 1.3.0 71 | * **New Feature** 72 | * Show list of packages that will be updated in the end of upgrade process (#18) 73 | 74 | * **Internal** 75 | * Drop support for Node 4 76 | * Update deps 77 | 78 | ## 1.2.0 79 | * **Internal** 80 | * Update deps 81 | 82 | ## 1.1.0 83 | * **New Feature** 84 | * Added `changelog` command 85 | 86 | * **Internal** 87 | * Update deps 88 | 89 | ## 1.0.4 90 | * **Bug Fix** 91 | * Fix Node 8 compatibility issue 92 | 93 | * **Internal** 94 | * Use `prepare` npm script instead of `prepublish` 95 | * Update `inquirer` 96 | 97 | ## 1.0.2 98 | * **Bug Fix** 99 | * Fix `ignore reset` command 100 | * Remove irrelevant `webpack` changelog url from db 101 | 102 | * **Internal** 103 | * Use `babel-preset-env` instead of `babel-preset-es2015` 104 | * Update deps 105 | 106 | ## 1.0.1 107 | * **New Feature** 108 | * Ignore modules feature 109 | 110 | * **Internal** 111 | * Update deps 112 | 113 | ## 0.7.0 114 | * **New Feature** 115 | * Ability to finish upgrade process on every step 116 | 117 | * **Bug Fix** 118 | * Fix npm loader shown during upgrade process 119 | 120 | ## 0.6.2 121 | * **Bug Fix** 122 | * Fixes #5: Changelogs do not work anymore 123 | 124 | ## 0.6.1 125 | * **Improvement** 126 | * Add `CHANGELOG` to the list of common changelog files 127 | 128 | ## 0.6.0 129 | * **New Feature** 130 | * Added `filter` CLI argument (see [Usage](README.md#usage) section in `README.md`) 131 | 132 | * **Internal** 133 | * Update deps 134 | 135 | ## 0.5.1 136 | * **Bug Fix** 137 | * Fixed URL to the issues page for the "couldn't find the changelog" message 138 | * Fixed detection of the repository's "Releases" page on GitHub if it contains dot in the name 139 | 140 | ## 0.5.0 141 | * **New Feature** 142 | * CLI options added to only check for specified groups of dependencies (see [Options](README.md#options) section in `README.md`) 143 | 144 | ## 0.4.4 145 | * **Bug Fix** 146 | * Fixed bug with requesting remote changelog URLs database 147 | 148 | ## 0.4.3 149 | * **Breaking Change** 150 | * Changelog URLs database have been moved from `data/homepages.json` to `db/changelogUrls.json` 151 | 152 | * **Improvement** 153 | * Utility now tries to find changelog URL for modules hosted on GitHub. 154 | It will check for some common changelog filenames like `CHANGELOG.md`, `History.md` etc. and 155 | open them in browser if they are present in the repository. 156 | If not, it will open project's `releases` page. 157 | 158 | * **New Feature** 159 | * Added dev CLI utility to easily add module's changelog URL to the database (`tools/addModuleChangelogUrlToDb.js`). 160 | Run it without arguments for more info. 161 | 162 | ## 0.3.0 163 | * **New Feature** 164 | * Option to open module's homepage or changelog during update process. 165 | 166 | ## 0.2.0 167 | * **New Feature** 168 | * Colorize new/old module versions diff. 169 | 170 | * **Internal** 171 | * Split code into ES2015 modules. 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 Yuriy Grunin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-upgrade 2 | Interactive CLI utility to easily update outdated NPM dependencies with changelogs inspection support. 3 | 4 | [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] 5 | 6 | ## What is this for? 7 | If you are tired of manually upgrading `package.json` every time your package dependencies are getting out of date then this utility is for you. 8 | 9 | Take a look at this demo: 10 | 11 | ![npm-upgrade outdated packages](https://cloud.githubusercontent.com/assets/302213/11168821/08311b90-8bb2-11e5-9a71-5da73682ed44.gif) 12 | 13 | ## Installation 14 | First, install [Node.js](https://nodejs.org) (at least `v10.19`). 15 | 16 | Then install this utility as global npm-module: 17 | ```sh 18 | npm i -g npm-upgrade 19 | ``` 20 | 21 | ## Usage 22 | This utility is supposed to be run in the root directory of your Node.js project (that contains `package.json`). 23 | Run `npm-upgrade --help` to see all available top-level commands: 24 | ``` 25 | check [filter] Check for outdated modules 26 | ignore Manage ignored modules 27 | changelog Show changelog for a module 28 | ``` 29 | Run `npm-upgrade --help` to see usage help for corresponding command. 30 | `check` is the default command and can be omitted so running `npm-upgrade [filter]` is the same as `npm-upgrade check [filter]`. 31 | 32 | ### `check` command 33 | It will find all your outdated deps and will ask to updated their versions in `package.json`, one by one. 34 | For example, here is what you will see if you use outdated version of `@angular/common` module: 35 | ``` 36 | Update "@angular/common" in package.json from 2.4.8 to 2.4.10? (Use arrow keys) 37 | ❯ Yes 38 | No 39 | Show changelog 40 | Ignore 41 | Finish update process 42 | ``` 43 | * `Yes` will update `@angular/common` version in `package.json` to `2.4.10`, but not immediately (see explanation below) 44 | * `No` will not update this module version. 45 | * `Show changelog` will try to find changelog url for the current module and open it in default browser. 46 | * `Ignore` will add this module to the ignored list (see details in [`Ignoring module`](#ignoring-module) section below). 47 | * `Finish update process` will ...hm... finish update process and save all the changes to `package.json`. 48 | 49 | A note on saving changes to `package.json`: when you choose `Yes` to update some module's version, `package.json` won't be immediately updated. It will be updated only after you will process all the outdated modules and confirm update **or** when you choose `Finish update process`. So if in the middle of the update process you've changed your mind just press `Ctrl+C` and `package.json` will remain untouched. 50 | 51 | If you want to check only some deps, you can use `filter` argument: 52 | ```sh 53 | # Will check only `babel-core` module: 54 | npm-upgrade babel-core 55 | 56 | # Will check all the deps with `babel` in the name: 57 | npm-upgrade '*babel*' 58 | 59 | # Note quotes around `filter`. They are necessary because without them bash may interpret `*` as wildcard character. 60 | 61 | # Will check all the deps, excluding any with `babel` in the name: 62 | npm-upgrade '!*babel*' 63 | 64 | # You can combine including and excluding rules: 65 | npm-upgrade '*babel* !babel-transform-* !babel-preset-*' 66 | ``` 67 | 68 | If you want to check only a group of deps use these options: 69 | ``` 70 | -p, --production Check only "dependencies" 71 | -d, --development Check only "devDependencies" 72 | -o, --optional Check only "optionalDependencies" 73 | ``` 74 | 75 | Alternatively, you can use the `-g` (`--global`) flag to upgrade your global packages. **Note** that this flag is mutually exclusive and `npm-upgrade` will only recognise the global flag if supplied with others. Also **Note** that this option will automatically attempt to upgrade your global packages using `npm install -g @`. 76 | 77 | #### Ignoring module 78 | Sometimes you just want to ignore newer versions of some dependency for some reason. For example, you use `jquery v2` because of the old IE support and don't want `npm-upgrade` to suggest you updating it to `v3`. Or you use `some-funky-module@6.6.5` and know that the new version `6.6.6` contains a bug that breaks your app. 79 | 80 | You can handle these situations by ignoring such modules. You can do it in two ways: choosing `Ignore` during update process or using `npm ignore add` command. 81 | 82 | You will asked two questions. First is a version range to ignore. It should be a valid [semver](http://semver.org/) version. Here are a few examples: 83 | * `6.6.6` - will ignore only version `6.6.6`. When the next version after `6.6.6` will be published `npm-upgrade` will suggest to update it. Can be used in `some-funky-module` example above. 84 | * `>2` - will ignore all versions starting from `3.0.0`. Can be used in `jquery v2` example above. 85 | * `6.6.x || 6.7.x` - will ignore all `6.6.x` and `6.7.x` versions. 86 | * `*` - will ignore all new versions. 87 | 88 | And after that `npm-upgrade` will ask about the ignore reason. The answer is optional but is strongly recommended because it will help to explain your motivation to your сolleagues and to yourself after a few months. 89 | 90 | All the data about ignored modules will be stored in `.npm-upgrade.json` file next to your project's `package.json`. 91 | 92 | ### `ignore` command 93 | It will help you manage ignored modules. See [Ignoring module](#ignoring-module) section for more details. 94 | It has the following subcommands: 95 | ``` 96 | npm-upgrade ignore 97 | 98 | Commands: 99 | add [module] Add module to ignored list 100 | list Show the list of ignored modules 101 | reset [modules...] Reset ignored modules 102 | ``` 103 | * `add` - will add a module from your deps to ignored list. You can either provide module name as optional `module` argument or interactively select it from the list of project's deps. 104 | * `list` - will show the list of currently ignored modules along with their ignored versions and reasons. 105 | * `reset` - will remove modules from the ignored list. You can either provide module names as `modules` argument (separated by space) or interactively select them from the list of project's deps. 106 | 107 | ### `changelog` command 108 | ``` 109 | npm-upgrade changelog 110 | ``` 111 | Will try to find changelog url for provided module and open it in default browser. 112 | 113 | ## Troubleshooting 114 | **Wrong changelog shown for _\_ or not shown at all!** 115 | 116 | Yes, It can happen sometimes. This is because there is no standardized way to specify changelog location for the module, so it tries to guess it, using these rules one by one: 117 | 118 | 1. Check `db/changelogUrls.json` from `master` branch on GitHub or the local copy if it's unreachable. 119 | 2. Check `changelog` field from module's `package.json`. 120 | 3. Parse module's `repository.url` field and if it's on GitHub or GitLab, try to request some common changelog files (`CHANGELOG.md`, `History.md` etc.) from main branch and if it fails, open `Releases` page. 121 | 122 | So, if it guessed wrong it would be great if you could either [fill an issue](../../issues/new) about this or submit a PR which adds proper changelog URL to `db/changelogUrls.json`. There is a tool in the repository for you to make it as easy as possible: 123 | ```sh 124 | ./tools/addModuleChangelogUrlToDb.js 125 | ``` 126 | 127 | ## License 128 | 129 | [MIT](LICENSE) 130 | 131 | [downloads-image]: https://img.shields.io/npm/dt/npm-upgrade.svg 132 | [npm-url]: https://www.npmjs.com/package/npm-upgrade 133 | [npm-image]: https://img.shields.io/npm/v/npm-upgrade.svg 134 | -------------------------------------------------------------------------------- /db/changelogUrls.json: -------------------------------------------------------------------------------- 1 | { 2 | "@babel/core": "https://github.com/babel/babel/blob/main/CHANGELOG.md", 3 | "@rollup/plugin-alias": "https://github.com/rollup/plugins/blob/master/packages/alias/CHANGELOG.md", 4 | "@rollup/plugin-auto-install": "https://github.com/rollup/plugins/blob/master/packages/auto-install/CHANGELOG.md", 5 | "@rollup/plugin-babel": "https://github.com/rollup/plugins/blob/master/packages/babel/CHANGELOG.md", 6 | "@rollup/plugin-beep": "https://github.com/rollup/plugins/blob/master/packages/beep/CHANGELOG.md", 7 | "@rollup/plugin-buble": "https://github.com/rollup/plugins/blob/master/packages/buble/CHANGELOG.md", 8 | "@rollup/plugin-commonjs": "https://github.com/rollup/plugins/blob/master/packages/commonjs/CHANGELOG.md", 9 | "@rollup/plugin-data-uri": "https://github.com/rollup/plugins/blob/master/packages/data-uri/CHANGELOG.md", 10 | "@rollup/plugin-dsv": "https://github.com/rollup/plugins/blob/master/packages/dsv/CHANGELOG.md", 11 | "@rollup/plugin-dynamic-import-vars": "https://github.com/rollup/plugins/blob/master/packages/dynamic-import-vars/CHANGELOG.md", 12 | "@rollup/plugin-eslint": "https://github.com/rollup/plugins/blob/master/packages/eslint/CHANGELOG.md", 13 | "@rollup/plugin-graphql": "https://github.com/rollup/plugins/blob/master/packages/graphql/CHANGELOG.md", 14 | "@rollup/plugin-html": "https://github.com/rollup/plugins/blob/master/packages/html/CHANGELOG.md", 15 | "@rollup/plugin-image": "https://github.com/rollup/plugins/blob/master/packages/image/CHANGELOG.md", 16 | "@rollup/plugin-inject": "https://github.com/rollup/plugins/blob/master/packages/inject/CHANGELOG.md", 17 | "@rollup/plugin-json": "https://github.com/rollup/plugins/blob/master/packages/json/CHANGELOG.md", 18 | "@rollup/plugin-legacy": "https://github.com/rollup/plugins/blob/master/packages/legacy/CHANGELOG.md", 19 | "@rollup/plugin-multi-entry": "https://github.com/rollup/plugins/blob/master/packages/multi-entry/CHANGELOG.md", 20 | "@rollup/plugin-node-resolve": "https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md", 21 | "@rollup/plugin-replace": "https://github.com/rollup/plugins/blob/master/packages/replace/CHANGELOG.md", 22 | "@rollup/plugin-run": "https://github.com/rollup/plugins/blob/master/packages/run/CHANGELOG.md", 23 | "@rollup/plugin-strip": "https://github.com/rollup/plugins/blob/master/packages/strip/CHANGELOG.md", 24 | "@rollup/plugin-sucrase": "https://github.com/rollup/plugins/blob/master/packages/sucrase/CHANGELOG.md", 25 | "@rollup/plugin-typescript": "https://github.com/rollup/plugins/blob/master/packages/typescript/CHANGELOG.md", 26 | "@rollup/plugin-url": "https://github.com/rollup/plugins/blob/master/packages/url/CHANGELOG.md", 27 | "@rollup/plugin-virtual": "https://github.com/rollup/plugins/blob/master/packages/virtual/CHANGELOG.md", 28 | "@rollup/plugin-wasm": "https://github.com/rollup/plugins/blob/master/packages/wasm/CHANGELOG.md", 29 | "@rollup/plugin-yaml": "https://github.com/rollup/plugins/blob/master/packages/yaml/CHANGELOG.md", 30 | "@rollup/pluginutils": "https://github.com/rollup/plugins/blob/master/packages/pluginutils/CHANGELOG.md", 31 | "@testing-library/dom": "https://github.com/testing-library/dom-testing-library/releases", 32 | "@testing-library/jest-dom": "https://github.com/testing-library/jest-dom/releases", 33 | "@testing-library/react": "https://github.com/testing-library/react-testing-library/releases", 34 | "@testing-library/react-hooks": "https://github.com/testing-library/react-hooks-testing-library/releases", 35 | "@testing-library/user-event": "https://github.com/testing-library/user-event/releases", 36 | "@zxing/ngx-scanner": "https://github.com/zxing-js/ngx-scanner/releases", 37 | "bluebird": "http://bluebirdjs.com/docs/changelog.html", 38 | "browserify": "https://github.com/substack/node-browserify/blob/master/changelog.markdown", 39 | "cypress": "https://on.cypress.io/changelog", 40 | "fastly": "https://github.com/fastly/fastly-js/blob/main/CHANGELOG.md", 41 | "fluxible": "https://github.com/yahoo/fluxible/blob/master/packages/fluxible/CHANGELOG.md", 42 | "lodash": "https://github.com/lodash/lodash/wiki/Changelog", 43 | "recharts": "https://github.com/recharts/recharts/releases", 44 | "vue-loader": "https://github.com/vuejs/vue-loader/releases" 45 | } 46 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | gulp.task('default', watch); 4 | gulp.task(watch); 5 | gulp.task('build', gulp.series(clean, build)); 6 | gulp.task(clean); 7 | 8 | const SRC = 'src/**/*.js'; 9 | const DEST = 'lib'; 10 | 11 | function clean() { 12 | const del = require('del'); 13 | return del('lib'); 14 | } 15 | 16 | function build() { 17 | const babel = require('gulp-babel'); 18 | 19 | return gulp.src(SRC) 20 | .pipe(babel()) 21 | .pipe(gulp.dest(DEST)); 22 | } 23 | 24 | function watch() { 25 | gulp.watch(SRC, {ignoreInitial: false}, build); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-upgrade", 3 | "version": "3.1.2", 4 | "description": "Interactive CLI utility to easily update outdated NPM dependencies", 5 | "author": "Yuriy Grunin ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/th0r/npm-upgrade", 8 | "changelog": "https://github.com/th0r/npm-upgrade/blob/master/CHANGELOG.md", 9 | "bugs": { 10 | "url": "https://github.com/th0r/npm-upgrade/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/th0r/npm-upgrade.git" 15 | }, 16 | "main": "./lib/bin/cli.js", 17 | "bin": "./lib/bin/cli.js", 18 | "scripts": { 19 | "prepare": "gulp build", 20 | "build": "gulp build", 21 | "dev": "gulp watch", 22 | "lint": "eslint ." 23 | }, 24 | "engines": { 25 | "node": ">= 10.19" 26 | }, 27 | "preferGlobal": true, 28 | "dependencies": { 29 | "@babel/runtime": "7.13.7", 30 | "bluebird": "3.7.2", 31 | "chalk": "4.1.0", 32 | "cli-table": "0.3.5", 33 | "del": "6.0.0", 34 | "detect-indent": "6.0.0", 35 | "got": "11.8.1", 36 | "inquirer": "7.3.3", 37 | "libnpmconfig": "1.2.1", 38 | "lodash": "4.17.21", 39 | "npm-check-updates": "11.1.9", 40 | "open": "7.4.2", 41 | "pacote": "11.2.7", 42 | "semver": "7.3.4", 43 | "shelljs": "^0.8.4", 44 | "yargs": "16.2.0" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "7.13.1", 48 | "@babel/plugin-transform-runtime": "7.13.7", 49 | "@babel/preset-env": "7.13.5", 50 | "babel-eslint": "10.1.0", 51 | "babel-plugin-lodash": "3.3.4", 52 | "eslint": "5.16.0", 53 | "eslint-config-th0r": "2.0.0", 54 | "gulp": "4.0.2", 55 | "gulp-babel": "8.0.0" 56 | }, 57 | "files": [ 58 | "lib", 59 | "src", 60 | "db" 61 | ], 62 | "directories": { 63 | "lib": "./lib" 64 | }, 65 | "keywords": [ 66 | "npm", 67 | "update", 68 | "outdated", 69 | "dependencies", 70 | "cli", 71 | "interactive", 72 | "automatic", 73 | "changelog", 74 | "ignore" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/Config.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import {writeFileSync} from 'fs'; 3 | import del from 'del'; 4 | import _ from 'lodash'; 5 | import detectIndent from 'detect-indent'; 6 | import {loadPackageJson} from './packageUtils'; 7 | 8 | const PROJECT_CONFIG_FILENAME = '.npm-upgrade.json'; 9 | 10 | const path = Symbol('path'); 11 | const storedData = Symbol('storedData'); 12 | const read = Symbol('read'); 13 | const getData = Symbol('getData'); 14 | 15 | export default class Config { 16 | 17 | constructor(opts) { 18 | const {projectRoot} = opts || {}; 19 | this[path] = resolve(projectRoot || process.cwd(), PROJECT_CONFIG_FILENAME); 20 | this[storedData] = this[read](); 21 | _.assign( 22 | this, 23 | _.cloneDeep(this[storedData]) 24 | ); 25 | } 26 | 27 | save() { 28 | const data = this[getData](); 29 | 30 | if (_.isEqual(data, this[storedData])) return; 31 | 32 | try { 33 | if (_.isEmpty(data)) { 34 | this.remove(); 35 | } else { 36 | const {source: packageSource} = loadPackageJson(); 37 | const {indent} = detectIndent(packageSource); 38 | 39 | writeFileSync( 40 | this[path], 41 | JSON.stringify(data, null, indent) 42 | ); 43 | } 44 | } catch (err) { 45 | err.message = `Unable to update npm-upgrade config file: ${err.message}`; 46 | throw err; 47 | } 48 | } 49 | 50 | remove() { 51 | return del.sync(this[path]); 52 | } 53 | 54 | [read]() { 55 | try { 56 | return require(this[path]); 57 | } catch (err) { 58 | return {}; 59 | } 60 | } 61 | 62 | [getData]() { 63 | const data = {...this}; 64 | return cleanDeep(data); 65 | } 66 | 67 | } 68 | 69 | function cleanDeep(obj) { 70 | _.each(obj, (val, key) => { 71 | if (_.isObjectLike(val)) { 72 | cleanDeep(val); 73 | if (_.isEmpty(val)) { 74 | delete obj[key]; 75 | } 76 | } else if (val === null || val === undefined) { 77 | delete obj[key]; 78 | } 79 | }); 80 | 81 | return obj; 82 | } 83 | -------------------------------------------------------------------------------- /src/askUser.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | export default async function askUser(question) { 4 | const {answer} = await inquirer.prompt([{...question, name: 'answer'}]); 5 | return answer; 6 | } 7 | -------------------------------------------------------------------------------- /src/bin/cli.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import yargs from 'yargs'; 4 | 5 | yargs 6 | .commandDir('../commands') 7 | .demandCommand() 8 | .strict() 9 | .version() 10 | .help() 11 | .argv; 12 | -------------------------------------------------------------------------------- /src/catchAsyncError.js: -------------------------------------------------------------------------------- 1 | export default function catchAsyncError(asyncFn) { 2 | return function () { 3 | return asyncFn 4 | .apply(this, arguments) 5 | .catch(err => { 6 | console.error(err); 7 | process.exit(1); 8 | }); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/changelogUtils.js: -------------------------------------------------------------------------------- 1 | import Bluebird from 'bluebird'; 2 | import _ from 'lodash'; 3 | import got from 'got'; 4 | 5 | import {getModuleInfo} from './packageUtils'; 6 | import {getRepositoryInfo} from './repositoryUtils'; 7 | 8 | const pkg = require('../package.json'); 9 | 10 | const COMMON_CHANGELOG_FILES = ['CHANGELOG.md', 'History.md', 'HISTORY.md', 'CHANGES.md', 'CHANGELOG']; 11 | const CURRENT_REPOSITORY_ID = getRepositoryInfo(pkg.repository.url).repositoryId; 12 | const DEFAULT_REMOTE_CHANGELOGS_DB_URL = 13 | `https://raw.githubusercontent.com/${CURRENT_REPOSITORY_ID}/master/db/changelogUrls.json`; 14 | 15 | export const fetchRemoteDb = _.memoize(async (url = DEFAULT_REMOTE_CHANGELOGS_DB_URL) => { 16 | try { 17 | const response = await got(url, {json: true}); 18 | 19 | return response.body; 20 | } catch (err) { 21 | return null; 22 | } 23 | }); 24 | 25 | export async function findModuleChangelogUrl(moduleName, remoteChangelogUrlsDbUrl = DEFAULT_REMOTE_CHANGELOGS_DB_URL) { 26 | let changelogUrls; 27 | 28 | if (remoteChangelogUrlsDbUrl) { 29 | changelogUrls = await fetchRemoteDb(remoteChangelogUrlsDbUrl); 30 | } 31 | 32 | changelogUrls = changelogUrls || require('../db/changelogUrls.json'); 33 | 34 | if (changelogUrls[moduleName]) { 35 | return changelogUrls[moduleName]; 36 | } 37 | 38 | const {changelog, repository} = await getModuleInfo(moduleName); 39 | 40 | if (changelog) { 41 | return changelog; 42 | } 43 | 44 | if (repository && repository.url) { 45 | // If repository is located on one of known hostings, then we will try to request 46 | // some common changelog files from there or return URL for "Releases" page 47 | const {fileUrlBuilder, releasesPageUrl} = getRepositoryInfo(repository.url) || {}; 48 | 49 | if (fileUrlBuilder) { 50 | const possibleChangelogUrls = _.map(COMMON_CHANGELOG_FILES, fileUrlBuilder); 51 | 52 | try { 53 | return await Bluebird.any( 54 | _.map(possibleChangelogUrls, url => 55 | Bluebird 56 | .try(() => got(url)) 57 | .then(response => { 58 | // Considering only 2xx codes as successful as e.g. GitLab returns 304 for missing files 59 | if (response.statusCode >= 300) { 60 | throw response; 61 | } else { 62 | return url; 63 | } 64 | }) 65 | ) 66 | ); 67 | } catch (err) { 68 | if (!(err instanceof Bluebird.AggregateError)) throw err; 69 | } 70 | } 71 | 72 | if (releasesPageUrl) { 73 | try { 74 | // Checking `releasesUrl`... 75 | await got(releasesPageUrl); 76 | // `releasesUrl` is fine 77 | return releasesPageUrl; 78 | } catch (err) { 79 | // `releasesPageUrl` is broken 80 | } 81 | } 82 | } 83 | 84 | return null; 85 | } 86 | -------------------------------------------------------------------------------- /src/cliStyles.js: -------------------------------------------------------------------------------- 1 | import {white, green, yellow} from 'chalk'; 2 | 3 | export const strong = white.bold; 4 | export const success = green.bold; 5 | export const attention = yellow.bold; 6 | -------------------------------------------------------------------------------- /src/cliTable.js: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table'; 2 | 3 | const COL_ALIGNS_MAP = { 4 | l: 'left', 5 | r: 'right', 6 | c: 'middle' 7 | }; 8 | 9 | export function createSimpleTable(rows, opts = {}) { 10 | if (opts.colAligns) { 11 | opts.colAligns = opts.colAligns 12 | .split('') 13 | .map(val => COL_ALIGNS_MAP[val]); 14 | } 15 | 16 | const table = new Table({ 17 | style: {'padding-left': 2}, 18 | colAligns: ['left', 'right', 'right', 'right', 'middle'], 19 | chars: { 20 | 'top': '', 21 | 'top-mid': '', 22 | 'top-left': '', 23 | 'top-right': '', 24 | 'bottom': '', 25 | 'bottom-mid': '', 26 | 'bottom-left': '', 27 | 'bottom-right': '', 28 | 'left': '', 29 | 'left-mid': '', 30 | 'mid': '', 31 | 'mid-mid': '', 32 | 'right': '', 33 | 'right-mid': '', 34 | 'middle': '' 35 | }, 36 | ...opts 37 | }); 38 | 39 | if (rows) { 40 | table.push(...rows); 41 | } 42 | 43 | return table; 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/changelog.js: -------------------------------------------------------------------------------- 1 | import open from 'open'; 2 | 3 | import catchAsyncError from '../catchAsyncError'; 4 | import {findModuleChangelogUrl} from '../changelogUtils'; 5 | import {strong} from '../cliStyles'; 6 | const pkg = require('../../package.json'); 7 | 8 | export const command = 'changelog '; 9 | export const describe = 'Show changelog for a module'; 10 | 11 | export const handler = catchAsyncError(async opts => { 12 | const {moduleName} = opts; 13 | 14 | console.log(`Trying to find changelog URL for ${strong(moduleName)}...`); 15 | let changelogUrl; 16 | try { 17 | changelogUrl = await findModuleChangelogUrl(moduleName); 18 | } catch (err) { 19 | if (err.code === 'E404') { 20 | console.log("Couldn't find info about this module in npm registry"); 21 | return; 22 | } 23 | } 24 | 25 | if (changelogUrl) { 26 | console.log(`Opening ${strong(changelogUrl)}...`); 27 | open(changelogUrl); 28 | } else { 29 | console.log( 30 | "Sorry, we haven't found any changelog URL for this module.\n" + 31 | `It would be great if you could fill an issue about this here: ${strong(pkg.bugs.url)}\n` + 32 | 'Thanks a lot!' 33 | ); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /src/commands/check.js: -------------------------------------------------------------------------------- 1 | import {writeFileSync} from 'fs'; 2 | 3 | import _ from 'lodash'; 4 | import {flow, map, partition} from 'lodash/fp'; 5 | import open from 'open'; 6 | import semver from 'semver'; 7 | import detectIndent from 'detect-indent'; 8 | import ncu from 'npm-check-updates'; 9 | import shell from 'shelljs'; 10 | import {colorizeDiff} from 'npm-check-updates/lib/version-util'; 11 | 12 | import catchAsyncError from '../catchAsyncError'; 13 | import {makeFilterFunction} from '../filterUtils'; 14 | import {DEPS_GROUPS, loadGlobalPackages, loadPackageJson, setModuleVersion, 15 | getModuleInfo, getModuleHomepage} from '../packageUtils'; 16 | import {fetchRemoteDb, findModuleChangelogUrl} from '../changelogUtils'; 17 | import {createSimpleTable} from '../cliTable'; 18 | import {strong, success, attention} from '../cliStyles'; 19 | import askUser from '../askUser'; 20 | import {toSentence} from '../stringUtils'; 21 | import {askIgnoreFields} from './ignore'; 22 | import Config from '../Config'; 23 | 24 | const pkg = require('../../package.json'); 25 | 26 | function createUpdatedModulesTable(modules) { 27 | return createSimpleTable( 28 | _.map(modules, ({name, from, to}) => [ 29 | strong(name), 30 | from, '→', colorizeDiff(from, to) 31 | ]) 32 | ); 33 | } 34 | 35 | export const command = 'check [filter]'; 36 | export const aliases = '*'; 37 | export const describe = 'Check for outdated modules'; 38 | 39 | export function builder(yargs) { 40 | DEPS_GROUPS 41 | .forEach(({name, field, flag}) => 42 | yargs.option(name, { 43 | type: 'boolean', 44 | alias: flag, 45 | describe: `check only "${field}"` 46 | }) 47 | ); 48 | } 49 | 50 | /* eslint complexity: "off" */ 51 | export const handler = catchAsyncError(async opts => { 52 | const {filter} = opts; 53 | 54 | // Checking all the deps if all of them are omitted 55 | if (_.every(DEPS_GROUPS, ({name}) => !opts[name])) { 56 | _.each(DEPS_GROUPS, ({name}) => (opts[name] = true)); 57 | opts.global = false; 58 | } else if (opts.global) { 59 | // Make global flag mutually exclusive with other flags 60 | _.each(DEPS_GROUPS, ({name}) => { opts[name] = false }); 61 | opts.global = true; 62 | } 63 | 64 | // Loading `package.json` from the current directory 65 | const {path: packageFile, content: packageJson, source: packageSource} = opts.global ? 66 | loadGlobalPackages() : loadPackageJson(); 67 | 68 | // Fetching remote changelogs db in background 69 | fetchRemoteDb(); 70 | 71 | const depsGroupsToCheck = _.filter(DEPS_GROUPS, ({name}) => !!opts[name]); 72 | const depsGroupsToCheckStr = (depsGroupsToCheck.length === DEPS_GROUPS.length) ? 73 | '' : `${toSentence(_.map(depsGroupsToCheck, ({name}) => strong(name)))} `; 74 | const filteredWith = filter ? `filtered with ${strong(filter)} ` : ''; 75 | 76 | console.log( 77 | `Checking for outdated ${depsGroupsToCheckStr}dependencies ${filteredWith}${opts.global ? '' : 78 | (`for "${strong(packageFile)}"`)}...` 79 | ); 80 | 81 | const ncuDepGroups = DEPS_GROUPS 82 | .filter(({name}) => opts[name]) 83 | .map(({ncuValue}) => ncuValue) 84 | .join(','); 85 | const filteredPackageJson = filterDepsInPackageJson(packageJson, makeFilterFunction(filter)); 86 | const currentVersions = ncu.getCurrentDependencies(filteredPackageJson, {dep: ncuDepGroups}); 87 | const latestVersions = await ncu.queryVersions(currentVersions, {versionTarget: 'latest', timeout: 0}); 88 | const upgradedVersions = ncu.upgradeDependencies(currentVersions, latestVersions); 89 | 90 | if (_.isEmpty(upgradedVersions)) { 91 | return console.log(success('All dependencies are up-to-date!')); 92 | } 93 | 94 | // Getting the list of ignored modules 95 | const config = new Config(); 96 | config.ignore = config.ignore || {}; 97 | 98 | // Making arrays of outdated modules 99 | const [ignoredModules, modulesToUpdate] = flow( 100 | map.convert({'cap': false})((newVersion, moduleName) => ({ 101 | name: moduleName, 102 | from: currentVersions[moduleName], 103 | to: newVersion 104 | })), 105 | partition(module => ( 106 | _.has(config.ignore, module.name) && 107 | semver.satisfies(latestVersions[module.name], config.ignore[module.name].versions) 108 | )) 109 | )(upgradedVersions); 110 | 111 | // Moving `@types/*` modules right below their original modules 112 | sortModules(modulesToUpdate); 113 | sortModules(ignoredModules); 114 | 115 | // Creating pretty-printed CLI tables with update info 116 | if (_.isEmpty(modulesToUpdate)) { 117 | console.log( 118 | success('\nAll active modules are up-to-date!') 119 | ); 120 | } else { 121 | console.log( 122 | `\n${strong('New versions of active modules available:')}\n\n${createUpdatedModulesTable(modulesToUpdate)}` 123 | ); 124 | } 125 | 126 | if (!_.isEmpty(ignoredModules)) { 127 | const rows = _.map(ignoredModules, ({name, from, to}) => [ 128 | strong(name), 129 | from, '→', colorizeDiff(from, to), 130 | attention(config.ignore[name].versions), 131 | config.ignore[name].reason 132 | ]); 133 | 134 | // Adding table header 135 | rows.unshift(_.map( 136 | ['', 'From', '', 'To', 'Ignored versions', 'Reason'], 137 | header => strong(header) 138 | )); 139 | 140 | console.log(`\n${strong('Ignored updates:')}\n\n${createSimpleTable(rows)}`); 141 | } 142 | 143 | const updatedModules = []; 144 | let isUpdateFinished = false; 145 | while (modulesToUpdate.length && !isUpdateFinished) { 146 | const outdatedModule = modulesToUpdate.shift(); 147 | const {name, from, to} = outdatedModule; 148 | let {changelogUrl, homepage} = outdatedModule; 149 | 150 | // Adds new line 151 | console.log(''); 152 | 153 | const answer = await askUser({ 154 | type: 'list', 155 | message: `${changelogUrl === undefined ? 'U' : 'So, u'}pdate "${name}" ${opts.global ? 'globally' : 156 | 'in package.json'} from ${from} to ${colorizeDiff(from, to)}?`, 157 | choices: _.compact([ 158 | {name: 'Yes', value: true}, 159 | {name: 'No', value: false}, 160 | // Don't show this option if we couldn't find module's changelog url 161 | (changelogUrl !== null) && 162 | {name: 'Show changelog', value: 'changelog'}, 163 | // Show this if we haven't found changelog 164 | (changelogUrl === null && homepage !== null) && 165 | {name: 'Open homepage', value: 'homepage'}, 166 | {name: 'Ignore', value: 'ignore'}, 167 | {name: 'Finish update process', value: 'finish'} 168 | ]), 169 | // Automatically setting cursor to "Open homepage" after we haven't found changelog 170 | default: (changelogUrl === null && homepage === undefined) ? 2 : 0 171 | }); 172 | 173 | switch (answer) { 174 | case 'changelog': 175 | // Ask user about this module again 176 | modulesToUpdate.unshift(outdatedModule); 177 | 178 | if (changelogUrl === undefined) { 179 | console.log('Trying to find changelog URL...'); 180 | changelogUrl = 181 | outdatedModule.changelogUrl = await findModuleChangelogUrl(name); 182 | } 183 | 184 | if (changelogUrl) { 185 | console.log(`Opening ${strong(changelogUrl)}...`); 186 | open(changelogUrl); 187 | } else { 188 | console.log( 189 | `Sorry, we haven't found any changelog URL for ${strong(name)} module.\n` + 190 | `It would be great if you could fill an issue about this here: ${strong(pkg.bugs.url)}\n` + 191 | 'Thanks a lot!' 192 | ); 193 | } 194 | break; 195 | 196 | case 'homepage': 197 | // Ask user about this module again 198 | modulesToUpdate.unshift(outdatedModule); 199 | 200 | if (homepage === undefined) { 201 | console.log('Trying to find homepage URL...'); 202 | homepage = outdatedModule.homepage = getModuleHomepage(await getModuleInfo(name)); 203 | } 204 | 205 | if (homepage) { 206 | console.log(`Opening ${strong(homepage)}...`); 207 | open(homepage); 208 | } else { 209 | console.log(`Sorry, there is no info about homepage URL in the ${strong(name)}'s package.json`); 210 | } 211 | break; 212 | 213 | case 'ignore': { 214 | const {versions, reason} = await askIgnoreFields(latestVersions[name]); 215 | config.ignore[name] = {versions, reason}; 216 | break; 217 | } 218 | 219 | case 'finish': 220 | isUpdateFinished = true; 221 | break; 222 | 223 | case true: 224 | updatedModules.push(outdatedModule); 225 | setModuleVersion(name, to, packageJson); 226 | delete config.ignore[name]; 227 | break; 228 | } 229 | } 230 | 231 | // Adds new line 232 | console.log(''); 233 | 234 | // Saving config 235 | config.save(); 236 | 237 | if (!updatedModules.length) { 238 | console.log('Nothing to update'); 239 | return; 240 | } 241 | 242 | // Showing the list of modules that are going to be updated 243 | console.log( 244 | `\n${strong('These packages will be updated:')}\n\n` + 245 | createUpdatedModulesTable(updatedModules) + 246 | '\n' 247 | ); 248 | 249 | if (opts.global) { 250 | const shouldUpdateGlobalPackages = await askUser( 251 | {type: 'confirm', message: 'Update global modules?', default: true} 252 | ); 253 | 254 | if (!shouldUpdateGlobalPackages) {return} 255 | 256 | console.log(`Automatically upgrading ${updatedModules.length} module${updatedModules.length !== 1 ? 's' : ''}...`); 257 | return shell.exec(`npm install --global ${updatedModules.map(({name, to}) => `${name}@${to}`).join(' ')}`); 258 | } 259 | 260 | const shouldUpdatePackageFile = await askUser( 261 | {type: 'confirm', message: 'Update package.json?', default: true} 262 | ); 263 | 264 | if (shouldUpdatePackageFile) { 265 | const {indent} = detectIndent(packageSource); 266 | 267 | writeFileSync( 268 | packageFile, 269 | // Adding newline to the end of file 270 | `${JSON.stringify(packageJson, null, indent)}\n` 271 | ); 272 | } 273 | }); 274 | 275 | function filterDepsInPackageJson(packageJson, moduleNameFilterFn) { 276 | const result = _.cloneDeep(packageJson); 277 | 278 | for (const depsGroup of DEPS_GROUPS) { 279 | const deps = result[depsGroup.field]; 280 | 281 | if (deps) { 282 | for (const moduleName of Object.keys(deps)) { 283 | if (!moduleNameFilterFn(moduleName)) { 284 | delete deps[moduleName]; 285 | } 286 | } 287 | } 288 | } 289 | 290 | return result; 291 | } 292 | 293 | function sortModules(modules) { 294 | const processedModules = new Set(); 295 | 296 | for (let i = 0, len = modules.length; i < len; i++) { 297 | const module = modules[i]; 298 | 299 | if (processedModules.has(module)) { 300 | continue; 301 | } 302 | 303 | const normalizedName = module.name.replace(/^@types\//, ''); 304 | 305 | if (module.name === normalizedName) { 306 | continue; 307 | } 308 | 309 | // Searching for corresponding module 310 | const originalModuleIndex = modules.findIndex(({name}) => name === normalizedName); 311 | 312 | if (originalModuleIndex === -1 || i === originalModuleIndex + 1) { 313 | continue; 314 | } 315 | 316 | if (originalModuleIndex > i) { 317 | modules.splice(originalModuleIndex + 1, 0, module); 318 | modules.splice(i, 1); 319 | processedModules.add(module); 320 | i--; 321 | } else { 322 | modules.splice(i, 1); 323 | modules.splice(originalModuleIndex + 1, 0, module); 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/commands/ignore.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import semver from 'semver'; 3 | 4 | import askUser from '../askUser'; 5 | import {createSimpleTable} from '../cliTable'; 6 | import {strong, attention} from '../cliStyles'; 7 | 8 | export const command = 'ignore '; 9 | export const describe = 'Manage ignored modules'; 10 | export const builder = yargs => 11 | yargs 12 | .commandDir('ignore') 13 | .demandCommand(1, 'Provide valid command'); 14 | 15 | export function createIgnoredModulesTable(ignoredModulesConfig, moduleNames = _.keys(ignoredModulesConfig)) { 16 | const rows = moduleNames.map(moduleName => [ 17 | strong(moduleName), 18 | attention(ignoredModulesConfig[moduleName].versions), 19 | ignoredModulesConfig[moduleName].reason 20 | ]); 21 | 22 | // Table header 23 | rows.unshift(['', 'Ignored versions', 'Reason'].map(header => strong(header))); 24 | 25 | return createSimpleTable(rows, {colAligns: 'lcl'}); 26 | } 27 | 28 | export async function askIgnoreFields(defaultVersions) { 29 | return { 30 | versions: await askUser({ 31 | message: 'Input version or version range to ignore', 32 | default: defaultVersions, 33 | validate: input => (semver.validRange(input) ? true : 'Input valid semver version range') 34 | }), 35 | reason: await askUser({message: 'Ignore reason'}) 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/ignore/add.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {Separator} from 'inquirer'; 3 | 4 | import catchAsyncError from '../../catchAsyncError'; 5 | import askUser from '../../askUser'; 6 | import {strong, success, attention} from '../../cliStyles'; 7 | import {createIgnoredModulesTable, askIgnoreFields} from '../ignore'; 8 | import Config from '../../Config'; 9 | import {DEPS_GROUPS, loadPackageJson, getModuleVersion} from '../../packageUtils'; 10 | 11 | export const command = 'add [module]'; 12 | export const describe = 'Add module to ignored list'; 13 | 14 | export const handler = catchAsyncError(async (opts) => { 15 | let {module: moduleName} = opts; 16 | const config = new Config(); 17 | config.ignore = config.ignore || {}; 18 | 19 | console.log( 20 | `Currently ignored modules:\n\n${createIgnoredModulesTable(config.ignore)}\n` 21 | ); 22 | 23 | if (moduleName && !getModuleVersion(moduleName, loadPackageJson().content)) { 24 | console.log(attention( 25 | `Couldn't find module ${strong(moduleName)} in ${strong('package.json')}. Choose existing module.\n` 26 | )); 27 | moduleName = null; 28 | } 29 | 30 | let ignoreMore; 31 | do { 32 | if (!moduleName) { 33 | moduleName = await askUser({ 34 | type: 'list', 35 | message: 'Select module to ignore:', 36 | choices: makeModulesToIgnoreList(config.ignore), 37 | pageSize: 20 38 | }); 39 | } 40 | 41 | config.ignore[moduleName] = await askIgnoreFields('*'); 42 | config.save(); 43 | 44 | console.log( 45 | success(`\nModule ${strong(moduleName)} added to ignored list.\n`) 46 | ); 47 | moduleName = null; 48 | 49 | ignoreMore = await askUser({ 50 | message: 'Do you want to ignore some other module?', 51 | type: 'confirm' 52 | }); 53 | } while (ignoreMore); 54 | }); 55 | 56 | function makeModulesToIgnoreList(ignoredModulesConfig) { 57 | const {content: packageJson} = loadPackageJson(); 58 | const ignoredModules = _.keys(ignoredModulesConfig); 59 | 60 | return _.transform(DEPS_GROUPS, (list, group) => { 61 | const groupModules = _.keys(packageJson[group.field]); 62 | const availableToIgnore = _.difference(groupModules, ignoredModules); 63 | 64 | if (availableToIgnore.length) { 65 | list.push( 66 | new Separator(strong(`--- ${group.field} ---`)), 67 | ...availableToIgnore 68 | ); 69 | } 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /src/commands/ignore/list.js: -------------------------------------------------------------------------------- 1 | import catchAsyncError from '../../catchAsyncError'; 2 | import {createIgnoredModulesTable} from '../ignore'; 3 | import Config from '../../Config'; 4 | 5 | export const command = 'list'; 6 | export const describe = 'Show the list of ignored modules'; 7 | 8 | export const handler = catchAsyncError(async () => { 9 | const config = new Config(); 10 | console.log( 11 | `Currently ignored modules:\n\n${createIgnoredModulesTable(config.ignore)}\n` 12 | ); 13 | }); 14 | -------------------------------------------------------------------------------- /src/commands/ignore/reset.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import catchAsyncError from '../../catchAsyncError'; 4 | import askUser from '../../askUser'; 5 | import {success, strong, attention} from '../../cliStyles'; 6 | import {createIgnoredModulesTable} from '../ignore'; 7 | import Config from '../../Config'; 8 | 9 | export const command = 'reset [modules...]'; 10 | export const describe = 'Reset ignored modules'; 11 | 12 | export const handler = catchAsyncError(async (opts) => { 13 | let {modules: modulesToReset} = opts; 14 | let invalidModules = []; 15 | const config = new Config(); 16 | const ignoredModules = _.keys(config.ignore); 17 | 18 | console.log( 19 | `Currently ignored modules:\n\n${createIgnoredModulesTable(config.ignore)}\n` 20 | ); 21 | 22 | if (modulesToReset.length) { 23 | [modulesToReset, invalidModules] = _.partition(modulesToReset, moduleName => 24 | _.includes(ignoredModules, moduleName) 25 | ); 26 | 27 | if (invalidModules.length) { 28 | console.log(attention( 29 | `These modules are not in the ignored list: ${strong(invalidModules.join(', '))}\n` 30 | )); 31 | } 32 | } 33 | 34 | if (!modulesToReset.length || invalidModules.length) { 35 | modulesToReset = await askUser({ 36 | type: 'checkbox', 37 | message: 'Select ignored modules to reset:', 38 | choices: ignoredModules, 39 | default: modulesToReset 40 | }); 41 | console.log(); 42 | } 43 | 44 | if (!modulesToReset.length) { 45 | return console.log(attention('Nothing to reset')); 46 | } 47 | 48 | console.log( 49 | `These ignored modules will be reset:\n\n${createIgnoredModulesTable(config.ignore, modulesToReset)}\n` 50 | ); 51 | 52 | const confirm = await askUser({ 53 | message: 'Are you sure?', 54 | type: 'confirm', 55 | default: false 56 | }); 57 | 58 | if (!confirm) return; 59 | 60 | config.ignore = _.omit(config.ignore, modulesToReset); 61 | config.save(); 62 | 63 | console.log(success('\nDone!')); 64 | }); 65 | -------------------------------------------------------------------------------- /src/filterUtils.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {flow, split, compact, partition} from 'lodash/fp'; 3 | 4 | export function globToRegexp(glob, flags) { 5 | const regexp = glob 6 | .split(/\*+/) 7 | .map(_.escapeRegExp) 8 | .join('.*?'); 9 | 10 | return new RegExp(`^${regexp}$`, flags); 11 | } 12 | 13 | export function makeFilterFunction(filterStr = '') { 14 | let [excludeFilters, includeFilters] = flow( 15 | split(/\s+/), 16 | compact, 17 | partition(filter => filter[0] === '!') 18 | )(filterStr); 19 | 20 | if (!includeFilters.length) { 21 | includeFilters.push('*'); 22 | } 23 | 24 | includeFilters = includeFilters 25 | .map(filter => globToRegexp(filter, 'i')) 26 | .map(filterRegexp => str => filterRegexp.test(str)); 27 | 28 | excludeFilters = excludeFilters 29 | .map(filter => globToRegexp(filter.slice(1), 'i')) 30 | .map(filterRegexp => str => filterRegexp.test(str)); 31 | 32 | return str => excludeFilters.every(filter => !filter(str)) && includeFilters.some(filter => filter(str)); 33 | } 34 | -------------------------------------------------------------------------------- /src/packageUtils.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import {readFileSync} from 'fs'; 3 | import libnpmconfig from 'libnpmconfig'; 4 | import pacote from 'pacote'; 5 | import shell from 'shelljs'; 6 | 7 | import _ from 'lodash'; 8 | 9 | export const DEPS_GROUPS = [ 10 | {name: 'global', field: 'dependencies', flag: 'g', ncuValue: 'prod'}, 11 | {name: 'production', field: 'dependencies', flag: 'p', ncuValue: 'prod'}, 12 | {name: 'optional', field: 'optionalDependencies', flag: 'o', ncuValue: 'optional'}, 13 | {name: 'development', field: 'devDependencies', flag: 'd', ncuValue: 'dev'}, 14 | {name: 'peer', field: 'peerDependencies', flag: 'r', ncuValue: 'peer'}, 15 | {name: 'bundled', field: 'bundledDependencies', altField: 'bundleDependencies', flag: 'b', ncuValue: 'bundle'} 16 | ]; 17 | 18 | const getNpmConfig = _.memoize(() => { 19 | const config = {}; 20 | 21 | libnpmconfig.read().forEach((value, key) => { 22 | if (typeof value === 'string') { 23 | // Replacing env ${VARS} in strings with the `process.env` values 24 | config[key] = value.replace(/\$\{(.+?)\}/gu, (_, envVar) => 25 | process.env[envVar] 26 | ); 27 | } else { 28 | config[key] = value; 29 | } 30 | }); 31 | 32 | return config; 33 | }); 34 | 35 | export function loadGlobalPackages() { 36 | const res = shell.exec('npm ls -g --depth 0 --json', {silent: true}); 37 | if (res.code !== 0) {throw new Error(`Could not determine global packages: ${res.stderr}`)} 38 | 39 | try { 40 | const {dependencies} = JSON.parse(res); 41 | const content = {dependencies}; 42 | 43 | for (const [pkg, {version}] of Object.entries(dependencies)) {content.dependencies[pkg] = version} 44 | 45 | return {content}; 46 | } catch (err) { 47 | console.error(`Error parsing global packages: ${err.message}`); 48 | process.exit(1); 49 | } 50 | } 51 | 52 | export function loadPackageJson() { 53 | const packageFile = resolve('./package.json'); 54 | let packageJson; 55 | let packageSource; 56 | 57 | try { 58 | packageSource = readFileSync(packageFile, 'utf-8'); 59 | packageJson = JSON.parse(packageSource); 60 | } catch (err) { 61 | console.error(`Error loading package.json: ${err.message}`); 62 | process.exit(1); 63 | } 64 | 65 | return {path: packageFile, content: packageJson, source: packageSource}; 66 | } 67 | 68 | export function findModuleDepsGroup(moduleName, packageJson) { 69 | for (const {field, altField} of DEPS_GROUPS) { 70 | let modules = packageJson[field]; 71 | 72 | if (!modules && altField) { 73 | modules = packageJson[altField]; 74 | } 75 | 76 | if (modules && modules[moduleName]) { 77 | return modules; 78 | } 79 | } 80 | 81 | return null; 82 | } 83 | 84 | export function getModuleVersion(moduleName, packageJson) { 85 | const depsGroup = findModuleDepsGroup(moduleName, packageJson); 86 | 87 | return depsGroup ? depsGroup[moduleName] : null; 88 | } 89 | 90 | export function setModuleVersion(moduleName, newVersion, packageJson) { 91 | const depsGroup = findModuleDepsGroup(moduleName, packageJson); 92 | 93 | if (depsGroup) { 94 | depsGroup[moduleName] = newVersion; 95 | return true; 96 | } else { 97 | return false; 98 | } 99 | } 100 | 101 | export function getModuleHomepage(packageJson) { 102 | return packageJson.homepage || packageJson.url || null; 103 | } 104 | 105 | export const getModuleInfo = _.memoize(async moduleName => 106 | pacote.manifest(moduleName, { 107 | ...getNpmConfig(), 108 | fullMetadata: true 109 | }) 110 | ); 111 | -------------------------------------------------------------------------------- /src/repositoryUtils.js: -------------------------------------------------------------------------------- 1 | import {parse as parseUrl} from 'url'; 2 | 3 | import _ from 'lodash'; 4 | 5 | const KNOWN_REPOSITORIES = { 6 | 'github.com': parsedRepositoryUrl => { 7 | const repositoryId = /^(.+?\/.+?)(?:\/|\.git$|$)/.exec(parsedRepositoryUrl.pathname.slice(1))[1]; 8 | const rootUrl = `https://github.com/${repositoryId}`; 9 | 10 | return { 11 | repositoryId, 12 | fileUrlBuilder: filename => `${rootUrl}/blob/-/${filename}`, 13 | releasesPageUrl: `${rootUrl}/releases` 14 | }; 15 | }, 16 | 'gitlab.com': parsedRepositoryUrl => { 17 | const repositoryId = /^(.+?\/.+?)(?:\/|\.git$|$)/.exec(parsedRepositoryUrl.pathname.slice(1))[1]; 18 | const rootUrl = `https://gitlab.com/${repositoryId}`; 19 | 20 | return { 21 | repositoryId, 22 | fileUrlBuilder: filename => `${rootUrl}/-/blob/master/${filename}`, 23 | releasesPageUrl: `${rootUrl}/-/releases` 24 | }; 25 | } 26 | }; 27 | 28 | export function getRepositoryInfo(repositoryUrl) { 29 | const parsedUrl = parseUrl(repositoryUrl); 30 | const {hostname} = parsedUrl; 31 | 32 | return _.has(KNOWN_REPOSITORIES, hostname) ? KNOWN_REPOSITORIES[hostname](parsedUrl) : null; 33 | } 34 | -------------------------------------------------------------------------------- /src/stringUtils.js: -------------------------------------------------------------------------------- 1 | export function toSentence(items) { 2 | if (items.length <= 1) { 3 | return items[0] || ''; 4 | } 5 | 6 | return items.slice(0, -1).join(', ') + ' and ' + items[items.length - 1]; 7 | } 8 | -------------------------------------------------------------------------------- /tools/addModuleChangelogUrlToDb.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const CHANGELOG_URLS_DB_FILE = path.resolve(__dirname, '../db/changelogUrls.json'); 7 | 8 | const args = process.argv.slice(2); 9 | const moduleName = args[0]; 10 | const changelogUrl = args[1]; 11 | 12 | if (!moduleName || !changelogUrl) { 13 | console.error(`Usage: ./${path.basename(__filename)} `); 14 | process.exit(); 15 | } 16 | 17 | let changelogUrls = require(CHANGELOG_URLS_DB_FILE); 18 | 19 | changelogUrls[moduleName] = changelogUrl; 20 | 21 | // Sorting keys in alphabetic order 22 | changelogUrls = Object.keys(changelogUrls) 23 | .sort() 24 | .reduce(function (newChangelogUrls, moduleName) { 25 | newChangelogUrls[moduleName] = changelogUrls[moduleName]; 26 | return newChangelogUrls; 27 | }, {}); 28 | 29 | fs.writeFileSync(CHANGELOG_URLS_DB_FILE, JSON.stringify(changelogUrls, null, 4) + '\n'); 30 | 31 | console.log(`Changelog URL for "${moduleName}" module set to "${changelogUrl}"`); 32 | --------------------------------------------------------------------------------