├── .editorconfig ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .travis.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── assets ├── updtr.gif ├── updtr.jpg └── updtr.psd ├── babel.config.js ├── bin └── updtr ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── Updtr.js ├── bin │ ├── argv.js │ └── index.js ├── constants │ ├── config.js │ └── filterReasons.js ├── errors.js ├── exec │ ├── cmds.js │ ├── exec.js │ └── parse.js ├── index.js ├── reporters │ ├── basic.js │ ├── dense.js │ ├── index.js │ ├── none.js │ └── util │ │ ├── Indicator.js │ │ ├── Message.js │ │ ├── Projector.js │ │ ├── Spinner.js │ │ ├── Terminal.js │ │ ├── configList.js │ │ ├── customConfigToLines.js │ │ ├── handleError.js │ │ ├── msToString.js │ │ └── pluralize.js ├── run.js ├── tasks │ ├── batchUpdate.js │ ├── finish.js │ ├── init.js │ ├── sequentialUpdate.js │ ├── updatePackageJson.js │ └── util │ │ ├── Sequence.js │ │ ├── createUpdateResult.js │ │ ├── createUpdateTask.js │ │ ├── createUpdatedPackageJson.js │ │ ├── filterUpdateResults.js │ │ ├── filterUpdateTask.js │ │ ├── rollbackTo.js │ │ ├── splitUpdateTasks.js │ │ ├── updateTo.js │ │ └── updateVersionRange.js └── util │ └── fs.js └── test ├── .eslintrc ├── Updtr.test.js ├── __snapshots__ ├── Updtr.test.js.snap ├── index.test.js.snap └── run.test.js.snap ├── bin ├── __snapshots__ │ └── index.test.js.snap └── index.test.js ├── exec ├── __snapshots__ │ ├── cmds.test.js.snap │ ├── exec.test.js.snap │ └── parse.test.js.snap ├── cmds.test.js ├── exec.test.js └── parse.test.js ├── fixtures ├── events.js ├── execResults.js ├── outdateds.js ├── packageJsons.js ├── updateResults.js └── versionRanges.js ├── helpers ├── ExecError.js ├── FakeUpdtr.js ├── binMocks.js ├── cleanupFixtures.js ├── pickEventNames.js ├── pickEvents.js ├── readFixtures.js ├── replayTerminalSnapshots.js ├── runBinMock.js ├── setupFixtures.js └── temp.js ├── index.test.js ├── integration ├── __snapshots__ │ ├── noOutdated.test.js.snap │ └── outdated.test.js.snap ├── helpers │ ├── constants.js │ ├── fs.js │ └── getInstalledVersions.js ├── noOutdated.test.js └── outdated.test.js ├── manual ├── package.json ├── test └── yarn.lock ├── reporters ├── __snapshots__ │ ├── basic.test.js.snap │ ├── dense.test.js.snap │ └── index.test.js.snap ├── basic.test.js ├── dense.test.js ├── index.test.js └── util │ ├── __snapshots__ │ └── configList.test.js.snap │ ├── configList.test.js │ └── handleError.test.js ├── run.test.js └── tasks ├── __snapshots__ ├── batchUpdate.test.js.snap ├── finish.test.js.snap ├── init.test.js.snap ├── sequentialUpdate.test.js.snap └── updatePackageJson.test.js.snap ├── batchUpdate.test.js ├── finish.test.js ├── init.test.js ├── sequentialUpdate.test.js ├── updatePackageJson.test.js └── util ├── Sequence.test.js ├── __snapshots__ ├── createUpdateTask.test.js.snap ├── createUpdatedPackageJson.test.js.snap └── updateVersionRange.test.js.snap ├── createUpdateTask.test.js ├── createUpdatedPackageJson.test.js ├── filterUpdateTask.test.js ├── splitUpdateTasks.test.js └── updateVersionRange.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | 4 | # No .editorconfig files above the root directory 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [package.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "peerigon" 4 | ], 5 | "env": { 6 | "node": true 7 | }, 8 | "rules": { 9 | "import/extensions": "off" 10 | }, 11 | "root": true 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # IDE stuff 30 | .idea 31 | 32 | # Will be generated by npm run ensure-fixtures 33 | test/fixtures/*/** 34 | 35 | # Rollup output 36 | dist 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.20.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | os: 4 | - linux 5 | node_js: 6 | - 16 7 | - 14 8 | - 12 9 | 10 | script: 11 | - npm install 12 | - npm install -g yarn 13 | - npm test 14 | 15 | after_success: 16 | - bash <(curl -s https://codecov.io/bash) 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Bin", 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/babel-node", 12 | "runtimeArgs": [ 13 | "--harmony_async_await" 14 | ], 15 | "program": "${workspaceRoot}/src/bin/index.js" 16 | }, 17 | { 18 | "name": "Current test", 19 | "type": "node", 20 | "request": "launch", 21 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 22 | "stopOnEntry": false, 23 | "args": [ 24 | "${file}", 25 | "--runInBand" 26 | ], 27 | "cwd": "${workspaceRoot}", 28 | "preLaunchTask": null, 29 | "runtimeExecutable": null, 30 | "runtimeArgs": [ 31 | "--harmony_async_await", 32 | "--nolazy" 33 | ], 34 | "env": { 35 | "NODE_ENV": "test" 36 | }, 37 | "externalConsole": false, 38 | "sourceMaps": true, 39 | "outDir": null 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [4.1.0](https://github.com/peerigon/updtr/compare/v4.0.0...v4.1.0) (2024-07-03) 6 | 7 | 8 | ### Features 9 | 10 | * Introduce interactive update ([d3975e2](https://github.com/peerigon/updtr/commit/d3975e27787fac1199ef751cb947f3675ccec70c)) 11 | 12 | ## [4.0.0](https://github.com/peerigon/updtr/compare/v3.1.0...v4.0.0) (2021-08-20) 13 | 14 | 15 | ### ⚠ BREAKING CHANGES 16 | 17 | * No longer supporting node versions 6-11 18 | 19 | ### Features 20 | 21 | * update dependencies and remove vulnerabilities ([#86](https://github.com/peerigon/updtr/issues/86)) ([eb7a1ef](https://github.com/peerigon/updtr/commit/eb7a1ef4281714405c64ef633ac2963b87510887)) 22 | 23 | 24 | * upgrade node version to v12 ([2e29ef0](https://github.com/peerigon/updtr/commit/2e29ef0c605a7889ca5c50c7da86e65444c0acec)) 25 | 26 | 27 | # [3.1.0](https://github.com/peerigon/updtr/compare/v3.0.0...v3.1.0) (2018-10-04) 28 | 29 | 30 | ### Features 31 | 32 | * Add basic reporter ([#78](https://github.com/peerigon/updtr/issues/78)) ([59885d3](https://github.com/peerigon/updtr/commit/59885d3)) 33 | 34 | 35 | 36 | 37 | # [3.0.0](https://github.com/peerigon/updtr/compare/v2.0.0...v3.0.0) (2018-10-03) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * Respect package.json indentation ([#77](https://github.com/peerigon/updtr/issues/77)) ([e766fb0](https://github.com/peerigon/updtr/commit/e766fb0)), closes [#68](https://github.com/peerigon/updtr/issues/68) 43 | 44 | 45 | ### Fix 46 | 47 | * Stdout output parsing from yarn ([ff56fbb](https://github.com/peerigon/updtr/commit/ff56fbb)) 48 | 49 | 50 | ### BREAKING CHANGES 51 | 52 | * Removed official Node 4 support. It may still work, but now you're on your own. 53 | 54 | 55 | 56 | 57 | # [2.0.0](https://github.com/peerigon/updtr/compare/v1.0.0...v2.0.0) (2017-06-20) 58 | 59 | 60 | ### Features 61 | 62 | * Rewrite ([7ffc10a](https://github.com/peerigon/updtr/commit/7ffc10a)), closes [#14](https://github.com/peerigon/updtr/issues/14) [#47](https://github.com/peerigon/updtr/issues/47) [#46](https://github.com/peerigon/updtr/issues/46) [#13](https://github.com/peerigon/updtr/issues/13) [#51](https://github.com/peerigon/updtr/issues/51) [#58](https://github.com/peerigon/updtr/issues/58) 63 | 64 | 65 | ### BREAKING CHANGES 66 | 67 | * New and changed CLI options 68 | 69 | ``` 70 | --use, -u Specify the package manager to use [choices: "npm", "yarn"] [default: "npm"] 71 | --exclude, --ex Space separated list of module names that should not be updated [array] 72 | --update-to, --to Specify which updates you want to install [choices: "latest", "non-breaking", "wanted"] [default: "latest"] 73 | --save, -s Specify how updated versions should be saved to the package.json [choices: "smart", "caret", "exact"] [default: "smart"] 74 | --reporter, -r Choose a reporter for the console output [choices: "dense", "none"] [default: "dense"] 75 | --test, -t Specify a custom test command. Surround with quotes. 76 | --test-stdout, --out Show test stdout if the update fails [boolean] 77 | --registry, --reg Specify a custom registry to use 78 | --version Show version number [boolean] 79 | --help Show help [boolean] 80 | ``` 81 | 82 | 83 | 84 | 85 | # [1.0.0](https://github.com/peerigon/updtr/compare/v0.2.1...v1.0.0) (2017-04-01) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * **npm:** ignore error status 1 on npm outdated ([ec4057c](https://github.com/peerigon/updtr/commit/ec4057c)) 91 | 92 | 93 | ### Features 94 | 95 | * Use yarn package manager when available ([bcca7ce](https://github.com/peerigon/updtr/commit/bcca7ce)) 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![updtr](assets/updtr.jpg) 2 | 3 | # updtr 4 | 5 | **Update outdated npm modules with zero pain™** 6 | 7 | [![Build Status](https://travis-ci.org/peerigon/updtr.svg?branch=master)](https://travis-ci.org/peerigon/updtr) 8 | [![](https://img.shields.io/npm/v/updtr.svg)](https://www.npmjs.com/package/updtr) 9 | [![](https://img.shields.io/npm/dm/updtr.svg)](https://www.npmjs.com/package/updtr) 10 | [![Coverage Status](https://coveralls.io/repos/peerigon/updtr/badge.svg?branch=master&service=github)](https://coveralls.io/github/peerigon/updtr?branch=master) 11 | 12 | Based on `npm outdated`, **updtr** installs the latest version and runs `npm test` for each dependency. In case the test succeeds, **updtr** saves the new version number to your `package.json`. Otherwise, **updtr** rolls back the conflicting update. 13 | 14 | Additionally, it will use `yarn` instead of `npm` when a `yarn.lock` file is present in your project. 15 | 16 | Made by [Peerigon](https://peerigon.com/?pk_campaign=gh-os&pk_kwd=updtr). 17 | 18 | ![updtr](assets/updtr.gif) 19 | 20 | ## Installation 21 | 22 | ``` 23 | npm install -g updtr 24 | ``` 25 | 26 | ## Options 27 | 28 | ### `--use` `-u` 29 | 30 | Specify the package manager to use: 31 | 32 | - `npm` 33 | - `yarn` 34 | 35 | Updtr tries to guess the package manager by looking for a `yarn.lock` file. If there is one in `process.cwd()`, it will use yarn. Setting this option overrides that default. 36 | 37 | ### `--exclude` `--ex` 38 | 39 | Space separated list of module names that should not be updated. 40 | 41 | ### `--update-to` `--to` 42 | 43 | - `latest` *(default)*: update all packages to the latest version number 44 | - `non-breaking`: update all packages to the latest version number that does not conflict with the installed version number 45 | - `wanted`: update all packages to the latest version number that does not conflict with the version number as specified in the `package.json` 46 | 47 | ### `--save` `-s` 48 | 49 | Specify how updated versions should be saved to the `package.json`: 50 | 51 | - `smart` *(default)*: tries to preserve the current style. Falls back to `caret` if the style cannot be preserved. 52 | - `caret`: saves `^x.y.z` 53 | - `exact`: saves `x.y.z` 54 | 55 | ### `--reporter` `-r` 56 | 57 | Choose a reporter for the console output: 58 | 59 | - `dense` *(default*): See screenshot 60 | - `basic`: Uses `console.log` for output, no need for a TTY (e.g when running on CI) 61 | - `none`: No console output 62 | 63 | ### `--test` `-t` 64 | 65 | Specify a custom test command. Surround with quotes: 66 | 67 | `updtr -t "mocha -R spec"` 68 | 69 | ### `--test-stdout` `--out` 70 | 71 | Show test stdout if the update fails. 72 | 73 | ### `--registry` `--reg` 74 | 75 | Specify a custom registry to use. 76 | 77 | **Please note:** yarn does not support to set a custom registry via command line. Use a `.npmrc` file to achieve this. See also [yarnpkg/yarn#606](https://github.com/yarnpkg/yarn/issues/606). 78 | 79 | ### `--version` 80 | 81 | Show the current updtr version. 82 | 83 | ### `--help` 84 | 85 | Show all commands. 86 | 87 | ## License 88 | 89 | Unlicense 90 | 91 | ## Sponsors 92 | 93 | [](https://peerigon.com) 94 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: 14 4 | - nodejs_version: 12 5 | 6 | platform: 7 | - x86 8 | 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm install 12 | 13 | test_script: 14 | - npm install -g yarn 15 | - node --version 16 | - npm --version 17 | - npm test 18 | 19 | build: off 20 | -------------------------------------------------------------------------------- /assets/updtr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerigon/updtr/bfc57515ae71ca9d5ad0ad0a53a3363518bcd56c/assets/updtr.gif -------------------------------------------------------------------------------- /assets/updtr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerigon/updtr/bfc57515ae71ca9d5ad0ad0a53a3363518bcd56c/assets/updtr.jpg -------------------------------------------------------------------------------- /assets/updtr.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerigon/updtr/bfc57515ae71ca9d5ad0ad0a53a3363518bcd56c/assets/updtr.psd -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous, strict */ 2 | "use strict"; 3 | 4 | module.exports = function (api) { 5 | api.cache(() => process.env.NODE_ENV); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | targets: { 13 | node: 12, 14 | }, 15 | }, 16 | ], 17 | ], 18 | plugins: [ 19 | "@babel/plugin-transform-runtime", 20 | ], 21 | sourceMaps: "inline", 22 | retainLines: true, 23 | }; 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /bin/updtr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../dist"); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "updtr", 3 | "version": "4.1.0", 4 | "description": "Update outdated npm modules with zero pain™", 5 | "main": "dist", 6 | "scripts": { 7 | "pretest": "npm run fixtures:ensure", 8 | "test": "cross-env FORCE_COLOR=1 jest --coverage", 9 | "test:watch": "jest --watch", 10 | "posttest": "npm run lint", 11 | "lint": "eslint --ignore-path ./.gitignore src test", 12 | "build": "cross-env NODE_ENV=production rollup -c", 13 | "prepublishOnly": "npm run build && node ./bin/updtr --help", 14 | "release": "standard-version", 15 | "fixtures:ensure": "babel-node ./test/helpers/setupFixtures.js", 16 | "fixtures:rebuild": "babel-node ./test/helpers/cleanupFixtures.js && npm run fixtures:ensure" 17 | }, 18 | "author": "developers@peerigon.com", 19 | "license": "Unlicense", 20 | "bin": { 21 | "updtr": "./bin/updtr" 22 | }, 23 | "dependencies": { 24 | "@babel/runtime": "^7.15.3", 25 | "ansi-escapes": "^4.3.0", 26 | "chalk": "^4.1.2", 27 | "cli-cursor": "^3.1.0", 28 | "cli-spinners": "^2.6.0", 29 | "detect-indent": "^6.0.0", 30 | "es6-error": "^4.0.2", 31 | "inquirer": "7.0.0", 32 | "pify": "^4.0.1", 33 | "semver": "^7.3.5", 34 | "string-width": "^4.2.0", 35 | "unicons": "0.0.3", 36 | "yargs": "^15.1.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.15.0", 40 | "@babel/node": "^7.14.9", 41 | "@babel/plugin-transform-runtime": "^7.15.0", 42 | "@babel/preset-env": "^7.15.0", 43 | "@babel/register": "^7.15.3", 44 | "babel-core": "^7.0.0-bridge.0", 45 | "babel-jest": "^27.0.6", 46 | "clone": "^2.1.1", 47 | "cross-env": "^7.0.3", 48 | "eslint": "^7.32.0", 49 | "eslint-config-peerigon": "^25.3.1", 50 | "eslint-plugin-jest": "^23.6.0", 51 | "jest": "^25.1.0", 52 | "rimraf": "^3.0.2", 53 | "rollup": "^1.31.0", 54 | "rollup-plugin-babel": "^4.4.0", 55 | "rollup-plugin-node-resolve": "^5.2.0", 56 | "sinon": "^8.1.1", 57 | "standard-version": "^9.3.1", 58 | "stream-buffers": "^3.0.1", 59 | "temp": "^0.9.4", 60 | "wrap-ansi": "^6.2.0" 61 | }, 62 | "engines": { 63 | "node": ">= 4.0.0", 64 | "npm": ">= 2.5.0" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "https://github.com/peerigon/updtr.git" 69 | }, 70 | "files": [ 71 | "bin", 72 | "dist", 73 | "README.md", 74 | "LICENSE" 75 | ], 76 | "jest": { 77 | "coveragePathIgnorePatterns": [ 78 | "/node_modules/", 79 | "/test/" 80 | ], 81 | "coverageThreshold": { 82 | "global": { 83 | "branches": 94, 84 | "functions": 94, 85 | "lines": 94, 86 | "statements": 94 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import babel from "rollup-plugin-babel"; 3 | import resolve from "rollup-plugin-node-resolve"; 4 | 5 | const rollupConfig = { 6 | input: "src/bin/index.js", 7 | plugins: [ 8 | resolve({ 9 | jail: path.resolve(__dirname, "src"), 10 | }), 11 | babel({ 12 | runtimeHelpers: true, 13 | exclude: "node_modules/**", // only transpile our source code 14 | }), 15 | ], 16 | output: { 17 | file: "dist/index.js", 18 | sourceMap: true, 19 | format: "cjs", 20 | }, 21 | }; 22 | 23 | export default rollupConfig; 24 | -------------------------------------------------------------------------------- /src/Updtr.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/no-undefined-types */ 2 | import EventEmitter from "events"; 3 | import path from "path"; 4 | import fs from "./util/fs"; 5 | import { 6 | USE_OPTIONS, 7 | UPDATE_TO_OPTIONS, 8 | SAVE_OPTIONS, 9 | } from "./constants/config"; 10 | import exec from "./exec/exec"; 11 | import cmds from "./exec/cmds"; 12 | import parse from "./exec/parse"; 13 | import { 14 | RequiredOptionMissingError, 15 | OptionValueNotSupportedError, 16 | YarnWithCustomRegistryError, 17 | } from "./errors"; 18 | 19 | // node v4 has no dedicated constants object. 20 | // Remove this if node v4 is not supported anymore. 21 | const FS_CONSTANTS = fs.constants === undefined ? fs : fs.constants; 22 | 23 | function checkCwd(cwd) { 24 | if (typeof cwd !== "string") { 25 | throw new RequiredOptionMissingError("cwd", cwd); 26 | } 27 | } 28 | 29 | function checkUse(use) { 30 | if (USE_OPTIONS.indexOf(use) === -1) { 31 | throw new OptionValueNotSupportedError("use", use); 32 | } 33 | } 34 | 35 | function checkUpdateTo(updateTo) { 36 | if (UPDATE_TO_OPTIONS.indexOf(updateTo) === -1) { 37 | throw new OptionValueNotSupportedError("updateTo", updateTo); 38 | } 39 | } 40 | 41 | function checkSave(save) { 42 | if (SAVE_OPTIONS.indexOf(save) === -1) { 43 | throw new OptionValueNotSupportedError("save", save); 44 | } 45 | } 46 | 47 | function checkForYarnWithCustomReg(packageManager, registry) { 48 | if (packageManager === "yarn" && registry !== undefined) { 49 | throw new YarnWithCustomRegistryError(); 50 | } 51 | } 52 | 53 | export default class Updtr extends EventEmitter { 54 | // TODO: Add typings for UpdtrConfig 55 | // eslint-disable-next-line 56 | /** 57 | * The config passed-in here should look identically to the CLI config. 58 | * Dash-cased properties should be renamed to camelCased. 59 | * The goal is to replicate the API of the CLI as close as possible so users don't 60 | * have to guess the options. 61 | * 62 | * @param {UpdtrConfig} config 63 | */ 64 | constructor(config) { 65 | super(); 66 | 67 | const cwd = config.cwd; 68 | const registry = config.registry; 69 | 70 | const packageManager = config.use === undefined ? 71 | USE_OPTIONS[0] : 72 | config.use; 73 | 74 | const updateTo = config.updateTo === undefined ? 75 | UPDATE_TO_OPTIONS[0] : 76 | config.updateTo; 77 | 78 | const exclude = Array.isArray(config.exclude) === true ? 79 | config.exclude : 80 | []; 81 | 82 | const save = config.save === undefined ? SAVE_OPTIONS[0] : config.save; 83 | 84 | checkCwd(cwd); 85 | checkUse(packageManager); 86 | checkForYarnWithCustomReg(packageManager, registry); 87 | checkUpdateTo(updateTo); 88 | checkSave(save); 89 | 90 | this.config = { 91 | cwd, 92 | use: packageManager, 93 | exclude, 94 | test: config.test, 95 | registry, 96 | updateTo, 97 | save, 98 | interactive: config.interactive, 99 | }; 100 | this.cmds = cmds[packageManager]; 101 | this.parse = parse[packageManager]; 102 | 103 | if (typeof config.test === "string") { 104 | this.cmds = { 105 | ...this.cmds, 106 | test: () => config.test, 107 | }; 108 | } 109 | } 110 | 111 | async canAccessPackageJson() { 112 | let result = true; 113 | 114 | try { 115 | await fs.access( 116 | path.join(this.config.cwd, "package.json"), 117 | FS_CONSTANTS.R_OK | FS_CONSTANTS.W_OK // eslint-disable-line no-bitwise 118 | ); 119 | } catch (err) { 120 | result = false; 121 | } 122 | 123 | return result; 124 | } 125 | 126 | exec(cmd) { 127 | return exec(this.config.cwd, cmd); 128 | } 129 | 130 | readFile(filenameInCwd) { 131 | return fs.readFile(path.join(this.config.cwd, filenameInCwd), "utf8"); 132 | } 133 | 134 | writeFile(filenameInCwd, contents) { 135 | return fs.writeFile( 136 | path.join(this.config.cwd, filenameInCwd), 137 | contents 138 | ); 139 | } 140 | 141 | dispose() { 142 | this.removeAllListeners(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/bin/argv.js: -------------------------------------------------------------------------------- 1 | import {EOL} from "os"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import yargs from "yargs"; 5 | import chalk from "chalk"; 6 | import reporters from "../reporters"; 7 | import { 8 | USE_OPTIONS, 9 | USE_NPM, 10 | USE_YARN, 11 | UPDATE_TO_OPTIONS, 12 | SAVE_OPTIONS, 13 | } from "../constants/config"; 14 | 15 | const reporterNames = Object.keys(reporters); 16 | const pathToYarnLock = path.join(process.cwd(), "yarn.lock"); 17 | const useDefault = fs.existsSync(pathToYarnLock) === true ? USE_YARN : USE_NPM; 18 | 19 | export default yargs 20 | .usage( 21 | [ 22 | "", 23 | chalk.bold.cyan("Update outdated npm modules with zero pain™"), 24 | `${chalk.bold("Usage:")} $0 ${chalk.dim("[options]")}`, 25 | ].join(EOL) 26 | ) 27 | .option("use", { 28 | describe: "Specify the package manager to use", 29 | choices: USE_OPTIONS, 30 | default: useDefault, 31 | alias: "u", 32 | }) 33 | .option("exclude", { 34 | describe: "Space separated list of module names that should not be updated", 35 | array: true, 36 | alias: "ex", 37 | }) 38 | .option("interactive", { 39 | describe: "Let you select the packages you want to update", 40 | alias: "i", 41 | boolean: true, 42 | default: false, 43 | }) 44 | .option("update-to", { 45 | describe: "Specify which updates you want to install", 46 | choices: UPDATE_TO_OPTIONS, 47 | default: UPDATE_TO_OPTIONS[0], 48 | alias: "to", 49 | }) 50 | .option("save", { 51 | describe: "Specify how updated versions should be saved to the package.json", 52 | choices: SAVE_OPTIONS, 53 | default: SAVE_OPTIONS[0], 54 | alias: "s", 55 | }) 56 | .option("reporter", { 57 | describe: "Choose a reporter for the console output", 58 | choices: reporterNames, 59 | default: reporterNames[0], 60 | alias: "r", 61 | }) 62 | .option("test", { 63 | describe: "Specify a custom test command. Surround with quotes.", 64 | alias: "t", 65 | }) 66 | .option("test-stdout", { 67 | describe: "Show test stdout if the update fails", 68 | boolean: true, 69 | default: false, 70 | alias: "out", 71 | }) 72 | .option("registry", { 73 | describe: "Specify a custom registry to use", 74 | alias: "reg", 75 | }) 76 | .version() 77 | .wrap(null) 78 | .help().argv; 79 | -------------------------------------------------------------------------------- /src/bin/index.js: -------------------------------------------------------------------------------- 1 | import reporters from "../reporters"; 2 | import argv from "./argv"; 3 | import {create, run} from ".."; 4 | 5 | async function start() { 6 | const cwd = process.cwd(); 7 | const config = {...argv}; 8 | 9 | const reporterConfig = { 10 | stream: process.stdout, 11 | testStdout: argv.testStdout, 12 | }; 13 | 14 | const reporter = reporters[argv.reporter]; 15 | 16 | config.cwd = cwd; 17 | 18 | const updtr = create(config); 19 | 20 | reporter(updtr, reporterConfig); 21 | try { 22 | await run(updtr); 23 | } catch (err) { 24 | updtr.emit("error", err); 25 | } 26 | } 27 | 28 | start(); 29 | -------------------------------------------------------------------------------- /src/constants/config.js: -------------------------------------------------------------------------------- 1 | // The first value in arrays is the default value 2 | 3 | export const USE_NPM = "npm"; 4 | export const USE_YARN = "yarn"; 5 | export const USE_OPTIONS = [USE_NPM, USE_YARN]; 6 | export const UPDATE_TO_LATEST = "latest"; 7 | export const UPDATE_TO_NON_BREAKING = "non-breaking"; 8 | export const UPDATE_TO_WANTED = "wanted"; 9 | export const UPDATE_TO_OPTIONS = [ 10 | UPDATE_TO_LATEST, 11 | UPDATE_TO_NON_BREAKING, 12 | UPDATE_TO_WANTED, 13 | ]; 14 | export const SAVE_SMART = "smart"; 15 | export const SAVE_CARET = "caret"; 16 | export const SAVE_EXACT = "exact"; 17 | export const SAVE_OPTIONS = [SAVE_SMART, SAVE_CARET, SAVE_EXACT]; 18 | -------------------------------------------------------------------------------- /src/constants/filterReasons.js: -------------------------------------------------------------------------------- 1 | export const GIT = "git"; 2 | export const EXOTIC = "exotic"; 3 | export const UNSTABLE = "unstable"; 4 | export const EXCLUDED = "excluded"; 5 | export const NOT_WANTED = "not-wanted"; 6 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | // Errors are not extendable in node v4. 2 | // Remove this if node v4 is not supported anymore. 3 | import Error from "es6-error"; 4 | 5 | export class PackageJsonNoAccessError extends Error { 6 | constructor(dir) { 7 | super(`Cannot access package.json in ${dir}`); 8 | } 9 | } 10 | 11 | export class RequiredOptionMissingError extends Error { 12 | constructor(optionName, optionValue) { 13 | super( 14 | `Required option ${optionName} is missing. Instead received ${optionValue}` 15 | ); 16 | } 17 | } 18 | 19 | export class OptionValueNotSupportedError extends Error { 20 | constructor(optionName, unsupportedValue) { 21 | super(`Unsupported value ${unsupportedValue} for option ${optionName}`); 22 | } 23 | } 24 | 25 | export class YarnWithCustomRegistryError extends Error { 26 | constructor() { 27 | super( 28 | "yarn does not support custom registries yet. Please use a .npmrc file to achieve this" 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/exec/cmds.js: -------------------------------------------------------------------------------- 1 | function installFn(baseCmd) { 2 | return ({registry, modules} = {}) => 3 | [baseCmd, stringifyRegistry(registry), stringifyModules(modules)].join( 4 | "" 5 | ); 6 | } 7 | 8 | function stringifyModules(modules) { 9 | return Array.isArray(modules) === true ? 10 | " " + 11 | modules 12 | // We need to wrap this in double-quotes because some semver 13 | // characters like the caret symbol are reserved characters on Windows. 14 | .map(({name, version}) => `"${name}@${version}"`) 15 | .join(" ") : 16 | ""; 17 | } 18 | 19 | function stringifyRegistry(registry) { 20 | return registry === undefined ? "" : ` --registry "${registry}"`; 21 | } 22 | 23 | const cmds = { 24 | npm: { 25 | outdated: () => "npm outdated --json --depth=0", 26 | installMissing: installFn("npm install"), 27 | install: installFn("npm install"), 28 | // remove: ({ name }) => ["npm remove ", name].join(""), 29 | test: () => "npm test", 30 | list: ({modules} = {}) => [ 31 | "npm ls --json --depth=0", 32 | Array.isArray(modules) === true ? " " + modules.join(" ") : "", 33 | ].join(""), 34 | }, 35 | // yarn does not support custom registries yet. 36 | // However, these renderers accept them anyway. 37 | yarn: { 38 | outdated: () => "yarn outdated --json --flat", 39 | installMissing: installFn("yarn"), 40 | install: installFn("yarn add"), 41 | // remove: ({ name }) => ["yarn remove ", name].join(""), 42 | test: () => "yarn test", 43 | list: ({modules} = {}) => [ 44 | "yarn list --json --depth=0", 45 | Array.isArray(modules) === true ? " " + modules.join(" ") : "", 46 | ].join(""), 47 | }, 48 | }; 49 | 50 | export default cmds; 51 | -------------------------------------------------------------------------------- /src/exec/exec.js: -------------------------------------------------------------------------------- 1 | import childProcess from "child_process"; 2 | 3 | function promiseExec(cwd, cmd) { 4 | return new Promise(resolve => { 5 | childProcess.exec( 6 | cmd, 7 | {maxBuffer: Infinity, encoding: "utf8", cwd}, 8 | // We need to use the callback API here 9 | (err, stdout, stderr) => void resolve({err, stdout, stderr}) // eslint-disable-line promise/prefer-await-to-callbacks 10 | ); 11 | }); 12 | } 13 | 14 | export default (async function exec(cwd, cmd) { 15 | const {err, stdout, stderr} = await promiseExec(cwd, cmd); 16 | 17 | if (err !== null) { 18 | err.stdout = stdout; 19 | err.stderr = stderr; 20 | 21 | throw err; 22 | } 23 | 24 | return {stdout, stderr}; 25 | }); 26 | -------------------------------------------------------------------------------- /src/exec/parse.js: -------------------------------------------------------------------------------- 1 | const STRING_PROPERTIES = ["name", "current", "wanted", "latest"]; 2 | 3 | function isNotEmptyString(value) { 4 | return typeof value === "string" && value.length > 0; 5 | } 6 | 7 | function returnIfValid(result) { 8 | STRING_PROPERTIES.forEach(prop => { 9 | if (isNotEmptyString(result[prop]) === false) { 10 | throw new Error("Unexpected output format of package manager"); 11 | } 12 | }); 13 | 14 | return result; 15 | } 16 | 17 | function tryParse(parser) { 18 | return (stdout, cmd) => { 19 | try { 20 | return parser(stdout); 21 | } catch (err) { 22 | err.message = `Error when trying to parse stdout from command '${cmd}': ${err.message}`; 23 | throw err; 24 | } 25 | }; 26 | } 27 | 28 | function arrToObj(arr, keys) { 29 | return keys.reduce((obj, key, i) => { 30 | obj[key] = arr[i]; 31 | 32 | return obj; 33 | }, {}); 34 | } 35 | 36 | // By sorting the parsed data, we get deterministic results across different npm and yarn versions. 37 | // As a nice side-effect, a package like eslint will always be updated before eslint-config-peerigon 38 | // which might have a peer dependency on eslint 39 | // See https://github.com/peerigon/updtr/issues/48 40 | function sortByName(o1, o2) { 41 | return o1.name > o2.name; 42 | } 43 | 44 | function npmParser(stdout) { 45 | const trimmed = stdout.trim(); 46 | 47 | if (trimmed.length === 0) { 48 | return null; 49 | } 50 | 51 | return JSON.parse(trimmed); 52 | } 53 | 54 | export function splitYarnLines(stdout) { 55 | // Yarn is using \n on all platforms now in their stdout 56 | return stdout.split("\n"); 57 | } 58 | 59 | function yarnParser(stdout, wantedTypeProperty) { 60 | try { 61 | return npmParser(stdout); 62 | } catch (error) { 63 | /* in some cases (e.g. when printing the outdated result), yarn prints for each line a separate JSON object */ 64 | /* in that case, we need to look for a { type: "table" } object which holds the interesting data to display */ 65 | } 66 | const dataLine = splitYarnLines(stdout) 67 | .map(line => line.trim()) 68 | .filter(line => line !== "") 69 | .find(line => { 70 | try { 71 | console.log(line); 72 | const parsedLine = JSON.parse(line); 73 | 74 | return parsedLine.type === wantedTypeProperty; 75 | } catch (error) { 76 | console.log(error); 77 | 78 | return false; 79 | } 80 | }); 81 | 82 | if (dataLine === undefined) { 83 | throw new Error(`Could not find object with type === ${wantedTypeProperty}`); 84 | } 85 | 86 | return JSON.parse(dataLine); 87 | } 88 | 89 | const parse = { 90 | npm: { 91 | outdated: tryParse(stdout => { 92 | const parsed = npmParser(stdout); 93 | 94 | if (parsed === null) { 95 | return []; 96 | } 97 | 98 | const names = Object.keys(parsed); 99 | 100 | return names 101 | .map(name => parsed[name]) 102 | .map((dep, index) => 103 | returnIfValid({ 104 | name: names[index], 105 | current: dep.current, 106 | wanted: dep.wanted, 107 | latest: dep.latest, 108 | }) 109 | ) 110 | .sort(sortByName); 111 | }), 112 | list: tryParse(stdout => { 113 | const parsed = npmParser(stdout); 114 | 115 | return (parsed.dependencies === undefined ? 116 | [] : 117 | Object.keys(parsed.dependencies) 118 | .map(name => ({ 119 | name, 120 | version: parsed.dependencies[name].version, 121 | })) 122 | .sort(sortByName)); 123 | }), 124 | }, 125 | yarn: { 126 | outdated: tryParse( 127 | stdout => { 128 | const parsed = yarnParser(stdout, "table"); 129 | 130 | if (parsed === null) { 131 | return []; 132 | } 133 | 134 | return parsed.data.body 135 | .map(row => arrToObj(row, parsed.data.head)) 136 | .map(dep => 137 | returnIfValid({ 138 | name: dep.Package, 139 | current: dep.Current, 140 | wanted: dep.Wanted, 141 | latest: dep.Latest, 142 | }) 143 | ) 144 | .sort(sortByName); 145 | } 146 | ), 147 | list: tryParse(stdout => { 148 | const parsed = yarnParser(stdout, "tree"); 149 | 150 | if (parsed.data.trees.length === 0) { 151 | return []; 152 | } 153 | 154 | return parsed.data.trees 155 | .map(dependency => { 156 | const [name, version] = dependency.name.split("@"); 157 | 158 | if (isNotEmptyString(name) === false || isNotEmptyString(version) === false) { 159 | throw new Error(`Could not parse dependency name "${dependency.name}"`); 160 | } 161 | 162 | return { 163 | name, 164 | version, 165 | }; 166 | }) 167 | .sort(sortByName); 168 | }), 169 | }, 170 | }; 171 | 172 | export default parse; 173 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import run from "./run"; 2 | import Updtr from "./Updtr"; 3 | import * as errors from "./errors"; 4 | 5 | export function create(config) { 6 | return new Updtr(config); 7 | } 8 | 9 | export {errors, run}; 10 | -------------------------------------------------------------------------------- /src/reporters/basic.js: -------------------------------------------------------------------------------- 1 | import ansiEscapes from "ansi-escapes"; 2 | import chalk from "chalk"; 3 | import unicons from "unicons"; 4 | import { 5 | filterSuccessfulUpdates, 6 | filterFailedUpdates, 7 | } from "../tasks/util/filterUpdateResults"; 8 | import Message from "./util/Message"; 9 | import Indicator, { 10 | INDICATOR_NEUTRAL, 11 | INDICATOR_PENDING, 12 | INDICATOR_OK, 13 | INDICATOR_FAIL, 14 | } from "./util/Indicator"; 15 | import customConfigToLines from "./util/customConfigToLines"; 16 | import pluralize from "./util/pluralize"; 17 | import handleError from "./util/handleError"; 18 | import msToString from "./util/msToString"; 19 | 20 | function updatingLine(updateTask) { 21 | return [ 22 | new Indicator(INDICATOR_PENDING), 23 | chalk.bold(updateTask.name), 24 | chalk.grey("updating"), 25 | updateTask.rollbackTo, 26 | chalk.grey(unicons.arrowRight), 27 | updateTask.updateTo + chalk.grey("..."), 28 | ].join(" "); 29 | } 30 | 31 | function testingLine(updateTask) { 32 | return [ 33 | new Indicator(INDICATOR_PENDING), 34 | chalk.bold(updateTask.name), 35 | chalk.grey("testing..."), 36 | ].join(" "); 37 | } 38 | 39 | function rollbackLine(updateTask) { 40 | return [ 41 | new Indicator(INDICATOR_FAIL), 42 | chalk.bold.red(updateTask.name), 43 | chalk.grey("rolling back"), 44 | updateTask.updateTo, 45 | chalk.grey(unicons.arrowRight), 46 | updateTask.rollbackTo + chalk.grey("..."), 47 | ].join(" "); 48 | } 49 | 50 | function successLine(updateTask) { 51 | return [ 52 | new Indicator(INDICATOR_OK), 53 | chalk.bold(updateTask.name), 54 | updateTask.updateTo, 55 | chalk.grey("success"), 56 | ].join(" "); 57 | } 58 | 59 | function failLine(updateTask) { 60 | return [ 61 | new Indicator(INDICATOR_FAIL), 62 | chalk.bold.red(updateTask.name), 63 | updateTask.updateTo, 64 | chalk.grey("failed"), 65 | ].join(" "); 66 | } 67 | 68 | function excludedLine(excluded) { 69 | return [ 70 | new Indicator(INDICATOR_NEUTRAL), 71 | chalk.bold(excluded.name), 72 | chalk.grey(excluded.reason), 73 | ].join(" "); 74 | } 75 | 76 | function cmdToLines(description, cmd) { 77 | const lines = Array.isArray(description) === true ? 78 | description : 79 | [description]; 80 | 81 | return lines.concat([chalk.grey(`> ${cmd} `)]); 82 | } 83 | 84 | function writeLinesToConsole(lines) { 85 | if (lines.length === 0) { 86 | return; 87 | } 88 | console.log(ansiEscapes.eraseDown + lines.join("\n")); 89 | } 90 | 91 | export default function basic(updtr, reporterConfig) { 92 | const startTime = Date.now(); 93 | let excludedModules; 94 | 95 | updtr.on("start", ({config}) => { 96 | writeLinesToConsole(customConfigToLines(config)); 97 | }); 98 | updtr.on("init/install-missing", ({cmd}) => { 99 | writeLinesToConsole( 100 | cmdToLines( 101 | "Installing missing dependencies" + chalk.grey("..."), 102 | cmd 103 | ) 104 | ); 105 | }); 106 | updtr.on("init/collect", ({cmd}) => { 107 | writeLinesToConsole( 108 | cmdToLines("Looking for outdated modules" + chalk.grey("..."), cmd) 109 | ); 110 | }); 111 | updtr.on("init/end", ({updateTasks, excluded}) => { 112 | excludedModules = excluded; 113 | if (updateTasks.length === 0 && excluded.length === 0) { 114 | writeLinesToConsole(["Everything " + chalk.bold("up-to-date")]); 115 | } else if (updateTasks.length === 0) { 116 | writeLinesToConsole([ 117 | chalk.bold("No updates available") + 118 | " for the given modules and version range", 119 | ]); 120 | } else { 121 | writeLinesToConsole([ 122 | new Message("Found " + chalk.bold("%s update%s") + ".", [ 123 | updateTasks.length, 124 | pluralize(updateTasks.length), 125 | ]), 126 | "", 127 | ]); 128 | } 129 | }); 130 | updtr.on("batch-update/updating", event => { 131 | writeLinesToConsole( 132 | cmdToLines(event.updateTasks.map(updatingLine), event.cmd) 133 | ); 134 | }); 135 | updtr.on("batch-update/testing", event => { 136 | writeLinesToConsole( 137 | cmdToLines(event.updateTasks.map(testingLine), event.cmd) 138 | ); 139 | }); 140 | updtr.on("batch-update/rollback", event => { 141 | writeLinesToConsole( 142 | cmdToLines(event.updateTasks.map(rollbackLine), event.cmd) 143 | ); 144 | }); 145 | updtr.on("batch-update/result", event => { 146 | if (event.success === true) { 147 | writeLinesToConsole( 148 | event.updateTasks.map(event.success ? successLine : failLine) 149 | ); 150 | } 151 | // Not showing the test stdout here when there was an error because 152 | // we will proceed with the sequential update. 153 | }); 154 | updtr.on("sequential-update/updating", event => { 155 | writeLinesToConsole(cmdToLines(updatingLine(event), event.cmd)); 156 | }); 157 | updtr.on("sequential-update/testing", event => { 158 | writeLinesToConsole(cmdToLines(testingLine(event), event.cmd)); 159 | }); 160 | updtr.on("sequential-update/rollback", event => { 161 | writeLinesToConsole(cmdToLines(rollbackLine(event), event.cmd)); 162 | }); 163 | updtr.on("sequential-update/result", event => { 164 | writeLinesToConsole([(event.success ? successLine : failLine)(event)]); 165 | if (reporterConfig.testStdout === true && event.success === false) { 166 | writeLinesToConsole([event.stdout]); 167 | } 168 | }); 169 | updtr.on("end", ({results}) => { 170 | const duration = msToString(Date.now() - startTime); 171 | const successful = filterSuccessfulUpdates(results); 172 | const failed = filterFailedUpdates(results); 173 | 174 | writeLinesToConsole([""]); 175 | 176 | if (successful.length > 0) { 177 | writeLinesToConsole([ 178 | new Message(chalk.bold("%s successful") + " update%s.", [ 179 | successful.length, 180 | pluralize(successful.length), 181 | ]), 182 | ]); 183 | } 184 | if (failed.length > 0) { 185 | writeLinesToConsole([ 186 | new Message(chalk.bold("%s failed") + " update%s.", [ 187 | failed.length, 188 | pluralize(failed.length), 189 | ]), 190 | ]); 191 | } 192 | if (excludedModules.length > 0) { 193 | const list = excludedModules.map(excludedLine); 194 | 195 | if (successful.length > 0 || failed.length > 0) { 196 | writeLinesToConsole([""]); 197 | } 198 | writeLinesToConsole( 199 | [ 200 | new Message(chalk.bold("%s skipped") + " module%s:", [ 201 | excludedModules.length, 202 | pluralize(excludedModules.length), 203 | ]), 204 | "", 205 | ].concat(list) 206 | ); 207 | } 208 | 209 | writeLinesToConsole(["", new Message("Finished after %s.", [duration])]); 210 | }); 211 | updtr.on("error", err => void handleError(err)); 212 | } 213 | -------------------------------------------------------------------------------- /src/reporters/dense.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import unicons from "unicons"; 3 | import { 4 | filterSuccessfulUpdates, 5 | filterFailedUpdates, 6 | } from "../tasks/util/filterUpdateResults"; 7 | import Projector from "./util/Projector"; 8 | import Terminal from "./util/Terminal"; 9 | import Message from "./util/Message"; 10 | import Spinner from "./util/Spinner"; 11 | import Indicator, { 12 | INDICATOR_NEUTRAL, 13 | INDICATOR_PENDING, 14 | INDICATOR_OK, 15 | INDICATOR_FAIL, 16 | } from "./util/Indicator"; 17 | import customConfigToLines from "./util/customConfigToLines"; 18 | import pluralize from "./util/pluralize"; 19 | import handleError from "./util/handleError"; 20 | import msToString from "./util/msToString"; 21 | 22 | const spinner = new Spinner("dots"); 23 | 24 | function updatingLine(updateTask) { 25 | return [ 26 | new Indicator(INDICATOR_PENDING), 27 | chalk.bold(updateTask.name), 28 | chalk.grey("updating"), 29 | updateTask.rollbackTo, 30 | chalk.grey(unicons.arrowRight), 31 | updateTask.updateTo + chalk.grey("..."), 32 | ].join(" "); 33 | } 34 | 35 | function testingLine(updateTask) { 36 | return [ 37 | new Indicator(INDICATOR_PENDING), 38 | chalk.bold(updateTask.name), 39 | chalk.grey("testing..."), 40 | ].join(" "); 41 | } 42 | 43 | function rollbackLine(updateTask) { 44 | return [ 45 | new Indicator(INDICATOR_FAIL), 46 | chalk.bold.red(updateTask.name), 47 | chalk.grey("rolling back"), 48 | updateTask.updateTo, 49 | chalk.grey(unicons.arrowRight), 50 | updateTask.rollbackTo + chalk.grey("..."), 51 | ].join(" "); 52 | } 53 | 54 | function successLine(updateTask) { 55 | return [ 56 | new Indicator(INDICATOR_OK), 57 | chalk.bold(updateTask.name), 58 | updateTask.updateTo, 59 | chalk.grey("success"), 60 | ].join(" "); 61 | } 62 | 63 | function failLine(updateTask) { 64 | return [ 65 | new Indicator(INDICATOR_FAIL), 66 | chalk.bold.red(updateTask.name), 67 | updateTask.updateTo, 68 | chalk.grey("failed"), 69 | ].join(" "); 70 | } 71 | 72 | function excludedLine(excluded) { 73 | return [ 74 | new Indicator(INDICATOR_NEUTRAL), 75 | chalk.bold(excluded.name), 76 | chalk.grey(excluded.reason), 77 | ].join(" "); 78 | } 79 | 80 | function cmdToLines(description, cmd) { 81 | const lines = Array.isArray(description) === true ? 82 | description : 83 | [description]; 84 | 85 | return lines.concat([chalk.grey(`> ${cmd} `), spinner]); 86 | } 87 | 88 | function dense(updtr, reporterConfig) { 89 | const terminal = new Terminal(reporterConfig.stream); 90 | const projector = new Projector(terminal); 91 | const startTime = Date.now(); 92 | let excludedModules; 93 | 94 | updtr.on("start", ({config}) => { 95 | terminal.append(customConfigToLines(config)); 96 | }); 97 | updtr.on("init/install-missing", ({cmd}) => { 98 | projector.display( 99 | cmdToLines( 100 | "Installing missing dependencies" + chalk.grey("..."), 101 | cmd 102 | ) 103 | ); 104 | }); 105 | updtr.on("init/collect", ({cmd}) => { 106 | projector.display( 107 | cmdToLines("Looking for outdated modules" + chalk.grey("..."), cmd) 108 | ); 109 | }); 110 | updtr.on("init/end", ({updateTasks, excluded}) => { 111 | excludedModules = excluded; 112 | projector.stop(); 113 | if (updateTasks.length === 0 && excluded.length === 0) { 114 | terminal.append(["Everything " + chalk.bold("up-to-date")]); 115 | } else if (updateTasks.length === 0) { 116 | terminal.append([ 117 | chalk.bold("No updates available") + 118 | " for the given modules and version range", 119 | ]); 120 | } else { 121 | terminal.append([ 122 | new Message("Found " + chalk.bold("%s update%s") + ".", [ 123 | updateTasks.length, 124 | pluralize(updateTasks.length), 125 | ]), 126 | "", 127 | ]); 128 | } 129 | }); 130 | updtr.on("batch-update/updating", event => { 131 | projector.display( 132 | cmdToLines(event.updateTasks.map(updatingLine), event.cmd) 133 | ); 134 | }); 135 | updtr.on("batch-update/testing", event => { 136 | projector.display( 137 | cmdToLines(event.updateTasks.map(testingLine), event.cmd) 138 | ); 139 | }); 140 | updtr.on("batch-update/rollback", event => { 141 | projector.display( 142 | cmdToLines(event.updateTasks.map(rollbackLine), event.cmd) 143 | ); 144 | }); 145 | updtr.on("batch-update/result", event => { 146 | projector.stop(); 147 | if (event.success === true) { 148 | terminal.append( 149 | event.updateTasks.map(event.success ? successLine : failLine) 150 | ); 151 | } 152 | // Not showing the test stdout here when there was an error because 153 | // we will proceed with the sequential update. 154 | }); 155 | updtr.on("sequential-update/updating", event => { 156 | projector.display(cmdToLines(updatingLine(event), event.cmd)); 157 | }); 158 | updtr.on("sequential-update/testing", event => { 159 | projector.display(cmdToLines(testingLine(event), event.cmd)); 160 | }); 161 | updtr.on("sequential-update/rollback", event => { 162 | projector.display(cmdToLines(rollbackLine(event), event.cmd)); 163 | }); 164 | updtr.on("sequential-update/result", event => { 165 | projector.stop(); 166 | terminal.append([(event.success ? successLine : failLine)(event)]); 167 | if (reporterConfig.testStdout === true && event.success === false) { 168 | terminal.append([event.stdout]); 169 | } 170 | }); 171 | updtr.on("end", ({results}) => { 172 | const duration = msToString(Date.now() - startTime); 173 | const successful = filterSuccessfulUpdates(results); 174 | const failed = filterFailedUpdates(results); 175 | 176 | terminal.append([""]); 177 | 178 | if (successful.length > 0) { 179 | terminal.append([ 180 | new Message(chalk.bold("%s successful") + " update%s.", [ 181 | successful.length, 182 | pluralize(successful.length), 183 | ]), 184 | ]); 185 | } 186 | if (failed.length > 0) { 187 | terminal.append([ 188 | new Message(chalk.bold("%s failed") + " update%s.", [ 189 | failed.length, 190 | pluralize(failed.length), 191 | ]), 192 | ]); 193 | } 194 | if (excludedModules.length > 0) { 195 | const list = excludedModules.map(excludedLine); 196 | 197 | if (successful.length > 0 || failed.length > 0) { 198 | terminal.append([""]); 199 | } 200 | terminal.append( 201 | [ 202 | new Message(chalk.bold("%s skipped") + " module%s:", [ 203 | excludedModules.length, 204 | pluralize(excludedModules.length), 205 | ]), 206 | "", 207 | ].concat(list) 208 | ); 209 | } 210 | 211 | terminal.append(["", new Message("Finished after %s.", [duration])]); 212 | }); 213 | updtr.on("error", err => void handleError(err)); 214 | } 215 | 216 | export default dense; 217 | -------------------------------------------------------------------------------- /src/reporters/index.js: -------------------------------------------------------------------------------- 1 | import basic from "./basic"; 2 | import dense from "./dense"; 3 | import none from "./none"; 4 | 5 | // The first property here is the default reporter 6 | const reporters = {dense, basic, none}; 7 | 8 | export default reporters; 9 | -------------------------------------------------------------------------------- /src/reporters/none.js: -------------------------------------------------------------------------------- 1 | export default Function.prototype; 2 | -------------------------------------------------------------------------------- /src/reporters/util/Indicator.js: -------------------------------------------------------------------------------- 1 | import unicons from "unicons"; 2 | import chalk from "chalk"; 3 | 4 | export const INDICATOR_NEUTRAL = 0; 5 | export const INDICATOR_FAIL = 1; 6 | export const INDICATOR_PENDING = 2; 7 | export const INDICATOR_OK = 3; 8 | 9 | const COLORS = [chalk.grey, chalk.red, chalk.yellow, chalk.green]; 10 | 11 | export default class Indicator { 12 | constructor(initialState) { 13 | this.state = initialState; 14 | } 15 | 16 | valueOf() { 17 | return COLORS[this.state](unicons.cli("circle")); 18 | } 19 | 20 | toString() { 21 | return this.valueOf(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/reporters/util/Message.js: -------------------------------------------------------------------------------- 1 | export default class Message { 2 | constructor(template, variables) { 3 | this.template = template; 4 | this.variables = variables; 5 | } 6 | 7 | valueOf() { 8 | const split = this.template.split(/%s/g); 9 | 10 | return split.reduce( 11 | (str, part, i) => str + part + (this.variables[i] || ""), 12 | "" 13 | ); 14 | } 15 | 16 | toString() { 17 | return this.valueOf(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/reporters/util/Projector.js: -------------------------------------------------------------------------------- 1 | function lineToString(line) { 2 | if (Array.isArray(line) === true) { 3 | return line.join(""); 4 | } 5 | 6 | return String(line); 7 | } 8 | 9 | export default class Projector { 10 | constructor(terminal, frameRate = 10) { 11 | this.terminal = terminal; 12 | this.delay = Math.floor(1000 / frameRate); 13 | this.timeoutId = null; 14 | } 15 | 16 | display(frame) { 17 | if (this.timeoutId !== null) { 18 | this.stop(); 19 | } 20 | this.terminal.append(frame.map(lineToString)); 21 | this.timeoutId = setTimeout(() => { 22 | this.display(frame); 23 | }, this.delay); 24 | } 25 | 26 | stop() { 27 | if (this.timeoutId === null) { 28 | return; 29 | } 30 | this.terminal.rewind(); 31 | clearTimeout(this.timeoutId); 32 | this.timeoutId = null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/reporters/util/Spinner.js: -------------------------------------------------------------------------------- 1 | import cliSpinners from "cli-spinners"; 2 | 3 | const isWin = process.platform === "win32"; 4 | const winFallback = "simpleDotsScrolling"; 5 | 6 | export default class Spinner { 7 | constructor(spinnerName) { 8 | const spinner = cliSpinners[isWin === true ? winFallback : spinnerName]; 9 | 10 | this.frames = spinner.frames; 11 | this.interval = spinner.interval; 12 | this.length = Math.max(...this.frames.map(frame => frame.length)); 13 | } 14 | 15 | valueOf() { 16 | const currentInterval = Math.floor(Date.now() / this.interval); 17 | const currentFrame = currentInterval % this.frames.length; 18 | 19 | return this.frames[currentFrame]; 20 | } 21 | 22 | toString() { 23 | return this.valueOf(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/reporters/util/Terminal.js: -------------------------------------------------------------------------------- 1 | import ansiEscapes from "ansi-escapes"; 2 | import cliCursor from "cli-cursor"; 3 | import stringWidth from "string-width"; 4 | 5 | function calcNumOfRows(lines, columns) { 6 | return lines 7 | .map(lineContent => Math.ceil(stringWidth(lineContent) / columns)) 8 | .reduce((allRows, rows) => allRows + rows, 0); 9 | } 10 | 11 | // Solves some issues where stdout output is truncated 12 | // See https://github.com/nodejs/node/issues/6456 13 | function setBlocking(stream) { 14 | if (stream._handle && typeof stream._handle.setBlocking === "function") { 15 | stream._handle.setBlocking(true); 16 | } 17 | } 18 | 19 | export default class Terminal { 20 | constructor(stream) { 21 | if (stream.isTTY !== true) { 22 | throw new Error("Given stream is not a TTY stream"); 23 | } 24 | setBlocking(stream); 25 | cliCursor.hide(stream); 26 | this.stream = stream; 27 | this.lines = []; 28 | this.hasBeenResized = false; 29 | this.stream.on("resize", () => { 30 | this.hasBeenResized = true; 31 | }); 32 | } 33 | 34 | append(lines) { 35 | if (lines.length === 0) { 36 | return; 37 | } 38 | this.lines.push(lines); 39 | 40 | const content = this.hasBeenResized === true ? 41 | ansiEscapes.clearScreen + 42 | this.lines.map(lines => lines.join("\n")).join("\n") : 43 | ansiEscapes.eraseDown + lines.join("\n"); 44 | 45 | this.stream.write(content + "\n"); 46 | this.hasBeenResized = false; 47 | } 48 | 49 | rewind() { 50 | const removedLines = this.lines.pop(); 51 | const rows = calcNumOfRows(removedLines, this.stream.columns); 52 | 53 | this.stream.write(ansiEscapes.cursorUp(rows)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/reporters/util/configList.js: -------------------------------------------------------------------------------- 1 | import { 2 | USE_OPTIONS, 3 | UPDATE_TO_OPTIONS, 4 | SAVE_OPTIONS, 5 | } from "../../constants/config"; 6 | 7 | const configNames = { 8 | use: "use", 9 | exclude: "exclude", 10 | test: "test command", 11 | registry: "registry", 12 | updateTo: "update to", 13 | save: "save", 14 | }; 15 | 16 | const configValues = { 17 | exclude: list => list.join(", "), 18 | }; 19 | 20 | const configFilter = { 21 | cwd: () => false, 22 | use: option => option !== USE_OPTIONS[0], 23 | exclude: list => list.length > 0, 24 | test: cmd => cmd !== undefined, 25 | registry: reg => reg !== undefined, 26 | updateTo: option => option !== UPDATE_TO_OPTIONS[0], 27 | save: option => option !== SAVE_OPTIONS[0], 28 | }; 29 | 30 | export default function configList(config) { 31 | return Object.keys(config) 32 | .filter(key => { 33 | const filter = configFilter[key]; 34 | const name = configNames[key]; 35 | 36 | return ( 37 | name !== undefined && 38 | (filter === undefined || filter(config[key]) === true) 39 | ); 40 | }) 41 | .map(key => { 42 | const toString = configValues[key] || String; 43 | 44 | return `${configNames[key]}: ${toString(config[key])}`; 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/reporters/util/customConfigToLines.js: -------------------------------------------------------------------------------- 1 | import unicons from "unicons"; 2 | import configList from "./configList"; 3 | 4 | export default function customConfigToLines(config) { 5 | const list = configList(config); 6 | const lines = []; 7 | 8 | if (list.length > 0) { 9 | lines.push( 10 | "Running updtr with custom configuration:", 11 | "", 12 | ...list.map(item => unicons.cli("circle") + " " + item), 13 | "" 14 | ); 15 | } 16 | 17 | return lines; 18 | } 19 | -------------------------------------------------------------------------------- /src/reporters/util/handleError.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import {PackageJsonNoAccessError} from "../../errors"; 3 | 4 | const ERROR = chalk.bgRed.bold(" ERROR "); 5 | 6 | export default function handleError(err) { 7 | const lines = [""]; 8 | 9 | switch (err.constructor) { 10 | case PackageJsonNoAccessError: 11 | lines.push( 12 | ERROR + " Cannot find package.json in current directory." 13 | ); 14 | break; 15 | default: { 16 | // The stack does only contain \n, also on windows 17 | const stack = err.stack.split("\n"); 18 | 19 | stack.shift(); 20 | lines.push(ERROR + " " + err.message); 21 | lines.push(...stack.map(line => chalk.grey(line))); 22 | } 23 | } 24 | lines.push(""); 25 | 26 | console.error(lines.join("\n")); 27 | process.exit(1); // eslint-disable-line no-process-exit 28 | } 29 | -------------------------------------------------------------------------------- /src/reporters/util/msToString.js: -------------------------------------------------------------------------------- 1 | function msToString(milliseconds) { 2 | return Math.floor(milliseconds / 1000).toFixed(1) + "s"; 3 | } 4 | 5 | export default msToString; 6 | -------------------------------------------------------------------------------- /src/reporters/util/pluralize.js: -------------------------------------------------------------------------------- 1 | export default function pluralize(num) { 2 | return num === 1 ? "" : "s"; 3 | } 4 | -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | import chalk from "chalk"; 3 | import init from "./tasks/init"; 4 | import sequentialUpdate from "./tasks/sequentialUpdate"; 5 | import splitUpdateTasks from "./tasks/util/splitUpdateTasks"; 6 | import batchUpdate from "./tasks/batchUpdate"; 7 | import createUpdateResult from "./tasks/util/createUpdateResult"; 8 | import finish from "./tasks/finish"; 9 | import updatePackageJson from "./tasks/updatePackageJson"; 10 | 11 | async function runUpdateTasks(updtr, updateTasks) { 12 | const results = []; 13 | const {breaking, nonBreaking} = splitUpdateTasks(updateTasks); 14 | const sequentialUpdateTasks = breaking.slice(); 15 | let batchSuccess; // can be undefined, true or false 16 | let batchUpdateFailure; 17 | 18 | // Run batch update if we have more than one non-breaking update 19 | // If the batch update fails, it will roll back all modules except the first one. 20 | // This way we can skip one install command since we will run the sequential update for it anyway. 21 | if (nonBreaking.length > 1) { 22 | batchSuccess = await batchUpdate(updtr, nonBreaking); 23 | } 24 | 25 | if (batchSuccess === true) { 26 | results.push( 27 | ...nonBreaking.map(updateTask => 28 | createUpdateResult(updateTask, true) 29 | ) 30 | ); 31 | } else { 32 | sequentialUpdateTasks.unshift(...nonBreaking); 33 | // If batchSuccess is false, we have actually executed the batch update and it returned false 34 | if (batchSuccess === false) { 35 | batchUpdateFailure = createUpdateResult(nonBreaking[0], false); 36 | } 37 | } 38 | 39 | // Run sequential update for all breaking updates and non-breaking batch updates that failed 40 | results.push( 41 | ...(await sequentialUpdate( 42 | updtr, 43 | sequentialUpdateTasks, 44 | batchUpdateFailure 45 | )) 46 | ); 47 | 48 | return finish(updtr, results); 49 | } 50 | 51 | function generateChoices(tasks) { 52 | let dims = [ 53 | "name".length, 54 | "from".length, 55 | // "to".length, 56 | ]; 57 | 58 | dims = tasks.reduce((acc, {name, rollbackTo, updateTo}) => ([ 59 | name.length > acc[0] ? name.length : acc[0], 60 | rollbackTo.length > acc[1] ? rollbackTo.length : acc[1], 61 | // updateTo.length > acc[2] ? updateTo.length : acc[2], 62 | ]), dims); 63 | 64 | let choices = [ 65 | new inquirer.Separator(" "), 66 | new inquirer.Separator(chalk` {green.bold.underline name}${"".padEnd((dims[0] + 3) - "name".length, " ")}{green.bold.underline from}${"".padEnd((dims[1] + 5) - "from".length, " ")}{green.bold.underline to}`), 67 | ]; 68 | 69 | choices = choices.concat(tasks.map(task => ({ 70 | name: chalk`{green ${task.name.padEnd(dims[0] + 3)}}{blue ${task.rollbackTo.padEnd(dims[1])}} ❯ {blue ${task.updateTo}}`, 71 | value: task, 72 | short: `${task.name}@${task.updateTo}`, 73 | }))); 74 | 75 | return choices; 76 | } 77 | 78 | async function selectUpdateTasks(tasks) { 79 | const prompt = inquirer.createPromptModule(); 80 | const choices = generateChoices(tasks); 81 | 82 | const {packagesToUpdate} = await prompt([ 83 | { 84 | name: "packagesToUpdate", 85 | type: "checkbox", 86 | message: chalk`{white.bold Choose which packages to update.}`, 87 | choices, 88 | pageSize: choices.length, 89 | validate: answer => Boolean(answer.length) || "You must choose at least one package.", 90 | }, 91 | ]); 92 | 93 | return packagesToUpdate; 94 | } 95 | 96 | export default (async function run(updtr) { 97 | const results = []; 98 | 99 | updtr.emit("start", { 100 | config: updtr.config, 101 | }); 102 | 103 | let {updateTasks} = await init(updtr); 104 | 105 | if (updtr.config.interactive === true) { 106 | updateTasks = await selectUpdateTasks(updateTasks); 107 | } 108 | 109 | if (updateTasks.length > 0) { 110 | results.push(...(await runUpdateTasks(updtr, updateTasks))); 111 | await updatePackageJson(updtr, results); 112 | } 113 | 114 | updtr.emit("end", { 115 | config: updtr.config, 116 | results, 117 | }); 118 | 119 | return results; 120 | }); 121 | 122 | -------------------------------------------------------------------------------- /src/tasks/batchUpdate.js: -------------------------------------------------------------------------------- 1 | import Sequence from "./util/Sequence"; 2 | import updateTo from "./util/updateTo"; 3 | import rollbackTo from "./util/rollbackTo"; 4 | 5 | function renderUpdate(updtr, updateTask) { 6 | return updtr.cmds.install({ 7 | registry: updtr.config.registry, 8 | modules: updateTask.map(updateTo), 9 | }); 10 | } 11 | 12 | function renderTest(updtr) { 13 | return updtr.cmds.test(); 14 | } 15 | 16 | function renderRollback(updtr, failedUpdateTasks) { 17 | return updtr.cmds.install({ 18 | registry: updtr.config.registry, 19 | modules: failedUpdateTasks.map(rollbackTo), 20 | }); 21 | } 22 | 23 | async function update(sequence, updateTasks) { 24 | const updtr = sequence.updtr; 25 | let success; 26 | let testResult; 27 | 28 | await sequence.exec("updating", renderUpdate(updtr, updateTasks)); 29 | try { 30 | testResult = await sequence.exec("testing", renderTest(updtr)); 31 | success = true; 32 | } catch (err) { 33 | // Remember: instanceof Error might not work in Jest as expected 34 | // https://github.com/facebook/jest/issues/2549 35 | testResult = err; 36 | success = false; 37 | } 38 | 39 | // eslint-disable-next-line require-atomic-updates 40 | sequence.baseEvent.success = success; 41 | 42 | if (success === false && updateTasks.length > 1) { 43 | // If the update was a failure, we roll back every update except the first. 44 | // The first update will be tested with the sequential-update. This way we skip one unnecessary install cycle. 45 | await sequence.exec( 46 | "rollback", 47 | renderRollback(updtr, updateTasks.slice(1)) 48 | ); 49 | } 50 | 51 | sequence.emit("result", { 52 | stdout: testResult.stdout, 53 | }); 54 | 55 | return success; 56 | } 57 | 58 | export default (async function batchUpdate(updtr, updateTasks) { 59 | const sequence = new Sequence("batch-update", updtr, { 60 | updateTasks, 61 | }); 62 | 63 | let success = true; 64 | 65 | if (updateTasks.length > 0) { 66 | sequence.start(); 67 | success = await update(sequence, updateTasks); 68 | sequence.baseEvent = { 69 | success, 70 | }; 71 | sequence.end(); 72 | } 73 | 74 | return success; 75 | }); 76 | -------------------------------------------------------------------------------- /src/tasks/finish.js: -------------------------------------------------------------------------------- 1 | import Sequence from "./util/Sequence"; 2 | import {isUpdateToNonBreaking} from "./util/createUpdateTask"; 3 | 4 | function isIncompleteResult(result) { 5 | return result.success === true && isUpdateToNonBreaking(result) === true; 6 | } 7 | 8 | async function finishIncomplete(sequence, incomplete, allResults) { 9 | const updtr = sequence.updtr; 10 | const modulesToCheck = incomplete.map(result => result.name); 11 | const listCmd = updtr.cmds.list({modules: modulesToCheck}); 12 | let stdout; 13 | 14 | try { 15 | stdout = (await sequence.exec("list-incomplete", listCmd)).stdout; 16 | } catch (err) { 17 | // npm may exit with zero code 1 complaining about invalid installed versions 18 | if (err.code > 1) { 19 | throw err; 20 | } 21 | stdout = err.stdout; 22 | } 23 | const moduleVersions = updtr.parse.list(stdout, listCmd); 24 | 25 | return ( 26 | allResults 27 | .map(result => { 28 | if (isIncompleteResult(result) === false) { 29 | return result; 30 | } 31 | 32 | const version = moduleVersions.find( 33 | module => module.name === result.name 34 | ).version; 35 | 36 | return { 37 | ...result, 38 | updateTo: version, 39 | }; 40 | }) 41 | // Remove results where no actual update did happen. 42 | // These results can happen if the updateTo option was set to non-breaking 43 | // and the module did not have a new version for the rollbackTo version range. 44 | .filter(result => result.rollbackTo !== result.updateTo) 45 | ); 46 | } 47 | 48 | export default (async function finish(updtr, results) { 49 | const incomplete = results.filter(isIncompleteResult); 50 | const sequence = new Sequence("finish", updtr); 51 | let finishedResults = results; 52 | 53 | if (incomplete.length > 0) { 54 | sequence.start(); 55 | sequence.emit("incomplete", {incomplete}); 56 | finishedResults = await finishIncomplete(sequence, incomplete, results); 57 | sequence.end({results: finishedResults}); 58 | } 59 | 60 | return finishedResults; 61 | }); 62 | -------------------------------------------------------------------------------- /src/tasks/init.js: -------------------------------------------------------------------------------- 1 | import {PackageJsonNoAccessError} from "../errors"; 2 | import Sequence from "./util/Sequence"; 3 | import createUpdateTask from "./util/createUpdateTask"; 4 | import filterUpdateTask from "./util/filterUpdateTask"; 5 | 6 | function getUpdateTasksFromStdout(updtr, outdatedCmd, stdout) { 7 | if (stdout.length === 0) { 8 | // When there is not stdout, there is nothing to update 9 | return []; 10 | } 11 | 12 | return updtr.parse 13 | .outdated(stdout, outdatedCmd) 14 | .map(outdated => createUpdateTask(outdated, updtr.config)); 15 | } 16 | 17 | export default (async function init(updtr) { 18 | const baseEvent = {config: updtr.config}; 19 | const outdatedCmd = updtr.cmds.outdated(); 20 | const sequence = new Sequence("init", updtr, baseEvent); 21 | let stdout; 22 | 23 | sequence.start(); 24 | 25 | if ((await updtr.canAccessPackageJson()) === false) { 26 | throw new PackageJsonNoAccessError(updtr.config.cwd); 27 | } 28 | 29 | await sequence.exec( 30 | "install-missing", 31 | updtr.cmds.installMissing({ 32 | registry: updtr.config.registry, 33 | }) 34 | ); 35 | 36 | try { 37 | stdout = (await sequence.exec("collect", outdatedCmd)).stdout; 38 | } catch (err) { 39 | // npm exits with zero code 1 when there are outdated dependencies 40 | // We don't check for the package manager here because yarn might change their 41 | // behavior in the future to be npm-compatible. 42 | if (err.code > 1) { 43 | throw err; 44 | } 45 | 46 | stdout = err.stdout; 47 | } 48 | 49 | const allUpdateTasks = getUpdateTasksFromStdout( 50 | updtr, 51 | outdatedCmd, 52 | stdout.trim() 53 | ); 54 | 55 | const filterResults = allUpdateTasks.map(updateTask => 56 | filterUpdateTask(updateTask, updtr.config)); 57 | 58 | const result = { 59 | updateTasks: allUpdateTasks.filter( 60 | (updateTask, index) => filterResults[index] === null 61 | ), 62 | excluded: allUpdateTasks.reduce( 63 | (excluded, updateTask, index) => { 64 | const reason = filterResults[index]; 65 | 66 | if (reason === null) { 67 | return excluded; 68 | } 69 | 70 | return excluded.concat({ 71 | ...updateTask, 72 | reason, 73 | }); 74 | }, 75 | [] 76 | ), 77 | }; 78 | 79 | sequence.end(result); 80 | 81 | return result; 82 | }); 83 | -------------------------------------------------------------------------------- /src/tasks/sequentialUpdate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-atomic-updates */ 2 | import Sequence from "./util/Sequence"; 3 | import createUpdateResult from "./util/createUpdateResult"; 4 | import updateTo from "./util/updateTo"; 5 | import rollbackTo from "./util/rollbackTo"; 6 | 7 | function renderUpdate(updtr, updateTask) { 8 | return updtr.cmds.install({ 9 | registry: updtr.config.registry, 10 | modules: [updateTo(updateTask)], 11 | }); 12 | } 13 | 14 | function renderTest(updtr) { 15 | return updtr.cmds.test(); 16 | } 17 | 18 | function renderRollback(updtr, failedUpdateTask, nextUpdateTask) { 19 | const modules = [rollbackTo(failedUpdateTask)]; 20 | 21 | if (nextUpdateTask !== undefined) { 22 | modules.push(updateTo(nextUpdateTask)); 23 | } 24 | 25 | return updtr.cmds.install({ 26 | registry: updtr.config.registry, 27 | modules, 28 | }); 29 | } 30 | 31 | async function runUpdateTask(sequence, updateTasks, i, previousUpdateResults) { 32 | const updateResults = await previousUpdateResults; 33 | const previousUpdateResult = updateResults[updateResults.length - 1]; 34 | const updateTask = updateTasks[i]; 35 | 36 | // If the previous update was a failure, we don't need to update now because 37 | // during the rollback, the next update is also installed in parallel 38 | const updateNecessary = previousUpdateResult === undefined ? 39 | true : 40 | previousUpdateResult.success === true; 41 | 42 | sequence.baseEvent = { 43 | updateTasks: { 44 | current: i + 1, 45 | total: updateTasks.length, 46 | }, 47 | ...updateTask, 48 | }; 49 | const updtr = sequence.updtr; 50 | let testResult; 51 | let success; 52 | 53 | if (updateNecessary === true) { 54 | await sequence.exec("updating", renderUpdate(updtr, updateTask)); 55 | } 56 | 57 | try { 58 | testResult = await sequence.exec("testing", renderTest(updtr)); 59 | success = true; 60 | } catch (err) { 61 | // Remember: instanceof Error might not work in Jest as expected 62 | // https://github.com/facebook/jest/issues/2549 63 | testResult = err; 64 | success = false; 65 | } 66 | 67 | sequence.baseEvent.success = success; 68 | 69 | if (success === false) { 70 | const nextUpdateTask = i + 1 < updateTasks.length ? 71 | updateTasks[i + 1] : 72 | undefined; 73 | 74 | await sequence.exec( 75 | "rollback", 76 | renderRollback(updtr, updateTask, nextUpdateTask) 77 | ); 78 | } 79 | 80 | sequence.emit("result", { 81 | stdout: testResult.stdout, 82 | }); 83 | 84 | return updateResults.concat(createUpdateResult(updateTask, success)); 85 | } 86 | 87 | export default (async function sequentialUpdate( 88 | updtr, 89 | updateTasks, 90 | previousUpdateResult 91 | ) { 92 | const sequence = new Sequence("sequential-update", updtr, { 93 | updateTasks, 94 | }); 95 | 96 | if (updateTasks.length === 0) { 97 | return []; 98 | } 99 | 100 | sequence.start(); 101 | 102 | const updateResults = await updateTasks.reduce( 103 | (updateResults, updateTask, i) => 104 | runUpdateTask(sequence, updateTasks, i, updateResults), 105 | previousUpdateResult === undefined ? [] : [previousUpdateResult] 106 | ); 107 | 108 | if (previousUpdateResult !== undefined) { 109 | // The previousUpdateResult is the first element in the updateResults array, so let's remove it. 110 | updateResults.shift(); 111 | } 112 | 113 | sequence.baseEvent = { 114 | updateResults, 115 | }; 116 | 117 | sequence.end(); 118 | 119 | return updateResults; 120 | }); 121 | -------------------------------------------------------------------------------- /src/tasks/updatePackageJson.js: -------------------------------------------------------------------------------- 1 | import detectIndent from "detect-indent"; 2 | import createUpdatedPackageJson from "./util/createUpdatedPackageJson"; 3 | import Sequence from "./util/Sequence"; 4 | 5 | function lastChar(str) { 6 | return str.charAt(str.length - 1); 7 | } 8 | 9 | function stringify(newPackageJson, oldPackageJsonStr) { 10 | const indent = detectIndent(oldPackageJsonStr).indent || " "; 11 | let newPackageJsonStr = JSON.stringify(newPackageJson, null, indent); 12 | const lastCharFromOldPackageJson = lastChar(oldPackageJsonStr); 13 | 14 | // Preserve the new line character at the end if there was one 15 | if (lastCharFromOldPackageJson !== lastChar(newPackageJsonStr)) { 16 | newPackageJsonStr += lastCharFromOldPackageJson; 17 | } 18 | 19 | return newPackageJsonStr; 20 | } 21 | 22 | async function enhanceErrorMessage(fn, enhancedMessage) { 23 | try { 24 | return await fn(); 25 | } catch (err) { 26 | err.message = enhancedMessage + err.message; 27 | throw err; 28 | } 29 | } 30 | 31 | export default (async function updatePackageJson(updtr, updateResults) { 32 | const sequence = new Sequence("update-package-json", updtr); 33 | let oldPackageJsonStr; 34 | 35 | sequence.start(); 36 | 37 | const oldPackageJson = await enhanceErrorMessage( 38 | async () => { 39 | oldPackageJsonStr = await updtr.readFile("package.json"); 40 | 41 | return JSON.parse(oldPackageJsonStr); 42 | }, 43 | "Error while trying to read the package.json: " 44 | ); 45 | 46 | const newPackageJson = createUpdatedPackageJson( 47 | oldPackageJson, 48 | updateResults, 49 | updtr.config 50 | ); 51 | 52 | await enhanceErrorMessage( 53 | () => 54 | updtr.writeFile( 55 | "package.json", 56 | stringify(newPackageJson, oldPackageJsonStr) 57 | ), 58 | "Error while trying to write the package.json: " 59 | ); 60 | 61 | sequence.end({ 62 | packageJson: newPackageJson, 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/tasks/util/Sequence.js: -------------------------------------------------------------------------------- 1 | export default class Sequence { 2 | constructor(name, updtr, baseEvent = {}) { 3 | this.name = name; 4 | this.updtr = updtr; 5 | this.baseEvent = baseEvent; 6 | this.isRunning = false; 7 | } 8 | 9 | start() { 10 | this.isRunning = true; 11 | this.emit("start"); 12 | } 13 | 14 | emit(eventName, event = {}) { 15 | const fullEventName = this.name + "/" + eventName; 16 | 17 | if (this.isRunning === false) { 18 | throw new Error( 19 | `Cannot emit event ${fullEventName}: sequence is not running` 20 | ); 21 | } 22 | this.updtr.emit(fullEventName, { 23 | ...this.baseEvent, 24 | ...event, 25 | }); 26 | } 27 | 28 | exec(step, cmd) { 29 | this.emit(step, {cmd}); 30 | 31 | return this.updtr.exec(cmd); 32 | } 33 | 34 | end(result) { 35 | this.emit("end", result); 36 | this.isRunning = false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tasks/util/createUpdateResult.js: -------------------------------------------------------------------------------- 1 | export default function createUpdateResult(updateTask, success) { 2 | return { 3 | name: updateTask.name, 4 | updateTo: updateTask.updateTo, 5 | rollbackTo: updateTask.rollbackTo, 6 | success, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/tasks/util/createUpdateTask.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_TO_LATEST, 3 | UPDATE_TO_NON_BREAKING, 4 | UPDATE_TO_WANTED, 5 | } from "../../constants/config"; 6 | 7 | function determineUpdateToVersion({current, wanted, latest}, {updateTo}) { 8 | switch (updateTo) { 9 | case UPDATE_TO_LATEST: 10 | return latest; 11 | case UPDATE_TO_WANTED: 12 | return wanted; 13 | case UPDATE_TO_NON_BREAKING: 14 | default: 15 | return "^" + current; 16 | } 17 | } 18 | 19 | export function isUpdateToNonBreaking(updateTask) { 20 | return updateTask.updateTo === "^" + updateTask.rollbackTo; 21 | } 22 | 23 | export default function createUpdateTask(outdated, updtrConfig) { 24 | return { 25 | name: outdated.name, 26 | updateTo: determineUpdateToVersion(outdated, updtrConfig), 27 | rollbackTo: outdated.current, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/tasks/util/createUpdatedPackageJson.js: -------------------------------------------------------------------------------- 1 | import {SAVE_CARET, SAVE_EXACT, SAVE_SMART} from "../../constants/config"; 2 | import updateVersionRange from "./updateVersionRange"; 3 | import {filterSuccessfulUpdates} from "./filterUpdateResults"; 4 | 5 | const dependencyTypes = [ 6 | "dependencies", 7 | "devDependencies", 8 | "optionalDependencies", 9 | ]; 10 | 11 | function newVersionRange(updtrConfig, oldVersionRange, update) { 12 | switch (updtrConfig.save) { 13 | case SAVE_CARET: 14 | return "^" + update.updateTo; 15 | case SAVE_EXACT: 16 | return update.updateTo; 17 | case SAVE_SMART: 18 | } 19 | 20 | return updateVersionRange(oldVersionRange, update.updateTo); 21 | } 22 | 23 | export default function createUpdatedPackageJson( 24 | oldPackageJson, 25 | updateResults, 26 | updtrConfig 27 | ) { 28 | const newPackageJson = {...oldPackageJson}; 29 | const successfulUpdates = filterSuccessfulUpdates(updateResults); 30 | let dependenciesToSave = successfulUpdates; 31 | 32 | dependencyTypes 33 | .filter(type => oldPackageJson[type] !== undefined) 34 | .forEach(type => { 35 | const dependencies = oldPackageJson[type]; 36 | const newDependencies = {}; 37 | 38 | Object.keys(dependencies).forEach(moduleName => { 39 | const update = successfulUpdates.find( 40 | ({name}) => name === moduleName 41 | ); 42 | 43 | const oldVersionRange = dependencies[moduleName]; 44 | 45 | newDependencies[moduleName] = update === undefined ? 46 | oldVersionRange : 47 | newVersionRange(updtrConfig, oldVersionRange, update); 48 | 49 | dependenciesToSave = dependenciesToSave.filter( 50 | ({name}) => name !== moduleName 51 | ); 52 | }); 53 | 54 | newPackageJson[type] = newDependencies; 55 | }); 56 | 57 | if (dependenciesToSave.length > 0) { 58 | const dependencies = newPackageJson.dependencies || {}; 59 | 60 | dependenciesToSave.forEach(update => { 61 | dependencies[update.name] = update.updateTo; 62 | }); 63 | 64 | newPackageJson.dependencies = dependencies; 65 | } 66 | 67 | return newPackageJson; 68 | } 69 | -------------------------------------------------------------------------------- /src/tasks/util/filterUpdateResults.js: -------------------------------------------------------------------------------- 1 | export function filterSuccessfulUpdates(results) { 2 | return results.filter(result => result.success === true); 3 | } 4 | 5 | export function filterFailedUpdates(results) { 6 | return results.filter(result => result.success === false); 7 | } 8 | -------------------------------------------------------------------------------- /src/tasks/util/filterUpdateTask.js: -------------------------------------------------------------------------------- 1 | import semver from "semver"; 2 | import { 3 | GIT, 4 | UNSTABLE, 5 | NOT_WANTED, 6 | EXCLUDED, 7 | EXOTIC, 8 | } from "../../constants/filterReasons"; 9 | import {isUpdateToNonBreaking} from "./createUpdateTask"; 10 | 11 | const prePattern = /^pre/; 12 | 13 | const reasonTests = [ 14 | { 15 | name: EXCLUDED, 16 | test: (updateTask, {exclude}) => 17 | exclude.some(name => updateTask.name === name) === true, 18 | }, 19 | { 20 | name: GIT, 21 | test: updateTask => updateTask.updateTo === "git", 22 | }, 23 | { 24 | name: EXOTIC, 25 | test: updateTask => updateTask.updateTo === "exotic", 26 | }, 27 | { 28 | name: NOT_WANTED, 29 | test: updateTask => 30 | isUpdateToNonBreaking(updateTask) === false && 31 | semver.lte(updateTask.updateTo, updateTask.rollbackTo) === true, 32 | }, 33 | { 34 | name: UNSTABLE, 35 | test(updateTask) { 36 | if (isUpdateToNonBreaking(updateTask) === true) { 37 | return null; 38 | } 39 | 40 | const diff = semver.diff( 41 | updateTask.rollbackTo, 42 | updateTask.updateTo 43 | ); 44 | 45 | const unstableTest = diff !== null && 46 | prePattern.test(diff) === true && 47 | diff !== "prerelease"; 48 | 49 | return unstableTest === true; 50 | }, 51 | }, 52 | ]; 53 | 54 | const reasons = reasonTests.map(test => test.name); 55 | 56 | export default function filterUpdateTask(updateTask, updtrConfig) { 57 | const reasonIndex = reasonTests.findIndex( 58 | reasonTest => reasonTest.test(updateTask, updtrConfig) === true 59 | ); 60 | 61 | return reasonIndex === -1 ? null : reasons[reasonIndex]; 62 | } 63 | -------------------------------------------------------------------------------- /src/tasks/util/rollbackTo.js: -------------------------------------------------------------------------------- 1 | export default function rollbackTo(updateTask) { 2 | return { 3 | name: updateTask.name, 4 | version: updateTask.rollbackTo, 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/tasks/util/splitUpdateTasks.js: -------------------------------------------------------------------------------- 1 | import semver from "semver"; 2 | import {isUpdateToNonBreaking} from "./createUpdateTask"; 3 | 4 | function isBreaking(updateTask) { 5 | return isUpdateToNonBreaking(updateTask) === false && 6 | semver.satisfies(updateTask.updateTo, "^" + updateTask.rollbackTo) === 7 | false; 8 | } 9 | 10 | export default function splitUpdateTask(updateTasks) { 11 | const breaking = []; 12 | const nonBreaking = []; 13 | 14 | for (const updateTask of updateTasks) { 15 | (isBreaking(updateTask) === true ? breaking : nonBreaking).push( 16 | updateTask 17 | ); 18 | } 19 | 20 | return { 21 | breaking, 22 | nonBreaking, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/tasks/util/updateTo.js: -------------------------------------------------------------------------------- 1 | export default function updateTo(updateTask) { 2 | return { 3 | name: updateTask.name, 4 | version: updateTask.updateTo, 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/tasks/util/updateVersionRange.js: -------------------------------------------------------------------------------- 1 | import semver from "semver"; 2 | 3 | // Matches a semver version range that can be transformed to the new version in a safe manner 4 | // eslint-disable-next-line optimize-regex/optimize-regex 5 | const expectedSemverPattern = /^(\^|~|>=|)(\d+)\.(\d+|x|\*)\.(\d+|x|\*)(-[a-z][\d\-.a-z]+|)$/i; 6 | const numberPattern = /^\d+$/; 7 | 8 | function parse(semverString) { 9 | const match = semverString.match(expectedSemverPattern); 10 | 11 | return match === null ? 12 | null : 13 | { 14 | operator: match[1], 15 | major: match[2], 16 | minor: match[3], 17 | patch: match[4], 18 | release: match[5], 19 | }; 20 | } 21 | 22 | function isPinned({operator, major, minor, patch}) { 23 | return operator === "" && 24 | [major, minor, patch].every( 25 | version => numberPattern.test(version) === true 26 | ); 27 | } 28 | 29 | function tryVersionRangeUpdate(parsedOldRange, parsedNewVersion) { 30 | const {minor, patch, operator} = parsedOldRange; 31 | let newMinor = parsedNewVersion.minor; 32 | let newPatch = parsedNewVersion.patch; 33 | 34 | if (numberPattern.test(minor) === false) { 35 | newMinor = minor; 36 | newPatch = numberPattern.test(patch) === true ? minor : patch; 37 | } else if (numberPattern.test(patch) === false) { 38 | newPatch = patch; 39 | } 40 | 41 | return operator + 42 | parsedNewVersion.major + 43 | "." + 44 | newMinor + 45 | "." + 46 | newPatch + 47 | parsedNewVersion.release; 48 | } 49 | 50 | function isExpectedNewVersion(parsedNewVersion) { 51 | return parsedNewVersion !== null && parsedNewVersion.operator === ""; 52 | } 53 | 54 | function fallbackRange(newVersion) { 55 | return "^" + newVersion; 56 | } 57 | 58 | /** 59 | * Tries to apply the newVersion while maintaining the range (see https://github.com/peerigon/updtr/issues/47) 60 | * This is kind of risky because there are tons of semver possibilities. That's why this 61 | * function is very conservative in accepting semver ranges. If the range is not easily updatable, 62 | * we opt-out to npm's default caret operator. 63 | * 64 | * @param {string} oldRange 65 | * @param {string} newVersion 66 | * @returns {string} 67 | */ 68 | export default function updateVersionRange(oldRange, newVersion) { 69 | const parsedOldRange = parse(oldRange.trim()); 70 | 71 | if (parsedOldRange !== null) { 72 | if (isPinned(parsedOldRange) === true) { 73 | // The old version was pinned, so the new should also be pinned 74 | return newVersion; 75 | } 76 | 77 | const parsedNewVersion = parse(newVersion); 78 | 79 | if (isExpectedNewVersion(parsedNewVersion) === false) { 80 | return newVersion; 81 | } 82 | 83 | const newVersionRange = tryVersionRangeUpdate( 84 | parsedOldRange, 85 | parsedNewVersion 86 | ); 87 | 88 | // All this is kind of error prone so let's do a sanity check if everything's ok 89 | if (semver.satisfies(newVersion, newVersionRange) === true) { 90 | return newVersionRange; 91 | } 92 | } 93 | 94 | return fallbackRange(newVersion); 95 | } 96 | -------------------------------------------------------------------------------- /src/util/fs.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import pify from "pify"; 3 | 4 | export default pify(fs); 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "peerigon/tests", 4 | "plugin:jest/recommended" 5 | ], 6 | "plugins": [ 7 | "jest" 8 | ], 9 | "env": { 10 | "jest/globals": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/__snapshots__/Updtr.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`new Updtr() .config should match the default shape 1`] = ` 4 | Object { 5 | "cwd": "/updtr/test/cwd", 6 | "exclude": Array [], 7 | "interactive": undefined, 8 | "registry": undefined, 9 | "save": "smart", 10 | "test": undefined, 11 | "updateTo": "latest", 12 | "use": "npm", 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`index .errors should be an object with expected shape 1`] = ` 4 | Object { 5 | "OptionValueNotSupportedError": [Function], 6 | "PackageJsonNoAccessError": [Function], 7 | "RequiredOptionMissingError": [Function], 8 | "YarnWithCustomRegistryError": [Function], 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /test/__snapshots__/run.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`run() should emit a start event of expected shape: start event 1`] = ` 4 | Array [ 5 | "start", 6 | Object { 7 | "config": Object { 8 | "cwd": "/updtr/test/cwd", 9 | "exclude": Array [], 10 | "interactive": undefined, 11 | "registry": undefined, 12 | "save": "smart", 13 | "test": undefined, 14 | "updateTo": "latest", 15 | "use": "npm", 16 | }, 17 | }, 18 | ] 19 | `; 20 | 21 | exports[`run() when there are just breaking updates should emit an end event of expected shape and return the results: breaking updates > end event 1`] = ` 22 | Array [ 23 | "end", 24 | Object { 25 | "config": Object { 26 | "cwd": "/updtr/test/cwd", 27 | "exclude": Array [], 28 | "interactive": undefined, 29 | "registry": undefined, 30 | "save": "smart", 31 | "test": undefined, 32 | "updateTo": "latest", 33 | "use": "npm", 34 | }, 35 | "results": Array [ 36 | Object { 37 | "name": "updtr-test-module-1", 38 | "rollbackTo": "1.0.0", 39 | "success": true, 40 | "updateTo": "2.0.0", 41 | }, 42 | Object { 43 | "name": "updtr-test-module-2", 44 | "rollbackTo": "1.0.0", 45 | "success": false, 46 | "updateTo": "2.0.0", 47 | }, 48 | ], 49 | }, 50 | ] 51 | `; 52 | 53 | exports[`run() when there are no outdated dependencies should emit an end event of expected shape and return the results: no-outdated > end event 1`] = ` 54 | Array [ 55 | "end", 56 | Object { 57 | "config": Object { 58 | "cwd": "/updtr/test/cwd", 59 | "exclude": Array [], 60 | "interactive": undefined, 61 | "registry": undefined, 62 | "save": "smart", 63 | "test": undefined, 64 | "updateTo": "latest", 65 | "use": "npm", 66 | }, 67 | "results": Array [], 68 | }, 69 | ] 70 | `; 71 | 72 | exports[`run() when there are two non-breaking updates and the update is not ok should emit an end event of expected shape and return the results: two non-breaking and update not ok > end event 1`] = ` 73 | Array [ 74 | "end", 75 | Object { 76 | "config": Object { 77 | "cwd": "/updtr/test/cwd", 78 | "exclude": Array [], 79 | "interactive": undefined, 80 | "registry": undefined, 81 | "save": "smart", 82 | "test": undefined, 83 | "updateTo": "latest", 84 | "use": "npm", 85 | }, 86 | "results": Array [ 87 | Object { 88 | "name": "updtr-test-module-1", 89 | "rollbackTo": "2.0.0", 90 | "success": false, 91 | "updateTo": "2.1.1", 92 | }, 93 | Object { 94 | "name": "updtr-test-module-2", 95 | "rollbackTo": "2.0.0", 96 | "success": true, 97 | "updateTo": "2.1.1", 98 | }, 99 | ], 100 | }, 101 | ] 102 | `; 103 | 104 | exports[`run() when there are two non-breaking updates and the update is ok should emit an end event of expected shape and return the results: two non-breaking and update ok > end event 1`] = ` 105 | Array [ 106 | "end", 107 | Object { 108 | "config": Object { 109 | "cwd": "/updtr/test/cwd", 110 | "exclude": Array [], 111 | "interactive": undefined, 112 | "registry": undefined, 113 | "save": "smart", 114 | "test": undefined, 115 | "updateTo": "latest", 116 | "use": "npm", 117 | }, 118 | "results": Array [ 119 | Object { 120 | "name": "updtr-test-module-1", 121 | "rollbackTo": "2.0.0", 122 | "success": true, 123 | "updateTo": "2.1.1", 124 | }, 125 | Object { 126 | "name": "updtr-test-module-2", 127 | "rollbackTo": "2.0.0", 128 | "success": true, 129 | "updateTo": "2.1.1", 130 | }, 131 | ], 132 | }, 133 | ] 134 | `; 135 | 136 | exports[`run() when there is just one non-breaking update should emit an end event of expected shape and return the results: one-breaking > end event 1`] = ` 137 | Array [ 138 | "end", 139 | Object { 140 | "config": Object { 141 | "cwd": "/updtr/test/cwd", 142 | "exclude": Array [], 143 | "interactive": undefined, 144 | "registry": undefined, 145 | "save": "smart", 146 | "test": undefined, 147 | "updateTo": "latest", 148 | "use": "npm", 149 | }, 150 | "results": Array [ 151 | Object { 152 | "name": "updtr-test-module-1", 153 | "rollbackTo": "2.0.0", 154 | "success": true, 155 | "updateTo": "2.1.1", 156 | }, 157 | ], 158 | }, 159 | ] 160 | `; 161 | 162 | exports[`run() when updateTo is "non-breaking" should finish incomplete results: updateTo non-breaking > end event 1`] = ` 163 | Array [ 164 | Object { 165 | "name": "updtr-test-module-1", 166 | "rollbackTo": "1.0.0", 167 | "success": true, 168 | "updateTo": "2.0.0", 169 | }, 170 | ] 171 | `; 172 | -------------------------------------------------------------------------------- /test/bin/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`bin when all arguments have been specified should run updtr with the expected config 1`] = ` 4 | Object { 5 | "reporter": "dense", 6 | "reporterConfig": Object { 7 | "stream": null, 8 | "testStdout": true, 9 | }, 10 | "updtrConfig": Object { 11 | "cwd": "/test/bin", 12 | "exclude": Array [ 13 | "a", 14 | "b", 15 | "c", 16 | ], 17 | "interactive": false, 18 | "save": "exact", 19 | "test": "jest -b", 20 | "updateTo": "non-breaking", 21 | "use": "yarn", 22 | }, 23 | } 24 | `; 25 | 26 | exports[`bin when no arguments have been specified should run updtr with the default config 1`] = ` 27 | Object { 28 | "reporter": "dense", 29 | "reporterConfig": Object { 30 | "stream": null, 31 | "testStdout": false, 32 | }, 33 | "updtrConfig": Object { 34 | "cwd": "/test/bin", 35 | "exclude": Array [], 36 | "interactive": false, 37 | "save": "smart", 38 | "updateTo": "latest", 39 | "use": "npm", 40 | }, 41 | } 42 | `; 43 | 44 | exports[`bin when there is an error should emit an error event on the updtr instance 1`] = `"Cannot access package.json in /test/bin"`; 45 | -------------------------------------------------------------------------------- /test/bin/index.test.js: -------------------------------------------------------------------------------- 1 | import {execFile} from "child_process"; 2 | import path from "path"; 3 | import {USE_YARN} from "../../src/constants/config"; 4 | 5 | const projectPath = path.resolve(__dirname, "..", "..").replace(/\\/g, "/"); 6 | const pathToRunBin = require.resolve("../helpers/runBinMock"); 7 | 8 | // These tests may take longer on travis 9 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 60 * 1000; 10 | 11 | async function execRunBinMock( 12 | {cwd = __dirname, args = [], runMock = ""} = {} 13 | ) { 14 | const stdout = await new Promise((resolve, reject) => { 15 | execFile( 16 | "node", 17 | [pathToRunBin, ...args], 18 | { 19 | cwd, 20 | env: { 21 | ...process.env, 22 | RUN_MOCK: runMock, 23 | }, 24 | }, 25 | (error, stdout, stderr) => { 26 | if (error || stderr) { 27 | reject(error || stderr); 28 | 29 | return; 30 | } 31 | resolve(stdout); 32 | } 33 | ); 34 | }); 35 | 36 | const stdoutWithoutBasePath = stdout 37 | // Replace double-backslashes with one forward slash 38 | .replace(/\\\\/g, "/") 39 | // Remove project path 40 | .split(projectPath) 41 | .join(""); 42 | 43 | return JSON.parse(stdoutWithoutBasePath); 44 | } 45 | 46 | describe("bin", () => { 47 | describe("when no arguments have been specified", () => { 48 | it("should run updtr with the default config", async () => { 49 | const configs = await execRunBinMock(); 50 | 51 | expect(configs).toMatchSnapshot(); 52 | }); 53 | }); 54 | describe("when the binary is executed in a directory with a yarn.lock file", () => { 55 | it("should use yarn", async () => { 56 | const cwd = path.resolve(__dirname, "..", "fixtures", "empty"); 57 | const configs = await execRunBinMock({cwd}); 58 | 59 | expect(configs.updtrConfig.use).toBe(USE_YARN); 60 | }); 61 | }); 62 | describe("when all arguments have been specified", () => { 63 | it("should run updtr with the expected config", async () => { 64 | const args = [ 65 | "--reporter", 66 | "dense", 67 | "--use", 68 | "yarn", 69 | "--exclude", 70 | "a", 71 | "b", 72 | "c", 73 | "--test", 74 | "jest -b", 75 | // yarn does not support custom registries 76 | // We don't test for this option here and assume that it'll work if all the other options did also work 77 | // "--registry http://example.com", 78 | "--update-to", 79 | "non-breaking", 80 | "--test-stdout", 81 | "--save", 82 | "exact", 83 | ]; 84 | 85 | const configs = await execRunBinMock({args}); 86 | 87 | expect(configs).toMatchSnapshot(); 88 | }); 89 | }); 90 | describe("when there is an error", () => { 91 | it("should emit an error event on the updtr instance", async () => { 92 | const error = await execRunBinMock({ 93 | runMock: "rejectWithAccessError", 94 | args: ["--reporter", "error"], 95 | }); 96 | 97 | expect(error).toMatchSnapshot(); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/exec/__snapshots__/cmds.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`cmds .npm .install() multiple modules custom registry should match snapshot 1`] = `"npm install --registry \\"http://example.com/registry\\" \\"a@1.0.0\\" \\"b@1.0.0\\""`; 4 | 5 | exports[`cmds .npm .install() multiple modules should match snapshot 1`] = `"npm install \\"a@1.0.0\\" \\"b@1.0.0\\""`; 6 | 7 | exports[`cmds .npm .install() one module custom registry should match snapshot 1`] = `"npm install --registry \\"http://example.com/registry\\" \\"a@1.0.0\\""`; 8 | 9 | exports[`cmds .npm .install() one module should match snapshot 1`] = `"npm install \\"a@1.0.0\\""`; 10 | 11 | exports[`cmds .npm .installMissing() custom registry should match snapshot 1`] = `"npm install --registry \\"http://example.com\\""`; 12 | 13 | exports[`cmds .npm .installMissing() should match snapshot 1`] = `"npm install"`; 14 | 15 | exports[`cmds .npm .list() no arguments should match snapshot 1`] = `"npm ls --json --depth=0"`; 16 | 17 | exports[`cmds .npm .list() with modules should match snapshot 1`] = `"npm ls --json --depth=0 a b c"`; 18 | 19 | exports[`cmds .npm .outdated() should match snapshot 1`] = `"npm outdated --json --depth=0"`; 20 | 21 | exports[`cmds .npm .test() should match snapshot 1`] = `"npm test"`; 22 | 23 | exports[`cmds .yarn .install() multiple modules custom registry should match snapshot 1`] = `"yarn add --registry \\"http://example.com/registry\\" \\"a@1.0.0\\" \\"b@1.0.0\\""`; 24 | 25 | exports[`cmds .yarn .install() multiple modules should match snapshot 1`] = `"yarn add \\"a@1.0.0\\" \\"b@1.0.0\\""`; 26 | 27 | exports[`cmds .yarn .install() one module custom registry should match snapshot 1`] = `"yarn add --registry \\"http://example.com/registry\\" \\"some-module@1.0.0\\""`; 28 | 29 | exports[`cmds .yarn .install() one module should match snapshot 1`] = `"yarn add \\"some-module@1.0.0\\""`; 30 | 31 | exports[`cmds .yarn .installMissing() custom registry should match snapshot 1`] = `"yarn --registry \\"http://example.com\\""`; 32 | 33 | exports[`cmds .yarn .installMissing() should match snapshot 1`] = `"yarn"`; 34 | 35 | exports[`cmds .yarn .list() should match snapshot 1`] = `"yarn list --json --depth=0"`; 36 | 37 | exports[`cmds .yarn .outdated() should match snapshot 1`] = `"yarn outdated --json --flat"`; 38 | 39 | exports[`cmds .yarn .test() should match snapshot 1`] = `"yarn test"`; 40 | -------------------------------------------------------------------------------- /test/exec/__snapshots__/exec.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`exec() should return a promise that resolves to an object of expected shape 1`] = ` 4 | Object { 5 | "stderr": "", 6 | "stdout": "", 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /test/exec/__snapshots__/parse.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`parse .npm .list() no-outdated fixture should return an array with installed versions 1`] = ` 4 | Array [ 5 | Object { 6 | "name": "updtr-test-module-1", 7 | "version": "2.0.0", 8 | }, 9 | Object { 10 | "name": "updtr-test-module-2", 11 | "version": "2.1.1", 12 | }, 13 | ] 14 | `; 15 | 16 | exports[`parse .npm .list() no-outdated-dev fixture should return an array with installed versions 1`] = ` 17 | Array [ 18 | Object { 19 | "name": "updtr-test-module-1", 20 | "version": "2.0.0", 21 | }, 22 | Object { 23 | "name": "updtr-test-module-2", 24 | "version": "2.1.1", 25 | }, 26 | ] 27 | `; 28 | 29 | exports[`parse .npm .list() outdated fixture should return an array with installed versions 1`] = ` 30 | Array [ 31 | Object { 32 | "name": "updtr-test-module-1", 33 | "version": "1.0.0", 34 | }, 35 | Object { 36 | "name": "updtr-test-module-2", 37 | "version": "2.0.0", 38 | }, 39 | ] 40 | `; 41 | 42 | exports[`parse .npm .list() outdated-dev fixture should return an array with installed versions 1`] = ` 43 | Array [ 44 | Object { 45 | "name": "updtr-test-module-1", 46 | "version": "1.0.0", 47 | }, 48 | Object { 49 | "name": "updtr-test-module-2", 50 | "version": "2.0.0", 51 | }, 52 | ] 53 | `; 54 | 55 | exports[`parse .npm .outdated() outdated fixture should return an array with normalized outdated data 1`] = ` 56 | Array [ 57 | Object { 58 | "current": "1.0.0", 59 | "latest": "2.0.0", 60 | "name": "updtr-test-module-1", 61 | "wanted": "1.1.1", 62 | }, 63 | Object { 64 | "current": "2.0.0", 65 | "latest": "2.1.1", 66 | "name": "updtr-test-module-2", 67 | "wanted": "2.1.1", 68 | }, 69 | ] 70 | `; 71 | 72 | exports[`parse .npm .outdated() outdated-dev fixture should return an array with normalized outdated data 1`] = ` 73 | Array [ 74 | Object { 75 | "current": "1.0.0", 76 | "latest": "2.0.0", 77 | "name": "updtr-test-module-1", 78 | "wanted": "1.1.1", 79 | }, 80 | Object { 81 | "current": "2.0.0", 82 | "latest": "2.1.1", 83 | "name": "updtr-test-module-2", 84 | "wanted": "2.1.1", 85 | }, 86 | ] 87 | `; 88 | 89 | exports[`parse .yarn .list() malformed dependency name should throw a helpful error message 1`] = `[Error: Error when trying to parse stdout from command 'undefined': Could not parse dependency name "updtr-test-module-1"]`; 90 | 91 | exports[`parse .yarn .list() no-outdated fixture should return an array with installed versions 1`] = ` 92 | Array [ 93 | Object { 94 | "name": "updtr-test-module-1", 95 | "version": "2.0.0", 96 | }, 97 | Object { 98 | "name": "updtr-test-module-2", 99 | "version": "2.1.1", 100 | }, 101 | Object { 102 | "name": "batch-replace", 103 | "version": "1.1.3", 104 | }, 105 | ] 106 | `; 107 | 108 | exports[`parse .yarn .list() no-outdated-dev fixture should return an array with installed versions 1`] = ` 109 | Array [ 110 | Object { 111 | "name": "updtr-test-module-1", 112 | "version": "2.0.0", 113 | }, 114 | Object { 115 | "name": "updtr-test-module-2", 116 | "version": "2.1.1", 117 | }, 118 | Object { 119 | "name": "batch-replace", 120 | "version": "1.1.3", 121 | }, 122 | ] 123 | `; 124 | 125 | exports[`parse .yarn .list() outdated fixture should return an array with installed versions 1`] = ` 126 | Array [ 127 | Object { 128 | "name": "updtr-test-module-1", 129 | "version": "1.1.1", 130 | }, 131 | Object { 132 | "name": "updtr-test-module-2", 133 | "version": "2.1.1", 134 | }, 135 | Object { 136 | "name": "batch-replace", 137 | "version": "1.1.3", 138 | }, 139 | ] 140 | `; 141 | 142 | exports[`parse .yarn .list() outdated-dev fixture should return an array with installed versions 1`] = ` 143 | Array [ 144 | Object { 145 | "name": "updtr-test-module-1", 146 | "version": "1.1.1", 147 | }, 148 | Object { 149 | "name": "updtr-test-module-2", 150 | "version": "2.1.1", 151 | }, 152 | Object { 153 | "name": "batch-replace", 154 | "version": "1.1.3", 155 | }, 156 | ] 157 | `; 158 | 159 | exports[`parse .yarn .outdated() outdated fixture should return an array with normalized outdated data 1`] = ` 160 | Array [ 161 | Object { 162 | "current": "1.0.0", 163 | "latest": "2.0.0", 164 | "name": "updtr-test-module-1", 165 | "wanted": "1.0.0", 166 | }, 167 | Object { 168 | "current": "2.0.0", 169 | "latest": "2.1.1", 170 | "name": "updtr-test-module-2", 171 | "wanted": "2.0.0", 172 | }, 173 | ] 174 | `; 175 | 176 | exports[`parse .yarn .outdated() outdated-dev fixture should return an array with normalized outdated data 1`] = ` 177 | Array [ 178 | Object { 179 | "current": "1.0.0", 180 | "latest": "2.0.0", 181 | "name": "updtr-test-module-1", 182 | "wanted": "1.0.0", 183 | }, 184 | Object { 185 | "current": "2.0.0", 186 | "latest": "2.1.1", 187 | "name": "updtr-test-module-2", 188 | "wanted": "2.0.0", 189 | }, 190 | ] 191 | `; 192 | -------------------------------------------------------------------------------- /test/exec/exec.test.js: -------------------------------------------------------------------------------- 1 | import exec from "../../src/exec/exec"; 2 | 3 | const logMemoryCode = (() => { 4 | const DEFAULT_STDOUT_BUFFER_SIZE = 200 * 1024; 5 | const size = DEFAULT_STDOUT_BUFFER_SIZE + 1; 6 | const memory = Buffer.allocUnsafe(size); 7 | 8 | console.log(memory.toString("utf8")); 9 | }) 10 | .toString() 11 | .replace(/"/g, '\\"') 12 | .replace(/\r?\n/g, ""); 13 | 14 | const logOkCmd = 'node -e "console.log(\\"ok\\")"'; 15 | const noopCmd = 'node -e ""'; 16 | const throwCmd = "node -e \"throw 'fail'\""; 17 | const logMemoryCmd = `node -e "(${logMemoryCode})()"`; 18 | const cwd = __dirname; 19 | 20 | describe("exec()", () => { 21 | it("should return a promise that resolves to an object of expected shape", async () => { 22 | const result = await exec(cwd, noopCmd); 23 | 24 | expect(result).toMatchSnapshot(); 25 | }); 26 | it("should return a promise that resolves to a value that contains the stdout", async () => { 27 | const result = await exec(cwd, logOkCmd); 28 | 29 | expect(result.stdout).toBe("ok\n"); 30 | }); 31 | it("should not fail with 'stdout maxBuffer exceeded' if stdout is pretty big", async () => { 32 | await exec(cwd, logMemoryCmd); 33 | }); 34 | describe("when the command fails", () => { 35 | it("should reject the promise with an error of expected shape", async () => { 36 | let givenErr; 37 | 38 | try { 39 | await exec(cwd, throwCmd); 40 | } catch (err) { 41 | givenErr = err; 42 | } 43 | 44 | expect(givenErr.message).toContain(throwCmd); 45 | expect(givenErr).toHaveProperty("stdout"); 46 | expect(givenErr).toHaveProperty("stderr"); 47 | expect(givenErr.stderr).toMatch(/throw 'fail'/); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/fixtures/execResults.js: -------------------------------------------------------------------------------- 1 | import readFixtures from "../helpers/readFixtures"; 2 | import ExecError from "../helpers/ExecError"; 3 | 4 | // Using exitCode 2 because this way we can re-use this error also for npm outdated 5 | export const execError = new ExecError({ 6 | message: "Oops", 7 | exitCode: 2, 8 | stdout: "Oh noez", 9 | }); 10 | 11 | export const npmNoOutdated = []; 12 | export const npmOutdated = []; 13 | export const yarnOutdated = []; 14 | export const update = []; 15 | export const testPass = []; 16 | export const testFail = []; 17 | export const testFailWithRollback = []; 18 | export const npmList = []; 19 | export const yarnList = []; 20 | export const errorExecInstallMissing = []; 21 | export const errorExecOutdated = []; 22 | export const errorParseOutdated = []; 23 | export const errorExecUpdate = []; 24 | export const errorExecRollback = []; 25 | export const errorExecList = []; 26 | 27 | beforeAll(async () => { 28 | const stdoutLogs = await readFixtures([ 29 | "no-outdated/outdated.npm.log", 30 | "no-outdated/outdated.yarn.log", 31 | "no-outdated/list.npm.log", 32 | "no-outdated/list.yarn.log", 33 | "outdated/outdated.npm.log", 34 | "outdated/outdated.yarn.log", 35 | ]); 36 | 37 | npmNoOutdated.push( 38 | {stdout: ""}, // installMissing 39 | { 40 | stdout: stdoutLogs.get("no-outdated/outdated.npm.log"), 41 | } // outdated 42 | ); 43 | 44 | npmOutdated.push( 45 | {stdout: ""}, // installMissing 46 | // npm exits with exit code 1 when there are outdated dependencies 47 | new ExecError({ 48 | stdout: stdoutLogs.get("outdated/outdated.npm.log"), 49 | exitCode: 1, 50 | }) // outdated 51 | ); 52 | yarnOutdated.push( 53 | {stdout: ""}, // installMissing 54 | { 55 | stdout: stdoutLogs.get("outdated/outdated.yarn.log"), 56 | } // outdated 57 | ); 58 | 59 | update.push( 60 | {stdout: ""} // update 61 | ); 62 | 63 | testPass.push( 64 | {stdout: "Everything ok"} // test 65 | ); 66 | testFail.push( 67 | execError // test 68 | ); 69 | testFailWithRollback.push( 70 | execError, // test 71 | {stdout: ""} // rollback 72 | ); 73 | 74 | // npm exits with exit code 1 when there are outdated dependencies 75 | npmList.push( 76 | new ExecError({ 77 | stdout: stdoutLogs.get("no-outdated/list.npm.log"), 78 | exitCode: 1, 79 | }) 80 | ); 81 | yarnList.push({ 82 | stdout: stdoutLogs.get("no-outdated/list.yarn.log"), 83 | }); 84 | 85 | errorExecInstallMissing.push( 86 | execError // installMissing 87 | ); 88 | errorExecOutdated.push( 89 | {stdout: ""}, // installMissing 90 | execError // outdated 91 | ); 92 | errorParseOutdated.push( 93 | {stdout: ""}, // installMissing 94 | {stdout: "Nonsense"} // outdated 95 | ); 96 | errorExecUpdate.push( 97 | execError // update 98 | ); 99 | errorExecRollback.push( 100 | {stdout: ""}, // update 101 | execError, // test 102 | execError // rollback 103 | ); 104 | errorExecList.push( 105 | execError // list 106 | ); 107 | }); 108 | -------------------------------------------------------------------------------- /test/fixtures/outdateds.js: -------------------------------------------------------------------------------- 1 | // location property missing on purpose because we don't care for now. 2 | // Maybe someday if we add support for linked modules? 3 | // See https://github.com/peerigon/updtr/issues/52 4 | 5 | const name = "module"; 6 | 7 | const outdateds = [ 8 | { 9 | name, 10 | current: "0.0.0", 11 | wanted: "0.0.0", 12 | latest: "0.0.1", 13 | }, 14 | { 15 | name, 16 | current: "0.0.0", 17 | wanted: "0.0.0", 18 | latest: "0.1.0", 19 | }, 20 | { 21 | name, 22 | current: "0.0.0", 23 | wanted: "0.0.0", 24 | latest: "1.0.0", 25 | }, 26 | { 27 | name, 28 | current: "0.0.0", 29 | wanted: "0.0.1", 30 | latest: "0.0.1", 31 | }, 32 | { 33 | name, 34 | current: "0.0.0", 35 | wanted: "0.0.1", 36 | latest: "0.1.1", 37 | }, 38 | { 39 | name, 40 | current: "0.0.0", 41 | wanted: "0.1.1", 42 | latest: "0.1.1", 43 | }, 44 | { 45 | name, 46 | current: "0.0.0", 47 | wanted: "0.1.1", 48 | latest: "1.1.1", 49 | }, 50 | { 51 | name, 52 | current: "0.0.0", 53 | wanted: "1.1.1", 54 | latest: "1.1.1", 55 | }, 56 | 57 | { 58 | name, 59 | current: "1.0.0", 60 | wanted: "1.0.0", 61 | latest: "1.0.1", 62 | }, 63 | { 64 | name, 65 | current: "1.0.0", 66 | wanted: "1.0.0", 67 | latest: "1.1.0", 68 | }, 69 | { 70 | name, 71 | current: "1.0.0", 72 | wanted: "1.0.0", 73 | latest: "2.0.0", 74 | }, 75 | { 76 | name, 77 | current: "1.0.0", 78 | wanted: "1.0.1", 79 | latest: "1.0.1", 80 | }, 81 | { 82 | name, 83 | current: "1.0.0", 84 | wanted: "1.0.1", 85 | latest: "1.1.1", 86 | }, 87 | { 88 | name, 89 | current: "1.0.0", 90 | wanted: "1.1.1", 91 | latest: "1.1.1", 92 | }, 93 | { 94 | name, 95 | current: "1.0.0", 96 | wanted: "1.0.0", 97 | latest: "2.0.0", 98 | }, 99 | { 100 | name, 101 | current: "1.0.0", 102 | wanted: "1.0.1", 103 | latest: "2.0.0", 104 | }, 105 | { 106 | name, 107 | current: "1.0.0", 108 | wanted: "1.1.0", 109 | latest: "2.0.0", 110 | }, 111 | { 112 | name, 113 | current: "1.0.0", 114 | wanted: "2.0.0", 115 | latest: "2.0.0", 116 | }, 117 | { 118 | name, 119 | current: "1.0.0-beta.1", 120 | wanted: "1.0.0-beta.1", 121 | latest: "1.0.0-beta.1", 122 | }, 123 | { 124 | name, 125 | current: "1.0.0-beta.1", 126 | wanted: "1.0.0-beta.1", 127 | latest: "1.0.0-beta.2", 128 | }, 129 | { 130 | name, 131 | current: "1.0.0-beta.1", 132 | wanted: "1.0.0-beta.2", 133 | latest: "1.0.0-beta.2", 134 | }, 135 | { 136 | name, 137 | current: "1.0.0-beta.1", 138 | wanted: "1.0.0-beta.1", 139 | latest: "2.0.0-beta.1", 140 | }, 141 | { 142 | name, 143 | current: "1.0.0-beta.1", 144 | wanted: "1.0.0-beta.2", 145 | latest: "2.0.0-beta.1", 146 | }, 147 | ]; 148 | 149 | export default outdateds; 150 | -------------------------------------------------------------------------------- /test/fixtures/packageJsons.js: -------------------------------------------------------------------------------- 1 | import readFixtures from "../helpers/readFixtures"; 2 | 3 | export let noOutdatedRegular; 4 | export let outdatedRegular; 5 | export let outdatedDev; 6 | 7 | beforeAll(async () => { 8 | const packageJsons = await readFixtures([ 9 | "no-outdated/package.json", 10 | // Currently not used anywhere 11 | // "no-outdated-dev/package.json", 12 | "outdated/package.json", 13 | "outdated-dev/package.json", 14 | ]); 15 | 16 | noOutdatedRegular = packageJsons.get("no-outdated/package.json"); 17 | outdatedRegular = packageJsons.get("outdated/package.json"); 18 | outdatedDev = packageJsons.get("outdated-dev/package.json"); 19 | }); 20 | -------------------------------------------------------------------------------- /test/fixtures/updateResults.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_TO_LATEST, 3 | UPDATE_TO_NON_BREAKING, 4 | } from "../../src/constants/config"; 5 | import parse from "../../src/exec/parse"; 6 | import createUpdateTask from "../../src/tasks/util/createUpdateTask"; 7 | import createUpdateResult from "../../src/tasks/util/createUpdateResult"; 8 | import {readFixture} from "../helpers/readFixtures"; 9 | import FakeUpdtr from "../helpers/FakeUpdtr"; 10 | 11 | export let module1ToLatestSuccess; 12 | export let module1ToNonBreakingSuccess; 13 | export let module2ToLatestSuccess; 14 | export let module2ToNonBreakingSuccess; 15 | export let module1ToLatestFail; 16 | export let module1ToNonBreakingFail; 17 | export let module2ToLatestFail; 18 | export let module2ToNonBreakingFail; 19 | 20 | function outdatedToUpdateResult(outdated, updateTo, success) { 21 | return createUpdateResult( 22 | createUpdateTask(outdated, { 23 | ...FakeUpdtr.baseConfig, 24 | updateTo, 25 | }), 26 | success 27 | ); 28 | } 29 | 30 | beforeAll(async () => { 31 | const stdoutLog = await readFixture("outdated/outdated.npm.log"); 32 | 33 | const [outdatedTestModule1, outdatedTestModule2] = parse.npm.outdated( 34 | stdoutLog 35 | ); 36 | 37 | module1ToLatestSuccess = outdatedToUpdateResult( 38 | outdatedTestModule1, 39 | UPDATE_TO_LATEST, 40 | true 41 | ); 42 | module1ToNonBreakingSuccess = outdatedToUpdateResult( 43 | outdatedTestModule1, 44 | UPDATE_TO_NON_BREAKING, 45 | true 46 | ); 47 | module1ToLatestFail = outdatedToUpdateResult( 48 | outdatedTestModule1, 49 | UPDATE_TO_LATEST, 50 | false 51 | ); 52 | module1ToNonBreakingFail = outdatedToUpdateResult( 53 | outdatedTestModule1, 54 | UPDATE_TO_NON_BREAKING, 55 | false 56 | ); 57 | module2ToLatestSuccess = outdatedToUpdateResult( 58 | outdatedTestModule2, 59 | UPDATE_TO_LATEST, 60 | true 61 | ); 62 | module2ToNonBreakingSuccess = outdatedToUpdateResult( 63 | outdatedTestModule2, 64 | UPDATE_TO_NON_BREAKING, 65 | true 66 | ); 67 | module2ToLatestFail = outdatedToUpdateResult( 68 | outdatedTestModule2, 69 | UPDATE_TO_LATEST, 70 | false 71 | ); 72 | module2ToNonBreakingFail = outdatedToUpdateResult( 73 | outdatedTestModule2, 74 | UPDATE_TO_NON_BREAKING, 75 | false 76 | ); 77 | }); 78 | -------------------------------------------------------------------------------- /test/fixtures/versionRanges.js: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/npm/node-semver 2 | const versionRanges = [ 3 | "~0.6.1-1", 4 | "1.0.0 - 2.0.0", 5 | "1.0.0-2.0.0", 6 | "1.0.0", 7 | " 1.0.0 ", 8 | "1.0.0-beta.1", 9 | ">=1.0.0-beta.1", 10 | "1.0.0-BETA.1", 11 | "1.0.0-alpha-1", 12 | "1.0.0 alpha", // Nonsense value 13 | "~1.0.0", 14 | " ~1.0.0 ", 15 | "~1.0.x", 16 | ">=*", 17 | "", 18 | "*", 19 | ">=1.0.0", 20 | " >=1.0.0 ", 21 | ">1.0.0", 22 | " >1.0.0 ", 23 | "<=2.0.0", 24 | "<2.0.0", 25 | ">= 1.0.0", 26 | "> 1.0.0", 27 | "<= 2.0.0", 28 | "< 2.0.0", 29 | "<\t2.0.0", 30 | ">=0.1.97", 31 | "0.1.20 || 1.2.4", 32 | ">=0.2.3 || <0.0.1", 33 | "||", 34 | "2.x.x", 35 | "1.2.x", 36 | "1.2.x || 2.x", 37 | "x", 38 | "2.*.*", 39 | "1.2.*", 40 | "1.2.* || 2.*", 41 | "2", 42 | "2.3", 43 | "~2.4", 44 | "~>3.2.1", 45 | "~1", 46 | "~>1", 47 | "~> 1", 48 | "~1.0", 49 | "~ 1.0", 50 | ">=1", 51 | ">= 1", 52 | "<1.2", 53 | "< 1.2", 54 | "1", 55 | "~v0.5.4-pre", 56 | "~v0.5.4-pre", 57 | "=0.7.x", 58 | ">=0.7.x", 59 | ">=0.7.X", 60 | "=0.7.x", 61 | ">=0.7.x", 62 | "<=0.7.x", 63 | ">0.2.3 >0.2.4 <=0.2.5", 64 | ">=0.2.3 <=0.2.4", 65 | "1.0.0 - 2.0.0", 66 | "^1", 67 | "^3.0.0", 68 | "^1.0.0 || ~2.0.1", 69 | "^0.1.0 || ~3.0.1 || 5.0.0", 70 | "^0.1.0 || ~3.0.1 || >4 <=5.0.0", 71 | // Nonsense values 72 | "*.1.2", 73 | "x.1.2", 74 | "1.x.2", 75 | "1.*.2", 76 | ]; 77 | 78 | export default versionRanges; 79 | -------------------------------------------------------------------------------- /test/helpers/ExecError.js: -------------------------------------------------------------------------------- 1 | export default class ExecError extends Error { 2 | constructor({message, stdout = "", stderr = "", exitCode}) { 3 | super(message); 4 | this.stdout = stdout; 5 | this.stderr = stderr; 6 | this.code = exitCode; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/helpers/FakeUpdtr.js: -------------------------------------------------------------------------------- 1 | import {stub} from "sinon"; 2 | import Updtr from "../../src/Updtr"; 3 | 4 | export default class FakeUpdtr extends Updtr { 5 | constructor(updtrConfig = {}) { 6 | super({ 7 | ...FakeUpdtr.baseConfig, 8 | ...updtrConfig, 9 | }); 10 | this.canAccessPackageJson = stub(); 11 | this.readFile = stub(); 12 | this.writeFile = stub(); 13 | this.emit = stub(); 14 | this._exec = stub(); 15 | this.exec.args = this._exec.args; 16 | 17 | this.canAccessPackageJson.resolves(true); 18 | } 19 | 20 | set execResults(execResults) { 21 | execResults.forEach((execResult, index) => { 22 | const call = this._exec.onCall(index); 23 | 24 | if (execResult instanceof Error) { 25 | call.rejects(execResult); 26 | } else { 27 | call.resolves(execResult); 28 | } 29 | }); 30 | } 31 | 32 | async exec(...args) { 33 | const result = await this._exec(...args); 34 | 35 | if (result === undefined) { 36 | throw new Error("Not enough execResults for FakeUpdtr"); 37 | } 38 | 39 | return result; 40 | } 41 | } 42 | 43 | FakeUpdtr.baseConfig = { 44 | cwd: "/updtr/test/cwd", 45 | }; 46 | -------------------------------------------------------------------------------- /test/helpers/binMocks.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous */ 2 | 3 | // We're using require() instead of import here because we need to replace some exports with mocks 4 | const runExports = require("../../src/run"); 5 | const reportersExports = require("../../src/reporters"); 6 | const errorsExports = require("../../src/errors"); 7 | 8 | const runMock = process.env.RUN_MOCK || "logConfigAndExit"; 9 | 10 | const runMocks = { 11 | logConfigAndExit(updtr) { 12 | console.log( 13 | JSON.stringify({ 14 | updtrConfig: updtr.config, 15 | reporter, 16 | reporterConfig, 17 | }) 18 | ); 19 | process.exit(0); // eslint-disable-line no-process-exit 20 | }, 21 | rejectWithAccessError() { 22 | return Promise.reject( 23 | new errorsExports.PackageJsonNoAccessError(process.cwd()) 24 | ); 25 | }, 26 | }; 27 | 28 | let reporterConfig; 29 | let reporter; 30 | 31 | // Replace run default exports with mock 32 | runExports.default = runMocks[runMock]; 33 | reportersExports.default.dense = function (updtr, config) { 34 | // Removing the stream property because we can't JSON stringify that 35 | reporterConfig = {...config, stream: null}; 36 | reporter = "dense"; 37 | }; 38 | reportersExports.default.error = function (updtr) { 39 | updtr.on("error", err => { 40 | console.log(JSON.stringify(err.message)); 41 | process.exit(0); // eslint-disable-line no-process-exit 42 | }); 43 | }; 44 | 45 | require("../../src/bin"); // eslint-disable-line import/no-unassigned-import 46 | -------------------------------------------------------------------------------- /test/helpers/cleanupFixtures.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import pify from "pify"; 3 | import rimraf from "rimraf"; 4 | import {fixtureSetups} from "./setupFixtures"; 5 | 6 | const pathToFixtures = path.resolve(__dirname, "..", "fixtures"); 7 | const fixtures = Object.keys(fixtureSetups); 8 | const promiseRimraf = pify(rimraf); 9 | 10 | function remove(fixture) { 11 | return promiseRimraf( 12 | path.join(pathToFixtures, fixture), 13 | ); 14 | } 15 | 16 | export default function cleanupFixtures() { 17 | return Promise.all(fixtures.map(remove)); 18 | } 19 | 20 | if (!module.parent) { 21 | process.on("unhandledRejection", error => { 22 | console.error(error.stack); 23 | process.exit(1); // eslint-disable-line no-process-exit 24 | }); 25 | cleanupFixtures(); 26 | } 27 | -------------------------------------------------------------------------------- /test/helpers/pickEventNames.js: -------------------------------------------------------------------------------- 1 | export default function pickEventNames(eventNames, emitArgs) { 2 | return emitArgs 3 | .filter(([eventName]) => eventNames.indexOf(eventName) > -1) 4 | .map(([eventName]) => eventName); 5 | } 6 | -------------------------------------------------------------------------------- /test/helpers/pickEvents.js: -------------------------------------------------------------------------------- 1 | export default function pickEvents(eventName, emitArgs) { 2 | return emitArgs.filter(([name]) => name === eventName).map(args => args[1]); 3 | } 4 | -------------------------------------------------------------------------------- /test/helpers/readFixtures.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "../../src/util/fs"; 3 | 4 | const pathToFixtures = path.resolve(__dirname, "..", "fixtures"); 5 | const cache = new Map(); 6 | 7 | export async function readFixture(fixture) { 8 | const filename = path.join(pathToFixtures, fixture); 9 | const cached = cache.get(filename); 10 | 11 | if (cached !== undefined) { 12 | return cached; 13 | } 14 | 15 | const contents = await fs.readFile(filename, "utf8"); 16 | 17 | if (cache.has(filename) === false) { 18 | cache.set(filename, contents); 19 | } 20 | 21 | return contents; 22 | } 23 | 24 | export default (async function readFixtures(fixtures) { 25 | const fixturesMap = new Map(); 26 | const contents = await Promise.all(fixtures.map(readFixture)); 27 | 28 | contents.forEach((content, index) => { 29 | fixturesMap.set(fixtures[index], content); 30 | }); 31 | 32 | return fixturesMap; 33 | }); 34 | -------------------------------------------------------------------------------- /test/helpers/replayTerminalSnapshots.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import ansiEscapes from "ansi-escapes"; 3 | import wrapAnsi from "wrap-ansi"; 4 | 5 | function timeout(ms) { 6 | return new Promise(resolve => setTimeout(resolve, ms)); 7 | } 8 | 9 | async function replayTerminalSnapshots(pathToSnapshots) { 10 | const snapshots = require(path.join(process.cwd(), pathToSnapshots)); // eslint-disable-line import/no-dynamic-require 11 | 12 | for (const snapshotTitle of Object.keys(snapshots)) { 13 | const stdout = snapshots[snapshotTitle] 14 | // Removing double quotes 15 | .replace(/^.*?"|".*?$/gm, ""); 16 | 17 | // Restoring ansi-escape-sequences 18 | // .replace(/(\[\d)/g, "\u001B$1"); 19 | const frames = stdout.split(ansiEscapes.eraseDown); 20 | const line = new Array(snapshotTitle.length + 1).join("-"); 21 | 22 | process.stdout.write(ansiEscapes.clearScreen); 23 | 24 | console.log(line); 25 | console.log(snapshotTitle); 26 | console.log(line); 27 | 28 | await timeout(3000); // eslint-disable-line no-await-in-loop 29 | 30 | for (let i = 0; i < frames.length; i++) { 31 | if (i !== 0) { 32 | process.stdout.write(ansiEscapes.eraseDown); 33 | } 34 | process.stdout.write(wrapAnsi(frames[i], 80, {hard: true})); 35 | await timeout(1000); // eslint-disable-line no-await-in-loop 36 | } 37 | await timeout(3000); // eslint-disable-line no-await-in-loop 38 | } 39 | } 40 | 41 | if (!module.parent) { 42 | process.on("unhandledRejection", error => { 43 | console.error(error.stack); 44 | process.exit(1); // eslint-disable-line no-process-exit 45 | }); 46 | replayTerminalSnapshots(process.argv[2]); 47 | } 48 | 49 | export default replayTerminalSnapshots; 50 | -------------------------------------------------------------------------------- /test/helpers/runBinMock.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous */ 2 | 3 | const path = require("path"); 4 | 5 | // babel-node would use the current process.cwd() for all the config resolving logic 6 | // That's why we use @babel/register where we can set all the options 7 | require("@babel/register")({ 8 | cwd: path.resolve(__dirname, "..", ".."), 9 | }); 10 | 11 | require("./binMocks"); // eslint-disable-line import/no-unassigned-import 12 | -------------------------------------------------------------------------------- /test/helpers/temp.js: -------------------------------------------------------------------------------- 1 | import pify from "pify"; 2 | import temp from "temp"; 3 | 4 | temp.track(); 5 | 6 | process.on("exit", () => { 7 | temp.cleanupSync(); 8 | }); 9 | 10 | export default pify(temp); 11 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import Updtr from "../src/Updtr"; 2 | import FakeUpdtr from "./helpers/FakeUpdtr"; 3 | const index = require("../src"); 4 | 5 | describe("index", () => { 6 | describe(".create", () => { 7 | it("should be a function", () => { 8 | expect(typeof index.create).toBe("function"); 9 | }); 10 | it("should return an instance of Updtr", () => { 11 | expect(index.create(FakeUpdtr.baseConfig)).toBeInstanceOf(Updtr); 12 | }); 13 | }); 14 | describe(".run", () => { 15 | it("should be a function", () => { 16 | expect(typeof index.run).toBe("function"); 17 | }); 18 | }); 19 | describe(".errors", () => { 20 | it("should be an object with expected shape", () => { 21 | expect(index.errors).toMatchSnapshot(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/noOutdated.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`integration test: when there are no outdated dependencies npm should not change the package.json, change the installed dependencies and resolve with an empty array as results: npm > installed versions 1`] = ` 4 | Object { 5 | "updtr-test-module-1": "2.0.0", 6 | "updtr-test-module-2": "2.1.1", 7 | } 8 | `; 9 | 10 | exports[`integration test: when there are no outdated dependencies yarn should not change the package.json and yarn.lock and resolve with an empty array as results: npm > installed versions 1`] = ` 11 | Object { 12 | "updtr-test-module-1": "2.0.0", 13 | "updtr-test-module-2": "2.1.1", 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/outdated.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`integration test: when there are outdated dependencies advanced configuration npm should install all updates, update the package.json and return the results: advanced configuration > npm > installed versions 1`] = ` 4 | Object { 5 | "updtr-test-module-1": "1.1.1", 6 | "updtr-test-module-2": "2.0.0", 7 | } 8 | `; 9 | 10 | exports[`integration test: when there are outdated dependencies advanced configuration npm should install all updates, update the package.json and return the results: advanced configuration > npm > package.json dependencies 1`] = ` 11 | Object { 12 | "updtr-test-module-1": "1.1.1", 13 | "updtr-test-module-2": "^2.0.0", 14 | } 15 | `; 16 | 17 | exports[`integration test: when there are outdated dependencies advanced configuration npm should install all updates, update the package.json and return the results: advanced configuration > npm > results 1`] = ` 18 | Array [ 19 | Object { 20 | "name": "updtr-test-module-1", 21 | "rollbackTo": "1.0.0", 22 | "success": true, 23 | "updateTo": "1.1.1", 24 | }, 25 | ] 26 | `; 27 | 28 | exports[`integration test: when there are outdated dependencies advanced configuration yarn should install all updates, update the package.json and return the results: advanced configuration > yarn > installed versions 1`] = ` 29 | Object { 30 | "updtr-test-module-1": "1.1.1", 31 | "updtr-test-module-2": "2.0.0", 32 | } 33 | `; 34 | 35 | exports[`integration test: when there are outdated dependencies advanced configuration yarn should install all updates, update the package.json and return the results: advanced configuration > yarn > package.json dependencies 1`] = ` 36 | Object { 37 | "updtr-test-module-1": "^1.1.1", 38 | "updtr-test-module-2": "2.0.0", 39 | } 40 | `; 41 | 42 | exports[`integration test: when there are outdated dependencies advanced configuration yarn should install all updates, update the package.json and return the results: advanced configuration > yarn > results 1`] = ` 43 | Array [ 44 | Object { 45 | "name": "updtr-test-module-1", 46 | "rollbackTo": "1.0.0", 47 | "success": true, 48 | "updateTo": "1.1.1", 49 | }, 50 | ] 51 | `; 52 | 53 | exports[`integration test: when there are outdated dependencies default configuration npm should install all updates, update the package.json and return the results: default configuration > npm > installed versions 1`] = ` 54 | Object { 55 | "updtr-test-module-1": "2.0.0", 56 | "updtr-test-module-2": "2.1.1", 57 | } 58 | `; 59 | 60 | exports[`integration test: when there are outdated dependencies default configuration npm should install all updates, update the package.json and return the results: default configuration > npm > package.json dependencies 1`] = ` 61 | Object { 62 | "updtr-test-module-1": "^2.0.0", 63 | "updtr-test-module-2": "^2.0.0", 64 | } 65 | `; 66 | 67 | exports[`integration test: when there are outdated dependencies default configuration npm should install all updates, update the package.json and return the results: default configuration > npm > results 1`] = ` 68 | Array [ 69 | Object { 70 | "name": "updtr-test-module-1", 71 | "rollbackTo": "1.1.1", 72 | "success": true, 73 | "updateTo": "2.0.0", 74 | }, 75 | ] 76 | `; 77 | 78 | exports[`integration test: when there are outdated dependencies default configuration yarn should install all updates, update the package.json and return the results: default configuration > yarn > installed versions 1`] = ` 79 | Object { 80 | "updtr-test-module-1": "2.0.0", 81 | "updtr-test-module-2": "2.1.1", 82 | } 83 | `; 84 | 85 | exports[`integration test: when there are outdated dependencies default configuration yarn should install all updates, update the package.json and return the results: default configuration > yarn > package.json dependencies 1`] = ` 86 | Object { 87 | "updtr-test-module-1": "2.0.0", 88 | "updtr-test-module-2": "^2.0.0", 89 | } 90 | `; 91 | 92 | exports[`integration test: when there are outdated dependencies default configuration yarn should install all updates, update the package.json and return the results: default configuration > yarn > results 1`] = ` 93 | Array [ 94 | Object { 95 | "name": "updtr-test-module-1", 96 | "rollbackTo": "1.1.1", 97 | "success": true, 98 | "updateTo": "2.0.0", 99 | }, 100 | ] 101 | `; 102 | -------------------------------------------------------------------------------- /test/integration/helpers/constants.js: -------------------------------------------------------------------------------- 1 | export const PACKAGE_JSON = "package.json"; 2 | export const YARN_LOCK = "yarn.lock"; 3 | export const FIXTURE_NO_OUTDATED = "no-outdated"; 4 | export const FIXTURE_OUTDATED = "outdated"; 5 | -------------------------------------------------------------------------------- /test/integration/helpers/fs.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import temp from "../../helpers/temp"; 3 | import fs from "../../../src/util/fs"; 4 | 5 | const pathToFixtures = path.resolve(__dirname, "..", "..", "fixtures"); 6 | let testCounter = 0; 7 | 8 | export function createTempDir() { 9 | return temp.mkdir("updtr-integration-test-" + testCounter++); 10 | } 11 | 12 | export function read(dir, file) { 13 | return fs.readFile( 14 | // If dir is an absolute path, pathToFixtures will just be ignored 15 | path.resolve(pathToFixtures, dir, file), 16 | "utf8" 17 | ); 18 | } 19 | 20 | export function write(dir, file, contents) { 21 | return fs.writeFile( 22 | // If dir is an absolute path, pathToFixtures will just be ignored 23 | path.resolve(dir, file), 24 | contents 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /test/integration/helpers/getInstalledVersions.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "../../../src/util/fs"; 3 | 4 | export const filterUpdtrTestModule = /^updtr-test-module-\d+/; 5 | 6 | export default (async function (dir, filterPattern) { 7 | const pathToNodeModules = path.resolve(dir, "node_modules"); 8 | const modules = await fs.readdir(pathToNodeModules); 9 | const errors = []; 10 | const versions = Object.create(null); 11 | 12 | await Promise.all( 13 | modules 14 | .filter( 15 | moduleName => 16 | filterPattern === undefined ? 17 | true : 18 | filterPattern.test(moduleName) === true 19 | ) 20 | .map(async moduleName => { 21 | const pathToPackageJson = path.resolve( 22 | pathToNodeModules, 23 | moduleName, 24 | "package.json" 25 | ); 26 | 27 | try { 28 | const packageJson = JSON.parse( 29 | await fs.readFile(pathToPackageJson, "utf8") 30 | ); 31 | 32 | versions[moduleName] = packageJson.version; 33 | } catch (error) { 34 | errors.push({ 35 | error, 36 | moduleName, 37 | }); 38 | } 39 | }) 40 | ); 41 | 42 | return { 43 | versions, 44 | errors, 45 | }; 46 | }); 47 | -------------------------------------------------------------------------------- /test/integration/noOutdated.test.js: -------------------------------------------------------------------------------- 1 | import {USE_YARN} from "../../src/constants/config"; 2 | import { 3 | noOutdatedRegular as noOutdatedPackageContents, 4 | } from "../fixtures/packageJsons"; 5 | import {create, run} from "../../src"; 6 | import getInstalledVersions, { 7 | filterUpdtrTestModule, 8 | } from "./helpers/getInstalledVersions"; 9 | import { 10 | PACKAGE_JSON, 11 | YARN_LOCK, 12 | FIXTURE_NO_OUTDATED, 13 | } from "./helpers/constants"; 14 | import {createTempDir, read, write} from "./helpers/fs"; 15 | 16 | // These tests may take longer on travis 17 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 60 * 1000; 18 | 19 | describe("integration test: when there are no outdated dependencies", () => { 20 | describe("npm", () => { 21 | it("should not change the package.json, change the installed dependencies and resolve with an empty array as results", async () => { 22 | const tempDir = await createTempDir(); 23 | 24 | await write(tempDir, PACKAGE_JSON, noOutdatedPackageContents); 25 | 26 | const updtr = create({ 27 | cwd: tempDir, 28 | }); 29 | 30 | const results = await run(updtr); 31 | 32 | const [ 33 | tempDirPackageContents, 34 | installedVersions, 35 | ] = await Promise.all([ 36 | read(tempDir, PACKAGE_JSON), 37 | getInstalledVersions(tempDir, filterUpdtrTestModule), 38 | ]); 39 | 40 | expect(results).toEqual([]); 41 | expect(tempDirPackageContents).toBe(noOutdatedPackageContents); 42 | expect(installedVersions.versions).toMatchSnapshot( 43 | "npm > installed versions" 44 | ); 45 | }); 46 | }); 47 | describe("yarn", () => { 48 | it("should not change the package.json and yarn.lock and resolve with an empty array as results", async () => { 49 | const [tempDir, yarnLockContents] = await Promise.all([ 50 | createTempDir(), 51 | read(FIXTURE_NO_OUTDATED, YARN_LOCK), 52 | ]); 53 | 54 | await write(tempDir, PACKAGE_JSON, noOutdatedPackageContents); 55 | 56 | const updtr = create({ 57 | cwd: tempDir, 58 | use: USE_YARN, 59 | }); 60 | 61 | const results = await run(updtr); 62 | 63 | const [ 64 | tempDirPackageContents, 65 | installedVersions, 66 | tempDirYarnLockContents, 67 | ] = await Promise.all([ 68 | read(tempDir, PACKAGE_JSON), 69 | getInstalledVersions(tempDir, filterUpdtrTestModule), 70 | read(tempDir, YARN_LOCK), 71 | ]); 72 | 73 | expect(results).toEqual([]); 74 | expect(tempDirPackageContents).toBe(noOutdatedPackageContents); 75 | expect(installedVersions.versions).toMatchSnapshot( 76 | "npm > installed versions" 77 | ); 78 | expect(tempDirYarnLockContents).toBe(yarnLockContents); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/manual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manual-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ../../node_modules/babel-cli/bin/babel-node ../../src/bin", 8 | "test": "node test" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "async": "1.0.0", 15 | "batch-replace": "1.0.0", 16 | "less": "2.0.0", 17 | "node-sass": "4.0.0", 18 | "updtr-test-module-1": "1.0.0", 19 | "updtr-test-module-2": "2.0.0" 20 | }, 21 | "devDependencies": { 22 | "express": "3.0.0", 23 | "mongoose": "4.0.0", 24 | "nodemon": "1.0.0", 25 | "underscore": "1.0.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/manual/test: -------------------------------------------------------------------------------- 1 | setTimeout(() => { 2 | const exitCode = Math.floor(Math.random() * 10) % 2; 3 | 4 | if (exitCode === 0) { 5 | console.log("Everything ok"); 6 | } else { 7 | console.trace("Not ok"); 8 | } 9 | process.exit(exitCode); 10 | }, 2000); 11 | -------------------------------------------------------------------------------- /test/manual/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | batch-replace@^1.1.3: 6 | version "1.1.3" 7 | resolved "https://registry.yarnpkg.com/batch-replace/-/batch-replace-1.1.3.tgz#d178467380d1df93efac7bb873274fa274d2d293" 8 | 9 | updtr-test-module-1@1.0.0: 10 | version "1.0.0" 11 | resolved "https://registry.yarnpkg.com/updtr-test-module-1/-/updtr-test-module-1-1.0.0.tgz#bbbe9d48f45a6c04952bb64f506ca7d5d2659c36" 12 | dependencies: 13 | batch-replace "^1.1.3" 14 | 15 | updtr-test-module-2@2.0.0: 16 | version "2.0.0" 17 | resolved "https://registry.yarnpkg.com/updtr-test-module-2/-/updtr-test-module-2-2.0.0.tgz#1c5ca3c2cc40e4e7004819074bb620dc76c6156d" 18 | -------------------------------------------------------------------------------- /test/reporters/__snapshots__/basic.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic() batch-update fail and show test stdout should print the expected lines: batch-update fail and show test stdout 1`] = ` 4 | "Installing missing dependencies... 5 | > npm install  6 | Looking for outdated modules... 7 | > npm outdated  8 | Found 4 updates. 9 | 10 | - module updating 0.0.0 → 0.0.1... 11 | - module updating 0.0.0 → 0.1.0... 12 | - module updating 0.0.0 → 1.0.0... 13 | - module updating 0.0.0 → 0.0.1... 14 | > npm install  15 | - module testing... 16 | - module testing... 17 | - module testing... 18 | - module testing... 19 | > npm test  20 | - module rolling back 0.0.1 → 0.0.0... 21 | - module rolling back 0.1.0 → 0.0.0... 22 | - module rolling back 1.0.0 → 0.0.0... 23 | - module rolling back 0.0.1 → 0.0.0... 24 | > npm install  25 |  26 | 2 successful updates. 27 | 1 failed update. 28 |  29 | Finished after 1.0s." 30 | `; 31 | 32 | exports[`basic() batch-update success and show test stdout should print the expected lines: batch-update success and show test stdout 1`] = ` 33 | "Installing missing dependencies... 34 | > npm install  35 | Looking for outdated modules... 36 | > npm outdated  37 | Found 4 updates. 38 | 39 | - module updating 0.0.0 → 0.0.1... 40 | - module updating 0.0.0 → 0.1.0... 41 | - module updating 0.0.0 → 1.0.0... 42 | - module updating 0.0.0 → 0.0.1... 43 | > npm install  44 | - module testing... 45 | - module testing... 46 | - module testing... 47 | - module testing... 48 | > npm test  49 | - module 0.0.1 success 50 | - module 0.1.0 success 51 | - module 1.0.0 success 52 | - module 0.0.1 success 53 |  54 | 3 successful updates. 55 |  56 | Finished after 1.0s." 57 | `; 58 | 59 | exports[`basic() custom config and only excluded modules should print the expected lines: custom config and only excluded modules 1`] = ` 60 | "Running updtr with custom configuration: 61 | 62 | - exclude: b, c 63 | 64 | Installing missing dependencies... 65 | > npm install  66 | Looking for outdated modules... 67 | > npm outdated  68 | No updates available for the given modules and version range 69 |  70 | 3 skipped modules: 71 | 72 | - a git 73 | - b excluded 74 | - c excluded 75 |  76 | Finished after 1.0s." 77 | `; 78 | 79 | exports[`basic() custom config and sequential-update with mixed success and show test stdout should print the expected lines: custom config and sequential-update with mixed success and show test stdout 1`] = ` 80 | "Running updtr with custom configuration: 81 | 82 | - exclude: b, c 83 | 84 | Installing missing dependencies... 85 | > npm install  86 | Looking for outdated modules... 87 | > npm outdated  88 | Found 4 updates. 89 | 90 | - module updating 0.0.0 → 0.0.1... 91 | > npm install  92 | - module testing... 93 | > npm test  94 | - module 0.0.1 success 95 | - module updating 0.0.0 → 0.1.0... 96 | > npm install  97 | - module testing... 98 | > npm test  99 | - module rolling back 0.1.0 → 0.0.0... 100 | > npm install  101 | - module 0.1.0 failed 102 | This is the test stdout 103 | - module updating 0.0.0 → 1.0.0... 104 | > npm install  105 | - module testing... 106 | > npm test  107 | - module 1.0.0 success 108 | - module updating 0.0.0 → 0.0.1... 109 | > npm install  110 | - module testing... 111 | > npm test  112 | - module rolling back 0.0.1 → 0.0.0... 113 | > npm install  114 | - module 0.0.1 failed 115 | This is the test stdout 116 |  117 | 2 successful updates. 118 | 2 failed updates. 119 |  120 | Finished after 1.0s." 121 | `; 122 | 123 | exports[`basic() no outdated modules should print the expected lines: no outdated modules 1`] = ` 124 | "Installing missing dependencies... 125 | > npm install  126 | Looking for outdated modules... 127 | > npm outdated  128 | Everything up-to-date 129 |  130 |  131 | Finished after 1.0s." 132 | `; 133 | -------------------------------------------------------------------------------- /test/reporters/__snapshots__/dense.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`dense() batch-update fail and show test stdout should print the expected lines: batch-update fail and show test stdout 1`] = ` 4 | "[?25lInstalling missing dependencies... 5 | > npm install  6 | ... 7 | Looking for outdated modules... 8 | > npm outdated  9 | ... 10 | Found 4 updates. 11 | 12 | - module updating 0.0.0 → 0.0.1... 13 | - module updating 0.0.0 → 0.1.0... 14 | - module updating 0.0.0 → 1.0.0... 15 | - module updating 0.0.0 → 0.0.1... 16 | > npm install  17 | ... 18 | - module testing... 19 | - module testing... 20 | - module testing... 21 | - module testing... 22 | > npm test  23 | ... 24 | - module rolling back 0.0.1 → 0.0.0... 25 | - module rolling back 0.1.0 → 0.0.0... 26 | - module rolling back 1.0.0 → 0.0.0... 27 | - module rolling back 0.0.1 → 0.0.0... 28 | > npm install  29 | ... 30 |  31 | 2 successful updates. 32 | 1 failed update. 33 |  34 | Finished after 1.0s. 35 | " 36 | `; 37 | 38 | exports[`dense() batch-update success and show test stdout should print the expected lines: batch-update success and show test stdout 1`] = ` 39 | "[?25lInstalling missing dependencies... 40 | > npm install  41 | ... 42 | Looking for outdated modules... 43 | > npm outdated  44 | ... 45 | Found 4 updates. 46 | 47 | - module updating 0.0.0 → 0.0.1... 48 | - module updating 0.0.0 → 0.1.0... 49 | - module updating 0.0.0 → 1.0.0... 50 | - module updating 0.0.0 → 0.0.1... 51 | > npm install  52 | ... 53 | - module testing... 54 | - module testing... 55 | - module testing... 56 | - module testing... 57 | > npm test  58 | ... 59 | - module 0.0.1 success 60 | - module 0.1.0 success 61 | - module 1.0.0 success 62 | - module 0.0.1 success 63 |  64 | 3 successful updates. 65 |  66 | Finished after 1.0s. 67 | " 68 | `; 69 | 70 | exports[`dense() custom config and only excluded modules should print the expected lines: custom config and only excluded modules 1`] = ` 71 | "[?25lRunning updtr with custom configuration: 72 | 73 | - exclude: b, c 74 | 75 | Installing missing dependencies... 76 | > npm install  77 | ... 78 | Looking for outdated modules... 79 | > npm outdated  80 | ... 81 | No updates available for the given modules and version range 82 |  83 | 3 skipped modules: 84 | 85 | - a git 86 | - b excluded 87 | - c excluded 88 |  89 | Finished after 1.0s. 90 | " 91 | `; 92 | 93 | exports[`dense() custom config and sequential-update with mixed success and show test stdout should print the expected lines: custom config and sequential-update with mixed success and show test stdout 1`] = ` 94 | "[?25lRunning updtr with custom configuration: 95 | 96 | - exclude: b, c 97 | 98 | Installing missing dependencies... 99 | > npm install  100 | ... 101 | Looking for outdated modules... 102 | > npm outdated  103 | ... 104 | Found 4 updates. 105 | 106 | - module updating 0.0.0 → 0.0.1... 107 | > npm install  108 | ... 109 | - module testing... 110 | > npm test  111 | ... 112 | - module 0.0.1 success 113 | - module updating 0.0.0 → 0.1.0... 114 | > npm install  115 | ... 116 | - module testing... 117 | > npm test  118 | ... 119 | - module rolling back 0.1.0 → 0.0.0... 120 | > npm install  121 | ... 122 | - module 0.1.0 failed 123 | This is the test stdout 124 | - module updating 0.0.0 → 1.0.0... 125 | > npm install  126 | ... 127 | - module testing... 128 | > npm test  129 | ... 130 | - module 1.0.0 success 131 | - module updating 0.0.0 → 0.0.1... 132 | > npm install  133 | ... 134 | - module testing... 135 | > npm test  136 | ... 137 | - module rolling back 0.0.1 → 0.0.0... 138 | > npm install  139 | ... 140 | - module 0.0.1 failed 141 | This is the test stdout 142 |  143 | 2 successful updates. 144 | 2 failed updates. 145 |  146 | Finished after 1.0s. 147 | " 148 | `; 149 | 150 | exports[`dense() no outdated modules should print the expected lines: no outdated modules 1`] = ` 151 | "[?25lInstalling missing dependencies... 152 | > npm install  153 | ... 154 | Looking for outdated modules... 155 | > npm outdated  156 | ... 157 | Everything up-to-date 158 |  159 |  160 | Finished after 1.0s. 161 | " 162 | `; 163 | -------------------------------------------------------------------------------- /test/reporters/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reporters should export all available reporters 1`] = ` 4 | Array [ 5 | "dense", 6 | "basic", 7 | "none", 8 | ] 9 | `; 10 | -------------------------------------------------------------------------------- /test/reporters/basic.test.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | import unicons from "unicons"; 3 | import {stub} from "sinon"; 4 | import basic from "../../src/reporters/basic"; 5 | import events from "../fixtures/events"; 6 | 7 | let consoleStub; 8 | 9 | function setup(reporterConfig = {}) { 10 | const updtr = new EventEmitter(); 11 | const output = []; 12 | 13 | consoleStub.callsFake((...args) => output.push(args)); 14 | basic(updtr, reporterConfig); 15 | 16 | return { 17 | updtr, 18 | output, 19 | }; 20 | } 21 | 22 | beforeAll(() => { 23 | // We need to replace platform-dependent characters with static counter parts to make snapshot testing 24 | // consistent across platforms. 25 | unicons.cli = sign => { 26 | switch (sign) { 27 | case "circle": 28 | return "-"; 29 | default: 30 | return ""; 31 | } 32 | }; 33 | consoleStub = stub(console, "log"); 34 | }); 35 | 36 | afterEach(() => { 37 | consoleStub.reset(); 38 | }); 39 | 40 | afterAll(() => { 41 | consoleStub.restore(); 42 | }); 43 | 44 | describe("basic()", () => { 45 | Object.keys(events).forEach(caseName => { 46 | describe(caseName, () => { 47 | it("should print the expected lines", async () => { 48 | const testCase = events[caseName]; 49 | const {updtr, output} = setup(testCase.reporterConfig); 50 | 51 | await testCase.events.reduce(async (previous, [ 52 | eventName, 53 | event, 54 | ]) => { 55 | await previous; 56 | updtr.emit(eventName, event); 57 | 58 | // Faking async events 59 | return Promise.resolve(); 60 | }, Promise.resolve()); 61 | 62 | expect( 63 | output.join("\n").replace( 64 | // We need to replace the timing because that is non-deterministic 65 | /Finished after \d+\.\ds/, 66 | "Finished after 1.0s" 67 | ) 68 | ).toMatchSnapshot(caseName); 69 | }); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/reporters/dense.test.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | import {WritableStreamBuffer} from "stream-buffers"; 3 | import unicons from "unicons"; 4 | import dense from "../../src/reporters/dense"; 5 | import Spinner from "../../src/reporters/util/Spinner"; 6 | import events from "../fixtures/events"; 7 | 8 | function setup(reporterConfig = {}) { 9 | const updtr = new EventEmitter(); 10 | const stdout = new WritableStreamBuffer(); 11 | 12 | stdout.isTTY = true; 13 | stdout.columns = 80; 14 | 15 | reporterConfig.stream = stdout; 16 | dense(updtr, reporterConfig); 17 | 18 | return { 19 | updtr, 20 | stdout, 21 | }; 22 | } 23 | 24 | beforeAll(() => { 25 | // We need to replace platform-dependent characters with static counter parts to make snapshot testing 26 | // consistent across platforms. 27 | Spinner.prototype.valueOf = () => "..."; 28 | unicons.cli = sign => { 29 | switch (sign) { 30 | case "circle": 31 | return "-"; 32 | default: 33 | return ""; 34 | } 35 | }; 36 | }); 37 | 38 | describe("dense()", () => { 39 | Object.keys(events).forEach(caseName => { 40 | describe(caseName, () => { 41 | it("should print the expected lines", async () => { 42 | const testCase = events[caseName]; 43 | const {updtr, stdout} = setup(testCase.reporterConfig); 44 | 45 | await testCase.events.reduce(async (previous, [ 46 | eventName, 47 | event, 48 | ]) => { 49 | await previous; 50 | updtr.emit(eventName, event); 51 | 52 | // Faking async events 53 | return Promise.resolve(); 54 | }, Promise.resolve()); 55 | 56 | const output = stdout.getContentsAsString("utf8"); 57 | 58 | expect( 59 | output.replace( 60 | // We need to replace the timing because that is non-deterministic 61 | /Finished after \d+\.\ds/, 62 | "Finished after 1.0s" 63 | ) 64 | ).toMatchSnapshot(caseName); 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/reporters/index.test.js: -------------------------------------------------------------------------------- 1 | import reporters from "../../src/reporters"; 2 | 3 | describe("reporters", () => { 4 | it("should export all available reporters", () => { 5 | expect(Object.keys(reporters)).toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/reporters/util/__snapshots__/configList.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`configList() when config.exclude is an array with module names should return the expected line: exclude 1`] = `"exclude: a, b, c"`; 4 | 5 | exports[`configList() when config.registry is specified should return the expected line: registry 1`] = `"registry: http://example.com"`; 6 | 7 | exports[`configList() when config.save is something else should return the expected line: save 1`] = `"save: exact"`; 8 | 9 | exports[`configList() when config.test is specified should return the expected line: test 1`] = `"test command: custom test command"`; 10 | 11 | exports[`configList() when config.updateTo is something else should return the expected line: updateTo 1`] = `"update to: non-breaking"`; 12 | 13 | exports[`configList() when config.use is something else should return the expected line: use 1`] = `"use: yarn"`; 14 | -------------------------------------------------------------------------------- /test/reporters/util/configList.test.js: -------------------------------------------------------------------------------- 1 | import configList from "../../../src/reporters/util/configList"; 2 | import { 3 | USE_OPTIONS, 4 | UPDATE_TO_OPTIONS, 5 | SAVE_OPTIONS, 6 | } from "../../../src/constants/config"; 7 | 8 | describe("configList()", () => { 9 | describe("when an empty object is passed", () => { 10 | it("should return an empty array", () => { 11 | expect(configList({})).toEqual([]); 12 | }); 13 | }); 14 | describe("when config.cwd is specified", () => { 15 | it("should return an empty array", () => { 16 | expect( 17 | configList({ 18 | cwd: "some/path", 19 | }) 20 | ).toEqual([]); 21 | }); 22 | }); 23 | describe("when config.use is", () => { 24 | describe(USE_OPTIONS[0], () => { 25 | it("should return an empty array", () => { 26 | expect( 27 | configList({ 28 | use: USE_OPTIONS[0], 29 | }) 30 | ).toEqual([]); 31 | }); 32 | }); 33 | describe("something else", () => { 34 | it("should return the expected line", () => { 35 | expect( 36 | configList({ 37 | use: "yarn", 38 | })[0] 39 | ).toMatchSnapshot("use"); 40 | }); 41 | }); 42 | }); 43 | describe("when config.exclude is", () => { 44 | describe("an empty array", () => { 45 | it("should return an empty array", () => { 46 | expect( 47 | configList({ 48 | exclude: [], 49 | }) 50 | ).toEqual([]); 51 | }); 52 | }); 53 | describe("an array with module names", () => { 54 | it("should return the expected line", () => { 55 | expect( 56 | configList({ 57 | exclude: ["a", "b", "c"], 58 | })[0] 59 | ).toMatchSnapshot("exclude"); 60 | }); 61 | }); 62 | }); 63 | describe("when config.updateTo is", () => { 64 | describe(UPDATE_TO_OPTIONS[0], () => { 65 | it("should return an empty array", () => { 66 | expect( 67 | configList({ 68 | updateTo: UPDATE_TO_OPTIONS[0], 69 | }) 70 | ).toEqual([]); 71 | }); 72 | }); 73 | describe("something else", () => { 74 | it("should return the expected line", () => { 75 | expect( 76 | configList({ 77 | updateTo: "non-breaking", 78 | })[0] 79 | ).toMatchSnapshot("updateTo"); 80 | }); 81 | }); 82 | }); 83 | describe("when config.save is", () => { 84 | describe(SAVE_OPTIONS[0], () => { 85 | it("should return an empty array", () => { 86 | expect( 87 | configList({ 88 | save: SAVE_OPTIONS[0], 89 | }) 90 | ).toEqual([]); 91 | }); 92 | }); 93 | describe("something else", () => { 94 | it("should return the expected line", () => { 95 | expect( 96 | configList({ 97 | save: "exact", 98 | })[0] 99 | ).toMatchSnapshot("save"); 100 | }); 101 | }); 102 | }); 103 | describe("when config.test is specified", () => { 104 | it("should return the expected line", () => { 105 | expect( 106 | configList({ 107 | test: "custom test command", 108 | })[0] 109 | ).toMatchSnapshot("test"); 110 | }); 111 | }); 112 | describe("when config.registry is specified", () => { 113 | it("should return the expected line", () => { 114 | expect( 115 | configList({ 116 | registry: "http://example.com", 117 | })[0] 118 | ).toMatchSnapshot("registry"); 119 | }); 120 | }); 121 | describe("when unknown config keys are specified", () => { 122 | it("should ignore them", () => { 123 | expect( 124 | configList({ 125 | someOtherOption: true, 126 | }) 127 | ).toEqual([]); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /test/reporters/util/handleError.test.js: -------------------------------------------------------------------------------- 1 | import {spy} from "sinon"; 2 | import handleError from "../../../src/reporters/util/handleError"; 3 | import {PackageJsonNoAccessError} from "../../../src/errors"; 4 | 5 | const processExit = process.exit; 6 | const consoleError = console.error; 7 | 8 | function setupSpies() { 9 | process.exit = spy(); 10 | console.error = spy(); 11 | } 12 | 13 | describe("handleError()", () => { 14 | describe("when err is a PackageJsonNoAccessError", () => { 15 | it("should console.error a descriptive error message and process.exit(1)", () => { 16 | setupSpies(); 17 | handleError(new PackageJsonNoAccessError()); 18 | 19 | const errorStr = console.error.firstCall.args[0]; 20 | 21 | expect(errorStr).toContain(" ERROR "); 22 | expect(errorStr).toContain( 23 | "Cannot find package.json in current directory" 24 | ); 25 | expect(process.exit.calledWith(1)).toBe(true); 26 | }); 27 | }); 28 | describe("when err is unknown", () => { 29 | it("should console.error the error message and process.exit(1)", () => { 30 | setupSpies(); 31 | handleError(new Error("Unknown error")); 32 | 33 | const errorStr = console.error.firstCall.args[0]; 34 | 35 | expect(errorStr).toContain(" ERROR "); 36 | expect(errorStr).toContain("Unknown error"); 37 | // Check for greyed out stack 38 | expect(errorStr).toContain(" at"); 39 | expect(process.exit.calledWith(1)).toBe(true); 40 | }); 41 | }); 42 | }); 43 | 44 | afterAll(() => { 45 | process.exit = processExit; 46 | console.error = consoleError; 47 | }); 48 | -------------------------------------------------------------------------------- /test/tasks/__snapshots__/finish.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`finish() when there are incomplete results using npm should return the expected results and emit the expected events: incomplete > npm > emit args 1`] = ` 4 | Array [ 5 | Array [ 6 | "finish/start", 7 | Object {}, 8 | ], 9 | Array [ 10 | "finish/incomplete", 11 | Object { 12 | "incomplete": Array [ 13 | Object { 14 | "name": "updtr-test-module-1", 15 | "rollbackTo": "1.0.0", 16 | "success": true, 17 | "updateTo": "^1.0.0", 18 | }, 19 | Object { 20 | "name": "updtr-test-module-2", 21 | "rollbackTo": "2.0.0", 22 | "success": true, 23 | "updateTo": "^2.0.0", 24 | }, 25 | ], 26 | }, 27 | ], 28 | Array [ 29 | "finish/list-incomplete", 30 | Object { 31 | "cmd": "npm ls --json --depth=0 updtr-test-module-1 updtr-test-module-2", 32 | }, 33 | ], 34 | Array [ 35 | "finish/end", 36 | Object { 37 | "results": Array [ 38 | Object { 39 | "name": "updtr-test-module-1", 40 | "rollbackTo": "1.0.0", 41 | "success": true, 42 | "updateTo": "2.0.0", 43 | }, 44 | Object { 45 | "name": "updtr-test-module-1", 46 | "rollbackTo": "1.0.0", 47 | "success": true, 48 | "updateTo": "2.0.0", 49 | }, 50 | Object { 51 | "name": "updtr-test-module-2", 52 | "rollbackTo": "2.0.0", 53 | "success": true, 54 | "updateTo": "2.1.1", 55 | }, 56 | Object { 57 | "name": "updtr-test-module-2", 58 | "rollbackTo": "2.0.0", 59 | "success": true, 60 | "updateTo": "2.1.1", 61 | }, 62 | ], 63 | }, 64 | ], 65 | ] 66 | `; 67 | 68 | exports[`finish() when there are incomplete results using npm should return the expected results and emit the expected events: incomplete > npm > exec args 1`] = ` 69 | Array [ 70 | Array [ 71 | "npm ls --json --depth=0 updtr-test-module-1 updtr-test-module-2", 72 | ], 73 | ] 74 | `; 75 | 76 | exports[`finish() when there are incomplete results using npm should return the expected results and emit the expected events: incomplete > npm > results 1`] = ` 77 | Array [ 78 | Object { 79 | "name": "updtr-test-module-1", 80 | "rollbackTo": "1.0.0", 81 | "success": true, 82 | "updateTo": "2.0.0", 83 | }, 84 | Object { 85 | "name": "updtr-test-module-1", 86 | "rollbackTo": "1.0.0", 87 | "success": true, 88 | "updateTo": "2.0.0", 89 | }, 90 | Object { 91 | "name": "updtr-test-module-2", 92 | "rollbackTo": "2.0.0", 93 | "success": true, 94 | "updateTo": "2.1.1", 95 | }, 96 | Object { 97 | "name": "updtr-test-module-2", 98 | "rollbackTo": "2.0.0", 99 | "success": true, 100 | "updateTo": "2.1.1", 101 | }, 102 | ] 103 | `; 104 | 105 | exports[`finish() when there are incomplete results using yarn should return the expected results and emit the expected events: incomplete > yarn > emit args 1`] = ` 106 | Array [ 107 | Array [ 108 | "finish/start", 109 | Object {}, 110 | ], 111 | Array [ 112 | "finish/incomplete", 113 | Object { 114 | "incomplete": Array [ 115 | Object { 116 | "name": "updtr-test-module-1", 117 | "rollbackTo": "1.0.0", 118 | "success": true, 119 | "updateTo": "^1.0.0", 120 | }, 121 | Object { 122 | "name": "updtr-test-module-2", 123 | "rollbackTo": "2.0.0", 124 | "success": true, 125 | "updateTo": "^2.0.0", 126 | }, 127 | ], 128 | }, 129 | ], 130 | Array [ 131 | "finish/list-incomplete", 132 | Object { 133 | "cmd": "yarn list --json --depth=0 updtr-test-module-1 updtr-test-module-2", 134 | }, 135 | ], 136 | Array [ 137 | "finish/end", 138 | Object { 139 | "results": Array [ 140 | Object { 141 | "name": "updtr-test-module-1", 142 | "rollbackTo": "1.0.0", 143 | "success": true, 144 | "updateTo": "2.0.0", 145 | }, 146 | Object { 147 | "name": "updtr-test-module-1", 148 | "rollbackTo": "1.0.0", 149 | "success": true, 150 | "updateTo": "2.0.0", 151 | }, 152 | Object { 153 | "name": "updtr-test-module-2", 154 | "rollbackTo": "2.0.0", 155 | "success": true, 156 | "updateTo": "2.1.1", 157 | }, 158 | Object { 159 | "name": "updtr-test-module-2", 160 | "rollbackTo": "2.0.0", 161 | "success": true, 162 | "updateTo": "2.1.1", 163 | }, 164 | ], 165 | }, 166 | ], 167 | ] 168 | `; 169 | 170 | exports[`finish() when there are incomplete results using yarn should return the expected results and emit the expected events: incomplete > yarn > exec args 1`] = ` 171 | Array [ 172 | Array [ 173 | "yarn list --json --depth=0 updtr-test-module-1 updtr-test-module-2", 174 | ], 175 | ] 176 | `; 177 | 178 | exports[`finish() when there are incomplete results using yarn should return the expected results and emit the expected events: incomplete > yarn > results 1`] = ` 179 | Array [ 180 | Object { 181 | "name": "updtr-test-module-1", 182 | "rollbackTo": "1.0.0", 183 | "success": true, 184 | "updateTo": "2.0.0", 185 | }, 186 | Object { 187 | "name": "updtr-test-module-1", 188 | "rollbackTo": "1.0.0", 189 | "success": true, 190 | "updateTo": "2.0.0", 191 | }, 192 | Object { 193 | "name": "updtr-test-module-2", 194 | "rollbackTo": "2.0.0", 195 | "success": true, 196 | "updateTo": "2.1.1", 197 | }, 198 | Object { 199 | "name": "updtr-test-module-2", 200 | "rollbackTo": "2.0.0", 201 | "success": true, 202 | "updateTo": "2.1.1", 203 | }, 204 | ] 205 | `; 206 | -------------------------------------------------------------------------------- /test/tasks/__snapshots__/updatePackageJson.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`updatePackageJson() should save a package.json with expected shape to the cwd 1`] = ` 4 | Object { 5 | "dependencies": Object { 6 | "updtr-test-module-1": "2.0.0", 7 | "updtr-test-module-2": "1.0.x", 8 | }, 9 | "devDependencies": Object { 10 | "updtr-test-module-1": "~2.0.x", 11 | "updtr-test-module-2": "1.0.x", 12 | }, 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /test/tasks/finish.test.js: -------------------------------------------------------------------------------- 1 | import {USE_YARN} from "../../src/constants/config"; 2 | import finish from "../../src/tasks/finish"; 3 | import FakeUpdtr from "../helpers/FakeUpdtr"; 4 | import { 5 | execError, 6 | npmList, 7 | yarnList, 8 | errorExecList, 9 | } from "../fixtures/execResults"; 10 | import { 11 | module1ToLatestSuccess, 12 | module1ToNonBreakingSuccess, 13 | module2ToLatestSuccess, 14 | module2ToNonBreakingSuccess, 15 | module1ToLatestFail, 16 | module1ToNonBreakingFail, 17 | module2ToLatestFail, 18 | module2ToNonBreakingFail, 19 | } from "../fixtures/updateResults"; 20 | 21 | describe("finish()", () => { 22 | describe("when the given results array is empty", () => { 23 | it("should resolve immediately with an empty array without emitting events", async () => { 24 | const updtr = new FakeUpdtr(); 25 | 26 | expect(await finish(updtr, [])).toEqual([]); 27 | expect(updtr.exec.args).toEqual([]); 28 | expect(updtr.emit.args).toEqual([]); 29 | }); 30 | }); 31 | describe("when the given results are complete", () => { 32 | it("should resolve immediately with the given results without emitting events", async () => { 33 | const updtr = new FakeUpdtr(); 34 | 35 | const results = [ 36 | module1ToLatestSuccess, 37 | module1ToLatestFail, 38 | module1ToNonBreakingFail, 39 | module2ToLatestSuccess, 40 | module2ToLatestFail, 41 | module2ToNonBreakingFail, 42 | ]; 43 | 44 | expect(await finish(updtr, results)).toEqual(results); 45 | expect(updtr.exec.args).toEqual([]); 46 | expect(updtr.emit.args).toEqual([]); 47 | }); 48 | }); 49 | describe("when there are incomplete results", () => { 50 | it("should filter tasks where rollbackTo and updateTo is the same value", async () => { 51 | const updtr = new FakeUpdtr(); 52 | 53 | const results = [ 54 | { 55 | ...module1ToLatestSuccess, 56 | updateTo: "^" + module1ToLatestSuccess.updateTo, 57 | rollbackTo: module1ToLatestSuccess.updateTo, 58 | }, 59 | ]; 60 | 61 | updtr.execResults = npmList; 62 | 63 | expect(await finish(updtr, results)).toEqual([]); 64 | }); 65 | describe("using npm", () => { 66 | it("should return the expected results and emit the expected events", async () => { 67 | const updtr = new FakeUpdtr(); 68 | 69 | const results = [ 70 | module1ToLatestSuccess, 71 | module1ToNonBreakingSuccess, 72 | module2ToLatestSuccess, 73 | module2ToNonBreakingSuccess, 74 | ]; 75 | 76 | updtr.execResults = npmList; 77 | 78 | expect(await finish(updtr, results)).toMatchSnapshot( 79 | "incomplete > npm > results" 80 | ); 81 | expect(updtr.exec.args).toMatchSnapshot( 82 | "incomplete > npm > exec args" 83 | ); 84 | expect(updtr.emit.args).toMatchSnapshot( 85 | "incomplete > npm > emit args" 86 | ); 87 | }); 88 | }); 89 | describe("using yarn", () => { 90 | it("should return the expected results and emit the expected events", async () => { 91 | const updtr = new FakeUpdtr({ 92 | use: USE_YARN, 93 | }); 94 | 95 | const results = [ 96 | module1ToLatestSuccess, 97 | module1ToNonBreakingSuccess, 98 | module2ToLatestSuccess, 99 | module2ToNonBreakingSuccess, 100 | ]; 101 | 102 | updtr.execResults = yarnList; 103 | 104 | expect(await finish(updtr, results)).toMatchSnapshot( 105 | "incomplete > yarn > results" 106 | ); 107 | expect(updtr.exec.args).toMatchSnapshot( 108 | "incomplete > yarn > exec args" 109 | ); 110 | expect(updtr.emit.args).toMatchSnapshot( 111 | "incomplete > yarn > emit args" 112 | ); 113 | }); 114 | }); 115 | }); 116 | describe("unexpected errors", () => { 117 | it("should bail out completely", async () => { 118 | const updtr = new FakeUpdtr(); 119 | const results = [module1ToNonBreakingSuccess]; 120 | let givenErr; 121 | 122 | updtr.execResults = errorExecList; 123 | 124 | try { 125 | await finish(updtr, results); 126 | } catch (err) { 127 | givenErr = err; 128 | } 129 | 130 | expect(givenErr).toBe(execError); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/tasks/init.test.js: -------------------------------------------------------------------------------- 1 | import init from "../../src/tasks/init"; 2 | import FakeUpdtr from "../helpers/FakeUpdtr"; 3 | import { 4 | execError, 5 | npmNoOutdated, 6 | npmOutdated, 7 | yarnOutdated, 8 | errorExecInstallMissing, 9 | errorExecOutdated, 10 | errorParseOutdated, 11 | } from "../fixtures/execResults"; 12 | import {PackageJsonNoAccessError} from "../../src/errors"; 13 | 14 | describe("init()", () => { 15 | describe("when there are no outdated dependencies", () => { 16 | it("should emit expected events and execute expected commands", async () => { 17 | const updtr = new FakeUpdtr(); 18 | 19 | updtr.execResults = npmNoOutdated; 20 | 21 | await init(updtr); 22 | 23 | expect(updtr.exec.args).toMatchSnapshot(); 24 | expect(updtr.emit.args).toMatchSnapshot(); 25 | }); 26 | it("should return the results as emitted with the 'init/end' event", async () => { 27 | const updtr = new FakeUpdtr(); 28 | 29 | updtr.execResults = npmNoOutdated; 30 | 31 | const returnedResults = await init(updtr); 32 | const [name, endEvent] = updtr.emit.args.pop(); 33 | 34 | expect(name).toBe("init/end"); 35 | // The emitted event has additional properties like the config 36 | expect(endEvent).toEqual(expect.objectContaining(returnedResults)); 37 | }); 38 | }); 39 | describe("when there are outdated dependencies", () => { 40 | describe("using npm", () => { 41 | it("should emit expected events and execute expected commands", async () => { 42 | const updtr = new FakeUpdtr(); 43 | 44 | updtr.execResults = npmOutdated; 45 | 46 | await init(updtr); 47 | 48 | expect(updtr.exec.args).toMatchSnapshot(); 49 | expect(updtr.emit.args).toMatchSnapshot(); 50 | }); 51 | it("should return the results as emitted with the 'init/end' event", async () => { 52 | const updtr = new FakeUpdtr(); 53 | 54 | updtr.execResults = npmOutdated; 55 | 56 | const returnedResults = await init(updtr); 57 | const [name, endEvent] = updtr.emit.args.pop(); 58 | 59 | expect(name).toBe("init/end"); 60 | // The emitted event has additional properties like the config 61 | expect(endEvent).toEqual( 62 | expect.objectContaining(returnedResults) 63 | ); 64 | }); 65 | }); 66 | describe("using yarn", () => { 67 | it("should emit expected events and execute expected commands", async () => { 68 | const updtr = new FakeUpdtr({ 69 | use: "yarn", 70 | }); 71 | 72 | updtr.execResults = yarnOutdated; 73 | 74 | await init(updtr); 75 | 76 | expect(updtr.exec.args).toMatchSnapshot(); 77 | expect(updtr.emit.args).toMatchSnapshot(); 78 | }); 79 | // We don't test for everything here because we assume that the rest works the same as with npm 80 | }); 81 | }); 82 | describe("when there are excluded dependencies", () => { 83 | it("should emit expected events and execute expected commands", async () => { 84 | const updtr = new FakeUpdtr({ 85 | exclude: ["updtr-test-module-1", "updtr-test-module-2"], 86 | }); 87 | 88 | updtr.execResults = npmOutdated; 89 | 90 | await init(updtr); 91 | 92 | expect(updtr.exec.args).toMatchSnapshot(); 93 | expect(updtr.emit.args).toMatchSnapshot(); 94 | }); 95 | }); 96 | describe("when there is no package.json in the cwd", () => { 97 | it("should throw a PackageJsonNoAccessError", async () => { 98 | const updtr = new FakeUpdtr(); 99 | let givenErr; 100 | 101 | updtr.canAccessPackageJson.resolves(false); 102 | 103 | try { 104 | await init(updtr); 105 | } catch (err) { 106 | givenErr = err; 107 | } 108 | expect(givenErr).toBeInstanceOf(PackageJsonNoAccessError); 109 | }); 110 | }); 111 | describe("unexpected errors", () => { 112 | it("should fail when installMissing cmd exits with a non-zero exit code", async () => { 113 | const updtr = new FakeUpdtr(); 114 | let givenErr; 115 | 116 | updtr.execResults = errorExecInstallMissing; 117 | 118 | try { 119 | await init(updtr); 120 | } catch (err) { 121 | givenErr = err; 122 | } 123 | 124 | expect(givenErr).toBe(execError); 125 | }); 126 | it("should fail when the outdated cmd exits with an exit code above 1", async () => { 127 | const updtr = new FakeUpdtr(); 128 | let givenErr; 129 | 130 | updtr.execResults = errorExecOutdated; 131 | 132 | try { 133 | await init(updtr); 134 | } catch (err) { 135 | givenErr = err; 136 | } 137 | 138 | expect(givenErr).toBe(execError); 139 | }); 140 | it("should fail with a parse error when the stdout could not be parsed", async () => { 141 | const updtr = new FakeUpdtr(); 142 | let givenErr; 143 | 144 | updtr.execResults = errorParseOutdated; 145 | 146 | try { 147 | await init(updtr); 148 | } catch (err) { 149 | givenErr = err; 150 | } 151 | 152 | expect(givenErr).toBeInstanceOf(SyntaxError); 153 | expect(givenErr.message).toMatch( 154 | /Error when trying to parse stdout from command 'npm outdated --json --depth=0': Unexpected token N/ 155 | ); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /test/tasks/updatePackageJson.test.js: -------------------------------------------------------------------------------- 1 | import updatePackageJson from "../../src/tasks/updatePackageJson"; 2 | import FakeUpdtr from "../helpers/FakeUpdtr"; 3 | import pickEventNames from "../helpers/pickEventNames"; 4 | import { 5 | module1ToLatestSuccess, 6 | module2ToLatestFail, 7 | } from "../fixtures/updateResults"; 8 | import {outdatedRegular} from "../fixtures/packageJsons"; 9 | 10 | describe("updatePackageJson()", () => { 11 | it("should read and write the package json", async () => { 12 | const updtr = new FakeUpdtr(); 13 | const updateResults = []; 14 | 15 | updtr.readFile.resolves(JSON.stringify({})); 16 | updtr.writeFile.resolves(); 17 | 18 | await updatePackageJson(updtr, updateResults); 19 | 20 | expect(updtr.readFile.calledWith("package.json")).toBe(true); 21 | expect(updtr.writeFile.calledWith("package.json")).toBe(true); 22 | }); 23 | it("should save a package.json with expected shape to the cwd", async () => { 24 | const updtr = new FakeUpdtr(); 25 | const updateResults = [module1ToLatestSuccess, module2ToLatestFail]; 26 | 27 | updtr.readFile.resolves( 28 | JSON.stringify({ 29 | dependencies: { 30 | [module1ToLatestSuccess.name]: "1.0.0", 31 | [module2ToLatestFail.name]: "1.0.x", 32 | }, 33 | devDependencies: { 34 | [module1ToLatestSuccess.name]: "~1.0.x", 35 | [module2ToLatestFail.name]: "1.0.x", 36 | }, 37 | }) 38 | ); 39 | updtr.writeFile.resolves(); 40 | 41 | await updatePackageJson(updtr, updateResults); 42 | 43 | expect(updtr.readFile.calledWith("package.json")).toBe(true); 44 | expect(updtr.writeFile.calledWith("package.json")).toBe(true); 45 | 46 | const packageJson = JSON.parse(updtr.writeFile.getCall(0).args[1]); 47 | 48 | expect(packageJson).toMatchSnapshot(); 49 | }); 50 | it("should not alter the formatting", async () => { 51 | const updtr = new FakeUpdtr(); 52 | const updateResults = [module1ToLatestSuccess]; 53 | const oldPackageJson = outdatedRegular; 54 | 55 | updtr.readFile.resolves(oldPackageJson); 56 | updtr.writeFile.resolves(); 57 | 58 | await updatePackageJson(updtr, updateResults); 59 | 60 | const newPackageJson = updtr.writeFile.getCall(0).args[1]; 61 | 62 | expect(newPackageJson).toBe( 63 | oldPackageJson.replace( 64 | /\^1\.0\.0/, 65 | "^" + module1ToLatestSuccess.updateTo 66 | ) 67 | ); 68 | }); 69 | it("should respect indentation", async () => { 70 | const updtr = new FakeUpdtr(); 71 | const updateResults = []; 72 | const fourSpacedPackageJson = outdatedRegular.replace(/ {2}/g, " "); 73 | const tabbedPackageJson = outdatedRegular.replace(/ {2}/g, "\t"); 74 | 75 | updtr.writeFile.resolves(); 76 | 77 | updtr.readFile.resolves(fourSpacedPackageJson); 78 | 79 | await updatePackageJson(updtr, updateResults); 80 | 81 | updtr.readFile.resolves(tabbedPackageJson); 82 | 83 | await updatePackageJson(updtr, updateResults); 84 | 85 | expect(updtr.writeFile.getCall(0).args[1]).toBe(fourSpacedPackageJson); 86 | expect(updtr.writeFile.getCall(1).args[1]).toBe(tabbedPackageJson); 87 | }); 88 | it("should emit the expected events", async () => { 89 | const updtr = new FakeUpdtr(); 90 | const updateResults = [module1ToLatestSuccess]; 91 | 92 | const eventNames = [ 93 | "update-package-json/start", 94 | "update-package-json/end", 95 | ]; 96 | 97 | updtr.readFile.resolves(JSON.stringify({})); 98 | updtr.writeFile.resolves(); 99 | 100 | await updatePackageJson(updtr, updateResults); 101 | 102 | expect(pickEventNames(eventNames, updtr.emit.args)).toEqual(eventNames); 103 | }); 104 | describe("errors", () => { 105 | it("should enhance the error message in case the package json could not been read", async () => { 106 | const updtr = new FakeUpdtr(); 107 | const updateResults = []; 108 | let givenErr; 109 | 110 | updtr.readFile.rejects(new Error("Oops")); 111 | 112 | try { 113 | await updatePackageJson(updtr, updateResults); 114 | } catch (err) { 115 | givenErr = err; 116 | } 117 | 118 | expect(givenErr).toBeInstanceOf(Error); 119 | expect(givenErr.message).toMatch( 120 | /Error while trying to read the package\.json: Oops/ 121 | ); 122 | }); 123 | it("should enhance the error message in case the package json could not been parsed", async () => { 124 | const updtr = new FakeUpdtr(); 125 | const updateResults = []; 126 | let givenErr; 127 | 128 | updtr.readFile.resolves("Invalid JSON"); 129 | 130 | try { 131 | await updatePackageJson(updtr, updateResults); 132 | } catch (err) { 133 | givenErr = err; 134 | } 135 | 136 | expect(givenErr).toBeInstanceOf(SyntaxError); 137 | expect(givenErr.message).toMatch( 138 | /Error while trying to read the package\.json: Unexpected token I/ 139 | ); 140 | }); 141 | it("should enhance the error message in case the package json could not been written", async () => { 142 | const updtr = new FakeUpdtr(); 143 | const updateResults = []; 144 | let givenErr; 145 | 146 | updtr.readFile.resolves(JSON.stringify({})); 147 | updtr.writeFile.rejects(new Error("Oops")); 148 | 149 | try { 150 | await updatePackageJson(updtr, updateResults); 151 | } catch (err) { 152 | givenErr = err; 153 | } 154 | 155 | expect(givenErr).toBeInstanceOf(Error); 156 | expect(givenErr.message).toMatch( 157 | /Error while trying to write the package\.json: Oops/ 158 | ); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/tasks/util/__snapshots__/createUpdateTask.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createUpdateTask() should return a valid update task 1`] = ` 4 | Object { 5 | "name": "module", 6 | "rollbackTo": "0.0.0", 7 | "updateTo": "0.0.1", 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /test/tasks/util/__snapshots__/createUpdatedPackageJson.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createUpdatedPackageJson() dependencies should write the versions of the successful updates to the package json: dependencies > only successful updates 1`] = ` 4 | Object { 5 | "dependencies": Object { 6 | "dep-1": "1.0.0", 7 | "dep-2": "1.0.0", 8 | "dep-3": "1.0.0", 9 | "updtr-test-module-1": "2.0.0", 10 | "updtr-test-module-2": "2.1.1", 11 | "updtr-test-module-3": "1.0.0", 12 | "updtr-test-module-5": "2.0.0", 13 | }, 14 | } 15 | `; 16 | 17 | exports[`createUpdatedPackageJson() devDependencies should write the versions of the successful updates to the package json: devDependencies > only successful updates 1`] = ` 18 | Object { 19 | "devDependencies": Object { 20 | "dep-1": "1.0.0", 21 | "dep-2": "1.0.0", 22 | "dep-3": "1.0.0", 23 | "updtr-test-module-1": "2.0.0", 24 | "updtr-test-module-2": "2.1.1", 25 | "updtr-test-module-3": "1.0.0", 26 | }, 27 | } 28 | `; 29 | 30 | exports[`createUpdatedPackageJson() optionalDependencies should write the versions of the successful updates to the package json: optionalDependencies > only successful updates 1`] = ` 31 | Object { 32 | "optionalDependencies": Object { 33 | "dep-1": "1.0.0", 34 | "dep-2": "1.0.0", 35 | "dep-3": "1.0.0", 36 | "updtr-test-module-1": "2.0.0", 37 | "updtr-test-module-2": "2.1.1", 38 | "updtr-test-module-3": "1.0.0", 39 | }, 40 | } 41 | `; 42 | 43 | exports[`createUpdatedPackageJson() when a module is listed in more than one dependency type should update all dependency occurrences: more than one dependency type 1`] = ` 44 | Object { 45 | "dependencies": Object { 46 | "updtr-test-module-1": "2.0.0", 47 | }, 48 | "devDependencies": Object { 49 | "updtr-test-module-1": "^2.0.0", 50 | }, 51 | "optionalDependencies": Object { 52 | "updtr-test-module-1": "2.x.x", 53 | }, 54 | } 55 | `; 56 | 57 | exports[`createUpdatedPackageJson() when the updated modules are not listed as any dependency type should save the modules as regular dependency: not listed as any dependency type 1`] = ` 58 | Object { 59 | "dependencies": Object { 60 | "updtr-test-module-1": "2.0.0", 61 | "updtr-test-module-2": "2.1.1", 62 | }, 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /test/tasks/util/__snapshots__/updateVersionRange.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`updateVersionRange() should match the expected new version range 1`] = ` 4 | Array [ 5 | "~0.6.1-1 becomes ^5.0.0", 6 | "1.0.0 - 2.0.0 becomes ^5.0.0", 7 | "1.0.0-2.0.0 becomes ^5.0.0", 8 | "1.0.0 becomes 5.0.0", 9 | " 1.0.0 becomes 5.0.0", 10 | "1.0.0-beta.1 becomes 5.0.0", 11 | ">=1.0.0-beta.1 becomes >=5.0.0", 12 | "1.0.0-BETA.1 becomes 5.0.0", 13 | "1.0.0-alpha-1 becomes 5.0.0", 14 | "1.0.0 alpha becomes ^5.0.0", 15 | "~1.0.0 becomes ~5.0.0", 16 | " ~1.0.0 becomes ~5.0.0", 17 | "~1.0.x becomes ~5.0.x", 18 | ">=* becomes ^5.0.0", 19 | " becomes ^5.0.0", 20 | "* becomes ^5.0.0", 21 | ">=1.0.0 becomes >=5.0.0", 22 | " >=1.0.0 becomes >=5.0.0", 23 | ">1.0.0 becomes ^5.0.0", 24 | " >1.0.0 becomes ^5.0.0", 25 | "<=2.0.0 becomes ^5.0.0", 26 | "<2.0.0 becomes ^5.0.0", 27 | ">= 1.0.0 becomes ^5.0.0", 28 | "> 1.0.0 becomes ^5.0.0", 29 | "<= 2.0.0 becomes ^5.0.0", 30 | "< 2.0.0 becomes ^5.0.0", 31 | "< 2.0.0 becomes ^5.0.0", 32 | ">=0.1.97 becomes >=5.0.0", 33 | "0.1.20 || 1.2.4 becomes ^5.0.0", 34 | ">=0.2.3 || <0.0.1 becomes ^5.0.0", 35 | "|| becomes ^5.0.0", 36 | "2.x.x becomes 5.x.x", 37 | "1.2.x becomes 5.0.x", 38 | "1.2.x || 2.x becomes ^5.0.0", 39 | "x becomes ^5.0.0", 40 | "2.*.* becomes 5.*.*", 41 | "1.2.* becomes 5.0.*", 42 | "1.2.* || 2.* becomes ^5.0.0", 43 | "2 becomes ^5.0.0", 44 | "2.3 becomes ^5.0.0", 45 | "~2.4 becomes ^5.0.0", 46 | "~>3.2.1 becomes ^5.0.0", 47 | "~1 becomes ^5.0.0", 48 | "~>1 becomes ^5.0.0", 49 | "~> 1 becomes ^5.0.0", 50 | "~1.0 becomes ^5.0.0", 51 | "~ 1.0 becomes ^5.0.0", 52 | ">=1 becomes ^5.0.0", 53 | ">= 1 becomes ^5.0.0", 54 | "<1.2 becomes ^5.0.0", 55 | "< 1.2 becomes ^5.0.0", 56 | "1 becomes ^5.0.0", 57 | "~v0.5.4-pre becomes ^5.0.0", 58 | "~v0.5.4-pre becomes ^5.0.0", 59 | "=0.7.x becomes ^5.0.0", 60 | ">=0.7.x becomes >=5.0.x", 61 | ">=0.7.X becomes >=5.0.X", 62 | "=0.7.x becomes ^5.0.0", 63 | ">=0.7.x becomes >=5.0.x", 64 | "<=0.7.x becomes ^5.0.0", 65 | ">0.2.3 >0.2.4 <=0.2.5 becomes ^5.0.0", 66 | ">=0.2.3 <=0.2.4 becomes ^5.0.0", 67 | "1.0.0 - 2.0.0 becomes ^5.0.0", 68 | "^1 becomes ^5.0.0", 69 | "^3.0.0 becomes ^5.0.0", 70 | "^1.0.0 || ~2.0.1 becomes ^5.0.0", 71 | "^0.1.0 || ~3.0.1 || 5.0.0 becomes ^5.0.0", 72 | "^0.1.0 || ~3.0.1 || >4 <=5.0.0 becomes ^5.0.0", 73 | "*.1.2 becomes ^5.0.0", 74 | "x.1.2 becomes ^5.0.0", 75 | "1.x.2 becomes 5.x.x", 76 | "1.*.2 becomes 5.*.*", 77 | ] 78 | `; 79 | 80 | exports[`updateVersionRange() when the new version contains a release tag should match the expected new version range 1`] = ` 81 | Array [ 82 | "~0.6.1-1 becomes ^5.0.0-beta.1", 83 | "1.0.0 - 2.0.0 becomes ^5.0.0-beta.1", 84 | "1.0.0-2.0.0 becomes ^5.0.0-beta.1", 85 | "1.0.0 becomes 5.0.0-beta.1", 86 | " 1.0.0 becomes 5.0.0-beta.1", 87 | "1.0.0-beta.1 becomes 5.0.0-beta.1", 88 | ">=1.0.0-beta.1 becomes >=5.0.0-beta.1", 89 | "1.0.0-BETA.1 becomes 5.0.0-beta.1", 90 | "1.0.0-alpha-1 becomes 5.0.0-beta.1", 91 | "1.0.0 alpha becomes ^5.0.0-beta.1", 92 | "~1.0.0 becomes ~5.0.0-beta.1", 93 | " ~1.0.0 becomes ~5.0.0-beta.1", 94 | "~1.0.x becomes ^5.0.0-beta.1", 95 | ">=* becomes ^5.0.0-beta.1", 96 | " becomes ^5.0.0-beta.1", 97 | "* becomes ^5.0.0-beta.1", 98 | ">=1.0.0 becomes >=5.0.0-beta.1", 99 | " >=1.0.0 becomes >=5.0.0-beta.1", 100 | ">1.0.0 becomes ^5.0.0-beta.1", 101 | " >1.0.0 becomes ^5.0.0-beta.1", 102 | "<=2.0.0 becomes ^5.0.0-beta.1", 103 | "<2.0.0 becomes ^5.0.0-beta.1", 104 | ">= 1.0.0 becomes ^5.0.0-beta.1", 105 | "> 1.0.0 becomes ^5.0.0-beta.1", 106 | "<= 2.0.0 becomes ^5.0.0-beta.1", 107 | "< 2.0.0 becomes ^5.0.0-beta.1", 108 | "< 2.0.0 becomes ^5.0.0-beta.1", 109 | ">=0.1.97 becomes >=5.0.0-beta.1", 110 | "0.1.20 || 1.2.4 becomes ^5.0.0-beta.1", 111 | ">=0.2.3 || <0.0.1 becomes ^5.0.0-beta.1", 112 | "|| becomes ^5.0.0-beta.1", 113 | "2.x.x becomes ^5.0.0-beta.1", 114 | "1.2.x becomes ^5.0.0-beta.1", 115 | "1.2.x || 2.x becomes ^5.0.0-beta.1", 116 | "x becomes ^5.0.0-beta.1", 117 | "2.*.* becomes ^5.0.0-beta.1", 118 | "1.2.* becomes ^5.0.0-beta.1", 119 | "1.2.* || 2.* becomes ^5.0.0-beta.1", 120 | "2 becomes ^5.0.0-beta.1", 121 | "2.3 becomes ^5.0.0-beta.1", 122 | "~2.4 becomes ^5.0.0-beta.1", 123 | "~>3.2.1 becomes ^5.0.0-beta.1", 124 | "~1 becomes ^5.0.0-beta.1", 125 | "~>1 becomes ^5.0.0-beta.1", 126 | "~> 1 becomes ^5.0.0-beta.1", 127 | "~1.0 becomes ^5.0.0-beta.1", 128 | "~ 1.0 becomes ^5.0.0-beta.1", 129 | ">=1 becomes ^5.0.0-beta.1", 130 | ">= 1 becomes ^5.0.0-beta.1", 131 | "<1.2 becomes ^5.0.0-beta.1", 132 | "< 1.2 becomes ^5.0.0-beta.1", 133 | "1 becomes ^5.0.0-beta.1", 134 | "~v0.5.4-pre becomes ^5.0.0-beta.1", 135 | "~v0.5.4-pre becomes ^5.0.0-beta.1", 136 | "=0.7.x becomes ^5.0.0-beta.1", 137 | ">=0.7.x becomes ^5.0.0-beta.1", 138 | ">=0.7.X becomes ^5.0.0-beta.1", 139 | "=0.7.x becomes ^5.0.0-beta.1", 140 | ">=0.7.x becomes ^5.0.0-beta.1", 141 | "<=0.7.x becomes ^5.0.0-beta.1", 142 | ">0.2.3 >0.2.4 <=0.2.5 becomes ^5.0.0-beta.1", 143 | ">=0.2.3 <=0.2.4 becomes ^5.0.0-beta.1", 144 | "1.0.0 - 2.0.0 becomes ^5.0.0-beta.1", 145 | "^1 becomes ^5.0.0-beta.1", 146 | "^3.0.0 becomes ^5.0.0-beta.1", 147 | "^1.0.0 || ~2.0.1 becomes ^5.0.0-beta.1", 148 | "^0.1.0 || ~3.0.1 || 5.0.0 becomes ^5.0.0-beta.1", 149 | "^0.1.0 || ~3.0.1 || >4 <=5.0.0 becomes ^5.0.0-beta.1", 150 | "*.1.2 becomes ^5.0.0-beta.1", 151 | "x.1.2 becomes ^5.0.0-beta.1", 152 | "1.x.2 becomes ^5.0.0-beta.1", 153 | "1.*.2 becomes ^5.0.0-beta.1", 154 | ] 155 | `; 156 | -------------------------------------------------------------------------------- /test/tasks/util/createUpdateTask.test.js: -------------------------------------------------------------------------------- 1 | import createUpdateTask from "../../../src/tasks/util/createUpdateTask"; 2 | import { 3 | UPDATE_TO_LATEST, 4 | UPDATE_TO_NON_BREAKING, 5 | UPDATE_TO_WANTED, 6 | } from "../../../src/constants/config"; 7 | import FakeUpdtr from "../../helpers/FakeUpdtr"; 8 | import outdateds from "../../fixtures/outdateds"; 9 | 10 | function stringify(outdated) { 11 | return [ 12 | "current " + outdated.current, 13 | "wanted " + outdated.wanted, 14 | "latest " + outdated.latest, 15 | ].join(", "); 16 | } 17 | 18 | describe("createUpdateTask()", () => { 19 | it("should return a valid update task", () => { 20 | expect( 21 | createUpdateTask(outdateds[0], { 22 | ...FakeUpdtr.baseConfig, 23 | updateTo: UPDATE_TO_LATEST, 24 | }) 25 | ).toMatchSnapshot(); 26 | }); 27 | describe(`when updateTo is "${UPDATE_TO_LATEST}"`, () => { 28 | outdateds.forEach(outdated => { 29 | it(`when given ${stringify(outdated)} should update to ${outdated.latest} and rollback to ${outdated.current}`, () => { 30 | const updateTask = createUpdateTask(outdated, { 31 | ...FakeUpdtr.baseConfig, 32 | updateTo: UPDATE_TO_LATEST, 33 | }); 34 | 35 | expect(updateTask.updateTo).toBe(outdated.latest); 36 | expect(updateTask.rollbackTo).toBe(outdated.current); 37 | }); 38 | }); 39 | }); 40 | describe(`when updateTo is "${UPDATE_TO_NON_BREAKING}"`, () => { 41 | outdateds.forEach(outdated => { 42 | const range = "^" + outdated.current; 43 | 44 | it(`when given ${stringify(outdated)} should update to ${range} and rollback to ${outdated.current}`, () => { 45 | const updateTask = createUpdateTask(outdated, { 46 | ...FakeUpdtr.baseConfig, 47 | updateTo: UPDATE_TO_NON_BREAKING, 48 | }); 49 | 50 | expect(updateTask.updateTo).toBe(range); 51 | expect(updateTask.rollbackTo).toBe(outdated.current); 52 | }); 53 | }); 54 | }); 55 | describe(`when updateTo is "${UPDATE_TO_WANTED}"`, () => { 56 | outdateds.forEach(outdated => { 57 | it(`when given ${stringify(outdated)} should update to ${outdated.wanted} and rollback to ${outdated.current}`, () => { 58 | const updateTask = createUpdateTask(outdated, { 59 | ...FakeUpdtr.baseConfig, 60 | updateTo: UPDATE_TO_WANTED, 61 | }); 62 | 63 | expect(updateTask.updateTo).toBe(outdated.wanted); 64 | expect(updateTask.rollbackTo).toBe(outdated.current); 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/tasks/util/filterUpdateTask.test.js: -------------------------------------------------------------------------------- 1 | import filterUpdateTask from "../../../src/tasks/util/filterUpdateTask"; 2 | import { 3 | GIT, 4 | UNSTABLE, 5 | NOT_WANTED, 6 | EXCLUDED, 7 | EXOTIC, 8 | } from "../../../src/constants/filterReasons"; 9 | import { 10 | isUpdateToNonBreaking, 11 | } from "../../../src/tasks/util/createUpdateTask"; 12 | 13 | const baseUpdateTask = { 14 | name: "some-module", 15 | updateTo: "2.0.0", 16 | rollbackTo: "1.0.0", 17 | }; 18 | 19 | const baseUpdtrConfig = { 20 | exclude: [], 21 | }; 22 | 23 | describe("filterUpdateTask()", () => { 24 | it("should not filter a regular update task", () => { 25 | expect(filterUpdateTask(baseUpdateTask, baseUpdtrConfig)).toBe(null); 26 | }); 27 | it("should not filter an update task that satisfies the isUpdateToNonBreaking test", () => { 28 | const updateTask = {...baseUpdateTask, updateTo: "^1.0.0"}; 29 | 30 | expect(isUpdateToNonBreaking(updateTask)).toBe(true); // sanity check 31 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe(null); 32 | }); 33 | describe("excluded dependencies", () => { 34 | it("should honor the given exclude filter", () => { 35 | const updtrConfig = {...baseUpdateTask}; 36 | 37 | updtrConfig.exclude = [baseUpdateTask.name]; 38 | expect(filterUpdateTask(baseUpdateTask, updtrConfig)).toBe( 39 | EXCLUDED 40 | ); 41 | }); 42 | }); 43 | describe("git dependencies", () => { 44 | it("should filter git dependencies", () => { 45 | const updateTask = {...baseUpdateTask}; 46 | 47 | updateTask.updateTo = "git"; 48 | 49 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe(GIT); 50 | }); 51 | }); 52 | describe("exotic dependencies", () => { 53 | it("should filter exotic dependencies", () => { 54 | const updateTask = {...baseUpdateTask}; 55 | 56 | updateTask.updateTo = "exotic"; 57 | 58 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe(EXOTIC); 59 | }); 60 | }); 61 | describe("not wanted", () => { 62 | it("should filter if rollbackTo is the same as updateTo", () => { 63 | const updateTask = {...baseUpdateTask}; 64 | 65 | updateTask.rollbackTo = "2.0.0"; 66 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 67 | NOT_WANTED 68 | ); 69 | updateTask.rollbackTo = "2.0.0-beta.1"; 70 | updateTask.updateTo = "2.0.0-beta.1"; 71 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 72 | NOT_WANTED 73 | ); 74 | }); 75 | it("should filter if rollbackTo is greater than updateTo", () => { 76 | const updateTask = {...baseUpdateTask}; 77 | 78 | updateTask.rollbackTo = "3.0.0"; 79 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 80 | NOT_WANTED 81 | ); 82 | updateTask.rollbackTo = "3.0.0-beta.1"; 83 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 84 | NOT_WANTED 85 | ); 86 | }); 87 | }); 88 | describe("unstable dependencies", () => { 89 | describe("when the current version is not a pre-release", () => { 90 | it("should filter pre-releases", () => { 91 | const updateTask = {...baseUpdateTask}; 92 | 93 | updateTask.updateTo = "2.0.0-alpha.1"; 94 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 95 | UNSTABLE 96 | ); 97 | updateTask.updateTo = "2.0.0-beta.1"; 98 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 99 | UNSTABLE 100 | ); 101 | updateTask.updateTo = "2.0.0-some-other-cryptic-thing"; 102 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 103 | UNSTABLE 104 | ); 105 | }); 106 | }); 107 | describe("when the current version is a pre-release within the same version range", () => { 108 | it("should not filter pre-releases", () => { 109 | const updateTask = { 110 | ...baseUpdateTask, 111 | rollbackTo: "1.0.0-alpha.1", 112 | }; 113 | 114 | updateTask.updateTo = "1.0.0-alpha.2"; 115 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 116 | null 117 | ); 118 | updateTask.updateTo = "1.0.0-beta.2"; 119 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 120 | null 121 | ); 122 | }); 123 | }); 124 | describe("when the current version is a pre-release not in the same version range", () => { 125 | it("should filter pre-releases", () => { 126 | const updateTask = { 127 | ...baseUpdateTask, 128 | rollbackTo: "1.0.0-beta.1", 129 | }; 130 | 131 | updateTask.updateTo = "1.1.0-alpha.1"; 132 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 133 | UNSTABLE 134 | ); 135 | updateTask.updateTo = "2.0.0-beta.1"; 136 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 137 | UNSTABLE 138 | ); 139 | updateTask.updateTo = "2.0.0-some-other-cryptic-thing"; 140 | expect(filterUpdateTask(updateTask, baseUpdtrConfig)).toBe( 141 | UNSTABLE 142 | ); 143 | }); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /test/tasks/util/splitUpdateTasks.test.js: -------------------------------------------------------------------------------- 1 | import splitUpdateTasks from "../../../src/tasks/util/splitUpdateTasks"; 2 | 3 | const breakingUpdateTasks = [ 4 | { 5 | updateTo: "2.0.0", 6 | rollbackTo: "1.0.1", 7 | }, 8 | { 9 | updateTo: "2.0.0", 10 | rollbackTo: "1.1.0", 11 | }, 12 | { 13 | updateTo: "0.2.0", 14 | rollbackTo: "0.1.0", 15 | }, 16 | { 17 | updateTo: "0.0.2", 18 | rollbackTo: "0.0.1", 19 | }, 20 | ]; 21 | 22 | const nonBreakingUpdateTasks = [ 23 | { 24 | // Does not occur in the real-world since these modules are not outdated. 25 | // However, we still test it here for consistency. 26 | updateTo: "1.0.0", 27 | rollbackTo: "1.0.0", 28 | }, 29 | { 30 | updateTo: "^1.0.0", 31 | rollbackTo: "1.0.0", 32 | }, 33 | { 34 | updateTo: "1.0.1", 35 | rollbackTo: "1.0.0", 36 | }, 37 | { 38 | updateTo: "^1.0.1", 39 | rollbackTo: "1.0.1", 40 | }, 41 | { 42 | updateTo: "1.1.0", 43 | rollbackTo: "1.0.0", 44 | }, 45 | { 46 | updateTo: "^1.1.0", 47 | rollbackTo: "1.1.0", 48 | }, 49 | { 50 | updateTo: "^0.1.0", 51 | rollbackTo: "0.1.0", 52 | }, 53 | { 54 | updateTo: "^0.0.1", 55 | rollbackTo: "0.0.1", 56 | }, 57 | ]; 58 | 59 | const nonBreakingPreVersionUpdateTasks = [ 60 | { 61 | updateTo: "^0.0.1-beta.1", 62 | rollbackTo: "0.0.1-beta.1", 63 | }, 64 | { 65 | updateTo: "^0.1.0-beta.1", 66 | rollbackTo: "0.1.0-beta.1", 67 | }, 68 | { 69 | updateTo: "^1.0.0-beta.1", 70 | rollbackTo: "1.0.0-beta.1", 71 | }, 72 | { 73 | updateTo: "^1.1.0-beta.1", 74 | rollbackTo: "1.1.0-beta.1", 75 | }, 76 | { 77 | updateTo: "^1.1.1-beta.1", 78 | rollbackTo: "1.1.1-beta.1", 79 | }, 80 | { 81 | updateTo: "1.0.1-beta.2", 82 | rollbackTo: "1.0.1-beta.1", 83 | }, 84 | { 85 | updateTo: "1.0.1-beta.1", 86 | rollbackTo: "1.0.1-alpha.1", 87 | }, 88 | ]; 89 | 90 | const breakingPreVersionUpdateTasks = [ 91 | { 92 | updateTo: "1.0.1-beta.1", 93 | rollbackTo: "1.0.0", 94 | }, 95 | // diff = prepatch 96 | { 97 | updateTo: "1.0.2-beta.2", 98 | rollbackTo: "1.0.1-beta.1", 99 | }, 100 | // diff = preminor 101 | { 102 | updateTo: "1.1.0-beta.2", 103 | rollbackTo: "1.0.1-beta.1", 104 | }, 105 | // diff = premajor 106 | { 107 | updateTo: "2.0.0-beta.2", 108 | rollbackTo: "1.0.1-beta.1", 109 | }, 110 | ]; 111 | 112 | describe("splitUpdateTasks()", () => { 113 | describe(".breaking", () => { 114 | it("should be empty by default", () => { 115 | expect(splitUpdateTasks([]).breaking).toEqual([]); 116 | }); 117 | it("should be empty if there are just non-breaking updates", () => { 118 | expect(splitUpdateTasks(nonBreakingUpdateTasks).breaking).toEqual( 119 | [] 120 | ); 121 | }); 122 | it("should be an array with all breaking updates", () => { 123 | const updateTasks = nonBreakingUpdateTasks.concat( 124 | breakingUpdateTasks 125 | ); 126 | 127 | expect(splitUpdateTasks(updateTasks).breaking).toEqual( 128 | breakingUpdateTasks 129 | ); 130 | }); 131 | it("should contain all breaking pre-version updates", () => { 132 | expect( 133 | splitUpdateTasks(breakingPreVersionUpdateTasks).breaking 134 | ).toEqual(breakingPreVersionUpdateTasks); 135 | }); 136 | it("should not contain non-breaking pre-version updates", () => { 137 | expect( 138 | splitUpdateTasks(nonBreakingPreVersionUpdateTasks).breaking 139 | ).toEqual([]); 140 | }); 141 | }); 142 | describe(".nonBreaking", () => { 143 | it("should be empty by default", () => { 144 | expect(splitUpdateTasks([]).nonBreaking).toEqual([]); 145 | }); 146 | it("should be empty if there are just breaking updates", () => { 147 | expect(splitUpdateTasks(breakingUpdateTasks).nonBreaking).toEqual( 148 | [] 149 | ); 150 | }); 151 | it("should be an array with all non-breaking updates", () => { 152 | const updateTasks = breakingUpdateTasks.concat( 153 | nonBreakingUpdateTasks 154 | ); 155 | 156 | expect(splitUpdateTasks(updateTasks).nonBreaking).toEqual( 157 | nonBreakingUpdateTasks 158 | ); 159 | }); 160 | it("should contain non-breaking pre-version updates", () => { 161 | expect( 162 | splitUpdateTasks(nonBreakingPreVersionUpdateTasks).nonBreaking 163 | ).toEqual(nonBreakingPreVersionUpdateTasks); 164 | }); 165 | it("should not contain breaking pre-version updates since they are considered to be unstable", () => { 166 | expect( 167 | splitUpdateTasks(breakingPreVersionUpdateTasks).nonBreaking 168 | ).toEqual([]); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/tasks/util/updateVersionRange.test.js: -------------------------------------------------------------------------------- 1 | import updateVersionRange from "../../../src/tasks/util/updateVersionRange"; 2 | import versionRanges from "../../fixtures/versionRanges"; 3 | 4 | describe("updateVersionRange()", () => { 5 | it("should match the expected new version range", () => { 6 | const newVersionRanges = versionRanges.map(oldVersionRange => 7 | updateVersionRange(oldVersionRange, "5.0.0") 8 | ); 9 | 10 | const comparisons = versionRanges.map( 11 | (oldVersionRange, index) => 12 | oldVersionRange + " becomes " + newVersionRanges[index] 13 | ); 14 | 15 | expect(comparisons).toMatchSnapshot(); 16 | }); 17 | describe("when the new version contains a release tag", () => { 18 | it("should match the expected new version range", () => { 19 | const newVersionRanges = versionRanges.map(oldVersionRange => 20 | updateVersionRange(oldVersionRange, "5.0.0-beta.1") 21 | ); 22 | 23 | const comparisons = versionRanges.map( 24 | (oldVersionRange, index) => 25 | oldVersionRange + 26 | " becomes " + 27 | newVersionRanges[index] 28 | ); 29 | 30 | expect(comparisons).toMatchSnapshot(); 31 | }); 32 | }); 33 | describe("unexpected input", () => { 34 | describe("when the new version is a parseable range", () => { 35 | it("should just return the range", () => { 36 | expect(updateVersionRange("^1.0.0", "^5.0.0")).toBe("^5.0.0"); 37 | }); 38 | }); 39 | describe("when the new version is not parseable", () => { 40 | it("should just return the new range", () => { 41 | expect(updateVersionRange("^1.0.0", "5.x")).toBe("5.x"); 42 | }); 43 | }); 44 | }); 45 | }); 46 | --------------------------------------------------------------------------------