├── .dont-break ├── .gitignore ├── .jshintrc ├── .npmrc ├── .travis.yml ├── Gruntfile.js ├── History.md ├── LICENSE-MIT ├── README.md ├── __snapshots__ ├── command-spec.js.snap-shot ├── filter-allowed-spec.js.snap-shot ├── registry-spec.js.snap-shot └── stats-spec.js.snap-shot ├── bin └── next-update.js ├── circle.yml ├── complexity.json ├── docs ├── README.tmpl.md ├── api.md ├── badges.md ├── dev.md ├── footer.md ├── related.md ├── third-party.md └── use.md ├── examples └── qunit │ ├── README.md │ └── test.js ├── index.js ├── package.json ├── renovate.json ├── src ├── cli-options.js ├── dependencies.js ├── exec-test.js ├── filter-allowed-spec.js ├── filter-allowed-updates.js ├── get-known-dependencies.js ├── local-module-version.js ├── module-install.js ├── moduleName.js ├── next-update-as-module.js ├── next-update.js ├── npm-test.js ├── print-modules-table.js ├── registry-spec.js ├── registry.js ├── report-available.js ├── report-install-command.js ├── report.js ├── revert.js ├── stats-spec.js ├── stats.js ├── test-module-version.js ├── test │ ├── all-local-deps.json │ ├── exec-test.coffee │ ├── get-known-dependencies.coffee │ ├── git-url-deps.json │ ├── local-module-version.coffee │ ├── moduleName.coffee │ ├── node_modules │ │ └── sample-module │ │ │ └── package.json │ ├── npm-test.coffee │ ├── registry.js │ ├── report-available.coffee │ ├── report-install-command.coffee │ ├── report.coffee │ └── stats.coffee └── utils.js └── test ├── as-parent-spec.js ├── as-parent.coffee ├── command-spec.js ├── e2e.coffee ├── file-deps-spec.js ├── revert-spec.js ├── scoped-packages-spec.js ├── single-module-check.coffee ├── test-command-map ├── .npmrc ├── __snapshots__ │ └── command-spec.js.snap-shot ├── lodash-test.js └── package.json ├── test-file-deps ├── .npmrc ├── index.js ├── local-dependency │ ├── index.js │ └── package.json └── package.json ├── test-next-updater ├── .npmrc ├── __snapshots__ │ └── as-parent-spec.js.snap-shot ├── index.js └── package.json ├── test-scoped-names ├── .npmrc ├── index.js └── package.json └── time-limit-promise.js /.dont-break: -------------------------------------------------------------------------------- 1 | next-updater 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/test/cover 3 | /cover 4 | /test/cover 5 | /src/test/npm-debug.log 6 | .idea/ 7 | *.eml 8 | *.iml 9 | .DS_Store 10 | node_modules/ 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, 3 | 4 | "indent" : 2, 5 | "smarttabs" : true, 6 | "bitwise" : true, 7 | "camelcase" : false, 8 | "curly" : true, 9 | "eqeqeq" : true, 10 | "forin" : true, 11 | "immed" : false, 12 | "latedef" : false, 13 | "newcap" : true, 14 | "noarg" : true, 15 | "noempty" : true, 16 | "nonew" : false, 17 | "plusplus" : false, 18 | "quotmark" : "single", 19 | "undef" : true, 20 | "unused" : true, 21 | "strict" : false, 22 | "trailing" : false, 23 | "maxparams" : 4, 24 | "maxdepth" : 3, 25 | "maxstatements" : 30, 26 | "maxcomplexity" : 10, 27 | "maxlen" : 100, 28 | 29 | "asi" : false, 30 | "boss" : false, 31 | "debug" : false, 32 | "eqnull" : false, 33 | "es5" : false, 34 | "esnext" : false, 35 | "moz" : false, 36 | "evil" : false, 37 | "expr" : false, 38 | "funcscope" : false, 39 | "globalstrict" : false, 40 | "iterator" : false, 41 | "lastsemic" : false, 42 | "laxbreak" : false, 43 | "laxcomma" : false, 44 | "loopfunc" : false, 45 | "multistr" : false, 46 | "proto" : false, 47 | "scripturl" : false, 48 | "shadow" : false, 49 | "sub" : false, 50 | "supernew" : false, 51 | "validthis" : false, 52 | 53 | "browser" : false, 54 | "couch" : false, 55 | "devel" : true, 56 | "dojo" : false, 57 | "jquery" : false, 58 | "mootools" : false, 59 | "node" : true, 60 | "nonstandard" : false, 61 | "prototypejs" : false, 62 | "rhino" : false, 63 | "worker" : false, 64 | "wsh" : false, 65 | "yui" : false, 66 | 67 | "nomen" : false, 68 | "onevar" : false, 69 | "passfail" : false, 70 | "white" : true, 71 | 72 | "globals" : {} 73 | } 74 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: true 8 | node_js: 9 | - '8' 10 | before_script: 11 | - npm prune 12 | script: 13 | - npm test 14 | - npm run e2e 15 | # - $(npm bin)/stop-build 16 | after_success: 17 | - npm run semantic-release 18 | - npm run coveralls 19 | branches: 20 | except: 21 | - "/^v\\d+\\.\\d+\\.\\d+$/" 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module:false */ 2 | module.exports = function (grunt) { 3 | require('time-grunt')(grunt) 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | lineending: { 7 | index: { 8 | options: { 9 | eol: 'lf' 10 | }, 11 | files: { 12 | 'index.js': 'index.js' 13 | } 14 | } 15 | }, 16 | jsonlint: { 17 | all: { 18 | src: ['*.json'] 19 | } 20 | }, 21 | 'nice-package': { 22 | all: { 23 | options: { 24 | blankLine: true 25 | } 26 | } 27 | }, 28 | 29 | // TODO fix help, requires grunt-help fix to 30 | // work with optimist 31 | help: { 32 | options: { 33 | destination: 'docs/help.md' 34 | }, 35 | all: {} 36 | }, 37 | 38 | readme: { 39 | options: { 40 | readme: './docs/README.tmpl.md', 41 | docs: '.', 42 | templates: './docs' 43 | } 44 | }, 45 | /* to bump version, then run grunt (to update readme), then commit 46 | grunt release 47 | */ 48 | bump: { 49 | options: { 50 | commit: true, 51 | commitMessage: 'Release v%VERSION%', 52 | commitFiles: ['-a'], // '-a' for all files 53 | createTag: true, 54 | tagName: '%VERSION%', 55 | tagMessage: 'Version %VERSION%', 56 | push: true, 57 | pushTo: 'origin' 58 | } 59 | } 60 | }) 61 | 62 | var plugins = require('matchdep').filterDev('grunt-*') 63 | plugins.forEach(grunt.loadNpmTasks) 64 | 65 | grunt.registerTask('pre-check', ['deps-ok', 'jsonlint', 'nice-package']) 66 | // if readme task crashes with error 67 | // TypeError: Cannot read property '1' of null 68 | // this is because it cannot parse package version "0.0.0-semantic-release" 69 | grunt.registerTask('default', ['pre-check', 'lineending', 'readme']) 70 | grunt.registerTask('release', ['bump-only:patch', 'readme', 'bump-commit']) 71 | } 72 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.2.0 / 2013-12-28 3 | ================== 4 | 5 | * using color in table, fixes #16 6 | * reporting install success for available modules, fixes #13 7 | * printing test command, fixes #15 8 | * added -h as alias to --help 9 | * running test before starting checking new versions, fixes #10 10 | * disabled error messages for 404 stats, fixes #12 11 | * running deps-ok first, fixes #9 12 | * rounding to percent, added link to http://npmt.abru.pt/ 13 | 14 | 0.1.0 / 2013-12-25 15 | ================== 16 | 17 | * added sending and getting global update success statistics 18 | * Add a Bitdeli badge to README 19 | 20 | 0.0.16 / 2013-11-05 21 | ================== 22 | 23 | * grabbing npm registry url only once, seems to fix #5 24 | 25 | 0.0.15 / 2013-10-31 26 | ================== 27 | 28 | * getting npm registry from config, fixes #2 29 | * bumped dependencies 30 | 31 | 0.0.14 / 2013-10-31 32 | ================== 33 | 34 | * updated for latest check-types 35 | 36 | 0.0.13 / 2013-10-29 37 | ================== 38 | 39 | * bumped check-types 40 | * bumped some versions 41 | * updated deps 42 | * added pre-git 43 | * added jsonlint 44 | * moved jshint options into separate file 45 | * added pre-commit check 46 | * using matchdep 47 | * cleaned up gruntfile 48 | * Release v0.0.12 49 | * removed console.log that was generating a lot of output 50 | * Release v0.0.11 51 | * handling case when nothing can be updated, fixes #59 52 | * Release v0.0.10 53 | * using npm-tools to check urls 54 | * fixed jshint 55 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Gleb Bahmutov 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # next-update 2 | 3 | > Tests if module's dependencies can be updated to the newer version without breaking the tests 4 | 5 | [![NPM][next-update-icon] ][next-update-url] 6 | 7 | [![Build status][next-update-ci-image] ][next-update-ci-url] 8 | [![Circle CI][circle-ci-image] ][circle-ci-url] 9 | [![Coverage Status][next-update-coverage-image] ][next-update-coverage-url] 10 | [![semantic-release][semantic-image] ][semantic-url] 11 | [![Known Vulnerabilities](https://snyk.io/test/github/bahmutov/next-update/230d136b5c68dadb1fd5459619df8f7678d28429/badge.svg)](https://snyk.io/test/github/bahmutov/next-update/230d136b5c68dadb1fd5459619df8f7678d28429) 12 | [![renovate-app badge][renovate-badge]][renovate-app] 13 | 14 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 15 | [renovate-app]: https://renovateapp.com/ 16 | 17 | [next-update-icon]: https://nodei.co/npm/next-update.svg?downloads=true 18 | [next-update-url]: https://npmjs.org/package/next-update 19 | [next-update-ci-image]: https://travis-ci.org/bahmutov/next-update.svg?branch=master 20 | [next-update-ci-url]: https://travis-ci.org/bahmutov/next-update 21 | [next-update-coverage-image]: https://coveralls.io/repos/bahmutov/next-update/badge.svg 22 | [next-update-coverage-url]: https://coveralls.io/r/bahmutov/next-update 23 | [circle-ci-image]: https://circleci.com/gh/bahmutov/next-update.svg?style=svg 24 | [circle-ci-url]: https://circleci.com/gh/bahmutov/next-update 25 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 26 | [semantic-url]: https://github.com/semantic-release/semantic-release 27 | 28 | 29 | 30 | **Note** I no longer maintain Node 0.12/4 compatibility. Please switch to 31 | Node 6. 32 | 33 | [![asciicast](https://asciinema.org/a/21645.png)](https://asciinema.org/a/21645) 34 | 35 | Also check out: 36 | 37 | * [next-updater](https://github.com/bahmutov/next-updater) can update all your repos 38 | * [dont-break](https://github.com/bahmutov/dont-break) 39 | that checks if your code is going to break everyone who depends on it. 40 | * [changed-log](https://github.com/bahmutov/changed-log) returns commit messages for 41 | the given NPM package or Github repo between two tags. 42 | 43 | ### Example 44 | 45 | Imagine your nodejs module *foo* has the following dependencies listed in *package.json* 46 | 47 | "dependencies": { 48 | "lodash": "~1.2.0", 49 | "async": "~0.2.5" 50 | } 51 | 52 | You would like to update lodash and async to latest versions, to not sure if 53 | this would break anything. With *next-update* it is easy: run command `next-update` 54 | in the folder with module *foo*. Here is the example output: 55 | 56 | next updates: 57 | lodash 58 | 1.2.1 PASS 59 | async 60 | 0.2.6 PASS 61 | 0.2.7 PASS 62 | 0.2.8 PASS 63 | 64 | 65 | Both *package.json* file and *node_modules* folder are left unchanged, 66 | and now you know that you can safely upgrade both libraries to later versions. 67 | 68 | #### It even tells you the install command ;) 69 | 70 | Use the following command to install working versions 71 | npm install --save lodash@2.1.0 72 | 73 | This might not appear like a big deal for a single module that is using 74 | popular 3rd party libraries with stable apis only. *next-update* is most useful 75 | in the larger development context, where multiple modules are being developed 76 | side by side, often by different teams. In such situations, checking if an upgrade 77 | is possible could be part of the continuous build pipeline. 78 | 79 | You can see if your dependencies are out of date by using 80 | [david](https://david-dm.org), 81 | it even has badges you can add to your README files. 82 | 83 | *next-update* reports the probability of success for a given dependency update using 84 | anonymous global statistics from [next-update](http://next-update.herokuapp.com/) server 85 | 86 | ``` 87 | available updates: 88 | package available from version average success % successful updates failed updates 89 | -------------------- --------- ------------ ----------------- ------------------ -------------- 90 | grunt-contrib-jshint 0.8.0 0.7.2 100% 34 0 91 | grunt-bump 0.0.13 0.0.12 100% 4 0 92 | ``` 93 | 94 | ### Install 95 | 96 | You can install this tool globally 97 | 98 | npm install -g next-update // installs module globally 99 | next-update --help // shows command line options 100 | 101 | Then run inside any package folder 102 | 103 | /git/my-awesome-module 104 | $ next-update 105 | 106 | Or you can use this module as a devDependency and a script command 107 | 108 | npm install --save-dev next-update 109 | 110 | ```json 111 | { 112 | "scripts": { 113 | "next-update": "next-update -k true --tldr" 114 | } 115 | } 116 | ``` 117 | 118 | This command will keep the successfuly version upgrades in the package.json file, 119 | but will not be very verbose when run. 120 | 121 | ### Anonymous usage collection 122 | 123 | After testing each module A upgrade from version X to Y, *next-update* sends 124 | anonymous result to [next-update.herokuapp.com/](http://next-update.herokuapp.com/). 125 | The only information transmitted is: 126 | 127 | ```json 128 | { 129 | "name": "lodash", 130 | "from": "1.0.0", 131 | "to": "2.0.0", 132 | "success": true 133 | } 134 | ``` 135 | 136 | This information is used to answer the following questions later: 137 | what is the probability module A can be upgraded from X to Y? 138 | Thus even if you do not have tests covering this particular module, 139 | you can judge how compatible version X and Y really are over the entire 140 | internet. 141 | 142 | You can inspect data send in 143 | [stats.js](https://github.com/bahmutov/next-update/blob/master/src/stats.js). 144 | 145 | If the dependency module has been upgraded by anyone else, its statistics 146 | will be displayed with each test. 147 | 148 | ```sh 149 | stats: deps-ok 0.0.7 -> 0.0.8 success probability 44.44% 8 success(es) 10 failure(s) 150 | ``` 151 | 152 | A lot of NPM modules [do not have tests](http://npmt.abru.pt/), but 153 | at least you can judge if someone else has success going from verion X to version Y 154 | of a dependency. 155 | 156 | ### Use 157 | 158 | Make sure the target module has unit / integration tests, 159 | and the tests can be run using `npm test` command. 160 | 161 | Run `next-update` from the command line in the same folder as 162 | the target module. In general this tool does the following: 163 | 164 | 1. Reads the module's dependencies (including dev) and their versions 165 | 2. Queries npm registry to see if there are newer versions 166 | 3. For each dependency that has newer versions available: 167 | 1. Installs each version 168 | 2. Runs command `npm test` to determine if the new version breaks the tests 169 | 3. Installs back the current version. 170 | 4. Reports results 171 | 172 | ### Checking specific modules 173 | 174 | You can check one or more specific modules (whitelist) using CLI flag 175 | `--module` or `-m` 176 | 177 | ```sh 178 | next-update --module foo,bar,baz 179 | ``` 180 | 181 | ### Ignoring or skipping some modules 182 | 183 | **note** [prerelease](https://github.com/npm/node-semver#functions) 184 | versions like `1.2.0-alpha` are skipped by default. I believe `next-update` is 185 | meant to upgrade to *stable* versions. 186 | 187 | Some modules are hard to unit test, thus the automatic upgrades are not appropriate. 188 | For example [benv](https://npmjs.org/package/benv) upgrade brings a new 189 | [jsdom](https://npmjs.org/package/jsdom) version, which does not work on Node 0.12 190 | Similarly, upgrading [Q](https://npmjs.org/package/q) from 1.x.x to 2.x.x is usually 191 | a breaking change. 192 | 193 | You can skip a list of modules by name using `config` property in the `package.json` 194 | 195 | ```json 196 | "config": { 197 | "next-update": { 198 | "skip": ["benv", "q"] 199 | } 200 | } 201 | ``` 202 | 203 | ### Custom test command per module 204 | 205 | Some modules are not really tested using the default `npm test` command or 206 | whatever is passed via `--test "..."` from CLI. For example a linter module 207 | should probably be tested using `npm run lint` command. You can set individual 208 | test commands for each module to override the default test command. In the 209 | `package.json` config object set "commands" object 210 | 211 | ```json 212 | "config": { 213 | "next-update": { 214 | "commands": { 215 | "git-issues": "npm run issues", 216 | "standard": "npm run lint" 217 | } 218 | } 219 | } 220 | ``` 221 | 222 | Then when `git-issues` module is checked by itself, it will run 223 | `npm run issues` command; when module `standard` is tested by itself, the 224 | test will use `npm run lint` command. 225 | 226 | ### Misc 227 | 228 | * To see what has changed in the latest version of any module, 229 | use my companion tool [changed](https://npmjs.org/package/changed) 230 | like this `changed foo` (*foo* is package name) 231 | * When comparing versions, keywords *latest* and *** are both assumed to equal to "0.0.0". 232 | * A good workflow using *next-update* 233 | * see available new versions `next-update --available` 234 | * check latest version of each module using `next-update --latest` 235 | * install new versions of the desired modules using standard `npm i dependency@version --save` 236 | * You can use custom test command, for example `next-update -t "grunt test"` 237 | * `npm test` is used by default. 238 | * You can keep each working version in package.json by using `--keep` flag. 239 | 240 | 241 | 242 | ## API 243 | 244 | You can use `next-update` as a module. See file 245 | [src/next-update-as-module](./src/next-update-as-module) for all options. 246 | 247 | ```js 248 | const nextUpdate = require('next-update') 249 | nextUpdate({ 250 | module: ['foo', 'bar'] 251 | }).then(results => { 252 | console.log(results) 253 | }) 254 | /* 255 | prints something like 256 | [[ 257 | { 258 | "name": "foo", 259 | "version": "0.2.0", 260 | "from": "0.2.1", 261 | "works": true 262 | }, 263 | { 264 | "name": "foo", 265 | "version": "0.2.0", 266 | "from": "0.3.0", 267 | "works": false 268 | } 269 | ], [ 270 | { 271 | "name": "bar", 272 | "version": "1.5.1", 273 | "from": "2.0.0", 274 | "works": true 275 | } 276 | }} 277 | */ 278 | ``` 279 | 280 | 281 | 282 | ## Development 283 | 284 | Edit source, run unit tests, run end to end tests and release 285 | new version commands: 286 | 287 | ```sh 288 | npm test 289 | npm run e2e 290 | grunt release 291 | npm publish 292 | ``` 293 | 294 | 295 | ### Related 296 | 297 | * [Painless modular development](http://glebbahmutov.com/blog/modular-development-using-nodejs/) 298 | * [Really painless modular development](http://glebbahmutov.com/blog/really-painless-modular-development/) 299 | 300 | 301 | 302 | ### 3rd party libraries 303 | 304 | * [lazy-ass](https://github.com/bahmutov/lazy-ass) and 305 | [check-more-types](https://github.com/kensho/check-more-types) are used to 306 | [defend against runtime errors](http://glebbahmutov.com/blog/lazy-and-async-assertions/). 307 | * [lo-dash](https://github.com/bestiejs/lodash) is used to deal with collections / iterators. 308 | * [check-types](https://github.com/philbooth/check-types.js) is used to verify arguments through out the code. 309 | * [optimist](https://github.com/substack/node-optimist) is used to process command line arguments. 310 | * [request](https://npmjs.org/package/request) is used to fetch NPM registry information. 311 | * [semver](https://npmjs.org/package/semver) is used to compare module version numbers. 312 | * [q](https://npmjs.org/package/q) library is used to handle promises. While developing this tool, 313 | I quickly ran into problems managing the asynchronous nature of fetching information, installing multiple modules, 314 | testing, etc. At first I used [async](https://npmjs.org/package/async), but it was still too complex. 315 | Using promises allowed to cut the program's code and the complexity to very manageable level. 316 | * [cli-color](https://npmjs.org/package/cli-color) prints colored text to the terminal. 317 | 318 | 319 | 320 | ### Small print 321 | 322 | Author: Gleb Bahmutov © 2014 323 | 324 | * [@bahmutov](https://twitter.com/bahmutov) 325 | * [glebbahmutov.com](https://glebbahmutov.com) 326 | * [blog](https://glebbahmutov.com/blog) 327 | 328 | License: MIT - do anything with the code, but don't blame me if it does not work. 329 | 330 | Spread the word: tweet, star on github, etc. 331 | 332 | Support: if you find any problems with this module, email / tweet / 333 | [open issue](https://github.com/bahmutov/next-update/issues?state=open) on Github 334 | 335 | 336 | 337 | ## MIT License 338 | 339 | Copyright (c) 2014 Gleb Bahmutov 340 | 341 | Permission is hereby granted, free of charge, to any person 342 | obtaining a copy of this software and associated documentation 343 | files (the "Software"), to deal in the Software without 344 | restriction, including without limitation the rights to use, 345 | copy, modify, merge, publish, distribute, sublicense, and/or sell 346 | copies of the Software, and to permit persons to whom the 347 | Software is furnished to do so, subject to the following 348 | conditions: 349 | 350 | The above copyright notice and this permission notice shall be 351 | included in all copies or substantial portions of the Software. 352 | 353 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 354 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 355 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 356 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 357 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 358 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 359 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 360 | OTHER DEALINGS IN THE SOFTWARE. 361 | 362 | 363 | -------------------------------------------------------------------------------- /__snapshots__/command-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['sorts by version 1'] = [ 2 | { 3 | "name": "lodash", 4 | "version": "1.0.1", 5 | "from": "1.0.0", 6 | "works": true 7 | }, 8 | { 9 | "name": "lodash", 10 | "version": "1.0.2", 11 | "from": "1.0.0", 12 | "works": true 13 | }, 14 | { 15 | "name": "lodash", 16 | "version": "1.1.0", 17 | "from": "1.0.0", 18 | "works": false 19 | }, 20 | { 21 | "name": "lodash", 22 | "version": "1.1.1", 23 | "from": "1.0.0", 24 | "works": false 25 | }, 26 | { 27 | "name": "lodash", 28 | "version": "1.2.0", 29 | "from": "1.0.0", 30 | "works": false 31 | }, 32 | { 33 | "name": "lodash", 34 | "version": "1.2.1", 35 | "from": "1.0.0", 36 | "works": false 37 | }, 38 | { 39 | "name": "lodash", 40 | "version": "1.3.0", 41 | "from": "1.0.0", 42 | "works": false 43 | }, 44 | { 45 | "name": "lodash", 46 | "version": "1.3.1", 47 | "from": "1.0.0", 48 | "works": false 49 | } 50 | ] 51 | 52 | -------------------------------------------------------------------------------- /__snapshots__/filter-allowed-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['allows major 1'] = { 2 | "current": { 3 | "q": { 4 | "name": "q", 5 | "version": "1.1.1", 6 | "type": "dev" 7 | } 8 | }, 9 | "options": { 10 | "allowed": "major" 11 | }, 12 | "allowed": [ 13 | { 14 | "name": "q", 15 | "versions": [ 16 | "1.3.0", 17 | "2.0.2", 18 | "3.0.0" 19 | ] 20 | } 21 | ] 22 | } 23 | 24 | exports['allows minor 1'] = { 25 | "current": { 26 | "q": { 27 | "name": "q", 28 | "version": "1.1.1", 29 | "type": "dev" 30 | } 31 | }, 32 | "options": { 33 | "allowed": "minor" 34 | }, 35 | "allowed": [ 36 | { 37 | "name": "q", 38 | "versions": [ 39 | "1.3.0" 40 | ] 41 | } 42 | ] 43 | } 44 | 45 | exports['allows patch 1'] = { 46 | "current": { 47 | "q": { 48 | "name": "q", 49 | "version": "1.1.1", 50 | "type": "dev" 51 | } 52 | }, 53 | "options": { 54 | "allowed": "patch" 55 | }, 56 | "allowed": [] 57 | } 58 | 59 | exports['allows everything by default 1'] = { 60 | "current": { 61 | "q": { 62 | "name": "q", 63 | "version": "1.1.1", 64 | "type": "dev" 65 | } 66 | }, 67 | "options": {}, 68 | "allowed": [ 69 | { 70 | "name": "q", 71 | "versions": [ 72 | "1.3.0", 73 | "2.0.2", 74 | "3.0.0" 75 | ] 76 | } 77 | ] 78 | } 79 | 80 | exports['can custom filter 1'] = { 81 | "current": { 82 | "q": { 83 | "name": "q", 84 | "version": "1.1.1", 85 | "type": "dev" 86 | } 87 | }, 88 | "options": {}, 89 | "allowed": [ 90 | { 91 | "name": "q", 92 | "versions": [ 93 | "2.0.2" 94 | ] 95 | } 96 | ] 97 | } 98 | 99 | exports['detects prerelease 1'] = { 100 | "name": "isPrerelease", 101 | "behavior": [ 102 | { 103 | "given": "3.0.0-alpha", 104 | "expect": true 105 | }, 106 | { 107 | "given": "3.0.0", 108 | "expect": false 109 | }, 110 | { 111 | "given": "0.1.0", 112 | "expect": false 113 | }, 114 | { 115 | "given": "10.0.0-beta.2", 116 | "expect": true 117 | } 118 | ] 119 | } 120 | 121 | -------------------------------------------------------------------------------- /__snapshots__/registry-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['hasVersions 1'] = [ 2 | { 3 | "name": "foo", 4 | "versions": [ 5 | "1.0.0", 6 | "1.0.1" 7 | ] 8 | }, 9 | { 10 | "name": "baz", 11 | "versions": [ 12 | "3.0.0-alpha" 13 | ] 14 | } 15 | ] 16 | 17 | exports['filters out empty versions 1'] = [ 18 | { 19 | "name": "foo", 20 | "versions": [ 21 | "1.0.0", 22 | "1.0.1" 23 | ] 24 | } 25 | ] 26 | 27 | exports['filters latest versions 1'] = [ 28 | { 29 | "name": "foo", 30 | "versions": [ 31 | "1.0.1" 32 | ] 33 | } 34 | ] 35 | 36 | exports['filters out alpha releases 1'] = [ 37 | { 38 | "name": "foo", 39 | "versions": [ 40 | "1.0.0", 41 | "1.0.1" 42 | ] 43 | } 44 | ] 45 | 46 | -------------------------------------------------------------------------------- /__snapshots__/stats-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['formats stats message 1'] = "stats: foo 1.0.0 -> 2.0.0 success probability 50% 5 successes 5 failures" 2 | 3 | exports['formats stats message with singular failure 1'] = "stats: foo 1.0.0 -> 2.0.0 success probability 50% 1 success 1 failure" 4 | 5 | -------------------------------------------------------------------------------- /bin/next-update.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var q = require('q') 4 | q.longStackSupport = true 5 | 6 | const debug = require('debug')('next-update') 7 | var nextUpdate = require('../src/next-update') 8 | var program = require('../src/cli-options') 9 | var join = require('path').join 10 | 11 | var parentFolder = join(__dirname, '..') 12 | var pkg = require(join(parentFolder, 'package.json')) 13 | var info = 14 | pkg.name + 15 | '@' + 16 | pkg.version + 17 | '\n' + 18 | ' - ' + 19 | pkg.description + 20 | '\n' + 21 | ' installed in folder ' + 22 | parentFolder 23 | 24 | var report = require('../src/report').report 25 | var revert = require('../src/revert') 26 | 27 | if (program.available) { 28 | nextUpdate 29 | .available(program.module, { 30 | color: program.color 31 | }) 32 | .done() 33 | } else if (program.revert) { 34 | debug('reverting %s', program.module ? program.module : 'all modules') 35 | revert(program.module).then( 36 | function () { 37 | console.log('done reverting') 38 | }, 39 | function (error) { 40 | console.error('error while reverting\n', error) 41 | } 42 | ) 43 | } else { 44 | if (!program.tldr) { 45 | console.log(info) 46 | } 47 | 48 | var updateNotifier = require('update-notifier') 49 | updateNotifier({ 50 | pkg: pkg, 51 | name: pkg.name, 52 | version: pkg.version 53 | }).notify() 54 | 55 | const allow = program.allowed || program.allow 56 | const latest = program.allowWasSpecified ? false : program.latest 57 | debug( 58 | 'allow was specified %j option "%s" latest %j', 59 | program.allowWasSpecified, 60 | allow, 61 | latest 62 | ) 63 | 64 | var opts = { 65 | names: program.module, 66 | latest: latest, 67 | testCommand: program.test, 68 | all: program.all, 69 | color: program.color, 70 | keep: program.keep, 71 | allow: allow, 72 | type: program.type, 73 | tldr: program.tldr, 74 | without: program.without, 75 | registry: program.registry, 76 | checkVersionTimeout: program['check-version-timeout'] || 10000 77 | } 78 | 79 | var checkCurrent = nextUpdate.checkCurrentInstall.bind(null, opts) 80 | var checkCurrentState = program.skip ? q : checkCurrent 81 | var checkUpdates = nextUpdate.checkAllUpdates.bind(null, opts) 82 | 83 | var reportTestResults = function reportTestResults (results) { 84 | if (Array.isArray(results)) { 85 | return report(results, { 86 | useColors: program.color, 87 | keptUpdates: program.keep, 88 | changedLog: program['changed-log'] 89 | }) 90 | } 91 | } 92 | 93 | checkCurrentState() 94 | .then(checkUpdates) 95 | .then(reportTestResults) 96 | .catch(function (error) { 97 | console.error('ERROR testing next working updates') 98 | if (error.stack) { 99 | console.error(error.stack) 100 | } 101 | process.exit(1) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 4 | deployment: 5 | npm: 6 | branch: master 7 | commands: 8 | # login using environment variables 9 | - echo -e "$NPM_USERNAME\n$NPM_PASSWORD\n$NPM_EMAIL" | npm login 10 | - npm run 2npm 11 | -------------------------------------------------------------------------------- /complexity.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": ["!Gruntfile.js", "src/*.js", "!src/cli-options.js"], 3 | "options": { 4 | "errorsOnly": false, 5 | "cyclomatic": 7, 6 | "halstead": 18, 7 | "maintainability": 100 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/README.tmpl.md: -------------------------------------------------------------------------------- 1 | # {%= name %} 2 | 3 | > {%= description %} 4 | 5 | {%= _.doc("./docs/badges.md") %} 6 | 7 | **Note** I no longer maintain Node 0.12/4 compatibility. Please switch to 8 | Node 6. 9 | 10 | [![asciicast](https://asciinema.org/a/21645.png)](https://asciinema.org/a/21645) 11 | 12 | {%= _.doc("./docs/use.md") %} 13 | 14 | {%= _.doc("./docs/api.md") %} 15 | 16 | {%= _.doc("./docs/dev.md") %} 17 | 18 | {%= _.doc("./docs/related.md") %} 19 | 20 | {%= _.doc("./docs/third-party.md") %} 21 | 22 | {%= _.doc("./docs/footer.md") %} 23 | 24 | ## MIT License 25 | 26 | {%= _.doc("./LICENSE-MIT") %} 27 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | You can use `next-update` as a module. See file 4 | [src/next-update-as-module](./src/next-update-as-module) for all options. 5 | 6 | ```js 7 | const nextUpdate = require('next-update') 8 | nextUpdate({ 9 | module: ['foo', 'bar'] 10 | }).then(results => { 11 | console.log(results) 12 | }) 13 | /* 14 | prints something like 15 | [[ 16 | { 17 | "name": "foo", 18 | "version": "0.2.0", 19 | "from": "0.2.1", 20 | "works": true 21 | }, 22 | { 23 | "name": "foo", 24 | "version": "0.2.0", 25 | "from": "0.3.0", 26 | "works": false 27 | } 28 | ], [ 29 | { 30 | "name": "bar", 31 | "version": "1.5.1", 32 | "from": "2.0.0", 33 | "works": true 34 | } 35 | ]] 36 | */ 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/badges.md: -------------------------------------------------------------------------------- 1 | [![NPM][next-update-icon] ][next-update-url] 2 | 3 | [![Build status][next-update-ci-image] ][next-update-ci-url] 4 | [![Circle CI][circle-ci-image] ][circle-ci-url] 5 | [![Coverage Status][next-update-coverage-image] ][next-update-coverage-url] 6 | [![semantic-release][semantic-image] ][semantic-url] 7 | [![Known Vulnerabilities](https://snyk.io/test/github/bahmutov/next-update/230d136b5c68dadb1fd5459619df8f7678d28429/badge.svg)](https://snyk.io/test/github/bahmutov/next-update/230d136b5c68dadb1fd5459619df8f7678d28429) 8 | 9 | [next-update-icon]: https://nodei.co/npm/next-update.svg?downloads=true 10 | [next-update-url]: https://npmjs.org/package/next-update 11 | [next-update-ci-image]: https://travis-ci.org/bahmutov/next-update.svg?branch=master 12 | [next-update-ci-url]: https://travis-ci.org/bahmutov/next-update 13 | [next-update-coverage-image]: https://coveralls.io/repos/bahmutov/next-update/badge.svg 14 | [next-update-coverage-url]: https://coveralls.io/r/bahmutov/next-update 15 | [circle-ci-image]: https://circleci.com/gh/bahmutov/next-update.svg?style=svg 16 | [circle-ci-url]: https://circleci.com/gh/bahmutov/next-update 17 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 18 | [semantic-url]: https://github.com/semantic-release/semantic-release 19 | -------------------------------------------------------------------------------- /docs/dev.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Edit source, run unit tests, run end to end tests and release 4 | new version commands: 5 | 6 | ```sh 7 | npm test 8 | npm run e2e 9 | grunt release 10 | npm publish 11 | ``` -------------------------------------------------------------------------------- /docs/footer.md: -------------------------------------------------------------------------------- 1 | ## Small print 2 | 3 | Author: Gleb Bahmutov © 2014 4 | 5 | * [@bahmutov](https://twitter.com/bahmutov) 6 | * [glebbahmutov.com](https://glebbahmutov.com) 7 | * [blog](https://glebbahmutov.com/blog) 8 | 9 | License: MIT - do anything with the code, but don't blame me if it does not work. 10 | 11 | Spread the word: tweet, star on github, etc. 12 | 13 | Support: if you find any problems with this module, email / tweet / 14 | [open issue](https://github.com/bahmutov/next-update/issues?state=open) on Github 15 | -------------------------------------------------------------------------------- /docs/related.md: -------------------------------------------------------------------------------- 1 | ## Related 2 | 3 | * [Painless modular development](http://glebbahmutov.com/blog/modular-development-using-nodejs/) 4 | * [Really painless modular development](http://glebbahmutov.com/blog/really-painless-modular-development/) 5 | -------------------------------------------------------------------------------- /docs/third-party.md: -------------------------------------------------------------------------------- 1 | ## 3rd party libraries 2 | 3 | * [lazy-ass](https://github.com/bahmutov/lazy-ass) and 4 | [check-more-types](https://github.com/kensho/check-more-types) are used to 5 | [defend against runtime errors](http://glebbahmutov.com/blog/lazy-and-async-assertions/). 6 | * [lo-dash](https://github.com/bestiejs/lodash) is used to deal with collections / iterators. 7 | * [check-types](https://github.com/philbooth/check-types.js) is used to verify arguments through out the code. 8 | * [optimist](https://github.com/substack/node-optimist) is used to process command line arguments. 9 | * [request](https://npmjs.org/package/request) is used to fetch NPM registry information. 10 | * [semver](https://npmjs.org/package/semver) is used to compare module version numbers. 11 | * [q](https://npmjs.org/package/q) library is used to handle promises. While developing this tool, 12 | I quickly ran into problems managing the asynchronous nature of fetching information, installing multiple modules, 13 | testing, etc. At first I used [async](https://npmjs.org/package/async), but it was still too complex. 14 | Using promises allowed to cut the program's code and the complexity to very manageable level. 15 | * [cli-color](https://npmjs.org/package/cli-color) prints colored text to the terminal. 16 | -------------------------------------------------------------------------------- /docs/use.md: -------------------------------------------------------------------------------- 1 | Also check out: 2 | 3 | * [next-updater](https://github.com/bahmutov/next-updater) can update all your repos 4 | * [dont-break](https://github.com/bahmutov/dont-break) 5 | that checks if your code is going to break everyone who depends on it. 6 | * [changed-log](https://github.com/bahmutov/changed-log) returns commit messages for 7 | the given NPM package or Github repo between two tags. 8 | 9 | ## Example 10 | 11 | Imagine your nodejs module *foo* has the following dependencies listed in *package.json* 12 | 13 | "dependencies": { 14 | "lodash": "~1.2.0", 15 | "async": "~0.2.5" 16 | } 17 | 18 | You would like to update lodash and async to latest versions, to not sure if 19 | this would break anything. With *next-update* it is easy: run command `next-update` 20 | in the folder with module *foo*. Here is the example output: 21 | 22 | next updates: 23 | lodash 24 | 1.2.1 PASS 25 | async 26 | 0.2.6 PASS 27 | 0.2.7 PASS 28 | 0.2.8 PASS 29 | 30 | 31 | Both *package.json* file and *node_modules* folder are left unchanged, 32 | and now you know that you can safely upgrade both libraries to later versions. 33 | 34 | ### It even tells you the install command ;) 35 | 36 | Use the following command to install working versions 37 | npm install --save lodash@2.1.0 38 | 39 | This might not appear like a big deal for a single module that is using 40 | popular 3rd party libraries with stable apis only. *next-update* is most useful 41 | in the larger development context, where multiple modules are being developed 42 | side by side, often by different teams. In such situations, checking if an upgrade 43 | is possible could be part of the continuous build pipeline. 44 | 45 | You can see if your dependencies are out of date by using 46 | [david](https://david-dm.org), 47 | it even has badges you can add to your README files. 48 | 49 | *next-update* reports the probability of success for a given dependency update using 50 | anonymous global statistics from [next-update](http://next-update.herokuapp.com/) server 51 | 52 | ``` 53 | available updates: 54 | package available from version average success % successful updates failed updates 55 | -------------------- --------- ------------ ----------------- ------------------ -------------- 56 | grunt-contrib-jshint 0.8.0 0.7.2 100% 34 0 57 | grunt-bump 0.0.13 0.0.12 100% 4 0 58 | ``` 59 | 60 | ## Install 61 | 62 | You can install this tool globally 63 | 64 | npm install -g next-update // installs module globally 65 | next-update --help // shows command line options 66 | 67 | Then run inside any package folder 68 | 69 | /git/my-awesome-module 70 | $ next-update 71 | 72 | Or you can use this module as a devDependency and a script command 73 | 74 | npm install --save-dev next-update 75 | 76 | ```json 77 | { 78 | "scripts": { 79 | "next-update": "next-update -k true --tldr" 80 | } 81 | } 82 | ``` 83 | 84 | This command will keep the successfuly version upgrades in the package.json file, 85 | but will not be very verbose when run. 86 | 87 | ## Anonymous usage collection 88 | 89 | After testing each module A upgrade from version X to Y, *next-update* sends 90 | anonymous result to [next-update.herokuapp.com/](http://next-update.herokuapp.com/). 91 | The only information transmitted is: 92 | 93 | ```json 94 | { 95 | "name": "lodash", 96 | "from": "1.0.0", 97 | "to": "2.0.0", 98 | "success": true 99 | } 100 | ``` 101 | 102 | This information is used to answer the following questions later: 103 | what is the probability module A can be upgraded from X to Y? 104 | Thus even if you do not have tests covering this particular module, 105 | you can judge how compatible version X and Y really are over the entire 106 | internet. 107 | 108 | You can inspect data send in 109 | [stats.js](https://github.com/bahmutov/next-update/blob/master/src/stats.js). 110 | 111 | If the dependency module has been upgraded by anyone else, its statistics 112 | will be displayed with each test. 113 | 114 | ```sh 115 | stats: deps-ok 0.0.7 -> 0.0.8 success probability 44.44% 8 success(es) 10 failure(s) 116 | ``` 117 | 118 | A lot of NPM modules [do not have tests](http://npmt.abru.pt/), but 119 | at least you can judge if someone else has success going from verion X to version Y 120 | of a dependency. 121 | 122 | ## Use 123 | 124 | Make sure the target module has unit / integration tests, 125 | and the tests can be run using `npm test` command. 126 | 127 | Run `next-update` from the command line in the same folder as 128 | the target module. In general this tool does the following: 129 | 130 | 1. Reads the module's dependencies (including dev) and their versions 131 | 2. Queries npm registry to see if there are newer versions 132 | 3. For each dependency that has newer versions available: 133 | 1. Installs each version 134 | 2. Runs command `npm test` to determine if the new version breaks the tests 135 | 3. Installs back the current version. 136 | 4. Reports results 137 | 138 | ## Checking specific modules 139 | 140 | You can check one or more specific modules (whitelist) using CLI flag 141 | `--module` or `-m` 142 | 143 | ```sh 144 | next-update --module foo,bar,baz 145 | ``` 146 | 147 | ## Ignoring or skipping some modules 148 | 149 | **note** [prerelease](https://github.com/npm/node-semver#functions) 150 | versions like `1.2.0-alpha` are skipped by default. I believe `next-update` is 151 | meant to upgrade to *stable* versions. 152 | 153 | Some modules are hard to unit test, thus the automatic upgrades are not appropriate. 154 | For example [benv](https://npmjs.org/package/benv) upgrade brings a new 155 | [jsdom](https://npmjs.org/package/jsdom) version, which does not work on Node 0.12 156 | Similarly, upgrading [Q](https://npmjs.org/package/q) from 1.x.x to 2.x.x is usually 157 | a breaking change. 158 | 159 | You can skip a list of modules by name using `config` property in the `package.json` 160 | 161 | ```json 162 | "config": { 163 | "next-update": { 164 | "skip": ["benv", "q"] 165 | } 166 | } 167 | ``` 168 | 169 | ## Custom test command per module 170 | 171 | Some modules are not really tested using the default `npm test` command or 172 | whatever is passed via `--test "..."` from CLI. For example a linter module 173 | should probably be tested using `npm run lint` command. You can set individual 174 | test commands for each module to override the default test command. In the 175 | `package.json` config object set "commands" object 176 | 177 | ```json 178 | "config": { 179 | "next-update": { 180 | "commands": { 181 | "git-issues": "npm run issues", 182 | "standard": "npm run lint" 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | Then when `git-issues` module is checked by itself, it will run 189 | `npm run issues` command; when module `standard` is tested by itself, the 190 | test will use `npm run lint` command. 191 | 192 | ## Misc 193 | 194 | * To see what has changed in the latest version of any module, 195 | use my companion tool [changed](https://npmjs.org/package/changed) 196 | like this `changed foo` (*foo* is package name) 197 | * When comparing versions, keywords *latest* and *** are both assumed to equal to "0.0.0". 198 | * A good workflow using *next-update* 199 | * see available new versions `next-update --available` 200 | * check latest version of each module using `next-update --latest` 201 | * install new versions of the desired modules using standard `npm i dependency@version --save` 202 | * You can use custom test command, for example `next-update -t "grunt test"` 203 | * `npm test` is used by default. 204 | * You can keep each working version in package.json by using `--keep` flag. 205 | -------------------------------------------------------------------------------- /examples/qunit/README.md: -------------------------------------------------------------------------------- 1 | Install qunit global module 2 | 3 | npm i -g qunit 4 | 5 | Run unit tests 6 | 7 | qunit -c test.js -t test.js 8 | -------------------------------------------------------------------------------- /examples/qunit/test.js: -------------------------------------------------------------------------------- 1 | test("a basic test example", function (assert) { 2 | ok(true, "this test is fine"); 3 | var value = "hello"; 4 | equal("hello", value, "We expect value to be hello"); 5 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var q = require('q') 2 | q.longStackSupport = true 3 | 4 | if (!module.parent) { 5 | throw new Error('Please run bin/next-update.js for stand alone CLI tool') 6 | } 7 | 8 | module.exports = require('./src/next-update-as-module') 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-update", 3 | "description": "Tests if module's dependencies can be updated to the newer version without breaking the tests", 4 | "version": "0.0.0", 5 | "author": "Gleb Bahmutov ", 6 | "bin": { 7 | "next-update": "bin/next-update.js" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/bahmutov/next-update/issues" 11 | }, 12 | "config": { 13 | "pre-git": { 14 | "commit-msg": "simple", 15 | "pre-commit": [ 16 | "npm run build", 17 | "npm test", 18 | "npm run e2e" 19 | ], 20 | "pre-push": [ 21 | "npm run size" 22 | ], 23 | "post-commit": [], 24 | "post-merge": [] 25 | } 26 | }, 27 | "contributors": [], 28 | "dependencies": { 29 | "changed-log": "0.13.0", 30 | "chdir-promise": "0.4.0", 31 | "check-more-types": "2.24.0", 32 | "cli-color": "1.2.0", 33 | "common-tags": "1.4.0", 34 | "console.json": "0.2.1", 35 | "console.table": "0.8.0", 36 | "debug": "2.6.8", 37 | "deps-ok": "1.2.0", 38 | "easy-table": "0.3.0", 39 | "execa": "0.7.0", 40 | "is-online": "5.1.2", 41 | "lazy-ass": "1.5.0", 42 | "lodash": "3.10.1", 43 | "npm-utils": "1.7.1", 44 | "optimist": "0.6.1", 45 | "pluralize": "5.0.0", 46 | "q": "2.0.3", 47 | "quote": "0.4.0", 48 | "ramda": "0.24.1", 49 | "request": "2.74.0", 50 | "semver": "5.3.0", 51 | "update-notifier": "0.5.0" 52 | }, 53 | "devDependencies": { 54 | "coveralls": "2.11.4", 55 | "git-issues": "1.3.1", 56 | "github-post-release": "1.12.1", 57 | "grunt": "0.4.5", 58 | "grunt-bump": "0.7.3", 59 | "grunt-cli": "0.1.13", 60 | "grunt-complexity": "0.3.0", 61 | "grunt-deps-ok": "0.9.0", 62 | "grunt-help": "0.5.0", 63 | "grunt-jsonlint": "1.1.0", 64 | "grunt-lineending": "1.0.0", 65 | "grunt-nice-package": "0.10.4", 66 | "grunt-readme": "0.4.5", 67 | "gt": "0.10.0", 68 | "matchdep": "1.0.1", 69 | "mocha": "3.4.2", 70 | "mockery": "1.7.0", 71 | "pre-git": "3.15.0", 72 | "prettier-standard": "5.1.0", 73 | "publish": "0.6.0", 74 | "semantic-release": "6.3.6", 75 | "simple-commit-message": "3.0.2", 76 | "snap-shot": "2.17.0", 77 | "standard": "10.0.2", 78 | "stop-build": "1.1.0", 79 | "time-grunt": "1.4.0" 80 | }, 81 | "engines": { 82 | "node": ">= 0.8.0" 83 | }, 84 | "files": [ 85 | "bin", 86 | "index.js", 87 | "src", 88 | "!src/*-spec.js", 89 | "!src/test" 90 | ], 91 | "homepage": "https://github.com/bahmutov/next-update", 92 | "keywords": [ 93 | "javascript", 94 | "node", 95 | "npm", 96 | "testing", 97 | "update", 98 | "version" 99 | ], 100 | "license": "MIT", 101 | "main": "index.js", 102 | "next-update-stats": "http://next-update.herokuapp.com", 103 | "preferGlobal": true, 104 | "readmeFilename": "README.md", 105 | "release": { 106 | "analyzeCommits": "simple-commit-message", 107 | "generateNotes": "github-post-release" 108 | }, 109 | "repository": { 110 | "type": "git", 111 | "url": "https://github.com/bahmutov/next-update.git" 112 | }, 113 | "scripts": { 114 | "2npm": "publish", 115 | "build": "grunt", 116 | "commit": "git-issues && commit-wizard", 117 | "coveralls": "cat cover/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 118 | "dont-break": "dont-break", 119 | "e2e": "npm run install-for-tests && gt test/*.coffee --output", 120 | "install-for-tests": "cd test/test-next-updater && npm install", 121 | "issues": "git-issues", 122 | "limited": "gt --filter 'allow major' --output src/test/*.coffee", 123 | "lint": "standard --verbose --fix *.js src/*.js src/**/*.js bin/*.js test/*.js", 124 | "mocha": "mocha src/*-spec.js test/*-spec.js", 125 | "posttest": "npm run mocha", 126 | "prebuild": "npm run lint", 127 | "prelint": "npm run pretty", 128 | "pretest": "npm run build", 129 | "pretty": "prettier-standard 'bin/*.js' '*.js' 'test/*.js'", 130 | "self-update": "node bin/next-update.js -k true", 131 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 132 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 133 | "test": "npm run unit", 134 | "unit": "gt src/test/*.js src/test/*.coffee --output" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | }, 7 | "excludePackageNames": ["^grunt", "semantic-release", "github-post-release"] 8 | } 9 | -------------------------------------------------------------------------------- /src/cli-options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var optimist = require('optimist') 4 | var pkg = require('../package.json') 5 | var la = require('lazy-ass') 6 | var is = require('check-more-types') 7 | var _ = require('lodash') 8 | var path = require('path') 9 | 10 | var parentFolder = path.join(__dirname, '..') 11 | var info = pkg.name + ' - ' + pkg.description + '\n' + 12 | ' version: ' + pkg.version + '\n' + 13 | ' folder: ' + parentFolder + '\n' + 14 | ' author: ' + JSON.stringify(pkg.author) 15 | 16 | var program = optimist 17 | .options('revert', { 18 | boolean: true, 19 | description: 'install original module versions listed in package.json', 20 | default: false 21 | }) 22 | .options('available', { 23 | boolean: true, 24 | alias: 'a', 25 | description: 'only query available later versions, do not test them', 26 | default: false 27 | }) 28 | .options('module', { 29 | string: true, 30 | alias: 'm', 31 | description: 'checks specific module(s), can include version name@version', 32 | default: null 33 | }) 34 | .options('without', { 35 | string: true, 36 | alias: 'w', 37 | description: 'skip checking this module(s)', 38 | default: null 39 | }) 40 | .option('latest', { 41 | boolean: true, 42 | alias: 'l', 43 | description: 'only check latest available update', 44 | default: true 45 | }) 46 | .option('color', { 47 | boolean: true, 48 | alias: 'c', 49 | description: 'color terminal output (if available)', 50 | default: true 51 | }) 52 | .option('version', { 53 | boolean: true, 54 | alias: 'v', 55 | description: 'show version and exit', 56 | default: false 57 | }) 58 | .option('test', { 59 | string: true, 60 | alias: 't', 61 | description: 'custom test command to run instead of npm test' 62 | }) 63 | .option('skip', { 64 | boolean: true, 65 | description: 'skip running tests first', 66 | default: false 67 | }) 68 | .option('all', { 69 | boolean: true, 70 | default: false, 71 | description: 'install all modules at once before testing' 72 | }) 73 | .option('keep', { 74 | boolean: true, 75 | default: true, 76 | alias: 'k', 77 | description: 'keep tested version if it is working' 78 | }) 79 | .option('allow', { 80 | string: true, 81 | default: 'major', 82 | description: 'allow major / minor / patch updates' 83 | }) 84 | .options('type', { 85 | string: true, 86 | default: 'all', 87 | description: 'check dependencies of type (all, prod, dev, peer)' 88 | }) 89 | .options('tldr', { 90 | boolean: true, 91 | default: false, 92 | description: 'only print VERY important log messages' 93 | }) 94 | .options('changed-log', { 95 | boolean: true, 96 | default: true, 97 | alias: 'L', 98 | description: 'print commit changes between working versions' 99 | }) 100 | .options('registry', { 101 | string: true, 102 | default: false, 103 | description: 'use a custom registry url' 104 | }) 105 | .options('check-version-timeout', { 106 | number: true, 107 | default: 10000, 108 | description: 'define a custom timeout value for checking next versions' 109 | }) 110 | .usage(info) 111 | .argv 112 | 113 | if (program.version) { 114 | console.log(info) 115 | process.exit(0) 116 | } 117 | 118 | if (program.help || program.h || program['?']) { 119 | optimist.showHelp() 120 | process.exit(0) 121 | } 122 | 123 | program.allowWasSpecified = _.includes(process.argv, '--allow') 124 | 125 | if (is.string(program.module)) { 126 | program.module = program.module.split(',').map(_.trim) 127 | la(is.array(program.module), 'expected list of modules', program.module) 128 | } 129 | 130 | if (is.string(program.without)) { 131 | program.without = program.without.split(',').map(_.trim) 132 | la(is.array(program.without), 133 | 'expected list of modules to skip in --without', program.without) 134 | } 135 | 136 | module.exports = program 137 | -------------------------------------------------------------------------------- /src/dependencies.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var debug = require('debug')('next-update') 4 | var la = require('lazy-ass') 5 | var check = require('check-more-types') 6 | var path = require('path') 7 | var print = require('./print-modules-table') 8 | var nameVersionParser = require('./moduleName') 9 | var getKnownDependencies = require('./get-known-dependencies') 10 | const {getSkippedModules} = require('./utils') 11 | const pluralize = require('pluralize') 12 | 13 | require('console.table') 14 | var _ = require('lodash') 15 | 16 | la(check.fn(console.table), 'missing console.table method') 17 | la(check.fn(console.json), 'missing console.json method') 18 | 19 | // eslint-disable-next-line no-unused-vars 20 | function printCurrentModules (infos) { 21 | check.verify.array(infos, 'expected array of modules') 22 | 23 | var modules = [] 24 | infos.forEach(function (nameVersionArray) { 25 | check.verify.array(nameVersionArray, 'expected name version in ' + modules) 26 | modules.push({ 27 | name: nameVersionArray[0], 28 | version: nameVersionArray[1] 29 | }) 30 | }) 31 | print(modules) 32 | } 33 | 34 | function printTable (options, nameVersionPairs) { 35 | if (options.tldr) { 36 | return 37 | } 38 | 39 | var allowedType = options.type || 'all' 40 | var title = 'module\'s current dependencies:' 41 | var filtered = allowedType === 'all' 42 | ? nameVersionPairs 43 | : _.filter(nameVersionPairs, { type: allowedType }) 44 | 45 | // TODO just use Ramda project 46 | console.table(title, _.map(filtered, function (nameVersion) { 47 | return { 48 | module: nameVersion.name, 49 | version: nameVersion.version, 50 | type: nameVersion.type 51 | } 52 | })) 53 | } 54 | 55 | function remove (nameVersionPairs, skipModules) { 56 | check.verify.array(skipModules, 'expected list of modules to skip') 57 | return nameVersionPairs.filter(function (dep) { 58 | check.verify.unemptyString(dep.name, 'missing name for dependency') 59 | return !_.includes(skipModules, dep.name) 60 | }) 61 | } 62 | 63 | function normalizeModuleNames (moduleNames) { 64 | if (!moduleNames) { 65 | return 66 | } 67 | console.log('returning dependencies for') 68 | console.dir(moduleNames) 69 | if (check.string(moduleNames)) { 70 | moduleNames = [moduleNames] 71 | } 72 | 73 | if (check.object(moduleNames)) { 74 | var names = Object.keys(moduleNames) 75 | moduleNames = names 76 | } 77 | 78 | check.verify.array(moduleNames, 'expected module names array ' + 79 | JSON.stringify(moduleNames)) 80 | return moduleNames 81 | } 82 | 83 | function getDependenciesToCheck (options, moduleNames) { 84 | check.verify.object(options, 'missing options') 85 | debug('get dependencies for options') 86 | debug(options) 87 | moduleNames = normalizeModuleNames(moduleNames) 88 | if (moduleNames) { 89 | debug('normalized module names', moduleNames) 90 | } else { 91 | debug('no --module filter') 92 | } 93 | 94 | var workingDirectory = process.cwd() 95 | 96 | var packageFilename = path.join(workingDirectory, 'package.json') 97 | var nameVersionPairs = getKnownDependencies(packageFilename) 98 | 99 | var skipModules = getSkippedModules(packageFilename) 100 | check.verify.array(skipModules, 'expected list of skipped modules') 101 | if (skipModules.length) { 102 | if (!options.tldr) { 103 | console.log('ignoring the following modules', skipModules.join(', ')) 104 | } 105 | nameVersionPairs = remove(nameVersionPairs, skipModules) 106 | } 107 | if (options.without) { 108 | la(check.array(options.without), 109 | 'without should be an array', options.without) 110 | if (check.unempty(options.without)) { 111 | debug('checking without %s %s', 112 | pluralize('module', options.without.length), 113 | options.without.join(', ')) 114 | nameVersionPairs = remove(nameVersionPairs, options.without) 115 | } 116 | } 117 | printTable(options, nameVersionPairs) 118 | 119 | var toCheck = nameVersionPairs 120 | if (moduleNames) { 121 | debug('matching module names', moduleNames) 122 | toCheck = nameVersionPairs.filter(function (nameVersion) { 123 | var name = nameVersion.name 124 | return moduleNames.some(function (aModule) { 125 | var moduleName = nameVersionParser(aModule).name 126 | return name === moduleName 127 | }) 128 | }) 129 | if (!options.tldr) { 130 | if (toCheck.length) { 131 | console.log('only checking modules') 132 | console.log(toCheck.map(function (m) { 133 | return m.name + '@' + m.version 134 | })) 135 | } else { 136 | console.log('Hmm, no modules to check') 137 | console.log('from initial list\n' + 138 | JSON.stringify(nameVersionPairs, null, 2)) 139 | } 140 | } 141 | } 142 | return toCheck 143 | } 144 | 145 | module.exports = getDependenciesToCheck 146 | -------------------------------------------------------------------------------- /src/exec-test.js: -------------------------------------------------------------------------------- 1 | var check = require('check-more-types') 2 | var verify = check.verify 3 | var spawn = require('child_process').spawn 4 | var q = require('q') 5 | var npmPath = require('./npm-test').npmPath 6 | var _ = require('lodash') 7 | const debug = require('debug')('next-update') 8 | 9 | // returns a promise 10 | // TODO switch to execa 11 | function test (options, testCommand) { 12 | options = options || {} 13 | var log = options.tldr ? _.noop : console.log.bind(console) 14 | debug('exec-test "%s"', testCommand) 15 | 16 | verify.unemptyString(testCommand, 'missing test command string') 17 | log(' ', testCommand) 18 | 19 | var testParts = testCommand.split(' ') 20 | console.assert(testParts.length > 0, 'missing any test words in ' + testCommand) 21 | var testExecutable = testParts.shift() 22 | verify.unemptyString(testExecutable, 'missing test executable for command ' + testCommand) 23 | if (testExecutable === 'npm') { 24 | testExecutable = npmPath 25 | } 26 | var testProcess = spawn(testExecutable, testParts) 27 | var testOutput = '' 28 | var testErrors = '' 29 | 30 | testProcess.stdout.setEncoding('utf-8') 31 | testProcess.stderr.setEncoding('utf-8') 32 | 33 | testProcess.stdout.on('data', function (data) { 34 | testOutput += data 35 | }) 36 | 37 | testProcess.stderr.on('data', function (data) { 38 | testErrors += data 39 | }) 40 | 41 | var deferred = q.defer() 42 | testProcess.on('error', function (err) { 43 | console.error('test command: "' + testCommand + '"') 44 | console.error(err) 45 | testErrors += err.toString() 46 | const e = new Error('test command failed') 47 | e.code = err.code 48 | e.errors = testErrors 49 | deferred.reject(e) 50 | }) 51 | 52 | testProcess.on('exit', function (code) { 53 | if (code) { 54 | console.error('testProcess test returned', code) 55 | console.error('test errors:\n' + testErrors) 56 | console.error(testOutput) 57 | const e = new Error('test exit code means error') 58 | e.code = code 59 | e.errors = testErrors 60 | return deferred.reject(e) 61 | } 62 | deferred.resolve() 63 | }) 64 | return deferred.promise 65 | } 66 | 67 | module.exports = test 68 | -------------------------------------------------------------------------------- /src/filter-allowed-spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const snapShot = require('snap-shot') 3 | const filter = require('./filter-allowed-updates') 4 | const {clone, equals} = require('ramda') 5 | const {isPrerelease} = require('./utils') 6 | 7 | /* global describe, it, afterEach */ 8 | describe('filter allowed', () => { 9 | const current = { 10 | q: { name: 'q', version: '1.1.1', type: 'dev' } 11 | } 12 | const available = [ { name: 'q', 13 | versions: [ '1.3.0', '2.0.2', '3.0.0', '3.0.0-alpha', '3.0.0-rc0' ] } ] 14 | const copy = clone(available) 15 | 16 | afterEach(() => { 17 | // sanity check to make sure we are not 18 | // mutating the input objects 19 | la(equals(available, copy), 20 | 'original available list is unchanged', available, copy) 21 | }) 22 | 23 | it('detects prerelease', () => { 24 | snapShot(isPrerelease, '3.0.0-alpha', '3.0.0', '0.1.0', '10.0.0-beta.2') 25 | }) 26 | 27 | it('allows major', () => { 28 | const options = { 29 | allowed: 'major' 30 | } 31 | const allowed = filter(current, available, options) 32 | snapShot({current, options, allowed}) 33 | }) 34 | 35 | it('allows minor', () => { 36 | const options = { 37 | allowed: 'minor' 38 | } 39 | const allowed = filter(current, available, options) 40 | snapShot({current, options, allowed}) 41 | }) 42 | 43 | it('allows patch', () => { 44 | const options = { 45 | allowed: 'patch' 46 | } 47 | const allowed = filter(current, available, options) 48 | snapShot({current, options, allowed}) 49 | }) 50 | 51 | it('allows everything by default', () => { 52 | const options = {} 53 | const allowed = filter(current, available, options) 54 | snapShot({current, options, allowed}) 55 | }) 56 | 57 | it('can custom filter', () => { 58 | const options = { 59 | limit: (name, version) => version === '2.0.2' 60 | } 61 | const allowed = filter(current, available, options) 62 | snapShot({current, options, allowed}) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/filter-allowed-updates.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass') 2 | var check = require('check-more-types') 3 | var semver = require('semver') 4 | var _ = require('lodash') 5 | const R = require('ramda') 6 | const debug = require('debug')('next-update') 7 | const {isPrerelease} = require('./utils') 8 | 9 | la(check.fn(semver.diff), 'semver missing diff method', semver) 10 | 11 | function isDiffAllowed (allowed, diff) { 12 | la(check.unemptyString(allowed), 'invalid allowed update', allowed) 13 | la(check.unemptyString(diff), 'invalid diff update', diff) 14 | 15 | switch (allowed) { 16 | case 'major': 17 | return true 18 | case 'minor': 19 | return diff === 'minor' || diff === 'patch' 20 | case 'patch': 21 | return diff === 'patch' 22 | default: 23 | throw new Error('Invalid allowed update ' + allowed) 24 | } 25 | } 26 | 27 | function isDependencyTypeAllowed (allowed, current) { 28 | la(check.unemptyString(allowed), 'expected allowed string', allowed) 29 | la(check.unemptyString(current), 'expected current string', current) 30 | 31 | allowed = allowed.trim().toLowerCase() 32 | current = current.trim().toLowerCase() 33 | 34 | if (allowed === 'all') { 35 | return true 36 | } 37 | return allowed === current 38 | } 39 | 40 | /* 41 | update is an object like this 42 | { 43 | name: 'check-types', 44 | versions: ['0.7.0', '0.7.1'] 45 | } 46 | and limit function tells for each (name, version) if it should remain 47 | in this list 48 | */ 49 | function limitUpdate (limit, update) { 50 | la(check.fn(limit), 'expected limit function', limit) 51 | 52 | const filter = R.filter(version => limit(update.name, version)) 53 | 54 | return R.evolve({ 55 | versions: filter 56 | })(update) 57 | } 58 | 59 | /* 60 | update is an object like this 61 | { 62 | name: 'check-types', 63 | versions: ['0.7.0', '0.7.1'] 64 | } 65 | but the versions list could be empty 66 | */ 67 | function hasVersionsToCheck (update) { 68 | return check.object(update) && check.unempty(update.versions) 69 | } 70 | 71 | function filterAllowedUpdates (current, available, options) { 72 | var allowed = options.allow || options.allowed || 'major' 73 | var isAllowed = _.partial(isDiffAllowed, allowed) 74 | const limit = options.limit || R.T 75 | 76 | var type = options.type || 'all' 77 | var isAllowedType = _.partial(isDependencyTypeAllowed, type) 78 | 79 | const askLimit = _.partial(limitUpdate, limit) 80 | 81 | // console.log('filtering available updates', available); 82 | // console.log('current versions', current); 83 | 84 | function filterVersions (fromVersion, toVersion) { 85 | var diff = semver.diff(fromVersion, toVersion) 86 | // console.log(availableUpdate.name, 'difference from', fromVersion, 'to', toVersion, diff); 87 | return isAllowed(diff) 88 | } 89 | 90 | function allowedDependencyType (availableUpdate) { 91 | var dependency = current[availableUpdate.name] 92 | la(check.object(dependency), 93 | 'cannot find dependency for update', availableUpdate, 'in', current) 94 | la(check.unemptyString(dependency.type), 95 | 'missing type of update', dependency) 96 | return isAllowedType(dependency.type) 97 | } 98 | 99 | function filterHasNewVersions (availableUpdate) { 100 | la(check.unemptyString(availableUpdate.name), 'missing name in available', availableUpdate) 101 | 102 | var fromVersion = current[availableUpdate.name].version 103 | la(check.unemptyString(fromVersion), 104 | 'cannot find current version for', availableUpdate.name, current) 105 | 106 | var versions = availableUpdate.versions 107 | la(check.array(versions), 'missing versions in update', availableUpdate) 108 | 109 | const filterByUpgradeType = _.partial(filterVersions, fromVersion) 110 | const notPrerelease = R.complement(isPrerelease) 111 | 112 | const filteredVersions = versions 113 | .filter(notPrerelease) 114 | .filter(filterByUpgradeType) 115 | 116 | availableUpdate.versions = filteredVersions 117 | return availableUpdate.versions.length > 0 118 | } 119 | 120 | debug('available updates') 121 | debug(available) 122 | 123 | const filtered = R.clone(available) 124 | .filter(filterHasNewVersions) 125 | .filter(allowedDependencyType) 126 | .map(askLimit) 127 | .filter(hasVersionsToCheck) 128 | 129 | debug('filtered updates') 130 | debug(filtered) 131 | 132 | return filtered 133 | } 134 | 135 | module.exports = check.defend(filterAllowedUpdates, 136 | check.object, 'expected object with all current dependencies', 137 | check.array, 'available list should be an array', 138 | check.object, 'options should be an object') 139 | -------------------------------------------------------------------------------- /src/get-known-dependencies.js: -------------------------------------------------------------------------------- 1 | var check = require('check-more-types') 2 | var registry = require('./registry') 3 | var cleanVersions = registry.cleanVersions 4 | 5 | function format (label, deps) { 6 | check.verify.unemptyString(label, 'missing label') 7 | check.verify.object(deps, 'expected deps') 8 | return Object.keys(deps).map(function (name) { 9 | return { 10 | type: label, 11 | name: name, 12 | version: deps[name] 13 | } 14 | }) 15 | } 16 | 17 | function getKnownDependencies (packageFilename) { 18 | check.verify.string(packageFilename, 'missing package filename string') 19 | 20 | var workingPackage = require(packageFilename) 21 | 22 | var dependencies = workingPackage.dependencies || {} 23 | var devDependencies = workingPackage.devDependencies || {} 24 | var peerDependencies = workingPackage.peerDependencies || {} 25 | 26 | var all = [].concat( 27 | format('prod', dependencies), 28 | format('dev', devDependencies), 29 | format('peer', peerDependencies) 30 | ) 31 | 32 | var cleaned = cleanVersions(all) 33 | // console.log('nameVersionPairs', cleaned); 34 | return cleaned 35 | } 36 | 37 | module.exports = getKnownDependencies 38 | -------------------------------------------------------------------------------- /src/local-module-version.js: -------------------------------------------------------------------------------- 1 | var check = require('check-more-types') 2 | var fs = require('fs') 3 | var path = require('path') 4 | 5 | // sync returns version 6 | function getLocalModuleVersion (name) { 7 | check.verify.string(name, 'missing name string') 8 | 9 | try { 10 | var filename = path.join('node_modules', name, 'package.json') 11 | var contents = fs.readFileSync(filename, 'utf-8') 12 | var pkg = JSON.parse(contents) 13 | return pkg.version 14 | } catch (error) { 15 | console.error('could not fetch version for local module', name) 16 | console.error(error) 17 | return null 18 | } 19 | } 20 | 21 | module.exports = getLocalModuleVersion 22 | -------------------------------------------------------------------------------- /src/module-install.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | var check = require('check-more-types') 3 | var spawn = require('child_process').spawn 4 | var q = require('q') 5 | var NPM_PATH = require('npm-utils').path 6 | var formInstallCommand = require('./report-install-command') 7 | 8 | // returns a promise 9 | function installModule (options, results) { 10 | check.verify.object(options, 'missing options') 11 | la(check.unemptyString(options.name), 12 | 'expected module name string', options.name, 'all options', options) 13 | check.verify.string(options.version, 'expected version string') 14 | 15 | if (options.keep) { 16 | console.assert(typeof options.keep === 'boolean', 'invalid keep') 17 | } 18 | if (results) { 19 | check.verify.array(results, 'missing results') 20 | } 21 | 22 | var cmd = formInstallCommand([[{ 23 | name: options.name, 24 | version: options.version, 25 | works: true 26 | }]]) 27 | check.verify.unemptyString(cmd, 'could not form install command') 28 | cmd = cmd.trim() 29 | 30 | var moduleVersion = options.name + '@' + options.version 31 | var npm 32 | if (options.keep) { 33 | console.log(' ', cmd) 34 | var args = cmd.split(' ') 35 | args.shift() 36 | args.push('--save-exact') 37 | npm = spawn(NPM_PATH, args) 38 | } else { 39 | if (!options.tldr) { 40 | console.log(' installing', moduleVersion) 41 | } 42 | npm = spawn(NPM_PATH, ['install', moduleVersion]) 43 | } 44 | 45 | // eslint-disable-next-line no-unused-vars 46 | var testOutput = '' 47 | var testErrors = '' 48 | 49 | npm.stdout.setEncoding('utf-8') 50 | npm.stderr.setEncoding('utf-8') 51 | 52 | function hasError (str) { 53 | return /error/i.test(str) 54 | } 55 | 56 | npm.stdout.on('data', function (data) { 57 | if (hasError(data)) { 58 | console.log('stdout:', data) 59 | } 60 | testOutput += data 61 | }) 62 | 63 | npm.stderr.on('data', function (data) { 64 | if (hasError(data)) { 65 | console.log('stderr:', data) 66 | } 67 | testErrors += data 68 | }) 69 | 70 | npm.on('error', function (err) { 71 | console.error('error:', err) 72 | testErrors += err.toString() 73 | }) 74 | 75 | var deferred = q.defer() 76 | npm.on('exit', function (code) { 77 | if (code) { 78 | console.error('npm returned', code) 79 | console.error('errors:\n' + testErrors) 80 | deferred.reject({ 81 | code: code, 82 | errors: testErrors 83 | }) 84 | } else { 85 | if (!options.tldr) { 86 | console.log(moduleVersion, 'installed successfully') 87 | } 88 | deferred.resolve(results) 89 | } 90 | }) 91 | return deferred.promise 92 | } 93 | 94 | module.exports = installModule 95 | -------------------------------------------------------------------------------- /src/moduleName.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var check = require('check-more-types') 4 | 5 | function isScopedName (str) { 6 | return str[0] === '@' && str.indexOf('/') !== -1 7 | } 8 | 9 | function parseScopedName (str) { 10 | var parsed = moduleName(str.substr(1)) 11 | parsed.name = '@' + parsed.name 12 | return parsed 13 | } 14 | 15 | function moduleName (str) { 16 | check.verify.string(str, 'expected string module name') 17 | 18 | if (isScopedName(str)) { 19 | return parseScopedName(str) 20 | } 21 | 22 | var parts = str.split('@') 23 | return { 24 | name: parts[0], 25 | version: parts[1] 26 | } 27 | } 28 | 29 | module.exports = moduleName 30 | -------------------------------------------------------------------------------- /src/next-update-as-module.js: -------------------------------------------------------------------------------- 1 | const nextUpdate = require('./next-update') 2 | const is = require('check-more-types') 3 | const {T} = require('ramda') 4 | 5 | module.exports = function nextUpdateTopLevel (options) { 6 | options = options || {} 7 | const opts = { 8 | names: options.module, 9 | testCommand: options.test, 10 | latest: Boolean(options.latest), 11 | keep: Boolean(options.keep), 12 | color: Boolean(options.color || options.colors), 13 | allow: options.allow || options.allowed, 14 | type: options.type, 15 | changedLog: options['changed-log'], 16 | limit: is.maybe.fn(options.limit) ? options.limit : T, 17 | without: options.without 18 | } 19 | 20 | const checkUpdates = nextUpdate.checkAllUpdates.bind(null, opts) 21 | 22 | return nextUpdate.checkCurrentInstall(opts) 23 | .then(checkUpdates) 24 | } 25 | -------------------------------------------------------------------------------- /src/next-update.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass') 2 | var check = require('check-more-types') 3 | var verify = check.verify 4 | require('console.json') 5 | 6 | var log = require('debug')('next-update') 7 | var Q = require('q') 8 | Q.longStackSupport = true 9 | var depsOk = require('deps-ok') 10 | var _ = require('lodash') 11 | 12 | var nameVersionParser = require('./moduleName') 13 | var registry = require('./registry') 14 | var nextVersions = registry.nextVersions 15 | var testVersions = require('./test-module-version').testModulesVersions 16 | var runTest = require('./test-module-version').testPromise 17 | var getDependenciesToCheck = require('./dependencies') 18 | var reportAvailable = require('./report-available') 19 | var npmUtils = require('npm-utils') 20 | 21 | var boundConsoleLog = console.log.bind(console) 22 | 23 | // returns a promise 24 | function available (moduleName, options) { 25 | options = options || {} 26 | var toCheck = getDependenciesToCheck(options, moduleName) 27 | la(check.array(toCheck), 'expected object of deps to check, was', toCheck) 28 | var toCheckHash = _.zipObject( 29 | _.pluck(toCheck, 'name'), 30 | _.pluck(toCheck, 'version') 31 | ) 32 | 33 | log('need to check these dependencies') 34 | log(toCheckHash) 35 | 36 | var nextVersionsPromise = nextVersions(options, toCheck) 37 | return nextVersionsPromise.then(function (info) { 38 | return reportAvailable(info, toCheckHash, options) 39 | }, function (error) { 40 | console.error('Could not fetch available modules\n', error) 41 | }) 42 | } 43 | 44 | function checkDependenciesInstalled () { 45 | var defer = Q.defer() 46 | process.nextTick(function () { 47 | if (depsOk(process.cwd())) { 48 | defer.resolve() 49 | } else { 50 | var msg = 'Current installation is incomplete. Please run `npm install` or `bower install` first' 51 | defer.reject(new Error(msg)) 52 | } 53 | }) 54 | return defer.promise 55 | } 56 | 57 | function cleanDependencies () { 58 | var update = _.partial(npmUtils.test, 'npm update') 59 | var prune = _.partial(npmUtils.test, 'npm prune') 60 | return update().then(prune) 61 | } 62 | 63 | function checkCurrentInstall (options) { 64 | options = options || {} 65 | var log = options.tldr ? _.noop : boundConsoleLog 66 | log('checking if the current state works') 67 | 68 | return cleanDependencies() 69 | .then(checkDependenciesInstalled) 70 | .then(function () { 71 | log('running test command') 72 | return runTest(options, options.testCommand)() 73 | }) 74 | .then(function () { 75 | console.log('> tests are passing at the start') 76 | }) 77 | } 78 | 79 | var isOnline = Q.denodeify(require('is-online')) 80 | 81 | function isSingleItem (names) { 82 | return names && 83 | check.array(names) && 84 | names.length === 1 85 | } 86 | 87 | function makeSureValidModule (moduleNames, checkingModules) { 88 | la(check.maybe.array(moduleNames), 'expected list of modules', moduleNames) 89 | la(check.array(checkingModules), 'expected list of modules to check', checkingModules) 90 | if (isSingleItem(moduleNames) && check.empty(checkingModules)) { 91 | console.error('Could not find module "%s" in the list of dependencies', moduleNames[0]) 92 | console.error('Please check the name') 93 | process.exit(-1) 94 | } 95 | } 96 | 97 | // returns promise 98 | function checkAllUpdates (options) { 99 | options = options || {} 100 | var moduleName = options.names 101 | var checkLatestOnly = !!options.latest 102 | var checkCommand = options.testCommand 103 | if (checkCommand) { 104 | verify.unemptyString(checkCommand, 'invalid test command ' + checkCommand) 105 | } 106 | var all = options.all 107 | if (all) { 108 | checkLatestOnly = true 109 | console.log('will check only latest versions because testing all') 110 | } 111 | 112 | if (check.string(options.without)) { 113 | options.without = [options.without] 114 | } 115 | 116 | if (check.string(moduleName)) { 117 | moduleName = [moduleName] 118 | } 119 | checkLatestOnly = !!checkLatestOnly 120 | if (checkCommand) { 121 | check.verify.string(checkCommand, 'expected string test command') 122 | } 123 | var toCheck = getDependenciesToCheck(options, moduleName) 124 | check.verify.array(toCheck, 'dependencies to check should be an array') 125 | 126 | makeSureValidModule(moduleName, toCheck) 127 | 128 | var testVersionsBound = testVersions.bind(null, { 129 | modules: toCheck, 130 | command: checkCommand, 131 | all: all, 132 | color: options.color, 133 | keep: options.keep, 134 | allowed: options.allow || options.allowed, 135 | tldr: options.tldr, 136 | type: options.type, 137 | limit: options.limit 138 | }) 139 | 140 | return isOnline() 141 | .then(function (online) { 142 | if (!online) { 143 | throw new Error('Need to be online to check new modules') 144 | } 145 | }).then(function () { 146 | if (isSingleSpecificVersion(moduleName)) { 147 | var nv = nameVersionParser(moduleName[0]) 148 | console.log('checking only specific:', nv.name, nv.version) 149 | var list = [{ 150 | name: nv.name, 151 | versions: [nv.version] 152 | }] 153 | return testVersionsBound(list) 154 | } else { 155 | var nextVersionsPromise = nextVersions(options, toCheck, checkLatestOnly) 156 | return nextVersionsPromise.then(testVersionsBound) 157 | } 158 | }) 159 | } 160 | 161 | function isSingleSpecificVersion (moduleNames) { 162 | if (!moduleNames) { 163 | return false 164 | } 165 | var name = moduleNames 166 | if (Array.isArray(moduleNames)) { 167 | if (moduleNames.length !== 1) { 168 | return false 169 | } 170 | name = moduleNames[0] 171 | } 172 | check.verify.string(name, 'expected module name string, not ' + 173 | JSON.stringify(name)) 174 | var parsed = nameVersionParser(name) 175 | if (check.object(parsed)) { 176 | return false 177 | } 178 | return check.string(parsed.version) 179 | } 180 | 181 | module.exports = { 182 | checkCurrentInstall: checkCurrentInstall, 183 | checkAllUpdates: checkAllUpdates, 184 | available: available 185 | } 186 | -------------------------------------------------------------------------------- /src/npm-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var debug = require('debug')('next-update') 4 | var check = require('check-more-types') 5 | var spawn = require('child_process').spawn 6 | var q = require('q') 7 | var _ = require('lodash') 8 | 9 | // hack to find npm bin script reliably 10 | function findNpmPath () { 11 | var os = require('os') 12 | var type = os.type() 13 | return (/windows/gi).test(type) ? 'npm.cmd' : 'npm' 14 | } 15 | 16 | var NPM_PATH = findNpmPath() 17 | debug('found npm path %s', NPM_PATH) 18 | 19 | function argsToString (arrayLike) { 20 | return Array.prototype.slice.call(arrayLike, 0).join(' ') 21 | } 22 | 23 | function writeToStderr () { 24 | process.stderr.write(argsToString(arguments)) 25 | } 26 | 27 | function writeToStdout () { 28 | process.stderr.write(argsToString(arguments)) 29 | } 30 | 31 | // returns a promise 32 | function test (options) { 33 | options = options || {} 34 | var log = options.tldr ? _.noop : writeToStdout 35 | var errorLog = options.tldr ? _.noop : writeToStderr 36 | log(' npm test') 37 | 38 | check.verify.string(NPM_PATH, 'missing npm path string') 39 | var npm = spawn(NPM_PATH, ['test']) 40 | var testOutput = '' 41 | var testErrors = '' 42 | 43 | npm.stdout.setEncoding('utf-8') 44 | npm.stderr.setEncoding('utf-8') 45 | 46 | npm.stdout.on('data', function (data) { 47 | testOutput += data 48 | log(data) 49 | }) 50 | 51 | npm.stderr.on('data', function (data) { 52 | testErrors += data 53 | log(data) 54 | }) 55 | 56 | npm.on('error', function (err) { 57 | errorLog(err) 58 | testErrors += err.toString() 59 | }) 60 | 61 | var deferred = q.defer() 62 | npm.on('exit', function (code) { 63 | if (code) { 64 | errorLog('npm test returned', code, '\n') 65 | errorLog('test output:\n' + testOutput) 66 | errorLog('test errors:\n' + testErrors) 67 | 68 | const e = new Error('npm test exit code means error') 69 | e.code = code 70 | e.errors = testErrors 71 | 72 | deferred.reject(e) 73 | } 74 | deferred.resolve() 75 | }) 76 | return deferred.promise 77 | } 78 | 79 | module.exports = { 80 | test: test, 81 | npmPath: findNpmPath() 82 | } 83 | -------------------------------------------------------------------------------- /src/print-modules-table.js: -------------------------------------------------------------------------------- 1 | var verify = require('check-more-types').verify 2 | var Table = require('easy-table') 3 | var colorProbability = require('./stats').colorProbability 4 | 5 | function markProbability (val, width) { 6 | if (width === null) { 7 | return val 8 | } 9 | return Table.padLeft(val, width) 10 | } 11 | 12 | function printModulesTable (modules, options) { 13 | verify.array(modules, 'expect an array of modules') 14 | var haveStats = modules.some(function (m) { 15 | return typeof m.stats === 'object' 16 | }) 17 | 18 | var t = new Table() 19 | modules.forEach(function (info) { 20 | verify.string(info.name, 'missing module name ' + info) 21 | verify.string(info.version, 'missing module version ' + info) 22 | 23 | t.cell('package', info.name) 24 | t.cell('current', info.from) 25 | t.cell('available', info.version) 26 | 27 | if (haveStats && info.stats) { 28 | var stats = info.stats 29 | verify.object(stats, 'expected stats object') 30 | 31 | var total = +stats.success + stats.failure 32 | var probability = total ? stats.success / total : null 33 | var probabilityStr = colorProbability(probability, options) 34 | t.cell('success %', probabilityStr, markProbability) 35 | t.cell('successful updates', info.stats.success, Table.Number(0)) 36 | t.cell('failed updates', info.stats.failure, Table.Number(0)) 37 | } 38 | t.newRow() 39 | }) 40 | console.log(t.toString()) 41 | } 42 | 43 | module.exports = printModulesTable 44 | -------------------------------------------------------------------------------- /src/registry-spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const snapShot = require('snap-shot') 3 | const {filterFetchedVersions, hasVersions} = require('./registry') 4 | const check = require('check-more-types') 5 | const la = require('lazy-ass') 6 | 7 | /* global describe, it */ 8 | describe('registry', () => { 9 | describe('filterFetchedVersions', () => { 10 | const foo = { 11 | name: 'foo', 12 | versions: ['1.0.0', '1.0.1'] 13 | } 14 | const bar = { 15 | name: 'bar', 16 | versions: [] 17 | } 18 | const baz = { 19 | name: 'baz', 20 | versions: ['3.0.0-alpha'] 21 | } 22 | const available = [foo, bar, baz] 23 | 24 | it('detects empty array property', () => { 25 | la(check.array(bar.versions), 'has versions') 26 | la(!check.unempty(bar.versions), 'is empty', bar.versions) 27 | }) 28 | 29 | it('hasVersions', () => { 30 | const filtered = available.filter(hasVersions) 31 | snapShot(filtered) 32 | }) 33 | 34 | it('filters out empty versions', () => { 35 | const filtered = filterFetchedVersions(false, available) 36 | snapShot(filtered) 37 | }) 38 | 39 | it('filters latest versions', () => { 40 | const filtered = filterFetchedVersions(true, available) 41 | snapShot(filtered) 42 | }) 43 | 44 | it('filters out alpha releases', () => { 45 | // make sure to filter out alpha releases 46 | // after fetching 47 | // https://github.com/bahmutov/next-update/issues/106 48 | const filtered = filterFetchedVersions(false, available) 49 | snapShot(filtered) 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var la = require('lazy-ass') 4 | var check = require('check-more-types') 5 | var log = require('debug')('next-update') 6 | const R = require('ramda') 7 | 8 | var request = require('request') 9 | var verify = check.verify 10 | var semver = require('semver') 11 | var q = require('q') 12 | var localVersion = require('./local-module-version') 13 | var isUrl = require('npm-utils').isUrl 14 | var _ = require('lodash') 15 | const {isPrerelease} = require('./utils') 16 | 17 | var _registryUrl = require('npm-utils').registryUrl 18 | la(check.fn(_registryUrl), 'expected registry url function') 19 | var registryUrl = _.once(_registryUrl) 20 | 21 | const notPrerelease = R.complement(isPrerelease) 22 | 23 | function cleanVersion (version, name) { 24 | var originalVersion = version 25 | verify.unemptyString(version, 'missing version string' + version) 26 | verify.unemptyString(name, 'missing name string' + name) 27 | 28 | if (isUrl(version)) { 29 | // version = version.substr(version.indexOf('#') + 1); 30 | 31 | // hmm, because we don't have a way to fetch git tags yet 32 | // just skip these dependencies 33 | console.log('Cannot handle git repos, skipping', name, 'at', version) 34 | return 35 | } 36 | if (version === 'original' || 37 | version === 'modified' || 38 | version === 'created') { 39 | return 40 | } 41 | 42 | version = version.replace('~', '').replace('^', '') 43 | var twoDigitVersion = /^\d+\.\d+$/ 44 | if (twoDigitVersion.test(version)) { 45 | version += '.0' 46 | } 47 | if (version === 'latest' || version === '*') { 48 | console.log('Module', name, 'uses version', version) 49 | console.log('It is recommented to list a specific version number') 50 | version = localVersion(name) 51 | if (!version) { 52 | version = '0.0.1' 53 | } 54 | console.log('module', name, 'local version', version) 55 | } 56 | try { 57 | version = semver.clean(version) 58 | } catch (err) { 59 | console.error('exception when cleaning version', version) 60 | return 61 | } 62 | if (!version) { 63 | log('could not clean version ' + originalVersion + ' for ' + name) 64 | return 65 | } 66 | console.assert(version, 'missing clean version ' + originalVersion + ' for ' + name) 67 | return version 68 | } 69 | 70 | // eslint-disable-next-line no-unused-vars 71 | function cleanVersionPair (nameVersion) { 72 | check.verify.array(nameVersion, 'expected an array') 73 | console.assert(nameVersion.length === 2, 74 | 'expected 2 items, name and version ' + nameVersion) 75 | var name = nameVersion[0] 76 | check.verify.string(name, 'could not get module name from ' + nameVersion) 77 | 78 | var version = nameVersion[1] 79 | check.verify.string(version, 'could not get module version from ' + nameVersion) 80 | version = cleanVersion(version, name) 81 | if (!version) { 82 | return 83 | } 84 | 85 | nameVersion[1] = version 86 | return nameVersion 87 | } 88 | 89 | function cleanVersionObject (info) { 90 | check.verify.object(info, 'expected info') 91 | var name = info.name 92 | check.verify.string(name, 'could not get module name from ' + info) 93 | 94 | var version = info.version 95 | check.verify.string(version, 'could not get module version from ' + info) 96 | version = cleanVersion(version, name) 97 | 98 | if (!version) { 99 | return 100 | } 101 | 102 | info.version = version 103 | return info 104 | } 105 | 106 | function cleanVersions (nameVersionPairs) { 107 | check.verify.array(nameVersionPairs, 'expected array') 108 | var cleaned = nameVersionPairs 109 | .map(cleanVersionObject) 110 | .filter(check.object) 111 | return cleaned 112 | } 113 | 114 | function formNpmErrorMessage (name, info) { 115 | var reason = info.reason || info.error || JSON.stringify(info) 116 | var str = 'ERROR in npm info for ' + name + ' reason ' + reason 117 | return str 118 | } 119 | 120 | function cleanVersionFor (name, version) { 121 | return cleanVersion(version, name) 122 | } 123 | 124 | function extractVersions (info) { 125 | if (info.time) { 126 | return Object.keys(info.time) 127 | } 128 | if (info.versions) { 129 | return Object.keys(info.versions) 130 | } 131 | } 132 | 133 | function is404 (response) { 134 | return response && response.statusCode === 404 135 | } 136 | 137 | function isNotFound (str) { 138 | var moduleNotFound = (/not found/).test(str) 139 | var cannotConnect = (/ENOTFOUND/).test(str) 140 | var errorInNpm = (/ERROR in npm/).test(str) 141 | var couldNotFetch = (/could not fetch/i).test(str) 142 | return moduleNotFound || cannotConnect || errorInNpm || couldNotFetch 143 | } 144 | 145 | // fetching versions inspired by 146 | // https://github.com/jprichardson/npm-latest 147 | // returns a promise 148 | function fetchVersions (options, nameVersion) { 149 | // console.log(nameVersion); 150 | // TODO use check.schema 151 | check.verify.object(nameVersion, 'expected name, version object') 152 | var name = nameVersion.name 153 | var version = nameVersion.version 154 | check.verify.string(name, 'missing name string') 155 | check.verify.string(version, 'missing version string') 156 | 157 | var cleanVersionForName = _.partial(cleanVersionFor, name) 158 | function isLaterVersion (ver) { 159 | var later = semver.gt(ver, version) 160 | return later 161 | } 162 | 163 | // console.log('fetching versions for', name, 'current version', version); 164 | var MAX_WAIT_TIMEOUT = options.checkVersionTimeout || 25000 165 | var deferred = q.defer() 166 | 167 | function rejectOnTimeout () { 168 | var msg = 'timed out waiting for NPM for package ' + name + 169 | ' after ' + MAX_WAIT_TIMEOUT + 'ms' 170 | console.error(msg) 171 | deferred.reject(msg) 172 | } 173 | 174 | function escapeName (str) { 175 | return str.replace('/', '%2F') 176 | } 177 | 178 | registryUrl().then(function (npmUrl) { 179 | log('NPM registry url', npmUrl) 180 | la(check.webUrl(npmUrl), 'need npm registry url, got', npmUrl) 181 | 182 | npmUrl = npmUrl.replace(/^https:/, 'http:').trim() 183 | var url = (options.registry || npmUrl) + escapeName(name) 184 | 185 | // TODO how to detect if the registry is not responding? 186 | 187 | log('getting url', url) 188 | request.get(url, onNPMversions) 189 | var timer = setTimeout(rejectOnTimeout, MAX_WAIT_TIMEOUT) 190 | 191 | function onNPMversions (err, response, body) { 192 | log('got response for', url) 193 | clearTimeout(timer) 194 | 195 | if (err) { 196 | console.error('ERROR when fetching info for package', name) 197 | deferred.reject(err.message) 198 | return 199 | } 200 | 201 | if (is404(response)) { 202 | log('404 response for', url) 203 | deferred.resolve({ 204 | name: name, 205 | versions: [] 206 | }) 207 | return 208 | } 209 | 210 | try { 211 | log('parsing response body') 212 | var info = JSON.parse(body) 213 | log('parsed response, error?', info.error) 214 | if (info.error) { 215 | log('error parsing\n' + body + '\n') 216 | var str = formNpmErrorMessage(name, info) 217 | console.error(str) 218 | 219 | if (isNotFound(info.error)) { 220 | deferred.resolve({ 221 | name: name, 222 | versions: [] 223 | }) 224 | return 225 | } 226 | 227 | deferred.reject(str) 228 | return 229 | } 230 | log('extracting versions') 231 | var versions = extractVersions(info) 232 | log('%d versions', versions.length) 233 | 234 | if (!Array.isArray(versions)) { 235 | throw new Error('Could not get versions for ' + name + 236 | ' from ' + JSON.stringify(info) + 237 | ' response ' + JSON.stringify(response, null, 2)) 238 | } 239 | 240 | var validVersions = versions.filter(cleanVersionForName) 241 | var newerVersions = validVersions.filter(isLaterVersion) 242 | log('%d valid versions', validVersions.length) 243 | if (validVersions.length) { 244 | log('last version %s', R.last(validVersions)) 245 | } 246 | if (newerVersions.length) { 247 | log('%d newer versions', newerVersions.length) 248 | log('from %s to %s', R.head(newerVersions), R.last(newerVersions)) 249 | } 250 | 251 | deferred.resolve({ 252 | name: name, 253 | versions: newerVersions 254 | }) 255 | return 256 | } catch (err) { 257 | console.error(err) 258 | deferred.reject('Could not fetch versions for ' + name) 259 | } 260 | } 261 | }) 262 | 263 | return deferred.promise 264 | } 265 | 266 | const verboseLog = (options) => options.tldr ? _.noop : console.log 267 | 268 | function logFetched (fetched) { 269 | fetched.forEach(individual => { 270 | log('%s - %d versions', individual.name, individual.versions.length) 271 | }) 272 | } 273 | 274 | const hasVersions = nameNewVersions => 275 | nameNewVersions && 276 | check.array(nameNewVersions.versions) && 277 | check.unempty(nameNewVersions.versions) 278 | 279 | const filterVersions = R.evolve({ 280 | versions: R.filter(notPrerelease) 281 | }) 282 | 283 | function filterFetchedVersions (checkLatestOnly, results) { 284 | la(arguments.length === 2, 'expected two arguments', arguments) 285 | checkLatestOnly = Boolean(checkLatestOnly) 286 | check.verify.array(results, 'expected list of results') 287 | log('fetch all new version results') 288 | logFetched(results) 289 | 290 | let available = results 291 | .map(filterVersions) 292 | .filter(hasVersions) 293 | 294 | if (checkLatestOnly) { 295 | available = available.map(function (nameVersions) { 296 | if (nameVersions.versions.length > 1) { 297 | nameVersions.versions = nameVersions.versions.slice(-1) 298 | } 299 | return nameVersions 300 | }) 301 | } else { 302 | verboseLog('checking ALL versions') 303 | } 304 | return available 305 | } 306 | 307 | // returns a promise with available new versions 308 | function nextVersions (options, nameVersionPairs, checkLatestOnly) { 309 | check.verify.object(options, 'expected object with options') 310 | check.verify.array(nameVersionPairs, 'expected array') 311 | nameVersionPairs = cleanVersions(nameVersionPairs) 312 | 313 | const verbose = verboseLog(options) 314 | verbose('checking NPM registry') 315 | var MAX_CHECK_TIMEOUT = options.checkVersionTimeout || 10000 316 | 317 | var fetchPromises = nameVersionPairs.map(fetchVersions.bind(null, options)) 318 | var fetchAllPromise = q.all(fetchPromises) 319 | .timeout(MAX_CHECK_TIMEOUT, 'timed out waiting for NPM after ' + MAX_CHECK_TIMEOUT + 'ms') 320 | 321 | return fetchAllPromise.then( 322 | _.partial(filterFetchedVersions, checkLatestOnly), 323 | q.reject 324 | ) 325 | } 326 | 327 | module.exports = { 328 | isUrl, 329 | cleanVersion, 330 | cleanVersions, 331 | fetchVersions, 332 | nextVersions, 333 | filterFetchedVersions, 334 | hasVersions 335 | } 336 | -------------------------------------------------------------------------------- /src/report-available.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var la = require('lazy-ass') 4 | var check = require('check-more-types') 5 | var log = require('debug')('next-update') 6 | 7 | require('console.json') 8 | var print = require('./print-modules-table') 9 | var stats = require('./stats') 10 | var clc = require('cli-color') 11 | var getSuccess = stats.getSuccessStats 12 | var q = require('q') 13 | 14 | function ignore () {} 15 | 16 | function reportAvailable (available, currentVersions, options) { 17 | la(check.array(available), 'expect an array of info objects', available) 18 | 19 | if (!available.length) { 20 | console.log('nothing new is available') 21 | return [] 22 | } 23 | 24 | log('report available') 25 | log(available) 26 | log('current versions') 27 | log(JSON.stringify(currentVersions)) 28 | 29 | la(check.maybe.object(currentVersions), 30 | 'expected current versions as an object, but was', currentVersions) 31 | 32 | function getCurrentVersion (name) { 33 | la(check.unemptyString(name), 'missing name') 34 | if (!currentVersions) { 35 | return 36 | } 37 | if (check.string(currentVersions[name])) { 38 | return currentVersions[name] 39 | } 40 | if (check.object(currentVersions[name])) { 41 | la(check.string(currentVersions[name].version), 42 | 'missing version for', name, 'in', currentVersions) 43 | return currentVersions[name].version 44 | } 45 | } 46 | 47 | var chain = q() 48 | var updateStats = {} 49 | 50 | available.forEach(function (info) { 51 | la(check.unemptyString(info.name), 'missing module name', info) 52 | la(check.array(info.versions), 'missing module versions', info) 53 | 54 | var currentVersion = getCurrentVersion(info.name) 55 | log('version for', info.name, currentVersion) 56 | 57 | if (currentVersion) { 58 | la(check.unemptyString(currentVersion), 59 | 'missing version', currentVersion, 'for', info.name) 60 | updateStats[info.name] = { 61 | name: info.name, 62 | from: currentVersion 63 | } 64 | } 65 | 66 | if (info.versions.length === 1) { 67 | if (currentVersion) { 68 | chain = chain.then(function () { 69 | return getSuccess({ 70 | name: info.name, 71 | from: currentVersion, 72 | to: info.versions[0] 73 | }).then(function (stats) { 74 | updateStats[info.name] = stats 75 | }).catch(ignore) 76 | }) 77 | } 78 | } 79 | }) 80 | 81 | return chain.then(function () { 82 | var modules = [] 83 | 84 | available.forEach(function (info) { 85 | la(check.unemptyString(info.name), 'missing module name', info) 86 | la(check.array(info.versions), 'missing module versions', info) 87 | 88 | var sep = ', ' 89 | var versions 90 | 91 | if (info.versions.length < 5) { 92 | versions = info.versions.join(sep) 93 | } else { 94 | versions = info.versions.slice(0, 2) 95 | .concat('...') 96 | .concat(info.versions.slice(info.versions.length - 2)) 97 | .join(sep) 98 | } 99 | 100 | var stats = updateStats[info.name] 101 | if (!stats) { 102 | return 103 | } 104 | la(check.object(stats), 'could not get stats for', info.name, 105 | 'all', updateStats, 'available stats', info) 106 | modules.push({ 107 | name: info.name, 108 | version: versions, 109 | from: stats.from, 110 | stats: stats 111 | }) 112 | }) 113 | console.log('\navailable updates:') 114 | if (modules.length) { 115 | print(modules, options) 116 | console.log('update stats from', clc.underline(stats.url)) 117 | } else { 118 | console.log(available) 119 | console.log('no stats is available yet for these updates') 120 | } 121 | return available 122 | }) 123 | } 124 | 125 | module.exports = reportAvailable 126 | -------------------------------------------------------------------------------- /src/report-install-command.js: -------------------------------------------------------------------------------- 1 | var check = require('check-more-types') 2 | 3 | function loadPackage () { 4 | var filename = './package.json' 5 | var fs = require('fs') 6 | if (fs.existsSync(filename)) { 7 | var txt = fs.readFileSync(filename, 'utf-8') 8 | if (txt) { 9 | return JSON.parse(txt) 10 | } 11 | } 12 | } 13 | 14 | function saveOption (type) { 15 | var saveCommands = { 16 | dependencies: '--save --save-exact', 17 | devDependencies: '--save-dev --save-exact', 18 | peerDependencies: '--save-peer --save-exact' 19 | } 20 | return saveCommands[type] 21 | } 22 | 23 | function splitByType (updates, pkg) { 24 | check.verify.array(updates, 'expected array of updates') 25 | check.verify.object(pkg, 'expected package object') 26 | 27 | var result = { 28 | dependencies: [], 29 | devDependencies: [], 30 | peerDependencies: [] 31 | } 32 | 33 | updates.forEach(function (moduleList) { 34 | if (!moduleList.length) { 35 | return 36 | } 37 | var moduleName = moduleList[0].name 38 | check.verify.string(moduleName, 'missing module name') 39 | if (pkg.dependencies && pkg.dependencies[moduleName]) { 40 | result.dependencies.push(moduleList) 41 | } else if (pkg.devDependencies && pkg.devDependencies[moduleName]) { 42 | result.devDependencies.push(moduleList) 43 | } else if (pkg.peerDependencies && pkg.peerDependencies[moduleName]) { 44 | result.peerDependencies.push(moduleList) 45 | } else { 46 | console.warn('Could not find dependency for', moduleName, 'assuming normal') 47 | result.dependencies.push(moduleList) 48 | } 49 | }) 50 | return result 51 | } 52 | 53 | function installCommand (updates) { 54 | check.verify.array(updates, 'expected array of updates') 55 | if (!updates.length) { 56 | return 57 | } 58 | 59 | var cmd = '' 60 | var pkg = loadPackage() 61 | if (!pkg) { 62 | // assume all dependencies are normal 63 | cmd = installCommandType(updates, 'dependencies') 64 | } else { 65 | var updatesByDependencyType = splitByType(updates, pkg) 66 | console.assert(updatesByDependencyType, 'could not split updates by type') 67 | 68 | // console.log(updatesByDependencyType) 69 | 70 | var depCmd = installCommandType(updatesByDependencyType.dependencies, 'dependencies') 71 | var devCmd = installCommandType(updatesByDependencyType.devDependencies, 'devDependencies') 72 | var peerCmd = installCommandType(updatesByDependencyType.peerDependencies, 73 | 'peerDependencies') 74 | if (depCmd) { 75 | cmd += depCmd + '\n' 76 | } 77 | if (devCmd) { 78 | cmd += devCmd + '\n' 79 | } 80 | if (peerCmd) { 81 | cmd += peerCmd + '\n' 82 | } 83 | } 84 | if (cmd) { 85 | return cmd 86 | } 87 | } 88 | 89 | function getLatestWorkingVersion (versions) { 90 | check.verify.array(versions, 'expected array of versions') 91 | var working 92 | versions.forEach(function (info) { 93 | check.verify.string(info.name, 'missing package name ' + info) 94 | if (info.works) { 95 | working = info 96 | } 97 | }) 98 | 99 | return working 100 | } 101 | 102 | function setWorkingVersion (updates) { 103 | updates.forEach(function (moduleVersions) { 104 | moduleVersions.latestWorkingVersion = getLatestWorkingVersion(moduleVersions) 105 | }) 106 | } 107 | 108 | function installCommandType (updates, type) { 109 | check.verify.array(updates, 'expected array of updates') 110 | if (!updates.length) { 111 | return 112 | } 113 | check.verify.string(type, 'missing type') 114 | 115 | var saveCommand = saveOption(type) 116 | if (!saveCommand) { 117 | throw new Error('invalid dependency type ' + type) 118 | } 119 | 120 | setWorkingVersion(updates) 121 | 122 | var originalCmd, cmd 123 | originalCmd = cmd = 'npm install ' + saveCommand 124 | updates.forEach(function (moduleVersions) { 125 | var latestWorkingVersion = moduleVersions.latestWorkingVersion 126 | if (latestWorkingVersion) { 127 | cmd += ' ' + latestWorkingVersion.name + '@' + latestWorkingVersion.version 128 | } 129 | }) 130 | 131 | if (originalCmd === cmd) { 132 | return 133 | } 134 | 135 | return cmd 136 | } 137 | 138 | module.exports = installCommand 139 | -------------------------------------------------------------------------------- /src/report.js: -------------------------------------------------------------------------------- 1 | var colors = require('cli-color') 2 | var check = require('check-more-types') 3 | var formInstallCommand = require('./report-install-command') 4 | var _ = require('lodash') 5 | var changedLog = require('changed-log') 6 | var Q = require('q') 7 | 8 | var colorAvailable = process.stdout.isTTY 9 | 10 | function report (updates, options) { 11 | options = options || {} 12 | var useColors = Boolean(options.useColors) && colorAvailable 13 | check.verify.array(updates, 'expected array of updates') 14 | // sets latest working version for each module too 15 | var cmd = formInstallCommand(updates) 16 | 17 | console.log('\n> next updates:') 18 | updates.forEach(function (moduleVersions) { 19 | reportModule(moduleVersions, useColors) 20 | }) 21 | 22 | var reportChanges = updates.map(function (moduleVersions) { 23 | return _.partial(printChangedLog, moduleVersions, useColors) 24 | }) 25 | 26 | function printInstallCommand () { 27 | if (_.isUndefined(cmd)) { 28 | console.log('> nothing can be updated :(') 29 | } else { 30 | if (options.keptUpdates) { 31 | console.log('> kept working updates') 32 | } else { 33 | cmd = cmd.trim() 34 | var lines = cmd.split('\n').length 35 | if (lines === 1) { 36 | console.log('> use the following command to install working versions') 37 | } else { 38 | console.log('> use the following commands to install working versions') 39 | } 40 | console.log(cmd) 41 | } 42 | } 43 | } 44 | 45 | function printError (err) { 46 | console.error('Error reporting changes') 47 | console.error(err.message) 48 | } 49 | var start = options.changedLog 50 | ? reportChanges.reduce(Q.when, Q()).catch(printError) 51 | : Q() 52 | return start.then(printInstallCommand) 53 | } 54 | 55 | function reportModule (moduleVersions, useColors) { 56 | check.verify.array(moduleVersions, 'expected module / versions array') 57 | if (!moduleVersions.length) { 58 | return 59 | } 60 | var name = moduleVersions[0].name 61 | check.verify.unemptyString(name, 'missing module name from ' + JSON.stringify(moduleVersions)) 62 | var fromVersion = moduleVersions[0].from 63 | check.verify.unemptyString(fromVersion, 'missing from version from ' + JSON.stringify(moduleVersions)) 64 | 65 | if (useColors) { 66 | var colorVersions = moduleVersions.map(function (info) { 67 | return (info.works ? colors.greenBright : colors.redBright)(info.version) 68 | }) 69 | var str = colorVersions.join(', ') 70 | console.log(name + ' ' + fromVersion + ' -> ' + str) 71 | } else { 72 | console.log(name + '@' + fromVersion) 73 | moduleVersions.forEach(function (info) { 74 | console.log(' ' + info.version + ' ' + (info.works ? 'PASS' : 'FAIL')) 75 | }) 76 | } 77 | } 78 | 79 | function printChangedLog (moduleVersions, useColors) { 80 | var info = moduleVersions[0] 81 | 82 | if (!info.works) { 83 | return 84 | } 85 | return changedLog({ 86 | name: info.name, 87 | from: info.from, 88 | to: info.version 89 | }) 90 | } 91 | 92 | function reportSuccess (text, useColors) { 93 | if (colorAvailable && useColors) { 94 | console.log(colors.greenBright(text)) 95 | } else { 96 | console.log('PASS', text) 97 | } 98 | } 99 | 100 | function reportFailure (text, useColors) { 101 | if (colorAvailable && useColors) { 102 | console.log(colors.redBright(text)) 103 | } else { 104 | console.log('FAIL', text) 105 | } 106 | } 107 | 108 | module.exports = { 109 | report: report, 110 | reportSuccess: reportSuccess, 111 | reportFailure: reportFailure 112 | } 113 | -------------------------------------------------------------------------------- /src/revert.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('next-update') 2 | const is = require('check-more-types') 3 | const la = require('lazy-ass') 4 | var getDependenciesToCheck = require('./dependencies') 5 | var installModule = require('./module-install') 6 | var q = require('q') 7 | 8 | const isRevertInfo = is.schema({ 9 | name: is.unemptyString, 10 | version: is.unemptyString 11 | }) 12 | 13 | // returns promise 14 | function revert (moduleName) { 15 | if (moduleName) { 16 | console.log('reverting module', JSON.stringify(moduleName)) 17 | } 18 | 19 | var toCheck = getDependenciesToCheck({}, moduleName) 20 | debug('need to check') 21 | debug(toCheck) 22 | 23 | var installPromises = toCheck.map(function (info) { 24 | la(isRevertInfo(info), 'invalid revert info', info) 25 | return installModule.bind(null, { 26 | name: info.name, 27 | version: info.version, 28 | tldr: false 29 | }) 30 | }) 31 | return installPromises.reduce(q.when, q()) 32 | } 33 | 34 | module.exports = revert 35 | -------------------------------------------------------------------------------- /src/stats-spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const snapShot = require('snap-shot') 3 | 4 | /* global describe, it */ 5 | describe('stats', () => { 6 | describe('formatStats', () => { 7 | const {formatStats} = require('./stats') 8 | it('formats stats message', () => { 9 | const message = formatStats({}, { 10 | name: 'foo', 11 | from: '1.0.0', 12 | to: '2.0.0', 13 | success: 5, 14 | failure: 5 15 | }) 16 | snapShot(message) 17 | }) 18 | 19 | it('formats stats message with singular failure', () => { 20 | const message = formatStats({}, { 21 | name: 'foo', 22 | from: '1.0.0', 23 | to: '2.0.0', 24 | success: 1, 25 | failure: 1 26 | }) 27 | snapShot(message) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/stats.js: -------------------------------------------------------------------------------- 1 | var verify = require('check-more-types').verify 2 | var request = require('request') 3 | var Q = require('q') 4 | var colors = require('cli-color') 5 | var colorAvailable = process.stdout.isTTY 6 | const pluralize = require('pluralize') 7 | const {oneLine} = require('common-tags') 8 | 9 | var nextUpdateStatsUrl = require('../package.json')['next-update-stats'] || 10 | 'http://next-update.herokuapp.com' 11 | 12 | function sendUpdateResult (options) { 13 | verify.unemptyString(options.name, 'missing name') 14 | verify.unemptyString(options.from, 'missing from version') 15 | verify.unemptyString(options.to, 'missing to version') 16 | if (options.success) { 17 | options.success = !!options.success 18 | } 19 | 20 | verify.webUrl(nextUpdateStatsUrl, 'missing next update stats server url') 21 | var sendOptions = { 22 | uri: nextUpdateStatsUrl + '/update', 23 | method: 'POST', 24 | json: options 25 | } 26 | request(sendOptions, function ignoreResponse () {}) 27 | } 28 | 29 | function getSuccessStats (options) { 30 | verify.unemptyString(options.name, 'missing name') 31 | verify.unemptyString(options.from, 'missing from version') 32 | verify.unemptyString(options.to, 'missing to version') 33 | 34 | verify.webUrl(nextUpdateStatsUrl, 'missing next update stats server url') 35 | var opts = { 36 | uri: nextUpdateStatsUrl + '/package/' + options.name + '/' + options.from + 37 | '/' + options.to, 38 | method: 'GET', 39 | json: options 40 | } 41 | var defer = Q.defer() 42 | request(opts, function (err, response, stats) { 43 | if (err || response.statusCode !== 200) { 44 | if (response) { 45 | if (response.statusCode !== 404) { 46 | console.error('could not get success stats for', options.name) 47 | console.error(opts) 48 | } 49 | console.error('response status:', response.statusCode) 50 | } 51 | if (err) { 52 | console.error(err) 53 | } 54 | defer.reject(err) 55 | return 56 | } 57 | defer.resolve(stats) 58 | }) 59 | return defer.promise 60 | } 61 | 62 | function colorProbability (probability, options) { 63 | if (probability === null) { 64 | return '' 65 | } 66 | 67 | options = options || {} 68 | var useColors = !!options.color && colorAvailable 69 | if (probability < 0 || probability > 1) { 70 | throw new Error('Expected probability between 0 and 1, not ' + probability) 71 | } 72 | var probabilityStr = (probability * 100).toFixed(0) + '%' 73 | if (useColors) { 74 | if (probability > 0.8) { 75 | probabilityStr = colors.greenBright(probabilityStr) 76 | } else if (probability > 0.5) { 77 | probabilityStr = colors.yellowBright(probabilityStr) 78 | } else { 79 | probabilityStr = colors.redBright(probabilityStr) 80 | } 81 | } 82 | return probabilityStr 83 | } 84 | 85 | function formatStats (options, stats) { 86 | verify.object(stats, 'missing stats object') 87 | verify.unemptyString(stats.name, 'missing name') 88 | verify.unemptyString(stats.from, 'missing from version') 89 | verify.unemptyString(stats.to, 'missing to version') 90 | stats.success = +stats.success || 0 91 | stats.failure = +stats.failure || 0 92 | var total = stats.success + stats.failure 93 | var probability = (total > 0 ? stats.success / total : 0) 94 | var probabilityStr = colorProbability(probability, options) 95 | return oneLine` 96 | stats: ${stats.name} ${stats.from} -> ${stats.to} 97 | success probability ${probabilityStr} 98 | ${stats.success} ${pluralize('success', stats.success)} 99 | ${stats.failure} ${pluralize('failure', stats.failure)} 100 | ` 101 | } 102 | 103 | function printStats (options, stats) { 104 | const message = formatStats(options, stats) 105 | console.log(message) 106 | } 107 | 108 | module.exports = { 109 | sendUpdateResult: sendUpdateResult, 110 | getSuccessStats: getSuccessStats, 111 | formatStats: formatStats, 112 | printStats: printStats, 113 | colorProbability: colorProbability, 114 | url: nextUpdateStatsUrl 115 | } 116 | -------------------------------------------------------------------------------- /src/test-module-version.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass') 2 | var check = require('check-more-types') 3 | var verify = check.verify 4 | var q = require('q') 5 | var _ = require('lodash') 6 | var semver = require('semver') 7 | var quote = require('quote') 8 | var installModule = require('./module-install') 9 | var reportSuccess = require('./report').reportSuccess 10 | var reportFailure = require('./report').reportFailure 11 | const {getTestCommand} = require('./utils') 12 | const path = require('path') 13 | const debug = require('debug')('next-update') 14 | var stats = require('./stats') 15 | 16 | var cleanVersions = require('./registry').cleanVersions 17 | check.verify.fn(cleanVersions, 'cleanVersions should be a function') 18 | 19 | var revertModules = require('./revert') 20 | check.verify.fn(revertModules, 'revert is not a function, but ' + 21 | JSON.stringify(revertModules)) 22 | 23 | var npmTest = require('./npm-test').test 24 | var execTest = require('./exec-test') 25 | var report = require('./report-available') 26 | var filterAllowed = require('./filter-allowed-updates') 27 | 28 | // expect array of objects, each {name, versions (Array) } 29 | // returns promise 30 | function testModulesVersions (options, available) { 31 | verify.object(options, 'missing options') 32 | verify.array(available, 'expected array of available modules') 33 | 34 | var cleaned = cleanVersions(options.modules) 35 | // console.log('cleaned', cleaned); 36 | // var listed = _.zipObject(cleaned); 37 | var names = _.pluck(cleaned, 'name') 38 | var listed = _.zipObject(names, cleaned) 39 | 40 | /* 41 | console.log('testing module versions'); 42 | console.log('current versions', listed); 43 | console.log('options', options); 44 | console.log('available', available); 45 | */ 46 | 47 | var allowed = filterAllowed(listed, available, options) 48 | la(check.array(allowed), 'could not filter allowed updates', listed, available, options) 49 | 50 | if (available.length && !allowed.length) { 51 | console.log('No updates allowed using option', quote(options.allow || options.allowed)) 52 | console.log(available.length + ' available updates filtered') 53 | return q(listed) 54 | } 55 | 56 | // console.log('allowed', allowed); 57 | return q.when(report(allowed, listed, options)) 58 | .then(function testInstalls () { 59 | // console.log('testing installs'); 60 | if (options.all) { 61 | var install = installAll(allowed, options) 62 | console.assert(install, 'could not get install all promise') 63 | var test = testPromise(options, options.command) 64 | console.assert(test, 'could not get test promise for command', options.command) 65 | // console.dir(listed); 66 | // console.dir(options.modules); 67 | 68 | var installThenTest = install.then(test) 69 | if (options.keep) { 70 | return installThenTest 71 | } 72 | 73 | var revert = revertModules.bind(null, listed) 74 | console.assert(revert, 'could not get revert promise') 75 | return installThenTest.then(revert) 76 | } 77 | 78 | return installEachTestRevert(listed, allowed, 79 | options.command, options.color, options.keep, options.tldr) 80 | }) 81 | } 82 | 83 | // returns promise, does not revert 84 | function installAll (available, options) { 85 | verify.array(available, 'expected array') 86 | 87 | var installFunctions = available.map(function (nameVersions) { 88 | var name = nameVersions.name 89 | var version = nameVersions.versions[0] 90 | verify.string(name, 'missing module name from ' + 91 | JSON.stringify(nameVersions)) 92 | verify.string(version, 'missing module version from ' + 93 | JSON.stringify(nameVersions)) 94 | 95 | var installOptions = { 96 | name: name, 97 | version: version, 98 | keep: options.keep, 99 | tldr: options.tldr 100 | } 101 | var installFunction = installModule.bind(null, installOptions) 102 | return installFunction 103 | }) 104 | var installAllPromise = installFunctions.reduce(q.when, q()) 105 | return installAllPromise 106 | } 107 | 108 | function installEachTestRevert (listed, available, command, color, keep, tldr) { 109 | verify.object(listed, 'expected listed object') 110 | verify.array(available, 'expected array') 111 | 112 | const packageFilename = path.resolve('./package.json') 113 | const getCommand = _.partial(getTestCommand, packageFilename) 114 | 115 | var checkModulesFunctions = available.map(function (nameVersion) { 116 | var name = nameVersion.name 117 | la(check.unemptyString(name), 'missing name', nameVersion) 118 | var currentVersion = listed[name].version 119 | la(check.string(currentVersion), 'cannot find current version for', name, 120 | 'among current dependencies', listed) 121 | 122 | var installOptions = { 123 | name: name, 124 | version: currentVersion, 125 | keep: keep, 126 | tldr: tldr 127 | } 128 | var revertFunction = installModule.bind(null, installOptions) 129 | 130 | const testCommand = getCommand(name) || command 131 | debug('module %s should use test command "%s"', name, testCommand) 132 | var checkModuleFunction = testModuleVersions.bind(null, { 133 | moduleVersions: nameVersion, 134 | revertFunction: revertFunction, 135 | command: testCommand, 136 | color: color, 137 | currentVersion: currentVersion, 138 | keep: keep, 139 | tldr: tldr 140 | }) 141 | return checkModuleFunction 142 | }) 143 | var checkAllPromise = checkModulesFunctions.reduce(q.when, q()) 144 | return checkAllPromise 145 | } 146 | 147 | // test particular dependency with multiple versions 148 | // returns promise 149 | function testModuleVersions (options, results) { 150 | verify.object(options, 'missing options') 151 | var nameVersions = options.moduleVersions 152 | var restoreVersionFunc = options.revertFunction 153 | 154 | var name = nameVersions.name 155 | var versions = nameVersions.versions 156 | verify.string(name, 'expected name string') 157 | verify.array(versions, 'expected versions array') 158 | results = results || [] 159 | verify.array(results, 'expected results array') 160 | if (!semver.valid(options.currentVersion)) { 161 | throw new Error('do not have current version for ' + name) 162 | } 163 | 164 | var deferred = q.defer() 165 | var checkPromises = versions.map(function (version) { 166 | return testModuleVersion.bind(null, { 167 | name: name, 168 | version: version, 169 | command: options.command, 170 | color: options.color, 171 | currentVersion: options.currentVersion, 172 | tldr: options.tldr 173 | }) 174 | }) 175 | var checkAllPromise = checkPromises.reduce(q.when, q()) 176 | if (options.keep) { 177 | debug('keep working updates for %s', name) 178 | checkAllPromise = checkAllPromise.then(function (result) { 179 | verify.array(result, 'expected array of results', result) 180 | var lastSuccess = _.last(_.filter(result, { works: true })) 181 | if (lastSuccess) { 182 | console.log('keeping last working version', lastSuccess.name + '@' + lastSuccess.version) 183 | return installModule({ 184 | name: lastSuccess.name, 185 | version: lastSuccess.version, 186 | keep: true, 187 | tldr: options.tldr 188 | }, result) 189 | } else { 190 | return restoreVersionFunc().then(function () { 191 | // console.log('returning result after reverting', result); 192 | return q(result) 193 | }) 194 | } 195 | }) 196 | } else { 197 | checkAllPromise = checkAllPromise 198 | .then(restoreVersionFunc, (err) => { 199 | console.error('Could not check all versions') 200 | console.error(err) 201 | throw err 202 | }) 203 | } 204 | checkAllPromise 205 | .then(function (result) { 206 | debug('got result') 207 | debug(result) 208 | check.verify.array(result, 'could not get result array') 209 | results.push(result) 210 | deferred.resolve(results) 211 | }, function (error) { 212 | console.error('could not check', nameVersions, error) 213 | deferred.reject(error) 214 | }) 215 | 216 | return deferred.promise 217 | } 218 | 219 | var logLine = (function formLine () { 220 | var n = process.stdout.isTTY ? process.stdout.columns : 40 221 | n = n || 40 222 | verify.positiveNumber(n, 'expected to get terminal width, got ' + n) 223 | var k 224 | var str = '' 225 | for (k = 0; k < n; k += 1) { 226 | str += '-' 227 | } 228 | return function () { 229 | console.log(str) 230 | } 231 | }()) 232 | 233 | // checks specific module@version 234 | // returns promise 235 | function testModuleVersion (options, results) { 236 | verify.object(options, 'missing test module options') 237 | verify.string(options.name, 'missing module name') 238 | verify.string(options.version, 'missing version string') 239 | verify.unemptyString(options.currentVersion, 'missing current version') 240 | 241 | if (options.command) { 242 | verify.string(options.command, 'expected command string') 243 | } 244 | // console.log('options', options); 245 | 246 | results = results || [] 247 | verify.array(results, 'missing previous results array') 248 | 249 | var nameVersion = options.name + '@' + options.version 250 | 251 | if (!options.tldr) { 252 | console.log('\ntesting', nameVersion) 253 | } 254 | 255 | var result = { 256 | name: options.name, 257 | version: options.version, 258 | from: options.currentVersion, 259 | works: true 260 | } 261 | 262 | var test = testPromise(options, options.command) 263 | console.assert(test, 'could not get test promise for command', options.command) 264 | 265 | var deferred = q.defer() 266 | 267 | var getSuccess = stats.getSuccessStats({ 268 | name: options.name, 269 | from: options.currentVersion, 270 | to: options.version 271 | }) 272 | 273 | getSuccess 274 | .then(stats.printStats.bind(null, options), function () { 275 | console.log('could not get update stats', options.name) 276 | }) 277 | .then(function () { 278 | return installModule({ 279 | name: options.name, 280 | version: options.version, 281 | keep: false, 282 | tldr: options.tldr 283 | }) 284 | }) 285 | .then(test) 286 | .then(function () { 287 | reportSuccess(nameVersion + ' works', options.color) 288 | 289 | stats.sendUpdateResult({ 290 | name: options.name, 291 | from: options.currentVersion, 292 | to: options.version, 293 | success: true 294 | }) 295 | results.push(result) 296 | deferred.resolve(results) 297 | }, function (error) { 298 | reportFailure(nameVersion + ' tests failed :(', options.color) 299 | 300 | debug('sending stats results') 301 | stats.sendUpdateResult({ 302 | name: options.name, 303 | from: options.currentVersion, 304 | to: options.version, 305 | success: false 306 | }) 307 | 308 | debug('checking error code', error.code) 309 | verify.number(error.code, 'expected code in error ' + 310 | JSON.stringify(error, null, 2)) 311 | 312 | var horizontalLine = options.tldr ? _.noop : logLine 313 | 314 | horizontalLine() 315 | if (!options.tldr) { 316 | console.error('test finished with exit code', error.code) 317 | verify.string(error.errors, 'expected errors string in error ' + 318 | JSON.stringify(error, null, 2)) 319 | console.error(error.errors) 320 | } 321 | 322 | horizontalLine() 323 | 324 | result.works = false 325 | results.push(result) 326 | deferred.resolve(results) 327 | }) 328 | return deferred.promise 329 | } 330 | 331 | function testPromise (options, command) { 332 | var testFunction = npmTest.bind(null, options) 333 | if (command) { 334 | verify.unemptyString(command, 'expected string command, not ' + command) 335 | testFunction = execTest.bind(null, options, command) 336 | } else { 337 | debug('missing test command') 338 | } 339 | return testFunction 340 | } 341 | 342 | module.exports = { 343 | testModulesVersions: testModulesVersions, 344 | testModuleVersion: testModuleVersion, 345 | testPromise: testPromise 346 | } 347 | -------------------------------------------------------------------------------- /src/test/all-local-deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "foo": "0.0.1" 4 | }, 5 | "devDependencies": { 6 | "bar": "~0.2.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/test/exec-test.coffee: -------------------------------------------------------------------------------- 1 | gt.module 'exec test', 2 | setup: -> 3 | @dir = process.cwd() 4 | process.chdir __dirname 5 | console.log 'changed directory to', __dirname 6 | teardown: -> 7 | console.log 'restoring dir', @dir 8 | process.chdir @dir 9 | 10 | test = require '../exec-test' 11 | npmPath = (require '../npm-test').npmPath 12 | 13 | ONE_MINUTE = 60000 14 | 15 | onError = (error) -> throw new Error(error) 16 | 17 | gt.test 'basics', -> 18 | gt.arity test, 2 19 | 20 | gt.async 'using npm test command', -> 21 | gt.string npmPath, 'has npm path' 22 | promise = test {}, npmPath + ' test' 23 | gt.object promise 24 | promise.then -> 25 | gt.ok false, 'there should not be npm test' 26 | .catch -> 27 | console.log 'Failed as expected' 28 | gt.ok true, 'failed as expected' 29 | .finally -> 30 | console.log 'finished test' 31 | gt.start() 32 | , ONE_MINUTE 33 | 34 | gt.async 'auto switch to npm test command', -> 35 | promise = test {}, 'npm test' 36 | gt.object promise 37 | promise.then -> 38 | gt.ok false, 'there should not be npm test' 39 | .catch -> 40 | console.log 'failed as expected' 41 | gt.ok true, 'failed as expected' 42 | .finally -> 43 | console.log 'finished test' 44 | gt.start() 45 | , ONE_MINUTE 46 | -------------------------------------------------------------------------------- /src/test/get-known-dependencies.coffee: -------------------------------------------------------------------------------- 1 | gt.module 'handling npmrg vs git urls' 2 | 3 | path = require 'path' 4 | getDeps = require '../get-known-dependencies' 5 | 6 | gt.test 'package with all npmrg', -> 7 | deps = getDeps path.join(__dirname, 'all-local-deps.json') 8 | gt.array deps, 'returns an array' 9 | gt.equal deps.length, 2, 'contains two items' 10 | 11 | gt.equal deps[0].name, 'foo', 'first item\'s name' 12 | gt.equal deps[0].version, '0.0.1', 'first item\'s version' 13 | gt.equal deps[0].type, 'prod' 14 | 15 | gt.equal deps[1].name, 'bar', 'second item\'s name' 16 | gt.equal deps[1].version, '0.2.2', 'second item\'s version' 17 | 18 | gt.test 'package with urls', -> 19 | deps = getDeps path.join(__dirname, 'git-url-deps.json') 20 | gt.array deps, 'returns an array' 21 | console.dir deps; 22 | gt.equal deps.length, 1, 'contains single item' 23 | gt.equal deps[0].name, 'bar', 'public item name' 24 | gt.equal deps[0].version, '0.2.2', 'public item version' 25 | 26 | -------------------------------------------------------------------------------- /src/test/git-url-deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "foo": "https://github.com/bahmutov/qunit-promises.git#0.0.7" 4 | }, 5 | "devDependencies": { 6 | "bar": "~0.2.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/test/local-module-version.coffee: -------------------------------------------------------------------------------- 1 | gt.module 'npm test', 2 | setup: -> 3 | @dir = process.cwd() 4 | process.chdir __dirname 5 | console.log 'changed directory to', __dirname 6 | teardown: -> 7 | console.log 'restoring dir', @dir 8 | process.chdir @dir 9 | 10 | localVersion = require '../local-module-version' 11 | 12 | gt.test 'basics', -> 13 | gt.arity localVersion, 1, 'expects single argument' 14 | 15 | gt.test 'fetch local version', -> 16 | version = localVersion 'sample-module' 17 | console.log version 18 | gt.string version, 'got back a string' 19 | gt.equal version, '1.0.8', 'got back correct value' -------------------------------------------------------------------------------- /src/test/moduleName.coffee: -------------------------------------------------------------------------------- 1 | moduleName = require '../moduleName' 2 | 3 | gt.module 'module name parsing' 4 | 5 | gt.test 'basic', -> 6 | gt.arity moduleName, 1 7 | 8 | gt.test 'just name', -> 9 | nv = moduleName 'lodash' 10 | gt.object nv, 'returns an object' 11 | gt.equal nv.name, 'lodash' 12 | gt.undefined nv.version, 'has no version' 13 | 14 | gt.test 'name with dashes', -> 15 | nv = moduleName 'lodash-one' 16 | gt.object nv, 'returns an object' 17 | gt.equal nv.name, 'lodash-one' 18 | gt.undefined nv.version, 'has no version' 19 | 20 | gt.test 'name with dots', -> 21 | nv = moduleName 'lodash.one' 22 | gt.object nv, 'returns an object' 23 | gt.equal nv.name, 'lodash.one' 24 | gt.undefined nv.version, 'has no version' 25 | 26 | gt.test 'name@version', -> 27 | nv = moduleName 'lodash@1' 28 | gt.object nv, 'returns an object' 29 | gt.equal nv.name, 'lodash' 30 | gt.equal nv.version, '1', 'has correct version' 31 | 32 | gt.test 'name@version 3 digits', -> 33 | nv = moduleName 'lodash@1.0.1' 34 | gt.object nv, 'returns an object' 35 | gt.equal nv.name, 'lodash' 36 | gt.equal nv.version, '1.0.1', 'has correct version' 37 | 38 | gt.test 'name@version 3 digits + rc', -> 39 | nv = moduleName 'lodash@1.0.1-rc1' 40 | gt.object nv, 'returns an object' 41 | gt.equal nv.name, 'lodash' 42 | gt.equal nv.version, '1.0.1-rc1', 'has correct version' 43 | 44 | gt.test 'scoped name @author/name', -> 45 | nv = moduleName '@bahmutov/csv' 46 | gt.object nv, 'returns an object' 47 | gt.equal nv.name, '@bahmutov/csv' 48 | gt.undefined nv.version, 'has no version' 49 | 50 | gt.test 'scoped name @author/name with version', -> 51 | nv = moduleName '@bahmutov/csv@1.1.0' 52 | gt.object nv, 'returns an object' 53 | gt.equal nv.name, '@bahmutov/csv' 54 | gt.equal nv.version, '1.1.0' 55 | -------------------------------------------------------------------------------- /src/test/node_modules/sample-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-module", 3 | "version": "1.0.8" 4 | } -------------------------------------------------------------------------------- /src/test/npm-test.coffee: -------------------------------------------------------------------------------- 1 | gt.module 'npm test', 2 | setup: -> 3 | @dir = process.cwd() 4 | process.chdir __dirname 5 | console.log 'changed directory to', __dirname 6 | teardown: -> 7 | console.log 'restoring dir', @dir 8 | process.chdir @dir 9 | 10 | npmTest = require '../npm-test' 11 | test = npmTest.test 12 | 13 | ONE_MINUTE = 60000 14 | 15 | onError = (error) -> 16 | throw new Error(error) 17 | 18 | gt.test 'basics', -> 19 | gt.arity test, 1 20 | gt.string npmTest.npmPath, 'has npm path' 21 | 22 | gt.async 'simple npm test', -> 23 | promise = test() 24 | gt.object promise 25 | promise.then -> 26 | gt.ok false, 'there should not be npm test' 27 | .catch -> 28 | gt.ok true, 'failed as expected' 29 | .finally -> 30 | gt.start() 31 | , ONE_MINUTE 32 | -------------------------------------------------------------------------------- /src/test/registry.js: -------------------------------------------------------------------------------- 1 | /* global gt */ 2 | gt.module('registry fetchVersions') 3 | 4 | var registry = require('../registry') 5 | var fetchVersions = registry.fetchVersions 6 | var cleanVersions = registry.cleanVersions 7 | var timeout = 90 * 1000 8 | 9 | function onError (error) { 10 | throw new Error(error) 11 | } 12 | 13 | gt.test('basic of fetch', function () { 14 | gt.func(fetchVersions) 15 | gt.arity(fetchVersions, 2) 16 | }) 17 | 18 | gt.async('fetch non existent module', 2, function () { 19 | var promise = fetchVersions({}, { 20 | name: 'this-module-should-not-exist-at-all', 21 | version: '0.2.0' 22 | }) 23 | gt.func(promise.then, 'return object has then method') 24 | promise.then(function (results) { 25 | console.log('has results', results) 26 | gt.ok(typeof results === 'object', 'has results') 27 | }).catch(function (error) { 28 | gt.ok(false, 'should not get success, error ' + JSON.stringify(error, null, 2)) 29 | }).finally(gt.start) 30 | }, timeout) 31 | 32 | gt.async('fetch gt later versions', function () { 33 | gt.func(fetchVersions) 34 | gt.arity(fetchVersions, 2) 35 | var promise = fetchVersions({}, { name: 'gt', version: '0.5.0' }) 36 | gt.func(promise.then, 'return object has then method') 37 | promise.then(function (results) { 38 | gt.object(results, 'returns an object') 39 | gt.equal(results.name, 'gt', 'correct name returned') 40 | gt.array(results.versions, 'has versions array') 41 | gt.ok(results.versions.length > 5, 'a few versions') 42 | }).catch(onError).finally(gt.start) 43 | }, timeout) 44 | 45 | gt.async('fetch module later versions', function () { 46 | gt.func(fetchVersions) 47 | gt.arity(fetchVersions, 2) 48 | var promise = fetchVersions({}, { name: 'lodash', version: '0.7.0' }) 49 | gt.func(promise.then, 'return object has then method') 50 | promise.then(function (results) { 51 | gt.object(results, 'returns an object') 52 | gt.equal(results.name, 'lodash', 'correct name returned') 53 | gt.array(results.versions, 'has versions array') 54 | gt.ok(results.versions.length > 5, 'a few versions') 55 | }).catch(onError).finally(gt.start) 56 | }, timeout) 57 | 58 | gt.module('registry nextVersions') 59 | 60 | var nextVersions = require('../registry').nextVersions 61 | 62 | gt.test('next versions basics', function () { 63 | gt.func(nextVersions) 64 | gt.arity(nextVersions, 3) 65 | }) 66 | 67 | gt.async('fetch gt, optimist versions', function () { 68 | var promise = nextVersions({}, [{ 69 | name: 'gt', 70 | version: '0.5.0' 71 | }, { 72 | name: 'lodash', 73 | version: '1.0.0' 74 | }]) 75 | gt.func(promise.then, 'return object has then method') 76 | promise.then(function (results) { 77 | gt.array(results) 78 | gt.equal(results.length, 2, 'two modules') 79 | gt.equal(results[0].name, 'gt') 80 | gt.equal(results[1].name, 'lodash') 81 | gt.equal(results[0].versions[0], '0.5.1') 82 | gt.equal(results[1].versions[0], '1.0.1') 83 | }).catch(onError).finally(gt.start) 84 | }, timeout) 85 | 86 | gt.async('fetch latest version', function () { 87 | var onlyLatest = true 88 | var promise = nextVersions({}, [{ 89 | name: 'gt', 90 | version: '0.5.0' 91 | }, { 92 | name: 'lodash', 93 | version: '1.0.0' 94 | }], onlyLatest) 95 | gt.func(promise.then, 'return object has then method') 96 | promise.then(function (results) { 97 | gt.array(results) 98 | gt.equal(results.length, 2, 'two modules') 99 | gt.equal(results[0].name, 'gt') 100 | gt.equal(results[1].name, 'lodash') 101 | gt.equal(results[0].versions.length, 1) 102 | gt.equal(results[1].versions.length, 1) 103 | }).catch(onError).finally(gt.start) 104 | }, timeout) 105 | 106 | gt.async('fetch latest version two digits', function () { 107 | var onlyLatest = true 108 | var promise = nextVersions({}, [{ name: 'mocha', version: '~1.8' }], onlyLatest) 109 | gt.func(promise.then, 'return object has then method') 110 | promise.then(function (results) { 111 | gt.array(results) 112 | gt.equal(results.length, 1, 'single module') 113 | gt.equal(results[0].name, 'mocha') 114 | gt.equal(results[0].versions.length, 1) 115 | }).catch(onError).finally(gt.start) 116 | }, timeout) 117 | 118 | gt.test('clean versions, 2 digits', function () { 119 | gt.arity(cleanVersions, 1) 120 | var cleaned = cleanVersions([{ name: 'mocha', version: '~1.8' }]) 121 | gt.array(cleaned) 122 | gt.equal(cleaned.length, 1) 123 | gt.equal(cleaned[0].name, 'mocha', 'correct name') 124 | gt.string(cleaned[0].version, 'version is a string') 125 | }) 126 | 127 | gt.test('clean two versions', function () { 128 | var input = [{ 129 | name: 'gt', 130 | version: '0.5.0' 131 | }, { 132 | name: 'lodash', 133 | version: '1.0.0' 134 | }] 135 | var cleaned = cleanVersions(input) 136 | gt.array(cleaned) 137 | // console.dir(cleaned); 138 | gt.equal(cleaned.length, 2) 139 | gt.string(cleaned[0].version, 'first module has string version') 140 | gt.string(cleaned[1].version, 'second module has string version') 141 | }) 142 | 143 | gt.test('clean latest versions', function () { 144 | var input = [{ 145 | name: 'gt', 146 | version: 'latest' 147 | }] 148 | var cleaned = cleanVersions(input) 149 | gt.array(cleaned, 'got back an array') 150 | gt.equal(cleaned.length, 1) 151 | gt.string(cleaned[0].version, 'module has string version') 152 | }) 153 | 154 | gt.test('clean version *', function () { 155 | var input = [{ 156 | name: 'gt', 157 | version: '*' 158 | }] 159 | var cleaned = cleanVersions(input) 160 | gt.array(cleaned) 161 | gt.equal(cleaned.length, 1) 162 | gt.string(cleaned[0].version, 'module has string version') 163 | }) 164 | -------------------------------------------------------------------------------- /src/test/report-available.coffee: -------------------------------------------------------------------------------- 1 | gt.module 'report-available' 2 | 3 | report = require '../report-available' 4 | 5 | gt.test 'number of arguments', -> 6 | gt.arity report, 3 7 | 8 | gt.test 'expects an array', -> 9 | gt.raises -> 10 | report "info" 11 | , 'needs and array' 12 | 13 | gt.test 'empty array', -> 14 | report [] 15 | 16 | gt.test 'good info', -> 17 | report [{name: 'test', versions: [1, 2]}] 18 | report [{name: 'test2', versions: ['1', '2']}] 19 | 20 | gt.test 'missing module name', -> 21 | gt.raises -> 22 | report [{versions: [1,2]}] 23 | , 'needs module name' 24 | 25 | gt.test 'lots of versions', -> 26 | report [{name: 'test', versions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}] 27 | -------------------------------------------------------------------------------- /src/test/report-install-command.coffee: -------------------------------------------------------------------------------- 1 | report = require '../report-install-command' 2 | mockery = require 'mockery' 3 | 4 | mockPackageJson = """ 5 | { 6 | "name": "next-update", 7 | "version": "0.0.4", 8 | "dependencies": { 9 | "optimist": "~0.6.0", 10 | "lodash": "~2.0.0", 11 | "check-types": "~0.6.4" 12 | }, 13 | "devDependencies": { 14 | "grunt": "~0.4.1", 15 | "grunt-contrib-jshint": "~0.6.4", 16 | "grunt-bump": "0.0.11", 17 | "gt": "~0.8.10" 18 | }, 19 | "peerDependencies": { 20 | "foo": "0.0.1" 21 | } 22 | } 23 | """ 24 | 25 | fsMock = 26 | existsSync: (name) -> name == './package.json' 27 | readFileSync: (name, encoding) -> 28 | console.assert name == './package.json', 'invalid name ' + name 29 | mockPackageJson 30 | 31 | mockery.registerMock 'fs', fsMock 32 | 33 | gt.module 'report-install-command', 34 | setup: -> mockery.enable { useCleanCache : true } 35 | teardown: -> mockery.disable() 36 | 37 | 38 | gt.test 'basics', -> 39 | gt.arity report, 1 40 | 41 | gt.test 'expects an array', -> 42 | gt.raises -> 43 | report {name: 'foo', versions: [1]} 44 | , 'needs an array of objects' 45 | 46 | gt.test 'empty input', -> 47 | cmd = report [] 48 | gt.undefined cmd 49 | 50 | gt.test 'nothing works', -> 51 | cmd = report [ 52 | [ 53 | name: 'foo' 54 | works: false 55 | version: 1 56 | ] 57 | ] 58 | gt.undefined cmd 59 | 60 | gt.test 'single working', -> 61 | cmd = report [ 62 | [ 63 | name: 'foo' 64 | works: true 65 | version: '0.3.0' 66 | ] 67 | ] 68 | gt.ok /foo@0.3.0/.test(cmd), 'latest version' 69 | 70 | gt.test 'peer dependency command', -> 71 | cmd = report [ 72 | [ 73 | name: 'foo' 74 | works: true 75 | version: '0.3.0' 76 | ] 77 | ] 78 | gt.ok /--save-peer/.test(cmd), 'save peer dependency' 79 | 80 | gt.test 'normal dependency command', -> 81 | cmd = report [ 82 | [ 83 | name: 'lodash' 84 | works: true 85 | version: '0.3.0' 86 | ] 87 | ] 88 | gt.ok /--save\ /.test(cmd), 'save normal dependency' 89 | 90 | gt.test 'dev dependency command', -> 91 | cmd = report [ 92 | [ 93 | name: 'grunt' 94 | works: true 95 | version: '0.3.0' 96 | ] 97 | ] 98 | gt.ok /--save-dev\ /.test(cmd), 'save dev dependency' 99 | 100 | gt.test 'usual input', -> 101 | cmd = report [ 102 | [ 103 | name: 'foo' 104 | works: true 105 | version: 1 106 | , 107 | name: 'foo' 108 | works: true 109 | version: 2 110 | , 111 | name: 'foo' 112 | works: true 113 | version: 3 114 | ] 115 | ] 116 | gt.ok /foo@3/.test(cmd), 'latest version is present' 117 | gt.ok !/foo@2/.test(cmd), 'previous version is not present' 118 | 119 | gt.test 'normal and dev dependency command', -> 120 | cmd = report [ 121 | [ 122 | name: 'lodash' 123 | works: true 124 | version: '0.3.0' 125 | ], 126 | [ 127 | name: 'grunt' 128 | works: true 129 | version: '0.3.0' 130 | ] 131 | ] 132 | console.log cmd 133 | gt.ok /--save --save-exact lodash@0\.3\.0/.test(cmd), 'normal dependency' 134 | gt.ok /--save-dev --save-exact grunt@0\.3\.0/.test(cmd), 'save dev dependency' 135 | -------------------------------------------------------------------------------- /src/test/report.coffee: -------------------------------------------------------------------------------- 1 | gt.module 'report' 2 | 3 | report = require '../report' 4 | 5 | gt.test 'reportSuccess', -> 6 | rs = report.reportSuccess 7 | gt.arity rs, 2 8 | rs 'sample values' 9 | 10 | gt.test 'reportFailure', -> 11 | rf = report.reportFailure 12 | gt.arity rf, 2 13 | rf 'sample values' 14 | 15 | gt.test 'report basics', -> 16 | r = report.report 17 | gt.ok r.length > 0 18 | data = [] 19 | r data 20 | 21 | info = [ 22 | [{name: 'a', version: '1.0', from: '0.0.0'}], 23 | [{name: 'a', version: '1.1', from: '0.0.0', works: true}], 24 | [{name: 'b', version: '1.0', from: '0.0.0', works: false}] 25 | ] 26 | 27 | gt.test 'report modules, default colors', 0, -> 28 | report.report info 29 | 30 | gt.test 'report modules, no colors', 0, -> 31 | report.report info, false 32 | 33 | gt.test 'report modules, with colors', 0, -> 34 | report.report info, true 35 | -------------------------------------------------------------------------------- /src/test/stats.coffee: -------------------------------------------------------------------------------- 1 | gt.module 'stats' 2 | 3 | Q = require 'q' 4 | stats = require '../stats' 5 | 6 | timeout = 35000 7 | 8 | gt.async 'get stats', -> 9 | gt.func stats.getSuccessStats 10 | opts = 11 | name: 'foo-does-not-exist' 12 | from: '1.0.0' 13 | to: '2.0.0' 14 | stats.getSuccessStats(opts) 15 | .then(-> gt.ok false, 'should not find nonexistent module') 16 | .finally(-> gt.start()) 17 | , timeout 18 | 19 | gt.async 'bad request for stats', -> 20 | opts = 21 | name: 'foo-does-not-exist' 22 | from: '1.0' 23 | to: '2.0' 24 | stats.getSuccessStats(opts) 25 | .then(-> gt.ok false, 'should not find nonexistent module') 26 | .finally(-> gt.start()) 27 | , timeout 28 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const R = require('ramda') 4 | const semver = require('semver') 5 | const debug = require('debug')('next-update') 6 | 7 | const name = 'next-update' 8 | 9 | const getConfig = R.memoize(function getConfig (packageFilename) { 10 | la(is.unemptyString(packageFilename), 11 | 'missing package filename', packageFilename) 12 | const pkg = require(packageFilename) 13 | const config = pkg && 14 | pkg.config && 15 | pkg.config[name] 16 | return config 17 | }) 18 | 19 | function getSkippedModules (packageFilename) { 20 | const config = getConfig(packageFilename) 21 | if (config) { 22 | return config.skip || 23 | config.skipped || 24 | config.lock || 25 | config.locked || 26 | config.ignore || 27 | config.ignored || 28 | [] 29 | } 30 | return [] 31 | } 32 | 33 | function getTestCommand (packageFilename, moduleName) { 34 | la(is.unemptyString(moduleName), 'missing module name') 35 | const config = getConfig(packageFilename) 36 | if (!config) { 37 | return 38 | } 39 | if (!config.commands) { 40 | return 41 | } 42 | return config.commands[moduleName] 43 | } 44 | 45 | const stringify = (x) => JSON.stringify(x, null, 2) 46 | 47 | const errorObject = (x) => (new Error(stringify(x))) 48 | 49 | function isPrerelease (version) { 50 | la(is.unemptyString(version), 'expected version string', version) 51 | // https://github.com/npm/node-semver#functions 52 | const prereleaseComponents = semver.prerelease(version) 53 | debug('version %s prerelease components %j', version, prereleaseComponents) 54 | return is.array(prereleaseComponents) && is.not.empty(prereleaseComponents) 55 | } 56 | 57 | module.exports = { 58 | name, 59 | getConfig, 60 | getSkippedModules, 61 | getTestCommand, 62 | stringify, 63 | errorObject, 64 | isPrerelease 65 | } 66 | -------------------------------------------------------------------------------- /test/as-parent-spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const chdir = require('chdir-promise') 3 | const nextUpdate = require('..') 4 | const execa = require('execa') 5 | const snapShot = require('snap-shot') 6 | const _ = require('lodash') 7 | const is = require('check-more-types') 8 | const la = require('lazy-ass') 9 | const TWO_MINUTES = 120000 10 | 11 | const testFolder = path.join(__dirname, 'test-next-updater') 12 | 13 | function prune () { 14 | return execa.shell('npm prune') 15 | } 16 | 17 | function install () { 18 | return execa.shell('npm install') 19 | } 20 | 21 | /* global describe, beforeEach, afterEach, it */ 22 | describe('testing check-types', () => { 23 | beforeEach(function () { 24 | this.timeout(TWO_MINUTES) 25 | return chdir.to(testFolder).then(prune).then(install) 26 | }) 27 | 28 | afterEach(function () { 29 | this.timeout(TWO_MINUTES) 30 | return chdir.to(testFolder).then(prune).then(install).then(chdir.back) 31 | }) 32 | 33 | it('skips without module', function () { 34 | this.timeout(TWO_MINUTES) 35 | const opts = { 36 | without: 'check-types', 37 | keep: false 38 | } 39 | return nextUpdate(opts).then(results => { 40 | la(results === undefined, 'no results without a module to check', results) 41 | }) 42 | }) 43 | 44 | it('checks latest check-types', function () { 45 | this.timeout(TWO_MINUTES) 46 | const opts = { 47 | module: 'check-types', 48 | latest: true, 49 | keep: false 50 | } 51 | const removeVersions = results => 52 | results.map(r => { 53 | la(is.semver(r.version), 'expected version', r) 54 | r.version = 'valid' 55 | return r 56 | }) 57 | return snapShot(nextUpdate(opts).then(_.flatten).then(removeVersions)) 58 | }) 59 | 60 | it('checks some versions of check-types', function () { 61 | this.timeout(TWO_MINUTES) 62 | 63 | const limit = (name, version) => { 64 | if (name === 'check-types' && version.startsWith('0.7')) { 65 | return true 66 | } 67 | return false 68 | } 69 | 70 | const opts = { 71 | module: 'check-types', 72 | keep: false, 73 | limit 74 | } 75 | return snapShot(nextUpdate(opts)) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/as-parent.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | q = require 'q' 3 | _ = require 'lodash' 4 | TWO_MINUTES = 120000 5 | 6 | gt.module 'using next-update as module', 7 | setup: -> 8 | process.chdir path.join(__dirname, 'test-next-updater') 9 | teardown: -> 10 | process.chdir __dirname 11 | 12 | nextUpdate = require '..' 13 | 14 | gt.test 'basics', -> 15 | gt.func nextUpdate, 'next update is a function' 16 | 17 | # [ [ { name: 'check-types', version: '1.1.1', works: false } ] ] 18 | # TODO use schem-shot 19 | gt.async 'results format', -> 20 | opts = 21 | module: 'check-types' 22 | latest: true 23 | promise = nextUpdate(opts) 24 | gt.ok q.isPromise(promise), 'next update returns a promise' 25 | promise.then (result) -> 26 | console.log 'Finished with', result 27 | gt.array result, 'result should be an array' 28 | result = _.flatten result 29 | gt.length result, 1, 'single result' 30 | gt.object result[0], 'single result is an object' 31 | gt.equal result[0].name, 'check-types' 32 | gt.string result[0].version, 'has version string' 33 | gt.equal typeof result[0].works, 'boolean', 'has works property' 34 | promise.catch -> gt.ok false, 'promise failed' 35 | promise.finally -> gt.start() 36 | , TWO_MINUTES 37 | -------------------------------------------------------------------------------- /test/command-spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const chdir = require('chdir-promise') 3 | const nextUpdate = require('..') 4 | const execa = require('execa') 5 | const snapShot = require('snap-shot') 6 | const R = require('ramda') 7 | const TWO_MINUTES = 120000 8 | 9 | const testFolder = path.join(__dirname, 'test-command-map') 10 | 11 | function prune () { 12 | return execa.shell('npm prune') 13 | } 14 | 15 | function install () { 16 | return execa.shell('npm install') 17 | } 18 | 19 | const flatVersionSort = R.compose(R.sortBy(R.prop('version')), R.flatten) 20 | 21 | it('sorts by version', () => { 22 | const results = [ 23 | [ 24 | { name: 'lodash', version: '1.0.1', from: '1.0.0', works: true }, 25 | { name: 'lodash', version: '1.1.0', from: '1.0.0', works: false }, 26 | { name: 'lodash', version: '1.1.1', from: '1.0.0', works: false }, 27 | { name: 'lodash', version: '1.2.0', from: '1.0.0', works: false }, 28 | { name: 'lodash', version: '1.2.1', from: '1.0.0', works: false }, 29 | { name: 'lodash', version: '1.3.0', from: '1.0.0', works: false }, 30 | { name: 'lodash', version: '1.3.1', from: '1.0.0', works: false }, 31 | { name: 'lodash', version: '1.0.2', from: '1.0.0', works: true } 32 | ] 33 | ] 34 | snapShot(flatVersionSort(results)) 35 | }) 36 | 37 | /* global describe, beforeEach, afterEach, it */ 38 | describe('per-module configured command', () => { 39 | beforeEach(function () { 40 | this.timeout(TWO_MINUTES) 41 | return chdir.to(testFolder).then(prune).then(install) 42 | }) 43 | 44 | afterEach(chdir.back) 45 | 46 | it('finds and uses module own test command', function () { 47 | this.timeout(TWO_MINUTES) 48 | const opts = { 49 | module: 'lodash', 50 | allow: 'minor' 51 | } 52 | return snapShot(nextUpdate(opts).then(flatVersionSort)) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/e2e.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | index = path.join __dirname, '../bin/next-update.js' 3 | ONE_MINUTE = 60000 4 | TWO_MINUTES = 120000 5 | moduleName = 'check-types' 6 | 7 | gt.module 'end 2 end tests', 8 | setup: -> 9 | process.chdir path.join(__dirname, 'test-next-updater') 10 | teardown: -> 11 | process.chdir __dirname 12 | 13 | # TODO set the final version limit to check 14 | gt.async '--module', -> 15 | gt.exec 'node', [index, '--module', moduleName], 0 16 | , ONE_MINUTE 17 | 18 | gt.async '--available', -> 19 | gt.exec 'node', [index, '--available'], 0 20 | , ONE_MINUTE 21 | 22 | gt.async '--available ' + moduleName, -> 23 | gt.exec 'node', [index, '--available', moduleName], 0 24 | , ONE_MINUTE 25 | 26 | gt.async '--revert ' + moduleName, -> 27 | gt.exec 'node', [index, '--revert', '--module', moduleName], 0 28 | , ONE_MINUTE 29 | 30 | gt.async '--revert', -> 31 | gt.exec 'node', [index, '--revert'], 0 32 | , TWO_MINUTES 33 | -------------------------------------------------------------------------------- /test/file-deps-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var la = require('lazy-ass') 4 | var is = require('check-more-types') 5 | var pause = 30 * 1000 6 | var chdir = require('chdir-promise') 7 | var join = require('path').join 8 | var testFolder = join(__dirname, 'test-file-deps') 9 | 10 | /* global describe, it */ 11 | describe('scoped packages', function () { 12 | var nextUpdate = require('../src/next-update') 13 | 14 | it('is an object', function () { 15 | la(is.object(nextUpdate)) 16 | }) 17 | 18 | it('has available method', function () { 19 | la(is.fn(nextUpdate.available)) 20 | }) 21 | 22 | it('handles file: package names', function () { 23 | this.timeout(pause) 24 | return chdir 25 | .to(testFolder) 26 | .then(function () { 27 | return nextUpdate.available() 28 | }) 29 | .then(function (available) { 30 | console.log('available', available) 31 | }) 32 | .then(chdir.back) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/revert-spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fromFolder = path.join.bind(null, __dirname) 3 | const chdir = require('chdir-promise') 4 | const execa = require('execa') 5 | const la = require('lazy-ass') 6 | const TWO_MINUTES = 120000 7 | 8 | const index = fromFolder('../bin/next-update.js') 9 | const testFolder = fromFolder('test-next-updater') 10 | 11 | /* global describe, beforeEach, afterEach, it */ 12 | describe('--revert', () => { 13 | beforeEach(function () { 14 | return chdir.to(testFolder) 15 | }) 16 | 17 | afterEach(chdir.back) 18 | 19 | it('--module check-types', function () { 20 | this.timeout(TWO_MINUTES) 21 | return execa('node', [ 22 | index, 23 | '--revert', 24 | '--module', 25 | 'check-types' 26 | ]).then(result => { 27 | la( 28 | result.code === 0, 29 | 'error exit', 30 | result.code, 31 | result.stdout, 32 | result.stderr 33 | ) 34 | }) 35 | }) 36 | 37 | it('--module all', function () { 38 | this.timeout(TWO_MINUTES) 39 | return execa('node', [index, '--revert', '--module']).then(result => { 40 | la( 41 | result.code === 0, 42 | 'error exit', 43 | result.code, 44 | result.stdout, 45 | result.stderr 46 | ) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/scoped-packages-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var la = require('lazy-ass') 4 | var is = require('check-more-types') 5 | var pause = 30 * 1000 6 | var chdir = require('chdir-promise') 7 | var join = require('path').join 8 | var testFolder = join(__dirname, 'test-scoped-names') 9 | 10 | /* global describe, it */ 11 | describe('scoped packages', function () { 12 | var nextUpdate = require('../src/next-update') 13 | 14 | it('is an object', function () { 15 | la(is.object(nextUpdate)) 16 | }) 17 | 18 | it('has available method', function () { 19 | la(is.fn(nextUpdate.available)) 20 | }) 21 | 22 | it('handles scoped package names', function () { 23 | this.timeout(pause) 24 | return chdir 25 | .to(testFolder) 26 | .then(function () { 27 | return nextUpdate.available('@bahmutov/csv@1.1.0') 28 | }) 29 | .then(function (available) { 30 | la(is.array(available), 'returns an array', available) 31 | la(is.not.empty(available), 'there should be a new version', available) 32 | var first = available[0] 33 | la(first.name === '@bahmutov/csv', 'wrong name', first) 34 | var versions = first.versions 35 | la(is.array(versions), 'expected list of new versions', first) 36 | la(versions[0] === '1.2.0', 'first available version', versions) 37 | }) 38 | .then(chdir.back) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/single-module-check.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | index = path.join __dirname, '../bin/next-update.js' 3 | TWO_MINUTES = 120000 4 | 5 | gt.module 'single module check', 6 | setup: -> 7 | process.chdir path.join(__dirname, 'test-next-updater') 8 | teardown: -> 9 | process.chdir __dirname 10 | 11 | gt.async '--skip -m check-types', -> 12 | gt.exec 'node', [index, '--skip', '--m check-types'], 0 13 | , TWO_MINUTES 14 | -------------------------------------------------------------------------------- /test/test-command-map/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /test/test-command-map/__snapshots__/command-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['finds and uses module own test command 1'] = [ 2 | { 3 | "name": "lodash", 4 | "version": "1.0.1", 5 | "from": "1.0.0", 6 | "works": true 7 | }, 8 | { 9 | "name": "lodash", 10 | "version": "1.0.2", 11 | "from": "1.0.0", 12 | "works": true 13 | }, 14 | { 15 | "name": "lodash", 16 | "version": "1.1.0", 17 | "from": "1.0.0", 18 | "works": false 19 | }, 20 | { 21 | "name": "lodash", 22 | "version": "1.1.1", 23 | "from": "1.0.0", 24 | "works": false 25 | }, 26 | { 27 | "name": "lodash", 28 | "version": "1.2.0", 29 | "from": "1.0.0", 30 | "works": false 31 | }, 32 | { 33 | "name": "lodash", 34 | "version": "1.2.1", 35 | "from": "1.0.0", 36 | "works": false 37 | }, 38 | { 39 | "name": "lodash", 40 | "version": "1.3.0", 41 | "from": "1.0.0", 42 | "works": false 43 | }, 44 | { 45 | "name": "lodash", 46 | "version": "1.3.1", 47 | "from": "1.0.0", 48 | "works": false 49 | } 50 | ] 51 | 52 | -------------------------------------------------------------------------------- /test/test-command-map/lodash-test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | console.log('lodash test version %s', _.VERSION) 3 | process.exit(_.VERSION.startsWith('1.0') ? 0 : 1) 4 | -------------------------------------------------------------------------------- /test/test-command-map/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-next-updater", 3 | "version": "0.0.1", 4 | "description": "Simple NPM module with dependencies for testing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo 'no test'", 8 | "lodash-test": "node lodash-test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:bahmutov/test-next-updater.git" 13 | }, 14 | "keywords": [ 15 | "test", 16 | "versions", 17 | "updater" 18 | ], 19 | "author": "Gleb Bahmutov ", 20 | "license": "MIT", 21 | "dependencies": { 22 | "lodash": "1.0.0" 23 | }, 24 | "config": { 25 | "next-update": { 26 | "commands": { 27 | "lodash": "npm run lodash-test" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/test-file-deps/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /test/test-file-deps/index.js: -------------------------------------------------------------------------------- 1 | console.log('do nothing') 2 | -------------------------------------------------------------------------------- /test/test-file-deps/local-dependency/index.js: -------------------------------------------------------------------------------- 1 | console.log('local dependency') 2 | -------------------------------------------------------------------------------- /test/test-file-deps/local-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local-dependency", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /test/test-file-deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-scoped-names", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "local-dependency": "file:./local-dependency" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/test-next-updater/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /test/test-next-updater/__snapshots__/as-parent-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['checks some versions of check-types 1'] = [ 2 | [ 3 | { 4 | "name": "check-types", 5 | "version": "0.7.0", 6 | "from": "0.6.5", 7 | "works": true 8 | }, 9 | { 10 | "name": "check-types", 11 | "version": "0.7.1", 12 | "from": "0.6.5", 13 | "works": true 14 | }, 15 | { 16 | "name": "check-types", 17 | "version": "0.7.2", 18 | "from": "0.6.5", 19 | "works": true 20 | } 21 | ] 22 | ] 23 | 24 | exports['checks latest check-types 1'] = [ 25 | { 26 | "name": "check-types", 27 | "version": "valid", 28 | "from": "0.6.5", 29 | "works": false 30 | } 31 | ] 32 | 33 | -------------------------------------------------------------------------------- /test/test-next-updater/index.js: -------------------------------------------------------------------------------- 1 | var check = require('check-types'); 2 | // using old check verifyString api on purpose 3 | check.verifyString('this is test string, works check-types < 1.0.0'); 4 | console.log('works'); 5 | -------------------------------------------------------------------------------- /test/test-next-updater/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-next-updater", 3 | "version": "0.0.1", 4 | "description": "Simple NPM module with dependencies for testing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:bahmutov/test-next-updater.git" 12 | }, 13 | "keywords": [ 14 | "test", 15 | "versions", 16 | "updater" 17 | ], 18 | "author": "Gleb Bahmutov ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "check-types": "0.6.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/test-scoped-names/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /test/test-scoped-names/index.js: -------------------------------------------------------------------------------- 1 | var csv = require('@bahmutov/csv') 2 | console.log('typeof csv', typeof csv) 3 | console.log(Object.keys(csv)) 4 | -------------------------------------------------------------------------------- /test/test-scoped-names/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-scoped-names", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bahmutov/csv": "1.1.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/time-limit-promise.js: -------------------------------------------------------------------------------- 1 | // how to time limit a promise? 2 | 3 | var Q = require('q') 4 | var started = +new Date() 5 | 6 | console.stamp = function stamp () { 7 | var elapsed = +new Date() - started 8 | var messageArgs = ['at %d ms:', elapsed].concat( 9 | Array.prototype.slice.call(arguments, 0) 10 | ) 11 | console.log.apply(console, messageArgs) 12 | } 13 | 14 | function onOk (value) { 15 | console.stamp('promise resolved ok', value) 16 | } 17 | 18 | function onError (value) { 19 | console.stamp('promise error with value', value) 20 | } 21 | 22 | // "normal" promise-returning function, knows nothing about the time limit 23 | function longAction () { 24 | var deferred = Q.defer() 25 | 26 | console.stamp('started long-running promise') 27 | setTimeout(function () { 28 | console.stamp('resolving long-running promise with 42 after 1 second') 29 | return deferred.resolve(42) 30 | }, 1000) 31 | 32 | return deferred.promise 33 | } 34 | 35 | longAction().timeout(400, 'timed out').then(onOk, onError).done() 36 | --------------------------------------------------------------------------------