├── .babelrc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── @hothouse │ ├── client-npm │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── fixtures │ │ │ │ └── is-npm │ │ │ │ │ └── package.json │ │ │ └── index.sepc.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── src │ │ │ └── index.js │ ├── client-yarn │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── fixtures │ │ │ │ ├── is-lerna-npm │ │ │ │ │ └── lerna.json │ │ │ │ ├── is-lerna-yarn │ │ │ │ │ └── lerna.json │ │ │ │ ├── is-yarn-workspaces │ │ │ │ │ └── package.json │ │ │ │ ├── is-yarn │ │ │ │ │ ├── package.json │ │ │ │ │ └── yarn.lock │ │ │ │ ├── update-available-with-resolutions │ │ │ │ │ ├── package.json │ │ │ │ │ └── yarn.lock │ │ │ │ └── update-available │ │ │ │ │ ├── package.json │ │ │ │ │ └── yarn.lock │ │ │ └── index.spec.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── src │ │ │ └── index.js │ ├── monorepo-lerna │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── fixtures │ │ │ │ ├── is-lerna │ │ │ │ │ ├── lerna.json │ │ │ │ │ └── package.json │ │ │ │ ├── lockfile-npm │ │ │ │ │ ├── lerna.json │ │ │ │ │ ├── package.json │ │ │ │ │ └── packages │ │ │ │ │ │ ├── has-not-package-lock-json │ │ │ │ │ │ └── package.json │ │ │ │ │ │ └── has-package-lock-json │ │ │ │ │ │ ├── package-lock.json │ │ │ │ │ │ └── package.json │ │ │ │ ├── lockfile-yarn │ │ │ │ │ ├── lerna.json │ │ │ │ │ ├── package.json │ │ │ │ │ └── packages │ │ │ │ │ │ ├── has-not-yarn-lock │ │ │ │ │ │ └── package.json │ │ │ │ │ │ └── has-yarn-lock │ │ │ │ │ │ ├── package.json │ │ │ │ │ │ └── yarn.lock │ │ │ │ └── not-lerna │ │ │ │ │ └── package.json │ │ │ └── index.spec.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── src │ │ │ └── index.js │ ├── monorepo-yarn-workspaces │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── fixtures │ │ │ │ ├── has-yarn-workspaces-with-lockfile │ │ │ │ │ ├── package.json │ │ │ │ │ ├── packages │ │ │ │ │ │ └── child-a │ │ │ │ │ │ │ └── package.json │ │ │ │ │ └── yarn.lock │ │ │ │ ├── is-yarn-workspaces │ │ │ │ │ └── package.json │ │ │ │ └── not-yarn-workspaces │ │ │ │ │ └── package.json │ │ │ └── index.spec.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── src │ │ │ └── index.js │ └── types │ │ ├── README.md │ │ ├── index.js │ │ └── package.json └── hothouse │ ├── README.md │ ├── __tests__ │ ├── BlackList.spec.js │ ├── Engine.spec.js │ ├── Hosting │ │ └── GitHub.spec.js │ ├── Package.spec.js │ ├── SinglePackage.spec.js │ ├── UpdateChunk.spec.js │ ├── cli │ │ └── options.spec.js │ ├── fixtures │ │ ├── has-not-package-lock-json │ │ │ └── package.json │ │ ├── has-package-lock-json │ │ │ ├── package-lock.json │ │ │ └── package.json │ │ └── has-yarn-lock │ │ │ ├── package.json │ │ │ └── yarn.lock │ ├── git.spec.js │ ├── pullRequest.spec.js │ └── utils │ │ └── semver.spec.js │ ├── package-lock.json │ ├── package.json │ └── src │ ├── BlackList.js │ ├── Engine.js │ ├── Hosting │ ├── GitHub.js │ ├── UnknownHosting.js │ ├── graphql.js │ └── index.js │ ├── Package.js │ ├── PackageManagerResolver.js │ ├── RepositoryStructureResolver.js │ ├── SinglePackage.js │ ├── UpdateChunk.js │ ├── WorkerPool.js │ ├── actions.js │ ├── cli │ ├── index.js │ ├── options.js │ └── runner.js │ ├── commitMessage.js │ ├── errors │ └── index.js │ ├── git.js │ ├── index.js │ ├── pullRequest.js │ ├── reporters │ ├── index.js │ └── text.js │ ├── tasks │ ├── applyUpdates.js │ ├── configure.js │ ├── fetchReleases.js │ └── fetchUpdates.js │ ├── utils │ ├── md2html.js │ └── semver.js │ └── worker.js ├── scripts └── release └── type-definition └── index.js.flow /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "8" 8 | }, 9 | "exclude": [ 10 | "transform-regenerator" 11 | ] 12 | } 13 | ], 14 | "@babel/preset-flow" 15 | ], 16 | "plugins": [ 17 | "@babel/plugin-proposal-class-properties", 18 | "@babel/plugin-proposal-object-rest-spread" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | workflows: 2 | version: 2 3 | node-multi-build: 4 | jobs: 5 | - node-v8 6 | - node-latest 7 | e2e: 8 | jobs: 9 | - update-dependencies 10 | triggers: 11 | - schedule: 12 | cron: "0 0 * * 2" # On every Tuesday 09:00 (JST) 13 | filters: 14 | branches: 15 | only: 16 | - master 17 | 18 | version: 2 19 | jobs: 20 | base: &base 21 | docker: 22 | - image: circleci/node:latest 23 | working_directory: ~/repo 24 | steps: 25 | - checkout 26 | - restore_cache: 27 | keys: 28 | - v{{ .Environment.CIRCLECI_CACHE_VERSION }}-dependencies-{{ checksum "package-lock.json" }} 29 | - v{{ .Environment.CIRCLECI_CACHE_VERSION }}-dependencies- 30 | - run: npm install 31 | - run: npm run bootstrap 32 | - save_cache: 33 | paths: 34 | - node_modules 35 | key: v{{ .Environment.CIRCLECI_CACHE_VERSION }}-dependencies-{{ checksum "package-lock.json" }} 36 | - run: npm run lint 37 | - run: npm run flow 38 | - run: npm test 39 | - run: | 40 | mkdir /tmp/hothouse-install-locally 41 | cd /tmp/hothouse-install-locally 42 | npm install $CIRCLE_WORKING_DIRECTORY/packages/hothouse 43 | - run: | 44 | mkdir /tmp/hothouse-install-globally 45 | cd /tmp/hothouse-install-globally 46 | npm install -g $CIRCLE_WORKING_DIRECTORY/packages/hothouse 47 | - run: bash <(curl -s https://codecov.io/bash) 48 | node-v8: 49 | <<: *base 50 | docker: 51 | - image: node:8 52 | node-latest: 53 | <<: *base 54 | docker: 55 | - image: node:latest 56 | 57 | update-dependencies: 58 | <<: *base 59 | docker: 60 | - image: node:8 61 | steps: 62 | - checkout 63 | - run: npm install 64 | - run: npm run bootstrap 65 | - run: | 66 | git config user.name hothouse 67 | git config user.email hothouse@example.com 68 | - run: npx hothouse -t $GITHUB_TOKEN 69 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "babel-eslint", 3 | extends: [ 4 | "eslint:recommended", 5 | "standard", 6 | "prettier", 7 | "prettier/flowtype", 8 | "prettier/standard" 9 | ], 10 | plugins: ["flowtype"], 11 | parserOptions: { 12 | ecmaFeatures: { 13 | jsx: true 14 | } 15 | }, 16 | rules: { 17 | "no-console": "warn", 18 | "no-multi-spaces": "off", 19 | "comma-dangle": "off", 20 | "flowtype/define-flow-type": 1, 21 | "flowtype/use-flow-type": 1 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; It can remove this line when after merged: https://github.com/dawsbot/config-chain/pull/31 3 | .*/node_modules/config-chain/test/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | ./type-definition/index.js.flow 9 | 10 | [lints] 11 | 12 | [options] 13 | 14 | [strict] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Environment 7 | 8 | - Version: `hothouse --version` 9 | - package manager: `npm` / `yarn` 10 | - directory structure: `single package` / `lerna` / `yarn workspaces` 11 | 12 | ## Overview 13 | 14 | ## Actual behavior 15 | 16 | ## Expected behavior 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | dist 63 | .npmrc 64 | .release_note 65 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shingo Inoue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hothouse 2 | 3 | [![npm](https://img.shields.io/npm/v/hothouse.svg)](https://www.npmjs.com/package/hothouse) 4 | [![license](https://img.shields.io/github/license/Leko/hothouse.svg)](https://opensource.org/licenses/MIT) 5 | [![CircleCI](https://circleci.com/gh/Leko/hothouse.svg?style=svg)](https://circleci.com/gh/Leko/hothouse) 6 | [![codecov](https://codecov.io/gh/Leko/hothouse/branch/master/graph/badge.svg)](https://codecov.io/gh/Leko/hothouse) 7 | 8 | Continuous dependency update for Node.js project like [Greenkeeper](https://greenkeeper.io/). 9 | 10 | ![image](https://user-images.githubusercontent.com/1424963/42268316-23121072-7fb6-11e8-8238-6a97ea7c3779.png) 11 | 12 | ## Feature 13 | 14 | - Support npm/yarn 15 | - Support monorepo (Currently, supported [lerna](https://github.com/lerna/lerna) and [Yarn workspaces](https://yarnpkg.com/en/docs/workspaces)) 16 | - Mergeable pull request 17 | - Each pull request separated by single package update. 18 | - [example1](https://github.com/Leko/zapshot/pull/25), [example2](https://github.com/Leko/zapshot/pull/23) 19 | - Extensible 20 | - Support some npm client by your plugin 21 | - Support some monorepo tools by your plugin 22 | 23 | ## Install 24 | 25 | ### Requirement 26 | 27 | - Node.js 8+ 28 | 29 | ``` 30 | npm i -g hothouse 31 | ``` 32 | 33 | ## Usage 34 | 35 | ``` 36 | $ hothouse --help 37 | $ hothouse -t {GITHUB_PERSONAL_TOKEN} 38 | ``` 39 | 40 | Please create a new [personal access token](https://github.com/settings/tokens/new). 41 | `hothouse` need to permission `public_repo`. And use it with `--token` option. 42 | 43 | If your packages depends on private repository, please add permission `repo` (Full control of private repositories). 44 | 45 | ### Run hothouse regularly 46 | 47 | `hothouse` expects scheduled jobs to run periodically rather than manually. 48 | 49 | Some CI service support scheduling job. 50 | Please configure scheduled job to run periodically. 51 | 52 | - [Cron Jobs - Travis CI](https://docs.travis-ci.com/user/cron-jobs/#Adding-Cron-Jobs) 53 | - [Orchestrating Workflows - CircleCI](https://circleci.com/docs/2.0/workflows/#nightly-example) 54 | 55 | Or you can see [our actual config for CircleCI](https://github.com/Leko/hothouse/blob/master/.circleci/config.yml#L10). 56 | If you want to add other CI service guide, please send a pull request :) 57 | 58 | ### Debug 59 | 60 | If you want to debug hothouse, please run with `DEBUG` environment variable like 61 | 62 | ``` 63 | DEBUG=hothouse* hothouse 64 | ``` 65 | 66 | ## Contribution 67 | 68 | 1. Fork this repo 69 | 1. Create your branch like `fix-hoge-foo-bar` `add-hige` 70 | 1. Write your code 71 | 1. Pass all checks (`npm run lint && npm run flow && npm test`) 72 | 1. Commit with [gitmoji](https://gitmoji.carloscuesta.me/) 73 | 1. Submit pull request to `master` branch 74 | 75 | ## Development 76 | 77 | ``` 78 | git clone git@github.com:Leko/hothouse.git 79 | cd hothouse 80 | npm i 81 | npm run bootstrap 82 | ``` 83 | 84 | ## License 85 | 86 | This package under [MIT](https://opensource.org/licenses/MIT) license. 87 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": ["packages/*", "packages/@hothouse/*"], 4 | "version": "0.4.13" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hothouse", 3 | "private": true, 4 | "description": "Continuous dependency update for Node.js project", 5 | "version": "0.3.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "precommit": "pretty-quick --staged", 9 | "bootstrap": "lerna bootstrap", 10 | "release": "./scripts/release", 11 | "build": "lerna run build", 12 | "test": "lerna run test", 13 | "lint": "eslint --cache '**/{src,__tests__}/**/*.js'", 14 | "flow": "flow --show-all-errors" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+ssh://git@github.com/Leko/hothouse.git" 19 | }, 20 | "keywords": [ 21 | "cli", 22 | "ci", 23 | "dependency", 24 | "update" 25 | ], 26 | "author": "Leko ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/Leko/hothouse/issues" 30 | }, 31 | "homepage": "https://github.com/Leko/hothouse#readme", 32 | "changelog": { 33 | "labels": { 34 | "Type: Breaking Change": "Breaking Change", 35 | "Type: Feature": "Feature", 36 | "Type: Bug": "Bug", 37 | "Type: Maintenance": "Maintenance", 38 | "Type: Documentation": "Documentation", 39 | "Type: Refactoring": "Refactoring" 40 | } 41 | }, 42 | "devDependencies": { 43 | "@babel/cli": "^7.0.0-beta.51", 44 | "@babel/core": "^7.0.0-beta.51", 45 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.51", 46 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.51", 47 | "@babel/preset-env": "^7.0.0-beta.51", 48 | "@babel/preset-flow": "^7.0.0-beta.51", 49 | "@babel/register": "^7.0.0-beta.51", 50 | "babel-core": "^7.0.0-bridge.0", 51 | "babel-eslint": "^10.0.1", 52 | "babel-jest": "^23.2.0", 53 | "can-npm-publish": "^1.3.1", 54 | "dotenv-cli": "^1.4.0", 55 | "eslint": "^5.0.0", 56 | "eslint-config-prettier": "^3.0.1", 57 | "eslint-config-standard": "^12.0.0", 58 | "eslint-plugin-flowtype": "^3.0.0", 59 | "eslint-plugin-import": "^2.12.0", 60 | "eslint-plugin-node": "^6.0.1", 61 | "eslint-plugin-promise": "^3.8.0", 62 | "eslint-plugin-standard": "^3.1.0", 63 | "flow-bin": "^0.75.0", 64 | "husky": "^0.14.3", 65 | "jest": "^23.2.0", 66 | "lerna": "^3.0.3", 67 | "lerna-changelog": "^0.8.0", 68 | "prettier": "^1.13.5", 69 | "pretty-quick": "^1.6.0", 70 | "regenerator-runtime": "^0.12.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/@hothouse/client-npm/README.md: -------------------------------------------------------------------------------- 1 | # @hothouse/client-npm 2 | 3 | Internal package for [hothouse](https://github.com/Leko/hothouse). 4 | 5 | TODO: Add more information 6 | -------------------------------------------------------------------------------- /packages/@hothouse/client-npm/__tests__/fixtures/is-npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-npm", 3 | "version": "1.0.0", 4 | "description": "Fixture of empty npm package", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /packages/@hothouse/client-npm/__tests__/index.sepc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import path from "path"; 5 | import Npm from "../src/index"; 6 | 7 | test("Npm#match should returns true if directory have package.json", async () => { 8 | const npm = new Npm(); 9 | assert.ok(await npm.match(path.join(__dirname, "fixtures", "is-npm"))); 10 | }); 11 | test("Npm#match should returns false if directory not have package.json", async () => { 12 | const npm = new Npm(); 13 | assert.ok(!(await npm.match(path.join(__dirname, "fixtures", "not-npm")))); 14 | }); 15 | 16 | test("Npm#getLockFileName should returns package-lock.json", async () => { 17 | const npm = new Npm(); 18 | assert.ok(npm.getLockFileName(), "package-lock.json"); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/@hothouse/client-npm/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/client-npm", 3 | "version": "0.4.13", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "debug": { 8 | "version": "4.0.1", 9 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", 10 | "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", 11 | "requires": { 12 | "ms": "^2.1.1" 13 | } 14 | }, 15 | "ms": { 16 | "version": "2.1.1", 17 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 18 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/@hothouse/client-npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/client-npm", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.4.13", 7 | "description": "Handle npm packages with npm", 8 | "main": "dist/index.js", 9 | "scripts": { 10 | "prepare": "npm run build", 11 | "build": "../../../node_modules/.bin/babel src --out-dir dist", 12 | "test": "../../../node_modules/.bin/jest --coverage" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/Leko/hothouse.git" 20 | }, 21 | "keywords": [ 22 | "hothouse", 23 | "npm", 24 | "package", 25 | "manager" 26 | ], 27 | "author": "Leko ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/Leko/hothouse/issues" 31 | }, 32 | "homepage": "https://github.com/Leko/hothouse#readme", 33 | "dependencies": { 34 | "debug": "^4.0.1" 35 | }, 36 | "devDependencies": { 37 | "@hothouse/types": "^0.4.13" 38 | }, 39 | "babel": { 40 | "extends": "../../../.babelrc" 41 | }, 42 | "collectCoverageFrom": [ 43 | "src/*.{js,jsx}" 44 | ], 45 | "gitHead": "97cee2b39df40662cb9a2767e8c047066b6c9a98" 46 | } 47 | -------------------------------------------------------------------------------- /packages/@hothouse/client-npm/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from "fs"; 3 | import path from "path"; 4 | import cp from "child_process"; 5 | import type { PackageManager, Updates } from "@hothouse/types"; 6 | 7 | class Npm implements PackageManager { 8 | async match(directory: string): Promise { 9 | return fs.existsSync(path.join(directory, "package.json")); 10 | } 11 | 12 | getLockFileName(): string { 13 | return "package-lock.json"; 14 | } 15 | 16 | async getUpdates(packageDirectory: string): Promise { 17 | // $FlowFixMe(dynamic-require) 18 | const pkg = require(path.join(packageDirectory, "package.json")); 19 | const result = cp.spawnSync("npm", ["outdated", "--json"], { 20 | cwd: packageDirectory, 21 | encoding: "utf8" 22 | }); 23 | if (result.stdout === "") { 24 | return []; 25 | } 26 | 27 | // $FlowFixMe(stdout-is-string) 28 | const outdated: { [string]: Outdated } = JSON.parse(result.stdout); 29 | // $FlowFixMe(entries-returns-mixed) 30 | const outdatedPackages: Array<[string, Outdated]> = Object.entries( 31 | outdated 32 | ); 33 | return outdatedPackages 34 | .filter( 35 | ([name, outdated]: [string, Outdated]) => outdated.latest !== "linked" 36 | ) 37 | .reduce((acc, [name, outdated]: [string, Outdated]) => { 38 | const inDev = !!(pkg.devDependencies && pkg.devDependencies[name]); 39 | return acc.concat({ 40 | name, 41 | current: outdated.current, 42 | currentRange: pkg[inDev ? "devDependencies" : "dependencies"][name], 43 | latest: outdated.latest, 44 | dev: inDev 45 | }); 46 | }, []); 47 | } 48 | 49 | async install(packageDirectory: string): Promise { 50 | const result = cp.spawnSync("npm", ["install"], { 51 | cwd: packageDirectory, 52 | encoding: "utf8" 53 | }); 54 | if (result.error) { 55 | throw result.error; 56 | } 57 | if (result.status !== 0) { 58 | throw new Error(result.stderr); 59 | } 60 | } 61 | 62 | async getPackageMeta(packageName: string): Promise { 63 | const result = cp.spawnSync("npm", ["show", "--json", packageName], { 64 | encoding: "utf8" 65 | }); 66 | 67 | // $FlowFixMe(stdio-is-string) 68 | return JSON.parse(result.stdout); 69 | } 70 | } 71 | 72 | module.exports = Npm; 73 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/README.md: -------------------------------------------------------------------------------- 1 | # @hothouse/client-yarn 2 | 3 | Internal package for [hothouse](https://github.com/Leko/hothouse). 4 | 5 | TODO: Add more information 6 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/fixtures/is-lerna-npm/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": ["packages/*"], 4 | "version": "0.0.3" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/fixtures/is-lerna-yarn/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": ["packages/*"], 4 | "npmClient": "yarn", 5 | "version": "0.0.3" 6 | } 7 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/fixtures/is-yarn-workspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-yarn-workspaces", 3 | "version": "1.0.0", 4 | "description": "Fixture of empty yarn workspaces projects", 5 | "main": "index.js", 6 | "workspaces": [ 7 | "workspace-a", 8 | "workspace-b" 9 | ], 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/fixtures/is-yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-npm", 3 | "version": "1.0.0", 4 | "description": "Fixture of empty npm package", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/fixtures/is-yarn/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leko/hothouse/225ec0db621445e5a68615b2d459572239b47ffc/packages/@hothouse/client-yarn/__tests__/fixtures/is-yarn/yarn.lock -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/fixtures/update-available-with-resolutions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "update-available-with-resolutions", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "resolutions": { 6 | "hothouse": "file:../../../../../hothouse", 7 | "babel-cli": "6.0.0" 8 | }, 9 | "dependencies": { 10 | "is-yarn": "file:../is-yarn", 11 | "babel": "~6.0.0" 12 | }, 13 | "devDependencies": { 14 | "hothouse": "file:../../../../../hothouse", 15 | "babel-cli": "~6.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/fixtures/update-available/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "update-available", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "is-yarn": "file:../is-yarn", 7 | "babel": "~6.0.0" 8 | }, 9 | "devDependencies": { 10 | "hothouse": "file:../../../../../hothouse", 11 | "babel-cli": "~6.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import path from "path"; 5 | import semver from "semver"; 6 | import Yarn from "../src/index"; 7 | 8 | test("Yarn#match should returns true if directory have yarn.lock", async () => { 9 | const yarn = new Yarn(); 10 | assert(await yarn.match(path.join(__dirname, "fixtures", "is-yarn"))); 11 | }); 12 | test("Yarn#match should returns true if directory have lerna.json and npmClient=yarn", async () => { 13 | const yarn = new Yarn(); 14 | assert(await yarn.match(path.join(__dirname, "fixtures", "is-lerna-yarn"))); 15 | }); 16 | test("Yarn#match should returns false if directory have lerna.json and npmClient is not yarn", async () => { 17 | const yarn = new Yarn(); 18 | assert(!(await yarn.match(path.join(__dirname, "fixtures", "is-lerna-npm")))); 19 | }); 20 | test("Yarn#match should returns true if directory have package.json and workspaces field", async () => { 21 | const yarn = new Yarn(); 22 | assert( 23 | await yarn.match(path.join(__dirname, "fixtures", "is-yarn-workspaces")) 24 | ); 25 | }); 26 | test("Yarn#match should returns false if directory not have yarn.lock", async () => { 27 | const yarn = new Yarn(); 28 | assert(!(await yarn.match(path.join(__dirname, "fixtures", "not-yarn")))); 29 | }); 30 | 31 | test("Yarn#getLockFileName should returns yarn.lock", async () => { 32 | const yarn = new Yarn(); 33 | assert(yarn.getLockFileName(), "yarn.lock"); 34 | }); 35 | 36 | test("Yarn#toUpdate can retries currentRange from dependencies", async () => { 37 | const yarn = new Yarn(); 38 | const pkg = { 39 | dependencies: { 40 | "pkg-a": "^1.2.0" 41 | } 42 | }; 43 | const payload = { 44 | Package: "pkg-a", 45 | Current: "1.2.3", 46 | Latest: "2.0.0", 47 | "Package Type": "dependencies" 48 | }; 49 | const expected = { 50 | name: "pkg-a", 51 | current: "1.2.3", 52 | currentRange: "^1.2.0", 53 | latest: "2.0.0", 54 | dev: false 55 | }; 56 | assert.deepStrictEqual(yarn.toUpdate(payload, pkg), expected); 57 | }); 58 | test("Yarn#toUpdate can retries currentRange from devDependencies", async () => { 59 | const yarn = new Yarn(); 60 | const pkg = { 61 | devDependencies: { 62 | "pkg-a": "^1.2.0" 63 | } 64 | }; 65 | const payload = { 66 | Package: "pkg-a", 67 | Current: "1.2.3", 68 | Latest: "2.0.0", 69 | "Package Type": "devDependencies" 70 | }; 71 | const expected = { 72 | name: "pkg-a", 73 | current: "1.2.3", 74 | currentRange: "^1.2.0", 75 | latest: "2.0.0", 76 | dev: true 77 | }; 78 | assert.deepStrictEqual(yarn.toUpdate(payload, pkg), expected); 79 | }); 80 | 81 | test("Yarn#filterRow should return true when Latest is not exotic", () => { 82 | const yarn = new Yarn(); 83 | const actual = yarn.filterRow( 84 | { 85 | Package: "some-pkg", 86 | Current: "1.2.3", 87 | Latest: "1.3.0", 88 | "Package Type": "devDependencies" 89 | }, 90 | { name: "pkg-a" } 91 | ); 92 | 93 | assert(actual); 94 | }); 95 | test("Yarn#filterRow should return false when Latest is exotic", () => { 96 | const yarn = new Yarn(); 97 | const actual = yarn.filterRow( 98 | { 99 | Package: "some-pkg", 100 | Current: "1.2.3", 101 | Latest: "exotic", 102 | "Package Type": "devDependencies" 103 | }, 104 | { name: "pkg-a" } 105 | ); 106 | 107 | assert(!actual); 108 | }); 109 | test("Yarn#filterRow should return true when it included in this package in Yarn workspaces", () => { 110 | const yarn = new Yarn(); 111 | const actual = yarn.filterRow( 112 | { 113 | Package: "some-pkg", 114 | Current: "1.2.3", 115 | Latest: "1.3.0", 116 | "Package Type": "devDependencies", 117 | Workspace: "pkg-a" 118 | }, 119 | { name: "pkg-a" } 120 | ); 121 | 122 | assert(actual); 123 | }); 124 | test("Yarn#filterRow should return false when outside dependency in Yarn workspaces", () => { 125 | const yarn = new Yarn(); 126 | const actual = yarn.filterRow( 127 | { 128 | Package: "some-pkg", 129 | Current: "1.2.3", 130 | Latest: "1.3.0", 131 | "Package Type": "devDependencies", 132 | Workspace: "pkg-b" 133 | }, 134 | { name: "pkg-a" } 135 | ); 136 | 137 | assert(!actual); 138 | }); 139 | 140 | test("Yarn#getUpdates can retrieve updates", async () => { 141 | const yarn = new Yarn(); 142 | const dir = path.join(__dirname, "fixtures", "update-available"); 143 | const updates = await yarn.getUpdates(dir); 144 | updates.forEach(({ name, current, currentRange, latest }) => { 145 | assert(semver.satisfies(current, currentRange), `${name}:currentRange`); 146 | assert(semver.valid(current), `${name}:current`); 147 | assert(semver.valid(latest), `${name}:latest`); 148 | }); 149 | const filetered = updates.map(({ name, current, currentRange, dev }) => ({ 150 | name, 151 | currentRange, 152 | dev 153 | })); 154 | assert.deepStrictEqual(filetered, [ 155 | { name: "babel", currentRange: "~6.0.0", dev: false }, 156 | { name: "babel-cli", currentRange: "~6.0.0", dev: true } 157 | ]); 158 | }); 159 | test("Yarn#getUpdates should only return packages not specified in resolutions", async () => { 160 | const yarn = new Yarn(); 161 | const dir = path.join( 162 | __dirname, 163 | "fixtures", 164 | "update-available-with-resolutions" 165 | ); 166 | const updates = await yarn.getUpdates(dir); 167 | updates.forEach(({ name, current, currentRange, latest }) => { 168 | assert(semver.satisfies(current, currentRange), `${name}:currentRange`); 169 | assert(semver.valid(current), `${name}:current`); 170 | assert(semver.valid(latest), `${name}:latest`); 171 | }); 172 | const filetered = updates.map(({ name, current, currentRange, dev }) => ({ 173 | name, 174 | currentRange, 175 | dev 176 | })); 177 | assert.deepStrictEqual(filetered, [ 178 | { name: "babel", currentRange: "~6.0.0", dev: false } 179 | ]); 180 | }); 181 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/client-yarn", 3 | "version": "0.4.13", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "debug": { 8 | "version": "4.0.1", 9 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", 10 | "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", 11 | "requires": { 12 | "ms": "^2.1.1" 13 | } 14 | }, 15 | "ms": { 16 | "version": "2.1.1", 17 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 18 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 19 | }, 20 | "semver": { 21 | "version": "5.5.0", 22 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", 23 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", 24 | "dev": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/client-yarn", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.4.13", 7 | "description": "Handle npm packages with Yarn", 8 | "main": "dist/index.js", 9 | "scripts": { 10 | "prepare": "npm run build", 11 | "build": "../../../node_modules/.bin/babel src --out-dir dist", 12 | "test": "../../../node_modules/.bin/jest --coverage" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/Leko/hothouse.git" 20 | }, 21 | "keywords": [ 22 | "hothouse", 23 | "yarn", 24 | "package", 25 | "manager" 26 | ], 27 | "author": "Leko ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/Leko/hothouse/issues" 31 | }, 32 | "homepage": "https://github.com/Leko/hothouse#readme", 33 | "dependencies": { 34 | "@hothouse/monorepo-lerna": "^0.4.13", 35 | "@hothouse/monorepo-yarn-workspaces": "^0.4.13", 36 | "debug": "^4.0.1" 37 | }, 38 | "devDependencies": { 39 | "@hothouse/types": "^0.4.13", 40 | "semver": "^5.5.0" 41 | }, 42 | "babel": { 43 | "extends": "../../../.babelrc" 44 | }, 45 | "collectCoverageFrom": [ 46 | "src/*.{js,jsx}" 47 | ], 48 | "gitHead": "97cee2b39df40662cb9a2767e8c047066b6c9a98" 49 | } 50 | -------------------------------------------------------------------------------- /packages/@hothouse/client-yarn/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { EOL } from "os"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import zipObject from "lodash/zipObject"; 6 | import cp from "child_process"; 7 | import type { PackageManager, Update, Updates } from "@hothouse/types"; 8 | import Lerna from "@hothouse/monorepo-lerna"; 9 | import YarnWorkspaces from "@hothouse/monorepo-yarn-workspaces"; 10 | 11 | const debug = require("debug")("hothouse:NpmClient:Yarn"); 12 | const lerna = new Lerna(); 13 | const yarnWorkspaces = new YarnWorkspaces(); 14 | 15 | type YarnOutdated = {| 16 | Package: string, 17 | Current: string, 18 | Latest: string, 19 | "Package Type": string, 20 | Workspace?: string 21 | |}; 22 | 23 | class Yarn implements PackageManager { 24 | async match(directory: string): Promise { 25 | if (await yarnWorkspaces.match(directory)) { 26 | return true; 27 | } 28 | if (await lerna.match(directory)) { 29 | // $FlowFixMe(dynamic-require) 30 | const settings = require(path.join(directory, "lerna.json")); 31 | return settings.npmClient === "yarn"; 32 | } 33 | 34 | return fs.existsSync(path.join(directory, "yarn.lock")); 35 | } 36 | 37 | getLockFileName(): string { 38 | return "yarn.lock"; 39 | } 40 | 41 | async getUpdates(packageDirectory: string): Promise { 42 | // $FlowFixMe(dynamic-require) 43 | const pkg = require(path.join(packageDirectory, "package.json")); 44 | const outdated = this.getOutdated(packageDirectory); 45 | const resolutions = outdated 46 | .filter( 47 | ({ "Package Type": PackageType }) => 48 | PackageType === "resolutionDependencies" 49 | ) 50 | .reduce((acc, { Package }) => ({ ...acc, [Package]: true }), {}); 51 | const filterd = outdated.filter(row => 52 | this.filterRow(row, pkg, resolutions) 53 | ); 54 | return filterd.map(row => this.toUpdate(row, pkg)); 55 | } 56 | 57 | async install(packageDirectory: string): Promise { 58 | const result = cp.spawnSync("yarn", ["install"], { 59 | cwd: packageDirectory, 60 | encoding: "utf8" 61 | }); 62 | if (result.error) { 63 | throw result.error; 64 | } 65 | if (result.status !== 0) { 66 | throw new Error(result.stderr); 67 | } 68 | } 69 | 70 | async getPackageMeta(packageName: string): Promise { 71 | const result = cp.spawnSync("yarn", ["info", "--json", packageName], { 72 | encoding: "utf8" 73 | }); 74 | 75 | // $FlowFixMe(stdio-is-string) 76 | return JSON.parse(result.stdout).data; 77 | } 78 | 79 | getOutdated(packageDirectory: string) { 80 | const result = cp.spawnSync("yarn", ["outdated", "--json"], { 81 | cwd: packageDirectory, 82 | encoding: "utf8" 83 | }); 84 | if (result.stdout === "") { 85 | return []; 86 | } 87 | 88 | // $FlowFixMe(stdout-is-string) 89 | const output = `${result.stdout}\n${result.stderr}`; 90 | const lines = this.parseLineJson(output); 91 | const table = lines.find(line => line.type === "table"); 92 | if (!table) { 93 | throw new Error(`Cannot find outdated results in:\n${output}`); 94 | } 95 | return this.parseOutdated(table.data); 96 | } 97 | 98 | parseLineJson(lines: string): Array { 99 | const parsed = lines 100 | .split(EOL) 101 | .filter(line => line.trim().length) 102 | .map(line => { 103 | try { 104 | return JSON.parse(line); 105 | } catch (e) { 106 | debug(`Failed to parse as JSON: ${line}. Ignored`); 107 | // $FlowFixMe(filterd-after) 108 | return null; 109 | } 110 | }) 111 | .filter(line => line !== null); 112 | parsed.forEach(line => { 113 | if (line.type === "error") { 114 | throw new Error(line.data); 115 | } 116 | }); 117 | return parsed; 118 | } 119 | 120 | parseOutdated(table: { 121 | head: Array, 122 | body: Array> 123 | }): Array { 124 | return table.body.map(row => zipObject(table.head, row)); 125 | } 126 | 127 | toUpdate(outdated: YarnOutdated, pkg: Object): Update { 128 | const { 129 | Package: name, 130 | Current: current, 131 | Latest: latest, 132 | "Package Type": type 133 | } = outdated; 134 | 135 | return { 136 | name, 137 | current, 138 | latest, 139 | currentRange: pkg[type][name], 140 | dev: type !== "dependencies" 141 | }; 142 | } 143 | 144 | filterRow( 145 | { Package, Latest, Workspace, "Package Type": PackageType }: YarnOutdated, 146 | pkg: Object, 147 | resolutions?: { [string]: boolean } = {} 148 | ): boolean { 149 | // Hothouse should ignore resolution dependencies 150 | // Some user specify `resolutions` when have any troubles 151 | // Also, I don’t know whether the trouble will be solved by updating 152 | if (resolutions[Package]) { 153 | debug(`${Package} is specified in resolutions. Ignored`); 154 | return false; 155 | } 156 | // exotic: local package 157 | if (Latest === "exotic") { 158 | debug(`${Package} is linked package. Ignored`); 159 | return false; 160 | } 161 | // Yarn workspaces include other package updates 162 | if (Workspace && Workspace !== pkg.name) { 163 | debug(`${Package} is outside dependency of ${pkg.name}. Ignored`); 164 | return false; 165 | } 166 | return true; 167 | } 168 | } 169 | 170 | module.exports = Yarn; 171 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/README.md: -------------------------------------------------------------------------------- 1 | # @hothouse/monorepo-lerna 2 | 3 | Internal package for [hothouse](https://github.com/Leko/hothouse). 4 | 5 | TODO: Add more information 6 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/is-lerna/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": ["packages/*"], 4 | "version": "0.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/is-lerna/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-lerna", 3 | "version": "0.0.0", 4 | "description": "Fixture of empty lerna projects", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-npm/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": ["packages/*"], 4 | "version": "0.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-lockfile-npm", 3 | "version": "0.0.0", 4 | "description": "Fixture of empty lerna projects with package-lock.json", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-npm/packages/has-not-package-lock-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-not-package-lock-json", 3 | "version": "0.0.0", 4 | "license": "MIT" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-npm/packages/has-package-lock-json/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-package-lock-json", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 11 | }, 12 | "brace-expansion": { 13 | "version": "1.1.11", 14 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 15 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 16 | "requires": { 17 | "balanced-match": "1.0.0", 18 | "concat-map": "0.0.1" 19 | } 20 | }, 21 | "concat-map": { 22 | "version": "0.0.1", 23 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 24 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 25 | }, 26 | "fs.realpath": { 27 | "version": "1.0.0", 28 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 29 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 30 | }, 31 | "glob": { 32 | "version": "7.1.2", 33 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 34 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 35 | "requires": { 36 | "fs.realpath": "1.0.0", 37 | "inflight": "1.0.6", 38 | "inherits": "2.0.3", 39 | "minimatch": "3.0.4", 40 | "once": "1.4.0", 41 | "path-is-absolute": "1.0.1" 42 | } 43 | }, 44 | "inflight": { 45 | "version": "1.0.6", 46 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 47 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 48 | "requires": { 49 | "once": "1.4.0", 50 | "wrappy": "1.0.2" 51 | } 52 | }, 53 | "inherits": { 54 | "version": "2.0.3", 55 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 56 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 57 | }, 58 | "minimatch": { 59 | "version": "3.0.4", 60 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 61 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 62 | "requires": { 63 | "brace-expansion": "1.1.11" 64 | } 65 | }, 66 | "once": { 67 | "version": "1.4.0", 68 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 69 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 70 | "requires": { 71 | "wrappy": "1.0.2" 72 | } 73 | }, 74 | "path-is-absolute": { 75 | "version": "1.0.1", 76 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 77 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 78 | }, 79 | "rimraf": { 80 | "version": "2.6.2", 81 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 82 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 83 | "requires": { 84 | "glob": "7.1.2" 85 | } 86 | }, 87 | "wrappy": { 88 | "version": "1.0.2", 89 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 90 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-npm/packages/has-package-lock-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-package-lock-json", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "rimraf": "^2.6.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-yarn/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "npmClient": "yarn", 4 | "packages": ["packages/*"], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-lockfile-yarn", 3 | "version": "0.0.0", 4 | "description": "Fixture of empty lerna projects with yarn.lock", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-yarn/packages/has-not-yarn-lock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-not-yarn-lock", 3 | "version": "0.0.0", 4 | "license": "MIT" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-yarn/packages/has-yarn-lock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-yarn-lock", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "rimraf": "^2.6.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/lockfile-yarn/packages/has-yarn-lock/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | balanced-match@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 8 | 9 | brace-expansion@^1.1.7: 10 | version "1.1.11" 11 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 12 | dependencies: 13 | balanced-match "^1.0.0" 14 | concat-map "0.0.1" 15 | 16 | concat-map@0.0.1: 17 | version "0.0.1" 18 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 19 | 20 | fs.realpath@^1.0.0: 21 | version "1.0.0" 22 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 23 | 24 | glob@^7.0.5: 25 | version "7.1.2" 26 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 27 | dependencies: 28 | fs.realpath "^1.0.0" 29 | inflight "^1.0.4" 30 | inherits "2" 31 | minimatch "^3.0.4" 32 | once "^1.3.0" 33 | path-is-absolute "^1.0.0" 34 | 35 | inflight@^1.0.4: 36 | version "1.0.6" 37 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 38 | dependencies: 39 | once "^1.3.0" 40 | wrappy "1" 41 | 42 | inherits@2: 43 | version "2.0.3" 44 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 45 | 46 | minimatch@^3.0.4: 47 | version "3.0.4" 48 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 49 | dependencies: 50 | brace-expansion "^1.1.7" 51 | 52 | once@^1.3.0: 53 | version "1.4.0" 54 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 55 | dependencies: 56 | wrappy "1" 57 | 58 | path-is-absolute@^1.0.0: 59 | version "1.0.1" 60 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 61 | 62 | rimraf@^2.6.2: 63 | version "2.6.2" 64 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 65 | dependencies: 66 | glob "^7.0.5" 67 | 68 | wrappy@1: 69 | version "1.0.2" 70 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 71 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/fixtures/not-lerna/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-yarn-workspaces", 3 | "version": "1.0.0", 4 | "description": "Fixture of empty basic npm package", 5 | "main": "index.js", 6 | "author": "", 7 | "license": "ISC" 8 | } 9 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import path from "path"; 5 | import Npm from "@hothouse/client-npm"; 6 | import Yarn from "@hothouse/client-yarn"; 7 | import Lerna from "../src/index"; 8 | 9 | test("Lerna#match should returns true if directory have lerna.json", async () => { 10 | const lerna = new Lerna(); 11 | assert.ok(await lerna.match(path.join(__dirname, "fixtures", "is-lerna"))); 12 | }); 13 | test("Lerna#match should returns true if directory not have lerna.json", async () => { 14 | const lerna = new Lerna(); 15 | assert.ok( 16 | !(await lerna.match(path.join(__dirname, "fixtures", "not-lerna"))) 17 | ); 18 | }); 19 | 20 | test("Lerna#getPackages should include top-level package.json", async () => { 21 | const lerna = new Lerna(); 22 | const pkg = path.join(__dirname, "fixtures", "lockfile-npm"); 23 | const expected = [ 24 | path.join(pkg), 25 | path.join(pkg, "packages", "has-not-package-lock-json"), 26 | path.join(pkg, "packages", "has-package-lock-json") 27 | ]; 28 | const actual = await lerna.getPackages(pkg); 29 | expect(expected).toEqual(actual); 30 | }); 31 | 32 | test("Lerna#getChanges should return [prefix/package.json] when lockfile not exists", async () => { 33 | const lerna = new Lerna(); 34 | const prefix = path.join("packages", "has-not-package-lock-json"); 35 | const expected = new Set([path.join(prefix, "package.json")]); 36 | const actual = await lerna.getChanges( 37 | path.join( 38 | __dirname, 39 | "fixtures", 40 | "lockfile-npm", 41 | "packages", 42 | "has-not-package-lock-json" 43 | ), 44 | path.join(__dirname, "fixtures", "lockfile-npm"), 45 | new Npm() 46 | ); 47 | expect(expected).toEqual(actual); 48 | }); 49 | test("Lerna#getChanges should return [prefix/package.json, prefix/package-lock.json] when npmClient=npm lockfile exists", async () => { 50 | const lerna = new Lerna(); 51 | const prefix = path.join("packages", "has-package-lock-json"); 52 | const expected = new Set([ 53 | path.join(prefix, "package.json"), 54 | path.join(prefix, "package-lock.json") 55 | ]); 56 | const actual = await lerna.getChanges( 57 | path.join( 58 | __dirname, 59 | "fixtures", 60 | "lockfile-npm", 61 | "packages", 62 | "has-package-lock-json" 63 | ), 64 | path.join(__dirname, "fixtures", "lockfile-npm"), 65 | new Npm() 66 | ); 67 | expect(expected).toEqual(actual); 68 | }); 69 | test("Lerna#getChanges should return [prefix/package.json] when npmClient=yarn lockfile not exists", async () => { 70 | const lerna = new Lerna(); 71 | const prefix = path.join("packages", "has-not-yarn-lock"); 72 | const expected = new Set([path.join(prefix, "package.json")]); 73 | const actual = await lerna.getChanges( 74 | path.join( 75 | __dirname, 76 | "fixtures", 77 | "lockfile-yarn", 78 | "packages", 79 | "has-not-yarn-lock" 80 | ), 81 | path.join(__dirname, "fixtures", "lockfile-yarn"), 82 | new Yarn() 83 | ); 84 | expect(expected).toEqual(actual); 85 | }); 86 | test("Lerna#getChanges should return [prefix/package.json, prefix/yarn.lock] when npmClient=yarn lockfile exists", async () => { 87 | const lerna = new Lerna(); 88 | const prefix = path.join("packages", "has-yarn-lock"); 89 | const expected = new Set([ 90 | path.join(prefix, "package.json"), 91 | path.join(prefix, "yarn.lock") 92 | ]); 93 | const actual = await lerna.getChanges( 94 | path.join( 95 | __dirname, 96 | "fixtures", 97 | "lockfile-yarn", 98 | "packages", 99 | "has-yarn-lock" 100 | ), 101 | path.join(__dirname, "fixtures", "lockfile-yarn"), 102 | new Yarn() 103 | ); 104 | expect(expected).toEqual(actual); 105 | }); 106 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/monorepo-lerna", 3 | "version": "0.4.13", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 11 | }, 12 | "brace-expansion": { 13 | "version": "1.1.11", 14 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 15 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 16 | "requires": { 17 | "balanced-match": "^1.0.0", 18 | "concat-map": "0.0.1" 19 | } 20 | }, 21 | "concat-map": { 22 | "version": "0.0.1", 23 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 24 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 25 | }, 26 | "debug": { 27 | "version": "4.0.1", 28 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", 29 | "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", 30 | "requires": { 31 | "ms": "^2.1.1" 32 | } 33 | }, 34 | "fs.realpath": { 35 | "version": "1.0.0", 36 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 37 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 38 | }, 39 | "glob": { 40 | "version": "7.1.2", 41 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 42 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 43 | "requires": { 44 | "fs.realpath": "^1.0.0", 45 | "inflight": "^1.0.4", 46 | "inherits": "2", 47 | "minimatch": "^3.0.4", 48 | "once": "^1.3.0", 49 | "path-is-absolute": "^1.0.0" 50 | } 51 | }, 52 | "inflight": { 53 | "version": "1.0.6", 54 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 55 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 56 | "requires": { 57 | "once": "^1.3.0", 58 | "wrappy": "1" 59 | } 60 | }, 61 | "inherits": { 62 | "version": "2.0.3", 63 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 64 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 65 | }, 66 | "minimatch": { 67 | "version": "3.0.4", 68 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 69 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 70 | "requires": { 71 | "brace-expansion": "^1.1.7" 72 | } 73 | }, 74 | "ms": { 75 | "version": "2.1.1", 76 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 77 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 78 | }, 79 | "once": { 80 | "version": "1.4.0", 81 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 82 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 83 | "requires": { 84 | "wrappy": "1" 85 | } 86 | }, 87 | "path-is-absolute": { 88 | "version": "1.0.1", 89 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 90 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 91 | }, 92 | "wrappy": { 93 | "version": "1.0.2", 94 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 95 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/monorepo-lerna", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.4.13", 7 | "description": "Handle repository structure with Lerna", 8 | "main": "./dist/index.js", 9 | "scripts": { 10 | "prepare": "npm run build", 11 | "build": "../../../node_modules/.bin/babel src --out-dir dist", 12 | "test": "../../../node_modules/.bin/jest --coverage" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Leko ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Leko/hothouse/issues" 21 | }, 22 | "homepage": "https://github.com/Leko/hothouse#readme", 23 | "dependencies": { 24 | "debug": "^4.0.1", 25 | "glob": "^7.1.2" 26 | }, 27 | "devDependencies": { 28 | "@hothouse/client-npm": "^0.4.13", 29 | "@hothouse/client-yarn": "^0.4.13", 30 | "@hothouse/types": "^0.4.13" 31 | }, 32 | "babel": { 33 | "extends": "../../../.babelrc" 34 | }, 35 | "collectCoverageFrom": [ 36 | "src/*.{js,jsx}" 37 | ], 38 | "gitHead": "97cee2b39df40662cb9a2767e8c047066b6c9a98" 39 | } 40 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-lerna/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from "fs"; 3 | import path from "path"; 4 | import cp from "child_process"; 5 | import glob from "glob"; 6 | import type { Structure, PackageManager } from "@hothouse/types"; 7 | 8 | const debug = require("debug")("hothouse:Structure:Lerna"); 9 | 10 | class Lerna implements Structure { 11 | async match(directory: string): Promise { 12 | return fs.existsSync(path.join(directory, "lerna.json")); 13 | } 14 | 15 | async getPackages(directory: string): Promise> { 16 | // $FlowFixMe(dynamic-require) 17 | const settings = require(path.join(directory, "lerna.json")); 18 | const children = settings.packages.reduce((acc, pattern) => { 19 | const prefix = path.join(directory, pattern); 20 | return acc.concat(glob.sync(prefix, { absolute: true })); 21 | }, []); 22 | return [directory, ...children].filter(packagePath => { 23 | const isPackage = fs.existsSync(path.join(packagePath, "package.json")); 24 | if (!isPackage) { 25 | debug( 26 | `${path.relative( 27 | directory, 28 | packagePath 29 | )} is not a npm package. Ignored` 30 | ); 31 | } 32 | return isPackage; 33 | }); 34 | } 35 | 36 | async install( 37 | packageDirectory: string, 38 | rootDirectory: string, 39 | npmClient: PackageManager 40 | ): Promise> { 41 | const result = cp.spawnSync("npx", ["lerna", "bootstrap"], { 42 | encoding: "utf8", 43 | cwd: rootDirectory, 44 | stdio: "inherit" 45 | }); 46 | if (result.error) { 47 | throw result.error; 48 | } 49 | if (result.status !== 0) { 50 | throw new Error(result.stderr); 51 | } 52 | 53 | return this.getChanges(packageDirectory, rootDirectory, npmClient); 54 | } 55 | 56 | async getChanges( 57 | packageDirectory: string, 58 | rootDirectory: string, 59 | npmClient: PackageManager 60 | ): Promise> { 61 | // #88 package-lock.json should not be added nor committed if not exist 62 | return new Set( 63 | [ 64 | path.join(packageDirectory, "package.json"), 65 | path.join(packageDirectory, npmClient.getLockFileName()) 66 | ] 67 | .filter(fs.existsSync) 68 | .map(p => path.relative(rootDirectory, p)) 69 | ); 70 | } 71 | } 72 | 73 | module.exports = Lerna; 74 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/README.md: -------------------------------------------------------------------------------- 1 | # @hothouse/monorepo-yarn-workspaces 2 | 3 | Internal package for [hothouse](https://github.com/Leko/hothouse). 4 | 5 | TODO: Add more information 6 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/__tests__/fixtures/has-yarn-workspaces-with-lockfile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-yarn-workspaces-with-lockfile", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Fixture of empty yarn workspaces projects", 6 | "main": "index.js", 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "dependencies": { 11 | "rimraf": "^2.6.2" 12 | }, 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/__tests__/fixtures/has-yarn-workspaces-with-lockfile/packages/child-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-a", 3 | "version": "0.0.0", 4 | "license": "MIT" 5 | } 6 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/__tests__/fixtures/has-yarn-workspaces-with-lockfile/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | balanced-match@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 8 | 9 | brace-expansion@^1.1.7: 10 | version "1.1.11" 11 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 12 | dependencies: 13 | balanced-match "^1.0.0" 14 | concat-map "0.0.1" 15 | 16 | concat-map@0.0.1: 17 | version "0.0.1" 18 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 19 | 20 | fs.realpath@^1.0.0: 21 | version "1.0.0" 22 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 23 | 24 | glob@^7.0.5: 25 | version "7.1.2" 26 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 27 | dependencies: 28 | fs.realpath "^1.0.0" 29 | inflight "^1.0.4" 30 | inherits "2" 31 | minimatch "^3.0.4" 32 | once "^1.3.0" 33 | path-is-absolute "^1.0.0" 34 | 35 | inflight@^1.0.4: 36 | version "1.0.6" 37 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 38 | dependencies: 39 | once "^1.3.0" 40 | wrappy "1" 41 | 42 | inherits@2: 43 | version "2.0.3" 44 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 45 | 46 | minimatch@^3.0.4: 47 | version "3.0.4" 48 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 49 | dependencies: 50 | brace-expansion "^1.1.7" 51 | 52 | once@^1.3.0: 53 | version "1.4.0" 54 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 55 | dependencies: 56 | wrappy "1" 57 | 58 | path-is-absolute@^1.0.0: 59 | version "1.0.1" 60 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 61 | 62 | rimraf@^2.6.2: 63 | version "2.6.2" 64 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 65 | dependencies: 66 | glob "^7.0.5" 67 | 68 | wrappy@1: 69 | version "1.0.2" 70 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 71 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/__tests__/fixtures/is-yarn-workspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-yarn-workspaces", 3 | "version": "1.0.0", 4 | "description": "Fixture of empty yarn workspaces projects", 5 | "main": "index.js", 6 | "workspaces": [], 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/__tests__/fixtures/not-yarn-workspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "not-yarn-workspaces", 3 | "version": "1.0.0", 4 | "description": "Fixture of empty yarn workspaces projects", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import path from "path"; 5 | import YarnWorkspaces from "../src/index"; 6 | 7 | test("YarnWorkspaces#match should returns true if directory have package.json and workspaces field", async () => { 8 | const yarnWorkspaces = new YarnWorkspaces(); 9 | assert.ok( 10 | await yarnWorkspaces.match( 11 | path.join(__dirname, "fixtures", "is-yarn-workspaces") 12 | ) 13 | ); 14 | }); 15 | test("YarnWorkspaces#match should returns false if directory not have package.json", async () => { 16 | const yarnWorkspaces = new YarnWorkspaces(); 17 | assert.ok( 18 | !(await yarnWorkspaces.match( 19 | path.join(__dirname, "fixtures", "not-yarn-workspaces") 20 | )) 21 | ); 22 | }); 23 | test("YarnWorkspaces#match must returns false if directory not have package.json", async () => { 24 | const yarnWorkspaces = new YarnWorkspaces(); 25 | assert.ok( 26 | !(await yarnWorkspaces.match( 27 | path.join(__dirname, "fixtures", "not-exists-path") 28 | )) 29 | ); 30 | }); 31 | 32 | test("YarnWorkspaces#getPackages should return package when lockfile exists", async () => { 33 | const yarnWorkspaces = new YarnWorkspaces(); 34 | const prefix = path.join( 35 | __dirname, 36 | "fixtures", 37 | "has-yarn-workspaces-with-lockfile" 38 | ); 39 | const expected = [ 40 | path.join(prefix), 41 | path.join(prefix, "packages", "child-a") 42 | ]; 43 | const actual = await yarnWorkspaces.getPackages(prefix); 44 | expect(expected).toEqual(actual); 45 | }); 46 | 47 | test("YarnWorkspaces#getChanges should return [package.json] when lockfile not exists", async () => { 48 | const yarnWorkspaces = new YarnWorkspaces(); 49 | const expected = new Set(["package.json"]); 50 | const actual = await yarnWorkspaces.getChanges( 51 | path.join(__dirname, "fixtures", "is-yarn-workspaces"), 52 | path.join(__dirname, "fixtures", "is-yarn-workspaces") 53 | ); 54 | expect(expected).toEqual(actual); 55 | }); 56 | test("YarnWorkspaces#getChanges should return [package.json, yarn.lock] when lockfile exists", async () => { 57 | const yarnWorkspaces = new YarnWorkspaces(); 58 | const expected = new Set([ 59 | path.join("packages", "child-a", "package.json"), 60 | "yarn.lock" 61 | ]); 62 | const actual = await yarnWorkspaces.getChanges( 63 | path.join( 64 | __dirname, 65 | "fixtures", 66 | "has-yarn-workspaces-with-lockfile", 67 | "packages", 68 | "child-a" 69 | ), 70 | path.join(__dirname, "fixtures", "has-yarn-workspaces-with-lockfile") 71 | ); 72 | expect(expected).toEqual(actual); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/monorepo-yarn-workspaces", 3 | "version": "0.4.13", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 11 | }, 12 | "brace-expansion": { 13 | "version": "1.1.11", 14 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 15 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 16 | "requires": { 17 | "balanced-match": "^1.0.0", 18 | "concat-map": "0.0.1" 19 | } 20 | }, 21 | "concat-map": { 22 | "version": "0.0.1", 23 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 24 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 25 | }, 26 | "debug": { 27 | "version": "4.0.1", 28 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", 29 | "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", 30 | "requires": { 31 | "ms": "^2.1.1" 32 | } 33 | }, 34 | "fs.realpath": { 35 | "version": "1.0.0", 36 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 37 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 38 | }, 39 | "glob": { 40 | "version": "7.1.2", 41 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 42 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 43 | "requires": { 44 | "fs.realpath": "^1.0.0", 45 | "inflight": "^1.0.4", 46 | "inherits": "2", 47 | "minimatch": "^3.0.4", 48 | "once": "^1.3.0", 49 | "path-is-absolute": "^1.0.0" 50 | } 51 | }, 52 | "inflight": { 53 | "version": "1.0.6", 54 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 55 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 56 | "requires": { 57 | "once": "^1.3.0", 58 | "wrappy": "1" 59 | } 60 | }, 61 | "inherits": { 62 | "version": "2.0.3", 63 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 64 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 65 | }, 66 | "minimatch": { 67 | "version": "3.0.4", 68 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 69 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 70 | "requires": { 71 | "brace-expansion": "^1.1.7" 72 | } 73 | }, 74 | "ms": { 75 | "version": "2.1.1", 76 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 77 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 78 | }, 79 | "once": { 80 | "version": "1.4.0", 81 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 82 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 83 | "requires": { 84 | "wrappy": "1" 85 | } 86 | }, 87 | "path-is-absolute": { 88 | "version": "1.0.1", 89 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 90 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 91 | }, 92 | "wrappy": { 93 | "version": "1.0.2", 94 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 95 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/monorepo-yarn-workspaces", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.4.13", 7 | "description": "Handle repository structure with Yarn workspaces", 8 | "main": "./dist/index.js", 9 | "scripts": { 10 | "prepare": "npm run build", 11 | "build": "../../../node_modules/.bin/babel src --out-dir dist", 12 | "test": "../../../node_modules/.bin/jest --coverage" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Leko ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Leko/hothouse/issues" 21 | }, 22 | "homepage": "https://github.com/Leko/hothouse#readme", 23 | "dependencies": { 24 | "debug": "^4.0.1", 25 | "glob": "^7.1.2" 26 | }, 27 | "devDependencies": { 28 | "@hothouse/types": "^0.4.13" 29 | }, 30 | "babel": { 31 | "extends": "../../../.babelrc" 32 | }, 33 | "collectCoverageFrom": [ 34 | "src/*.{js,jsx}" 35 | ], 36 | "gitHead": "97cee2b39df40662cb9a2767e8c047066b6c9a98" 37 | } 38 | -------------------------------------------------------------------------------- /packages/@hothouse/monorepo-yarn-workspaces/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from "fs"; 3 | import path from "path"; 4 | import glob from "glob"; 5 | import type { Structure, PackageManager } from "@hothouse/types"; 6 | 7 | const debug = require("debug")("hothouse:Structure:YarnWorkspaces"); 8 | 9 | class YarnWorkspaces implements Structure { 10 | async match(directory: string): Promise { 11 | if (!fs.existsSync(path.join(directory, "package.json"))) { 12 | return false; 13 | } 14 | // $FlowFixMe(dynamic-require) 15 | const pkg = require(path.join(directory, "package.json")); 16 | return !!pkg.workspaces; 17 | } 18 | 19 | async getPackages(directory: string): Promise> { 20 | // $FlowFixMe(dynamic-require) 21 | const settings = require(path.join(directory, "package.json")); 22 | const globs = Array.isArray(settings.workspaces) 23 | ? settings.workspaces 24 | : settings.workspaces.packages; 25 | const children = globs.reduce((acc, pattern) => { 26 | const prefix = path.join(directory, pattern); 27 | return acc.concat(glob.sync(prefix, { absolute: true })); 28 | }, []); 29 | return [directory, ...children].filter(packagePath => { 30 | const isPackage = fs.existsSync(path.join(packagePath, "package.json")); 31 | if (!isPackage) { 32 | debug( 33 | `${path.relative( 34 | directory, 35 | packagePath 36 | )} is not a npm package. Ignored` 37 | ); 38 | } 39 | return isPackage; 40 | }); 41 | } 42 | 43 | async install( 44 | packageDirectory: string, 45 | rootDirectory: string, 46 | npmClient: PackageManager 47 | ): Promise> { 48 | await npmClient.install(rootDirectory); 49 | return this.getChanges(packageDirectory, rootDirectory); 50 | } 51 | 52 | async getChanges( 53 | packageDirectory: string, 54 | rootDirectory: string 55 | ): Promise> { 56 | // #88 package-lock.json should not be added nor committed if not exist 57 | return new Set( 58 | [ 59 | path.join(packageDirectory, "package.json"), 60 | path.join(rootDirectory, "yarn.lock") 61 | ] 62 | .filter(fs.existsSync) 63 | .map(p => path.relative(rootDirectory, p)) 64 | ); 65 | } 66 | } 67 | 68 | module.exports = YarnWorkspaces; 69 | -------------------------------------------------------------------------------- /packages/@hothouse/types/README.md: -------------------------------------------------------------------------------- 1 | # @hothouse/types 2 | 3 | Internal package for [hothouse](https://github.com/Leko/hothouse). 4 | 5 | TODO: Add more information 6 | -------------------------------------------------------------------------------- /packages/@hothouse/types/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Semver = string; 4 | export type Outdated = {| 5 | current: Semver, 6 | wanted: Semver, 7 | latest: Semver 8 | |}; 9 | 10 | export type Update = {| 11 | name: string, 12 | current: Semver, 13 | currentRange: string, 14 | latest: Semver, 15 | dev: boolean 16 | |}; 17 | export type Updates = Array; 18 | 19 | export type UpdateDetail = {| 20 | +name: string, 21 | +current: Semver, 22 | +currentRange: string, 23 | +latest: Semver, 24 | +dev: boolean, 25 | +repositoryUrl: ?string, 26 | +compareUrl: ?string, 27 | +releaseNote: ?string 28 | |}; 29 | export type UpdateDetails = Array; 30 | 31 | export type PullRequest = {| 32 | title: string, 33 | url: string 34 | |}; 35 | export type ApplyResult = {| 36 | pullRequest: PullRequest 37 | |}; 38 | 39 | export interface PackageManager { 40 | match(directory: string): Promise; 41 | getLockFileName(): string; 42 | getUpdates(packageDirectory: string): Promise; 43 | getPackageMeta(packageName: string): Promise; 44 | install(packageDirectory: string): Promise; 45 | } 46 | 47 | export interface Structure { 48 | match(directory: string): Promise; 49 | getPackages(directory: string): Promise>; 50 | install( 51 | packageDirectory: string, 52 | rootDirectory: string, 53 | npmClient: PackageManager 54 | ): Promise>; 55 | } 56 | 57 | export interface Hosting { 58 | match(repositoryUrl: string): Promise; 59 | shaToTag(token: string, repositoryUrl: string, sha: string): Promise; 60 | tagExists( 61 | token: string, 62 | repositoryUrl: string, 63 | tag: string 64 | ): Promise; 65 | tagToReleaseNote( 66 | token: string, 67 | repositoryUrl: string, 68 | tag: string 69 | ): Promise; 70 | getCompareUrl( 71 | token: string, 72 | repositoryUrl: string, 73 | base: string, 74 | head: string 75 | ): Promise; 76 | createPullRequest( 77 | token: string, 78 | repositoryUrl: string, 79 | base: string, 80 | head: string, 81 | title: string, 82 | body: string 83 | ): Promise; 84 | getDefaultBranch(token: string, repositoryUrl: string): Promise; 85 | } 86 | 87 | export interface GitImpl { 88 | add(...paths: Array): Promise; 89 | checkout(branchName: string): Promise; 90 | createBranch(branchName: string): Promise; 91 | commit(message: string): Promise; 92 | push(token: string, remoteUrl: string, ref: string): Promise; 93 | getCurrentBranch(): Promise; 94 | inBranch(branchName: string, fn: () => any): Promise; 95 | } 96 | 97 | export interface Reporter { 98 | reportError(Error): Promise; 99 | reportUpdates(cwd: string, allUpdates: { [string]: Updates }): Promise; 100 | reportApplyResult( 101 | cwd: string, 102 | applyResult: Array 103 | ): Promise; 104 | } 105 | -------------------------------------------------------------------------------- /packages/@hothouse/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hothouse/types", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.4.13", 7 | "description": "Internal types for hothouse", 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "echo 'Does nothing' && exit 0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/Leko/hothouse.git" 15 | }, 16 | "keywords": [ 17 | "hothouse", 18 | "flowtype" 19 | ], 20 | "author": "Leko ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Leko/hothouse/issues" 24 | }, 25 | "homepage": "https://github.com/Leko/hothouse#readme", 26 | "gitHead": "97cee2b39df40662cb9a2767e8c047066b6c9a98" 27 | } 28 | -------------------------------------------------------------------------------- /packages/hothouse/README.md: -------------------------------------------------------------------------------- 1 | # Hothouse 2 | 3 | [![npm](https://img.shields.io/npm/v/hothouse.svg)](https://www.npmjs.com/package/hothouse) 4 | [![license](https://img.shields.io/github/license/Leko/hothouse.svg)](https://opensource.org/licenses/MIT) 5 | [![CircleCI](https://circleci.com/gh/Leko/hothouse.svg?style=svg)](https://circleci.com/gh/Leko/hothouse) 6 | [![codecov](https://codecov.io/gh/Leko/hothouse/branch/master/graph/badge.svg)](https://codecov.io/gh/Leko/hothouse) 7 | 8 | Continuous dependency update for Node.js project like [Greenkeeper](https://greenkeeper.io/). 9 | 10 | ![image](https://user-images.githubusercontent.com/1424963/42268316-23121072-7fb6-11e8-8238-6a97ea7c3779.png) 11 | 12 | For more details, please refer [GitHub repository](https://github.com/Leko/hothouse) 13 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/BlackList.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import BlackList from "../src/BlackList"; 5 | 6 | test("BlackList#match can use exact string", () => { 7 | const list = new BlackList(["react-native", "react-native-svg"]); 8 | assert(list.match("react-native")); 9 | assert(list.match("react-native-svg")); 10 | assert(!list.match("react")); 11 | }); 12 | test("BlackList#match can use pattern string", () => { 13 | const list = new BlackList(["react-native*"]); 14 | assert(list.match("react-native")); 15 | assert(list.match("react-native-svg")); 16 | assert(!list.match("react")); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/Engine.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import type { Updates, ApplyResult, Reporter } from "@hothouse/types"; 5 | import Engine from "../src/Engine"; 6 | 7 | const gitImpl = { 8 | async add(...paths: Array): Promise {}, 9 | async checkout(branchName: string): Promise {}, 10 | async createBranch(branchName: string): Promise {}, 11 | async commit(message: string): Promise {}, 12 | async push(token: string, ref?: string): Promise {}, 13 | async getCurrentBranch(): Promise { 14 | return "master"; 15 | }, 16 | async inBranch(branchName: string, fn: () => any): Promise {} 17 | }; 18 | const reporter: Reporter = { 19 | async reportError(Error): Promise {}, 20 | async reportUpdates( 21 | cwd: string, 22 | allUpdates: { [string]: Updates } 23 | ): Promise {}, 24 | async reportApplyResult( 25 | cwd: string, 26 | applyResult: Array 27 | ): Promise {} 28 | }; 29 | 30 | describe("Engine#logPrefix", () => { 31 | test("Engine#logPrefix returns '(dryRun) ' when dryRun=true", () => { 32 | const engine = new Engine({ 33 | concurrency: 1, 34 | token: "xxx", 35 | bail: false, 36 | ignore: [], 37 | perPackage: true, 38 | packageManager: null, 39 | repositoryStructure: null, 40 | dryRun: true, 41 | reporter, 42 | gitImpl 43 | }); 44 | assert.equal(engine.logPrefix, "(dryRun) "); 45 | }); 46 | test("Engine#logPrefix returns empty string when dryRun=false", () => { 47 | const engine = new Engine({ 48 | concurrency: 1, 49 | token: "xxx", 50 | bail: false, 51 | ignore: [], 52 | perPackage: true, 53 | packageManager: null, 54 | repositoryStructure: null, 55 | dryRun: false, 56 | reporter, 57 | gitImpl 58 | }); 59 | assert.equal(engine.logPrefix, ""); 60 | }); 61 | }); 62 | 63 | // describe("Engine#getUpdates", () => { 64 | // const mock = (returns: Updates) => 65 | // new class MockNpmClient extends Npm { 66 | // async getUpdates(packageDirectory: string): Promise { 67 | // return returns; 68 | // } 69 | // }(); 70 | 71 | // const assertFilter = async ( 72 | // updates: Updates, 73 | // expected: Updates 74 | // ): Promise => { 75 | // const engine = new Engine({ 76 | // concurrency: 1, 77 | // token: "xxx", 78 | // bail: false, 79 | // ignore: [], 80 | // perPackage: true, 81 | // packageManager: null, 82 | // repositoryStructure: null, 83 | // dryRun: true, 84 | // reporter, 85 | // gitImpl 86 | // }); 87 | 88 | // assert.deepStrictEqual( 89 | // await engine.getUpdates(mock(updates), "", []), 90 | // expected 91 | // ); 92 | // }; 93 | 94 | // test("Engine#getUpdates must ignore revert updates", async () => { 95 | // const expected = { 96 | // name: "prerelease+fixed", 97 | // current: "1.2.3-beta.30", 98 | // currentRange: "1.2.3-beta.30", 99 | // latest: "1.2.3-beta.32", 100 | // dev: false 101 | // }; 102 | 103 | // await assertFilter( 104 | // [ 105 | // { 106 | // name: "prerelease+tilde(ignored)", 107 | // current: "1.2.3-beta.30", 108 | // currentRange: "1.2.3-beta.30", 109 | // latest: "1.2.3-beta.29", 110 | // dev: false 111 | // }, 112 | // expected 113 | // ], 114 | // [expected] 115 | // ); 116 | // }); 117 | 118 | // test("Engine#getUpdates must ignore covered updates (prerelease)", async () => { 119 | // const expected = { 120 | // name: "prerelease+fixed", 121 | // current: "1.2.3-beta.30", 122 | // currentRange: "1.2.3-beta.30", 123 | // latest: "1.2.3-beta.32", 124 | // dev: false 125 | // }; 126 | 127 | // await assertFilter( 128 | // [ 129 | // { 130 | // name: "prerelease+tilde(ignored)", 131 | // current: "1.2.3-beta.30", 132 | // currentRange: "~1.2.3-beta.30", 133 | // latest: "1.2.3-beta.32", 134 | // dev: false 135 | // }, 136 | // expected 137 | // ], 138 | // [expected] 139 | // ); 140 | // }); 141 | 142 | // test("Engine#getUpdates must ignore covered updates (patch)", async () => { 143 | // const expected = [ 144 | // { 145 | // name: "patch+fixed", 146 | // current: "1.2.3", 147 | // currentRange: "1.2.3", 148 | // latest: "1.2.4", 149 | // dev: false 150 | // } 151 | // ]; 152 | 153 | // await assertFilter( 154 | // [ 155 | // { 156 | // name: "patch+tilde(ignored)", 157 | // current: "1.2.3", 158 | // currentRange: "~1.2.3", 159 | // latest: "1.2.4", 160 | // dev: false 161 | // }, 162 | // { 163 | // name: "patch+hat(ignored)", 164 | // current: "1.2.3", 165 | // currentRange: "^1.2.3", 166 | // latest: "1.2.4", 167 | // dev: false 168 | // }, 169 | // ...expected 170 | // ], 171 | // expected 172 | // ); 173 | // }); 174 | 175 | // test("Engine#getUpdates must ignore covered updates (minor)", async () => { 176 | // const expected = [ 177 | // { 178 | // name: "minor+fixed", 179 | // current: "1.2.3", 180 | // currentRange: "1.2.3", 181 | // latest: "1.3.0", 182 | // dev: false 183 | // }, 184 | // { 185 | // name: "minor+tilde(ignored)", 186 | // current: "1.2.3", 187 | // currentRange: "~1.2.3", 188 | // latest: "1.3.0", 189 | // dev: false 190 | // } 191 | // ]; 192 | 193 | // await assertFilter( 194 | // [ 195 | // { 196 | // name: "minor+hat(ignored)", 197 | // current: "1.2.3", 198 | // currentRange: "^1.2.3", 199 | // latest: "1.3.0", 200 | // dev: false 201 | // }, 202 | // ...expected 203 | // ], 204 | // expected 205 | // ); 206 | // }); 207 | 208 | // test("Engine#getUpdates must ignore covered updates (major)", async () => { 209 | // const expected = [ 210 | // { 211 | // name: "major+fixed", 212 | // current: "1.2.3", 213 | // currentRange: "1.2.3", 214 | // latest: "2.0.0", 215 | // dev: false 216 | // }, 217 | // { 218 | // name: "major+tilde(ignored)", 219 | // current: "1.2.3", 220 | // currentRange: "~1.2.3", 221 | // latest: "2.0.0", 222 | // dev: false 223 | // }, 224 | // { 225 | // name: "major+hat(ignored)", 226 | // current: "1.2.3", 227 | // currentRange: "^1.2.3", 228 | // latest: "2.0.0", 229 | // dev: false 230 | // } 231 | // ]; 232 | 233 | // await assertFilter( 234 | // [ 235 | // { 236 | // name: "major+gt(ignored)", 237 | // current: "1.2.3", 238 | // currentRange: ">= 1.2.3", 239 | // latest: "2.0.0", 240 | // dev: false 241 | // }, 242 | // ...expected 243 | // ], 244 | // expected 245 | // ); 246 | // }); 247 | // }); 248 | 249 | describe("Engine#inBranch", () => { 250 | test("Engine#inBranch must not call GitImpl.inBranch when dryRun=true", () => { 251 | const engine = new Engine({ 252 | concurrency: 1, 253 | token: "xxx", 254 | bail: false, 255 | ignore: [], 256 | perPackage: true, 257 | packageManager: null, 258 | repositoryStructure: null, 259 | dryRun: true, 260 | reporter, 261 | gitImpl 262 | }); 263 | const spy = jest.spyOn(gitImpl, "inBranch"); 264 | engine.inBranch("x", () => {}); 265 | 266 | expect(spy).not.toHaveBeenCalled(); 267 | 268 | spy.mockReset(); 269 | spy.mockRestore(); 270 | }); 271 | test("Engine#inBranch must call GitImpl.inBranch when dryRun=false", () => { 272 | const engine = new Engine({ 273 | concurrency: 1, 274 | token: "xxx", 275 | bail: false, 276 | ignore: [], 277 | perPackage: true, 278 | packageManager: null, 279 | repositoryStructure: null, 280 | dryRun: false, 281 | reporter, 282 | gitImpl 283 | }); 284 | const spy = jest.spyOn(gitImpl, "inBranch"); 285 | engine.inBranch("x", () => {}); 286 | 287 | expect(spy).toHaveBeenCalled(); 288 | 289 | spy.mockReset(); 290 | spy.mockRestore(); 291 | }); 292 | }); 293 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/Hosting/GitHub.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import { parseRepositoryUrl } from "../../src/Hosting/GitHub"; 5 | 6 | test("parseRepositoryUrl can git+ssh url", () => { 7 | const [owner, repo] = parseRepositoryUrl( 8 | "git+ssh://git@github.com/Leko/hothouse.git" 9 | ); 10 | assert.deepStrictEqual([owner, repo], ["Leko", "hothouse"]); 11 | }); 12 | test("parseRepositoryUrl can basic http url", () => { 13 | const [owner, repo] = parseRepositoryUrl("https://github.com/Leko/hothouse"); 14 | assert.deepStrictEqual([owner, repo], ["Leko", "hothouse"]); 15 | }); 16 | test("parseRepositoryUrl can parse monorepo url", () => { 17 | const [owner, repo] = parseRepositoryUrl( 18 | "https://github.com/babel/babel/tree/master/packages/babel-cli" 19 | ); 20 | assert.deepStrictEqual([owner, repo], ["babel", "babel"]); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/Package.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import Package from "../src/Package"; 5 | 6 | test("Package.createFromDirectory can instantiate with valid directory", () => { 7 | const pkg = Package.createFromDirectory("../"); 8 | assert.equal(pkg.constructor, Package); 9 | }); 10 | test("Package.createFromDirectory cannot instantiate with invalid directory", () => { 11 | assert.throws(() => Package.createFromDirectory("./not-exists-path")); 12 | }); 13 | 14 | test("Package can instantiate with valid package path", () => { 15 | const pkg = new Package("../package.json"); 16 | assert.equal(pkg.constructor, Package); 17 | }); 18 | test("Package cannot instantiate with invalid package path", () => { 19 | assert.throws(() => new Package("./not-exists-path.json")); 20 | }); 21 | 22 | test("Package#getRepositoryHttpsUrl can resolve basic repository.url as https", () => { 23 | const pkg = new Package("../package.json"); 24 | const url = "git+ssh://git@github.com/Leko/hothouse.git"; 25 | pkg.pkgJson.repository = { 26 | type: "git", 27 | url 28 | }; 29 | assert.equal( 30 | pkg.getRepositoryHttpsUrl(), 31 | "https://github.com/Leko/hothouse.git" 32 | ); 33 | }); 34 | test("Package#getRepositoryHttpsUrl can resolve https protocol", () => { 35 | const pkg = new Package("../package.json"); 36 | pkg.pkgJson.repository = "https://github.com/Leko/hothouse.git"; 37 | assert.equal( 38 | pkg.getRepositoryHttpsUrl(), 39 | "https://github.com/Leko/hothouse.git" 40 | ); 41 | }); 42 | test("Package#getRepositoryHttpsUrl can resolve git+https protocol", () => { 43 | const pkg = new Package("../package.json"); 44 | pkg.pkgJson.repository = "git+https://github.com/Leko/hothouse.git"; 45 | assert.equal( 46 | pkg.getRepositoryHttpsUrl(), 47 | "https://github.com/Leko/hothouse.git" 48 | ); 49 | }); 50 | test("Package#getRepositoryHttpsUrl can resolve shortcut format (parse as GitHub) as https", () => { 51 | const pkg = new Package("../package.json"); 52 | pkg.pkgJson.repository = "Leko/hothouse"; 53 | assert.equal( 54 | pkg.getRepositoryHttpsUrl(), 55 | "https://github.com/Leko/hothouse.git" 56 | ); 57 | }); 58 | test("Package#getRepositoryHttpsUrl can resolve github shortcut format", () => { 59 | const pkg = new Package("../package.json"); 60 | pkg.pkgJson.repository = "github:Leko/hothouse"; 61 | assert.equal( 62 | pkg.getRepositoryHttpsUrl(), 63 | "https://github.com/Leko/hothouse.git" 64 | ); 65 | }); 66 | test("Package#getRepositoryHttpsUrl throws when repository.url is not defined with pkgJsonPath", () => { 67 | const pkg = new Package("../package.json"); 68 | delete pkg.pkgJsonNormalized.repository; 69 | assert.throws( 70 | () => pkg.getRepositoryHttpsUrl(), 71 | new RegExp(`repository.url is not defined in ../package.json`) 72 | ); 73 | }); 74 | test("Package#getRepositoryHttpsUrl throws when repository.url is not defined without pkgJsonPath", () => { 75 | const pkgJson = require("../package.json"); 76 | const pkg = new Package(pkgJson); 77 | delete pkg.pkgJsonNormalized.repository; 78 | assert.throws( 79 | () => pkg.getRepositoryHttpsUrl(), 80 | new RegExp(`repository.url is not defined in ${pkgJson.name}`) 81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/SinglePackage.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import path from "path"; 5 | import Npm from "@hothouse/client-npm"; 6 | import Yarn from "@hothouse/client-yarn"; 7 | import SinglePackage from "../src/SinglePackage"; 8 | 9 | test("SinglePackage#getChanges should return [package.json] when lockfile not exists", async () => { 10 | const pkg = new SinglePackage(); 11 | const expected = new Set(["package.json"]); 12 | const actual = await pkg.getChanges( 13 | path.join(__dirname, "fixtures", "has-not-package-lock-json"), 14 | new Npm() 15 | ); 16 | assert.deepStrictEqual(actual, expected); 17 | }); 18 | test("SinglePackage#getChanges should return [package.json, package-lock.json] when lockfile exists with Npm", async () => { 19 | const pkg = new SinglePackage(); 20 | const expected = new Set(["package.json", "package-lock.json"]); 21 | const actual = await pkg.getChanges( 22 | path.join(__dirname, "fixtures", "has-package-lock-json"), 23 | new Npm() 24 | ); 25 | assert.deepStrictEqual(actual, expected); 26 | }); 27 | test("SinglePackage#getChanges should return [package.json, yarn.lock] when lockfile exists with Yarn", async () => { 28 | const pkg = new SinglePackage(); 29 | const expected = new Set(["package.json", "yarn.lock"]); 30 | const actual = await pkg.getChanges( 31 | path.join(__dirname, "fixtures", "has-yarn-lock"), 32 | new Yarn() 33 | ); 34 | assert.deepStrictEqual(actual, expected); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/UpdateChunk.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import UpdateChunk, { split } from "../src/UpdateChunk"; 5 | 6 | test("split returns single chunk when perPackage=false", () => { 7 | const allUpdates = { 8 | "/some/package/a": [ 9 | { 10 | name: "pkg-a", 11 | current: "1.2.3", 12 | currentRange: "^1.2.3", 13 | latest: "4.5.6", 14 | dev: true 15 | } 16 | ] 17 | }; 18 | const expected = [new UpdateChunk(allUpdates)]; 19 | assert.deepStrictEqual(split(allUpdates, false), expected); 20 | }); 21 | test("split can split per packages package when perPackage=true", () => { 22 | const allUpdates = { 23 | "/some/package/a": [ 24 | { 25 | name: "pkg-a", 26 | current: "1.2.3", 27 | currentRange: "^1.2.3", 28 | latest: "4.5.6", 29 | dev: true 30 | }, 31 | { 32 | name: "pkg-b", 33 | current: "3.4.5", 34 | currentRange: "^3.4.5", 35 | latest: "6.7.8", 36 | dev: true 37 | } 38 | ] 39 | }; 40 | const expected = [ 41 | new UpdateChunk({ 42 | "/some/package/a": [ 43 | { 44 | name: "pkg-a", 45 | current: "1.2.3", 46 | currentRange: "^1.2.3", 47 | latest: "4.5.6", 48 | dev: true 49 | } 50 | ] 51 | }), 52 | new UpdateChunk({ 53 | "/some/package/a": [ 54 | { 55 | name: "pkg-b", 56 | current: "3.4.5", 57 | currentRange: "^3.4.5", 58 | latest: "6.7.8", 59 | dev: true 60 | } 61 | ] 62 | }) 63 | ]; 64 | assert.deepStrictEqual(split(allUpdates, true), expected); 65 | }); 66 | test("split can split per packages between each multiple package when perPackage=true", () => { 67 | const allUpdates = { 68 | "/some/package/a": [ 69 | { 70 | name: "pkg-a", 71 | current: "1.2.3", 72 | currentRange: "^1.2.3", 73 | latest: "4.5.6", 74 | dev: true 75 | }, 76 | { 77 | name: "pkg-b", 78 | current: "3.4.5", 79 | currentRange: "^3.4.5", 80 | latest: "6.7.8", 81 | dev: true 82 | } 83 | ], 84 | "/some/package/b": [ 85 | { 86 | name: "pkg-b", 87 | current: "1.2.3", 88 | currentRange: "^1.2.3", 89 | latest: "6.7.8", 90 | dev: true 91 | }, 92 | { 93 | name: "pkg-c", 94 | current: "5.6.7", 95 | currentRange: "^5.6.7", 96 | latest: "7.8.9", 97 | dev: true 98 | } 99 | ] 100 | }; 101 | const expected = [ 102 | new UpdateChunk({ 103 | "/some/package/a": [ 104 | { 105 | name: "pkg-a", 106 | current: "1.2.3", 107 | currentRange: "^1.2.3", 108 | latest: "4.5.6", 109 | dev: true 110 | } 111 | ], 112 | "/some/package/b": [] 113 | }), 114 | new UpdateChunk({ 115 | "/some/package/a": [ 116 | { 117 | name: "pkg-b", 118 | current: "3.4.5", 119 | currentRange: "^3.4.5", 120 | latest: "6.7.8", 121 | dev: true 122 | } 123 | ], 124 | "/some/package/b": [ 125 | { 126 | name: "pkg-b", 127 | current: "1.2.3", 128 | currentRange: "^1.2.3", 129 | latest: "6.7.8", 130 | dev: true 131 | } 132 | ] 133 | }), 134 | new UpdateChunk({ 135 | "/some/package/a": [], 136 | "/some/package/b": [ 137 | { 138 | name: "pkg-c", 139 | current: "5.6.7", 140 | currentRange: "^5.6.7", 141 | latest: "7.8.9", 142 | dev: true 143 | } 144 | ] 145 | }) 146 | ]; 147 | assert.deepStrictEqual(split(allUpdates, true), expected); 148 | }); 149 | 150 | test("UpdateChunk#slugify can return hashed string", () => { 151 | const chunk = new UpdateChunk({ 152 | "/some/package/a": [], 153 | "/some/package/b": [ 154 | { 155 | name: "pkg-c", 156 | current: "5.6.7", 157 | currentRange: "^5.6.7", 158 | latest: "7.8.9", 159 | dev: true 160 | } 161 | ] 162 | }); 163 | assert.ok(typeof chunk.slugify(), "string"); 164 | }); 165 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/cli/options.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import options from "../../src/cli/options"; 5 | 6 | const baseOptions = ["-t", "xxx"]; 7 | 8 | test("options can parse --token as string", () => { 9 | const token = "xxx"; 10 | assert.equal(options.parse(["--token", token]).token, token); 11 | }); 12 | test("options throws when --token is missing", () => { 13 | assert.throws(() => 14 | options 15 | .exitProcess(false) 16 | .showHelpOnFail(false) 17 | .parse([]) 18 | ); 19 | }); 20 | 21 | test("options returns perPackage=true when --per-package is missing", () => { 22 | assert.ok(options.parse([...baseOptions]).perPackage); 23 | }); 24 | test("options returns perPackage=false when --no-per-package", () => { 25 | assert.ok(!options.parse([...baseOptions, "--no-per-package"]).perPackage); 26 | }); 27 | 28 | test("options returns bail=true when --bail is missing", () => { 29 | assert.ok(options.parse([...baseOptions]).bail); 30 | }); 31 | test("options can parse --bail as boolean", () => { 32 | assert.equal(options.parse([...baseOptions, "--bail", "false"]).bail, false); 33 | }); 34 | 35 | test("options can parse --package-manager as string", () => { 36 | const packageManager = "hoge"; 37 | assert.equal( 38 | options.parse([...baseOptions, "--package-manager", packageManager]) 39 | .packageManager, 40 | packageManager 41 | ); 42 | }); 43 | 44 | test("options can parse --repository-structure as string", () => { 45 | const repositoryStructure = "hoge"; 46 | assert.equal( 47 | options.parse([ 48 | ...baseOptions, 49 | "--repository-structure", 50 | repositoryStructure 51 | ]).repositoryStructure, 52 | repositoryStructure 53 | ); 54 | }); 55 | 56 | test("options can parse --ignore as CSV", () => { 57 | assert.deepStrictEqual( 58 | options.parse([...baseOptions, "--ignore", "hoge,foo,bar"]).ignore, 59 | ["hoge", "foo", "bar"] 60 | ); 61 | }); 62 | 63 | test("options returns dryRun=false when --dry-run is missing", () => { 64 | assert.ok(!options.parse([...baseOptions]).dryRun); 65 | }); 66 | test("options can parse --dry-run as boolean", () => { 67 | assert.ok(options.parse([...baseOptions, "--dry-run"]).dryRun); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/fixtures/has-not-package-lock-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-not-package-lock-json", 3 | "version": "0.0.0", 4 | "license": "MIT" 5 | } 6 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/fixtures/has-package-lock-json/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-package-lock-json", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 11 | }, 12 | "brace-expansion": { 13 | "version": "1.1.11", 14 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 15 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 16 | "requires": { 17 | "balanced-match": "1.0.0", 18 | "concat-map": "0.0.1" 19 | } 20 | }, 21 | "concat-map": { 22 | "version": "0.0.1", 23 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 24 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 25 | }, 26 | "fs.realpath": { 27 | "version": "1.0.0", 28 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 29 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 30 | }, 31 | "glob": { 32 | "version": "7.1.2", 33 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 34 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 35 | "requires": { 36 | "fs.realpath": "1.0.0", 37 | "inflight": "1.0.6", 38 | "inherits": "2.0.3", 39 | "minimatch": "3.0.4", 40 | "once": "1.4.0", 41 | "path-is-absolute": "1.0.1" 42 | } 43 | }, 44 | "inflight": { 45 | "version": "1.0.6", 46 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 47 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 48 | "requires": { 49 | "once": "1.4.0", 50 | "wrappy": "1.0.2" 51 | } 52 | }, 53 | "inherits": { 54 | "version": "2.0.3", 55 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 56 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 57 | }, 58 | "minimatch": { 59 | "version": "3.0.4", 60 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 61 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 62 | "requires": { 63 | "brace-expansion": "1.1.11" 64 | } 65 | }, 66 | "once": { 67 | "version": "1.4.0", 68 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 69 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 70 | "requires": { 71 | "wrappy": "1.0.2" 72 | } 73 | }, 74 | "path-is-absolute": { 75 | "version": "1.0.1", 76 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 77 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 78 | }, 79 | "rimraf": { 80 | "version": "2.6.2", 81 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 82 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 83 | "requires": { 84 | "glob": "7.1.2" 85 | } 86 | }, 87 | "wrappy": { 88 | "version": "1.0.2", 89 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 90 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/fixtures/has-package-lock-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-package-lock-json", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "rimraf": "^2.6.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/fixtures/has-yarn-lock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "has-yarn-lock", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "rimraf": "^2.6.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/fixtures/has-yarn-lock/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | balanced-match@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 8 | 9 | brace-expansion@^1.1.7: 10 | version "1.1.11" 11 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 12 | dependencies: 13 | balanced-match "^1.0.0" 14 | concat-map "0.0.1" 15 | 16 | concat-map@0.0.1: 17 | version "0.0.1" 18 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 19 | 20 | fs.realpath@^1.0.0: 21 | version "1.0.0" 22 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 23 | 24 | glob@^7.0.5: 25 | version "7.1.2" 26 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 27 | dependencies: 28 | fs.realpath "^1.0.0" 29 | inflight "^1.0.4" 30 | inherits "2" 31 | minimatch "^3.0.4" 32 | once "^1.3.0" 33 | path-is-absolute "^1.0.0" 34 | 35 | inflight@^1.0.4: 36 | version "1.0.6" 37 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 38 | dependencies: 39 | once "^1.3.0" 40 | wrappy "1" 41 | 42 | inherits@2: 43 | version "2.0.3" 44 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 45 | 46 | minimatch@^3.0.4: 47 | version "3.0.4" 48 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 49 | dependencies: 50 | brace-expansion "^1.1.7" 51 | 52 | once@^1.3.0: 53 | version "1.4.0" 54 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 55 | dependencies: 56 | wrappy "1" 57 | 58 | path-is-absolute@^1.0.0: 59 | version "1.0.1" 60 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 61 | 62 | rimraf@^2.6.2: 63 | version "2.6.2" 64 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 65 | dependencies: 66 | glob "^7.0.5" 67 | 68 | wrappy@1: 69 | version "1.0.2" 70 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 71 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/git.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import { tmpdir } from "os"; 5 | import fs from "fs"; 6 | import { sep } from "path"; 7 | import rimraf from "rimraf"; 8 | import git, { spawn } from "../src/git"; 9 | 10 | describe("git", () => { 11 | const workDir = fs.mkdtempSync(`${tmpdir()}${sep}`); 12 | const original = process.cwd(); 13 | beforeEach(() => { 14 | if (!fs.existsSync(workDir)) { 15 | fs.mkdirSync(workDir); 16 | } 17 | process.chdir(workDir); 18 | spawn("git", "init"); 19 | spawn("git", "config", "user.name", "Hothouse tests"); 20 | spawn("git", "config", "user.email", "hothouse@example.com"); 21 | spawn("git", "commit", "--allow-empty", "-m", "Initial commit"); 22 | }); 23 | afterEach(() => { 24 | rimraf.sync(workDir); 25 | process.chdir(original); 26 | }); 27 | 28 | test("git.getCurrentBranch can return current branch", async () => { 29 | assert.strictEqual(await git.getCurrentBranch(), "master"); 30 | }); 31 | test("git.getCurrentBranch can return current branch after checkout another branch", async () => { 32 | const branch = "hoge"; 33 | spawn("git", "checkout", "-b", branch); 34 | assert.strictEqual(await git.getCurrentBranch(), branch); 35 | }); 36 | 37 | test("git.inBranch can checkout current when error occured in callback", async () => { 38 | const currentBranch = await git.getCurrentBranch(); 39 | try { 40 | await git.inBranch("test-branch", async () => { 41 | throw new Error("Some error"); 42 | }); 43 | } catch (error) { 44 | assert.strictEqual(error.message, "Some error"); 45 | assert.strictEqual(await git.getCurrentBranch(), currentBranch); 46 | } 47 | }); 48 | test("git.inBranch can checkout current when callback successful", async () => { 49 | const currentBranch = await git.getCurrentBranch(); 50 | const brannch = "test-branch"; 51 | await git.inBranch(brannch, async () => { 52 | assert.strictEqual(await git.getCurrentBranch(), brannch); 53 | }); 54 | assert.strictEqual(await git.getCurrentBranch(), currentBranch); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/pullRequest.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import { 5 | createPullRequestTitle, 6 | createPullRequestMessage 7 | } from "../src/pullRequest"; 8 | 9 | test("createPullRequestTitle can concat package names format as CSV", () => { 10 | const base = { 11 | current: "1.2.3", 12 | currentRange: "^1.2.3", 13 | latest: "3.4.5", 14 | dev: false, 15 | repositoryUrl: null, 16 | compareUrl: null, 17 | releaseNote: null 18 | }; 19 | 20 | const title = createPullRequestTitle([ 21 | { ...base, name: "hoge" }, 22 | { ...base, name: "@scope/foo" }, 23 | { ...base, name: "bar" } 24 | ]); 25 | assert.strictEqual( 26 | title, 27 | "Update hoge, @scope/foo, bar to the latest version" 28 | ); 29 | }); 30 | 31 | test("createPullRequestMessage must present repository link when repositoryUrl is passed", () => { 32 | const body = createPullRequestMessage([ 33 | { 34 | name: "@scope/pkg", 35 | current: "1.2.3-beta.30", 36 | currentRange: "^1.2.3", 37 | latest: "2.0.0", 38 | dev: false, 39 | repositoryUrl: "https://github.com/pkg/pkg", 40 | compareUrl: null, 41 | releaseNote: null 42 | } 43 | ]); 44 | assert.ok(body.includes("Package: [repository](https://github.com/pkg/pkg)")); 45 | }); 46 | test("createPullRequestMessage must not present repository link when repositoryUrl is null", () => { 47 | const body = createPullRequestMessage([ 48 | { 49 | name: "@scope/pkg", 50 | current: "1.2.3-beta.30", 51 | currentRange: "^1.2.3", 52 | latest: "2.0.0", 53 | dev: false, 54 | repositoryUrl: null, 55 | compareUrl: null, 56 | releaseNote: null 57 | } 58 | ]); 59 | assert.ok( 60 | !body.includes("Package: [repository](https://github.com/pkg/pkg)") 61 | ); 62 | }); 63 | test("createPullRequestMessage must present compare URL when compareUrl is passed", () => { 64 | const body = createPullRequestMessage([ 65 | { 66 | name: "@scope/pkg", 67 | current: "1.2.3-beta.30", 68 | currentRange: "^1.2.3", 69 | latest: "2.0.0", 70 | dev: false, 71 | repositoryUrl: null, 72 | compareUrl: "https://github.com/pkg/pkg/compare/v1.2.3-beta.30...v2.0.0", 73 | releaseNote: null 74 | } 75 | ]); 76 | assert.ok( 77 | body.includes( 78 | "* [compare 1.2.3-beta.30 to 2.0.0 diffs](https://github.com/pkg/pkg/compare/v1.2.3-beta.30...v2.0.0)" 79 | ) 80 | ); 81 | }); 82 | test("createPullRequestMessage must not present compare URL when compareUrl is null", () => { 83 | const body = createPullRequestMessage([ 84 | { 85 | name: "@scope/pkg", 86 | current: "1.2.3-beta.30", 87 | currentRange: "^1.2.3", 88 | latest: "2.0.0", 89 | dev: false, 90 | repositoryUrl: null, 91 | compareUrl: null, 92 | releaseNote: null 93 | } 94 | ]); 95 | assert.ok( 96 | !body.includes( 97 | "* [compare 1.2.3-beta.30 to 2.0.0 diffs](https://github.com/pkg/pkg/compare/v1.2.3-beta.30...v2.0.0)" 98 | ) 99 | ); 100 | }); 101 | test("createPullRequestMessage must present release note when releaseNote is passed", () => { 102 | const body = createPullRequestMessage([ 103 | { 104 | name: "@scope/pkg", 105 | current: "1.2.3-beta.30", 106 | currentRange: "^1.2.3", 107 | latest: "2.0.0", 108 | dev: false, 109 | repositoryUrl: null, 110 | compareUrl: null, 111 | releaseNote: "

Some updates

" 112 | } 113 | ]); 114 | assert.ok(body.includes("
"), body); 115 | assert.ok(body.includes("

Some updates

"), body); 116 | }); 117 | test("createPullRequestMessage must not present release note when releaseNote is null", () => { 118 | const body = createPullRequestMessage([ 119 | { 120 | name: "@scope/pkg", 121 | current: "1.2.3-beta.30", 122 | currentRange: "^1.2.3", 123 | latest: "2.0.0", 124 | dev: false, 125 | repositoryUrl: null, 126 | compareUrl: null, 127 | releaseNote: null 128 | } 129 | ]); 130 | assert.ok(!body.includes("
")); 131 | assert.ok(body.includes("Release note is not available")); 132 | }); 133 | -------------------------------------------------------------------------------- /packages/hothouse/__tests__/utils/semver.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // @flow 3 | import assert from "assert"; 4 | import { replace } from "../../src/utils/semver"; 5 | 6 | test("replace can replace exact semver", () => { 7 | assert.equal(replace("1.2.3", "4.5.6"), "4.5.6"); 8 | }); 9 | test("replace can replace tilde semver range", () => { 10 | assert.equal(replace("~0.11.3", "0.12.0"), "~0.12.0"); 11 | }); 12 | test("replace can replace hat semver range", () => { 13 | assert.equal(replace("^1.13.1", "2.0.0"), "^2.0.0"); 14 | }); 15 | test("replace can replace semver range with prerelease (like Babel)", () => { 16 | assert.equal(replace("^7.0.0-beta.49", "7.0.0-beta.51"), "^7.0.0-beta.51"); 17 | }); 18 | test("replace throws Error when too many diffs", () => { 19 | assert.throws(() => replace("^1.0.0-beta.1", "1.0.0-beta.202")); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/hothouse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hothouse", 3 | "version": "0.4.13", 4 | "description": "Continuous dependency update for Node.js project", 5 | "bin": { 6 | "hothouse": "./dist/cli/index.js" 7 | }, 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "scripts": { 12 | "prepare": "npm run build", 13 | "build": "../../node_modules/.bin/babel --config-file ../../.babelrc src --out-dir dist", 14 | "test": "../../node_modules/.bin/jest --coverage" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+ssh://git@github.com/Leko/hothouse.git" 22 | }, 23 | "keywords": [ 24 | "cli", 25 | "ci", 26 | "dependency", 27 | "update" 28 | ], 29 | "author": "Leko ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/Leko/hothouse/issues" 33 | }, 34 | "homepage": "https://github.com/Leko/hothouse#readme", 35 | "dependencies": { 36 | "@hothouse/client-npm": "^0.4.13", 37 | "@hothouse/client-yarn": "^0.4.13", 38 | "@hothouse/monorepo-lerna": "^0.4.13", 39 | "@hothouse/monorepo-yarn-workspaces": "^0.4.13", 40 | "@hothouse/types": "^0.4.13", 41 | "@octokit/rest": "^17.0.0", 42 | "chalk": "^3.0.0", 43 | "debug": "^4.0.1", 44 | "glob": "^7.1.2", 45 | "hosted-git-info": "^3.0.2", 46 | "lodash": "^4.17.10", 47 | "minimatch": "^3.0.4", 48 | "mustache": "^3.0.0", 49 | "node-emoji": "^1.8.1", 50 | "normalize-package-data": "^2.4.0", 51 | "remark": "^10.0.1", 52 | "remark-github": "^7.0.3", 53 | "remark-html": "^9.0.0", 54 | "remark-parse": "^6.0.3", 55 | "semver": "^5.5.0", 56 | "threads": "^0.12.0", 57 | "unified": "^7.0.0", 58 | "yargs": "^12.0.1" 59 | }, 60 | "babel": { 61 | "extends": "../../.babelrc" 62 | }, 63 | "collectCoverageFrom": [ 64 | "src/*.{js,jsx}" 65 | ], 66 | "devDependencies": { 67 | "rimraf": "^2.6.2" 68 | }, 69 | "gitHead": "97cee2b39df40662cb9a2767e8c047066b6c9a98" 70 | } 71 | -------------------------------------------------------------------------------- /packages/hothouse/src/BlackList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import minimatch from "minimatch"; 3 | 4 | export default class BlackList { 5 | ignores: Array; 6 | 7 | constructor(ignores: Array) { 8 | this.ignores = ignores; 9 | } 10 | 11 | match(packageName: string): boolean { 12 | return this.ignores.some(name => minimatch(packageName, name)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/hothouse/src/Engine.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from "path"; 3 | import zipObject from "lodash/zipObject"; 4 | import type { 5 | PackageManager, 6 | Structure, 7 | Updates, 8 | UpdateDetails, 9 | ApplyResult, 10 | Reporter, 11 | GitImpl 12 | } from "@hothouse/types"; 13 | import type UpdateChunk from "./UpdateChunk"; 14 | import { split } from "./UpdateChunk"; 15 | import Package from "./Package"; 16 | import configure from "./tasks/configure"; 17 | import createCommitMessage from "./commitMessage"; 18 | import WorkerPool from "./WorkerPool"; 19 | import * as actions from "./actions"; 20 | 21 | const debug = require("debug")("hothouse:Engine"); 22 | 23 | type EngineOptions = {| 24 | token: string, 25 | bail: boolean, 26 | ignore: Array, 27 | perPackage: boolean, 28 | concurrency: number, 29 | dryRun: boolean, 30 | packageManager: ?string, 31 | repositoryStructure: ?string, 32 | reporter: Reporter, 33 | gitImpl: GitImpl 34 | |}; 35 | 36 | export default class Engine { 37 | token: string; 38 | bail: boolean; 39 | ignore: Array; 40 | perPackage: boolean; 41 | dryRun: boolean; 42 | concurrency: number; 43 | reporter: Reporter; 44 | gitImpl: GitImpl; 45 | packageManager: ?string; 46 | repositoryStructure: ?string; 47 | 48 | constructor({ 49 | token, 50 | bail, 51 | ignore, 52 | perPackage, 53 | dryRun, 54 | concurrency, 55 | packageManager, 56 | repositoryStructure, 57 | reporter, 58 | gitImpl 59 | }: EngineOptions) { 60 | this.token = token; 61 | this.bail = bail; 62 | this.ignore = ignore; 63 | this.perPackage = perPackage; 64 | this.dryRun = dryRun; 65 | this.concurrency = concurrency; 66 | this.reporter = reporter; 67 | this.gitImpl = gitImpl; 68 | this.packageManager = packageManager; 69 | this.repositoryStructure = repositoryStructure; 70 | debug(`dryRun=${String(dryRun)}`); 71 | } 72 | 73 | get logPrefix(): string { 74 | return this.dryRun ? "(dryRun) " : ""; 75 | } 76 | 77 | async run(directory: string): Promise> { 78 | const config = { 79 | rootDirectory: directory, 80 | packageManager: this.packageManager, 81 | repositoryStructure: this.repositoryStructure, 82 | ignore: this.ignore, 83 | dryRun: this.dryRun, 84 | token: this.token 85 | }; 86 | const { 87 | token, 88 | pkg, 89 | packageManager, 90 | repositoryStructure, 91 | hosting 92 | } = await configure(actions.configure(config)); 93 | const pool = new WorkerPool({ 94 | concurrency: this.concurrency, 95 | reporter: this.reporter 96 | }); 97 | try { 98 | await pool.configure(config); 99 | 100 | const repositoryUrl: string = pkg.getRepositoryHttpsUrl(); 101 | const baseBranch = await hosting.getDefaultBranch(token, repositoryUrl); 102 | const localPackages = await repositoryStructure.getPackages(directory); 103 | debug(`Found ${localPackages.length} packages:`, localPackages); 104 | 105 | const updatesList = await Promise.all( 106 | localPackages.map(localPackage => 107 | pool.dispatch(actions.fetchUpdates(localPackage)) 108 | ) 109 | ); 110 | const allUpdates = zipObject(localPackages, updatesList); 111 | await this.reporter.reportUpdates(directory, allUpdates); 112 | if (updatesList.every(updates => updates.length === 0)) { 113 | return []; 114 | } 115 | 116 | const chunks = split(allUpdates, this.perPackage); 117 | const updateDetails: Array = await Promise.all( 118 | chunks.map(chunk => pool.dispatch(actions.fetchReleases(chunk))) 119 | ); 120 | 121 | // FIXME: Parallelize 122 | const branches: Array = []; 123 | for (let updateChunk of chunks) { 124 | const branchName = this.createBranchName(updateChunk); 125 | branches.push(branchName); 126 | await this.inBranch(branchName, async () => { 127 | let allChangeSet: Set = new Set([]); 128 | for (let localPackage of updateChunk.getPackagePaths()) { 129 | try { 130 | const updates = updateChunk.getUpdatesBy(localPackage); 131 | if (updates.length === 0) { 132 | debug(`No updates available in: ${localPackage}. Skipped`); 133 | continue; 134 | } 135 | const changeSet = await this.applyUpdates( 136 | localPackage, 137 | directory, 138 | packageManager, 139 | repositoryStructure, 140 | updates 141 | ); 142 | debug(path.relative(directory, localPackage), changeSet); 143 | allChangeSet = new Set([...allChangeSet, ...changeSet]); 144 | } catch (error) { 145 | if (!this.bail) { 146 | throw error; 147 | } 148 | 149 | // eslint-disable-next-line no-console 150 | console.error( 151 | `An error occured during update ${path.basename( 152 | localPackage 153 | )}\n${error.stack}` 154 | ); 155 | } 156 | } 157 | debug({ allChangeSet }); 158 | // FIXME: refactor structure 159 | await this.commit( 160 | this.token, 161 | repositoryUrl, 162 | directory, 163 | updateChunk, 164 | allChangeSet, 165 | branchName 166 | ); 167 | }); 168 | } 169 | 170 | const results: Array = await Promise.all( 171 | chunks 172 | .map((chunk, i) => ({ 173 | updateChunk: chunk, 174 | updateDetails: updateDetails[i], 175 | source: branches[i], 176 | base: baseBranch 177 | })) 178 | .map(payload => pool.dispatch(actions.applyUpdates(payload))) 179 | ); 180 | await this.reporter.reportApplyResult(directory, results); 181 | 182 | return results; 183 | } finally { 184 | await pool.terminate(); 185 | } 186 | } 187 | 188 | async applyUpdates( 189 | packageDirectory: string, 190 | rootDirectory: string, 191 | packageManager: PackageManager, 192 | repositoryStructure: Structure, 193 | updates: Updates 194 | ): Promise> { 195 | const pkg = Package.createFromDirectory(packageDirectory); 196 | updates.forEach(update => { 197 | debug( 198 | `${this.logPrefix}Apply update (${update.name} ${update.current}->${ 199 | update.latest 200 | }) to ${path.basename(packageDirectory)}:`, 201 | update 202 | ); 203 | pkg.apply(update); 204 | }); 205 | 206 | debug(`${this.logPrefix}Try to save: ${pkg.pkgJsonPath}`); 207 | debug( 208 | `${this.logPrefix}Try to update dependencies in ${path.basename( 209 | packageDirectory 210 | )} with ${packageManager.constructor.name}` 211 | ); 212 | if (this.dryRun) { 213 | return new Set([]); 214 | } 215 | await pkg.save(); 216 | return repositoryStructure.install( 217 | packageDirectory, 218 | rootDirectory, 219 | packageManager 220 | ); 221 | } 222 | 223 | createBranchName(updateChunk: UpdateChunk): string { 224 | const now = new Date(); 225 | const branchName = `hothouse-${[ 226 | now.getFullYear(), 227 | String(now.getMonth()).padStart(2, "0"), 228 | String(now.getDate()).padStart(2, "0"), 229 | String(now.getHours()).padStart(2, "0"), 230 | String(now.getMinutes()).padStart(2, "0"), 231 | String(now.getSeconds()).padStart(2, "0") 232 | ].join("")}`; 233 | debug(`Branch name created: ${branchName} with ${now.toISOString()}`); 234 | return `${branchName}-${updateChunk.slugify()}`; 235 | } 236 | 237 | async inBranch(branchName: string, fn: () => any): Promise { 238 | debug(`${this.logPrefix}Try to git operations in branch: ${branchName}`); 239 | if (this.dryRun) { 240 | return fn(); 241 | } else { 242 | return this.gitImpl.inBranch(branchName, fn); 243 | } 244 | } 245 | 246 | async commit( 247 | token: string, 248 | repositoryUrl: string, 249 | rootDirectory: string, 250 | updateChunk: UpdateChunk, 251 | changeSet: Set, 252 | branchName: string 253 | ): Promise { 254 | // FIXME: Make customizable 255 | const message = createCommitMessage(updateChunk); 256 | 257 | debug(`${this.logPrefix}Try to git add .`); 258 | debug(`${this.logPrefix}Try to commit with message:`, { message }); 259 | debug(`${this.logPrefix}Try to git push origin ${branchName}`); 260 | if (!this.dryRun) { 261 | await this.gitImpl.add(...changeSet); 262 | await this.gitImpl.commit(message); 263 | await this.gitImpl.push(token, repositoryUrl, branchName); 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /packages/hothouse/src/Hosting/GitHub.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { URL } from "url"; 3 | import octokit from "@octokit/rest"; 4 | import type { Hosting, PullRequest } from "@hothouse/types"; 5 | import { fromUrl } from "hosted-git-info"; 6 | import { getTagsQuery } from "./graphql"; 7 | 8 | export const parseRepositoryUrl = (url: string): [string, string] => { 9 | const parsed = fromUrl(url); 10 | if (!parsed) { 11 | const urlObj = new URL(url); 12 | const [owner, repo] = urlObj.pathname.split("/").slice(1); 13 | return [owner, repo]; 14 | } 15 | const { user, project } = parsed; 16 | return [user, project]; 17 | }; 18 | 19 | export default class GitHub implements Hosting { 20 | async match(repositoryUrl: string): Promise { 21 | return repositoryUrl.toLowerCase().includes("github"); 22 | } 23 | 24 | async tagExists( 25 | token: string, 26 | repositoryUrl: string, 27 | tag: string 28 | ): Promise { 29 | const client = this.prepare(token); 30 | const [owner, repo] = parseRepositoryUrl(repositoryUrl); 31 | // FIXME: Currently, get 100 tags order by commited date but it's not incomplete. 32 | // We shuold get all tags and compare it. 33 | const { data } = await client.request({ 34 | method: "POST", 35 | url: "/graphql", 36 | query: getTagsQuery(owner, repo) 37 | }); 38 | 39 | return data.data.repository.refs.nodes.some(({ name }) => name === tag); 40 | } 41 | 42 | async shaToTag( 43 | token: string, 44 | repositoryUrl: string, 45 | sha: string 46 | ): Promise { 47 | const client = this.prepare(token); 48 | const [owner, repo] = parseRepositoryUrl(repositoryUrl); 49 | 50 | const PER_PAGE = 100; 51 | let page = 1; 52 | while (1) { 53 | const res = await client.repos.getTags({ 54 | owner, 55 | repo, 56 | per_page: PER_PAGE, 57 | page 58 | }); 59 | const tag = res.data.find(({ commit }) => commit.sha === sha); 60 | if (tag) { 61 | return tag.name; 62 | } 63 | if (!client.hasNextPage(res)) { 64 | break; 65 | } 66 | page++; 67 | } 68 | throw new Error(`Cannot resolve sha: ${sha}`); 69 | } 70 | 71 | async tagToReleaseNote( 72 | token: string, 73 | repositoryUrl: string, 74 | tag: string 75 | ): Promise { 76 | const client = this.prepare(token); 77 | const [owner, repo] = parseRepositoryUrl(repositoryUrl); 78 | try { 79 | const release = await client.repos.getReleaseByTag({ owner, repo, tag }); 80 | const markdown = `# ${release.data.name}\n${release.data.body}`; 81 | return markdown.trim() || null; 82 | } catch (error) { 83 | if (JSON.parse(error.message).message === "Not Found") { 84 | return null; 85 | } 86 | throw error; 87 | } 88 | } 89 | 90 | async getCompareUrl( 91 | token: string, 92 | repositoryUrl: string, 93 | base: string, 94 | head: string 95 | ): Promise { 96 | const client = this.prepare(token); 97 | const [owner, repo] = parseRepositoryUrl(repositoryUrl); 98 | const result = await client.repos.compareCommits({ 99 | owner, 100 | repo, 101 | base, 102 | head 103 | }); 104 | return result.data.html_url; 105 | } 106 | 107 | async createPullRequest( 108 | token: string, 109 | repositoryUrl: string, 110 | base: string, 111 | head: string, 112 | title: string, 113 | body: string 114 | ): Promise { 115 | const client = this.prepare(token); 116 | const [owner, repo] = parseRepositoryUrl(repositoryUrl); 117 | const result = await client.pullRequests.create({ 118 | owner, 119 | repo, 120 | base, 121 | head, 122 | title, 123 | body 124 | }); 125 | return { 126 | title, 127 | url: result.data.html_url 128 | }; 129 | } 130 | 131 | async getDefaultBranch( 132 | token: string, 133 | repositoryUrl: string 134 | ): Promise { 135 | const client = this.prepare(token); 136 | const [owner, repo] = parseRepositoryUrl(repositoryUrl); 137 | const result = await client.repos.get({ owner, repo }); 138 | return result.data.default_branch; 139 | } 140 | 141 | prepare(token: string) { 142 | const client = octokit(); 143 | client.authenticate({ 144 | type: "token", 145 | token 146 | }); 147 | 148 | return client; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /packages/hothouse/src/Hosting/UnknownHosting.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Hosting, PullRequest } from "@hothouse/types"; 3 | 4 | export default class UnknownHosting implements Hosting { 5 | async match(repositoryUrl: string): Promise { 6 | return true; 7 | } 8 | 9 | async tagExists( 10 | token: string, 11 | repositoryUrl: string, 12 | tag: string 13 | ): Promise { 14 | return false; 15 | } 16 | 17 | async shaToTag( 18 | token: string, 19 | repositoryUrl: string, 20 | sha: string 21 | ): Promise { 22 | return "unknown"; 23 | } 24 | 25 | async tagToReleaseNote( 26 | token: string, 27 | repositoryUrl: string, 28 | tag: string 29 | ): Promise { 30 | return ""; 31 | } 32 | 33 | async getCompareUrl( 34 | token: string, 35 | repositoryUrl: string, 36 | base: string, 37 | head: string 38 | ): Promise { 39 | return ""; 40 | } 41 | 42 | async createPullRequest( 43 | token: string, 44 | repositoryUrl: string, 45 | base: string, 46 | head: string, 47 | title: string, 48 | body: string 49 | ): Promise { 50 | return { url: "", title: "" }; 51 | } 52 | 53 | async getDefaultBranch( 54 | token: string, 55 | repositoryUrl: string 56 | ): Promise { 57 | return ""; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/hothouse/src/Hosting/graphql.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const getTagsQuery = (owner: string, name: string) => ` 4 | query { 5 | repository(owner: "${owner}", name: "${name}") { 6 | refs(refPrefix: "refs/tags/", first: 100, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) { 7 | nodes { 8 | name 9 | } 10 | } 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /packages/hothouse/src/Hosting/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Hosting } from "@hothouse/types"; 3 | import type Package from "../Package"; 4 | import GitHub from "./GitHub"; 5 | import UnknownHosting from "./UnknownHosting"; 6 | 7 | const hostings = [new GitHub()]; 8 | 9 | export { GitHub, UnknownHosting }; 10 | 11 | export const detect = async (pkg: Package): Promise => { 12 | if (!pkg.hasRepositoryUrl()) { 13 | return new UnknownHosting(); 14 | } 15 | 16 | const repositoryUrl = pkg.getRepositoryHttpsUrl(); 17 | for (let hosting of hostings) { 18 | if (await hosting.match(repositoryUrl)) { 19 | return hosting; 20 | } 21 | } 22 | 23 | return new UnknownHosting(); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/hothouse/src/Package.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { fromUrl } from "hosted-git-info"; 5 | import normalize from "normalize-package-data"; 6 | import type { Update } from "@hothouse/types"; 7 | import { replace } from "./utils/semver"; 8 | 9 | export default class Package { 10 | pkgJsonPath: string; 11 | pkgJson: Object; 12 | pkgJsonNormalized: Object; 13 | 14 | static createFromDirectory(dir: string): Package { 15 | return new Package(path.join(dir, "package.json")); 16 | } 17 | 18 | constructor(pkgJsonPath: string | Object) { 19 | if (typeof pkgJsonPath === "string") { 20 | this.pkgJsonPath = pkgJsonPath; 21 | // $FlowFixMe(dynamic-require) 22 | this.pkgJson = require(pkgJsonPath); 23 | } else { 24 | this.pkgJsonPath = ""; 25 | this.pkgJson = pkgJsonPath; 26 | } 27 | 28 | this.pkgJsonNormalized = JSON.parse(JSON.stringify(this.pkgJson)); // Deep clone 29 | normalize(this.pkgJsonNormalized); 30 | } 31 | 32 | apply(update: Update): void { 33 | const deps = update.dev 34 | ? this.pkgJson.devDependencies 35 | : this.pkgJson.dependencies; 36 | 37 | deps[update.name] = replace(deps[update.name], update.latest); 38 | } 39 | 40 | hasRepositoryUrl(): boolean { 41 | const { repository } = this.pkgJsonNormalized; 42 | return repository && repository.url; 43 | } 44 | 45 | getRepositoryHttpsUrl(): string { 46 | if (!this.hasRepositoryUrl()) { 47 | throw new Error( 48 | `repository.url is not defined in ${this.pkgJsonPath || 49 | this.pkgJsonNormalized.name}` 50 | ); 51 | } 52 | if (/^https:/.test(this.pkgJsonNormalized.repository.url)) { 53 | return this.pkgJsonNormalized.repository.url; 54 | } 55 | 56 | // https() returns git+https protocol always. 57 | const gitHost = fromUrl(this.pkgJsonNormalized.repository.url); 58 | return gitHost.https().replace("git+", ""); 59 | } 60 | 61 | async save(): Promise { 62 | const json = JSON.stringify(this.pkgJson, null, 2) + "\n"; 63 | fs.writeFileSync(this.pkgJsonPath, json); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/hothouse/src/PackageManagerResolver.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { PackageManager } from "@hothouse/types"; 3 | 4 | const debug = require("debug")("hothouse:PackageManagerResolver"); 5 | 6 | export default class PackageManagerResolver { 7 | pluginNames: Array = [ 8 | "@hothouse/client-yarn", 9 | "@hothouse/client-npm" 10 | ]; 11 | 12 | addPlugin(pluginName: string): void { 13 | this.pluginNames.push(pluginName); 14 | } 15 | 16 | async detect(directory: string): Promise { 17 | debug(`Detect package manager in: ${directory}`); 18 | const plugins = this.pluginNames.map(pluginName => { 19 | // $FlowFixMe(allow-dynamic-require) 20 | const Plugin = require(pluginName); 21 | return new Plugin(); 22 | }); 23 | for (let plugin of plugins) { 24 | const matched = await plugin.match(directory); 25 | debug(`${plugin.constructor.name}: matched=${String(matched)}`); 26 | if (matched) { 27 | return plugin; 28 | } 29 | } 30 | 31 | throw new Error(`Cannot detect any package manager in: ${directory}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/hothouse/src/RepositoryStructureResolver.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Structure } from "@hothouse/types"; 3 | 4 | const debug = require("debug")("hothouse:RepositoryStructureResolver"); 5 | 6 | export default class RepositoryStructureResolver { 7 | pluginNames: Array = [ 8 | "@hothouse/monorepo-yarn-workspaces", 9 | "@hothouse/monorepo-lerna", 10 | "./SinglePackage" 11 | ]; 12 | 13 | addPlugin(pluginName: string): void { 14 | this.pluginNames.push(pluginName); 15 | } 16 | 17 | async detect(directory: string): Promise { 18 | debug(`Detect repository structure in: ${directory}`); 19 | const plugins = this.pluginNames.map(pluginName => { 20 | // $FlowFixMe(allow-dynamic-require) 21 | const Plugin = require(pluginName); 22 | return new Plugin(); 23 | }); 24 | for (let plugin of plugins) { 25 | const matched = await plugin.match(directory); 26 | debug(`${plugin.constructor.name}: matched=${String(matched)}`); 27 | if (matched) { 28 | return plugin; 29 | } 30 | } 31 | 32 | throw new Error(`Cannot detect any repository structure in: ${directory}`); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/hothouse/src/SinglePackage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from "fs"; 3 | import path from "path"; 4 | import type { Structure, PackageManager } from "@hothouse/types"; 5 | 6 | class SinglePackage implements Structure { 7 | async match(directory: string): Promise { 8 | return fs.existsSync(path.join(directory, "package.json")); 9 | } 10 | 11 | async getPackages(directory: string): Promise> { 12 | return [directory]; 13 | } 14 | 15 | async install( 16 | packageDirectory: string, 17 | rootDirectory: string, 18 | npmClient: PackageManager 19 | ): Promise> { 20 | await npmClient.install(packageDirectory); 21 | return this.getChanges(rootDirectory, npmClient); 22 | } 23 | 24 | async getChanges( 25 | rootDirectory: string, 26 | npmClient: PackageManager 27 | ): Promise> { 28 | // #88 package-lock.json should not be added nor committed if not exist 29 | return new Set( 30 | [ 31 | path.join(rootDirectory, "package.json"), 32 | path.join(rootDirectory, npmClient.getLockFileName()) 33 | ] 34 | .filter(fs.existsSync) 35 | .map(p => path.relative(rootDirectory, p)) 36 | ); 37 | } 38 | } 39 | 40 | module.exports = SinglePackage; 41 | -------------------------------------------------------------------------------- /packages/hothouse/src/UpdateChunk.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import crypto from "crypto"; 3 | import type { Updates } from "@hothouse/types"; 4 | 5 | export default class UpdateChunk { 6 | allUpdates: { [string]: Updates }; 7 | 8 | constructor(allUpdates: { [string]: Updates }) { 9 | this.allUpdates = allUpdates; 10 | } 11 | 12 | getPackagePaths(): Array { 13 | return Object.keys(this.allUpdates); 14 | } 15 | 16 | getUpdatesBy(packagePath: string): Updates { 17 | if (!this.allUpdates[packagePath]) { 18 | throw new Error(`Cannot find path: ${packagePath}`); 19 | } 20 | return this.allUpdates[packagePath]; 21 | } 22 | 23 | slugify(): string { 24 | const jsonStr = JSON.stringify(this.allUpdates); 25 | return crypto 26 | .createHash("md5") 27 | .update(jsonStr) 28 | .digest("hex") 29 | .slice(0, 10); 30 | } 31 | } 32 | 33 | export const split = ( 34 | allUpdates: { [string]: Updates }, 35 | perPackage: boolean 36 | ): Array => { 37 | if (perPackage) { 38 | // $FlowFixMe(entires-returns-no-mixed) 39 | const allUpdateEntries: Array> = Object.entries( 40 | allUpdates 41 | ); 42 | const packageNameMap = allUpdateEntries.reduce( 43 | (acc, [packagePath, updates]: [string, Updates]) => ({ 44 | ...acc, 45 | ...updates.reduce((acc2, update) => ({ ...acc2, [update.name]: 1 }), {}) 46 | }), 47 | {} 48 | ); 49 | const uniquePackageNames = Object.keys(packageNameMap); 50 | return uniquePackageNames.map(packageName => { 51 | const chunk = allUpdateEntries.reduce( 52 | (acc, [packagePath, updates]: [string, Updates]) => ({ 53 | ...acc, 54 | [packagePath]: updates.filter(update => update.name === packageName) 55 | }), 56 | {} 57 | ); 58 | return new UpdateChunk(chunk); 59 | }); 60 | } else { 61 | return [new UpdateChunk(allUpdates)]; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /packages/hothouse/src/WorkerPool.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from "path"; 3 | import times from "lodash/times"; 4 | import { Pool } from "threads"; 5 | import type { Reporter } from "@hothouse/types"; 6 | import type { Action } from "./actions"; 7 | import type { Config } from "./worker"; 8 | import * as actions from "./actions"; 9 | 10 | type WorkerInit = {| 11 | concurrency: number, 12 | reporter: Reporter 13 | |}; 14 | export default class WorkerPool { 15 | options: WorkerInit; 16 | pool: Pool; 17 | 18 | constructor(options: WorkerInit) { 19 | const { concurrency, reporter } = options; 20 | 21 | this.options = options; 22 | this.pool = new Pool(concurrency); 23 | this.pool.on("error", (job, e) => reporter.reportError(e)); 24 | } 25 | 26 | async configure(config: Config): Promise { 27 | this.pool.run(path.resolve(__dirname, "worker.js")); 28 | const ready = times(this.options.concurrency, () => 29 | this.dispatch(actions.configure(config)) 30 | ); 31 | return Promise.all(ready); 32 | } 33 | 34 | async dispatch(action: Action): Promise { 35 | return this.pool.send(action).promise(); 36 | } 37 | 38 | async terminate(): Promise { 39 | this.pool.killAll(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/hothouse/src/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { UpdateDetails } from "@hothouse/types"; 3 | import type { Config } from "./worker"; 4 | import type UpdateChunk from "./UpdateChunk"; 5 | 6 | export type ConfigureAction = {| 7 | type: "configure", 8 | payload: Config 9 | |}; 10 | export type ApplyUpdatesAction = {| 11 | type: "applyUpdates", 12 | payload: { 13 | updateChunk: UpdateChunk, 14 | updateDetails: UpdateDetails, 15 | source: string, 16 | base: string 17 | } 18 | |}; 19 | export type FetchReleasesAction = {| 20 | type: "fetchReleases", 21 | payload: { 22 | updateChunk: UpdateChunk 23 | } 24 | |}; 25 | export type FetchUpdatesAction = {| 26 | type: "fetchUpdates", 27 | payload: { 28 | packageDir: string 29 | } 30 | |}; 31 | 32 | export type Action = 33 | | ConfigureAction 34 | | ApplyUpdatesAction 35 | | FetchReleasesAction 36 | | FetchUpdatesAction; 37 | 38 | export const configure = (config: Config): ConfigureAction => ({ 39 | type: "configure", 40 | payload: config 41 | }); 42 | 43 | export const applyUpdates = ({ 44 | updateChunk, 45 | updateDetails, 46 | source, 47 | base 48 | }: { 49 | updateChunk: UpdateChunk, 50 | updateDetails: UpdateDetails, 51 | source: string, 52 | base: string 53 | }): ApplyUpdatesAction => ({ 54 | type: "applyUpdates", 55 | payload: { 56 | updateChunk, 57 | updateDetails, 58 | source, 59 | base 60 | } 61 | }); 62 | 63 | export const fetchReleases = ( 64 | updateChunk: UpdateChunk 65 | ): FetchReleasesAction => ({ 66 | type: "fetchReleases", 67 | payload: { 68 | updateChunk 69 | } 70 | }); 71 | 72 | export const fetchUpdates = (packageDir: string): FetchUpdatesAction => ({ 73 | type: "fetchUpdates", 74 | payload: { 75 | packageDir 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /packages/hothouse/src/cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @flow 3 | import cliOptions from "./options"; 4 | import runner from "./runner"; 5 | import gitImpl from "../git"; 6 | import reporter from "../reporters"; 7 | 8 | runner({ 9 | cliOptions: cliOptions.parse(), 10 | cwd: process.cwd(), 11 | gitImpl, 12 | reporter 13 | }).catch(async error => { 14 | reporter.reportError(error); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/hothouse/src/cli/options.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { cpus } from "os"; 3 | import yargs from "yargs"; 4 | import { version } from "../../package.json"; 5 | 6 | export type CLIOptions = { 7 | ignore: Array, 8 | bail: boolean, 9 | perPackage: boolean, 10 | token: string, 11 | dryRun: boolean, 12 | packageManager: ?string, 13 | repositoryStructure: ?string, 14 | concurrency: number 15 | }; 16 | 17 | export default yargs 18 | .version(version) 19 | .option("token", { 20 | alias: "t", 21 | required: true, 22 | type: "string", 23 | description: "Access token of GitHub" 24 | }) 25 | .option("per-package", { 26 | alias: "p", 27 | type: "boolean", 28 | description: 29 | "Send pull requests per package\nIf you want to send unified pull requests, please specify --no-per-package", 30 | default: true 31 | }) 32 | .option("bail", { 33 | alias: "b", 34 | type: "boolean", 35 | description: 36 | "Continue another updates when error occured during updating package", 37 | default: true 38 | }) 39 | .option("package-manager", { 40 | group: "Advanced", 41 | type: "string", 42 | description: 43 | "Plugin names for package manager\nIf not specified, detect your project automatically" 44 | }) 45 | .option("repository-structure", { 46 | group: "Advanced", 47 | type: "string", 48 | description: 49 | "Plugin names for repository structure\nIf not specified, detect your project automatically" 50 | }) 51 | .option("concurrency", { 52 | group: "Advanced", 53 | type: "number", 54 | description: "Specify the maximum number of concurrency", 55 | default: cpus().length 56 | }) 57 | .option("ignore", { 58 | type: "string", 59 | description: "Comma separated package names to ignore updates", 60 | coerce: (ignore: string) => 61 | ignore 62 | .split(",") 63 | .map(pkg => pkg.trim()) 64 | .filter(pkg => !!pkg), 65 | default: "" 66 | }) 67 | .option("dry-run", { 68 | type: "boolean", 69 | description: 70 | "Don't cause any side effects (e.g. commit, push, pull request)", 71 | default: false 72 | }); 73 | -------------------------------------------------------------------------------- /packages/hothouse/src/cli/runner.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GitImpl, Reporter } from "@hothouse/types"; 3 | import type { CLIOptions } from "./options"; 4 | import Engine from "../Engine"; 5 | 6 | type RunningEnvironment = { 7 | cliOptions: CLIOptions, 8 | cwd: string, 9 | gitImpl: GitImpl, 10 | reporter: Reporter 11 | }; 12 | 13 | const debug = require("debug")("hothouse:cli"); 14 | 15 | const main = async (env: RunningEnvironment) => { 16 | debug(`CLI options are:`, env.cliOptions); 17 | const { 18 | token, 19 | bail, 20 | ignore, 21 | perPackage, 22 | concurrency, 23 | dryRun, 24 | packageManager, 25 | repositoryStructure 26 | } = env.cliOptions; 27 | 28 | const engine = new Engine({ 29 | token, 30 | bail, 31 | ignore, 32 | perPackage, 33 | packageManager, 34 | repositoryStructure, 35 | concurrency, 36 | dryRun, 37 | reporter: env.reporter, 38 | gitImpl: env.gitImpl 39 | }); 40 | 41 | await engine.run(env.cwd); 42 | }; 43 | 44 | export default main; 45 | -------------------------------------------------------------------------------- /packages/hothouse/src/commitMessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from "path"; 3 | import type { Updates } from "@hothouse/types"; 4 | import type UpdateChunk from "./UpdateChunk"; 5 | 6 | export default (updateChunk: UpdateChunk): string => { 7 | return `chore(package): update dependencies to latest version\n\n${Object.entries( 8 | updateChunk.allUpdates 9 | ) 10 | .map( 11 | // $FlowFixMe(entries-returns-Updates) 12 | ([pkgPath, updates]: [string, Updates]) => 13 | `${path.basename(pkgPath)}:\n${updates 14 | .map( 15 | update => `- ${update.name}: ${update.current} -> ${update.latest}` 16 | ) 17 | .join("\n")}` 18 | ) 19 | .join("\n\n")}`; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hothouse/src/errors/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export class InternalError extends Error {} 4 | -------------------------------------------------------------------------------- /packages/hothouse/src/git.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import cp from "child_process"; 3 | import type { GitImpl } from "@hothouse/types"; 4 | 5 | const debug = require("debug")("hothouse:git"); 6 | 7 | export const spawn = (bin: string, ...args: Array): string => { 8 | const result = cp.spawnSync(bin, args, { 9 | encoding: "utf8" 10 | }); 11 | if (result.error) { 12 | throw result.error; 13 | } 14 | if (result.status !== 0) { 15 | throw new Error(result.stderr); 16 | } 17 | 18 | return result.stdout; 19 | }; 20 | 21 | const impl: GitImpl = { 22 | async add(...paths: Array): Promise { 23 | debug("add", { paths }); 24 | spawn("git", "add", ...paths); 25 | }, 26 | 27 | async checkout(branchName: string): Promise { 28 | debug("checkout", { branchName }); 29 | spawn("git", "checkout", branchName); 30 | }, 31 | 32 | async createBranch(branchName: string): Promise { 33 | spawn("git", "checkout", "-b", branchName); 34 | }, 35 | 36 | async deleteBranch(branchName: string): Promise { 37 | spawn("git", "branch", "-D", branchName); 38 | }, 39 | 40 | async commit(message: string): Promise { 41 | debug("commit", message); 42 | spawn("git", "commit", "-m", message); 43 | }, 44 | 45 | async push(token: string, remoteUrl: string, ref: string): Promise { 46 | debug(`push`, { ref }); 47 | spawn("git", "push", "origin", ref); 48 | }, 49 | 50 | async getCurrentBranch(): Promise { 51 | debug("getCurrentBranch"); 52 | return spawn("git", "rev-parse", "--abbrev-ref", "HEAD").trim(); 53 | }, 54 | 55 | async inBranch(branchName: string, fn: () => any): Promise { 56 | debug(`inBranch ${branchName}`); 57 | const currentBranch = await this.getCurrentBranch(); 58 | try { 59 | await this.createBranch(branchName); 60 | await this.checkout(branchName); 61 | await fn(); 62 | await this.checkout(currentBranch); 63 | await this.deleteBranch(branchName); 64 | } finally { 65 | await this.checkout(currentBranch); 66 | } 67 | } 68 | }; 69 | 70 | export default impl; 71 | -------------------------------------------------------------------------------- /packages/hothouse/src/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leko/hothouse/225ec0db621445e5a68615b2d459572239b47ffc/packages/hothouse/src/index.js -------------------------------------------------------------------------------- /packages/hothouse/src/pullRequest.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Mustache from "mustache"; 3 | import type { UpdateDetails } from "@hothouse/types"; 4 | 5 | const titleTemplate = `Update {{&packages}} to the latest version`; 6 | 7 | const bodyTemplate = ` 8 | ## Version **{{latest}}** of **{{&name}}** was just published. 9 | 10 | * Package: {{#repositoryUrl}}[repository]({{&repositoryUrl}}), {{/repositoryUrl}}[npm](https://www.npmjs.com/package/{{&name}}) 11 | * Current Version: {{current}} 12 | * Dev: {{dev}} 13 | {{#compareUrl}}* [compare {{current}} to {{latest}} diffs]({{&compareUrl}}){{/compareUrl}} 14 | 15 | The version(\`{{latest}}\`) is **not covered** by your current version range(\`{{currentRange}}\`). 16 | 17 | {{#releaseNote}} 18 |
19 | Release Notes 20 | {{&releaseNote}} 21 |
22 | {{/releaseNote}} 23 | {{^releaseNote}} 24 | Release note is not available 25 | {{/releaseNote}} 26 | `.trim(); 27 | 28 | export const createPullRequestTitle = (updateDetails: UpdateDetails): string => 29 | Mustache.render(titleTemplate, { 30 | packages: updateDetails.map(detail => detail.name).join(", ") 31 | }); 32 | 33 | export const createPullRequestMessage = ( 34 | updateDetails: UpdateDetails 35 | ): string => 36 | updateDetails 37 | .map(detail => Mustache.render(bodyTemplate, detail)) 38 | .concat([ 39 | "Powered by [hothouse](https://github.com/Leko/hothouse) :honeybee:" 40 | ]) 41 | .join("\n\n----------------------------------------\n\n"); 42 | -------------------------------------------------------------------------------- /packages/hothouse/src/reporters/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import text from "./text"; 3 | 4 | export default text; 5 | -------------------------------------------------------------------------------- /packages/hothouse/src/reporters/text.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from "path"; 3 | import semver from "semver"; 4 | import chalk from "chalk"; 5 | import { emojify as e } from "node-emoji"; 6 | import flatten from "lodash/flatten"; 7 | import type { Reporter, Updates, ApplyResult } from "@hothouse/types"; 8 | import { bugs } from "../../package.json"; 9 | 10 | const diffToColors = { 11 | major: chalk.red, 12 | premajor: chalk.red, 13 | minor: chalk.yellow, 14 | preminor: chalk.yellow, 15 | patch: chalk.cyan, 16 | prepatch: chalk.cyan, 17 | prerelease: chalk.green 18 | }; 19 | 20 | const text = (stream: stream$Writable): Reporter => ({ 21 | async reportError(error: Error): Promise { 22 | stream.write(chalk.red(error.stack) + "\n"); 23 | stream.write(`Please report issue from here: ${bugs.url}\n`); 24 | }, 25 | 26 | async reportUpdates( 27 | cwd: string, 28 | allUpdates: { [string]: Updates } 29 | ): Promise { 30 | const updateGroups = Object.values(allUpdates).filter( 31 | // $FlowFixMe(entries-no-returns-mixed) 32 | updates => updates.length > 0 33 | ); 34 | const updates = flatten(updateGroups).length; 35 | 36 | if (updates === 0) { 37 | stream.write(e("All packages are already up-to-date :sparkles:\n")); 38 | return; 39 | } 40 | stream.write( 41 | `${updateGroups.length} packages have ${updates} updates:\n\n` 42 | ); 43 | 44 | // $FlowFixMe(entries-no-returns-mixed) 45 | Object.entries(allUpdates).forEach(([pkg, updates]: [string, Updates]) => { 46 | if (updates.length === 0) { 47 | return; 48 | } 49 | 50 | const dir = path.relative(cwd, pkg); 51 | stream.write(` ${dir}\n\n`); 52 | updates.forEach(({ name, latest, current, currentRange }) => { 53 | const decorate = diffToColors[semver.diff(current, latest)]; 54 | stream.write(` - ${name}: ${current} -> ${decorate(latest)}\n`); 55 | }); 56 | stream.write("\n"); 57 | }); 58 | }, 59 | 60 | async reportApplyResult( 61 | cwd: string, 62 | applyResults: Array 63 | ): Promise { 64 | const numOfPR = applyResults.length; 65 | stream.write(`${numOfPR} pull requests have been created:\n\n`); 66 | applyResults.forEach(({ pullRequest }) => { 67 | stream.write(` ${pullRequest.title}\n`); 68 | stream.write(` ${pullRequest.url}\n\n`); 69 | }); 70 | } 71 | }); 72 | 73 | export { text }; 74 | export default text(process.stdout); 75 | -------------------------------------------------------------------------------- /packages/hothouse/src/tasks/applyUpdates.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ApplyResult } from "@hothouse/types"; 3 | import type { ApplyUpdatesAction } from "../actions"; 4 | import type { Environment } from "./configure"; 5 | import { 6 | createPullRequestTitle, 7 | createPullRequestMessage 8 | } from "../pullRequest"; 9 | 10 | const debug = require("debug")("hothouse:applyUpdates"); 11 | 12 | export default async ( 13 | action: ApplyUpdatesAction, 14 | env: Environment 15 | ): Promise => { 16 | const { token, hosting, pkg, dryRun } = env; 17 | const { updateDetails, source, base } = action.payload; 18 | const repositoryUrl = pkg.getRepositoryHttpsUrl(); 19 | 20 | const title = createPullRequestTitle(updateDetails); 21 | const body = createPullRequestMessage(updateDetails); 22 | 23 | debug(`${dryRun ? "(dryRun): " : ""}Try to create PR:`, { 24 | repositoryUrl, 25 | base, 26 | source, 27 | title, 28 | body 29 | }); 30 | 31 | let pullRequest = { title: "(dry run)", url: "(dry run)" }; 32 | if (!dryRun) { 33 | pullRequest = await hosting.createPullRequest( 34 | token, 35 | repositoryUrl, 36 | base, 37 | source, 38 | title, 39 | body 40 | ); 41 | } 42 | return { pullRequest }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/hothouse/src/tasks/configure.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Hosting, PackageManager, Structure } from "@hothouse/types"; 3 | import type { ConfigureAction } from "../actions"; 4 | import PackageManagerResolver from "../PackageManagerResolver"; 5 | import RepositoryStructureResolver from "../RepositoryStructureResolver"; 6 | import { detect } from "../Hosting"; 7 | import BlackList from "../BlackList"; 8 | import Package from "../Package"; 9 | 10 | export type Environment = {| 11 | rootDirectory: string, 12 | token: string, 13 | token: string, 14 | dryRun: boolean, 15 | pkg: Package, 16 | packageManager: PackageManager, 17 | repositoryStructure: Structure, 18 | hosting: Hosting, 19 | blackList: BlackList 20 | |}; 21 | 22 | export default async (action: ConfigureAction): Promise => { 23 | const { 24 | rootDirectory, 25 | token, 26 | dryRun, 27 | packageManager, 28 | repositoryStructure, 29 | ignore 30 | } = action.payload; 31 | const packageManagerResolver = new PackageManagerResolver(); 32 | const structureResolver = new RepositoryStructureResolver(); 33 | const pkg = Package.createFromDirectory(rootDirectory); 34 | 35 | if (packageManager) { 36 | packageManagerResolver.addPlugin(packageManager); 37 | } 38 | if (repositoryStructure) { 39 | structureResolver.addPlugin(repositoryStructure); 40 | } 41 | 42 | return { 43 | rootDirectory, 44 | token, 45 | dryRun, 46 | packageManager: await packageManagerResolver.detect(rootDirectory), 47 | repositoryStructure: await structureResolver.detect(rootDirectory), 48 | hosting: await detect(pkg), 49 | pkg, 50 | blackList: new BlackList(ignore) 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/hothouse/src/tasks/fetchReleases.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | Update, 4 | UpdateDetail, 5 | UpdateDetails, 6 | PackageManager 7 | } from "@hothouse/types"; 8 | import type { FetchReleasesAction } from "../actions"; 9 | import type { Environment } from "./configure"; 10 | import Package from "../Package"; 11 | import { detect } from "../Hosting"; 12 | import md2html from "../utils/md2html"; 13 | 14 | const debug = require("debug")("hothouse:fetchReleases"); 15 | 16 | export default async ( 17 | action: FetchReleasesAction, 18 | env: Environment 19 | ): Promise => { 20 | const { updateChunk } = action.payload; 21 | 22 | const updateDetails: UpdateDetails = []; 23 | for (let pkgPath in updateChunk.allUpdates) { 24 | for (let update of updateChunk.allUpdates[pkgPath]) { 25 | const updateDetail = await getUpdateDetail(update, env); 26 | updateDetails.push(updateDetail); 27 | } 28 | } 29 | return updateDetails; 30 | }; 31 | 32 | export const getUpdateDetail = async ( 33 | update: Update, 34 | env: Environment 35 | ): Promise => { 36 | const { packageManager, token } = env; 37 | const { name, current, latest } = update; 38 | 39 | const pkgAnnotation = `${update.name}@${update.latest}`; 40 | const meta = await packageManager.getPackageMeta(pkgAnnotation); 41 | const pkg = new Package(meta); 42 | if (!pkg.hasRepositoryUrl()) { 43 | return { 44 | ...update, 45 | repositoryUrl: null, 46 | compareUrl: null, 47 | releaseNote: null 48 | }; 49 | } 50 | 51 | const repositoryUrl = pkg.getRepositoryHttpsUrl(); 52 | const currentTag = await getTag(packageManager, token, name, current); 53 | const latestTag = await getTag(packageManager, token, name, latest); 54 | debug(pkgAnnotation, { currentTag, latestTag }); 55 | 56 | const compareUrl = 57 | currentTag && latestTag 58 | ? await getCompareUrl(token, meta, currentTag, latestTag) 59 | : null; 60 | const releaseNote = latestTag 61 | ? await getReleaseNote(token, meta, latestTag) 62 | : null; 63 | 64 | return { 65 | ...update, 66 | repositoryUrl, 67 | compareUrl, 68 | releaseNote: releaseNote ? md2html(releaseNote) : null 69 | }; 70 | }; 71 | 72 | export const getTag = async ( 73 | packageManager: PackageManager, 74 | token: string, 75 | packageName: string, 76 | version: string 77 | ): Promise => { 78 | const pkgAnnotation = `${packageName}@${version}`; 79 | debug(`Try to fetch tag of ${pkgAnnotation}`); 80 | 81 | try { 82 | const meta = await packageManager.getPackageMeta(pkgAnnotation); 83 | const pkg = new Package(meta); 84 | const hosting = await detect(pkg); 85 | 86 | const commonTagNames = [`v${version}`, version]; 87 | for (let tag of commonTagNames) { 88 | debug(`Try to check tag exists: ${tag} in ${pkgAnnotation}`); 89 | if (await hosting.tagExists(token, meta.repository.url, tag)) { 90 | return tag; 91 | } 92 | } 93 | if (!meta.gitHead) { 94 | debug( 95 | `gitHead is not specified so cannot resolve tag name: ${pkgAnnotation}` 96 | ); 97 | return null; 98 | } 99 | debug(`Try to resolve tag by gitHead: ${meta.gitHead}`); 100 | return await hosting.shaToTag(token, meta.repository.url, meta.gitHead); 101 | } catch (error) { 102 | debug( 103 | `An error occured during fetch compare url in ${pkgAnnotation}:`, 104 | error.stack 105 | ); 106 | return null; 107 | } 108 | }; 109 | 110 | export const getCompareUrl = async ( 111 | token: string, 112 | meta: Object, 113 | currentTag: ?string, 114 | latestTag: ?string 115 | ): Promise => { 116 | if (!currentTag || !latestTag) { 117 | return null; 118 | } 119 | 120 | debug(`Try to fetch compare url ${meta.name}@${meta.version}`); 121 | try { 122 | const pkg = new Package(meta); 123 | const hosting = await detect(pkg); 124 | return await hosting.getCompareUrl( 125 | token, 126 | meta.repository.url, 127 | currentTag, 128 | latestTag 129 | ); 130 | } catch (error) { 131 | debug( 132 | `An error occured during fetch compare url in ${meta.name}@${ 133 | meta.version 134 | }:`, 135 | error.stack 136 | ); 137 | return null; 138 | } 139 | }; 140 | 141 | export const getReleaseNote = async ( 142 | token: string, 143 | meta: Object, 144 | latestTag: ?string 145 | ): Promise => { 146 | if (!latestTag) { 147 | return null; 148 | } 149 | 150 | debug(`Try to fetch release note about ${meta.name} with tag ${latestTag}`); 151 | try { 152 | const pkg = new Package(meta); 153 | const hosting = await detect(pkg); 154 | const releaseNote = await hosting.tagToReleaseNote( 155 | token, 156 | meta.repository.url, 157 | latestTag 158 | ); 159 | return releaseNote; 160 | } catch (error) { 161 | debug( 162 | `An error occured during fetch release note in ${meta.name}@${ 163 | meta.version 164 | }:`, 165 | error.stack 166 | ); 167 | return null; 168 | } 169 | }; 170 | -------------------------------------------------------------------------------- /packages/hothouse/src/tasks/fetchUpdates.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import semver from "semver"; 3 | import type { Updates } from "@hothouse/types"; 4 | import type { FetchUpdatesAction } from "../actions"; 5 | import type { Environment } from "./configure"; 6 | 7 | const debug = require("debug")("hothouse:fetchUpdates"); 8 | 9 | export default async ( 10 | action: FetchUpdatesAction, 11 | env: Environment 12 | ): Promise => { 13 | const { packageDir } = action.payload; 14 | const { packageManager, blackList } = env; 15 | 16 | const updates = await packageManager.getUpdates(packageDir); 17 | return updates 18 | .filter(update => { 19 | const { name, latest, current, currentRange } = update; 20 | if (semver.satisfies(latest, currentRange)) { 21 | debug(`${name}@${latest} is covered current semver:${currentRange}`); 22 | return false; 23 | } 24 | if (semver.lt(latest, current)) { 25 | debug(`${name}@${latest} less than current: ${current}`); 26 | return false; 27 | } 28 | return true; 29 | }) 30 | .filter(update => { 31 | if (blackList.match(update.name)) { 32 | debug(`${update.name} is matched with black list. Ignored`); 33 | return false; 34 | } 35 | return true; 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/hothouse/src/utils/md2html.js: -------------------------------------------------------------------------------- 1 | import remark from "remark"; 2 | import github from "remark-github"; 3 | import markdown from "remark-parse"; 4 | import html from "remark-html"; 5 | 6 | const pipeline = remark() 7 | .use(github) 8 | .use(markdown) 9 | .use(html); 10 | 11 | export default (markdown: string): string => 12 | pipeline.processSync(markdown).contents; 13 | -------------------------------------------------------------------------------- /packages/hothouse/src/utils/semver.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import semver from "semver"; 3 | 4 | export const replace = ( 5 | fromVersionRange: string, 6 | toVersion: string 7 | ): string => { 8 | const UPDATE_LIMITS = 200; 9 | const prefixRegEx = /^(\^|~|>=|>|<|<=)?\s*(.*)$/; 10 | if (!prefixRegEx.test(fromVersionRange)) { 11 | throw new Error(`Unsupported semver format: ${fromVersionRange}`); 12 | } 13 | // $FlowFixMe(already-tested) 14 | const [, prefix, fromVersion] = fromVersionRange.match(prefixRegEx); 15 | let updated = fromVersion; 16 | for (let i = 0; i < UPDATE_LIMITS; i++) { 17 | const diff = semver.diff(updated, toVersion); 18 | if (!diff) { 19 | break; 20 | } 21 | updated = semver.inc(updated, diff); 22 | } 23 | if (semver.diff(updated, toVersion)) { 24 | throw new Error( 25 | `Too many diffs to update to ${toVersion} from ${fromVersionRange}` 26 | ); 27 | } 28 | return (prefix || "") + updated; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hothouse/src/worker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from "./actions"; 3 | import applyUpdates from "./tasks/applyUpdates"; 4 | import fetchReleases from "./tasks/fetchReleases"; 5 | import fetchUpdates from "./tasks/fetchUpdates"; 6 | import configure, { type Environment } from "./tasks/configure"; 7 | 8 | export type Config = {| 9 | rootDirectory: string, 10 | token: string, 11 | dryRun: boolean, 12 | packageManager: ?string, 13 | repositoryStructure: ?string, 14 | ignore: Array 15 | |}; 16 | 17 | let env: Environment; 18 | 19 | // threads.js expected module.exports not `export default` 20 | module.exports = async (action: Action) => { 21 | switch (action.type) { 22 | case "configure": 23 | env = await configure(action); 24 | return; 25 | case "applyUpdates": 26 | return applyUpdates(action, env); 27 | case "fetchReleases": 28 | return fetchReleases(action, env); 29 | case "fetchUpdates": 30 | return fetchUpdates(action, env); 31 | default: 32 | throw new Error(`Unknown action type: ${action.type}`); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | lerna exec can-npm-publish -- --verbose 5 | 6 | mkdir -p .release_note 7 | npm run bootstrap 8 | 9 | PREVIOUS_VERSION=$(jq -r .version lerna.json) 10 | PREVIOUS_TAG=v$PREVIOUS_VERSION 11 | 12 | lerna publish --force-publish=* 13 | 14 | LATEST_VERSION=$(jq -r .version lerna.json) 15 | LATEST_TAG=v$LATEST_VERSION 16 | 17 | $(npm bin)/dotenv $(npm bin)/lerna-changelog -- \ 18 | --from=$PREVIOUS_TAG \ 19 | | sed 's/^## //' > .release_note/$LATEST_TAG.md 20 | 21 | cat .release_note/$LATEST_TAG.md 22 | hub release create -F .release_note/$LATEST_TAG.md $LATEST_TAG 23 | -------------------------------------------------------------------------------- /type-definition/index.js.flow: -------------------------------------------------------------------------------- 1 | // For jest 2 | declare var jest: any; 3 | declare var expect: any; 4 | declare var test: (string, Function) => void; 5 | declare var describe: (string, Function) => void; 6 | declare var beforeEach: Function => void; 7 | declare var afterEach: Function => void; 8 | --------------------------------------------------------------------------------