├── .prettierignore ├── .prettierrc ├── ROADMAP.md ├── test └── fixtures │ ├── packages │ ├── dummy.txt │ ├── no-package │ │ └── dummy.txt │ ├── oao │ │ └── package.json │ ├── oao-b │ │ └── package.json │ ├── oao-d │ │ └── package.json │ ├── oao-priv │ │ └── package.json │ └── oao-c │ │ └── package.json │ ├── packages3 │ ├── dummy.txt │ ├── no-package │ │ └── dummy.txt │ ├── oao │ │ └── package.json │ ├── oao-b │ │ └── package.json │ ├── oao-d │ │ └── package.json │ ├── oao-c │ │ └── package.json │ └── oao-priv │ │ └── package.json │ ├── CHANGELOG.md │ ├── packages2 │ ├── oao │ │ └── package.json │ ├── oao-c │ │ └── package.json │ └── oao-b │ │ └── package.json │ ├── packagesCustomLinks │ ├── oao │ │ └── package.json │ ├── oao-b │ │ └── package.json │ └── oao-c │ │ └── package.json │ ├── packagesWrongVersion │ └── oao │ │ └── package.json │ ├── packagesWrongVersion2 │ └── oao │ │ └── package.json │ ├── packagesWrongName │ └── wrong-name │ │ └── package.json │ ├── packagesScoped │ ├── example-package │ │ └── package.json │ └── example-package-b │ │ └── package.json │ └── yarnInception │ ├── a │ └── package.json │ └── b │ └── package.json ├── docs ├── status.png └── parallel.gif ├── .babelrc ├── .flowconfig ├── __mocks__ └── rimraf.js ├── src ├── utils │ ├── constants.js │ ├── writeSpecs.js │ ├── types.js │ ├── initConsole.js │ ├── __tests__ │ │ ├── shell.test.js │ │ ├── __snapshots__ │ │ │ ├── calcGraph.test.js.snap │ │ │ └── readSpecs.test.js.snap │ │ ├── calcGraph.test.js │ │ └── readSpecs.test.js │ ├── promises.js │ ├── listPaths.js │ ├── changelog.js │ ├── __mocks__ │ │ └── git.js │ ├── helpers.js │ ├── removeInternalLinks.js │ ├── readSpecs.js │ ├── calcGraph.js │ ├── git.js │ ├── shell.js │ └── multiRun.js ├── __tests__ │ ├── __snapshots__ │ │ ├── publish.test.js.snap │ │ ├── clean.test.js.snap │ │ ├── prepublish.test.js.snap │ │ ├── outdated.test.js.snap │ │ ├── all.test.js.snap │ │ ├── runScript.test.js.snap │ │ ├── removeAll.test.js.snap │ │ ├── bump.test.js.snap │ │ ├── addRemoveUpgrade.test.js.snap │ │ └── bootstrap.test.js.snap │ ├── clean.test.js │ ├── all.test.js │ ├── runScript.test.js │ ├── removeAll.test.js │ ├── resetAllVersions.test.js │ ├── outdated.test.js │ ├── bump.test.js │ ├── prepublish.test.js │ ├── bootstrap.test.js │ ├── addRemoveUpgrade.test.js │ └── publish.test.js ├── all.js ├── runScript.js ├── clean.js ├── removeAll.js ├── resetAllVersions.js ├── bump.js ├── outdated.js ├── prepublish.js ├── status.js ├── bootstrap.js ├── addRemoveUpgrade.js ├── index.js └── publish.js ├── .travis.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── .eslintrc.yaml ├── package.json ├── CHANGELOG.md └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | trailingComma: es5 -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | *Nothing else identified right now* 2 | -------------------------------------------------------------------------------- /test/fixtures/packages/dummy.txt: -------------------------------------------------------------------------------- 1 | This file is required for some tests. -------------------------------------------------------------------------------- /test/fixtures/packages3/dummy.txt: -------------------------------------------------------------------------------- 1 | This file is required for some tests. -------------------------------------------------------------------------------- /docs/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guigrpa/oao/HEAD/docs/status.png -------------------------------------------------------------------------------- /docs/parallel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guigrpa/oao/HEAD/docs/parallel.gif -------------------------------------------------------------------------------- /test/fixtures/packages/no-package/dummy.txt: -------------------------------------------------------------------------------- 1 | This file is required for some tests. -------------------------------------------------------------------------------- /test/fixtures/packages3/no-package/dummy.txt: -------------------------------------------------------------------------------- 1 | This file is required for some tests. -------------------------------------------------------------------------------- /test/fixtures/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | * Add awesome functionality 2 | * Add epic feature 3 | * Remove 1e5 bugs 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "6" } }], 4 | "@babel/preset-flow" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/packages/oao/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/packages2/oao/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/packagesCustomLinks/oao/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/packagesWrongVersion/oao/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "999.1.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/packagesWrongVersion2/oao/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "xxxxx", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/packagesWrongName/wrong-name/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "999.1.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/__tests__/.* 3 | .*/__mocks__/.* 4 | /lib/.* 5 | /node_modules/fbjs 6 | /node_modules/jest/.* 7 | -------------------------------------------------------------------------------- /__mocks__/rimraf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | module.exports = jest.fn((p, options, cb) => { 4 | if (typeof options === 'function') { 5 | options(); 6 | } else { 7 | cb(); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const DEP_TYPES = [ 4 | 'dependencies', 5 | 'devDependencies', 6 | 'peerDependencies', 7 | 'optionalDependencies', 8 | ]; 9 | 10 | export { DEP_TYPES }; 11 | -------------------------------------------------------------------------------- /test/fixtures/packagesScoped/example-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@guigrpa/example-package", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/packages2/oao-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-c", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*" 8 | }, 9 | "license": "MIT" 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/packages2/oao-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-b", 3 | "version": "0.1.16", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*" 8 | }, 9 | "license": "MIT" 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/writeSpecs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fs from 'fs'; 4 | 5 | const writeSpecs = (specPath: string, specs: Object) => { 6 | fs.writeFileSync(specPath, `${JSON.stringify(specs, null, 2)}\n`, 'utf8'); 7 | }; 8 | 9 | export default writeSpecs; 10 | -------------------------------------------------------------------------------- /test/fixtures/packages3/oao/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "oao-b": "*" 8 | }, 9 | "scripts": { 10 | "start": "ls -al" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/packagesCustomLinks/oao-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-b", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*", 8 | "ext-one": "*" 9 | }, 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/packages3/oao-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-b", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x" 7 | }, 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "ls -al" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | - '10' 5 | before_script: 6 | - export TZ=Europe/Madrid 7 | script: npm run travis 8 | after_success: 9 | - 'cat ./coverage/lcov.info | ./node_modules/.bin/coveralls' 10 | cache: 11 | yarn: true 12 | -------------------------------------------------------------------------------- /src/utils/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type OaoSpecs = { 4 | pkgPath: string, 5 | specPath: string, // including .package.json 6 | name: string, 7 | displayName: string, 8 | specs: Object, 9 | }; 10 | export type AllSpecs = { [key: string]: OaoSpecs }; 11 | -------------------------------------------------------------------------------- /test/fixtures/packages/oao-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-b", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*" 8 | }, 9 | "license": "MIT", 10 | "scripts": { 11 | "start": "ls -al", 12 | "start2": "ls" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/packagesScoped/example-package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@guigrpa/example-package-b", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "dependencies": { 7 | "timm": "1.x", 8 | "@guigrpa/example-package": "*" 9 | }, 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/yarnInception/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "inception": "yarn inception2", 8 | "inception2": "yarn inception3", 9 | "inception3": "yarn hello", 10 | "hello": "echo hello" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/yarnInception/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "b", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "inception": "yarn inception2", 8 | "inception2": "yarn inception3", 9 | "inception3": "yarn hello", 10 | "hello": "echo hello" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/packages/oao-d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-d", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*" 8 | }, 9 | "devDependencies": { 10 | "xxl": "1.x", 11 | "oao-b": "*", 12 | "oao-c": "*" 13 | }, 14 | "peerDependencies": {}, 15 | "license": "MIT", 16 | "scripts": {} 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # GIT-NPM common 2 | # -------------- 3 | # All projects 4 | logs 5 | *.log 6 | npm-debug.log* 7 | pids 8 | *.pid 9 | *.seed 10 | node_modules 11 | /coverage 12 | /.nyc_output 13 | /.nyc_tmp 14 | *.sublime-project 15 | *.sublime-workspace 16 | 17 | # Project-specific 18 | 19 | 20 | # GIT-specific 21 | # ------------ 22 | # All projects 23 | lib 24 | 25 | # Project-specific 26 | -------------------------------------------------------------------------------- /test/fixtures/packages/oao-priv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-priv", 3 | "private": true, 4 | "version": "0.1.0", 5 | "main": "index.js", 6 | "dependencies": { 7 | "timm": "1.x", 8 | "oao": "*" 9 | }, 10 | "devDependencies": { 11 | "xxl": "1.x", 12 | "oao-b": "*", 13 | "oao-c": "*" 14 | }, 15 | "peerDependencies": {}, 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/publish.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PUBLISH command updates the changelog correctly 1`] = ` 4 | Array [ 5 | Array [ 6 | "test/fixtures/CHANGELOG.md", 7 | "## 99.99.99 (2017-1-1) 8 | 9 | * Add awesome functionality 10 | * Add epic feature 11 | * Remove 1e5 bugs 12 | ", 13 | "utf8", 14 | ], 15 | ] 16 | `; 17 | -------------------------------------------------------------------------------- /src/utils/initConsole.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { addListener } from 'storyboard'; 4 | import consoleListener from 'storyboard-listener-console'; 5 | 6 | type Options = { 7 | relativeTime?: boolean, 8 | }; 9 | 10 | const initConsole = (options?: Options = {}) => { 11 | const { relativeTime } = options; 12 | addListener(consoleListener, { relativeTime }); 13 | }; 14 | 15 | export default initConsole; 16 | -------------------------------------------------------------------------------- /test/fixtures/packages/oao-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-c", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*" 8 | }, 9 | "peerDependencies": { 10 | "oao-b": "*" 11 | }, 12 | "devDependencies": { 13 | "xxl": "1.x", 14 | "oao-b": "*" 15 | }, 16 | "license": "MIT", 17 | "scripts": { 18 | "start": "ls -al" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/packages3/oao-d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-d", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*" 8 | }, 9 | "devDependencies": { 10 | "xxl": "1.x", 11 | "oao-b": "*", 12 | "oao-c": "*" 13 | }, 14 | "peerDependencies": {}, 15 | "license": "MIT", 16 | "scripts": { 17 | "start": "ls -al" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/all.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import multiRun from './utils/multiRun'; 4 | 5 | type Options = { 6 | src: string, 7 | ignoreSrc?: string, 8 | tree?: boolean, 9 | parallel?: boolean, 10 | parallelLogs?: boolean, 11 | parallelLimit?: number, 12 | ignoreErrors?: boolean, 13 | relativeTime?: boolean, 14 | }; 15 | 16 | const run = (cmd: string, options: Options) => multiRun(options, () => [cmd]); 17 | 18 | export default run; 19 | -------------------------------------------------------------------------------- /test/fixtures/packages3/oao-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-c", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*" 8 | }, 9 | "peerDependencies": { 10 | "oao-b": "*" 11 | }, 12 | "devDependencies": { 13 | "xxl": "1.x", 14 | "oao-b": "*" 15 | }, 16 | "license": "MIT", 17 | "scripts": { 18 | "start": "ls -al" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/packages3/oao-priv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-priv", 3 | "private": true, 4 | "version": "0.1.0", 5 | "main": "index.js", 6 | "dependencies": { 7 | "timm": "1.x", 8 | "oao": "*" 9 | }, 10 | "devDependencies": { 11 | "xxl": "1.x", 12 | "oao-b": "*", 13 | "oao-c": "*" 14 | }, 15 | "peerDependencies": {}, 16 | "license": "MIT", 17 | "scripts": { 18 | "start": "ls -al" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/clean.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CLEAN command executes the correct rimraf calls 1`] = ` 4 | Array [ 5 | "test/fixtures/packages/oao/node_modules", 6 | "test/fixtures/packages/oao-b/node_modules", 7 | "test/fixtures/packages/oao-c/node_modules", 8 | "test/fixtures/packages/oao-d/node_modules", 9 | "test/fixtures/packages/oao-priv/node_modules", 10 | "node_modules", 11 | ] 12 | `; 13 | -------------------------------------------------------------------------------- /test/fixtures/packagesCustomLinks/oao-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao-c", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "timm": "1.x", 7 | "oao": "*", 8 | "ext-one": "*" 9 | }, 10 | "peerDependencies": { 11 | "oao-b": "*", 12 | "ext-two": "*" 13 | }, 14 | "devDependencies": { 15 | "xxl": "1.x", 16 | "oao-b": "*", 17 | "ext-two": "*" 18 | }, 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # GIT-NPM common 2 | # -------------- 3 | # All projects 4 | logs 5 | *.log 6 | npm-debug.log* 7 | pids 8 | *.pid 9 | *.seed 10 | node_modules 11 | /coverage 12 | /.nyc_output 13 | /.nyc_tmp 14 | *.sublime-project 15 | *.sublime-workspace 16 | 17 | # Project-specific 18 | 19 | 20 | # NPM-specific 21 | # ------------ 22 | # All projects 23 | /src 24 | /tools 25 | /test 26 | /package.coffee 27 | /ROADMAP.md 28 | /.travis.yml 29 | /.npmignore 30 | 31 | # Project-specific 32 | /docs 33 | -------------------------------------------------------------------------------- /src/__tests__/clean.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import clean from '../clean'; 5 | 6 | jest.mock('rimraf'); 7 | 8 | describe('CLEAN command', () => { 9 | it('executes the correct rimraf calls', async () => { 10 | const rimraf = require('rimraf'); 11 | await clean({ src: 'test/fixtures/packages/*' }); 12 | const paths = rimraf.mock.calls.map(call => call[0].replace(/\\/g, '/')); 13 | expect(paths).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/prepublish.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PREPUBLISH command copies READMEs as appropriate 1`] = ` 4 | Array [ 5 | Array [ 6 | "README.md", 7 | "test/fixtures/packages/oao/README.md", 8 | ], 9 | Array [ 10 | "README-LINK.md", 11 | "test/fixtures/packages/oao-b/README.md", 12 | ], 13 | Array [ 14 | "README-LINK.md", 15 | "test/fixtures/packages/oao-c/README.md", 16 | ], 17 | Array [ 18 | "README-LINK.md", 19 | "test/fixtures/packages/oao-d/README.md", 20 | ], 21 | ] 22 | `; 23 | -------------------------------------------------------------------------------- /src/utils/__tests__/shell.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import path from 'path'; 4 | import { exec } from '../shell'; 5 | 6 | describe('shell helpers', () => { 7 | it('exec (simple case)', async () => { 8 | const { stdout } = await exec('echo HELLO'); 9 | expect(stdout.trim()).toEqual('HELLO'); 10 | }); 11 | 12 | it('exec (with cwd)', async () => { 13 | const cmd = process.platform === 'win32' ? 'cd' : 'pwd'; 14 | const { stdout } = await exec(cmd, { cwd: 'test' }); 15 | expect( 16 | stdout 17 | .trim() 18 | .split(path.sep) 19 | .slice(-2) 20 | ).toEqual(['oao', 'test']); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils/promises.js: -------------------------------------------------------------------------------- 1 | const runInSeries = async (items, cb) => { 2 | const out = []; 3 | for (let i = 0; i < items.length; i++) { 4 | out[i] = await cb(items[i]); 5 | } 6 | return out; 7 | }; 8 | 9 | const runInParallel = async (items, cb, { waitForAllToResolve } = {}) => { 10 | const promises = items.map(cb); 11 | try { 12 | await Promise.all(promises); 13 | } catch (err) { 14 | if (waitForAllToResolve) { 15 | for (let i = 0; i < promises.length; i++) { 16 | try { 17 | await promises[i]; 18 | } catch (err2) { 19 | /* ignore */ 20 | } 21 | } 22 | } 23 | throw err; 24 | } 25 | }; 26 | 27 | export { runInSeries, runInParallel }; 28 | -------------------------------------------------------------------------------- /src/__tests__/all.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require */ 3 | 4 | import all from '../all'; 5 | 6 | jest.mock('../utils/shell'); 7 | 8 | describe('ALL command', () => { 9 | it('executes the specified command on all sub-packages', async () => { 10 | const helpers = require('../utils/shell'); 11 | await all('ls', { src: 'test/fixtures/packages/*' }); 12 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 13 | }); 14 | 15 | it('can do it following the dependency tree', async () => { 16 | const helpers = require('../utils/shell'); 17 | await all('ls', { src: 'test/fixtures/packages3/*', tree: true }); 18 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/runScript.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import minimatch from 'minimatch'; 4 | import multiRun from './utils/multiRun'; 5 | 6 | type Options = { 7 | src: string, 8 | ignoreSrc?: string, 9 | tree?: boolean, 10 | parallel?: boolean, 11 | parallelLogs?: boolean, 12 | parallelLimit?: number, 13 | ignoreErrors?: boolean, 14 | relativeTime?: boolean, 15 | }; 16 | 17 | const run = (script: string, options: Options) => 18 | multiRun(options, specs => { 19 | const { scripts } = specs; 20 | if (!scripts) return []; 21 | const scriptNames = Object.keys(scripts).filter(o => minimatch(o, script)); 22 | if (!scriptNames.length) return []; 23 | return scriptNames.map(o => `yarn run ${o}`); 24 | }); 25 | 26 | export default run; 27 | -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/calcGraph.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`buildGraph returns list as an allSpecs object 1`] = ` 4 | Object { 5 | "a": Object { 6 | "name": "a", 7 | "specs": Object { 8 | "dependencies": Object { 9 | "b": "*", 10 | "c": "*", 11 | }, 12 | }, 13 | }, 14 | "b": Object { 15 | "name": "b", 16 | "specs": Object { 17 | "dependencies": Object { 18 | "ext": "*", 19 | }, 20 | }, 21 | }, 22 | "c": Object { 23 | "name": "c", 24 | "specs": Object { 25 | "dependencies": Object { 26 | "ext": "*", 27 | }, 28 | "devDependencies": Object { 29 | "d": "*", 30 | }, 31 | }, 32 | }, 33 | "d": Object { 34 | "name": "d", 35 | "specs": Object {}, 36 | }, 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/outdated.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`OUTDATED command executes the correct \`yarn outdated\`s 1`] = ` 4 | Array [ 5 | Array [ 6 | "yarn outdated", 7 | Object { 8 | "cwd": "test/fixtures/packages/oao", 9 | }, 10 | ], 11 | Array [ 12 | "yarn outdated", 13 | Object { 14 | "cwd": "test/fixtures/packages/oao-b", 15 | }, 16 | ], 17 | Array [ 18 | "yarn outdated", 19 | Object { 20 | "cwd": "test/fixtures/packages/oao-c", 21 | }, 22 | ], 23 | Array [ 24 | "yarn outdated", 25 | Object { 26 | "cwd": "test/fixtures/packages/oao-d", 27 | }, 28 | ], 29 | Array [ 30 | "yarn outdated", 31 | Object { 32 | "cwd": "test/fixtures/packages/oao-priv", 33 | }, 34 | ], 35 | Array [ 36 | "yarn outdated", 37 | Object { 38 | "cwd": ".", 39 | }, 40 | ], 41 | ] 42 | `; 43 | -------------------------------------------------------------------------------- /src/utils/listPaths.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import globby from 'globby'; 6 | 7 | const listPaths = async ( 8 | src: string | Array, 9 | ignoreSrc?: ?string 10 | ): Promise> => { 11 | const patterns = Array.isArray(src) ? src : [src]; 12 | if (ignoreSrc) patterns.push(`!${ignoreSrc}`); 13 | const paths = await globby(patterns); 14 | return paths 15 | .filter(filePath => { 16 | try { 17 | return ( 18 | fs.statSync(path.resolve(process.cwd(), filePath)).isDirectory() && 19 | fs.existsSync(path.resolve(process.cwd(), filePath, 'package.json')) 20 | ); 21 | } catch (err) { 22 | return false; 23 | } 24 | }) 25 | .map(filePath => 26 | filePath === '/' || filePath[filePath.length - 1] !== '/' 27 | ? filePath 28 | : filePath.slice(0, -1) 29 | ); 30 | }; 31 | 32 | export default listPaths; 33 | -------------------------------------------------------------------------------- /src/clean.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import path from 'path'; 4 | import rimraf from 'rimraf'; 5 | import { mainStory, chalk } from 'storyboard'; 6 | import { readAllSpecs } from './utils/readSpecs'; 7 | 8 | type Options = { 9 | src: string, 10 | ignoreSrc?: string, 11 | }; 12 | 13 | const run = async ({ src, ignoreSrc }: Options) => { 14 | const allSpecs = await readAllSpecs(src, ignoreSrc); 15 | const pkgNames = Object.keys(allSpecs); 16 | await Promise.all( 17 | pkgNames.map( 18 | pkgName => 19 | new Promise((resolve, reject) => { 20 | const { pkgPath } = allSpecs[pkgName]; 21 | const nodeModulesPath = path.join(pkgPath, 'node_modules'); 22 | mainStory.info(`Removing ${chalk.cyan.bold(nodeModulesPath)}...`); 23 | rimraf(nodeModulesPath, err => { 24 | if (err) { 25 | reject(err); 26 | return; 27 | } 28 | resolve(); 29 | }); 30 | }) 31 | ) 32 | ); 33 | }; 34 | 35 | export default run; 36 | -------------------------------------------------------------------------------- /src/utils/changelog.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fs from 'fs'; 4 | import { mainStory, chalk } from 'storyboard'; 5 | 6 | type Options = { 7 | changelogPath: string, 8 | version: string, 9 | // Unit tests 10 | _date?: ?Object, 11 | }; 12 | 13 | const addVersionLine = ({ changelogPath, version, _date }: Options) => { 14 | let contents; 15 | try { 16 | contents = fs.readFileSync(changelogPath, 'utf8'); 17 | } catch (err) { 18 | mainStory.warn( 19 | `Could not find changelog (${chalk.cyan.bold( 20 | changelogPath 21 | )}). Skipped update` 22 | ); 23 | return; 24 | } 25 | 26 | const date = _date || new Date(); 27 | const line = `## ${version} (${date.getFullYear()}-${date.getMonth() + 28 | 1}-${date.getDate()})`; 29 | const finalContents = `${line}\n\n${contents}`; 30 | try { 31 | fs.writeFileSync(changelogPath, finalContents, 'utf8'); 32 | } catch (err) { 33 | throw new Error(`Could not update changelog (${changelogPath})`); 34 | } 35 | }; 36 | 37 | export { addVersionLine }; 38 | -------------------------------------------------------------------------------- /src/__tests__/runScript.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require */ 3 | 4 | import runScript from '../runScript'; 5 | 6 | jest.mock('../utils/shell'); 7 | 8 | describe('RUN-SCRIPT command', () => { 9 | it('executes the specified script on all sub-packages', async () => { 10 | const helpers = require('../utils/shell'); 11 | await runScript('start', { src: 'test/fixtures/packages/*' }); 12 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 13 | }); 14 | 15 | it('executes the specified script with wildcards on all sub-packages', async () => { 16 | const helpers = require('../utils/shell'); 17 | await runScript('st*', { src: 'test/fixtures/packages/*' }); 18 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 19 | }); 20 | 21 | it('can do it following the dependency tree', async () => { 22 | const helpers = require('../utils/shell'); 23 | await runScript('start', { src: 'test/fixtures/packages3/*', tree: true }); 24 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/utils/__mocks__/git.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const git = jest.genMockFromModule('../git'); 4 | const pkg = require('../../../package.json'); 5 | 6 | git._initStubs = () => { 7 | let branch = 'master'; 8 | git._setBranch = branch0 => { 9 | branch = branch0; 10 | }; 11 | git.gitCurBranch = jest.fn(() => Promise.resolve(branch)); 12 | 13 | let uncommitted = ''; 14 | git._setUncommitted = uncommitted0 => { 15 | uncommitted = uncommitted0; 16 | }; 17 | git.gitUncommittedChanges = jest.fn(() => Promise.resolve(uncommitted)); 18 | 19 | let unpulled = '0'; 20 | git._setUnpulled = unpulled0 => { 21 | unpulled = unpulled0; 22 | }; 23 | git.gitUnpulledChanges = jest.fn(() => Promise.resolve(unpulled)); 24 | 25 | const lastTag = `v${pkg.version}`; 26 | git.gitLastTag = jest.fn(() => Promise.resolve(lastTag)); 27 | 28 | let diff = 'SOMETHING_HAS_CHANGED'; 29 | git._setSubpackageDiff = diff0 => { 30 | diff = diff0; 31 | }; 32 | git.gitDiffSinceIn = jest.fn(() => Promise.resolve(diff)); 33 | }; 34 | 35 | module.exports = git; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017- Guillermo Grau Panea 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 | -------------------------------------------------------------------------------- /src/removeAll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { set as timmSet, omit } from 'timm'; 4 | import { readAllSpecs } from './utils/readSpecs'; 5 | import { DEP_TYPES } from './utils/constants'; 6 | import { parseDep } from './utils/helpers'; 7 | import writeSpecs from './utils/writeSpecs'; 8 | 9 | type Options = { 10 | src: string, 11 | ignoreSrc?: string, 12 | link: ?string, 13 | }; 14 | 15 | const run = async (deps: Array, opts: Options) => { 16 | const { src, ignoreSrc } = opts; 17 | const allSpecs = await readAllSpecs(src, ignoreSrc); 18 | const pkgNames = Object.keys(allSpecs); 19 | 20 | // Update all package.json files with this version 21 | pkgNames.forEach(pkgName => { 22 | const { specPath, specs: prevSpecs } = allSpecs[pkgName]; 23 | let nextSpecs = prevSpecs; 24 | deps.forEach(dep => { 25 | const { name: depName } = parseDep(dep); 26 | DEP_TYPES.forEach(type => { 27 | const depsOfType = nextSpecs[type] || {}; 28 | if (depsOfType[depName] != null) { 29 | const nextDeps = omit(depsOfType, [depName]); 30 | nextSpecs = timmSet(nextSpecs, type, nextDeps); 31 | } 32 | }); 33 | }); 34 | if (nextSpecs !== prevSpecs) writeSpecs(specPath, nextSpecs); 35 | }); 36 | }; 37 | 38 | export default run; 39 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { DEP_TYPES } from './constants'; 4 | import type { OaoSpecs } from './types'; 5 | 6 | const shortenName = (name: string, maxLen: number): string => { 7 | if (name.length <= maxLen) return name; 8 | return `${name.slice(0, 2)}…${name.slice(-(maxLen - 3))}`; 9 | }; 10 | 11 | const isObject = (o: any) => !!o && o.constructor === Object; 12 | 13 | const delay = (ms: number): Promise<*> => 14 | new Promise(resolve => { 15 | setTimeout(resolve, ms); 16 | }); 17 | 18 | const dependsOn = (pkg: OaoSpecs, possibleDep: string) => { 19 | const { specs } = pkg; 20 | for (let i = 0; i < DEP_TYPES.length; i++) { 21 | const depType = DEP_TYPES[i]; 22 | const deps = specs[depType] || {}; 23 | if (deps[possibleDep]) return true; 24 | } 25 | return false; 26 | }; 27 | 28 | const parseDep = (dep: string) => { 29 | // Extract package name from the dependency specs 30 | // (forget about the first character, for compatibility with scoped packages) 31 | const idx = dep.indexOf('@', 1); 32 | const name = idx >= 1 ? dep.slice(0, idx) : dep; 33 | const version = idx >= 1 ? dep.slice(idx + 1) : ''; 34 | return { name, version }; 35 | }; 36 | 37 | const masterOrMainBranch = (branch: string): boolean => { 38 | return branch === 'master' || branch === 'main'; 39 | }; 40 | 41 | export { 42 | shortenName, 43 | isObject, 44 | delay, 45 | dependsOn, 46 | parseDep, 47 | masterOrMainBranch, 48 | }; 49 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | extends: 3 | - airbnb 4 | - prettier 5 | plugins: 6 | - flowtype 7 | rules: 8 | eqeqeq: ['error', 'allow-null'] 9 | prefer-object-spread: off 10 | no-unused-expressions: 11 | - error 12 | - allowShortCircuit: true 13 | no-use-before-define: off 14 | no-multi-spaces: off 15 | no-nested-ternary: off 16 | no-cond-assign: ['error', 'except-parens'] 17 | no-underscore-dangle: off 18 | no-confusing-arrow: off 19 | comma-dangle: 20 | - error 21 | - arrays: always-multiline 22 | objects: always-multiline 23 | imports: always-multiline 24 | exports: always-multiline 25 | functions: ignore 26 | no-plusplus: 27 | - error 28 | - allowForLoopAfterthoughts: true 29 | no-continue: off 30 | no-await-in-loop: off 31 | key-spacing: 32 | - warn 33 | - beforeColon: false 34 | afterColon: true 35 | mode: 'minimum' 36 | object-property-newline: off 37 | class-methods-use-this: off 38 | arrow-parens: off 39 | react/sort-comp: off 40 | react/jsx-first-prop-new-line: off 41 | react/jsx-indent: off 42 | react/jsx-indent-props: off 43 | react/jsx-closing-bracket-location: off 44 | react/jsx-filename-extension: off 45 | react/forbid-prop-types: off 46 | react/prop-types: off 47 | react/require-extension: off 48 | react/require-default-props: off 49 | import/no-extraneous-dependencies: 50 | - error 51 | - devDependencies: true 52 | peerDependencies: true 53 | optionalDependencies: false 54 | import/prefer-default-export: off 55 | jsx-a11y/no-static-element-interactions: off 56 | globals: 57 | chrome: false 58 | -------------------------------------------------------------------------------- /src/resetAllVersions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { set as timmSet } from 'timm'; 4 | import { mainStory, chalk } from 'storyboard'; 5 | import inquirer from 'inquirer'; 6 | import semver from 'semver'; 7 | import { readAllSpecs } from './utils/readSpecs'; 8 | import writeSpecs from './utils/writeSpecs'; 9 | 10 | type Options = { 11 | src: string, 12 | ignoreSrc?: string, 13 | confirm?: boolean, 14 | }; 15 | 16 | const run = async ( 17 | version: string, 18 | { src, ignoreSrc, confirm = true }: Options 19 | ) => { 20 | if (!semver.valid(version)) { 21 | mainStory.error(`Version ${version} is not valid`); 22 | throw new Error('INVALID_VERSION'); 23 | } 24 | 25 | const allSpecs = await readAllSpecs(src, ignoreSrc); 26 | const pkgNames = Object.keys(allSpecs); 27 | 28 | // Ask for confirmation 29 | if (confirm) { 30 | const { goAhead } = await inquirer.prompt([ 31 | { 32 | name: 'goAhead', 33 | type: 'confirm', 34 | message: 35 | 'Are you sure you want to reset the version number of all packages, ' + 36 | `including the monorepo root, to ${chalk.cyan.yellow(version)} ` + 37 | `(${chalk.cyan.bold( 38 | pkgNames.length 39 | )} package/s, including monorepo)?`, 40 | default: false, 41 | }, 42 | ]); 43 | if (!goAhead) process.exit(0); 44 | } 45 | 46 | for (let i = 0; i < pkgNames.length; i++) { 47 | const pkgName = pkgNames[i]; 48 | const { specPath, specs: prevSpecs } = allSpecs[pkgName]; 49 | const nextSpecs = timmSet(prevSpecs, 'version', version); 50 | writeSpecs(specPath, nextSpecs); 51 | } 52 | }; 53 | 54 | export default run; 55 | -------------------------------------------------------------------------------- /src/utils/removeInternalLinks.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { omit, set as timmSet } from 'timm'; 4 | import { DEP_TYPES } from './constants'; 5 | 6 | type PkgVersionMap = { [pkgName: string]: string }; 7 | 8 | const removeInternalLinks = ( 9 | prevSpecs: Object, 10 | pkgNames: Array, 11 | linkPattern: ?string 12 | ): { 13 | nextSpecs: Object, 14 | removedPackagesByType: { [key: string]: PkgVersionMap }, 15 | allRemovedPackages: PkgVersionMap, 16 | } => { 17 | const removedPackagesByType = {}; 18 | const allRemovedPackages = {}; 19 | const regex = linkPattern ? new RegExp(linkPattern) : null; 20 | 21 | let nextSpecs = prevSpecs; 22 | DEP_TYPES.forEach(type => { 23 | const prevDeps = nextSpecs[type]; 24 | if (prevDeps == null) return; 25 | let nextDeps = prevDeps; 26 | Object.keys(prevDeps).forEach(name => { 27 | // Is package to be removed? Only if it belongs to the internal 28 | // subpackage list (`pkgNames`) or it matches the custom `linkPattern` 29 | const fRemove = 30 | pkgNames.indexOf(name) >= 0 || (regex != null && regex.test(name)); 31 | if (!fRemove) return; 32 | const version = prevDeps[name]; 33 | if (version == null) return; 34 | nextDeps = omit(nextDeps, [name]); 35 | if (!removedPackagesByType[type]) removedPackagesByType[type] = {}; 36 | removedPackagesByType[type][name] = version; 37 | allRemovedPackages[name] = version; 38 | }); 39 | nextSpecs = timmSet(nextSpecs, type, nextDeps); 40 | }); 41 | 42 | return { 43 | nextSpecs, 44 | removedPackagesByType, 45 | allRemovedPackages, 46 | }; 47 | }; 48 | 49 | export default removeInternalLinks; 50 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/all.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ALL command can do it following the dependency tree 1`] = ` 4 | Array [ 5 | Array [ 6 | "ls", 7 | Object { 8 | "cwd": "test/fixtures/packages3/oao-b", 9 | "storySrc": undefined, 10 | }, 11 | ], 12 | Array [ 13 | "ls", 14 | Object { 15 | "cwd": "test/fixtures/packages3/oao", 16 | "storySrc": undefined, 17 | }, 18 | ], 19 | Array [ 20 | "ls", 21 | Object { 22 | "cwd": "test/fixtures/packages3/oao-c", 23 | "storySrc": undefined, 24 | }, 25 | ], 26 | Array [ 27 | "ls", 28 | Object { 29 | "cwd": "test/fixtures/packages3/oao-d", 30 | "storySrc": undefined, 31 | }, 32 | ], 33 | Array [ 34 | "ls", 35 | Object { 36 | "cwd": "test/fixtures/packages3/oao-priv", 37 | "storySrc": undefined, 38 | }, 39 | ], 40 | ] 41 | `; 42 | 43 | exports[`ALL command executes the specified command on all sub-packages 1`] = ` 44 | Array [ 45 | Array [ 46 | "ls", 47 | Object { 48 | "cwd": "test/fixtures/packages/oao", 49 | "storySrc": undefined, 50 | }, 51 | ], 52 | Array [ 53 | "ls", 54 | Object { 55 | "cwd": "test/fixtures/packages/oao-b", 56 | "storySrc": undefined, 57 | }, 58 | ], 59 | Array [ 60 | "ls", 61 | Object { 62 | "cwd": "test/fixtures/packages/oao-c", 63 | "storySrc": undefined, 64 | }, 65 | ], 66 | Array [ 67 | "ls", 68 | Object { 69 | "cwd": "test/fixtures/packages/oao-d", 70 | "storySrc": undefined, 71 | }, 72 | ], 73 | Array [ 74 | "ls", 75 | Object { 76 | "cwd": "test/fixtures/packages/oao-priv", 77 | "storySrc": undefined, 78 | }, 79 | ], 80 | ] 81 | `; 82 | -------------------------------------------------------------------------------- /src/__tests__/removeAll.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import removeAll from '../removeAll'; 5 | 6 | jest.mock('../utils/shell'); 7 | jest.mock('../utils/writeSpecs'); 8 | 9 | describe('REMOVE-ALL command', () => { 10 | it("leaves package.json untouched if it doesn't depend on dep", async () => { 11 | const writeSpecs = require('../utils/writeSpecs').default; 12 | await removeAll(['mady'], { 13 | src: 'test/fixtures/packages/*', 14 | }); 15 | expect(writeSpecs.mock.calls).toHaveLength(0); 16 | }); 17 | 18 | it('modifies package.json if it depends on dep', async () => { 19 | const writeSpecs = require('../utils/writeSpecs').default; 20 | await removeAll(['ext-one'], { 21 | src: 'test/fixtures/packagesCustomLinks/*', 22 | }); 23 | expect(writeSpecs.mock.calls).toHaveLength(2); 24 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); 25 | expect(writeSpecs.mock.calls[1][1]).toMatchSnapshot(); 26 | }); 27 | 28 | it('removes a dep everywhere it may appear', async () => { 29 | const writeSpecs = require('../utils/writeSpecs').default; 30 | await removeAll(['ext-two'], { 31 | src: 'test/fixtures/packagesCustomLinks/*', 32 | }); 33 | expect(writeSpecs.mock.calls).toHaveLength(1); 34 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); 35 | }); 36 | 37 | it('removes multiple deps', async () => { 38 | const writeSpecs = require('../utils/writeSpecs').default; 39 | await removeAll(['ext-one', 'ext-two'], { 40 | src: 'test/fixtures/packagesCustomLinks/*', 41 | }); 42 | expect(writeSpecs.mock.calls).toHaveLength(2); 43 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); 44 | expect(writeSpecs.mock.calls[1][1]).toMatchSnapshot(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/runScript.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RUN-SCRIPT command can do it following the dependency tree 1`] = ` 4 | Array [ 5 | Array [ 6 | "yarn run start", 7 | Object { 8 | "cwd": "test/fixtures/packages3/oao-b", 9 | "storySrc": undefined, 10 | }, 11 | ], 12 | Array [ 13 | "yarn run start", 14 | Object { 15 | "cwd": "test/fixtures/packages3/oao", 16 | "storySrc": undefined, 17 | }, 18 | ], 19 | Array [ 20 | "yarn run start", 21 | Object { 22 | "cwd": "test/fixtures/packages3/oao-c", 23 | "storySrc": undefined, 24 | }, 25 | ], 26 | Array [ 27 | "yarn run start", 28 | Object { 29 | "cwd": "test/fixtures/packages3/oao-d", 30 | "storySrc": undefined, 31 | }, 32 | ], 33 | Array [ 34 | "yarn run start", 35 | Object { 36 | "cwd": "test/fixtures/packages3/oao-priv", 37 | "storySrc": undefined, 38 | }, 39 | ], 40 | ] 41 | `; 42 | 43 | exports[`RUN-SCRIPT command executes the specified script on all sub-packages 1`] = ` 44 | Array [ 45 | Array [ 46 | "yarn run start", 47 | Object { 48 | "cwd": "test/fixtures/packages/oao-b", 49 | "storySrc": undefined, 50 | }, 51 | ], 52 | Array [ 53 | "yarn run start", 54 | Object { 55 | "cwd": "test/fixtures/packages/oao-c", 56 | "storySrc": undefined, 57 | }, 58 | ], 59 | ] 60 | `; 61 | 62 | exports[`RUN-SCRIPT command executes the specified script with wildcards on all sub-packages 1`] = ` 63 | Array [ 64 | Array [ 65 | "yarn run start", 66 | Object { 67 | "cwd": "test/fixtures/packages/oao-b", 68 | "storySrc": undefined, 69 | }, 70 | ], 71 | Array [ 72 | "yarn run start2", 73 | Object { 74 | "cwd": "test/fixtures/packages/oao-b", 75 | "storySrc": undefined, 76 | }, 77 | ], 78 | Array [ 79 | "yarn run start", 80 | Object { 81 | "cwd": "test/fixtures/packages/oao-c", 82 | "storySrc": undefined, 83 | }, 84 | ], 85 | ] 86 | `; 87 | -------------------------------------------------------------------------------- /src/__tests__/resetAllVersions.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import path from 'path'; 5 | import { set as timmSet } from 'timm'; 6 | import resetAllVersions from '../resetAllVersions'; 7 | import { ROOT_PACKAGE } from '../utils/readSpecs'; 8 | 9 | jest.mock('../utils/shell'); 10 | jest.mock('../utils/writeSpecs'); 11 | 12 | const PACKAGE_NAMES = ['oao', 'oao-b', 'oao-c', 'oao-d', 'oao-priv']; 13 | 14 | describe('RESET_ALL_VERSIONS command', () => { 15 | it('does not modify any package.json (except for the version number)', async () => { 16 | const writeSpecs = require('../utils/writeSpecs').default; 17 | const base = path.join(process.cwd(), 'test/fixtures/packages'); 18 | const newVersion = '27.4.2013'; 19 | 20 | // Copy original specs 21 | const originalSpecs = {}; 22 | PACKAGE_NAMES.forEach(name => { 23 | originalSpecs[name] = require(path.join(base, `${name}/package.json`)); 24 | }); 25 | originalSpecs[ROOT_PACKAGE] = require(path.join( 26 | process.cwd(), 27 | 'package.json' 28 | )); 29 | 30 | // Run command 31 | await resetAllVersions(newVersion, { 32 | src: 'test/fixtures/packages/*', 33 | confirm: false, 34 | }); 35 | 36 | // Checks 37 | expect(writeSpecs.mock.calls).toHaveLength(PACKAGE_NAMES.length + 1); 38 | writeSpecs.mock.calls.forEach(([specPath, specs]) => { 39 | const name = 40 | specPath === path.join(process.cwd(), 'package.json') 41 | ? ROOT_PACKAGE 42 | : specs.name; 43 | expect(specs).toEqual( 44 | timmSet(originalSpecs[name], 'version', newVersion) 45 | ); 46 | }); 47 | }); 48 | 49 | it('throws when the version is invalid', async () => { 50 | try { 51 | await resetAllVersions('xxx', { 52 | src: 'test/fixtures/packages/*', 53 | confirm: false, 54 | }); 55 | throw new Error('DID_NOT_THROW'); 56 | } catch (err) { 57 | if (err.message === 'DID_NOT_THROW') throw err; 58 | } 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/removeAll.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`REMOVE-ALL command modifies package.json if it depends on dep 1`] = ` 4 | Object { 5 | "dependencies": Object { 6 | "oao": "*", 7 | "timm": "1.x", 8 | }, 9 | "license": "MIT", 10 | "main": "index.js", 11 | "name": "oao-b", 12 | "version": "0.1.0", 13 | } 14 | `; 15 | 16 | exports[`REMOVE-ALL command modifies package.json if it depends on dep 2`] = ` 17 | Object { 18 | "dependencies": Object { 19 | "oao": "*", 20 | "timm": "1.x", 21 | }, 22 | "devDependencies": Object { 23 | "ext-two": "*", 24 | "oao-b": "*", 25 | "xxl": "1.x", 26 | }, 27 | "license": "MIT", 28 | "main": "index.js", 29 | "name": "oao-c", 30 | "peerDependencies": Object { 31 | "ext-two": "*", 32 | "oao-b": "*", 33 | }, 34 | "version": "0.1.0", 35 | } 36 | `; 37 | 38 | exports[`REMOVE-ALL command removes a dep everywhere it may appear 1`] = ` 39 | Object { 40 | "dependencies": Object { 41 | "ext-one": "*", 42 | "oao": "*", 43 | "timm": "1.x", 44 | }, 45 | "devDependencies": Object { 46 | "oao-b": "*", 47 | "xxl": "1.x", 48 | }, 49 | "license": "MIT", 50 | "main": "index.js", 51 | "name": "oao-c", 52 | "peerDependencies": Object { 53 | "oao-b": "*", 54 | }, 55 | "version": "0.1.0", 56 | } 57 | `; 58 | 59 | exports[`REMOVE-ALL command removes multiple deps 1`] = ` 60 | Object { 61 | "dependencies": Object { 62 | "oao": "*", 63 | "timm": "1.x", 64 | }, 65 | "license": "MIT", 66 | "main": "index.js", 67 | "name": "oao-b", 68 | "version": "0.1.0", 69 | } 70 | `; 71 | 72 | exports[`REMOVE-ALL command removes multiple deps 2`] = ` 73 | Object { 74 | "dependencies": Object { 75 | "oao": "*", 76 | "timm": "1.x", 77 | }, 78 | "devDependencies": Object { 79 | "oao-b": "*", 80 | "xxl": "1.x", 81 | }, 82 | "license": "MIT", 83 | "main": "index.js", 84 | "name": "oao-c", 85 | "peerDependencies": Object { 86 | "oao-b": "*", 87 | }, 88 | "version": "0.1.0", 89 | } 90 | `; 91 | -------------------------------------------------------------------------------- /src/utils/readSpecs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import { mainStory } from 'storyboard'; 6 | import type { OaoSpecs, AllSpecs } from './types'; 7 | import listPaths from './listPaths'; 8 | 9 | const ROOT_PACKAGE = '__ROOT_PACKAGE__'; 10 | 11 | const readAllSpecs = async ( 12 | src: string | Array, 13 | ignoreSrc?: ?string, 14 | includeRootPkg: boolean = true 15 | ): Promise => { 16 | const pkgPaths = await listPaths(src, ignoreSrc); 17 | if (includeRootPkg) pkgPaths.push('.'); 18 | const allSpecs = {}; 19 | mainStory.info('Reading all package.json files...'); 20 | pkgPaths.forEach(pkgPath => { 21 | const pkg = readOneSpec(pkgPath); 22 | allSpecs[pkg.name] = pkg; 23 | }); 24 | return allSpecs; 25 | }; 26 | 27 | const readOneSpec = (pkgPath: string): OaoSpecs => { 28 | const pkg = {}; 29 | pkg.pkgPath = pkgPath; 30 | try { 31 | pkg.specPath = path.resolve(process.cwd(), pkgPath, 'package.json'); 32 | pkg.specs = JSON.parse(fs.readFileSync(pkg.specPath, 'utf8')); 33 | } catch (err) { 34 | mainStory.error(`Could not read package.json at ${pkg.specPath}`); 35 | throw err; 36 | } 37 | const name = pkgPath === '.' ? ROOT_PACKAGE : pkg.specs.name; 38 | validatePkgName(pkgPath, name); 39 | pkg.name = name; 40 | pkg.displayName = name === ROOT_PACKAGE ? 'MONOREPO ROOT' : name; 41 | return pkg; 42 | }; 43 | 44 | const validatePkgName = (pkgPath: string, name: string): void => { 45 | if (name == null || name === '') { 46 | throw new Error(`Package has no name (${pkgPath})`); 47 | } 48 | if (pkgPath === '.') return; 49 | const segments = pkgPath.split('/'); 50 | if (name[0] !== '@' && name !== segments[segments.length - 1]) { 51 | const errMsg = `Package name (${name}) does not match directory name ${pkgPath}`; 52 | mainStory.error(errMsg); 53 | const err = new Error('INVALID_DIR_NAME'); 54 | // $FlowFixMe (piggyback on exception) 55 | err.details = errMsg; 56 | throw err; 57 | } 58 | }; 59 | 60 | export { readAllSpecs, readOneSpec, ROOT_PACKAGE }; 61 | -------------------------------------------------------------------------------- /src/utils/__tests__/calcGraph.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import calcGraph, { calcGraphAndReturnAsAllSpecs } from '../calcGraph'; 4 | 5 | const ALL_SPECS_NO_CYCLE = { 6 | a: { 7 | name: 'a', 8 | specs: { 9 | dependencies: { b: '*', c: '*' }, 10 | }, 11 | }, 12 | b: { 13 | name: 'b', 14 | specs: { 15 | dependencies: { ext: '*' }, 16 | }, 17 | }, 18 | c: { 19 | name: 'c', 20 | specs: { 21 | dependencies: { ext: '*' }, 22 | devDependencies: { d: '*' }, 23 | }, 24 | }, 25 | d: { 26 | name: 'd', 27 | specs: {}, 28 | }, 29 | }; 30 | 31 | const ALL_SPECS_CYCLE = { 32 | a: { 33 | name: 'a', 34 | specs: { 35 | dependencies: { b: '*', c: '*' }, 36 | }, 37 | }, 38 | b: { 39 | name: 'b', 40 | specs: { 41 | dependencies: { ext: '*' }, 42 | }, 43 | }, 44 | c: { 45 | name: 'c', 46 | specs: { 47 | dependencies: { ext: '*' }, 48 | devDependencies: { d: '*', e: '*' }, 49 | }, 50 | }, 51 | d: { 52 | name: 'd', 53 | specs: {}, 54 | }, 55 | e: { 56 | name: 'e', 57 | specs: { 58 | optionalDependencies: { a: '*' }, 59 | }, 60 | }, 61 | }; 62 | 63 | const ALL_SPECS_MULTI_ROOT = { 64 | a: { name: 'a', specs: {} }, 65 | b: { name: 'b', specs: {} }, 66 | c: { name: 'c', specs: {} }, 67 | }; 68 | 69 | describe('buildGraph', () => { 70 | it('calculates a directed acyclic graph correctly when no cycles are present', () => { 71 | const dag = calcGraph(ALL_SPECS_NO_CYCLE); 72 | expect(dag).toEqual(['b', 'd', 'c', 'a']); 73 | }); 74 | 75 | it('handles multiple roots correctly', () => { 76 | const dag = calcGraph(ALL_SPECS_MULTI_ROOT); 77 | expect(dag).toEqual(['a', 'b', 'c']); 78 | }); 79 | 80 | it('handles cycles correctly', () => { 81 | const dag = calcGraph(ALL_SPECS_CYCLE); 82 | expect(dag).toEqual(['b', 'd', 'e', 'c', 'a']); 83 | }); 84 | 85 | it('returns list as an allSpecs object', () => { 86 | const allSpecs = calcGraphAndReturnAsAllSpecs(ALL_SPECS_NO_CYCLE); 87 | expect(allSpecs).toMatchSnapshot(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/bump.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { setIn } from 'timm'; 4 | import { mainStory } from 'storyboard'; 5 | import { readAllSpecs } from './utils/readSpecs'; 6 | import { DEP_TYPES } from './utils/constants'; 7 | import { parseDep } from './utils/helpers'; 8 | import writeSpecs from './utils/writeSpecs'; 9 | import { exec } from './utils/shell'; 10 | 11 | type Options = { 12 | src: string, 13 | ignoreSrc?: string, 14 | link: ?string, 15 | }; 16 | 17 | const run = async (deps: Array, opts: Options) => { 18 | const { src, ignoreSrc, link: linkPattern } = opts; 19 | const allSpecs = await readAllSpecs(src, ignoreSrc); 20 | const pkgNames = Object.keys(allSpecs); 21 | 22 | // Determine correct version for each dep 23 | const versions = {}; 24 | for (let i = 0; i < deps.length; i++) { 25 | const dep = deps[i]; 26 | const { name, version } = parseDep(dep); 27 | let finalVersion = version; 28 | if (!finalVersion) { 29 | if (pkgNames.indexOf(dep) >= 0) { 30 | finalVersion = `^${allSpecs[dep].specs.version}`; 31 | } else if (linkPattern && new RegExp(linkPattern).test(dep)) { 32 | finalVersion = '*'; 33 | } else { 34 | const { stdout } = await exec(`npm info ${dep} version`, { 35 | logLevel: 'trace', 36 | }); 37 | finalVersion = `^${stdout.trim()}`; 38 | } 39 | } 40 | versions[name] = finalVersion; 41 | } 42 | mainStory.info('New versions of these packages:', { attach: versions }); 43 | 44 | // Update all package.json files with this version 45 | pkgNames.forEach(pkgName => { 46 | const { specPath, specs: prevSpecs } = allSpecs[pkgName]; 47 | let nextSpecs = prevSpecs; 48 | deps.forEach(dep => { 49 | const { name: depName } = parseDep(dep); 50 | DEP_TYPES.forEach(type => { 51 | const depsOfType = nextSpecs[type] || {}; 52 | if (depsOfType[depName] != null) { 53 | nextSpecs = setIn(nextSpecs, [type, depName], versions[depName]); 54 | } 55 | }); 56 | }); 57 | if (nextSpecs !== prevSpecs) writeSpecs(specPath, nextSpecs); 58 | }); 59 | }; 60 | 61 | export default run; 62 | -------------------------------------------------------------------------------- /src/__tests__/outdated.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import path from 'path'; 5 | import outdated from '../outdated'; 6 | 7 | jest.mock('../utils/shell'); 8 | jest.mock('../utils/writeSpecs'); 9 | 10 | const PACKAGE_NAMES_1 = ['oao', 'oao-b', 'oao-c', 'oao-d', 'oao-priv']; 11 | 12 | const readOriginalSpecs = (base, names) => { 13 | const originalSpecs = {}; 14 | names.forEach(name => { 15 | originalSpecs[name] = require(path.join(base, `${name}/package.json`)); 16 | }); 17 | return originalSpecs; 18 | }; 19 | 20 | const spyFinalSpec = (spy, pkgName) => { 21 | let finalSpec; 22 | spy.mock.calls.forEach(([, specs]) => { 23 | if (specs.name === pkgName) finalSpec = specs; 24 | }); 25 | return finalSpec; 26 | }; 27 | 28 | describe('OUTDATED command', () => { 29 | it('does not modify any package.json', async () => { 30 | const writeSpecs = require('../utils/writeSpecs').default; 31 | const base = path.join(process.cwd(), 'test/fixtures/packages'); 32 | const originalSpecs = readOriginalSpecs(base, PACKAGE_NAMES_1); 33 | await outdated({ src: 'test/fixtures/packages/*' }); 34 | // Process call arguments; we keep the last time a spec is stored 35 | // and compare it with the original one 36 | writeSpecs.mock.calls.forEach(([specPath, specs]) => { 37 | const { name } = specs; 38 | expect(specPath.split(path.sep)).toContain(name); 39 | expect(specPath.split(path.sep)).toContain('package.json'); 40 | }); 41 | PACKAGE_NAMES_1.forEach(name => { 42 | const finalSpecWritten = spyFinalSpec(writeSpecs, name); 43 | if (finalSpecWritten === undefined) return; // not changed at all! 44 | expect(finalSpecWritten).toEqual(originalSpecs[name]); 45 | }); 46 | }); 47 | 48 | it('executes the correct `yarn outdated`s', async () => { 49 | const helpers = require('../utils/shell'); 50 | await outdated({ src: 'test/fixtures/packages/*' }); 51 | const { calls } = helpers.exec.mock; 52 | const calls2 = calls.map(call => [call[0], { cwd: call[1].cwd }]); 53 | expect(calls2).toMatchSnapshot(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/utils/calcGraph.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { AllSpecs, OaoSpecs } from './types'; 4 | import { DEP_TYPES } from './constants'; 5 | 6 | const calcGraph = (allSpecs: AllSpecs): Array => { 7 | const out = []; 8 | const pkgNames = Object.keys(allSpecs); 9 | if (!pkgNames.length) return out; 10 | 11 | // Build virtual root node 12 | const virtualRootDeps = {}; 13 | pkgNames.forEach(name => { 14 | virtualRootDeps[name] = true; 15 | }); 16 | const virtualRootNode: any = { 17 | name: '__VIRTUAL_ROOT__', 18 | specs: { dependencies: virtualRootDeps }, 19 | }; 20 | 21 | // Build graph starting from virtual root node, then remove it 22 | buildGraph(allSpecs, virtualRootNode, pkgNames, out); 23 | return out.slice(0, out.length - 1); 24 | }; 25 | 26 | export const calcGraphAndReturnAsAllSpecs = (allSpecs: AllSpecs): AllSpecs => { 27 | const newAllSpecs = {}; 28 | const orderedPackages = calcGraph(allSpecs); 29 | orderedPackages.forEach(pkg => { 30 | newAllSpecs[pkg] = allSpecs[pkg]; 31 | }); 32 | return newAllSpecs; 33 | }; 34 | 35 | const buildGraph = ( 36 | allSpecs: AllSpecs, 37 | pkg: OaoSpecs, 38 | pkgNames: Array, 39 | out: Array, 40 | visited?: Array = [] 41 | ) => { 42 | const { name } = pkg; 43 | visited.push(name); 44 | const internalDeps = getInternalDeps(pkg, pkgNames); 45 | for (let i = 0; i < internalDeps.length; i++) { 46 | const depName = internalDeps[i]; 47 | if (visited.indexOf(depName) >= 0) continue; 48 | buildGraph(allSpecs, allSpecs[depName], pkgNames, out, visited); 49 | } 50 | out.push(name); 51 | }; 52 | 53 | const getInternalDeps = (pkg: OaoSpecs, pkgNames: Array) => { 54 | const { specs } = pkg; 55 | const internalDeps = {}; 56 | for (let i = 0; i < DEP_TYPES.length; i++) { 57 | const depType = DEP_TYPES[i]; 58 | const deps = specs[depType] || {}; 59 | const depNames = Object.keys(deps); 60 | for (let k = 0; k < depNames.length; k++) { 61 | const pkgName = depNames[k]; 62 | if (pkgNames.indexOf(pkgName) >= 0) internalDeps[pkgName] = true; 63 | } 64 | } 65 | return Object.keys(internalDeps); 66 | }; 67 | 68 | export default calcGraph; 69 | -------------------------------------------------------------------------------- /src/__tests__/bump.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import bump from '../bump'; 5 | 6 | jest.mock('../utils/shell'); 7 | jest.mock('../utils/writeSpecs'); 8 | 9 | describe('BUMP command', () => { 10 | it("leaves package.json untouched if it doesn't depend on dep", async () => { 11 | const writeSpecs = require('../utils/writeSpecs').default; 12 | await bump(['mady@18'], { 13 | src: 'test/fixtures/packages/*', 14 | }); 15 | expect(writeSpecs.mock.calls).toHaveLength(0); 16 | }); 17 | 18 | it('modifies package.json if it depends on dep', async () => { 19 | const writeSpecs = require('../utils/writeSpecs').default; 20 | await bump(['ext-one@18'], { 21 | src: 'test/fixtures/packagesCustomLinks/*', 22 | }); 23 | expect(writeSpecs.mock.calls).toHaveLength(2); 24 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); 25 | expect(writeSpecs.mock.calls[1][1]).toMatchSnapshot(); 26 | }); 27 | 28 | it('modifies a dep version everywhere it may appear', async () => { 29 | const writeSpecs = require('../utils/writeSpecs').default; 30 | await bump(['ext-two@18'], { 31 | src: 'test/fixtures/packagesCustomLinks/*', 32 | }); 33 | expect(writeSpecs.mock.calls).toHaveLength(1); 34 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); 35 | }); 36 | 37 | it('modifies multiple deps', async () => { 38 | const writeSpecs = require('../utils/writeSpecs').default; 39 | await bump(['ext-one@18', 'ext-two@18'], { 40 | src: 'test/fixtures/packagesCustomLinks/*', 41 | }); 42 | expect(writeSpecs.mock.calls).toHaveLength(2); 43 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); 44 | expect(writeSpecs.mock.calls[1][1]).toMatchSnapshot(); 45 | }); 46 | 47 | it('uses current version for internal deps', async () => { 48 | const writeSpecs = require('../utils/writeSpecs').default; 49 | await bump(['oao-c'], { 50 | src: 'test/fixtures/packages/*', 51 | }); 52 | expect(writeSpecs.mock.calls).toHaveLength(2); 53 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); 54 | expect(writeSpecs.mock.calls[1][1]).toMatchSnapshot(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/utils/__tests__/readSpecs.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require */ 3 | 4 | import { readAllSpecs, ROOT_PACKAGE } from '../readSpecs'; 5 | 6 | describe('readAllSpecs', () => { 7 | it('reads all package.json files as well as metadata', async () => { 8 | const allSpecs = await readAllSpecs('test/fixtures/packages/*'); 9 | const rootSpecs = allSpecs[ROOT_PACKAGE]; 10 | expect(rootSpecs.pkgPath).toBe('.'); 11 | expect(rootSpecs.displayName).toBe('MONOREPO ROOT'); 12 | expect(rootSpecs.specs.name).toBe('oao'); 13 | delete allSpecs[ROOT_PACKAGE]; 14 | Object.keys(allSpecs).forEach(name => { 15 | expect(allSpecs[name].specPath).not.toBeNull(); 16 | delete allSpecs[name].specPath; 17 | }); 18 | expect(allSpecs).toMatchSnapshot(); 19 | }); 20 | 21 | it('accepts --src ending with slash', async () => { 22 | const allSpecs = await readAllSpecs('test/fixtures/packages/*/'); 23 | delete allSpecs[ROOT_PACKAGE]; 24 | Object.keys(allSpecs).forEach(name => { 25 | delete allSpecs[name].specPath; 26 | }); 27 | expect(allSpecs).toMatchSnapshot(); 28 | }); 29 | 30 | it('supports excluding packages', async () => { 31 | const allSpecs = await readAllSpecs( 32 | 'test/fixtures/packages/*/', 33 | 'test/fixtures/packages/oao-{b,c}' 34 | ); 35 | delete allSpecs[ROOT_PACKAGE]; 36 | Object.keys(allSpecs).forEach(name => { 37 | delete allSpecs[name].specPath; 38 | }); 39 | expect(allSpecs).toMatchSnapshot(); 40 | }); 41 | 42 | it('supports scoped packages', async () => { 43 | const allSpecs = await readAllSpecs('test/fixtures/packagesScoped/*'); 44 | expect(allSpecs['@guigrpa/example-package'].specs.name).toEqual( 45 | '@guigrpa/example-package' 46 | ); 47 | expect(allSpecs['@guigrpa/example-package-b'].specs.name).toEqual( 48 | '@guigrpa/example-package-b' 49 | ); 50 | }); 51 | 52 | it('throws on invalid directory names (for non-scoped packages)', async () => { 53 | try { 54 | await readAllSpecs('test/fixtures/packagesWrongName/*'); 55 | throw new Error('DID_NOT_THROW'); 56 | } catch (err) { 57 | if (err.message !== 'INVALID_DIR_NAME') throw err; 58 | } 59 | }); 60 | 61 | it('supports an empty array', async () => { 62 | const allSpecs = await readAllSpecs([]); 63 | expect(Object.keys(allSpecs).length).toEqual(1); // just the monorepo root 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/utils/git.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { exec } from './shell'; 4 | 5 | const gitLastTag = async (): Promise => { 6 | try { 7 | let { stdout: commit } = await exec('git rev-list --tags --max-count=1', { 8 | logLevel: 'trace', 9 | errorLogLevel: 'info', 10 | }); 11 | commit = commit.trim(); 12 | if (commit === '') return null; 13 | let { stdout: tag } = await exec(`git describe --tags ${commit}`, { 14 | logLevel: 'trace', 15 | }); 16 | tag = tag.trim(); 17 | tag = tag !== '' ? tag : null; 18 | return tag; 19 | } catch (err) { 20 | return null; 21 | } 22 | }; 23 | 24 | const gitCurBranch = async (): Promise => { 25 | const { stdout } = await exec('git symbolic-ref --short HEAD', { 26 | logLevel: 'trace', 27 | }); 28 | return stdout.trim(); 29 | }; 30 | 31 | const gitUncommittedChanges = async (): Promise => { 32 | const { stdout } = await exec('git status --porcelain', { 33 | logLevel: 'trace', 34 | }); 35 | return stdout.trim(); 36 | }; 37 | 38 | // Ripped off from: https://github.com/sindresorhus/np/blob/master/lib/git.js 39 | const gitUnpulledChanges = async (): Promise => { 40 | const { stdout } = await exec( 41 | 'git rev-list --count --left-only @{u}...HEAD', 42 | { 43 | logLevel: 'trace', 44 | } 45 | ); 46 | return stdout.trim(); 47 | }; 48 | 49 | const gitDiffSinceIn = async ( 50 | sinceTag: ?string, 51 | inPath: string 52 | ): Promise => { 53 | if (sinceTag == null) return 'CHANGED'; 54 | const { stdout } = await exec( 55 | `git diff --name-only ${sinceTag} -- ${inPath}`, 56 | { 57 | logLevel: 'trace', 58 | } 59 | ); 60 | return stdout.trim(); 61 | }; 62 | 63 | const gitCommitChanges = async (msg: string): Promise => { 64 | await exec('git add .', { logLevel: 'trace' }); 65 | await exec(`git commit -m ${msg}`, { logLevel: 'trace' }); 66 | }; 67 | 68 | const gitAddTag = async (tag: string): Promise => { 69 | await exec(`git tag ${tag}`, { logLevel: 'trace' }); 70 | }; 71 | 72 | const gitPushWithTags = async (): Promise => { 73 | await exec('git push --quiet', { logLevel: 'trace' }); 74 | await exec('git push --tags --quiet', { logLevel: 'trace' }); 75 | }; 76 | 77 | export { 78 | gitLastTag, 79 | gitCurBranch, 80 | gitUncommittedChanges, 81 | gitUnpulledChanges, 82 | gitDiffSinceIn, 83 | gitCommitChanges, 84 | gitAddTag, 85 | gitPushWithTags, 86 | }; 87 | -------------------------------------------------------------------------------- /src/outdated.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { mainStory, chalk } from 'storyboard'; 4 | import semver from 'semver'; 5 | import { readAllSpecs } from './utils/readSpecs'; 6 | import removeInternalLinks from './utils/removeInternalLinks'; 7 | import writeSpecs from './utils/writeSpecs'; 8 | import { exec } from './utils/shell'; 9 | 10 | type Options = { 11 | src: string, 12 | ignoreSrc?: string, 13 | link: ?string, 14 | }; 15 | 16 | const run = async (opts: Options) => { 17 | const { src, ignoreSrc, link: linkPattern } = opts; 18 | const allSpecs = await readAllSpecs(src, ignoreSrc); 19 | const pkgNames = Object.keys(allSpecs); 20 | for (let i = 0; i < pkgNames.length; i++) { 21 | const pkgName = pkgNames[i]; 22 | const { pkgPath, specPath, specs: prevSpecs } = allSpecs[pkgName]; 23 | const story = mainStory.child({ 24 | title: `Outdated dependencies in ${chalk.cyan.bold(pkgName)}`, 25 | level: 'info', 26 | }); 27 | 28 | // Rewrite package.json without own/linked packages, run `yarn outdated`, and revert changes 29 | let fModified = false; 30 | let allRemovedPackages; 31 | try { 32 | const tmp = removeInternalLinks(prevSpecs, pkgNames, linkPattern); 33 | const { nextSpecs } = tmp; 34 | ({ allRemovedPackages } = tmp); 35 | if (nextSpecs !== prevSpecs) { 36 | writeSpecs(specPath, nextSpecs); 37 | fModified = true; 38 | } 39 | await exec('yarn outdated', { 40 | cwd: pkgPath, 41 | story, 42 | createChildStory: false, 43 | ignoreErrorCode: true, 44 | logLevel: 'trace', 45 | }); 46 | } catch (err) { 47 | story.close(); 48 | throw err; 49 | } finally { 50 | if (prevSpecs != null && fModified) writeSpecs(specPath, prevSpecs); 51 | } 52 | 53 | // Log warnings when linked sub-packages do not match the specified range 54 | try { 55 | Object.keys(allRemovedPackages).forEach(depName => { 56 | const depVersionRange = allRemovedPackages[depName]; 57 | const depSpecs = allSpecs[depName]; 58 | if (!depSpecs) return; // might not exist, if it's a custom link 59 | const depActualVersion = depSpecs.specs.version; 60 | if (!semver.satisfies(depActualVersion, depVersionRange)) { 61 | story.warn( 62 | `| - Warning: ${chalk.cyan.bold( 63 | `${depName}@${depActualVersion}` 64 | )} ` + 65 | `does not satisfy the specified range: ${chalk.cyan.bold( 66 | depVersionRange 67 | )}` 68 | ); 69 | } 70 | }); 71 | } finally { 72 | story.close(); 73 | } 74 | } 75 | }; 76 | 77 | export default run; 78 | -------------------------------------------------------------------------------- /src/prepublish.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import path from 'path'; 4 | import { merge } from 'timm'; 5 | import semver from 'semver'; 6 | import { mainStory, chalk } from 'storyboard'; 7 | import { readAllSpecs, ROOT_PACKAGE } from './utils/readSpecs'; 8 | import writeSpecs from './utils/writeSpecs'; 9 | import { cp } from './utils/shell'; 10 | 11 | type Options = { 12 | src: string, 13 | ignoreSrc?: string, 14 | copyAttrs: string, 15 | }; 16 | 17 | const run = async ({ src, ignoreSrc, copyAttrs: copyAttrsStr }: Options) => { 18 | const allSpecs = await readAllSpecs(src, ignoreSrc); 19 | const pkgNames = Object.keys(allSpecs); 20 | const rootSpecs = allSpecs[ROOT_PACKAGE].specs; 21 | 22 | // Check version numbers! 23 | const masterVersion = rootSpecs.version; 24 | for (let i = 0; i < pkgNames.length; i++) { 25 | const pkgName = pkgNames[i]; 26 | if (pkgName === ROOT_PACKAGE) continue; 27 | const { specs } = allSpecs[pkgName]; 28 | const { version } = specs; 29 | if (specs.private) continue; 30 | if (!semver.valid(version)) { 31 | mainStory.error( 32 | `Invalid version for ${chalk.bold(pkgName)}: ${chalk.bold(version)}` 33 | ); 34 | throw new Error('INVALID_VERSION'); 35 | } 36 | if (semver.gt(version, masterVersion)) { 37 | mainStory.error( 38 | `Version for ${pkgName} (${chalk.bold( 39 | version 40 | )}) > master version (${chalk.bold(masterVersion)})` 41 | ); 42 | throw new Error('INVALID_VERSION'); 43 | } 44 | } 45 | 46 | // Copy READMEs to all non-private packages 47 | for (let i = 0; i < pkgNames.length; i++) { 48 | const pkgName = pkgNames[i]; 49 | if (pkgName === ROOT_PACKAGE) continue; 50 | const { pkgPath, specs } = allSpecs[pkgName]; 51 | if (specs.private) continue; 52 | const srcFile = pkgName === rootSpecs.name ? 'README.md' : 'README-LINK.md'; 53 | const dstFile = path.join(pkgPath, 'README.md'); 54 | cp(srcFile, dstFile); 55 | } 56 | 57 | // Merge common attributes with submodules 58 | const commonSpecs = {}; 59 | const copyAttrs = copyAttrsStr.split(/\s*,\s*/); 60 | copyAttrs.forEach(attr => { 61 | commonSpecs[attr] = rootSpecs[attr]; 62 | }); 63 | mainStory.info('Updating package attributes', { attach: commonSpecs }); 64 | for (let i = 0; i < pkgNames.length; i++) { 65 | const pkgName = pkgNames[i]; 66 | if (pkgName === ROOT_PACKAGE) continue; 67 | const { specPath, specs: prevSpecs } = allSpecs[pkgName]; 68 | if (prevSpecs.private) continue; 69 | const nextSpecs = merge(prevSpecs, commonSpecs); 70 | writeSpecs(specPath, nextSpecs); 71 | } 72 | 73 | mainStory.warn( 74 | 'Please make sure you commit all changes before you attempt "oao publish"' 75 | ); 76 | }; 77 | 78 | export default run; 79 | -------------------------------------------------------------------------------- /src/__tests__/prepublish.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import path from 'path'; 5 | import prepublish from '../prepublish'; 6 | 7 | jest.mock('../utils/shell'); 8 | jest.mock('../utils/writeSpecs'); 9 | 10 | const COPY_SPECS = [ 11 | 'description', 12 | 'keywords', 13 | 'author', 14 | 'license', 15 | 'homepage', 16 | 'bugs', 17 | 'repository', 18 | ]; 19 | 20 | const normalizePath = p => p.split(path.sep).join('/'); 21 | 22 | describe('PREPUBLISH command', () => { 23 | it('copies READMEs as appropriate', async () => { 24 | const helpers = require('../utils/shell'); 25 | await prepublish({ 26 | src: 'test/fixtures/packages/*', 27 | copyAttrs: COPY_SPECS.join(','), 28 | }); 29 | const normalizedArgs = helpers.cp.mock.calls.map(([src, dst]) => [ 30 | normalizePath(src), 31 | normalizePath(dst), 32 | ]); 33 | expect(normalizedArgs).toMatchSnapshot(); 34 | }); 35 | 36 | it('copies common attributes to subpackages', async () => { 37 | const writeSpecs = require('../utils/writeSpecs').default; 38 | const refSpecs = require(path.join(process.cwd(), 'package.json')); 39 | await prepublish({ 40 | src: 'test/fixtures/packages/*', 41 | copyAttrs: COPY_SPECS.join(','), 42 | }); 43 | expect(writeSpecs.mock.calls.map(args => args[1].name)).toEqual([ 44 | 'oao', 45 | 'oao-b', 46 | 'oao-c', 47 | 'oao-d', 48 | ]); 49 | writeSpecs.mock.calls.forEach(([, specs]) => { 50 | COPY_SPECS.forEach(attr => { 51 | expect(specs[attr]).toEqual(refSpecs[attr]); 52 | }); 53 | }); 54 | }); 55 | 56 | it('allows configuring attributes to be copied to subpackages', async () => { 57 | const writeSpecs = require('../utils/writeSpecs').default; 58 | const refSpecs = require(path.join(process.cwd(), 'package.json')); 59 | await prepublish({ src: 'test/fixtures/packages/*', copyAttrs: 'author' }); 60 | expect(writeSpecs.mock.calls.map(args => args[1].name)).toEqual([ 61 | 'oao', 62 | 'oao-b', 63 | 'oao-c', 64 | 'oao-d', 65 | ]); 66 | writeSpecs.mock.calls.forEach(([, specs]) => { 67 | expect(specs.author).toEqual(refSpecs.author); 68 | expect(specs.description).toBeUndefined(); 69 | }); 70 | }); 71 | 72 | it('throws when a package has a version > master', async () => { 73 | try { 74 | await prepublish({ 75 | src: 'test/fixtures/packagesWrongVersion/*', 76 | copyAttrs: COPY_SPECS.join(','), 77 | }); 78 | throw new Error('DID_NOT_THROW'); 79 | } catch (err) { 80 | if (err.message === 'DID_NOT_THROW') throw err; 81 | } 82 | }); 83 | 84 | it('throws when a package has an invalid version', async () => { 85 | try { 86 | await prepublish({ 87 | src: 'test/fixtures/packagesWrongVersion2/*', 88 | copyAttrs: COPY_SPECS.join(','), 89 | }); 90 | throw new Error('DID_NOT_THROW'); 91 | } catch (err) { 92 | if (err.message === 'DID_NOT_THROW') throw err; 93 | } 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/bump.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BUMP command modifies a dep version everywhere it may appear 1`] = ` 4 | Object { 5 | "dependencies": Object { 6 | "ext-one": "*", 7 | "oao": "*", 8 | "timm": "1.x", 9 | }, 10 | "devDependencies": Object { 11 | "ext-two": "18", 12 | "oao-b": "*", 13 | "xxl": "1.x", 14 | }, 15 | "license": "MIT", 16 | "main": "index.js", 17 | "name": "oao-c", 18 | "peerDependencies": Object { 19 | "ext-two": "18", 20 | "oao-b": "*", 21 | }, 22 | "version": "0.1.0", 23 | } 24 | `; 25 | 26 | exports[`BUMP command modifies multiple deps 1`] = ` 27 | Object { 28 | "dependencies": Object { 29 | "ext-one": "18", 30 | "oao": "*", 31 | "timm": "1.x", 32 | }, 33 | "license": "MIT", 34 | "main": "index.js", 35 | "name": "oao-b", 36 | "version": "0.1.0", 37 | } 38 | `; 39 | 40 | exports[`BUMP command modifies multiple deps 2`] = ` 41 | Object { 42 | "dependencies": Object { 43 | "ext-one": "18", 44 | "oao": "*", 45 | "timm": "1.x", 46 | }, 47 | "devDependencies": Object { 48 | "ext-two": "18", 49 | "oao-b": "*", 50 | "xxl": "1.x", 51 | }, 52 | "license": "MIT", 53 | "main": "index.js", 54 | "name": "oao-c", 55 | "peerDependencies": Object { 56 | "ext-two": "18", 57 | "oao-b": "*", 58 | }, 59 | "version": "0.1.0", 60 | } 61 | `; 62 | 63 | exports[`BUMP command modifies package.json if it depends on dep 1`] = ` 64 | Object { 65 | "dependencies": Object { 66 | "ext-one": "18", 67 | "oao": "*", 68 | "timm": "1.x", 69 | }, 70 | "license": "MIT", 71 | "main": "index.js", 72 | "name": "oao-b", 73 | "version": "0.1.0", 74 | } 75 | `; 76 | 77 | exports[`BUMP command modifies package.json if it depends on dep 2`] = ` 78 | Object { 79 | "dependencies": Object { 80 | "ext-one": "18", 81 | "oao": "*", 82 | "timm": "1.x", 83 | }, 84 | "devDependencies": Object { 85 | "ext-two": "*", 86 | "oao-b": "*", 87 | "xxl": "1.x", 88 | }, 89 | "license": "MIT", 90 | "main": "index.js", 91 | "name": "oao-c", 92 | "peerDependencies": Object { 93 | "ext-two": "*", 94 | "oao-b": "*", 95 | }, 96 | "version": "0.1.0", 97 | } 98 | `; 99 | 100 | exports[`BUMP command uses current version for internal deps 1`] = ` 101 | Object { 102 | "dependencies": Object { 103 | "oao": "*", 104 | "timm": "1.x", 105 | }, 106 | "devDependencies": Object { 107 | "oao-b": "*", 108 | "oao-c": "^0.1.0", 109 | "xxl": "1.x", 110 | }, 111 | "license": "MIT", 112 | "main": "index.js", 113 | "name": "oao-d", 114 | "peerDependencies": Object {}, 115 | "scripts": Object {}, 116 | "version": "0.1.0", 117 | } 118 | `; 119 | 120 | exports[`BUMP command uses current version for internal deps 2`] = ` 121 | Object { 122 | "dependencies": Object { 123 | "oao": "*", 124 | "timm": "1.x", 125 | }, 126 | "devDependencies": Object { 127 | "oao-b": "*", 128 | "oao-c": "^0.1.0", 129 | "xxl": "1.x", 130 | }, 131 | "license": "MIT", 132 | "main": "index.js", 133 | "name": "oao-priv", 134 | "peerDependencies": Object {}, 135 | "private": true, 136 | "version": "0.1.0", 137 | } 138 | `; 139 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oao", 3 | "version": "2.0.2", 4 | "description": "A Yarn-based, opinionated monorepo management tool", 5 | "bin": { 6 | "oao": "lib/index.js" 7 | }, 8 | "scripts": { 9 | "build": "yarn lint && yarn flow && yarn compile && yarn test && yarn xxl", 10 | "travis": "yarn test", 11 | "lint": "eslint src", 12 | "flow": "flow check || exit 0", 13 | "compile": "rm -rf lib && babel src -d lib --ignore \"**/__mocks__/**\",\"**/__tests__/**\"", 14 | "compileWatch": "yarn compile --watch", 15 | "jest": "jest --watch --coverage", 16 | "test": "yarn testCovFull", 17 | "testFast": "jest", 18 | "testCovFull": "yarn _testCovPrepare && yarn _testDev && yarn _testCovReport", 19 | "_testCovPrepare": "rm -rf ./coverage .nyc_output .nyc_tmp && mkdir .nyc_tmp", 20 | "_testCovReport": "cp -r .nyc_tmp .nyc_output && nyc report --reporter=html --reporter=lcov --reporter=text", 21 | "_testDev": "yarn _testCov && mv .nyc_tmp/coverage-final.json .nyc_tmp/coverage-dev.json", 22 | "_testCov": "jest --coverage && mv .nyc_output/coverage-final.json .nyc_tmp && rm -rf .nyc_output", 23 | "prettier": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\"", 24 | "example": "node lib all --src \"test/fixtures/packages/*\" ls", 25 | "xxl": "xxl" 26 | }, 27 | "repository": "guigrpa/oao", 28 | "keywords": [ 29 | "monorepo", 30 | "lerna", 31 | "mono-repo", 32 | "yarn", 33 | "publish", 34 | "git", 35 | "workspaces" 36 | ], 37 | "author": "Guillermo Grau Panea", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/guigrpa/oao/issues" 41 | }, 42 | "homepage": "https://github.com/guigrpa/oao#readme", 43 | "dependencies": { 44 | "commander": "2.19.0", 45 | "execa": "0.6.3", 46 | "globby": "6.1.0", 47 | "inquirer": "^6.2.2", 48 | "kebab-case": "1.0.0", 49 | "minimatch": "^3.0.4", 50 | "rimraf": "2.6.3", 51 | "semver": "5.6.0", 52 | "shelljs": "^0.8.4", 53 | "split": "1.0.1", 54 | "storyboard": "^3.3.1", 55 | "storyboard-listener-console": "^3.3.1", 56 | "storyboard-listener-console-parallel": "^3.3.1", 57 | "timm": "^1.6.2" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.5.5", 61 | "@babel/core": "^7.5.5", 62 | "@babel/preset-env": "^7.5.5", 63 | "@babel/preset-flow": "7.0.0", 64 | "babel-core": "7.0.0-bridge.0", 65 | "babel-eslint": "^10.1.0", 66 | "babel-jest": "^24.8.0", 67 | "coveralls": "^3.0.6", 68 | "eslint": "^7.3.1", 69 | "eslint-config-airbnb": "^18.2.0", 70 | "eslint-config-prettier": "^6.11.0", 71 | "eslint-plugin-flowtype": "^5.1.3", 72 | "eslint-plugin-import": "^2.21.2", 73 | "eslint-plugin-jsx-a11y": "^6.3.1", 74 | "eslint-plugin-react": "^7.20.0", 75 | "flow-bin": "^0.93.0", 76 | "jest": "^24.8.0", 77 | "nyc": "10.3.2", 78 | "prettier": "^1.18.2", 79 | "xxl": "^1.3.0" 80 | }, 81 | "jest": { 82 | "clearMocks": true, 83 | "testRegex": "src/.*__tests__/.*\\.(test|spec)\\.(js|jsx)$", 84 | "coverageDirectory": ".nyc_output", 85 | "coverageReporters": [ 86 | "json", 87 | "text", 88 | "html" 89 | ], 90 | "collectCoverageFrom": [ 91 | "src/**/*.js", 92 | "!src/index.js", 93 | "!src/status.js", 94 | "!src/utils/git.js", 95 | "!src/utils/parallelConsoleListener.js", 96 | "!**/node_modules/**", 97 | "!**/__tests__/**", 98 | "!**/__mocks__/**" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/utils/shell.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-underscore-dangle */ 4 | 5 | import path from 'path'; 6 | import shell from 'shelljs'; 7 | import split from 'split'; 8 | import execa from 'execa'; 9 | import { mainStory, chalk } from 'storyboard'; 10 | import type { StoryT } from 'storyboard'; 11 | 12 | const cp = ( 13 | src: string, 14 | dst: string, 15 | { story = mainStory }: { story?: StoryT } = {} 16 | ) => { 17 | story.debug(`Copying ${chalk.cyan.bold(src)} -> ${chalk.cyan.bold(dst)}...`); 18 | shell.cp('-rf', path.normalize(src), path.normalize(dst)); 19 | }; 20 | 21 | const mv = ( 22 | src: string, 23 | dst: string, 24 | { story = mainStory }: { story?: StoryT } = {} 25 | ) => { 26 | story.debug(`Moving ${chalk.cyan.bold(src)} -> ${chalk.cyan.bold(dst)}...`); 27 | shell.mv('-rf', path.normalize(src), path.normalize(dst)); 28 | }; 29 | 30 | type ExecOptions = {| 31 | story?: StoryT, 32 | storySrc?: ?string, 33 | createChildStory?: boolean, 34 | logLevel?: *, 35 | errorLogLevel?: string, 36 | ignoreErrorCode?: boolean, 37 | cwd?: string, 38 | |}; 39 | 40 | type ExecResult = { 41 | code: number, 42 | stdout: string, 43 | stderr: string, 44 | }; 45 | 46 | const exec = async ( 47 | cmd: string, 48 | { 49 | story = mainStory, 50 | storySrc, 51 | createChildStory = true, 52 | logLevel = 'info', 53 | errorLogLevel = 'error', 54 | ignoreErrorCode = false, 55 | cwd, 56 | }: ExecOptions = {} 57 | ): Promise => { 58 | let title = `Run cmd ${chalk.green.bold(cmd)}`; 59 | if (cwd) title += ` at ${chalk.green(cwd)}`; 60 | const ownStory = createChildStory 61 | ? story.child({ title, level: logLevel }) 62 | : story || mainStory; 63 | try { 64 | return await _exec(cmd, { 65 | cwd, 66 | story: ownStory, 67 | storySrc, 68 | errorLogLevel, 69 | ignoreErrorCode, 70 | }); 71 | } finally { 72 | if (createChildStory) ownStory.close(); 73 | } 74 | }; 75 | 76 | const _exec = async ( 77 | cmd, 78 | { cwd, story, storySrc, errorLogLevel, ignoreErrorCode } 79 | ) => { 80 | try { 81 | const src = storySrc || cmd.split(' ')[0].slice(0, 10); 82 | const child = execa.shell(cmd, { 83 | cwd: cwd || '.', 84 | // Workaround for Node.js bug: https://github.com/nodejs/node/issues/10836 85 | // See also: https://github.com/yarnpkg/yarn/issues/2462 86 | stdio: 87 | process.platform === 'win32' ? ['ignore', 'pipe', 'pipe'] : undefined, 88 | }); 89 | child.stdout.pipe(split()).on('data', line => { 90 | story.info(src, line); 91 | }); 92 | child.stderr.pipe(split()).on('data', line => { 93 | if (line) story[errorLogLevel](src, line); 94 | }); 95 | const { code, stdout, stderr } = await child; 96 | if (code !== 0 && !ignoreErrorCode) { 97 | throw execError(cmd, cwd, code, stdout, stderr); 98 | } 99 | return { code, stdout, stderr }; 100 | } catch (err) { 101 | if (err.code && ignoreErrorCode) { 102 | const { code, stdout, stderr } = err; 103 | return { code, stdout, stderr }; 104 | } 105 | const err2 = execError(cmd, cwd, err.code, err.stdout, err.stderr); 106 | story[errorLogLevel](err2.message); 107 | throw err2; 108 | } 109 | }; 110 | 111 | const execError = (cmd, cwd, code, stdout, stderr) => { 112 | const errorMsg = `Command '${cmd}' failed ${ 113 | code != null ? `[${code}]` : '' 114 | } at ${cwd || "'.'"}`; 115 | const err: any = new Error(errorMsg); 116 | err.code = code; 117 | err.stdout = stdout; 118 | err.stderr = stderr; 119 | return err; 120 | }; 121 | 122 | export { cp, mv, exec }; 123 | -------------------------------------------------------------------------------- /src/status.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-console */ 4 | 5 | import { chalk, config as storyboardConfig } from 'storyboard'; 6 | import { readAllSpecs, ROOT_PACKAGE } from './utils/readSpecs'; 7 | import { 8 | gitLastTag, 9 | gitCurBranch, 10 | gitUncommittedChanges, 11 | gitUnpulledChanges, 12 | gitDiffSinceIn, 13 | } from './utils/git'; 14 | 15 | type Options = { 16 | src: string, 17 | ignoreSrc?: string, 18 | }; 19 | 20 | const run = async (opts: Options) => { 21 | storyboardConfig({ filter: '-*' }); 22 | const lastTag = await gitStatus(); 23 | await subpackageStatus(opts, lastTag); 24 | console.log(''); 25 | }; 26 | 27 | const gitStatus = async () => { 28 | console.log(''); 29 | console.log('* Git status:'); 30 | console.log(''); 31 | try { 32 | const branch = await gitCurBranch(); 33 | console.log(` - Current branch: ${chalk.cyan.bold(branch)}`); 34 | } catch (err) { 35 | console.log( 36 | ` - ${chalk.red.bold('Could not be determined')} (is this a git repo?)` 37 | ); 38 | } 39 | let lastTag; 40 | try { 41 | lastTag = await gitLastTag(); 42 | console.log( 43 | ` - Last tag: ${ 44 | lastTag != null 45 | ? chalk.cyan.bold(lastTag) 46 | : chalk.yellow.bold('NONE YET') 47 | }` 48 | ); 49 | } catch (err) { 50 | /* ignore */ 51 | } 52 | try { 53 | const uncommitted = await gitUncommittedChanges(); 54 | console.log( 55 | ` - Uncommitted changes: ${ 56 | uncommitted !== '' ? chalk.yellow.bold('YES') : chalk.cyan.bold('no') 57 | }` 58 | ); 59 | } catch (err) { 60 | /* ignore */ 61 | } 62 | try { 63 | const unpulled = await gitUnpulledChanges(); 64 | console.log( 65 | ` - Unpulled changes: ${ 66 | unpulled !== '0' ? chalk.yellow.bold('YES') : chalk.cyan.bold('no') 67 | }` 68 | ); 69 | } catch (err) { 70 | console.log( 71 | ` - Unpulled changes: ${chalk.yellow.bold('UNKNOWN (no upstream?)')}` 72 | ); 73 | } 74 | return lastTag; 75 | }; 76 | 77 | const subpackageStatus = async (opts: Options, lastTag: ?string) => { 78 | const { src, ignoreSrc } = opts; 79 | let allSpecs; 80 | try { 81 | allSpecs = await readAllSpecs(src, ignoreSrc); 82 | } catch (err) { 83 | if (err.message === 'INVALID_DIR_NAME') { 84 | console.error(`INVALID_DIR_NAME - ${err.details}`); 85 | } 86 | throw err; 87 | } 88 | const pkgNames = Object.keys(allSpecs); 89 | console.log(''); 90 | console.log( 91 | `* Subpackage status: [${chalk.cyan.bold( 92 | pkgNames.length 93 | )} package/s, incl. root]` 94 | ); 95 | console.log(''); 96 | console.log( 97 | chalk.gray( 98 | ' Name Version Private Changes Dependencies' 99 | ) 100 | ); 101 | for (let i = 0; i < pkgNames.length; i++) { 102 | const pkgName = pkgNames[i]; 103 | const { pkgPath, specs } = allSpecs[pkgName]; 104 | let name = pkgName === ROOT_PACKAGE ? 'Root' : pkgName; 105 | name = field(name, 40); 106 | if (pkgName === ROOT_PACKAGE) name = chalk.italic(name); 107 | const version = chalk.cyan.bold(field(specs.version, 14)); 108 | const isPrivate = specs.private 109 | ? chalk.cyan.bold(field('yes', 7)) 110 | : chalk.yellow.bold(field('NO', 7)); 111 | let changes; 112 | if (pkgName !== ROOT_PACKAGE) { 113 | const diff = await gitDiffSinceIn(lastTag, pkgPath); 114 | changes = 115 | diff !== '' 116 | ? chalk.yellow.bold(field(String(diff.split('\n').length), 7)) 117 | : chalk.gray(field('-', 7)); 118 | } else { 119 | changes = chalk.gray(field('N/A', 7)); 120 | } 121 | const { dependencies, devDependencies } = specs; 122 | const numDeps = Object.keys(dependencies || {}).length; 123 | const numDevDeps = Object.keys(devDependencies || {}).length; 124 | let deps = `${chalk.cyan.bold(numDeps)}`; 125 | if (numDevDeps) deps += ` (+ ${chalk.cyan.bold(numDevDeps)} dev)`; 126 | console.log(` ${name} ${version} ${isPrivate} ${changes} ${deps}`); 127 | } 128 | }; 129 | 130 | const field = (str = '', n) => { 131 | if (str.length > n) return `${str.slice(0, n - 1)}…`; 132 | let out = str; 133 | // inefficient, slow, etc. but doesn't matter in this case, and easy to read 134 | while (out.length < n) out += ' '; 135 | return out; 136 | }; 137 | 138 | export default run; 139 | -------------------------------------------------------------------------------- /src/__tests__/bootstrap.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import path from 'path'; 5 | import bootstrap from '../bootstrap'; 6 | 7 | jest.mock('../utils/shell'); 8 | jest.mock('../utils/writeSpecs'); 9 | 10 | const PACKAGE_NAMES_1 = ['oao', 'oao-b', 'oao-c', 'oao-d', 'oao-priv']; 11 | const PACKAGE_NAMES_2 = ['oao', 'oao-b', 'oao-c']; 12 | 13 | const readOriginalSpecs = (base, names) => { 14 | const originalSpecs = {}; 15 | names.forEach(name => { 16 | originalSpecs[name] = require(path.join(base, `${name}/package.json`)); 17 | }); 18 | return originalSpecs; 19 | }; 20 | 21 | const spyFinalSpec = (spy, pkgName) => { 22 | let finalSpec; 23 | spy.mock.calls.forEach(([, specs]) => { 24 | if (specs.name === pkgName) finalSpec = specs; 25 | }); 26 | return finalSpec; 27 | }; 28 | 29 | describe('BOOTSTRAP command', () => { 30 | it('does not modify any package.json', async () => { 31 | const writeSpecs = require('../utils/writeSpecs').default; 32 | const base = path.join(process.cwd(), 'test/fixtures/packages'); 33 | const originalSpecs = readOriginalSpecs(base, PACKAGE_NAMES_1); 34 | await bootstrap({ src: 'test/fixtures/packages/*' }); 35 | // Process call arguments; we keep the last time a spec is stored 36 | // and compare it with the original one 37 | writeSpecs.mock.calls.forEach(([specPath, specs]) => { 38 | const { name } = specs; 39 | expect(specPath.split(path.sep)).toContain(name); 40 | expect(specPath.split(path.sep)).toContain('package.json'); 41 | }); 42 | PACKAGE_NAMES_1.forEach(name => { 43 | const finalSpecWritten = spyFinalSpec(writeSpecs, name); 44 | if (finalSpecWritten === undefined) return; // not changed at all! 45 | expect(finalSpecWritten).toEqual(originalSpecs[name]); 46 | }); 47 | }); 48 | 49 | it('does not modify any package.json with custom links', async () => { 50 | const writeSpecs = require('../utils/writeSpecs').default; 51 | const base = path.join(process.cwd(), 'test/fixtures/packagesCustomLinks'); 52 | const originalSpecs = readOriginalSpecs(base, PACKAGE_NAMES_2); 53 | await bootstrap({ 54 | src: 'test/fixtures/packagesCustomLinks/*', 55 | link: 'ext-.*', 56 | }); 57 | PACKAGE_NAMES_2.forEach(name => { 58 | const finalSpecWritten = spyFinalSpec(writeSpecs, name); 59 | if (finalSpecWritten === undefined) return; // not changed at all! 60 | expect(finalSpecWritten).toEqual(originalSpecs[name]); 61 | }); 62 | }); 63 | 64 | it('executes the correct `yarn link`s and `yarn install`s', async () => { 65 | const helpers = require('../utils/shell'); 66 | await bootstrap({ src: 'test/fixtures/packages/*' }); 67 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 68 | }); 69 | 70 | it('executes the correct `yarn link`s and `yarn install`s in production', async () => { 71 | const helpers = require('../utils/shell'); 72 | await bootstrap({ src: 'test/fixtures/packages/*', production: true }); 73 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 74 | }); 75 | 76 | it('executes the correct `yarn link`s and `yarn install`s with --frozen-lockfile', async () => { 77 | const helpers = require('../utils/shell'); 78 | await bootstrap({ src: 'test/fixtures/packages/*', frozenLockfile: true }); 79 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 80 | }); 81 | 82 | it('executes the correct `yarn link`s and `yarn install`s with --pure-lockfile', async () => { 83 | const helpers = require('../utils/shell'); 84 | await bootstrap({ src: 'test/fixtures/packages/*', pureLockfile: true }); 85 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 86 | }); 87 | 88 | it('executes the correct `yarn link`s and `yarn install`s with --no-lockfile', async () => { 89 | const helpers = require('../utils/shell'); 90 | await bootstrap({ src: 'test/fixtures/packages/*', noLockfile: true }); 91 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 92 | }); 93 | 94 | it('executes the correct `yarn link`s and `yarn install`s with custom links', async () => { 95 | const helpers = require('../utils/shell'); 96 | await bootstrap({ 97 | src: 'test/fixtures/packagesCustomLinks/*', 98 | link: 'ext-.*', 99 | }); 100 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 101 | }); 102 | 103 | it('supports scoped packages', async () => { 104 | const helpers = require('../utils/shell'); 105 | await bootstrap({ src: 'test/fixtures/packagesScoped/*' }); 106 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 107 | }); 108 | 109 | it('just runs `yarn install` when workspaces are configured', async () => { 110 | const helpers = require('../utils/shell'); 111 | await bootstrap({ src: ['test/fixtures/packages/*'], workspaces: true }); 112 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { mainStory, chalk } from 'storyboard'; 4 | import kebabCase from 'kebab-case'; 5 | import semver from 'semver'; 6 | import { readAllSpecs, ROOT_PACKAGE } from './utils/readSpecs'; 7 | import removeInternalLinks from './utils/removeInternalLinks'; 8 | import writeSpecs from './utils/writeSpecs'; 9 | import { exec } from './utils/shell'; 10 | import { runInParallel, runInSeries } from './utils/promises'; 11 | 12 | const PASS_THROUGH_OPTS = [ 13 | 'production', 14 | 'noLockfile', 15 | 'pureLockfile', 16 | 'frozenLockfile', 17 | ]; 18 | 19 | type Options = { 20 | src: string, 21 | ignoreSrc?: string, 22 | link: ?string, 23 | production?: boolean, 24 | noLockfile?: boolean, 25 | pureLockfile?: boolean, 26 | frozenLockfile?: boolean, 27 | parallel?: boolean, 28 | workspaces?: boolean, 29 | }; 30 | 31 | const run = async (opts: Options) => { 32 | const { src, ignoreSrc, link: linkPattern } = opts; 33 | const production = opts.production || process.env.NODE_ENV === 'production'; 34 | 35 | // Almost nothing to do when using yarn workspaces ;) 36 | if (opts.workspaces) { 37 | mainStory.info('Using yarn workspaces...'); 38 | await exec('yarn install'); 39 | return; 40 | } 41 | 42 | // Proceed, the old way 43 | const allSpecs = await readAllSpecs(src, ignoreSrc); 44 | const pkgNames = Object.keys(allSpecs); 45 | const allRemovedDepsByPackage = {}; 46 | const allRemovedDepsByPackageAndType = {}; 47 | 48 | // Pass 0: register all subpackages (yarn link) [PARALLEL] 49 | mainStory.info(`${chalk.bold('PASS 0:')} registering all subpackages...`); 50 | await runInParallel(pkgNames, async pkgName => { 51 | if (pkgName === ROOT_PACKAGE) return; 52 | const { displayName, pkgPath } = allSpecs[pkgName]; 53 | mainStory.info(` - ${chalk.cyan.bold(displayName)}`); 54 | await exec('yarn link', { 55 | cwd: pkgPath, 56 | logLevel: 'trace', 57 | errorLogLevel: 'info', // reduce yarn's log level (stderr) when subpackage is already registered 58 | }); 59 | }); 60 | 61 | // Pass 1: install external deps for all subpackages [PARALLEL] 62 | mainStory.info( 63 | `${chalk.bold('PASS 1:')} installing external dependencies...` 64 | ); 65 | const installer = async pkgName => { 66 | // if (pkgName === ROOT_PACKAGE) return; 67 | const { displayName, pkgPath, specPath, specs: prevSpecs } = allSpecs[ 68 | pkgName 69 | ]; 70 | mainStory.info(` - ${chalk.cyan.bold(displayName)}`); 71 | 72 | // Rewrite package.json without own/linked packages, install, and revert changes 73 | let fModified = false; 74 | try { 75 | const { 76 | nextSpecs, 77 | allRemovedPackages, 78 | removedPackagesByType, 79 | } = removeInternalLinks(prevSpecs, pkgNames, linkPattern); 80 | allRemovedDepsByPackage[pkgName] = allRemovedPackages; 81 | allRemovedDepsByPackageAndType[pkgName] = removedPackagesByType; 82 | if (nextSpecs !== prevSpecs) { 83 | writeSpecs(specPath, nextSpecs); 84 | fModified = true; 85 | } 86 | let cmd = 'yarn install'; 87 | PASS_THROUGH_OPTS.forEach(key => { 88 | if (opts[key]) cmd += ` --${kebabCase(key)}`; 89 | }); 90 | await exec(cmd, { cwd: pkgPath, logLevel: 'trace' }); 91 | } finally { 92 | if (prevSpecs != null && fModified) writeSpecs(specPath, prevSpecs); 93 | } 94 | }; 95 | if (opts.parallel) { 96 | await runInParallel(pkgNames, installer, { waitForAllToResolve: true }); 97 | } else { 98 | await runInSeries(pkgNames, installer); 99 | } 100 | 101 | // Pass 2: link internal and user-specified deps [PARALLEL] 102 | mainStory.info( 103 | `${chalk.bold('PASS 2:')} Installing all internal dependencies...` 104 | ); 105 | await runInParallel(pkgNames, async pkgName => { 106 | const allRemovedPackages = allRemovedDepsByPackage[pkgName]; 107 | const removedPackagesByType = allRemovedDepsByPackageAndType[pkgName]; 108 | const packagesToLink = Object.keys(allRemovedPackages); 109 | const { displayName, pkgPath } = allSpecs[pkgName]; 110 | await runInParallel(packagesToLink, async depName => { 111 | if (production && isPureDevDependency(removedPackagesByType, depName)) { 112 | return; 113 | } 114 | mainStory.info( 115 | ` - ${chalk.cyan.bold(displayName)} -> ${chalk.cyan.bold(depName)}` 116 | ); 117 | const depVersionRange = allRemovedPackages[depName]; 118 | const depSpecs = allSpecs[depName]; // might not exist, if it's a custom link 119 | const depActualVersion = depSpecs ? depSpecs.specs.version : null; 120 | if ( 121 | depActualVersion && 122 | !semver.satisfies(depActualVersion, depVersionRange) 123 | ) { 124 | mainStory.warn( 125 | ` Warning: ${chalk.cyan.bold(`${depName}@${depActualVersion}`)} ` + 126 | `does not satisfy specified range: ${chalk.cyan.bold( 127 | depVersionRange 128 | )}` 129 | ); 130 | } 131 | await exec(`yarn link ${depName}`, { cwd: pkgPath, logLevel: 'trace' }); 132 | }); 133 | }); 134 | }; 135 | 136 | const isPureDevDependency = (deps, depName) => 137 | !( 138 | (deps.dependencies && deps.dependencies[depName]) || 139 | (deps.optionalDependencies && deps.optionalDependencies[depName]) || 140 | (deps.peerDependencies && deps.peerDependencies[depName]) 141 | ); 142 | 143 | export default run; 144 | -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/readSpecs.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`readAllSpecs accepts --src ending with slash 1`] = ` 4 | Object { 5 | "oao": Object { 6 | "displayName": "oao", 7 | "name": "oao", 8 | "pkgPath": "test/fixtures/packages/oao", 9 | "specs": Object { 10 | "license": "MIT", 11 | "main": "index.js", 12 | "name": "oao", 13 | "version": "0.1.0", 14 | }, 15 | }, 16 | "oao-b": Object { 17 | "displayName": "oao-b", 18 | "name": "oao-b", 19 | "pkgPath": "test/fixtures/packages/oao-b", 20 | "specs": Object { 21 | "dependencies": Object { 22 | "oao": "*", 23 | "timm": "1.x", 24 | }, 25 | "license": "MIT", 26 | "main": "index.js", 27 | "name": "oao-b", 28 | "scripts": Object { 29 | "start": "ls -al", 30 | "start2": "ls", 31 | }, 32 | "version": "0.1.0", 33 | }, 34 | }, 35 | "oao-c": Object { 36 | "displayName": "oao-c", 37 | "name": "oao-c", 38 | "pkgPath": "test/fixtures/packages/oao-c", 39 | "specs": Object { 40 | "dependencies": Object { 41 | "oao": "*", 42 | "timm": "1.x", 43 | }, 44 | "devDependencies": Object { 45 | "oao-b": "*", 46 | "xxl": "1.x", 47 | }, 48 | "license": "MIT", 49 | "main": "index.js", 50 | "name": "oao-c", 51 | "peerDependencies": Object { 52 | "oao-b": "*", 53 | }, 54 | "scripts": Object { 55 | "start": "ls -al", 56 | }, 57 | "version": "0.1.0", 58 | }, 59 | }, 60 | "oao-d": Object { 61 | "displayName": "oao-d", 62 | "name": "oao-d", 63 | "pkgPath": "test/fixtures/packages/oao-d", 64 | "specs": Object { 65 | "dependencies": Object { 66 | "oao": "*", 67 | "timm": "1.x", 68 | }, 69 | "devDependencies": Object { 70 | "oao-b": "*", 71 | "oao-c": "*", 72 | "xxl": "1.x", 73 | }, 74 | "license": "MIT", 75 | "main": "index.js", 76 | "name": "oao-d", 77 | "peerDependencies": Object {}, 78 | "scripts": Object {}, 79 | "version": "0.1.0", 80 | }, 81 | }, 82 | "oao-priv": Object { 83 | "displayName": "oao-priv", 84 | "name": "oao-priv", 85 | "pkgPath": "test/fixtures/packages/oao-priv", 86 | "specs": Object { 87 | "dependencies": Object { 88 | "oao": "*", 89 | "timm": "1.x", 90 | }, 91 | "devDependencies": Object { 92 | "oao-b": "*", 93 | "oao-c": "*", 94 | "xxl": "1.x", 95 | }, 96 | "license": "MIT", 97 | "main": "index.js", 98 | "name": "oao-priv", 99 | "peerDependencies": Object {}, 100 | "private": true, 101 | "version": "0.1.0", 102 | }, 103 | }, 104 | } 105 | `; 106 | 107 | exports[`readAllSpecs reads all package.json files as well as metadata 1`] = ` 108 | Object { 109 | "oao": Object { 110 | "displayName": "oao", 111 | "name": "oao", 112 | "pkgPath": "test/fixtures/packages/oao", 113 | "specs": Object { 114 | "license": "MIT", 115 | "main": "index.js", 116 | "name": "oao", 117 | "version": "0.1.0", 118 | }, 119 | }, 120 | "oao-b": Object { 121 | "displayName": "oao-b", 122 | "name": "oao-b", 123 | "pkgPath": "test/fixtures/packages/oao-b", 124 | "specs": Object { 125 | "dependencies": Object { 126 | "oao": "*", 127 | "timm": "1.x", 128 | }, 129 | "license": "MIT", 130 | "main": "index.js", 131 | "name": "oao-b", 132 | "scripts": Object { 133 | "start": "ls -al", 134 | "start2": "ls", 135 | }, 136 | "version": "0.1.0", 137 | }, 138 | }, 139 | "oao-c": Object { 140 | "displayName": "oao-c", 141 | "name": "oao-c", 142 | "pkgPath": "test/fixtures/packages/oao-c", 143 | "specs": Object { 144 | "dependencies": Object { 145 | "oao": "*", 146 | "timm": "1.x", 147 | }, 148 | "devDependencies": Object { 149 | "oao-b": "*", 150 | "xxl": "1.x", 151 | }, 152 | "license": "MIT", 153 | "main": "index.js", 154 | "name": "oao-c", 155 | "peerDependencies": Object { 156 | "oao-b": "*", 157 | }, 158 | "scripts": Object { 159 | "start": "ls -al", 160 | }, 161 | "version": "0.1.0", 162 | }, 163 | }, 164 | "oao-d": Object { 165 | "displayName": "oao-d", 166 | "name": "oao-d", 167 | "pkgPath": "test/fixtures/packages/oao-d", 168 | "specs": Object { 169 | "dependencies": Object { 170 | "oao": "*", 171 | "timm": "1.x", 172 | }, 173 | "devDependencies": Object { 174 | "oao-b": "*", 175 | "oao-c": "*", 176 | "xxl": "1.x", 177 | }, 178 | "license": "MIT", 179 | "main": "index.js", 180 | "name": "oao-d", 181 | "peerDependencies": Object {}, 182 | "scripts": Object {}, 183 | "version": "0.1.0", 184 | }, 185 | }, 186 | "oao-priv": Object { 187 | "displayName": "oao-priv", 188 | "name": "oao-priv", 189 | "pkgPath": "test/fixtures/packages/oao-priv", 190 | "specs": Object { 191 | "dependencies": Object { 192 | "oao": "*", 193 | "timm": "1.x", 194 | }, 195 | "devDependencies": Object { 196 | "oao-b": "*", 197 | "oao-c": "*", 198 | "xxl": "1.x", 199 | }, 200 | "license": "MIT", 201 | "main": "index.js", 202 | "name": "oao-priv", 203 | "peerDependencies": Object {}, 204 | "private": true, 205 | "version": "0.1.0", 206 | }, 207 | }, 208 | } 209 | `; 210 | 211 | exports[`readAllSpecs supports excluding packages 1`] = ` 212 | Object { 213 | "oao": Object { 214 | "displayName": "oao", 215 | "name": "oao", 216 | "pkgPath": "test/fixtures/packages/oao", 217 | "specs": Object { 218 | "license": "MIT", 219 | "main": "index.js", 220 | "name": "oao", 221 | "version": "0.1.0", 222 | }, 223 | }, 224 | "oao-d": Object { 225 | "displayName": "oao-d", 226 | "name": "oao-d", 227 | "pkgPath": "test/fixtures/packages/oao-d", 228 | "specs": Object { 229 | "dependencies": Object { 230 | "oao": "*", 231 | "timm": "1.x", 232 | }, 233 | "devDependencies": Object { 234 | "oao-b": "*", 235 | "oao-c": "*", 236 | "xxl": "1.x", 237 | }, 238 | "license": "MIT", 239 | "main": "index.js", 240 | "name": "oao-d", 241 | "peerDependencies": Object {}, 242 | "scripts": Object {}, 243 | "version": "0.1.0", 244 | }, 245 | }, 246 | "oao-priv": Object { 247 | "displayName": "oao-priv", 248 | "name": "oao-priv", 249 | "pkgPath": "test/fixtures/packages/oao-priv", 250 | "specs": Object { 251 | "dependencies": Object { 252 | "oao": "*", 253 | "timm": "1.x", 254 | }, 255 | "devDependencies": Object { 256 | "oao-b": "*", 257 | "oao-c": "*", 258 | "xxl": "1.x", 259 | }, 260 | "license": "MIT", 261 | "main": "index.js", 262 | "name": "oao-priv", 263 | "peerDependencies": Object {}, 264 | "private": true, 265 | "version": "0.1.0", 266 | }, 267 | }, 268 | } 269 | `; 270 | -------------------------------------------------------------------------------- /src/utils/multiRun.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-constant-condition */ 4 | 5 | import { cpus } from 'os'; 6 | import { removeAllListeners, addListener } from 'storyboard'; 7 | import parallelConsoleListener from 'storyboard-listener-console-parallel'; 8 | import type { OaoSpecs } from './types'; 9 | import { readAllSpecs } from './readSpecs'; 10 | import { exec } from './shell'; 11 | import { shortenName, delay, dependsOn } from './helpers'; 12 | import calcGraph from './calcGraph'; 13 | 14 | type Options = { 15 | src: string, 16 | ignoreSrc?: string, 17 | tree?: boolean, 18 | parallel?: boolean, 19 | parallelLogs?: boolean, 20 | parallelLimit?: number, 21 | ignoreErrors?: boolean, 22 | relativeTime?: boolean, 23 | }; 24 | 25 | type Job = { 26 | cmd: string, 27 | cwd: string, 28 | storySrc?: ?string, 29 | status: 'idle' | 'running' | 'done', 30 | pkg: OaoSpecs, 31 | promise?: Promise, 32 | }; 33 | type JobCreator = (specs: Object) => Array; 34 | 35 | const DELAY_MAIN_LOOP = 20; // [ms] 36 | const PLACEHOLDER_COMMAND = '__OAO_PLACEHOLDER_COMMAND__'; 37 | 38 | // ------------------------------------------------ 39 | // Main 40 | // ------------------------------------------------ 41 | const multiRun = async ( 42 | { 43 | src, 44 | ignoreSrc, 45 | tree: useTree, 46 | parallel, 47 | parallelLogs, 48 | parallelLimit, 49 | ignoreErrors, 50 | relativeTime, 51 | }: Options, 52 | getCommandsForSubpackage: JobCreator 53 | ) => { 54 | if (parallel && parallelLogs) { 55 | removeAllListeners(); 56 | addListener(parallelConsoleListener, { relativeTime }); 57 | } 58 | 59 | // Gather all jobs 60 | const allJobs: Array = []; 61 | const allSpecs = await readAllSpecs(src, ignoreSrc, false); 62 | const pkgNames = useTree ? calcGraph(allSpecs) : Object.keys(allSpecs); 63 | for (let i = 0; i < pkgNames.length; i += 1) { 64 | const pkgName = pkgNames[i]; 65 | const pkg = allSpecs[pkgName]; 66 | const { pkgPath } = pkg; 67 | const storySrc = 68 | parallel && !parallelLogs ? shortenName(pkgName, 20) : undefined; 69 | const commands = getCommandsForSubpackage(pkg.specs); 70 | if (commands.length) { 71 | commands.forEach(cmd => { 72 | allJobs.push({ 73 | cmd, 74 | cwd: pkgPath, 75 | storySrc, 76 | status: 'idle', 77 | pkg, 78 | }); 79 | }); 80 | } else if (useTree) { 81 | // Suppose A --> B --> C (where --> means "depends on"), 82 | // and B generates no jobs, whilst A and C do. 83 | // Creating a placeholder job for B simplifies getNextJob(), 84 | // since it will only need to check direct dependencies between 85 | // subpackages 86 | allJobs.push({ 87 | cmd: PLACEHOLDER_COMMAND, 88 | cwd: pkgPath, 89 | status: 'idle', 90 | pkg, 91 | }); 92 | } 93 | } 94 | 95 | // Run in serial or parallel mode 96 | if (!parallel) { 97 | await runSerially(allJobs, { ignoreErrors }); 98 | } else { 99 | await runInParallel(allJobs, { 100 | ignoreErrors, 101 | parallelLogs, 102 | parallelLimit, 103 | useTree, 104 | }); 105 | } 106 | }; 107 | 108 | // ------------------------------------------------ 109 | // Serial and parallel runners 110 | // ------------------------------------------------ 111 | const runSerially = async (allJobs, { ignoreErrors }) => { 112 | for (let i = 0; i < allJobs.length; i++) { 113 | const job = allJobs[i]; 114 | executeJob(job, { ignoreErrors }); 115 | await job.promise; 116 | } 117 | }; 118 | 119 | const runInParallel = async ( 120 | allJobs, 121 | { ignoreErrors, parallelLogs, parallelLimit, useTree } 122 | ) => { 123 | const maxConcurrency = parallelLimit || Math.max(cpus().length - 1, 1); 124 | while (true) { 125 | // No pending idle jobs? We end the loop; Node will wait for them 126 | // to finish 127 | if (getIdleJobs(allJobs).length === 0) break; 128 | 129 | // Get a job! 130 | const job = getNextJob(allJobs, { useTree }); 131 | if (job) { 132 | if (getRunningJobs(allJobs).length >= maxConcurrency) { 133 | await delay(DELAY_MAIN_LOOP); 134 | continue; 135 | } 136 | executeJob(job, { ignoreErrors }); 137 | } else { 138 | // We still have pending jobs, but cannot run yet (they depend on 139 | // others). Wait a bit... 140 | await delay(DELAY_MAIN_LOOP); 141 | } 142 | } 143 | 144 | // If parallel logs are enabled, we have to manually exit (`process.exit`). 145 | // We should also show the error again, since the parallel console 146 | // most probably swallowed it or only showed the final part. 147 | if (parallelLogs) { 148 | const pendingPromises = allJobs 149 | .filter(o => o.status !== 'done') 150 | .map(job => job.promise); 151 | try { 152 | await Promise.all(pendingPromises); 153 | } catch (err) { 154 | if (err.stderr) { 155 | console.error(err.message); // eslint-disable-line 156 | console.error(err.stderr); // eslint-disable-line 157 | throw new Error(err.message); 158 | } else { 159 | throw err; 160 | } 161 | } 162 | process.exit(0); 163 | } 164 | }; 165 | 166 | // ------------------------------------------------ 167 | // Helpers 168 | // ------------------------------------------------ 169 | /* eslint-disable no-param-reassign */ 170 | const executeJob = (job, { ignoreErrors }) => { 171 | job.promise = _executeJob(job, { ignoreErrors }); 172 | }; 173 | 174 | const _executeJob = async (job, { ignoreErrors }) => { 175 | const { cmd, cwd, storySrc } = job; 176 | if (cmd === PLACEHOLDER_COMMAND) { 177 | job.status = 'done'; 178 | return; 179 | } 180 | const promise = exec(cmd, { cwd, storySrc }); 181 | job.status = 'running'; 182 | try { 183 | await promise; 184 | job.status = 'done'; 185 | } catch (err) { 186 | job.status = 'done'; 187 | if (!ignoreErrors) throw err; 188 | } 189 | }; 190 | /* eslint-enable no-param-reassign */ 191 | 192 | const getNextJob = (jobs, { useTree }) => { 193 | for (let i = 0; i < jobs.length; i++) { 194 | const candidateJob = jobs[i]; 195 | if (candidateJob.status !== 'idle') continue; 196 | const { pkg: candidateJobPkg } = candidateJob; 197 | let isFound = true; 198 | if (useTree) { 199 | // Check whether a previous job that hasn't finished 200 | // belongs to a direct dependency of the candidate (notice 201 | // that we have _placeholder_ jobs, so we don't need to worry 202 | // about packages that are indirect dependencies. 203 | for (let k = 0; k < i; k++) { 204 | const previousJob = jobs[k]; 205 | if (previousJob.status === 'done') continue; 206 | const { pkg: previousJobPkg } = previousJob; 207 | if (dependsOn(candidateJobPkg, previousJobPkg.name)) { 208 | isFound = false; 209 | break; 210 | } 211 | } 212 | } 213 | if (isFound) return candidateJob; 214 | } 215 | return null; 216 | }; 217 | const getRunningJobs = jobs => jobs.filter(job => job.status === 'running'); 218 | const getIdleJobs = jobs => jobs.filter(job => job.status === 'idle'); 219 | 220 | // ------------------------------------------------ 221 | // Public 222 | // ------------------------------------------------ 223 | export default multiRun; 224 | -------------------------------------------------------------------------------- /src/addRemoveUpgrade.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { merge, set as timmSet, setIn } from 'timm'; 4 | import { mainStory, chalk } from 'storyboard'; 5 | import kebabCase from 'kebab-case'; 6 | import { readAllSpecs, readOneSpec, ROOT_PACKAGE } from './utils/readSpecs'; 7 | import { DEP_TYPES } from './utils/constants'; 8 | import { parseDep } from './utils/helpers'; 9 | import removeInternalLinks from './utils/removeInternalLinks'; 10 | import writeSpecs from './utils/writeSpecs'; 11 | import { exec } from './utils/shell'; 12 | 13 | const PASS_THROUGH_OPTS = [ 14 | 'dev', 15 | 'peer', 16 | 'optional', 17 | 'exact', 18 | 'tilde', 19 | 'ignoreEngines', 20 | ]; 21 | 22 | type Operation = 'add' | 'remove' | 'upgrade'; 23 | type Options = { 24 | src: string, 25 | ignoreSrc?: string, 26 | link: ?string, 27 | dev?: boolean, 28 | peer?: boolean, 29 | optional?: boolean, 30 | exact?: boolean, 31 | tilde?: boolean, 32 | ignoreEngines?: boolean, 33 | workspaces?: boolean, 34 | }; 35 | 36 | const run = async ( 37 | pkgName0: string, 38 | op: Operation, 39 | deps: Array, 40 | opts: Options 41 | ) => { 42 | const { src, ignoreSrc, link: linkPattern } = opts; 43 | const pkgName = 44 | pkgName0 === '.' || pkgName0 === 'ROOT' ? ROOT_PACKAGE : pkgName0; 45 | const allSpecs = await readAllSpecs(src, ignoreSrc); 46 | if (!allSpecs[pkgName]) { 47 | mainStory.error(`No such package: ${pkgName}`); 48 | process.exit(1); 49 | } 50 | const { pkgPath, specPath, specs: prevSpecs } = allSpecs[pkgName]; 51 | 52 | // Very little to do when using yarn workspaces ;) 53 | if (opts.workspaces) { 54 | mainStory.info('Using yarn workspaces...'); 55 | const cmd = getYarnCommand(op, deps, opts); 56 | await exec(cmd, { cwd: pkgPath }); 57 | return; 58 | } 59 | 60 | // Proceed, the old way 61 | const pkgNames = Object.keys(allSpecs); 62 | 63 | // Add/remove/upgrade EXTERNAL dependencies: 64 | // 1. Remove internal links from package.json 65 | // 2. Run `yarn add/remove/upgrade` as needed (if it fails, revert to original specs and abort) 66 | // 3. Add the original internal links back to package.json 67 | const externalDeps = deps.filter( 68 | dep => !isLinked(pkgNames, linkPattern, dep) 69 | ); 70 | const externalOperation = 71 | externalDeps.length || (op === 'upgrade' && !deps.length); 72 | if (externalOperation) { 73 | const { nextSpecs, removedPackagesByType } = removeInternalLinks( 74 | prevSpecs, 75 | pkgNames, 76 | linkPattern 77 | ); 78 | let succeeded = false; 79 | try { 80 | if (nextSpecs !== prevSpecs) writeSpecs(specPath, nextSpecs); 81 | mainStory.info(`Executing 'yarn ${op}'...`); 82 | const cmd = getYarnCommand(op, externalDeps, opts); 83 | await exec(cmd, { cwd: pkgPath }); 84 | succeeded = true; 85 | } catch (err) { 86 | /* ignore */ 87 | } 88 | // If unsuccessful, revert to the original specs 89 | if (!succeeded) { 90 | if (prevSpecs != null) writeSpecs(specPath, prevSpecs); 91 | return; 92 | } 93 | // Read the updated package.json, and add the internal deps 94 | const { specs: updatedSpecs } = readOneSpec(pkgPath); 95 | let finalSpecs = updatedSpecs; 96 | Object.keys(removedPackagesByType).forEach(type => { 97 | const removedPackages = removedPackagesByType[type]; 98 | const nextDeps = merge(updatedSpecs[type] || {}, removedPackages); 99 | finalSpecs = timmSet(finalSpecs, type, nextDeps); 100 | }); 101 | writeSpecs(specPath, finalSpecs); 102 | } 103 | 104 | // Add/remove/upgrade INTERNAL dependencies: 105 | const internalDeps = deps.filter(dep => isLinked(pkgNames, linkPattern, dep)); 106 | const internalOperation = 107 | internalDeps.length || (op === 'upgrade' && !deps.length); 108 | if (internalOperation) { 109 | mainStory.info(`Processing '${op}' on internal dependencies...`); 110 | const { specs } = readOneSpec(pkgPath); 111 | let nextSpecs; 112 | switch (op) { 113 | case 'add': 114 | nextSpecs = await addInternal( 115 | specs, 116 | internalDeps, 117 | pkgPath, 118 | allSpecs, 119 | opts 120 | ); 121 | break; 122 | case 'remove': 123 | nextSpecs = await removeInternal(specs, internalDeps, pkgPath); 124 | break; 125 | case 'upgrade': 126 | nextSpecs = upgradeInternal(specs, internalDeps, allSpecs, linkPattern); 127 | break; 128 | default: 129 | throw new Error('INVALID_ADD_REMOVE_UPGRADE_COMMAND'); 130 | } 131 | if (nextSpecs !== prevSpecs) writeSpecs(specPath, nextSpecs); 132 | } 133 | }; 134 | 135 | const addInternal = async (prevSpecs, deps, pkgPath, allSpecs, opts) => { 136 | let nextSpecs = prevSpecs; 137 | for (let i = 0; i < deps.length; i++) { 138 | const { name: depName, version: depVersion0 } = parseDep(deps[i]); 139 | try { 140 | mainStory.info(`Linking ${chalk.cyan.bold(depName)}...`); 141 | await exec(`yarn link ${depName}`, { 142 | cwd: pkgPath, 143 | logLevel: 'trace', 144 | errorLogLevel: 'trace', 145 | }); 146 | } catch (err) { 147 | /* ignore unlink errors */ 148 | } 149 | let depType; 150 | if (opts.dev) depType = 'devDependencies'; 151 | else if (opts.peer) depType = 'peerDependencies'; 152 | else if (opts.optional) depType = 'optionalDependencies'; 153 | else depType = 'dependencies'; 154 | let depVersion = depVersion0; 155 | if (!depVersion) { 156 | depVersion = allSpecs[depName] ? allSpecs[depName].specs.version : '*'; 157 | if (depVersion !== '*') { 158 | if (opts.tilde) depVersion = `~${depVersion}`; 159 | else if (!opts.exact) depVersion = `^${depVersion}`; 160 | } 161 | } 162 | nextSpecs = setIn(nextSpecs, [depType, depName], depVersion); 163 | } 164 | return nextSpecs; 165 | }; 166 | 167 | const removeInternal = async (prevSpecs, deps, pkgPath) => { 168 | let nextSpecs = prevSpecs; 169 | for (let i = 0; i < deps.length; i++) { 170 | const { name: depName } = parseDep(deps[i]); 171 | try { 172 | mainStory.info(`Unlinking ${chalk.cyan.bold(depName)}...`); 173 | await exec(`yarn unlink ${depName}`, { 174 | cwd: pkgPath, 175 | logLevel: 'trace', 176 | errorLogLevel: 'trace', 177 | }); 178 | } catch (err) { 179 | /* ignore unlink errors */ 180 | } 181 | for (let k = 0; k < DEP_TYPES.length; k++) { 182 | const type = DEP_TYPES[k]; 183 | if (!nextSpecs[type]) continue; 184 | nextSpecs = setIn(nextSpecs, [type, depName], undefined); 185 | } 186 | } 187 | return nextSpecs; 188 | }; 189 | 190 | const upgradeInternal = (prevSpecs, deps, allSpecs, linkPattern) => { 191 | const pkgNames = Object.keys(allSpecs); 192 | let nextSpecs = prevSpecs; 193 | const targetVersions = {}; 194 | deps.forEach(dep => { 195 | const { name, version } = parseDep(dep); 196 | targetVersions[name] = version; 197 | }); 198 | DEP_TYPES.forEach(type => { 199 | Object.keys(nextSpecs[type] || {}).forEach(depName => { 200 | if (!isLinked(pkgNames, linkPattern, depName)) return; 201 | let depVersion = targetVersions[depName]; 202 | if (!depVersion && allSpecs[depName]) { 203 | depVersion = `^${allSpecs[depName].specs.version}`; 204 | } 205 | nextSpecs = setIn(nextSpecs, [type, depName], depVersion); 206 | }); 207 | }); 208 | return nextSpecs; 209 | }; 210 | 211 | const isLinked = (pkgNames, linkPattern, dep) => { 212 | const { name: pkgName } = parseDep(dep); 213 | if (pkgNames.indexOf(pkgName) >= 0) return true; 214 | if (linkPattern && new RegExp(linkPattern).test(pkgName)) return true; 215 | return false; 216 | }; 217 | 218 | const getYarnCommand = (op, dependencies, options) => { 219 | let cmd = `yarn ${op}`; 220 | if (dependencies.length) cmd += ` ${dependencies.join(' ')}`; 221 | PASS_THROUGH_OPTS.forEach(key => { 222 | if (options[key]) cmd += ` --${kebabCase(key)}`; 223 | }); 224 | return cmd; 225 | }; 226 | 227 | export default run; 228 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/addRemoveUpgrade.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (external/internal deps, flags) 1`] = ` 4 | Array [ 5 | Array [ 6 | "yarn add mady jest-html --dev", 7 | Object { 8 | "cwd": "test/fixtures/packages2/oao-c", 9 | }, 10 | ], 11 | Array [ 12 | "yarn link oao-b", 13 | Object { 14 | "cwd": "test/fixtures/packages2/oao-c", 15 | "errorLogLevel": "trace", 16 | "logLevel": "trace", 17 | }, 18 | ], 19 | ] 20 | `; 21 | 22 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (external/internal deps, flags) 2`] = ` 23 | Object { 24 | "dependencies": Object { 25 | "oao": "*", 26 | "timm": "1.x", 27 | }, 28 | "devDependencies": Object { 29 | "oao-b": "^0.1.16", 30 | }, 31 | "license": "MIT", 32 | "main": "index.js", 33 | "name": "oao-c", 34 | "version": "0.1.0", 35 | } 36 | `; 37 | 38 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (flags) 1`] = ` 39 | Array [ 40 | Array [ 41 | "yarn add mady jest-html --dev --exact", 42 | Object { 43 | "cwd": "test/fixtures/packages2/oao-b", 44 | }, 45 | ], 46 | ] 47 | `; 48 | 49 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (internal deps, flags) 1`] = ` 50 | Array [ 51 | Array [ 52 | "yarn link oao-b", 53 | Object { 54 | "cwd": "test/fixtures/packages2/oao-c", 55 | "errorLogLevel": "trace", 56 | "logLevel": "trace", 57 | }, 58 | ], 59 | ] 60 | `; 61 | 62 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (internal deps, flags) 2`] = ` 63 | Object { 64 | "dependencies": Object { 65 | "oao": "*", 66 | "timm": "1.x", 67 | }, 68 | "devDependencies": Object { 69 | "oao-b": "^0.1.16", 70 | }, 71 | "license": "MIT", 72 | "main": "index.js", 73 | "name": "oao-c", 74 | "version": "0.1.0", 75 | } 76 | `; 77 | 78 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (internal scoped dep) 1`] = ` 79 | Array [ 80 | Array [ 81 | "yarn link @guigrpa/example-package-b", 82 | Object { 83 | "cwd": "test/fixtures/packagesScoped/example-package", 84 | "errorLogLevel": "trace", 85 | "logLevel": "trace", 86 | }, 87 | ], 88 | ] 89 | `; 90 | 91 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (internal scoped dep) 2`] = ` 92 | Object { 93 | "dependencies": Object { 94 | "@guigrpa/example-package-b": "^1.0.0", 95 | }, 96 | "license": "MIT", 97 | "main": "index.js", 98 | "name": "@guigrpa/example-package", 99 | "private": true, 100 | "version": "1.0.0", 101 | } 102 | `; 103 | 104 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (multiple packages, no flags) 1`] = ` 105 | Array [ 106 | Array [ 107 | "yarn add mady jest-html", 108 | Object { 109 | "cwd": "test/fixtures/packages2/oao-b", 110 | }, 111 | ], 112 | ] 113 | `; 114 | 115 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly (one package, no flags) 1`] = ` 116 | Array [ 117 | Array [ 118 | "yarn add mady", 119 | Object { 120 | "cwd": "test/fixtures/packages2/oao-b", 121 | }, 122 | ], 123 | ] 124 | `; 125 | 126 | exports[`ADD/REMOVE/UPGRADE commands executes ADD correctly with workspaces (one package, flags) 1`] = ` 127 | Array [ 128 | Array [ 129 | "yarn add mady --dev", 130 | Object { 131 | "cwd": "test/fixtures/packages2/oao-b", 132 | }, 133 | ], 134 | ] 135 | `; 136 | 137 | exports[`ADD/REMOVE/UPGRADE commands executes REMOVE correctly (external/internal deps) 1`] = ` 138 | Array [ 139 | Array [ 140 | "yarn remove timm", 141 | Object { 142 | "cwd": "test/fixtures/packages2/oao-c", 143 | }, 144 | ], 145 | Array [ 146 | "yarn unlink oao", 147 | Object { 148 | "cwd": "test/fixtures/packages2/oao-c", 149 | "errorLogLevel": "trace", 150 | "logLevel": "trace", 151 | }, 152 | ], 153 | ] 154 | `; 155 | 156 | exports[`ADD/REMOVE/UPGRADE commands executes REMOVE correctly (internal deps) 1`] = ` 157 | Array [ 158 | Array [ 159 | "yarn unlink oao", 160 | Object { 161 | "cwd": "test/fixtures/packages2/oao-c", 162 | "errorLogLevel": "trace", 163 | "logLevel": "trace", 164 | }, 165 | ], 166 | ] 167 | `; 168 | 169 | exports[`ADD/REMOVE/UPGRADE commands executes REMOVE correctly (internal scoped dep) 1`] = ` 170 | Array [ 171 | Array [ 172 | "yarn unlink @guigrpa/example-package", 173 | Object { 174 | "cwd": "test/fixtures/packagesScoped/example-package-b", 175 | "errorLogLevel": "trace", 176 | "logLevel": "trace", 177 | }, 178 | ], 179 | ] 180 | `; 181 | 182 | exports[`ADD/REMOVE/UPGRADE commands executes REMOVE correctly (internal scoped dep) 2`] = ` 183 | Object { 184 | "dependencies": Object { 185 | "@guigrpa/example-package": undefined, 186 | "timm": "1.x", 187 | }, 188 | "license": "MIT", 189 | "main": "index.js", 190 | "name": "@guigrpa/example-package-b", 191 | "private": true, 192 | "version": "1.0.0", 193 | } 194 | `; 195 | 196 | exports[`ADD/REMOVE/UPGRADE commands executes REMOVE correctly 1`] = ` 197 | Array [ 198 | Array [ 199 | "yarn remove mady", 200 | Object { 201 | "cwd": "test/fixtures/packages2/oao-b", 202 | }, 203 | ], 204 | ] 205 | `; 206 | 207 | exports[`ADD/REMOVE/UPGRADE commands executes REMOVE correctly with workspaces (one package, no flags) 1`] = ` 208 | Array [ 209 | Array [ 210 | "yarn remove mady", 211 | Object { 212 | "cwd": "test/fixtures/packages2/oao-b", 213 | }, 214 | ], 215 | ] 216 | `; 217 | 218 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (external/internal deps, no flags) 1`] = ` 219 | Array [ 220 | Array [ 221 | "yarn upgrade timm", 222 | Object { 223 | "cwd": "test/fixtures/packages2/oao-c", 224 | }, 225 | ], 226 | ] 227 | `; 228 | 229 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (external/internal deps, no flags) 2`] = ` 230 | Object { 231 | "dependencies": Object { 232 | "oao": "2.0.0", 233 | "timm": "1.x", 234 | }, 235 | "license": "MIT", 236 | "main": "index.js", 237 | "name": "oao-c", 238 | "version": "0.1.0", 239 | } 240 | `; 241 | 242 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (internal scoped dep) 1`] = `Array []`; 243 | 244 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (internal scoped dep) 2`] = ` 245 | Object { 246 | "dependencies": Object { 247 | "@guigrpa/example-package": "2", 248 | "timm": "1.x", 249 | }, 250 | "license": "MIT", 251 | "main": "index.js", 252 | "name": "@guigrpa/example-package-b", 253 | "private": true, 254 | "version": "1.0.0", 255 | } 256 | `; 257 | 258 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (no package, no flags) 1`] = ` 259 | Array [ 260 | Array [ 261 | "yarn upgrade", 262 | Object { 263 | "cwd": "test/fixtures/packages2/oao-c", 264 | }, 265 | ], 266 | ] 267 | `; 268 | 269 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (no package, no flags) 2`] = ` 270 | Object { 271 | "dependencies": Object { 272 | "oao": "^0.1.0", 273 | "timm": "1.x", 274 | }, 275 | "license": "MIT", 276 | "main": "index.js", 277 | "name": "oao-c", 278 | "version": "0.1.0", 279 | } 280 | `; 281 | 282 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (one package, flags) 1`] = ` 283 | Array [ 284 | Array [ 285 | "yarn upgrade mady --ignore-engines", 286 | Object { 287 | "cwd": "test/fixtures/packages2/oao-b", 288 | }, 289 | ], 290 | ] 291 | `; 292 | 293 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly (one package, no flags) 1`] = ` 294 | Array [ 295 | Array [ 296 | "yarn upgrade mady", 297 | Object { 298 | "cwd": "test/fixtures/packages2/oao-b", 299 | }, 300 | ], 301 | ] 302 | `; 303 | 304 | exports[`ADD/REMOVE/UPGRADE commands executes UPGRADE correctly with workspaces (one package, no flags) 1`] = ` 305 | Array [ 306 | Array [ 307 | "yarn upgrade mady", 308 | Object { 309 | "cwd": "test/fixtures/packages2/oao-b", 310 | }, 311 | ], 312 | ] 313 | `; 314 | 315 | exports[`ADD/REMOVE/UPGRADE commands touches only the correct package.json 1`] = ` 316 | Object { 317 | "dependencies": Object { 318 | "timm": "1.x", 319 | }, 320 | "license": "MIT", 321 | "main": "index.js", 322 | "name": "oao-b", 323 | "version": "0.1.16", 324 | } 325 | `; 326 | 327 | exports[`ADD/REMOVE/UPGRADE commands touches only the correct package.json 2`] = ` 328 | Object { 329 | "dependencies": Object { 330 | "oao": "*", 331 | "timm": "1.x", 332 | }, 333 | "license": "MIT", 334 | "main": "index.js", 335 | "name": "oao-b", 336 | "version": "0.1.16", 337 | } 338 | `; 339 | 340 | exports[`ADD/REMOVE/UPGRADE commands touches only the correct package.json, with custom links 1`] = ` 341 | Object { 342 | "dependencies": Object { 343 | "timm": "1.x", 344 | }, 345 | "license": "MIT", 346 | "main": "index.js", 347 | "name": "oao-b", 348 | "version": "0.1.0", 349 | } 350 | `; 351 | 352 | exports[`ADD/REMOVE/UPGRADE commands touches only the correct package.json, with custom links 2`] = ` 353 | Object { 354 | "dependencies": Object { 355 | "ext-one": "*", 356 | "oao": "*", 357 | "timm": "1.x", 358 | }, 359 | "license": "MIT", 360 | "main": "index.js", 361 | "name": "oao-b", 362 | "version": "0.1.0", 363 | } 364 | `; 365 | -------------------------------------------------------------------------------- /src/__tests__/addRemoveUpgrade.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import addRemoveUpgrade from '../addRemoveUpgrade'; 5 | 6 | jest.mock('../utils/shell'); 7 | jest.mock('../utils/writeSpecs'); 8 | 9 | describe('ADD/REMOVE/UPGRADE commands', () => { 10 | it('touches only the correct package.json', async () => { 11 | const writeSpecs = require('../utils/writeSpecs').default; 12 | await addRemoveUpgrade('oao-b', 'add', ['mady'], { 13 | src: 'test/fixtures/packages2/*', 14 | }); 15 | expect(writeSpecs.mock.calls).toHaveLength(2); 16 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); // removes internal deps 17 | expect(writeSpecs.mock.calls[1][1]).toMatchSnapshot(); // restores internal deps 18 | }); 19 | 20 | it('touches only the correct package.json, with custom links', async () => { 21 | const writeSpecs = require('../utils/writeSpecs').default; 22 | await addRemoveUpgrade('oao-b', 'add', ['mady'], { 23 | src: 'test/fixtures/packagesCustomLinks/*', 24 | link: 'ext-.*', 25 | }); 26 | expect(writeSpecs.mock.calls).toHaveLength(2); 27 | expect(writeSpecs.mock.calls[0][1]).toMatchSnapshot(); // removes internal deps 28 | expect(writeSpecs.mock.calls[1][1]).toMatchSnapshot(); // restores internal deps 29 | }); 30 | 31 | it('executes ADD correctly (one package, no flags)', async () => { 32 | const helpers = require('../utils/shell'); 33 | await addRemoveUpgrade('oao-b', 'add', ['mady'], { 34 | src: 'test/fixtures/packages2/*', 35 | }); 36 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 37 | }); 38 | 39 | it('executes ADD correctly (multiple packages, no flags)', async () => { 40 | const helpers = require('../utils/shell'); 41 | await addRemoveUpgrade('oao-b', 'add', ['mady', 'jest-html'], { 42 | src: 'test/fixtures/packages2/*', 43 | }); 44 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 45 | }); 46 | 47 | it('executes ADD correctly (flags)', async () => { 48 | const helpers = require('../utils/shell'); 49 | await addRemoveUpgrade('oao-b', 'add', ['mady', 'jest-html'], { 50 | src: 'test/fixtures/packages2/*', 51 | dev: true, 52 | exact: true, 53 | }); 54 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 55 | }); 56 | 57 | it('executes ADD correctly (external/internal deps, flags)', async () => { 58 | const helpers = require('../utils/shell'); 59 | const writeSpecs = require('../utils/writeSpecs').default; 60 | await addRemoveUpgrade('oao-c', 'add', ['mady', 'jest-html', 'oao-b'], { 61 | src: 'test/fixtures/packages2/*', 62 | dev: true, 63 | }); 64 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 65 | expect(writeSpecs).toHaveBeenCalled(); 66 | const finalSpecs = 67 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 68 | expect(finalSpecs).toMatchSnapshot(); 69 | }); 70 | 71 | it('executes ADD correctly (internal deps, flags)', async () => { 72 | const helpers = require('../utils/shell'); 73 | const writeSpecs = require('../utils/writeSpecs').default; 74 | await addRemoveUpgrade('oao-c', 'add', ['oao-b'], { 75 | src: 'test/fixtures/packages2/*', 76 | dev: true, 77 | }); 78 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 79 | const finalSpecs = 80 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 81 | expect(finalSpecs).toMatchSnapshot(); 82 | }); 83 | 84 | it('executes ADD correctly (internal scoped dep)', async () => { 85 | const helpers = require('../utils/shell'); 86 | const writeSpecs = require('../utils/writeSpecs').default; 87 | await addRemoveUpgrade( 88 | '@guigrpa/example-package', 89 | 'add', 90 | ['@guigrpa/example-package-b'], 91 | { 92 | src: 'test/fixtures/packagesScoped/*', 93 | } 94 | ); 95 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 96 | const finalSpecs = 97 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 98 | expect(finalSpecs).toMatchSnapshot(); 99 | }); 100 | 101 | it('executes REMOVE correctly', async () => { 102 | const helpers = require('../utils/shell'); 103 | await addRemoveUpgrade('oao-b', 'remove', ['mady'], { 104 | src: 'test/fixtures/packages2/*', 105 | }); 106 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 107 | }); 108 | 109 | it('executes REMOVE correctly (external/internal deps)', async () => { 110 | const helpers = require('../utils/shell'); 111 | const writeSpecs = require('../utils/writeSpecs').default; 112 | await addRemoveUpgrade('oao-c', 'remove', ['timm', 'oao'], { 113 | src: 'test/fixtures/packages2/*', 114 | }); 115 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 116 | expect(writeSpecs).toHaveBeenCalled(); 117 | const finalSpecs = 118 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 119 | expect(finalSpecs.dependencies.oao).toBeUndefined(); 120 | }); 121 | 122 | it('executes REMOVE correctly (internal deps)', async () => { 123 | const helpers = require('../utils/shell'); 124 | const writeSpecs = require('../utils/writeSpecs').default; 125 | await addRemoveUpgrade('oao-c', 'remove', ['oao'], { 126 | src: 'test/fixtures/packages2/*', 127 | }); 128 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 129 | expect(writeSpecs).toHaveBeenCalled(); 130 | const finalSpecs = 131 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 132 | expect(finalSpecs.dependencies.oao).toBeUndefined(); 133 | }); 134 | 135 | it('executes REMOVE correctly (internal scoped dep)', async () => { 136 | const helpers = require('../utils/shell'); 137 | const writeSpecs = require('../utils/writeSpecs').default; 138 | await addRemoveUpgrade( 139 | '@guigrpa/example-package-b', 140 | 'remove', 141 | ['@guigrpa/example-package'], 142 | { 143 | src: 'test/fixtures/packagesScoped/*', 144 | } 145 | ); 146 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 147 | const finalSpecs = 148 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 149 | expect(finalSpecs).toMatchSnapshot(); 150 | }); 151 | 152 | it('executes UPGRADE correctly (one package, no flags)', async () => { 153 | const helpers = require('../utils/shell'); 154 | await addRemoveUpgrade('oao-b', 'upgrade', ['mady'], { 155 | src: 'test/fixtures/packages2/*', 156 | }); 157 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 158 | }); 159 | 160 | it('executes UPGRADE correctly (one package, flags)', async () => { 161 | const helpers = require('../utils/shell'); 162 | await addRemoveUpgrade('oao-b', 'upgrade', ['mady'], { 163 | src: 'test/fixtures/packages2/*', 164 | ignoreEngines: true, 165 | }); 166 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 167 | }); 168 | 169 | it('executes UPGRADE correctly (external/internal deps, no flags)', async () => { 170 | const helpers = require('../utils/shell'); 171 | const writeSpecs = require('../utils/writeSpecs').default; 172 | await addRemoveUpgrade('oao-c', 'upgrade', ['timm', 'oao@2.0.0'], { 173 | src: 'test/fixtures/packages2/*', 174 | }); 175 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 176 | const finalSpecs = 177 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 178 | expect(finalSpecs).toMatchSnapshot(); 179 | }); 180 | 181 | it('executes UPGRADE correctly (no package, no flags)', async () => { 182 | const helpers = require('../utils/shell'); 183 | const writeSpecs = require('../utils/writeSpecs').default; 184 | await addRemoveUpgrade('oao-c', 'upgrade', [], { 185 | src: 'test/fixtures/packages2/*', 186 | }); 187 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 188 | const finalSpecs = 189 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 190 | expect(finalSpecs).toMatchSnapshot(); 191 | }); 192 | 193 | it('executes UPGRADE correctly (internal scoped dep)', async () => { 194 | const helpers = require('../utils/shell'); 195 | const writeSpecs = require('../utils/writeSpecs').default; 196 | await addRemoveUpgrade( 197 | '@guigrpa/example-package-b', 198 | 'upgrade', 199 | ['@guigrpa/example-package@2'], 200 | { 201 | src: 'test/fixtures/packagesScoped/*', 202 | } 203 | ); 204 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 205 | const finalSpecs = 206 | writeSpecs.mock.calls[writeSpecs.mock.calls.length - 1][1]; 207 | expect(finalSpecs).toMatchSnapshot(); 208 | }); 209 | 210 | it('executes ADD correctly with workspaces (one package, flags)', async () => { 211 | const helpers = require('../utils/shell'); 212 | await addRemoveUpgrade('oao-b', 'add', ['mady'], { 213 | src: ['test/fixtures/packages2/*'], 214 | workspaces: true, 215 | dev: true, 216 | }); 217 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 218 | }); 219 | 220 | it('executes UPGRADE correctly with workspaces (one package, no flags)', async () => { 221 | const helpers = require('../utils/shell'); 222 | await addRemoveUpgrade('oao-b', 'upgrade', ['mady'], { 223 | src: ['test/fixtures/packages2/*'], 224 | workspaces: true, 225 | }); 226 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 227 | }); 228 | 229 | it('executes REMOVE correctly with workspaces (one package, no flags)', async () => { 230 | const helpers = require('../utils/shell'); 231 | await addRemoveUpgrade('oao-b', 'remove', ['mady'], { 232 | src: ['test/fixtures/packages2/*'], 233 | workspaces: true, 234 | }); 235 | expect(helpers.exec.mock.calls).toMatchSnapshot(); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.2 (2021-4-18) 2 | 3 | - Bump shelljs (avoids lots of warnings when running various OAO commands). 4 | 5 | ## 2.0.1 (2021-2-7) 6 | 7 | - Bump storyboard. 8 | 9 | ## 2.0.0 (2020-7-25) 10 | 11 | - **Breaking**: Always publishes _all_ (non-private) sub-packages, irrespective of whether they have been updated. 12 | - **Breaking** (but can opt-out with `--bump-dependent-reqs no`): `oao publish` now bumps inner dependency requirements (i.e. cross-links inside the monorepo). (jeroenptrs, onigoetz, guigrpa, #100). 13 | - `oao publish` now always publishes in reverse-dependency order (jeroenptrs, #102). 14 | 15 | ## 1.10.0 (2020-6-26) 16 | 17 | - Allow 2-factor auth when publishing (jeroenptrs, #99). 18 | - Allow publishing scoped packages publicly (jeroenptrs, #99). 19 | 20 | ## 1.9.0 (2020-6-24) 21 | 22 | - Allow publishing from the `main` branch by default, in addition to `master` (jeroenptrs, #98). 23 | 24 | ## 1.8.0 (2020-1-28) 25 | 26 | - Limit `parallelLimit` by number of cpus by default (SimenB, #92). 27 | 28 | ## 1.7.0 (2019-8-13) 29 | 30 | - Add `--no-bump` for `oao publish`, useful in CI/CD environments (#83). 31 | 32 | ## 1.6.1 (2019-8-11) 33 | 34 | - Bump dependencies. Remove @babel/polyfill since it's not needed in Node 6+. 35 | 36 | ## 1.6.0 (2019-2-20) 37 | 38 | - Add `oao remove-all` (removes a dependency throughout the monorepo) (#80). 39 | 40 | ## 1.5.1 (2018-3-21) 41 | 42 | - Fix bug in `oao all` and `oao run-script` which caused incorrect serial execution (#72). 43 | 44 | ## 1.5.0 (2018-3-19) 45 | 46 | - Add `oao bump` (upgrades a dependency across all sub-packages) (#28). 47 | 48 | ## 1.5.0-beta.0 (2018-3-14) 49 | 50 | - Add `--parallel-limit <#processes>` to `oao all` and `oao run-script`, to limit concurrency when running things in parallel (#69). 51 | - Allow simultaneous usage of `--parallel` and `--tree` for `oao all` and `oao run-script`. In this case, jobs may block if other jobs associated to dependent subpackages are still pending (#68). 52 | - `oao run-script`: add the possibility to generate more than one job per subpackage (e.g. `oao run-script test:*`) (#70). 53 | 54 | ## 1.4.1 (2018-3-12) 55 | 56 | - Fix issues caused by new yarn `workspaces` semantics (#71, #67). 57 | 58 | ## 1.4.0 (2018-2-16) 59 | 60 | - Add `--relative-time` to all commands, shortening the date column in logs by 61 | 14 characters (#64). 62 | 63 | ## 1.3.1 (2018-2-13) 64 | 65 | - Add `--no-checks` for `oao publish` (removes all prepublish checks), useful in 66 | some cases (#62). 67 | 68 | ## 1.3.0 (2018-2-13) 69 | 70 | - Add support for extra arguments in `oao all`, e.g. `oao all ls -- -al` is now 71 | equivalent to `oao all 'ls -al'` (#61). 72 | 73 | ## 1.2.1 (2017-11-24) 74 | 75 | - Remove subpackage prefix in logs generated with `oao all` and `oao run-script` 76 | when not running in parallel. 77 | 78 | ## 1.2.0 (2017-11-24) 79 | 80 | - Improve error logging with `oao all` and `oao run-script` in parallel mode -- 81 | re-print the whole stderr contents at the end (#57). 82 | - Add subpackage prefix to all logs in `oao all` and `oao run-script` (related 83 | to #57). 84 | 85 | ## 1.1.0 (2017-11-24) 86 | 87 | - Add `--tree` to `oao all` and `oao run-script` (follows dependency tree, 88 | starting with the tree leaves and going upwards) (closes issue #58). 89 | 90 | ## 1.0.0 (2017-11-11) 91 | 92 | - Bump to 1.0.0. No breaking changes expected (at least not so often). 93 | 94 | ## 0.10.5 (2017-11-11) 95 | 96 | - Add **`oao run-script`** (#55, @kevroadrunner). 97 | 98 | ## 0.10.4 (2017-10-10) 99 | 100 | - Log error code when external command fails and prevent some log redundancy 101 | (#52). 102 | 103 | ## 0.10.3 (2017-10-6) 104 | 105 | - Improve error detail when running `oao status` and some package has an invalid 106 | `name` in its `package.json` (#40). 107 | 108 | ## 0.10.2 (2017-10-6) 109 | 110 | - Set `process.env.YARN_SILENT` to 0, so that yarn's output is not removed in 111 | some cases (#50, see also https://github.com/yarnpkg/yarn/pull/3536, 112 | https://github.com/yarnpkg/yarn/issues/4615). 113 | - Ignore `yarn outdated`'s non-zero error code when it finds outdated packages 114 | (#50). 115 | 116 | ## 0.10.1 (2017-9-13) 117 | 118 | - Bugfix: in `oao add|remove|upgrade`, fix handling of scoped packages (#45). 119 | 120 | ## 0.10.0 (2017-8-23) 121 | 122 | ## 0.10.0-beta.3 (2017-8-18) 123 | 124 | - Add support for **non-monorepo publishing** Use the `oao publish --single` to 125 | indicate that your root package is _not_ a monorepo, and you can benefit from 126 | oao's features even in normal packages: publishing checks, automatic tagging, 127 | interactive version selection, etc. 128 | 129 | ## 0.10.0-beta.2 (2017-8-17) 130 | 131 | ## 0.10.0-beta.1 (2017-8-16) 132 | 133 | - Add support for **yarn workspaces**. This mode is enabled automatically when 134 | the root package.json has a `workspaces` field, which overrides any other 135 | `src` option. 136 | 137 | ## 0.9.0 (Jul. 15, 2017) 138 | 139 | - Experimentally add the possibility to specify some config options in 140 | package.json (#47). 141 | 142 | ## 0.8.5 (Jun. 16, 2017) 143 | 144 | - Add **`--increment-version-by` option for `oao publish`**. This allows setting 145 | the next version automatically, e.g. in a continuous deployment scheme (#41). 146 | - Add prettier. 147 | 148 | ## 0.8.4 (Jun. 16, 2017) 149 | 150 | - Bugfix: in parallel `oao bootstrap`, recover original subpackage 151 | `package.json` files always, even if one of the subpackages fails to install 152 | (#42). 153 | 154 | ## 0.8.3 (Jun. 15, 2017) 155 | 156 | - Parallelize `oao bootstrap` -- **substantially improved performance** (#42). 157 | - Add support for `--frozen-lockfile`, `--pure-lockfile` and `--no-lockfile` 158 | flags in `oao bootstrap` (see Yarn documentation) (#43). 159 | 160 | ## 0.8.2 (Apr. 14, 2017) 161 | 162 | - Bump deps 163 | 164 | ## 0.8.1 (Apr. 3, 2017) 165 | 166 | - Add `--ignore-src ` option to all commands to exclude sub-packages 167 | (#38). 168 | - Add warning to `oao oudated` for internal deps that do not meet the specified 169 | version range (#34). 170 | 171 | ## 0.8.0 (Mar. 8, 2017) 172 | 173 | - Rename `--version` option (incompatible with `commander`'s default option') to 174 | `--new-version` (#35). 175 | - During `oao publish`, automatically **update the changelog** with the new 176 | version and release date. 177 | - Add **`--no-npm-publish` option to `oao publish`** to prevent accidental 178 | publishing on npm of parts of an all-private monorepo. 179 | - During `oao publish`, also **update the versions of _private_ sub-packages** 180 | that have changed. 181 | 182 | ## 0.7.3 (Mar. 4, 2017) 183 | 184 | - Add more granular configuration options for `oao publish`: 185 | `--no-check-uncommitted`, `--no-check-unpulled`, `--no-git-commit` (#29). 186 | - Add `--version ` option to `oao publish` (overrides manual 187 | version specification) (#30). 188 | 189 | ## 0.7.2 (Mar. 1, 2017) 190 | 191 | - When executing a command, inhibit `stdin` access on Windows (see 192 | [this](https://github.com/nodejs/node/issues/10836) and 193 | [this](https://github.com/yarnpkg/yarn/issues/2462)). 194 | 195 | ## 0.7.1 (Feb. 28, 2017) 196 | 197 | - Add **`oao clean`** to remove all `node_modules` directories in sub-packages. 198 | - Provide **more explicit errors when unhandled rejections occur**. 199 | - Add `--no-confirm` option to `oao reset-all-versions` (#26). 200 | - Extract Parallel Console (now published as 201 | `storyboard-listener-console-parallel` under the Storyboard monorepo). 202 | 203 | ## 0.7.0 (Feb. 27, 2017) 204 | 205 | - Add support for **internal links in `oao add|remove|upgrade`** (#17). 206 | - Add support for `oao add|remove|upgrade` on the root package (use either `.` 207 | or `ROOT` as package name). 208 | 209 | ## 0.6.1 (Feb. 26, 2017) 210 | 211 | - Remove extra blank lines (above the fold) caused when clearing the terminal in 212 | parallel logs (#18). 213 | - Show help when user enters no valid command. 214 | 215 | ## 0.6.0 (Feb. 25, 2017) 216 | 217 | - Also process the monorepo root during `oao bootstrap`, including links to 218 | sub-packages (if any) (#24). 219 | - Modify `oao status` so that it provides more accurate information (e.g. in git 220 | repos with no tags, no upstream, etc.) (#23). 221 | - Warn during `oao bootstrap` when linked package version does not satisfy the 222 | required range (#25). 223 | 224 | ## 0.5.7 (Feb. 24, 2017) 225 | 226 | - Add **`oao outdated`**: runs `yarn outdated` on all sub-packages and the root 227 | package, taking care that internal and custom links are omitted. 228 | - Add **`--production` option** to `oao bootstrap`: skip external and internal 229 | development-only dependencies (also available by setting the `NODE_ENV` 230 | environment variable to `production`) (#19). See also discussion in #16. 231 | - Filter sub-package paths, keeping only those that contain a `package.json` 232 | file (#20). 233 | 234 | ## 0.5.6 (Feb. 23, 2017) 235 | 236 | - Allow `--src` pattern to have a trailing slash (optional). 237 | - Other minor tweaks. 238 | 239 | ## 0.5.5 (Feb. 22, 2017) 240 | 241 | - Add **parallel logging in `oao all`** (can be disabled using the 242 | `--no-parallel-logs` option) (#10). 243 | 244 | ## 0.5.4 (Feb. 21, 2017) 245 | 246 | - Add **parallel support to `oao all`**, using the `--parallel` and 247 | `--ignore-errors` options (#10, #13). 248 | - Bugfix: filter out non-directory paths from globby results (#11). 249 | 250 | ## 0.5.3 (Feb. 20, 2017) 251 | 252 | - Add **`oao status`**: provides lots of information on the monorepo. 253 | - Add **`--link ` option** to force some packages to be linked, not 254 | installed (useful in some development environments). Used in `oao bootstrap` 255 | and `oao add|remove|upgrade`. 256 | - Add **`--ignore-engines` option** to `oao upgrade` (passed through to Yarn). 257 | - Add **`--copy-attrs` option** to `oao prepublish` (attributes that are copied 258 | to the sub-package's `package.json` file). 259 | 260 | ## 0.5.2 (Feb. 16, 2017) 261 | 262 | - Add **tentative support for scoped packages** (#7). 263 | - Internal: 264 | - Add unit tests. 265 | - Add static types (Flow). 266 | 267 | ## 0.5.1 (Feb. 15, 2017) 268 | 269 | - Add **`oao upgrade [deps...]`**. 270 | - Add unit tests, Travis, Coveralls. 271 | 272 | ## 0.5.0 (Feb. 14, 2017) 273 | 274 | - Add **`oao add `**. 275 | - Add **`oao remove `**. 276 | - Bump `storyboard` yet again (some warnings remained). 277 | - Fix missing newlines at the end of `package.json` files (#3). 278 | 279 | ## 0.4.1 (Feb. 13, 2017) 280 | 281 | - Bump `storyboard` (prevents "unmet peer dependency" during installation). 282 | 283 | ## 0.4.0 (Feb. 12, 2017) 284 | 285 | - Greatly reduce the number of oao dependencies by bumping `storyboard` to v3 286 | (prerelease). 287 | - Add **`--publish-tag ` option** to `oao publish`: (publishes with a 288 | custom tag, instead of `latest`). 289 | 290 | ## 0.3.3 (Feb. 12, 2017) 291 | 292 | - Fix bad repo links in `package.json`. 293 | 294 | ## 0.3.2 (Feb. 12, 2017) 295 | 296 | - Bugfixes: 297 | - Fix prerelease version updates. 298 | - Move `babel-polyfill` to `dependencies` (#2). 299 | - Prevent normal `git push` output from being shown as errors. 300 | 301 | ## 0.3.0, 0.3.1 (Feb. 12, 2017) 302 | 303 | - Add options to `oao publish`: 304 | - `--no-master` (allow publishing from non-`master` branches). 305 | - `--no-confirm` (skip confirmation steps). 306 | 307 | ## 0.2.0 (Feb. 10, 2017) 308 | 309 | - Automatically detect updated packages, allow user selection of 310 | major/minor/patch/prerelease increment, commit, tag, push and publish. 311 | - Allow custom package directories. 312 | - Improve docs. 313 | 314 | ## 0.1.0 (Feb. 9, 2017) 315 | 316 | - First public release. 317 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable max-len, global-require, import/no-dynamic-require, no-console */ 4 | 5 | import path from 'path'; 6 | import { addDefaults, merge } from 'timm'; 7 | import program from 'commander'; 8 | import initConsole from './utils/initConsole'; 9 | import { isObject } from './utils/helpers'; 10 | import status from './status'; 11 | import bootstrap from './bootstrap'; 12 | import clean from './clean'; 13 | import addRemoveUpgrade from './addRemoveUpgrade'; 14 | import removeAll from './removeAll'; 15 | import bump from './bump'; 16 | import outdated from './outdated'; 17 | import prepublish from './prepublish'; 18 | import publish from './publish'; 19 | import resetAllVersions from './resetAllVersions'; 20 | import all from './all'; 21 | import runScript from './runScript'; 22 | 23 | process.env.YARN_SILENT = 0; 24 | 25 | const pkg = require('../package.json'); 26 | 27 | const monorepoPkg = require(path.resolve('package.json')); 28 | 29 | const OAO_CONFIG = monorepoPkg.oao || {}; 30 | const DEFAULT_SRC_DIR = OAO_CONFIG.src || 'packages/*'; 31 | const DEFAULT_COPY_ATTRS = 32 | 'description,keywords,author,license,homepage,bugs,repository'; 33 | const DEFAULT_CHANGELOG = 'CHANGELOG.md'; 34 | 35 | program.version(pkg.version); 36 | 37 | // ========================================= 38 | // Helpers 39 | // ========================================= 40 | const processOptions = options0 => { 41 | let options = options0; 42 | 43 | if (options.single) { 44 | options = merge(options, { src: [] }); 45 | } else { 46 | // If workspaces are enabled in the monorepo, some configuration is 47 | // overriden by the monorepo package.json 48 | if (monorepoPkg.workspaces) { 49 | let src = monorepoPkg.workspaces; 50 | if (isObject(src)) src = src.packages; 51 | if (!src) { 52 | throw new Error('Could not find correct config for Yarn workspaces'); 53 | } 54 | options = merge(options, { src, workspaces: true }); 55 | } 56 | 57 | // Add extra configuration in the `oao` field of the monorepo package.json 58 | options = addDefaults(options, { ignoreSrc: OAO_CONFIG.ignoreSrc }); 59 | } 60 | 61 | return options; 62 | }; 63 | 64 | // Create a command with common options 65 | const createCommand = (syntax, description) => 66 | program 67 | .command(syntax) 68 | .description(description) 69 | .option( 70 | '-s --src ', 71 | `glob pattern for sub-package paths [${DEFAULT_SRC_DIR}]`, 72 | DEFAULT_SRC_DIR 73 | ) 74 | .option( 75 | '-i --ignore-src ', 76 | 'glob pattern for sub-package paths that should be ignored' 77 | ) 78 | .option( 79 | '-l --link ', 80 | 'regex pattern for dependencies that should be linked, not installed' 81 | ) 82 | .option('--single', 'no subpackages, just the root one') 83 | .option('--relative-time', 'shorten log dates'); 84 | 85 | // ========================================= 86 | // Commands 87 | // ========================================= 88 | createCommand('status', 'Show an overview of the monorepo status').action( 89 | cmd => { 90 | const options = processOptions(cmd.opts()); 91 | return status(options); 92 | } 93 | ); 94 | 95 | createCommand( 96 | 'bootstrap', 97 | 'Install external dependencies and create internal links' 98 | ) 99 | .option( 100 | '--prod --production', 101 | 'skip external and internal development-only dependencies (also via NODE_ENV=production)' 102 | ) 103 | .option('--no-lockfile', "don't read or generate a lockfile") 104 | .option('--pure-lockfile', "don't generate a lockfile") 105 | .option( 106 | '--frozen-lockfile', 107 | "don't generate a lockfile and fail if an update is needed" 108 | ) 109 | .option( 110 | '--no-parallel', 111 | "don't run yarn install in parallel (use it to debug errors, since parallel logs may be hard to read)" 112 | ) 113 | .action(cmd => { 114 | const options = processOptions(cmd.opts()); 115 | initConsole(options); 116 | return bootstrap(options); 117 | }); 118 | 119 | createCommand( 120 | 'clean', 121 | 'Delete all node_modules directories from sub-packages and the root package' 122 | ).action(cmd => { 123 | const options = processOptions(cmd.opts()); 124 | initConsole(options); 125 | return clean(options); 126 | }); 127 | 128 | createCommand( 129 | 'add ', 130 | 'Add dependencies to a sub-package' 131 | ) 132 | .option('-D --dev', 'add to `devDependencies` instead of `dependencies`') 133 | .option('-P --peer', 'add to `peerDependencies` instead of `dependencies`') 134 | .option( 135 | '-O --optional', 136 | 'add to `optionalDependencies` instead of `dependencies`' 137 | ) 138 | .option('-E --exact', 'install the exact version') 139 | .option( 140 | '-T --tilde', 141 | 'install the most recent release with the same minor version' 142 | ) 143 | .action((subpackage, deps, cmd) => { 144 | const options = processOptions(cmd.opts()); 145 | initConsole(options); 146 | return addRemoveUpgrade(subpackage, 'add', deps, options); 147 | }); 148 | 149 | createCommand( 150 | 'remove ', 151 | 'Remove dependencies from a sub-package' 152 | ).action((subpackage, deps, cmd) => { 153 | const options = processOptions(cmd.opts()); 154 | initConsole(options); 155 | return addRemoveUpgrade(subpackage, 'remove', deps, options); 156 | }); 157 | 158 | createCommand( 159 | 'remove-all ', 160 | 'Remove one or several dependencies throughout the monorepo' 161 | ).action(async (deps, cmd) => { 162 | const options = processOptions(cmd.opts()); 163 | initConsole(options); 164 | await removeAll(deps, options); 165 | return bootstrap(options); 166 | }); 167 | 168 | createCommand( 169 | 'upgrade [packages...]', 170 | 'Upgrade some/all dependencies of a package' 171 | ) 172 | .option('--ignore-engines', 'disregard engines check during upgrade') 173 | .action((subpackage, deps, cmd) => { 174 | const options = processOptions(cmd.opts()); 175 | initConsole(options); 176 | return addRemoveUpgrade(subpackage, 'upgrade', deps, options); 177 | }); 178 | 179 | createCommand( 180 | 'bump ', 181 | 'Upgrade one or several dependencies throughout the monorepo (e.g. react@next, timm)' 182 | ).action(async (deps, cmd) => { 183 | const options = processOptions(cmd.opts()); 184 | initConsole(options); 185 | await bump(deps, options); 186 | return bootstrap(options); 187 | }); 188 | 189 | createCommand('outdated', 'Check for outdated dependencies').action(cmd => { 190 | const options = processOptions(cmd.opts()); 191 | initConsole(options); 192 | return outdated(options); 193 | }); 194 | 195 | createCommand( 196 | 'prepublish', 197 | 'Prepare for a release: validate versions, copy READMEs and package.json attrs' 198 | ) 199 | .option( 200 | '--copy-attrs ', 201 | `copy these package.json attrs to sub-packages [${DEFAULT_COPY_ATTRS}]`, 202 | DEFAULT_COPY_ATTRS 203 | ) 204 | .action(cmd => { 205 | const options = processOptions(cmd.opts()); 206 | initConsole(options); 207 | return prepublish(options); 208 | }); 209 | 210 | createCommand('publish', 'Publish all (non-private) sub-packages') 211 | .option( 212 | '--no-master', 213 | 'allow publishing from a non-master or non-main branch' 214 | ) 215 | .option('--no-check-uncommitted', 'skip uncommitted check') 216 | .option('--no-check-unpulled', 'skip unpulled check') 217 | .option('--no-checks', 'skip all pre-publish checks') 218 | .option( 219 | '--no-bump', 220 | 'do not increment version numbers (also disables git commit)' 221 | ) 222 | .option( 223 | '--bump-dependent-reqs ', 224 | 'bump dependent requirements (inside the monorepo) following this approach: no bumping, exact version, version range (default: range)' 225 | ) 226 | .option('--no-confirm', 'do not ask for confirmation before publishing') 227 | .option('--no-git-commit', 'skip the commit-tag-push step before publishing') 228 | .option('--no-npm-publish', 'skip the npm publish step') 229 | .option( 230 | '--new-version ', 231 | 'use this version for publishing, instead of asking' 232 | ) 233 | .option( 234 | '--increment-version-by ', 235 | 'increment version by this, instead of asking' 236 | ) 237 | .option( 238 | '--publish-tag ', 239 | 'publish with a custom tag (instead of `latest`)' 240 | ) 241 | .option( 242 | '--changelog-path ', 243 | `changelog path [${DEFAULT_CHANGELOG}]`, 244 | DEFAULT_CHANGELOG 245 | ) 246 | .option('--no-changelog', 'skip changelog updates') 247 | .option('--otp ', 'use 2-factor authentication to publish your package') 248 | .option( 249 | '--access ', 250 | 'publish "public" or "restricted" packages' 251 | ) 252 | .action(cmd => { 253 | const options = processOptions(cmd.opts()); 254 | initConsole(options); 255 | return publish(options); 256 | }); 257 | 258 | createCommand( 259 | 'reset-all-versions ', 260 | 'Reset all versions (incl. monorepo package) to the specified one' 261 | ) 262 | .option('--no-confirm', 'do not ask for confirmation') 263 | .action((version, cmd) => { 264 | const options = processOptions(cmd.opts()); 265 | initConsole(options); 266 | return resetAllVersions(version, options); 267 | }); 268 | 269 | createCommand('all ', 'Run a given command on all sub-packages') 270 | .option('--tree', 'follow dependency tree (starting from the tree leaves)') 271 | .option('--parallel', 'run command in parallel on all sub-packages') 272 | .option( 273 | '--no-parallel-logs', 274 | 'use chronological logging, even in parallel mode' 275 | ) 276 | .option('--parallel-limit <#processes>', 'max number of processes to launch') 277 | .option( 278 | '--ignore-errors', 279 | 'do not stop even if there are errors in some packages' 280 | ) 281 | .action((command, cmd) => { 282 | // Extract arguments following the first separator (`--`) and 283 | // add them to the command to be executed 284 | const { rawArgs } = cmd.parent; 285 | const idxSeparator = rawArgs.indexOf('--'); 286 | const finalCommand = 287 | idxSeparator >= 0 288 | ? [command].concat(rawArgs.slice(idxSeparator + 1)).join(' ') 289 | : command; 290 | // Run the `all` command 291 | const options = processOptions(cmd.opts()); 292 | initConsole(options); 293 | return all(finalCommand, options); 294 | }); 295 | 296 | createCommand('run-script ', 'Run a given script on all sub-packages') 297 | .option('--tree', 'follow dependency tree (starting from the tree leaves)') 298 | .option('--parallel', 'run script in parallel on all sub-packages') 299 | .option( 300 | '--no-parallel-logs', 301 | 'use chronological logging, even in parallel mode' 302 | ) 303 | .option('--parallel-limit <#processes>', 'max number of processes to launch') 304 | .option( 305 | '--ignore-errors', 306 | 'do not stop even if there are errors in some packages' 307 | ) 308 | .action((command, cmd) => { 309 | const options = processOptions(cmd.opts()); 310 | initConsole(options); 311 | return runScript(command, options); 312 | }); 313 | 314 | process.on('unhandledRejection', err => { 315 | console.error(err); // eslint-disable-line 316 | process.exit(1); 317 | }); 318 | process.on('SIGINT', () => { 319 | process.exit(0); 320 | }); 321 | 322 | // Syntax error -> show CLI help 323 | program.command('*', '', { noHelp: true }).action(() => program.outputHelp()); 324 | if (process.argv.length <= 2) program.outputHelp(); 325 | 326 | // Let's go! 327 | program.parse(process.argv); 328 | -------------------------------------------------------------------------------- /src/publish.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import semver from 'semver'; 4 | import inquirer from 'inquirer'; 5 | import { mainStory, chalk } from 'storyboard'; 6 | import { readAllSpecs, ROOT_PACKAGE } from './utils/readSpecs'; 7 | import writeSpecs from './utils/writeSpecs'; 8 | import { exec } from './utils/shell'; 9 | import { 10 | gitLastTag, 11 | gitCurBranch, 12 | gitUncommittedChanges, 13 | gitUnpulledChanges, 14 | gitCommitChanges, 15 | gitAddTag, 16 | gitPushWithTags, 17 | } from './utils/git'; 18 | import { addVersionLine } from './utils/changelog'; 19 | import { masterOrMainBranch } from './utils/helpers'; 20 | import { calcGraphAndReturnAsAllSpecs } from './utils/calcGraph'; 21 | import bumpDeps from './bump'; 22 | 23 | const DEBUG_SKIP_CHECKS = false; 24 | const RELEASE_INCREMENTS = ['major', 'minor', 'patch']; 25 | const PRERELEASE_INCREMENTS = ['rc', 'beta', 'alpha']; 26 | const INCREMENTS = [...RELEASE_INCREMENTS, ...PRERELEASE_INCREMENTS]; 27 | 28 | type Options = { 29 | src: string, 30 | ignoreSrc?: string, 31 | link: ?string, 32 | incrementVersionBy?: string, 33 | master: boolean, 34 | checkUncommitted: boolean, 35 | checkUnpulled: boolean, 36 | checks: boolean, 37 | confirm: boolean, 38 | bump: boolean, 39 | bumpDependentReqs: string, 40 | gitCommit: boolean, 41 | newVersion?: string, 42 | npmPublish: boolean, 43 | publishTag?: string, 44 | otp?: string, 45 | access?: string, 46 | changelog: boolean, 47 | changelogPath: string, 48 | single: boolean, 49 | // For unit tests 50 | _date?: ?Object, // overrides the current date 51 | _masterVersion?: string, // overrides the current master version 52 | }; 53 | 54 | const run = async ({ 55 | src, 56 | ignoreSrc, 57 | link, 58 | master, 59 | checkUncommitted, 60 | checkUnpulled, 61 | checks, 62 | confirm, 63 | bump, 64 | bumpDependentReqs, 65 | gitCommit, 66 | newVersion, 67 | npmPublish, 68 | publishTag, 69 | incrementVersionBy, 70 | changelog, 71 | changelogPath, 72 | single, 73 | otp, 74 | access, 75 | _date, 76 | _masterVersion, 77 | }: Options) => { 78 | const allSpecs = calcGraphAndReturnAsAllSpecs( 79 | await readAllSpecs(src, ignoreSrc) 80 | ); 81 | 82 | // Confirm that we have run build and run prepublish checks 83 | if (confirm && !(await confirmBuild())) return; 84 | await prepublishChecks({ checks, master, checkUncommitted, checkUnpulled }); 85 | 86 | // Get list of packages to be updated. This is NOT the list of packages to 87 | // be published, since some of them might be private. But all of them will 88 | // be version-bumped 89 | const pkgList = []; 90 | let numPublic = 0; 91 | Object.keys(allSpecs).forEach(pkgName => { 92 | if (pkgName === ROOT_PACKAGE && !single) return; 93 | pkgList.push(pkgName); 94 | const { specs } = allSpecs[pkgName]; 95 | if (!specs.private) numPublic += 1; 96 | }); 97 | if (!pkgList.length) { 98 | mainStory.info('No packages found!'); 99 | return; 100 | } 101 | 102 | if (bump) { 103 | // Determine a suitable new version number 104 | const lastTag = await gitLastTag(); 105 | const masterVersion = 106 | _masterVersion || (await getMasterVersion(allSpecs, lastTag)); 107 | if (masterVersion == null) return; 108 | if (incrementVersionBy) validateVersionIncrement(incrementVersionBy); 109 | const nextVersion = 110 | newVersion || 111 | calcNextVersion(masterVersion, incrementVersionBy) || 112 | (await promptNextVersion(masterVersion)); 113 | 114 | // Confirm before proceeding 115 | if (confirm && !(await confirmPublish({ pkgList, numPublic, nextVersion }))) 116 | return; 117 | 118 | // Update package.json's for pkgList packages AND THE ROOT PACKAGE 119 | const pkgListPlusRoot = single ? pkgList : pkgList.concat(ROOT_PACKAGE); 120 | pkgListPlusRoot.forEach(pkgName => { 121 | const { specPath, specs } = allSpecs[pkgName]; 122 | specs.version = nextVersion; 123 | writeSpecs(specPath, specs); 124 | }); 125 | 126 | // Bump dependent requirements 127 | if (!single && bumpDependentReqs !== 'no') { 128 | const bumpList = pkgList.map(pkgName => 129 | bumpDependentReqs === 'exact' 130 | ? `${pkgName}@${nextVersion}` 131 | : `${pkgName}@^${nextVersion}` 132 | ); 133 | await bumpDeps(bumpList, { src, ignoreSrc, link }); 134 | } 135 | 136 | // Update changelog 137 | if (changelog) { 138 | addVersionLine({ changelogPath, version: nextVersion, _date }); 139 | } 140 | 141 | // Commit, tag and push 142 | if (gitCommit) { 143 | await gitCommitChanges(`v${nextVersion}`); 144 | await gitAddTag(`v${nextVersion}`); 145 | await gitPushWithTags(); 146 | } 147 | } 148 | 149 | // Publish 150 | if (npmPublish) { 151 | for (let i = 0; i < pkgList.length; i++) { 152 | const pkgName = pkgList[i]; 153 | const { pkgPath, specs } = allSpecs[pkgName]; 154 | if (specs.private) continue; // we don't want npm to complain :) 155 | let cmd = 'npm publish'; 156 | if (publishTag != null) cmd += ` --tag ${publishTag}`; 157 | if (otp != null) cmd += ` --otp ${otp}`; 158 | if (access === 'public' || access === 'restricted') 159 | cmd += ` --access ${access}`; 160 | await exec(cmd, { cwd: pkgPath }); 161 | } 162 | } 163 | }; 164 | 165 | // ------------------------------------------------ 166 | // Helpers 167 | // ------------------------------------------------ 168 | const prepublishChecks = async ({ 169 | checks, 170 | master, 171 | checkUncommitted, 172 | checkUnpulled, 173 | }) => { 174 | if (!checks) return; 175 | if (DEBUG_SKIP_CHECKS) { 176 | mainStory.warn('DEBUG_SKIP_CHECKS should be disabled!!'); 177 | } 178 | 179 | // Check current branch 180 | const branch = await gitCurBranch(); 181 | if (!masterOrMainBranch(branch)) { 182 | if (master) { 183 | mainStory.error( 184 | `Can't publish from current branch: ${chalk.bold(branch)}` 185 | ); 186 | if (!DEBUG_SKIP_CHECKS) throw new Error('BRANCH_CHECK_FAILED'); 187 | } 188 | mainStory.warn( 189 | `Publishing from a non-master or non-main branch: ${chalk.red.bold( 190 | branch 191 | )}` 192 | ); 193 | } else { 194 | mainStory.info(`Current branch: ${chalk.yellow.bold(branch)}`); 195 | } 196 | 197 | // Check that the branch is clean 198 | const uncommitted = await gitUncommittedChanges(); 199 | if (uncommitted !== '') { 200 | if (checkUncommitted) { 201 | mainStory.error( 202 | `Can't publish with uncommitted changes (stash/commit them): \n${chalk.bold( 203 | uncommitted 204 | )}` 205 | ); 206 | if (!DEBUG_SKIP_CHECKS) throw new Error('UNCOMMITTED_CHECK_FAILED'); 207 | } 208 | mainStory.warn('Publishing with uncommitted changes'); 209 | } else { 210 | mainStory.info('No uncommitted changes'); 211 | } 212 | 213 | // Check remote history 214 | const unpulled = await gitUnpulledChanges(); 215 | if (unpulled !== '0') { 216 | if (checkUnpulled) { 217 | mainStory.error('Remote history differs. Please pull changes'); 218 | if (!DEBUG_SKIP_CHECKS) throw new Error('UNPULLED_CHECK_FAILED'); 219 | } 220 | mainStory.warn('Publishing with unpulled changes'); 221 | } else { 222 | mainStory.info('Remote history matches local history'); 223 | } 224 | }; 225 | 226 | const confirmBuild = async () => { 227 | const { confirmBuild: out } = await inquirer.prompt([ 228 | { 229 | name: 'confirmBuild', 230 | type: 'confirm', 231 | message: 'Have you built all your packages for production?', 232 | default: false, 233 | }, 234 | ]); 235 | return out; 236 | }; 237 | 238 | const confirmPublish = async ({ pkgList, numPublic, nextVersion }) => { 239 | const { confirmPublish: out } = await inquirer.prompt([ 240 | { 241 | name: 'confirmPublish', 242 | type: 'confirm', 243 | message: `Confirm release (${chalk.yellow.bold( 244 | pkgList.length 245 | )} package/s, ${chalk.yellow.bold(numPublic)} public, v${chalk.cyan.bold( 246 | nextVersion 247 | )})?`, 248 | default: false, 249 | }, 250 | ]); 251 | return out; 252 | }; 253 | 254 | const validateVersionIncrement = incrementVersionBy => { 255 | if (INCREMENTS.indexOf(incrementVersionBy) < 0) { 256 | mainStory.error( 257 | `Value specified for --increment-version-by: ${chalk.bold( 258 | incrementVersionBy 259 | )} is invalid.` 260 | ); 261 | mainStory.error( 262 | `It should be one of (${INCREMENTS.join(', ')}), or not specified.` 263 | ); 264 | if (!DEBUG_SKIP_CHECKS) throw new Error('INVALID_INCREMENT_BY_VALUE'); 265 | } 266 | }; 267 | 268 | const getMasterVersion = async (allSpecs, lastTag) => { 269 | let masterVersion = allSpecs[ROOT_PACKAGE].specs.version; 270 | if (lastTag != null) { 271 | const tagVersion = semver.clean(lastTag); 272 | mainStory.info(`Last tag found: ${chalk.yellow.bold(lastTag)}`); 273 | if (tagVersion !== masterVersion) { 274 | mainStory.warn( 275 | `Last tagged version ${chalk.cyan.bold( 276 | tagVersion 277 | )} does not match package.json version ${chalk.cyan.bold( 278 | masterVersion 279 | )}` 280 | ); 281 | const { confirm } = await inquirer.prompt([ 282 | { 283 | name: 'confirm', 284 | type: 'confirm', 285 | message: 'Continue?', 286 | default: false, 287 | }, 288 | ]); 289 | if (!confirm) return null; 290 | if (semver.valid(tagVersion) && semver.gt(tagVersion, masterVersion)) { 291 | masterVersion = tagVersion; 292 | } 293 | mainStory.warn( 294 | `Using ${chalk.cyan.bold( 295 | masterVersion 296 | )} as reference (the highest one of both)` 297 | ); 298 | } 299 | } else { 300 | mainStory.warn('Repo has no tags yet'); 301 | } 302 | if (!semver.valid(masterVersion)) { 303 | mainStory.error( 304 | `Master version ${chalk.cyan.bold( 305 | masterVersion 306 | )} is invalid. Please correct it manually` 307 | ); 308 | throw new Error('INVALID_VERSION'); 309 | } 310 | return masterVersion; 311 | }; 312 | 313 | const calcNextVersion = (prevVersion: string, incrementBy = ''): ?string => { 314 | if (!incrementBy) return null; 315 | const isPreRelease = PRERELEASE_INCREMENTS.indexOf(incrementBy) >= 0; 316 | const increment = isPreRelease ? 'prerelease' : incrementBy; 317 | const isNewPreRelease = isPreRelease && prevVersion.indexOf(incrementBy) < 0; 318 | return isNewPreRelease 319 | ? `${semver.inc(prevVersion, 'major')}-${incrementBy}.0` 320 | : semver.inc(prevVersion, increment); 321 | }; 322 | 323 | const promptNextVersion = async (prevVersion: string): Promise => { 324 | const major = semver.inc(prevVersion, 'major'); 325 | const minor = semver.inc(prevVersion, 'minor'); 326 | const patch = semver.inc(prevVersion, 'patch'); 327 | const prerelease = semver.inc(prevVersion, 'prerelease'); 328 | const rc = prevVersion.indexOf('rc') < 0 ? `${major}-rc.0` : prerelease; 329 | const beta = prevVersion.indexOf('beta') < 0 ? `${major}-beta.0` : prerelease; 330 | const alpha = 331 | prevVersion.indexOf('alpha') < 0 ? `${major}-alpha.0` : prerelease; 332 | const { nextVersion } = await inquirer.prompt([ 333 | { 334 | name: 'nextVersion', 335 | type: 'list', 336 | message: `Current version is ${chalk.cyan.bold(prevVersion)}. Next one?`, 337 | choices: [ 338 | { name: `Major (${chalk.cyan.bold(major)})`, value: major }, 339 | { name: `Minor (${chalk.cyan.bold(minor)})`, value: minor }, 340 | { name: `Patch (${chalk.cyan.bold(patch)})`, value: patch }, 341 | { name: `Release candidate (${chalk.cyan.bold(rc)})`, value: rc }, 342 | { name: `Beta (${chalk.cyan.bold(beta)})`, value: beta }, 343 | { name: `Alpha (${chalk.cyan.bold(alpha)})`, value: alpha }, 344 | ], 345 | defaultValue: 2, 346 | }, 347 | ]); 348 | return nextVersion; 349 | }; 350 | 351 | // ------------------------------------------------ 352 | // Public 353 | // ------------------------------------------------ 354 | export default run; 355 | -------------------------------------------------------------------------------- /src/__tests__/publish.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | /* eslint-disable global-require, import/no-dynamic-require */ 3 | 4 | import { merge } from 'timm'; 5 | import publish from '../publish'; 6 | 7 | jest.mock('../utils/shell'); 8 | jest.mock('../utils/writeSpecs'); 9 | jest.mock('../utils/git'); 10 | 11 | const NOMINAL_OPTIONS = { 12 | src: 'test/fixtures/packages/*', 13 | master: true, 14 | checkUncommitted: true, 15 | checkUnpulled: true, 16 | confirm: false, 17 | bump: true, 18 | bumpDependentReqs: 'no', 19 | checks: true, 20 | gitCommit: true, 21 | npmPublish: true, 22 | newVersion: '99.99.99', 23 | changelog: false, 24 | changelogPath: 'test/fixtures/CHANGELOG.md', 25 | _date: new Date('2017-01-01T05:00:00Z'), 26 | _masterVersion: '0.8.2', 27 | }; 28 | const NUM_FIXTURE_SUBPACKAGES = 5; 29 | const NUM_FIXTURE_PRIVATE_SUBPACKAGES = 1; 30 | 31 | describe('PUBLISH command', () => { 32 | let git; 33 | beforeEach(() => { 34 | git = require('../utils/git'); 35 | git._initStubs(); 36 | }); 37 | 38 | it('allows all kind of errors if --no-checks is enabled', async () => { 39 | git._setBranch('non-master'); 40 | git._setUncommitted('SOMETHING_HAS_NOT_YET_BEEN_COMMITTED'); 41 | git._setUnpulled('SOMETHING_HAS_NOT_YET_BEEN_PULLED'); 42 | await publish(merge(NOMINAL_OPTIONS, { checks: false })); 43 | expect(git.gitPushWithTags).toHaveBeenCalled(); 44 | }); 45 | 46 | it('throws when current branch is not master', async () => { 47 | git._setBranch('non-master'); 48 | try { 49 | await publish(NOMINAL_OPTIONS); 50 | throw new Error('DID_NOT_THROW'); 51 | } catch (err) { 52 | if (err.message !== 'BRANCH_CHECK_FAILED') throw err; 53 | } 54 | }); 55 | 56 | it('allows publishing from main', async () => { 57 | git._setBranch('main'); 58 | await publish(merge(NOMINAL_OPTIONS)); 59 | }); 60 | 61 | it('allows overriding the non-master check', async () => { 62 | git._setBranch('non-master'); 63 | await publish(merge(NOMINAL_OPTIONS, { master: false })); 64 | }); 65 | 66 | it('throws with invalid increment by value', async () => { 67 | try { 68 | await publish( 69 | merge(NOMINAL_OPTIONS, { incrementVersionBy: 'argle-bargle' }) 70 | ); 71 | throw new Error('DID_NOT_THROW'); 72 | } catch (err) { 73 | if (err.message !== 'INVALID_INCREMENT_BY_VALUE') throw err; 74 | } 75 | }); 76 | 77 | it('throws with uncommitted changes', async () => { 78 | git._setUncommitted('SOMETHING_HAS_NOT_YET_BEEN_COMMITTED'); 79 | try { 80 | await publish(NOMINAL_OPTIONS); 81 | throw new Error('DID_NOT_THROW'); 82 | } catch (err) { 83 | if (err.message !== 'UNCOMMITTED_CHECK_FAILED') throw err; 84 | } 85 | }); 86 | 87 | it('throws with unpulled changes', async () => { 88 | git._setUnpulled('SOMETHING_HAS_NOT_YET_BEEN_PULLED'); 89 | try { 90 | await publish(NOMINAL_OPTIONS); 91 | throw new Error('DID_NOT_THROW'); 92 | } catch (err) { 93 | if (err.message !== 'UNPULLED_CHECK_FAILED') throw err; 94 | } 95 | }); 96 | 97 | it('performs a commit-tag-push on all sub-packages increasing the version number', async () => { 98 | const writeSpecs = require('../utils/writeSpecs').default; 99 | await publish(NOMINAL_OPTIONS); 100 | expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); 101 | writeSpecs.mock.calls.forEach(([, specs]) => { 102 | expect(specs.version).toEqual('99.99.99'); 103 | }); 104 | expect(git.gitCommitChanges).toHaveBeenCalledTimes(1); 105 | expect(git.gitAddTag).toHaveBeenCalledTimes(1); 106 | expect(git.gitPushWithTags).toHaveBeenCalledTimes(1); 107 | }); 108 | 109 | it('increments version by major when incrementVersionBy is "major" and newVersion is not set', async () => { 110 | const writeSpecs = require('../utils/writeSpecs').default; 111 | const options = Object.assign({}, NOMINAL_OPTIONS, { 112 | newVersion: undefined, 113 | incrementVersionBy: 'major', 114 | }); 115 | await publish(options); 116 | expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); 117 | writeSpecs.mock.calls.forEach(([, specs]) => { 118 | expect(specs.version).toEqual('1.0.0'); 119 | }); 120 | }); 121 | 122 | it('increments version by minor when incrementVersionBy is "minor" and newVersion is not set', async () => { 123 | const writeSpecs = require('../utils/writeSpecs').default; 124 | const options = Object.assign({}, NOMINAL_OPTIONS, { 125 | newVersion: undefined, 126 | incrementVersionBy: 'minor', 127 | }); 128 | await publish(options); 129 | expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); 130 | writeSpecs.mock.calls.forEach(([, specs]) => { 131 | expect(specs.version).toEqual('0.9.0'); 132 | }); 133 | }); 134 | 135 | it('increments version by patch when incrementVersionBy is "patch" and newVersion is not set', async () => { 136 | const writeSpecs = require('../utils/writeSpecs').default; 137 | const options = Object.assign({}, NOMINAL_OPTIONS, { 138 | newVersion: undefined, 139 | incrementVersionBy: 'patch', 140 | }); 141 | await publish(options); 142 | expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); 143 | writeSpecs.mock.calls.forEach(([, specs]) => { 144 | expect(specs.version).toEqual('0.8.3'); 145 | }); 146 | }); 147 | 148 | it('increments version by prerelease when incrementVersionBy is "rc" and newVersion is not set', async () => { 149 | const writeSpecs = require('../utils/writeSpecs').default; 150 | const options = Object.assign({}, NOMINAL_OPTIONS, { 151 | newVersion: undefined, 152 | incrementVersionBy: 'rc', 153 | }); 154 | await publish(options); 155 | expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); 156 | writeSpecs.mock.calls.forEach(([, specs]) => { 157 | expect(specs.version).toEqual('1.0.0-rc.0'); 158 | }); 159 | }); 160 | 161 | it('increments version by prerelease when incrementVersionBy is "beta" and newVersion is not set', async () => { 162 | const writeSpecs = require('../utils/writeSpecs').default; 163 | const options = Object.assign({}, NOMINAL_OPTIONS, { 164 | newVersion: undefined, 165 | incrementVersionBy: 'beta', 166 | }); 167 | await publish(options); 168 | expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); 169 | writeSpecs.mock.calls.forEach(([, specs]) => { 170 | expect(specs.version).toEqual('1.0.0-beta.0'); 171 | }); 172 | }); 173 | 174 | it('increments version by prerelease when incrementVersionBy is "alpha" and newVersion is not set', async () => { 175 | const writeSpecs = require('../utils/writeSpecs').default; 176 | const options = Object.assign({}, NOMINAL_OPTIONS, { 177 | newVersion: undefined, 178 | incrementVersionBy: 'alpha', 179 | }); 180 | await publish(options); 181 | expect(writeSpecs).toHaveBeenCalledTimes(1 + NUM_FIXTURE_SUBPACKAGES); 182 | writeSpecs.mock.calls.forEach(([, specs]) => { 183 | expect(specs.version).toEqual('1.0.0-alpha.0'); 184 | }); 185 | }); 186 | 187 | it('runs `npm publish` on all non-private sub-packages', async () => { 188 | const { exec } = require('../utils/shell'); 189 | await publish(NOMINAL_OPTIONS); 190 | expect(exec).toHaveBeenCalledTimes( 191 | NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES 192 | ); 193 | exec.mock.calls.forEach(([cmd]) => { 194 | expect(cmd).toEqual('npm publish'); 195 | }); 196 | }); 197 | 198 | it('runs `npm publish` following the dependency graph by default', async () => { 199 | const { exec } = require('../utils/shell'); 200 | await publish( 201 | Object.assign({}, NOMINAL_OPTIONS, { 202 | src: 'test/fixtures/packages3/*', 203 | }) 204 | ); 205 | expect(exec).toHaveBeenCalledTimes( 206 | NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES 207 | ); 208 | const paths = exec.mock.calls.map(([, { cwd }]) => cwd); 209 | expect(paths).toEqual([ 210 | 'test/fixtures/packages3/oao-b', 211 | 'test/fixtures/packages3/oao', 212 | 'test/fixtures/packages3/oao-c', 213 | 'test/fixtures/packages3/oao-d', 214 | ]); 215 | }); 216 | 217 | it('runs `npm publish --tag X` on all sub-packages', async () => { 218 | const { exec } = require('../utils/shell'); 219 | await publish(merge(NOMINAL_OPTIONS, { publishTag: 'next' })); 220 | expect(exec).toHaveBeenCalledTimes( 221 | NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES 222 | ); 223 | exec.mock.calls.forEach(([cmd]) => { 224 | expect(cmd).toEqual('npm publish --tag next'); 225 | }); 226 | }); 227 | 228 | it('runs `npm publish --otp X` on all sub-packages', async () => { 229 | const { exec } = require('../utils/shell'); 230 | await publish(merge(NOMINAL_OPTIONS, { otp: '123456' })); 231 | expect(exec).toHaveBeenCalledTimes( 232 | NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES 233 | ); 234 | exec.mock.calls.forEach(([cmd]) => { 235 | expect(cmd).toEqual('npm publish --otp 123456'); 236 | }); 237 | }); 238 | 239 | it('runs `npm publish --access X` on all sub-packages', async () => { 240 | const { exec } = require('../utils/shell'); 241 | await publish(merge(NOMINAL_OPTIONS, { access: 'public' })); 242 | expect(exec).toHaveBeenCalledTimes( 243 | NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES 244 | ); 245 | exec.mock.calls.forEach(([cmd]) => { 246 | expect(cmd).toEqual('npm publish --access public'); 247 | }); 248 | }); 249 | 250 | it('does not run `npm publish --access X` with bogus access param', async () => { 251 | const { exec } = require('../utils/shell'); 252 | await publish(merge(NOMINAL_OPTIONS, { access: 'bogus' })); 253 | expect(exec).toHaveBeenCalledTimes( 254 | NUM_FIXTURE_SUBPACKAGES - NUM_FIXTURE_PRIVATE_SUBPACKAGES 255 | ); 256 | exec.mock.calls.forEach(([cmd]) => { 257 | expect(cmd).toEqual('npm publish'); 258 | }); 259 | }); 260 | 261 | it('skips `npm publish` when using --no-npm-publish', async () => { 262 | const { exec } = require('../utils/shell'); 263 | await publish(merge(NOMINAL_OPTIONS, { npmPublish: false })); 264 | expect(exec).not.toHaveBeenCalled(); 265 | }); 266 | 267 | it('updates the changelog correctly', async () => { 268 | const fs = require('fs'); 269 | const { writeFileSync } = fs; 270 | let calls; 271 | try { 272 | fs.writeFileSync = jest.fn(); 273 | await publish(merge(NOMINAL_OPTIONS, { changelog: true })); 274 | ({ calls } = fs.writeFileSync.mock); 275 | } finally { 276 | fs.writeFileSync = writeFileSync; 277 | } 278 | expect(calls).toMatchSnapshot(); 279 | }); 280 | 281 | it('publishes non-monorepo packages', async () => { 282 | const { exec } = require('../utils/shell'); 283 | await publish(merge(NOMINAL_OPTIONS, { src: [], single: true })); 284 | expect(exec).toHaveBeenCalledTimes(1); 285 | exec.mock.calls.forEach(([cmd]) => { 286 | expect(cmd).toEqual('npm publish'); 287 | }); 288 | }); 289 | 290 | it('does not bump versions when using --no-bump', async () => { 291 | const writeSpecs = require('../utils/writeSpecs').default; 292 | const { exec } = require('../utils/shell'); 293 | await publish(merge(NOMINAL_OPTIONS, { bump: false })); 294 | expect(writeSpecs).not.toHaveBeenCalled(); 295 | expect(git.gitPushWithTags).not.toHaveBeenCalled(); 296 | exec.mock.calls.forEach(([cmd]) => { 297 | expect(cmd).toEqual('npm publish'); 298 | }); 299 | }); 300 | 301 | it('bumps cross-deps using ranges by default', async () => { 302 | const writeSpecs = require('../utils/writeSpecs').default; 303 | await publish( 304 | merge(NOMINAL_OPTIONS, { 305 | src: 'test/fixtures/packages2/*', 306 | bumpDependentReqs: null, 307 | }) 308 | ); 309 | expect(writeSpecs).toHaveBeenCalledTimes(6); 310 | writeSpecs.mock.calls.slice(0, 4).forEach(([, specs]) => { 311 | expect(specs.version).toEqual('99.99.99'); 312 | }); 313 | expect(writeSpecs.mock.calls[4][1].dependencies.oao).toEqual('^99.99.99'); 314 | expect(writeSpecs.mock.calls[5][1].dependencies.oao).toEqual('^99.99.99'); 315 | }); 316 | 317 | it('bumps cross-deps using exact versions with a flag', async () => { 318 | const writeSpecs = require('../utils/writeSpecs').default; 319 | await publish( 320 | merge(NOMINAL_OPTIONS, { 321 | src: 'test/fixtures/packages2/*', 322 | bumpDependentReqs: 'exact', 323 | }) 324 | ); 325 | expect(writeSpecs).toHaveBeenCalledTimes(6); 326 | writeSpecs.mock.calls.slice(0, 4).forEach(([, specs]) => { 327 | expect(specs.version).toEqual('99.99.99'); 328 | }); 329 | expect(writeSpecs.mock.calls[4][1].dependencies.oao).toEqual('99.99.99'); 330 | expect(writeSpecs.mock.calls[5][1].dependencies.oao).toEqual('99.99.99'); 331 | }); 332 | }); 333 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oao :package: [![Build Status](https://travis-ci.org/guigrpa/oao.svg?branch=master)](https://travis-ci.org/guigrpa/oao) [![Coverage Status](https://coveralls.io/repos/github/guigrpa/oao/badge.svg?branch=master)](https://coveralls.io/github/guigrpa/oao?branch=master) [![npm version](https://img.shields.io/npm/v/oao.svg)](https://www.npmjs.com/package/oao) 2 | 3 | ![oao all --parallel](https://raw.githubusercontent.com/guigrpa/oao/master/docs/parallel.gif) 4 | 5 | A Yarn-based, opinionated monorepo management tool. 6 | 7 | ## Why? :sparkles: 8 | 9 | - Works with **yarn**, hence (relatively) **fast!**. 10 | - **Simple to use** and extend (hope so!). 11 | - Provides a number of monorepo **workflow enhancers**: installing all dependencies, adding/removing/upgrading sub-package dependencies, validating version numbers, determining updated sub-packages, publishing everything at once, updating the changelog, etc. 12 | - Supports **yarn workspaces**, optimising the monorepo dependency tree as a whole and simplifying bootstrap as well as dependency add/upgrade/remove. 13 | - **Prevents some typical publish errors** (using a non-master branch, uncommitted/non-pulled changes). 14 | - Runs a command or `package.json` script on all sub-packages, **serially or in parallel**, optionally following the inverse dependency tree. 15 | - Provides an easy-to-read, **detailed status overview**. 16 | - Support for **non-monorepo publishing**: benefit from _oao_'s pre-publish checks, tagging, version selection, changelog updates, etc. also in your single-package, non-monorepos. 17 | 18 | ## Assumptions :thought_balloon: 19 | 20 | As stated in the tagline, _oao_ is somewhat opinionated and makes the following assumptions on your monorepo: 21 | 22 | - It uses a **synchronized versioning scheme**. In other words: a _master version_ is configured in the root-level `package.json`, and sub-packages will be in sync with that version (whenever they are updated). Some sub-packages can be _left behind_ version-wise if they're not updated, but they'll jump to the master version when they get some love. 23 | - You use **git** for version control and have already initialised your repo. 24 | - **Git tags** are used for releases (and _only_ for releases), and follow semver: `v0.1.3`, `v2.3.5`, `v3.1.0-rc.1` and so on. 25 | - Some sub-packages may be public, others private (flagged `"private": true` in `package.json`). OK, _no assumption here_: rest assured that no private sub-packages will be published by mistake. 26 | 27 | ## Installation 28 | 29 | If _yarn_ is not installed in your system, please [install it first](https://yarnpkg.com/en/docs/install). If you want to use [**yarn workspaces**](https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/) (available since yarn 0.28), enable them by running `yarn config set workspaces-experimental true` and configure the following in your monorepo package.json (replace the glob patterns for your subpackages/workspaces as needed): 30 | 31 | ``` 32 | "workspaces": [ 33 | "packages/*" 34 | ] 35 | ``` 36 | 37 | Add **oao** to your development dependencies (use the `-W` flag to avoid Yarn's warning when installing dependencies on the monorepo root): 38 | 39 | ```sh 40 | $ yarn add oao --dev -W 41 | ``` 42 | 43 | ## Usage 44 | 45 | To see all CLI options, run `oao --help`: 46 | 47 | ``` 48 | Usage: oao [options] [command] 49 | 50 | Options: 51 | 52 | -V, --version output the version number 53 | -h, --help output usage information 54 | 55 | Commands: 56 | 57 | status [options] Show an overview of the monorepo status 58 | bootstrap [options] Install external dependencies and create internal links 59 | clean [options] Delete all node_modules directories from sub-packages and the root package 60 | add [options] Add dependencies to a sub-package 61 | remove [options] Remove dependencies from a sub-package 62 | upgrade [options] [packages...] Upgrade some/all dependencies of a package 63 | outdated [options] Check for outdated dependencies 64 | prepublish [options] Prepare for a release: validate versions, copy READMEs and package.json attrs 65 | publish [options] Publish updated sub-packages 66 | reset-all-versions [options] Reset all versions (incl. monorepo package) to the specified one 67 | all [options] Run a given command on all sub-packages 68 | run-script [options] Run a given script on all sub-packages 69 | ``` 70 | 71 | You can also get help from particular commands, which may have additional options, e.g. `oao publish --help`: 72 | 73 | ``` 74 | Usage: publish [options] 75 | 76 | Publish all (non-private) sub-packages 77 | 78 | Options: 79 | 80 | -s --src glob pattern for sub-package paths [packages/*] (default: "packages/*") 81 | -i --ignore-src glob pattern for sub-package paths that should be ignored 82 | -l --link regex pattern for dependencies that should be linked, not installed 83 | --single no subpackages, just the root one 84 | --relative-time shorten log dates 85 | --no-master allow publishing from a non-master or non-main branch 86 | --no-check-uncommitted skip uncommitted check 87 | --no-check-unpulled skip unpulled check 88 | --no-checks skip all pre-publish checks 89 | --no-bump do not increment version numbers (also disables git commit) 90 | --no-confirm do not ask for confirmation before publishing 91 | --no-git-commit skip the commit-tag-push step before publishing 92 | --no-npm-publish skip the npm publish step 93 | --new-version use this version for publishing, instead of asking 94 | --increment-version-by increment version by this, instead of asking 95 | --publish-tag publish with a custom tag (instead of `latest`) 96 | --changelog-path changelog path [CHANGELOG.md] (default: "CHANGELOG.md") 97 | --no-changelog skip changelog updates 98 | --otp use 2-factor authentication to publish your package 99 | --access publish public or restricted packages 100 | -h, --help output usage information 101 | ``` 102 | 103 | ## Main commands 104 | 105 | In recent versions of npm, remember that you can run oao commands conveniently with the `npx` tool: 106 | 107 | ```sh 108 | $ npx oao bootstrap 109 | $ npx oao add my-subpackage my-new-dependency --dev 110 | $ npx oao publish 111 | ``` 112 | 113 | This uses the local oao package inside your monorepo. 114 | 115 | ### `oao status` 116 | 117 | Provides lots of information on the git repo (current branch, last tag, uncommitted/unpulled changes) and subpackage status (version, private flag, changes since last tag, dependencies). 118 | 119 | ![oao status](https://raw.githubusercontent.com/guigrpa/oao/master/docs/status.png) 120 | 121 | ### `oao bootstrap` 122 | 123 | Installs all sub-package dependencies using **yarn**. External dependencies are installed normally, whereas those belonging to the monorepo itself (and custom links specified with the `--link` option) are `yarn link`ed. Note that dependencies may end up in different places depending on whether you use [yarn workspaces](https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/) or not (see above). 124 | 125 | Development-only dependencies can be skipped by enabling the `--production` option, or setting the `NODE_ENV` environment variable to `production`. Other flags that are passed through to `yarn install` include `--frozen-lockfile`, `--pure-lockfile` and `--no-lockfile`. 126 | 127 | ### `oao clean` 128 | 129 | Removes `node_modules` directories from all sub-packages, as well as from the root package. 130 | 131 | ### `oao add ` 132 | 133 | Adds one or several dependencies to a sub-package. For external dependencies, it passes through [`yarn add`'s flags](https://yarnpkg.com/en/docs/cli/add). Internal dependencies are linked. Examples: 134 | 135 | ```sh 136 | $ oao add subpackage-1 jest --dev 137 | $ oao add subpackage-2 react subpackage-1 --exact 138 | ``` 139 | 140 | ### `oao remove ` 141 | 142 | Removes one or several dependencies from a sub-package. Examples: 143 | 144 | ```sh 145 | $ oao remove subpackage-1 jest 146 | $ oao remove subpackage-2 react subpackage-1 147 | ``` 148 | 149 | ### `oao remove-all ` 150 | 151 | Remove one or deveral dependencies from the monorepo (root and subpackages). It automatically runs `oao bootstrap` after upgrading the `package.json` files as needed. Examples: 152 | 153 | ```sh 154 | $ oao remove-all leftpad 155 | $ oao remove-all leftpad rightpad centerpad 156 | ``` 157 | 158 | ### `oao upgrade [deps...]` 159 | 160 | Upgrade one/several/all dependencies of a sub-package. For external dependencies, it will download the upgraded dependency using yarn. For internal dependencies, it will just update the sub-package's `package.json` file. Examples: 161 | 162 | ```sh 163 | $ oao upgrade subpackage-1 jest@18.0.1 164 | $ oao upgrade subpackage-2 react subpackage-1@3.1.2 165 | $ oao upgrade subpackage-3 166 | ``` 167 | 168 | ### `oao bump ` 169 | 170 | Upgrade one or several dependencies to either their latest version or to a specific version range. In case of internal dependencies, if no version range is given the current version will be used. It automatically runs `oao bootstrap` after upgrading the `package.json` files as needed. Examples: 171 | 172 | ```sh 173 | $ oao bump moment 174 | $ oao bump react@^16 react-dom@^16 175 | $ oao bump subpackage-2 176 | ``` 177 | 178 | ### `oao outdated` 179 | 180 | Runs `yarn outdated` on all sub-packages, as well as the root package. 181 | 182 | ### `oao prepublish` 183 | 184 | Carries out a number of chores that are needed before publishing: 185 | 186 | - Checks that all version numbers are valid and <= the master version. 187 | - Copies `/README.md` to the _main_ sub-package (the one having the same name as the monorepo). 188 | - Copies `/README-LINK.md` to all other sub-packages. 189 | - Copies several fields from the root `package.json` to all other `package.json` files: `description`, `keywords`, `author`, `license`, `homepage`, `bugs`, `repository`. 190 | 191 | ### `oao publish` 192 | 193 | Carries out a number of steps: 194 | 195 | - Asks the user for confirmation that it has _built_ all sub-packages for publishing (using something like `yarn build`). 196 | - Performs a number of checks: 197 | - The current branch should be `master` or `main`. 198 | - No uncommitted changes should remain in the working directory. 199 | - No unpulled changes should remain. 200 | - Determines which sub-packages need publishing (those which have changed with respect to the last tagged version). 201 | - Asks the user for an incremented master version (major, minor, patch or pre-release major), that will be used for the root package as well as all updated sub-packages. 202 | - Asks the user for final confirmation before publishing. 203 | - Updates versions in `package.json` files, commits the updates, adds a tag and pushes all the changes. 204 | - Publishes updated sub-packages. 205 | 206 | There are lots of custom options for `oao publish`. Chances are, you can disable each one of the previous steps by means of one of those options. Check them all with `oao publish --help`. 207 | 208 | **Note: There is a problem when running `oao publish` as a script run with `yarn`. As a workaround, either run `oao publish` manually from the command line, or put it in a script and run it with `npm`, not `yarn`.** 209 | 210 | ### `oao all ` 211 | 212 | Executes the specified command on all sub-packages (private ones included), with the sub-package's root as _current working directory_. Examples: 213 | 214 | ```sh 215 | $ oao all ls 216 | $ oao all "ls -al" 217 | $ oao all "yarn run compile" 218 | $ oao all --tree "yarn run compile" 219 | ``` 220 | 221 | By default, `oao all` runs sequentially. Sometimes you must run commands in parallel, for example when you want to compile all sub-packages with a _watch_ option: 222 | 223 | ```sh 224 | $ oao all "yarn run compileWatch" --parallel 225 | ``` 226 | 227 | **Note: some terminals may have problems with parallel logs (based on [terminal-kit](https://github.com/cronvel/terminal-kit)). If you experience issues, use the `--no-parallel-logs` flag. If you're using the default terminal or Hyper on OS X or Windows, you should be fine.** 228 | 229 | Use `--tree` if you want to follow the inverse dependency tree (starting from the tree leaves). 230 | 231 | You can also pass extra arguments to the command separating them with a `--`: `oao all ls -- -al` is equivalent to `oao all 'ls -al'`. This can be useful for adding extra commands to scripts in `package.json`. 232 | 233 | ### `oao run-script