├── .nvmrc ├── .yarnrc ├── packages ├── lib │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── fns.js │ │ ├── packages │ │ │ ├── clean-packages.js │ │ │ ├── index.js │ │ │ ├── clean-package.js │ │ │ ├── build-package.js │ │ │ └── resolve-packages.js │ │ ├── logic.js │ │ ├── objects.js │ │ ├── arrays.js │ │ ├── index.js │ │ ├── errors.js │ │ ├── plugins │ │ │ ├── develop-build │ │ │ │ └── index.js │ │ │ ├── clean-build │ │ │ │ └── index.js │ │ │ ├── develop-server │ │ │ │ ├── index.js │ │ │ │ └── develop.js │ │ │ ├── utils.js │ │ │ ├── resolve-plugin.js │ │ │ └── script │ │ │ │ └── index.js │ │ ├── strings.js │ │ ├── fs.js │ │ ├── config │ │ │ ├── get-package-roots.js │ │ │ ├── utils.js │ │ │ └── index.js │ │ ├── childProcess.js │ │ ├── colors.js │ │ ├── types.js │ │ └── terminal.js │ └── package.json ├── utils │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── index.js │ │ ├── load-env-vars.js │ │ ├── get-web-package-manifest.js │ │ └── configure-graceful-exit.js │ └── package.json ├── cli │ ├── README.md │ ├── src │ │ ├── lib │ │ │ ├── handle-error.js │ │ │ ├── prevent-script-exit.js │ │ │ └── async-command.js │ │ ├── commands │ │ │ ├── deploy.js │ │ │ ├── clean.js │ │ │ ├── develop.js │ │ │ └── build.js │ │ ├── bin │ │ │ └── lerna-cola.js │ │ ├── development-service │ │ │ ├── create-package-conductor.js │ │ │ ├── create-package-watcher.js │ │ │ ├── graceful-shutdown-manager.js │ │ │ └── index.js │ │ └── deployment-service │ │ │ └── index.js │ └── package.json ├── plugin-build-babel │ ├── README.md │ ├── package.json │ └── src │ │ └── index.js ├── plugin-build-flow │ ├── README.md │ ├── package.json │ └── src │ │ └── index.js ├── plugin-deploy-now │ ├── README.md │ ├── package.json │ └── src │ │ └── index.js ├── babel-plugin-inject-source-map-init │ ├── README.md │ ├── package.json │ └── src │ │ └── index.js └── babel-config │ ├── package.json │ └── src │ └── index.js ├── .eslintignore ├── lerna.json ├── flow └── .flowconfig ├── scripts ├── clean.js ├── build.js └── utils.js ├── .gitignore ├── LICENSE ├── tasks.todo ├── package.json ├── CODE_OF_CONDUCT.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.strict-semver true -------------------------------------------------------------------------------- /packages/lib/.gitignore: -------------------------------------------------------------------------------- 1 | /modules 2 | -------------------------------------------------------------------------------- /packages/utils/.gitignore: -------------------------------------------------------------------------------- 1 | /modules 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/*/build 2 | packages/*/flow-typed -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @lerna-cola/cli 2 | 3 | The [lerna-cola](https://github.com/ctrlplusb/lerna-cola) CLI. 4 | -------------------------------------------------------------------------------- /packages/lib/README.md: -------------------------------------------------------------------------------- 1 | # @lerna-cola/lib 2 | 3 | The lib for [lerna-cola](https://github.com/ctrlplusb/lerna-cola) packages. 4 | -------------------------------------------------------------------------------- /packages/plugin-build-babel/README.md: -------------------------------------------------------------------------------- 1 | # @lerna-cola/plugin-build-babel 2 | 3 | See [lerna-cola](https://github.com/ctrlplusb/lerna-cola) 4 | -------------------------------------------------------------------------------- /packages/plugin-build-flow/README.md: -------------------------------------------------------------------------------- 1 | # @lerna-cola/plugin-build-flow 2 | 3 | See [lerna-cola](https://github.com/ctrlplusb/lerna-cola) 4 | -------------------------------------------------------------------------------- /packages/plugin-deploy-now/README.md: -------------------------------------------------------------------------------- 1 | # @lerna-cola/plugin-deploy-now 2 | 3 | See [lerna-cola](https://github.com/ctrlplusb/lerna-cola) 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-inject-source-map-init/README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-inject-source-map-init 2 | 3 | See [lerna-cola](https://github.com/ctrlplusb/lerna-cola) 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "npmClient": "yarn", 4 | "packages": ["packages/*"], 5 | "registry": "https://registry.npmjs.org/", 6 | "version": "independent" 7 | } 8 | -------------------------------------------------------------------------------- /flow/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | build/ 3 | node_modules/ 4 | packages/*/build 5 | flow-typed/npm/execa* 6 | 7 | [include] 8 | flow-typed/npm 9 | 10 | [libs] 11 | 12 | [lints] 13 | 14 | [options] 15 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |
6 | 7 | # @lerna-cola/utils 8 | 9 | Helpful utilities for [lerna-cola](https://github.com/ctrlplusb/lerna-cola) projects. 10 | -------------------------------------------------------------------------------- /scripts/clean.js: -------------------------------------------------------------------------------- 1 | const { cleanAsync } = require('./utils') 2 | 3 | cleanAsync().then(() => process.exit(0)) 4 | 5 | function preventScriptExit() { 6 | ;(function wait() { 7 | // eslint-disable-next-line no-constant-condition 8 | if (true) setTimeout(wait, 1000) 9 | })() 10 | } 11 | 12 | preventScriptExit() 13 | -------------------------------------------------------------------------------- /packages/utils/src/index.js: -------------------------------------------------------------------------------- 1 | const configureGracefulExit = require('./configure-graceful-exit') 2 | const getWebPackageManifest = require('./get-web-package-manifest') 3 | const loadEnvVars = require('./load-env-vars') 4 | 5 | module.exports = { 6 | configureGracefulExit, 7 | getWebPackageManifest, 8 | loadEnvVars, 9 | } 10 | -------------------------------------------------------------------------------- /packages/lib/src/fns.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | function throttle(duration: number, fn: () => mixed) { 4 | let throttler = null 5 | return (...args: Array) => { 6 | if (throttler) { 7 | clearTimeout(throttler) 8 | } 9 | throttler = setTimeout(() => fn(...args), duration) 10 | } 11 | } 12 | 13 | module.exports = { 14 | throttle, 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const { cleanAsync, buildAsync } = require('./utils') 2 | 3 | cleanAsync() 4 | .then(buildAsync) 5 | .then(() => process.exit(0)) 6 | 7 | function preventScriptExit() { 8 | ;(function wait() { 9 | // eslint-disable-next-line no-constant-condition 10 | if (true) setTimeout(wait, 1000) 11 | })() 12 | } 13 | 14 | preventScriptExit() 15 | -------------------------------------------------------------------------------- /packages/babel-plugin-inject-source-map-init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "version": "0.26.9", 4 | "name": "babel-plugin-inject-source-map-init", 5 | "description": "Injects a safe initialisation of source-map-support into your transpiled files", 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "babel-template": "6.26.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /deprecated 2 | 3 | # Various OS / Runtimes / Node 4 | .DS_Store 5 | .lock-wscript 6 | .simeerc 7 | .vscode 8 | *.log 9 | *.pid 10 | *.seed 11 | flow-typed 12 | lerna-debug.log 13 | logs 14 | node_modules 15 | npm-debug.log 16 | pids 17 | Thumbs.db 18 | 19 | # Transpiled Packages 20 | packages/*/build 21 | 22 | # Generated files 23 | packages/*/.flowconfig 24 | 25 | -------------------------------------------------------------------------------- /packages/lib/src/packages/clean-packages.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package } from '../types' 4 | 5 | const pSeries = require('p-series') 6 | const cleanPackage = require('./clean-package') 7 | 8 | module.exports = async function cleanPackages( 9 | packages: Array, 10 | ): Promise { 11 | await pSeries(packages.map(p => () => cleanPackage(p))) 12 | } 13 | -------------------------------------------------------------------------------- /packages/lib/src/packages/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const buildPackage = require('./build-package') 4 | const cleanPackages = require('./clean-packages') 5 | const cleanPackage = require('./clean-package') 6 | const resolvePackages = require('./resolve-packages') 7 | 8 | module.exports = { 9 | buildPackage, 10 | cleanPackages, 11 | cleanPackage, 12 | resolvePackages, 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/src/lib/handle-error.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const { Errors, TerminalUtils } = require('@lerna-cola/lib') 4 | 5 | const handleError = (err: Error) => { 6 | if (err instanceof Errors.PackageError) { 7 | TerminalUtils.errorPkg(err.pkg, err.message, err) 8 | } else { 9 | TerminalUtils.error(err.message, err) 10 | } 11 | 12 | process.exit(1) 13 | } 14 | 15 | module.exports = handleError 16 | -------------------------------------------------------------------------------- /packages/cli/src/lib/prevent-script-exit.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Prevent node process from exiting. (until CTRL + C or process.exit is called) 4 | // We do this to allow our scripts to respont to process exit events and do 5 | // cleaning up etc. 6 | module.exports = function preventScriptExit() { 7 | ;(function wait() { 8 | // eslint-disable-next-line no-constant-condition 9 | if (true) setTimeout(wait, 1000) 10 | })() 11 | } 12 | -------------------------------------------------------------------------------- /packages/lib/src/logic.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const R = require('ramda') 4 | 5 | function execIfFunc(x: T | (() => T)): ?T { 6 | return typeof x === 'function' ? x() : x 7 | } 8 | 9 | const onlyIf = R.curry(function onlyIfUncurried( 10 | condition: boolean | (() => boolean), 11 | value: T | (() => T), 12 | ): ?T { 13 | return execIfFunc(condition) ? execIfFunc(value) : undefined 14 | }) 15 | 16 | module.exports = { 17 | onlyIf, 18 | } 19 | -------------------------------------------------------------------------------- /packages/lib/src/objects.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const deepMerge = require('deepmerge') 4 | 5 | /** 6 | * Deeply merges a given set of objects together. 7 | * 8 | * Objects to the right take priority. 9 | * 10 | * @param {...Object} args - The objects to merge. 11 | * 12 | * @return {Object} - The merged object. 13 | */ 14 | function mergeDeep(left: Object, right: Object): Object { 15 | return deepMerge(left, right) 16 | } 17 | 18 | module.exports = { 19 | mergeDeep, 20 | } 21 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.2.3", 7 | "name": "@lerna-cola/utils", 8 | "description": "Helpful utilities for lerna-cola projects.", 9 | "repository": "https://github.com/ctrlplusb/lerna-cola", 10 | "license": "MIT", 11 | "main": "src/index.js", 12 | "files": [ 13 | "src" 14 | ], 15 | "dependencies": { 16 | "deepmerge": "3.2.0", 17 | "fs-extra": "7.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/lib/src/arrays.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const R = require('ramda') 4 | 5 | /** 6 | * Filters out all null/undefined items from the given array. 7 | * 8 | * @param {Array} as - the target array 9 | * 10 | * @return {Array} The filtered array. 11 | */ 12 | function removeNil(as: Array): Array { 13 | return as.filter(a => a != null) 14 | } 15 | 16 | function removeEmpty(as: Array): Array { 17 | return as.filter(a => R.not(R.isEmpty(a))) 18 | } 19 | 20 | module.exports = { 21 | removeNil, 22 | removeEmpty, 23 | } 24 | -------------------------------------------------------------------------------- /packages/cli/src/lib/async-command.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const handleError = require('./handle-error') 4 | 5 | module.exports = function asyncCommand(fn: (...any) => mixed) { 6 | return (...args: Array) => { 7 | const [yargv] = args 8 | try { 9 | yargv.promisedResult = Promise.resolve(fn(...args)).catch(handleError) 10 | return yargv.promisedResult 11 | } catch (err) { 12 | handleError(err) 13 | } 14 | throw new Error( 15 | 'Invalid state. Should not have reached this branch of code.', 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/plugin-build-babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.3.5", 7 | "name": "@lerna-cola/plugin-build-babel", 8 | "description": "Babel plugin for lerna-cola projects.", 9 | "repository": "https://github.com/ctrlplusb/lerna-cola", 10 | "main": "build/index.js", 11 | "files": [ 12 | "build" 13 | ], 14 | "dependencies": { 15 | "@lerna-cola/lib": "^0.5.5", 16 | "babel-core": "6.26.3", 17 | "fs-extra": "7.0.1", 18 | "globby": "9.1.0", 19 | "p-limit": "2.2.0", 20 | "pify": "4.0.1" 21 | }, 22 | "devDependencies": { 23 | "flow-bin": "^0.94.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/plugin-build-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.3.5", 7 | "name": "@lerna-cola/plugin-build-flow", 8 | "description": "Flow plugin for lerna-cola projects.", 9 | "repository": "https://github.com/ctrlplusb/lerna-cola", 10 | "main": "build/index.js", 11 | "files": [ 12 | "build" 13 | ], 14 | "dependencies": { 15 | "@lerna-cola/lib": "^0.5.5", 16 | "flow-remove-types": "1.2.3", 17 | "fs-extra": "7.0.1", 18 | "globby": "9.1.0", 19 | "p-limit": "2.2.0", 20 | "ramda": "0.26.1" 21 | }, 22 | "devDependencies": { 23 | "flow-bin": "^0.94.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/commands/deploy.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const { TerminalUtils } = require('@lerna-cola/lib') 4 | const deploymentService = require('../deployment-service') 5 | const asyncCommand = require('../lib/async-command') 6 | 7 | module.exports = { 8 | command: 'deploy', 9 | desc: 'Executes the deployment process', 10 | handler: asyncCommand(async () => { 11 | try { 12 | if (!process.env.NODE_ENV) { 13 | process.env.NODE_ENV = 'production' 14 | } 15 | TerminalUtils.title('Running deploy...') 16 | await deploymentService() 17 | TerminalUtils.success('Done') 18 | } catch (ex) { 19 | TerminalUtils.error('Deployment failed', ex) 20 | } 21 | }), 22 | } 23 | -------------------------------------------------------------------------------- /packages/lib/src/packages/clean-package.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package } from '../types' 4 | 5 | const TerminalUtils = require('../terminal') 6 | const config = require('../config') 7 | 8 | module.exports = async function cleanPackage(pkg: Package) { 9 | const cleanPlugin = pkg.plugins.cleanPlugin 10 | if (cleanPlugin != null) { 11 | TerminalUtils.verbosePkg( 12 | pkg, 13 | `Running clean plugin: ${cleanPlugin.plugin.name}`, 14 | ) 15 | await cleanPlugin.plugin.clean(pkg, cleanPlugin.options, { 16 | config: config(), 17 | }) 18 | TerminalUtils.verbosePkg(pkg, `Ran clean`) 19 | } else { 20 | TerminalUtils.verbosePkg(pkg, `No clean plugin to run`) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/lib/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const ArrayUtils = require('./arrays') 4 | const ChildProcessUtils = require('./childProcess') 5 | const config = require('./config') 6 | const Errors = require('./errors') 7 | const FnUtils = require('./fns') 8 | const FsUtils = require('./fs') 9 | const LogicUtils = require('./logic') 10 | const ObjectUtils = require('./objects') 11 | const PackageUtils = require('./packages') 12 | const StringUtils = require('./strings') 13 | const TerminalUtils = require('./terminal') 14 | 15 | module.exports = { 16 | ArrayUtils, 17 | ChildProcessUtils, 18 | config, 19 | Errors, 20 | FnUtils, 21 | FsUtils, 22 | LogicUtils, 23 | ObjectUtils, 24 | PackageUtils, 25 | StringUtils, 26 | TerminalUtils, 27 | } 28 | -------------------------------------------------------------------------------- /packages/plugin-deploy-now/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.3.5", 7 | "name": "@lerna-cola/plugin-deploy-now", 8 | "description": "Allows deployment of lerna-cola projects to Zeit's now", 9 | "repository": "https://github.com/ctrlplusb/lerna-cola", 10 | "main": "build/index.js", 11 | "files": [ 12 | "build" 13 | ], 14 | "dependencies": { 15 | "@lerna-cola/lib": "^0.5.5", 16 | "chalk": "2.4.2", 17 | "dedent": "0.7.0", 18 | "deepmerge": "3.2.0", 19 | "fs-extra": "^7.0.1", 20 | "p-whilst": "1.0.0", 21 | "ramda": "0.26.1", 22 | "temp-write": "3.4.0" 23 | }, 24 | "devDependencies": { 25 | "flow-bin": "^0.94.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/lib/src/errors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package } from './types' 4 | 5 | function LernaColaError(message: string) { 6 | this.name = 'LernaColaError' 7 | this.message = message 8 | this.stack = Error().stack 9 | } 10 | 11 | LernaColaError.prototype = Object.create(Error.prototype) 12 | LernaColaError.prototype.name = 'LernaColaError' 13 | LernaColaError.prototype.constructor = LernaColaError 14 | 15 | function PackageError(pkg: Package, message: string, err: ?Error) { 16 | this.name = 'PackageError' 17 | this.message = message 18 | this.stack = err ? err.stack : Error().stack 19 | } 20 | 21 | PackageError.prototype = Object.create(Error.prototype) 22 | PackageError.prototype.name = 'PackageError' 23 | PackageError.prototype.constructor = PackageError 24 | 25 | module.exports = { 26 | LernaColaError, 27 | PackageError, 28 | } 29 | -------------------------------------------------------------------------------- /packages/utils/src/load-env-vars.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const dotenv = require('dotenv') 4 | 5 | module.exports = function loadEnvVars() { 6 | // First load anv env specific files 7 | const env = process.env.NODE_ENV || 'production' 8 | const envSpecificPath = path.resolve(process.cwd(), `./.env.${env}`) 9 | if (fs.existsSync(envSpecificPath)) { 10 | // eslint-disable-next-line no-console 11 | console.log(`Loading environment variables from ${envSpecificPath}`) 12 | dotenv.config({ path: envSpecificPath }) 13 | } 14 | 15 | // Then load any generic .env "overides" 16 | const envPath = path.resolve(process.cwd(), './.env') 17 | if (fs.existsSync(envPath)) { 18 | // eslint-disable-next-line no-console 19 | console.log(`Loading environment variables from ${envPath}`) 20 | dotenv.config({ path: envPath }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/commands/clean.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const { TerminalUtils, PackageUtils } = require('@lerna-cola/lib') 4 | const pSeries = require('p-series') 5 | const asyncCommand = require('../lib/async-command') 6 | 7 | module.exports = { 8 | command: 'clean', 9 | describe: 'Cleans the build output for packages', 10 | // $FlowFixMe 11 | builder: yargs => 12 | yargs.option('packages', { 13 | alias: 'p', 14 | describe: 'The packages to clean', 15 | type: 'array', 16 | }), 17 | handler: asyncCommand(async argv => { 18 | try { 19 | TerminalUtils.title('Running clean command...') 20 | const packages = PackageUtils.resolvePackages(argv.packages) 21 | const clean = pkg => () => PackageUtils.cleanPackage(pkg) 22 | await pSeries(packages.map(clean)) 23 | TerminalUtils.success('Done') 24 | } catch (ex) { 25 | TerminalUtils.error('Clean failed', ex) 26 | } 27 | }), 28 | } 29 | -------------------------------------------------------------------------------- /packages/babel-plugin-inject-source-map-init/src/index.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require('os') 2 | const template = require('babel-template') 3 | 4 | // We wrap our code with a check for the webpack inject env var to ensure that 5 | // we don't initialise source maps twice. 6 | const sourceMapImportAST = template(` 7 | try { 8 | var sourceMapSupport = require('source-map-support'); 9 | sourceMapSupport.install({ 10 | environment: 'node' 11 | }); 12 | } catch(err) { 13 | console.log('In order to provide a better debugging experience of your lerna-cola packages please ensure that source-map-support is installed.${EOL}${EOL}\tnpm i source-map-support -S -E'); 14 | } 15 | `)() 16 | 17 | module.exports = function sourceMapSupportPlugin() { 18 | return { 19 | visitor: { 20 | Program: { 21 | exit(path) { 22 | path.unshiftContainer('body', sourceMapImportAST) 23 | }, 24 | }, 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.5.5", 7 | "name": "@lerna-cola/cli", 8 | "description": "The lerna-cola CLI.", 9 | "repository": "https://github.com/ctrlplusb/lerna-cola", 10 | "bin": { 11 | "lerna-cola": "build/bin/lerna-cola.js" 12 | }, 13 | "engines": { 14 | "node": ">=8" 15 | }, 16 | "files": [ 17 | "build" 18 | ], 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/ctrlplusb/lerna-cola/issues" 22 | }, 23 | "dependencies": { 24 | "@lerna-cola/lib": "^0.5.5", 25 | "@lerna-cola/utils": "^0.2.3", 26 | "chokidar": "2.1.2", 27 | "dotenv": "7.0.0", 28 | "fs-extra": "7.0.1", 29 | "p-series": "2.0.0", 30 | "ramda": "0.26.1", 31 | "read-pkg": "5.0.0", 32 | "semver": "5.6.0", 33 | "yargs": "13.2.2" 34 | }, 35 | "devDependencies": { 36 | "flow-bin": "0.94.0", 37 | "flow-typed": "2.5.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/lib/src/plugins/develop-build/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package, DevelopPlugin } from '../../types' 4 | 5 | const PackageUtils = require('../../packages') 6 | const { PackageError } = require('../../errors') 7 | 8 | const developBuildPlugin: DevelopPlugin = { 9 | name: 'plugin-develop-build', 10 | clean: (pkg: Package) => { 11 | throw new PackageError(pkg, '"clean" not supported by "build" plugin') 12 | }, 13 | build: (pkg: Package) => { 14 | throw new PackageError(pkg, '"build" not supported by "build" plugin') 15 | }, 16 | develop: (pkg: Package) => 17 | PackageUtils.buildPackage(pkg) 18 | // we ensure that nothing is returned as we won't be resolving a 19 | // develop instance with kill cmd etc 20 | .then(() => ({ kill: () => Promise.resolve(undefined) })), 21 | deploy: (pkg: Package) => { 22 | throw new PackageError(pkg, '"deploy" not supported by "build" plugin') 23 | }, 24 | } 25 | 26 | module.exports = developBuildPlugin 27 | -------------------------------------------------------------------------------- /packages/lib/src/plugins/clean-build/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package, CleanPlugin } from '../../types' 4 | 5 | const rimraf = require('rimraf') 6 | const pify = require('pify') 7 | const PackageUtils = require('../../packages') 8 | const { PackageError } = require('../../errors') 9 | 10 | const rimrafAsync = pify(rimraf) 11 | 12 | const cleanBuildPlugin: CleanPlugin = { 13 | name: 'plugin-clean-build', 14 | clean: (pkg: Package) => rimrafAsync(pkg.paths.packageBuildOutput), 15 | build: (pkg: Package) => { 16 | throw new PackageError(pkg, '"build" not supported by "build" plugin') 17 | }, 18 | develop: (pkg: Package) => 19 | PackageUtils.buildPackage(pkg) 20 | // we ensure that nothing is returned as we won't be resolving a 21 | // develop instance with kill cmd etc 22 | .then(() => ({ kill: () => Promise.resolve(undefined) })), 23 | deploy: (pkg: Package) => { 24 | throw new PackageError(pkg, '"deploy" not supported by "build" plugin') 25 | }, 26 | } 27 | 28 | module.exports = cleanBuildPlugin 29 | -------------------------------------------------------------------------------- /packages/lib/src/plugins/develop-server/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package, DevelopPlugin } from '../../types' 4 | 5 | const develop = require('./develop') 6 | const { PackageError } = require('../../errors') 7 | 8 | const developServerPlugin: DevelopPlugin = { 9 | name: 'plugin-develop-server', 10 | build: (pkg: Package) => { 11 | throw new PackageError(pkg, '"build" not supported by "server" plugin') 12 | }, 13 | clean: (pkg: Package) => { 14 | throw new PackageError(pkg, '"clean" not supported by "server" plugin') 15 | }, 16 | develop: (pkg: Package) => { 17 | if (!pkg.packageJson.main) { 18 | throw new PackageError( 19 | pkg, 20 | `You must provide a "main" within your package.json when using the "server" develop plugin. See the configuration for ${ 21 | pkg.name 22 | }`, 23 | ) 24 | } 25 | return develop(pkg) 26 | }, 27 | deploy: (pkg: Package) => { 28 | throw new PackageError(pkg, '"deploy" not supported by "server" plugin') 29 | }, 30 | } 31 | 32 | module.exports = developServerPlugin 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sean Matheson 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 | -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.5.5", 7 | "name": "@lerna-cola/lib", 8 | "description": "The lib for lerna-cola packages.", 9 | "repository": "https://github.com/ctrlplusb/lerna-cola", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/ctrlplusb/lerna-cola/issues" 13 | }, 14 | "main": "build/index.js", 15 | "files": [ 16 | "build" 17 | ], 18 | "dependencies": { 19 | "chalk": "2.4.2", 20 | "dedent": "0.7.0", 21 | "deepmerge": "3.2.0", 22 | "execa": "1.0.0", 23 | "figures": "2.0.0", 24 | "fs-extra": "7.0.1", 25 | "globby": "9.1.0", 26 | "inquirer": "6.2.2", 27 | "load-json-file": "5.2.0", 28 | "p-map": "^2.0.0", 29 | "p-series": "2.0.0", 30 | "pify": "^4.0.1", 31 | "pretty-format": "^24.5.0", 32 | "ramda": "0.26.1", 33 | "rimraf": "^2.6.3", 34 | "semver": "5.6.0", 35 | "toposort": "2.0.2", 36 | "write-json-file": "3.1.0" 37 | }, 38 | "devDependencies": { 39 | "flow-bin": "0.94.0", 40 | "flow-typed": "2.5.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/lib/src/strings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Chalk } from 'chalk' 4 | import type { Package } from './types' 5 | 6 | const { EOL } = require('os') 7 | const R = require('ramda') 8 | const chalk = require('chalk') 9 | const { removeNil, removeEmpty } = require('./arrays') 10 | const config = require('./config') 11 | 12 | const multiLineStringToArray = (str: string): Array => 13 | R.pipe( 14 | R.defaultTo(''), 15 | x => x.split(EOL), 16 | removeNil, 17 | removeEmpty, 18 | )(str) 19 | 20 | const prefixedMsg = (color: Chalk, prefix: string, msg: string): string => { 21 | const formattedPrefix = `${color( 22 | `${prefix.padEnd(config().terminalLabelMinLength + 1)}`, 23 | )}|` 24 | 25 | return `${formattedPrefix} ${(msg || '') 26 | .toString() 27 | .replace(/\n/gi, `\n${formattedPrefix} `)}` 28 | } 29 | 30 | const lernaColaMsg = (msg: string): string => 31 | prefixedMsg(chalk.bgBlack.gray, 'lerna-cola', msg) 32 | 33 | const packageMsg = (pkg: Package, msg: string): string => 34 | prefixedMsg(pkg.color, pkg.name, msg) 35 | 36 | module.exports = { 37 | lernaColaMsg, 38 | multiLineStringToArray, 39 | packageMsg, 40 | } 41 | -------------------------------------------------------------------------------- /packages/lib/src/plugins/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ExecaChildProcess } from 'execa' 4 | import type { Package } from '../types' 5 | 6 | const TerminalUtils = require('../terminal') 7 | 8 | function killChildProcess( 9 | pkg: Package, 10 | childProcess: ExecaChildProcess, 11 | ): Promise { 12 | TerminalUtils.verbosePkg(pkg, `Killing child process...`) 13 | 14 | return new Promise(resolve => { 15 | let killed = false 16 | 17 | childProcess.on('close', () => { 18 | killed = true 19 | }) 20 | 21 | childProcess.catch(err => { 22 | TerminalUtils.verbosePkg(pkg, `Process killed with errors`) 23 | TerminalUtils.verbosePkg(pkg, err) 24 | resolve() 25 | }) 26 | 27 | const checkInterval = setInterval(() => { 28 | TerminalUtils.verbosePkg(pkg, `Checking if killed`) 29 | if (killed) { 30 | TerminalUtils.verbosePkg(pkg, `Process killed`) 31 | clearInterval(checkInterval) 32 | resolve() 33 | } 34 | }, 1000) 35 | 36 | childProcess.kill('SIGTERM') 37 | }).catch(err => { 38 | TerminalUtils.verbosePkg(pkg, `Fatal error whilst killing process`) 39 | throw err 40 | }) 41 | } 42 | 43 | module.exports = { killChildProcess } 44 | -------------------------------------------------------------------------------- /packages/babel-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.3.5", 7 | "name": "@lerna-cola/babel-config", 8 | "description": "A Babel config for the Babel build plugin for lerna-cola projects.", 9 | "repository": "https://github.com/ctrlplusb/lerna-cola", 10 | "main": "build/index.js", 11 | "files": [ 12 | "build" 13 | ], 14 | "dependencies": { 15 | "@lerna-cola/lib": "^0.5.5", 16 | "babel-plugin-dynamic-import-node": "2.2.0", 17 | "babel-plugin-inject-source-map-init": "^0.26.9", 18 | "babel-plugin-syntax-dynamic-import": "6.18.0", 19 | "babel-plugin-syntax-trailing-function-commas": "6.22.0", 20 | "babel-plugin-transform-class-properties": "6.24.1", 21 | "babel-plugin-transform-object-rest-spread": "6.26.0", 22 | "babel-plugin-transform-react-constant-elements": "6.23.0", 23 | "babel-plugin-transform-react-inline-elements": "6.22.0", 24 | "babel-plugin-transform-react-jsx-self": "6.22.0", 25 | "babel-plugin-transform-react-jsx-source": "6.22.0", 26 | "babel-plugin-transform-react-remove-prop-types": "0.4.24", 27 | "babel-plugin-transform-runtime": "6.23.0", 28 | "babel-preset-env": "1.7.0", 29 | "babel-preset-react": "6.24.1", 30 | "semver": "^5.6.0" 31 | }, 32 | "devDependencies": { 33 | "flow-bin": "^0.94.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/lib/src/packages/build-package.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package } from '../types' 4 | 5 | const TerminalUtils = require('../terminal') 6 | const config = require('../config') 7 | 8 | type Options = { 9 | quiet?: boolean, 10 | } 11 | 12 | const defaultOptions = { quiet: false } 13 | 14 | const executeBuild = pkg => { 15 | if (pkg.plugins.buildPlugin) { 16 | TerminalUtils.verbosePkg( 17 | pkg, 18 | `Building using build plugin: ${pkg.plugins.buildPlugin.plugin.name}`, 19 | ) 20 | } 21 | return pkg.plugins.buildPlugin 22 | ? pkg.plugins.buildPlugin.plugin.build( 23 | pkg, 24 | pkg.plugins.buildPlugin.options, 25 | { config: config() }, 26 | ) 27 | : Promise.resolve() 28 | } 29 | 30 | /** 31 | * Builds the given package. 32 | * 33 | * @param {*} pkg 34 | * The package to be built. 35 | * @param {*} options.quiet 36 | * If set then the logs will only be displayed when the VERBOSE flag is enabled. 37 | */ 38 | async function buildPackage( 39 | pkg: Package, 40 | options: ?Options = {}, 41 | ): Promise { 42 | const { quiet } = Object.assign({}, defaultOptions, options) 43 | TerminalUtils[quiet ? 'verbosePkg' : 'infoPkg'](pkg, `Building...`) 44 | 45 | try { 46 | await executeBuild(pkg) 47 | TerminalUtils.verbosePkg(pkg, `Built`) 48 | } catch (err) { 49 | TerminalUtils.errorPkg(pkg, `Build failed`) 50 | throw err 51 | } 52 | } 53 | 54 | module.exports = buildPackage 55 | -------------------------------------------------------------------------------- /packages/cli/src/commands/develop.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const { TerminalUtils } = require('@lerna-cola/lib') 4 | const developmentService = require('../development-service') 5 | const asyncCommand = require('../lib/async-command') 6 | 7 | module.exports = { 8 | command: 'develop', 9 | desc: 'Starts the coordinated development service', 10 | builder: yargs => 11 | yargs 12 | .option('packages', { 13 | alias: 'p', 14 | describe: 'The packages to develop', 15 | type: 'array', 16 | }) 17 | .option('select', { 18 | alias: 's', 19 | describe: 'Enable selection of packages to develop', 20 | type: 'boolean', 21 | }) 22 | .option('exact', { 23 | alias: 'e', 24 | describe: 25 | 'Useful when selecting/filtering packages. When enabled only the selected/filtered packages will be tracked.', 26 | type: 'boolean', 27 | }), 28 | handler: asyncCommand(async argv => { 29 | try { 30 | TerminalUtils.title('Starting development service...') 31 | if (!process.env.NODE_ENV) { 32 | process.env.NODE_ENV = 'development' 33 | } 34 | await developmentService({ 35 | packagesFilter: argv.packages, 36 | selectPackages: argv.select, 37 | exact: argv.exact, 38 | }) 39 | TerminalUtils.success('Done') 40 | } catch (ex) { 41 | TerminalUtils.error('Unhandled exception in development service', ex) 42 | } 43 | }), 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli/src/bin/lerna-cola.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // @flow 4 | /* eslint-disable no-console */ 5 | 6 | const yargs = require('yargs') 7 | const { TerminalUtils } = require('@lerna-cola/lib') 8 | const buildCommand = require('../commands/build') 9 | const cleanCommand = require('../commands/clean') 10 | const developCommand = require('../commands/develop') 11 | const deployCommand = require('../commands/deploy') 12 | const preventScriptExit = require('../lib/prevent-script-exit') 13 | const handleError = require('../lib/handle-error') 14 | 15 | const onComplete = output => { 16 | if (output) { 17 | console.log(output) 18 | } 19 | process.exit(0) 20 | } 21 | 22 | const args = process.argv.slice(2) 23 | 24 | yargs 25 | .command(buildCommand) 26 | .command(cleanCommand) 27 | .command(developCommand) 28 | .command(deployCommand) 29 | .demandCommand() 30 | .help('h') 31 | .alias('h', 'help') 32 | 33 | if (args.length > 0) { 34 | yargs.parse(args, (err, argv, output) => { 35 | TerminalUtils.verbose(argv) 36 | if (argv.promisedResult) { 37 | TerminalUtils.verbose('Waiting for async command to complete...') 38 | argv.promisedResult.catch(handleError).then(onComplete) 39 | } else { 40 | if (err) { 41 | handleError(err) 42 | } 43 | onComplete(output) 44 | } 45 | }) 46 | } else { 47 | yargs.parse() 48 | } 49 | 50 | process.on('uncaughtException', handleError) 51 | process.on('unhandledRejection', handleError) 52 | 53 | preventScriptExit() 54 | -------------------------------------------------------------------------------- /tasks.todo: -------------------------------------------------------------------------------- 1 | Bugs: 2 | ☐ If no command/arg is provided to the lerna-cola cli an error occurs. Should print help. 3 | 4 | Docs: 5 | ☐ Intro 6 | ☐ Video overview 7 | ☐ Blog post 8 | ☐ lerna-cola cli 9 | ☐ Tips and Tricks 10 | ☐ SIGTERM/SIGINT effective usage 11 | ☐ Usage of process.stdin.read() to stop process exiting (even though it has child processes) 12 | 13 | Plugins: 14 | ☐ reasonml 15 | ☐ typescript 16 | ☐ rollup 17 | ☐ parceljs 18 | 19 | Features: 20 | ☐ When deploying check that dependencies have previously been published 21 | ☐ Pre/Post command hooks missing on: clean, build, and deploy commands. 22 | ☐ Better help on lerna-cola bin 23 | ☐ Command hooks + error handling of them! 24 | ✔ predevelop @done (2018-3-2 15:15:11) 25 | ✔ postdevelop @done (2018-3-2 15:15:11) 26 | ✔ pretest @done (2018-3-2 15:15:10) 27 | ✔ posttest @done (2018-3-2 15:15:10) 28 | ☐ Finish all inline TODO comments within code. 29 | ☐ Validate the lerna-cola configuration during runtime (tcomb?) 30 | ☐ Chokidar events should result in the absolute minimal amount of work being done. e.g. transpile a single file. remove a dir, etc 31 | ☐ auto-add source-map-support to build dependencies, and then re-enable for production builds. 32 | ☐ plugin interface for compilers - pre/post compile 33 | ☐ plugin interface for compilers - pre/post develop 34 | ☐ Manual reload override for development mode (like nodemon rs). 35 | ☐ Allow for explicit ignoring of a dependency when doing hot development reloads. 36 | -------------------------------------------------------------------------------- /packages/lib/src/fs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const TerminalUtils = require('./terminal') 4 | const path = require('path') 5 | const fs = require('fs-extra') 6 | 7 | /** 8 | * Resolves a package with the given name. 9 | * 10 | * i.e. returns the equivalent of require('foo') 11 | */ 12 | function resolvePackage(resolvedPackageName: string): mixed { 13 | const packagePath = require.resolve(resolvedPackageName) 14 | TerminalUtils.verbose(`Trying to resolve package ${packagePath}`) 15 | let resolvedPackage 16 | try { 17 | // eslint-disable-next-line global-require,import/no-dynamic-require 18 | resolvedPackage = require(packagePath) 19 | } catch (err) { 20 | TerminalUtils.verbose(`Failed to resolve package ${packagePath}`) 21 | TerminalUtils.verbose(err) 22 | TerminalUtils.verbose( 23 | `Trying to resolve package ${packagePath} as a symlink`, 24 | ) 25 | // EEK! Could be a symlink? 26 | try { 27 | fs.lstatSync(packagePath) 28 | const symLinkPath = fs.readlinkSync(path) 29 | // eslint-disable-next-line global-require,import/no-dynamic-require 30 | resolvedPackage = require(symLinkPath) 31 | } catch (symErr) { 32 | // DO nothing 33 | TerminalUtils.verbose( 34 | `Failed to resolve package ${packagePath} as a symlink`, 35 | ) 36 | TerminalUtils.verbose(symErr) 37 | } 38 | } 39 | 40 | TerminalUtils.verbose(`Resolved package ${packagePath}`) 41 | 42 | return resolvedPackage 43 | } 44 | 45 | module.exports = { 46 | resolvePackage, 47 | } 48 | -------------------------------------------------------------------------------- /packages/cli/src/development-service/create-package-conductor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | Package, 5 | PackageWatcher, 6 | PackageConductor, 7 | } from '@lerna-cola/lib/build/types' 8 | 9 | const { config, TerminalUtils } = require('@lerna-cola/lib') 10 | 11 | const noPluginResult = { 12 | kill: () => Promise.resolve(), 13 | } 14 | 15 | module.exports = function createPackageConductor( 16 | pkg: Package, 17 | watcher: PackageWatcher, 18 | ): PackageConductor { 19 | let runningDevelopInstance 20 | 21 | return { 22 | run: (runType, changedDependency) => { 23 | const developPlugin = pkg.plugins.developPlugin 24 | if (!developPlugin) { 25 | TerminalUtils.verbosePkg( 26 | pkg, 27 | `No develop plugin, skipping develop execution/conductor`, 28 | ) 29 | return Promise.resolve(noPluginResult) 30 | } 31 | 32 | TerminalUtils.verbosePkg( 33 | pkg, 34 | `Running develop plugin for change type: ${runType}`, 35 | ) 36 | 37 | return developPlugin.plugin 38 | .develop(pkg, developPlugin.options, { 39 | config: config(), 40 | watcher, 41 | runType, 42 | changedDependency, 43 | }) 44 | .then(developInstance => { 45 | runningDevelopInstance = developInstance 46 | return developInstance 47 | }) 48 | }, 49 | stop: () => 50 | runningDevelopInstance 51 | ? runningDevelopInstance.kill() 52 | : Promise.resolve(), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/lib/src/config/get-package-roots.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Lifted from Lerna ❤️ 5 | */ 6 | 7 | const globby = require('globby') 8 | const path = require('path') 9 | 10 | module.exports = function resolvePackageRoots( 11 | packageSources: Array = ['packages/*'], 12 | ): Array { 13 | const globOpts = { 14 | cwd: process.cwd(), 15 | absolute: true, 16 | followSymlinkedDirectories: false, 17 | ignore: [], 18 | } 19 | 20 | const hasNodeModules = packageSources.some( 21 | cfg => cfg.indexOf('node_modules') > -1, 22 | ) 23 | const hasGlobStar = packageSources.some(cfg => cfg.indexOf('**') > -1) 24 | 25 | if (hasGlobStar) { 26 | if (hasNodeModules) { 27 | throw new Error( 28 | 'An explicit node_modules package path does not allow globstars (**)', 29 | ) 30 | } 31 | 32 | globOpts.ignore.push( 33 | // allow globs like "packages/**", 34 | // but avoid picking up node_modules/**/package.json 35 | '**/node_modules/**', 36 | ) 37 | } 38 | 39 | return globby 40 | .sync( 41 | packageSources.map( 42 | globPath => path.join(process.cwd(), globPath, 'package.json'), 43 | globOpts, 44 | ), 45 | ) 46 | .map(globResult => { 47 | // https://github.com/isaacs/node-glob/blob/master/common.js#L104 48 | // glob always returns "\\" as "/" in windows, so everyone 49 | // gets normalized because we can't have nice things. 50 | const packageConfigPath = path.normalize(globResult) 51 | const packageDir = path.dirname(packageConfigPath) 52 | return packageDir 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /packages/cli/src/development-service/create-package-watcher.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package, PackageWatcher } from '@lerna-cola/lib/build/types' 4 | 5 | const chokidar = require('chokidar') 6 | const { TerminalUtils } = require('@lerna-cola/lib') 7 | 8 | const disabledPackageWatchingResult = { 9 | start: () => undefined, 10 | stop: () => undefined, 11 | } 12 | 13 | module.exports = function createPackageWatcher( 14 | onChange: Function, 15 | pkg: Package, 16 | ): PackageWatcher { 17 | TerminalUtils.verbosePkg(pkg, `Creating development source watcher...`) 18 | 19 | if (pkg.disableSrcWatching) { 20 | TerminalUtils.verbosePkg( 21 | pkg, 22 | `Did not create source watcher as it was disable via package config`, 23 | ) 24 | return disabledPackageWatchingResult 25 | } 26 | 27 | const createWatcher = () => { 28 | const watcher = chokidar.watch( 29 | [pkg.paths.packageSrc, pkg.paths.packageJson], 30 | { 31 | ignored: pkg.paths.packageBuildOutput 32 | ? pkg.paths.packageBuildOutput 33 | : undefined, 34 | ignoreInitial: true, 35 | cwd: pkg.paths.packageRoot, 36 | ignorePermissionErrors: true, 37 | }, 38 | ) 39 | watcher 40 | .on('add', onChange) 41 | .on('change', onChange) 42 | .on('unlink', onChange) 43 | .on('addDir', onChange) 44 | .on('unlinkDir', onChange) 45 | return watcher 46 | } 47 | 48 | let watcher = null 49 | 50 | return { 51 | start: () => { 52 | if (!watcher) { 53 | watcher = createWatcher() 54 | } 55 | }, 56 | stop: () => { 57 | if (watcher) { 58 | watcher.close() 59 | watcher = null 60 | } 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/utils/src/get-web-package-manifest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | const path = require('path') 4 | const fs = require('fs-extra') 5 | 6 | const requireFn = 7 | typeof __non_webpack_require__ !== 'undefined' 8 | ? // eslint-disable-next-line no-undef 9 | __non_webpack_require__ 10 | : require 11 | 12 | const cache = {} 13 | 14 | /** 15 | * Gets the manifest for the lerna-cola web package by the given name. 16 | * 17 | * @param {string} packageName The package's name 18 | * @return {Object} The manifest 19 | */ 20 | module.exports = function getWebPackageManifest( 21 | packageName, 22 | basePath = './dist', 23 | ) { 24 | if (cache[packageName]) { 25 | return cache[packageName] 26 | } 27 | let distPath = path.resolve( 28 | process.cwd(), 29 | `./node_modules/${packageName}/${basePath}`, 30 | ) 31 | let manifestFile = path.resolve(distPath, `./webpack-manifest.json`) 32 | if (!fs.existsSync(manifestFile)) { 33 | // lerna-cola app packageRoot path 34 | distPath = path.resolve( 35 | process.cwd(), 36 | `../../node_modules/${packageName}/${basePath}`, 37 | ) 38 | manifestFile = path.resolve(distPath, `./webpack-manifest.json`) 39 | if (!fs.existsSync(manifestFile)) { 40 | throw new Error(`No manifest found at ${manifestFile}`) 41 | } 42 | } 43 | // eslint-disable-next-line global-require,import/no-dynamic-require 44 | const manifest = requireFn(manifestFile) 45 | if (!manifest.index) { 46 | throw new Error( 47 | `Invalid lerna-cola web package manifest found at ${manifestFile}`, 48 | ) 49 | } 50 | 51 | const jsParts = manifest.index.js 52 | .substr(manifest.index.js.indexOf('/lerna-cola/')) 53 | .split('/') 54 | const rootHttpPath = jsParts.slice(0, jsParts.length - 1).join('/') 55 | 56 | cache[packageName] = { 57 | serverPaths: { 58 | packageRoot: distPath, 59 | }, 60 | httpPaths: { 61 | packageRoot: rootHttpPath, 62 | js: manifest.index.js, 63 | css: manifest.index.css, 64 | }, 65 | manifest, 66 | } 67 | 68 | return cache[packageName] 69 | } 70 | -------------------------------------------------------------------------------- /packages/utils/src/configure-graceful-exit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const deepMerge = require('deepmerge') 4 | 5 | const defaultConfig = { 6 | timeout: 10 * 1000, 7 | messages: { 8 | start: 'Received termination request. Attempting gracefully exit...', 9 | error: 'An error occurred whilst attempting to gracefully exit.', 10 | timeout: 11 | 'Graceful shutdown period timeout lapsed, forcefully shutting down.', 12 | exit: 'Exiting.', 13 | }, 14 | catchUnhandled: true, 15 | } 16 | 17 | module.exports = function configureGracefulExit(config = {}) { 18 | const { onExit, messages, timeout, name, catchUnhandled } = deepMerge( 19 | defaultConfig, 20 | config, 21 | ) 22 | 23 | if (typeof onExit !== 'function') { 24 | console.error( 25 | 'You must provide an onExit "function" to configureGracefulExit', 26 | ) 27 | } 28 | 29 | const frmtMsg = msg => `[${name || process.pid}] ${msg}` 30 | 31 | let exiting = false 32 | 33 | const handleExit = exitCode => { 34 | if (exiting) { 35 | return 36 | } 37 | exiting = true 38 | 39 | const exit = code => { 40 | console.log(frmtMsg(messages.end)) 41 | process.exit(code) 42 | } 43 | 44 | try { 45 | console.log(frmtMsg(messages.start)) 46 | 47 | Promise.resolve(onExit) 48 | .then(() => process.exit(exitCode)) 49 | .catch(err => { 50 | console.error(frmtMsg(messages.error)) 51 | console.error(err) 52 | exit(1) 53 | }) 54 | 55 | setTimeout(() => { 56 | console.error(frmtMsg(messages.timeout)) 57 | exit(1) 58 | }, timeout) 59 | } catch (err) { 60 | console.error(frmtMsg(messages.error)) 61 | console.error(err) 62 | exit(1) 63 | } 64 | } 65 | 66 | // Respond to any termination requests 67 | ;['SIGTERM', 'SIGINT'].forEach(signal => { 68 | process.on(signal, () => handleExit(0)) 69 | }) 70 | 71 | // Catch any unhandled promise rejections 72 | if (catchUnhandled) { 73 | process.on('unhandledRejection', err => { 74 | console.error('An unhandled rejection occurred', err) 75 | handleExit(1) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "scripts": { 7 | "bootstrap": "yarn install && yarn build && yarn flow-types", 8 | "build": "node ./scripts/build.js", 9 | "clean": "node ./scripts/clean.js", 10 | "flow-types": "rimraf projects/*/flow-typed && flow-mono create-symlinks flow/.flowconfig && flow-mono install-types", 11 | "publish": "yarn run build && lerna publish" 12 | }, 13 | "devDependencies": { 14 | "babel-core": "^6.26.3", 15 | "babel-eslint": "10.0.1", 16 | "eslint": "5.15.1", 17 | "eslint-config-airbnb": "17.1.0", 18 | "eslint-config-prettier": "4.1.0", 19 | "eslint-plugin-import": "2.16.0", 20 | "eslint-plugin-jsx-a11y": "6.2.1", 21 | "eslint-plugin-react": "7.12.4", 22 | "flow-mono-cli": "1.4.3", 23 | "flow-remove-types": "^1.2.3", 24 | "fs-extra": "^7.0.1", 25 | "globby": "^9.1.0", 26 | "lerna": "^3.13.1", 27 | "p-limit": "^2.2.0", 28 | "ramda": "^0.26.1", 29 | "rimraf": "^2.6.3" 30 | }, 31 | "eslintConfig": { 32 | "root": true, 33 | "parser": "babel-eslint", 34 | "extends": [ 35 | "eslint-config-airbnb", 36 | "eslint-config-prettier" 37 | ], 38 | "env": { 39 | "browser": true, 40 | "es6": true, 41 | "node": true, 42 | "jest": true 43 | }, 44 | "ecmaFeatures": { 45 | "defaultParams": true 46 | }, 47 | "rules": { 48 | "array-callback-return": 0, 49 | "arrow-parens": [ 50 | "error", 51 | "as-needed" 52 | ], 53 | "camelcase": 0, 54 | "import/no-extraneous-dependencies": 0, 55 | "import/prefer-default-export": 0, 56 | "no-nested-ternary": 0, 57 | "no-underscore-dangle": 0, 58 | "prefer-arrow-callback": 0, 59 | "prefer-destructuring": 0, 60 | "react/forbid-prop-types": 0, 61 | "react/jsx-filename-extension": 0, 62 | "react/no-array-index-key": 0, 63 | "react/no-did-mount-set-state": 0, 64 | "react/no-find-dom-node": 0, 65 | "react/sort-comp": 0 66 | } 67 | }, 68 | "prettier": { 69 | "semi": false, 70 | "singleQuote": true, 71 | "trailingComma": "all" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/cli/src/commands/build.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const { TerminalUtils, PackageUtils } = require('@lerna-cola/lib') 4 | const pSeries = require('p-series') 5 | const asyncCommand = require('../lib/async-command') 6 | 7 | module.exports = { 8 | command: 'build', 9 | desc: 'Executes the configured build plugin for each package', 10 | builder: yargs => 11 | yargs 12 | .option('packages', { 13 | alias: 'p', 14 | describe: 'The packages to build', 15 | type: 'array', 16 | }) 17 | .option('select', { 18 | alias: 's', 19 | describe: 'Enable selection of packages to build', 20 | type: 'boolean', 21 | }) 22 | .option('exact', { 23 | alias: 'e', 24 | describe: 25 | 'Useful when selecting/filtering packages. When enabled only the specifically selected/filtered packages will be built.', 26 | type: 'boolean', 27 | }), 28 | handler: asyncCommand(async argv => { 29 | try { 30 | if (!process.env.NODE_ENV) { 31 | process.env.NODE_ENV = 'production' 32 | } 33 | TerminalUtils.title('Running build command...') 34 | const queueBuild = pkg => () => PackageUtils.buildPackage(pkg) 35 | 36 | let packages = PackageUtils.resolvePackages(argv.packages || [], { 37 | strict: argv.exact, 38 | }) 39 | 40 | if (argv.select) { 41 | // Ask which packages to develop if the select option was enabled 42 | const selectedPackages = await TerminalUtils.multiSelect( 43 | 'Which packages would you like to develop?', 44 | { 45 | choices: packages.map(x => ({ 46 | value: x.name, 47 | text: `${x.name} (${x.version})`, 48 | })), 49 | }, 50 | ) 51 | packages = PackageUtils.resolvePackages(selectedPackages, { 52 | strict: argv.exact, 53 | }) 54 | } 55 | 56 | TerminalUtils.verbose(`Building packages:`) 57 | TerminalUtils.verbose(packages.map(x => x.name)) 58 | 59 | await pSeries(packages.map(queueBuild)) 60 | TerminalUtils.success('Done') 61 | } catch (ex) { 62 | TerminalUtils.error('Build failed', ex) 63 | } 64 | }), 65 | } 66 | -------------------------------------------------------------------------------- /packages/cli/src/deployment-service/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package } from '@lerna-cola/lib/build/types' 4 | 5 | const pSeries = require('p-series') 6 | const { config, TerminalUtils, PackageUtils } = require('@lerna-cola/lib') 7 | 8 | module.exports = async function deploymentService() { 9 | // Determine which packages have a deployment plugin configured 10 | const packagesWithDeployConfig = config().packages.filter( 11 | pkg => !!pkg.plugins.deployPlugin, 12 | ) 13 | if (packagesWithDeployConfig.length === 0) { 14 | throw new Error( 15 | 'You do not have any packages with a deploy configuration. Exiting...', 16 | ) 17 | } 18 | 19 | // Ask which packages to deploy? 20 | const namesOfPackagesToDeploy = await TerminalUtils.multiSelect( 21 | 'Which packages would you like to deploy?', 22 | { 23 | choices: packagesWithDeployConfig.map(x => ({ 24 | value: x.name, 25 | text: `${x.name} (${x.version})`, 26 | })), 27 | }, 28 | ) 29 | 30 | // Ensure at least one package was selected for deploymnet 31 | if (namesOfPackagesToDeploy.length === 0) { 32 | throw new Error('No packages selected. Exiting...') 33 | } 34 | 35 | // Map the package names to packages 36 | const packagesToDeploy = namesOfPackagesToDeploy.map( 37 | x => config().packageMap[x], 38 | ) 39 | 40 | TerminalUtils.info('Building packages...') 41 | 42 | // Get full package tree so we know which related packages need to be built. 43 | const packagesWithDependencies = PackageUtils.resolvePackages( 44 | namesOfPackagesToDeploy, 45 | ) 46 | 47 | // First we need to make sure we have built all packages 48 | await pSeries( 49 | packagesWithDependencies.map(pkg => () => PackageUtils.buildPackage(pkg)), 50 | ) 51 | 52 | TerminalUtils.info('Deploying packages...') 53 | 54 | // Deploy each of the packages 55 | await pSeries( 56 | packagesToDeploy.map((pkg: Package) => async () => { 57 | const deployPlugin = pkg.plugins.deployPlugin 58 | if (!deployPlugin) { 59 | return 60 | } 61 | 62 | await deployPlugin.plugin.deploy(pkg, deployPlugin.options, { 63 | config: config(), 64 | }) 65 | }), 66 | ) 67 | 68 | TerminalUtils.success('Deployments complete') 69 | } 70 | -------------------------------------------------------------------------------- /packages/lib/src/packages/resolve-packages.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package } from '../types' 4 | 5 | const R = require('ramda') 6 | const config = require('../config') 7 | const TerminalUtils = require('../terminal') 8 | 9 | type Options = { 10 | strict?: boolean, 11 | } 12 | 13 | type DefaultedOptions = { 14 | strict: boolean, 15 | } 16 | 17 | const defaultOptions = { 18 | strict: false, 19 | } 20 | 21 | /** 22 | * Filters the packages down to the given. 23 | * 24 | * @param packageFilters 25 | * The names of the packages to resolve. If none is specified then 26 | * all of them are resolved. 27 | * @param options.strict 28 | * When enabled then only the packages within packageFilters will 29 | * be returned. Otherwise the packages plus their dependencies will 30 | * be resolved. 31 | * 32 | * @return {Promise>} The resolved packages 33 | */ 34 | module.exports = function resolvePackages( 35 | packageFilters: ?Array = [], 36 | options: Options = defaultOptions, 37 | ): Array { 38 | const defaultedOptions: DefaultedOptions = { 39 | ...defaultOptions, 40 | ...options, 41 | } 42 | 43 | const packages = config().packages 44 | 45 | if (!packageFilters || packageFilters.length === 0) { 46 | return packages 47 | } 48 | 49 | TerminalUtils.verbose( 50 | `Resolving packages with filter [${packageFilters.join(', ')}]`, 51 | ) 52 | 53 | const packageNames = packages.map(x => x.name) 54 | const invalidFilters = R.without(packageNames, packageFilters) 55 | if (invalidFilters.length > 0) { 56 | throw new Error( 57 | `The following packages could not be resolved:\n[${invalidFilters.join( 58 | ',', 59 | )}]`, 60 | ) 61 | } 62 | 63 | const targets = new Set() 64 | 65 | packageFilters.forEach(name => { 66 | targets.add(name) 67 | if (!defaultedOptions.strict) { 68 | config().packageMap[name].allDependencies.forEach(x => { 69 | targets.add(x) 70 | }) 71 | } 72 | }) 73 | 74 | const filteredPackagesNames = [...targets] 75 | 76 | // Let's get a sorted version by filtering allPackages 77 | // which will already be in a safe build order. 78 | const result = packages.filter( 79 | x => !!filteredPackagesNames.find(name => name === x.name), 80 | ) 81 | 82 | TerminalUtils.verbose(`Resolved: [${result.map(R.prop('name')).join(', ')}]`) 83 | return result 84 | } 85 | -------------------------------------------------------------------------------- /packages/lib/src/plugins/resolve-plugin.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable global-require */ 4 | /* eslint-disable no-console */ 5 | 6 | import type { 7 | CleanPlugin, 8 | BuildPlugin, 9 | DevelopPlugin, 10 | DeployPlugin, 11 | } from '../types' 12 | 13 | const R = require('ramda') 14 | const fs = require('fs-extra') 15 | const path = require('path') 16 | 17 | const pluginCache = {} 18 | 19 | const resolvePackage = (packageName: string): mixed => { 20 | const packagePath = require.resolve(packageName) 21 | console.info(`Trying to resolve package ${packagePath}`) 22 | let resolvedPackage 23 | try { 24 | // eslint-disable-next-line global-require,import/no-dynamic-require 25 | resolvedPackage = require(packagePath) 26 | } catch (err) { 27 | console.info(`Failed to resolve package ${packagePath}`) 28 | console.info(err) 29 | console.info(`Trying to resolve package ${packagePath} as a symlink`) 30 | // EEK! Could be a symlink? 31 | try { 32 | fs.lstatSync(packagePath) 33 | const symLinkPath = fs.readlinkSync(path) 34 | // eslint-disable-next-line global-require,import/no-dynamic-require 35 | resolvedPackage = require(symLinkPath) 36 | } catch (symErr) { 37 | // DO nothing 38 | console.info(`Failed to resolve package ${packagePath} as a symlink`) 39 | console.info(symErr) 40 | } 41 | } 42 | 43 | console.info(`Resolved package ${packagePath}`) 44 | 45 | return resolvedPackage 46 | } 47 | 48 | module.exports = ( 49 | pluginName: string, 50 | ): CleanPlugin | BuildPlugin | DevelopPlugin | DeployPlugin => { 51 | if (R.isEmpty(pluginName) || R.isNil(pluginName)) { 52 | throw new Error('No plugin name was given to resolvePlugin') 53 | } 54 | 55 | // Core plugins 56 | switch (pluginName) { 57 | case 'plugin-clean-build': 58 | return require('./clean-build') 59 | case 'plugin-develop-build': 60 | return require('./develop-build') 61 | case 'plugin-develop-server': 62 | return require('./develop-server') 63 | case 'plugin-script': 64 | return require('./script') 65 | default: 66 | // Do nothing, fall through and resolve custom plugin... 67 | } 68 | 69 | if (pluginCache[pluginName]) { 70 | return pluginCache[pluginName] 71 | } 72 | 73 | const packagePlugin = resolvePackage(pluginName) 74 | 75 | if (!packagePlugin) { 76 | throw new Error( 77 | `Could not resolve "${pluginName}" plugin. Make sure you have the plugin installed.`, 78 | ) 79 | } 80 | 81 | pluginCache[pluginName] = packagePlugin 82 | 83 | // $FlowFixMe 84 | return packagePlugin 85 | } 86 | -------------------------------------------------------------------------------- /packages/lib/src/childProcess.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ExecaChildProcess } from 'execa' 4 | import type { Package } from './types' 5 | 6 | const execa = require('execa') 7 | const TerminalUtils = require('./terminal') 8 | const StringUtils = require('./strings') 9 | 10 | function exec( 11 | command: string, 12 | args?: Array = [], 13 | opts?: Object = {}, 14 | ): ExecaChildProcess { 15 | TerminalUtils.verbose( 16 | `exec child process: ${command} ${args.join(' ')}${ 17 | opts.cwd ? ` (${opts.cwd})` : '' 18 | }`, 19 | ) 20 | 21 | process.env.FORCE_COLOR = 'true' 22 | 23 | return execa( 24 | command, 25 | args, 26 | Object.assign( 27 | {}, 28 | { 29 | env: process.env, 30 | stdio: 'pipe', 31 | }, 32 | opts, 33 | ), 34 | ).then(result => result.stdout) 35 | } 36 | 37 | function execPkg( 38 | pkg: Package, 39 | command: string, 40 | args?: Array = [], 41 | opts?: Object = {}, 42 | ): ExecaChildProcess { 43 | process.env.FORCE_COLOR = 'true' 44 | 45 | const childProcess = execa( 46 | command, 47 | args, 48 | Object.assign({}, opts, { 49 | env: process.env, 50 | }), 51 | ) 52 | 53 | childProcess.stdout.on('data', data => { 54 | // eslint-disable-next-line no-console 55 | console.log(StringUtils.packageMsg(pkg, data)) 56 | }) 57 | 58 | childProcess.stderr.on('data', data => { 59 | // eslint-disable-next-line no-console 60 | console.error(StringUtils.packageMsg(pkg, data)) 61 | }) 62 | 63 | return childProcess 64 | } 65 | 66 | function execSync( 67 | command: string, 68 | args?: Array = [], 69 | opts?: Object = {}, 70 | ): string { 71 | process.env.FORCE_COLOR = 'true' 72 | 73 | TerminalUtils.verbose( 74 | `execSync child process: ${command} ${args.join(' ')}${ 75 | opts.cwd ? ` (${opts.cwd})` : '' 76 | }`, 77 | ) 78 | 79 | return execa.sync( 80 | command, 81 | args, 82 | Object.assign( 83 | {}, 84 | { 85 | env: process.env, 86 | }, 87 | opts, 88 | ), 89 | ).stdout 90 | } 91 | 92 | function execSyncPkg( 93 | pkg: Package, 94 | command: string, 95 | args?: Array = [], 96 | opts?: Object = {}, 97 | ): string { 98 | const output = execSync(command, args, opts) 99 | // eslint-disable-next-line no-console 100 | console.log(StringUtils.packageMsg(pkg, output)) 101 | return output 102 | } 103 | 104 | module.exports = { 105 | exec, 106 | execPkg, 107 | execSync, 108 | execSyncPkg, 109 | } 110 | -------------------------------------------------------------------------------- /packages/lib/src/plugins/develop-server/develop.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package, DevelopInstance } from '../../types' 4 | 5 | const path = require('path') 6 | const TerminalUtils = require('../../terminal') 7 | const ChildProcessUtils = require('../../childProcess') 8 | const PackageUtils = require('../../packages') 9 | const PluginUtils = require('../utils') 10 | 11 | const childProcessMap = {} 12 | 13 | const killChildProcessFor = (pkg: Package): Promise => { 14 | const childProcess = childProcessMap[pkg.name] 15 | if (!childProcess) { 16 | TerminalUtils.verbose(`No running child process for ${pkg.name} to kill`) 17 | return Promise.resolve() 18 | } 19 | 20 | return PluginUtils.killChildProcess(pkg, childProcess).then(() => { 21 | TerminalUtils.verbose(`${pkg.name} killed successfully`) 22 | if (childProcessMap[pkg.name]) { 23 | delete childProcessMap[pkg.name] 24 | } 25 | }) 26 | } 27 | 28 | module.exports = function develop(pkg: Package): Promise { 29 | const startServer = (): Promise => 30 | new Promise((resolve, reject) => { 31 | const childProcess = ChildProcessUtils.execPkg( 32 | pkg, 33 | // Spawn a node process 34 | 'node', 35 | // That runs the main file 36 | [path.resolve(pkg.paths.packageRoot, pkg.packageJson.main)], 37 | { 38 | cwd: pkg.paths.packageRoot, 39 | }, 40 | ) 41 | 42 | childProcess.catch(err => { 43 | TerminalUtils.verbosePkg(pkg, `Error starting`) 44 | reject(err) 45 | }) 46 | 47 | // Give the catch above a tick of space, so that it can resolve any 48 | // error that may have occurred 49 | process.nextTick(() => { 50 | if (!childProcess.stderr) { 51 | TerminalUtils.verbosePkg( 52 | pkg, 53 | 'Not resolving server as childProcess was not created properly. An error probably occurred.', 54 | ) 55 | reject(new Error(`${pkg.name} has problems. Please fix`)) 56 | } else { 57 | childProcess.on('close', () => { 58 | TerminalUtils.verbosePkg(pkg, `Server process stopped`) 59 | }) 60 | 61 | childProcessMap[pkg.name] = childProcess 62 | 63 | resolve() 64 | } 65 | }) 66 | }) 67 | 68 | return ( 69 | PackageUtils.buildPackage(pkg) 70 | // Ensure any existing childProcess is killed 71 | .then(() => killChildProcessFor(pkg)) 72 | // Fire up the new childProcess 73 | .then(startServer) 74 | // Return the dev instance API 75 | .then(() => ({ 76 | kill: () => killChildProcessFor(pkg), 77 | })) 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /packages/plugin-build-flow/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | Package, 5 | CleanPlugin, 6 | BuildPlugin, 7 | } from '@lerna-cola/lib/build/types' 8 | 9 | const os = require('os') 10 | const path = require('path') 11 | const pLimit = require('p-limit') 12 | const R = require('ramda') 13 | const fs = require('fs-extra') 14 | const globby = require('globby') 15 | const flowRemoveTypes = require('flow-remove-types') 16 | const { 17 | Errors: { PackageError }, 18 | } = require('@lerna-cola/lib') 19 | 20 | const maxConcurrentTranspiles = os.cpus().length 21 | 22 | // :: string -> void 23 | const ensureParentDirectoryExists = filePath => { 24 | const dir = path.dirname(filePath) 25 | fs.ensureDirSync(dir) 26 | } 27 | 28 | type PluginOptions = { 29 | inputs?: Array, 30 | } 31 | 32 | const flowBuildPlugin: CleanPlugin & BuildPlugin = { 33 | name: '@lerna-cola/plugin-build-flow', 34 | build: (pkg: Package, options: PluginOptions) => { 35 | const patterns = ( 36 | options.inputs || ['**/*.js', '!__tests__', '!test.js'] 37 | ).concat(['!node_modules/**/*', `!${pkg.paths.packageBuildOutput}/**/*`]) 38 | 39 | // :: string -> Array 40 | const getJsFilePaths = () => 41 | globby(patterns, { 42 | cwd: pkg.paths.packageSrc, 43 | }) 44 | 45 | return getJsFilePaths().then(filePaths => { 46 | const transpileFile = filePath => 47 | new Promise(resolve => { 48 | const module = path.resolve(pkg.paths.packageSrc, filePath) 49 | const input = fs.readFileSync(module, 'utf8') 50 | const output = flowRemoveTypes(input) 51 | const outFile = path.resolve(pkg.paths.packageBuildOutput, filePath) 52 | ensureParentDirectoryExists(outFile) 53 | fs.writeFileSync(outFile, output.toString(), { encoding: 'utf8' }) 54 | fs.writeFileSync(`${outFile}.flow`, input, { encoding: 'utf8' }) 55 | resolve() 56 | }) 57 | 58 | const limit = pLimit(maxConcurrentTranspiles) 59 | const queueTranspile = filePath => limit(() => transpileFile(filePath)) 60 | return Promise.all(R.map(queueTranspile, filePaths)) 61 | }) 62 | }, 63 | clean: (pkg: Package) => 64 | new Promise(resolve => { 65 | if (fs.pathExistsSync(pkg.paths.packageBuildOutput)) { 66 | fs.removeSync(pkg.paths.packageBuildOutput) 67 | } 68 | resolve() 69 | }), 70 | deploy: (pkg: Package) => { 71 | throw new PackageError(pkg, '"deploy" not supported by "flow" plugin') 72 | }, 73 | develop: (pkg: Package) => { 74 | throw new PackageError(pkg, '"develop" not supported by "flow" plugin') 75 | }, 76 | } 77 | 78 | module.exports = flowBuildPlugin 79 | -------------------------------------------------------------------------------- /packages/lib/src/colors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Chalk } from 'chalk' 4 | 5 | const chalk = require('chalk') 6 | 7 | const background = [ 8 | 'bgGreen', 9 | 'bgYellow', 10 | 'bgBlue', 11 | 'bgMagenta', 12 | 'bgCyan', 13 | 'bgWhite', 14 | 'bgBlackBright', 15 | 'bgRedBright', 16 | 'bgGreenBright', 17 | 'bgYellowBright', 18 | 'bgBlueBright', 19 | 'bgMagentaBright', 20 | 'bgCyanBright', 21 | 'bgWhiteBright', 22 | 'bgBlack', 23 | 'bgRed', 24 | ] 25 | 26 | const foreground = [ 27 | /* 0 */ 'green', 28 | /* 1 */ 'yellow', 29 | /* 2 */ 'blue', 30 | /* 3 */ 'magenta', 31 | /* 4 */ 'cyan', 32 | /* 5 */ 'white', 33 | /* 6 */ 'gray', 34 | /* 7 */ 'redBright', 35 | /* 8 */ 'greenBright', 36 | /* 9 */ 'yellowBright', 37 | /* 10 */ 'blueBright', 38 | /* 11 */ 'magentaBright', 39 | /* 12 */ 'cyanBright', 40 | /* 13 */ 'whiteBright', 41 | /* 14 */ 'black', 42 | /* 15 */ 'red', 43 | ] 44 | 45 | const colorPairs = [ 46 | [0, 2], 47 | [0, 3], 48 | [0, 9], 49 | [0, 13], 50 | [0, 14], 51 | [1, 2], 52 | [1, 3], 53 | [1, 11], 54 | [1, 14], 55 | [1, 15], 56 | [2, 0], 57 | [2, 1], 58 | [2, 8], 59 | [2, 11], 60 | [2, 12], 61 | [2, 13], 62 | [2, 15], 63 | [3, 5], 64 | [3, 8], 65 | [3, 9], 66 | [3, 12], 67 | [3, 13], 68 | [4, 2], 69 | [4, 3], 70 | [4, 6], 71 | [4, 13], 72 | [4, 14], 73 | [4, 15], 74 | [5, 2], 75 | [5, 3], 76 | [5, 6], 77 | [5, 7], 78 | [5, 10], 79 | [5, 14], 80 | [5, 15], 81 | [6, 0], 82 | [6, 1], 83 | [6, 2], 84 | [6, 4], 85 | [6, 8], 86 | [6, 9], 87 | [6, 12], 88 | [6, 13], 89 | [6, 14], 90 | [7, 2], 91 | [7, 9], 92 | [7, 12], 93 | [7, 13], 94 | [7, 14], 95 | [8, 2], 96 | [8, 3], 97 | [8, 6], 98 | [8, 7], 99 | [8, 10], 100 | [8, 14], 101 | [8, 15], 102 | [9, 2], 103 | [9, 3], 104 | [9, 6], 105 | [9, 7], 106 | [9, 10], 107 | [9, 11], 108 | [9, 14], 109 | [9, 15], 110 | [10, 8], 111 | [10, 9], 112 | [10, 12], 113 | [10, 13], 114 | [10, 14], 115 | [11, 2], 116 | [11, 14], 117 | [12, 2], 118 | [12, 3], 119 | [12, 6], 120 | [12, 7], 121 | [12, 10], 122 | [12, 14], 123 | [12, 15], 124 | [13, 0], 125 | [13, 2], 126 | [13, 3], 127 | [13, 4], 128 | [13, 6], 129 | [13, 7], 130 | [13, 10], 131 | [13, 14], 132 | [13, 15], 133 | ] 134 | .reverse() 135 | .map(([b, f]) => [f, b]) 136 | .sort(([fx], [fy]) => fy - fx) 137 | 138 | let index = -1 139 | 140 | const nextColorPair = (): Chalk => { 141 | index += 1 142 | if (index >= colorPairs.length) { 143 | index = 0 144 | } 145 | const [fgIndex, bgIndex] = colorPairs[index] 146 | const bg = background[bgIndex] 147 | const fg = foreground[fgIndex] 148 | // $FlowFixMe 149 | return chalk[bg][fg] 150 | } 151 | 152 | module.exports = { 153 | nextColorPair, 154 | } 155 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const os = require('os') 4 | const path = require('path') 5 | const pLimit = require('p-limit') 6 | const R = require('ramda') 7 | const fs = require('fs-extra') 8 | const globby = require('globby') 9 | const flowRemoveTypes = require('flow-remove-types') 10 | const rimraf = require('rimraf') 11 | const pify = require('pify') 12 | 13 | const rimrafAsync = pify(rimraf) 14 | 15 | const flowPackages = [ 16 | 'babel-config', 17 | 'cli', 18 | 'lib', 19 | 'plugin-build-babel', 20 | 'plugin-build-flow', 21 | 'plugin-deploy-now', 22 | ] 23 | 24 | const maxConcurrentTranspiles = os.cpus().length 25 | 26 | const ensureParentDirectoryExists = filePath => { 27 | const dir = path.dirname(filePath) 28 | fs.ensureDirSync(dir) 29 | } 30 | 31 | async function cleanFlowPackage(packageName) { 32 | const outputDir = path.resolve( 33 | process.cwd(), 34 | `./packages/${packageName}/build`, 35 | ) 36 | return rimrafAsync(outputDir) 37 | } 38 | 39 | async function buildFlowPackage(packageName) { 40 | const outputDir = path.resolve( 41 | process.cwd(), 42 | `./packages/${packageName}/build`, 43 | ) 44 | const sourceDir = path.resolve(process.cwd(), `./packages/${packageName}/src`) 45 | 46 | const patterns = ['**/*.js', '!__tests__', '!test.js'].concat([ 47 | '!node_modules/**/*', 48 | `!${path.basename(outputDir)}/**/*`, 49 | ]) 50 | 51 | // :: string -> Array 52 | const getJsFilePaths = () => 53 | globby(patterns, { 54 | cwd: sourceDir, 55 | }) 56 | 57 | const filePaths = await getJsFilePaths() 58 | 59 | const transpileFile = filePath => 60 | new Promise(resolve => { 61 | const module = path.resolve(sourceDir, filePath) 62 | const input = fs.readFileSync(module, 'utf8') 63 | const output = flowRemoveTypes(input) 64 | const outFile = path.resolve(outputDir, filePath) 65 | ensureParentDirectoryExists(outFile) 66 | fs.writeFileSync(outFile, output.toString(), { encoding: 'utf8' }) 67 | fs.writeFileSync(`${outFile}.flow`, input, { encoding: 'utf8' }) 68 | resolve() 69 | }) 70 | 71 | const limit = pLimit(maxConcurrentTranspiles) 72 | const queueTranspile = filePath => limit(() => transpileFile(filePath)) 73 | await Promise.all(R.map(queueTranspile, filePaths)) 74 | } 75 | 76 | module.exports = { 77 | buildAsync: () => { 78 | console.log('Building...') 79 | return Promise.all(flowPackages.map(buildFlowPackage)).then( 80 | () => { 81 | console.log('Build complete') 82 | }, 83 | err => { 84 | console.error('Build failed') 85 | console.error(err) 86 | process.exit(1) 87 | }, 88 | ) 89 | }, 90 | cleanAsync: () => { 91 | console.log('Cleaning...') 92 | return Promise.all(flowPackages.map(cleanFlowPackage)).then( 93 | () => { 94 | console.log('Clean complete') 95 | }, 96 | err => { 97 | console.error('Clean failed') 98 | console.error(err) 99 | process.exit(1) 100 | }, 101 | ) 102 | }, 103 | } 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sean+lerna-cola@ctrlplusb.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /packages/cli/src/development-service/graceful-shutdown-manager.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | PackageConductor, 5 | PackageWatcher, 6 | } from '@lerna-cola/lib/build/types' 7 | 8 | const R = require('ramda') 9 | const { config, TerminalUtils } = require('@lerna-cola/lib') 10 | 11 | type PackageWatchers = { [key: string]: PackageWatcher } 12 | type PackageConductors = { [key: string]: PackageConductor } 13 | 14 | module.exports = function gracefulShutdownManager( 15 | packageConductors: PackageConductors, 16 | packageWatchers: PackageWatchers, 17 | ) { 18 | let shuttingDown = false 19 | let postDevelopRun = false 20 | 21 | const postDevelopHook = config().commandHooks.develop.pre 22 | 23 | const ensurePostDevelopHookRun = async () => { 24 | if (postDevelopHook && !postDevelopRun) { 25 | TerminalUtils.info('Running post develop hook') 26 | await postDevelopHook() 27 | } 28 | postDevelopRun = true 29 | } 30 | 31 | async function performGracefulShutdown(exitCode) { 32 | // Avoid multiple calls (e.g. if ctrl+c pressed multiple times) 33 | if (shuttingDown) return 34 | shuttingDown = true 35 | try { 36 | TerminalUtils.info('Shutting down development service...') 37 | 38 | // This will ensure that the process exits after a 10 second grace period. 39 | // Hopefully all the dispose functions below would have completed 40 | setTimeout(async () => { 41 | TerminalUtils.verbose('Forcing shutdown after grace period') 42 | setTimeout(() => { 43 | TerminalUtils.warning( 44 | 'Your post develop hook seems to be taking a long time to complete. 10 seconds have passed so we are now forcing an exit on the develop process.', 45 | ) 46 | process.exit(1) 47 | }, 10 * 1000) 48 | // Even if we are forcing an exit we should wait for pross develop 49 | // hook to execute 50 | await ensurePostDevelopHookRun() 51 | process.exit(1) 52 | }, 10 * 1000) 53 | 54 | // Firstly kill all our packageWatchers. 55 | Object.keys(packageWatchers).forEach(packageName => 56 | packageWatchers[packageName].stop(), 57 | ) 58 | 59 | // Then call off the `.stop()` against all our package conductors. 60 | await Promise.all( 61 | R.values(packageConductors).map(packageDevelopConductor => 62 | packageDevelopConductor.stop(), 63 | ), 64 | ) 65 | 66 | // Then call the post develop hook 67 | await ensurePostDevelopHookRun() 68 | } catch (err) { 69 | TerminalUtils.error( 70 | 'An error occurred whilst shutting down the development service', 71 | err, 72 | ) 73 | process.exit(1) 74 | } 75 | process.exit(exitCode) 76 | } 77 | 78 | // Ensure that we perform a graceful shutdown when any of the following 79 | // signals are sent to our process. 80 | ;['SIGINT', 'SIGTERM'].forEach(signal => { 81 | process.on(signal, () => { 82 | TerminalUtils.verbose(`Received ${signal} termination signal`) 83 | performGracefulShutdown(0) 84 | }) 85 | }) 86 | 87 | process.on('unhandledRejection', err => { 88 | TerminalUtils.error('Unhandled error.', err) 89 | performGracefulShutdown(1) 90 | }) 91 | 92 | process.on('exit', () => { 93 | performGracefulShutdown(0) 94 | TerminalUtils.info('Till next time. *kiss*') 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /packages/babel-config/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package } from '@lerna-cola/lib/build/types' 4 | 5 | const semver = require('semver') 6 | const { ArrayUtils, LogicUtils } = require('@lerna-cola/lib') 7 | 8 | type Options = { 9 | nodeVersion?: string, 10 | } 11 | 12 | module.exports = function generateConfig(pkg: Package, options: Options = {}) { 13 | const env = process.env.BABEL_ENV || process.env.NODE_ENV 14 | 15 | const targetNodeVersion = options.nodeVersion || process.versions.node 16 | 17 | return { 18 | babelrc: false, 19 | 20 | // Handy for sourcemaps generation. 21 | sourceRoot: pkg.paths.packageRoot, 22 | 23 | // Source maps will be useful for debugging errors in our node executions. 24 | sourceMaps: 'both', 25 | 26 | presets: ArrayUtils.removeNil([ 27 | [ 28 | 'env', 29 | { 30 | targets: { 31 | node: targetNodeVersion, 32 | }, 33 | }, 34 | ], 35 | 36 | // jsx && flow support 37 | 'react', 38 | ]), 39 | 40 | plugins: ArrayUtils.removeNil([ 41 | // const { foo, ...others } = object 42 | // object = { foo, ...others } 43 | // This plugin uses Object.assign directly. 44 | [ 45 | 'transform-object-rest-spread', 46 | { 47 | // For node >= 6 we can rely on native Object.assign, else it will 48 | // need to be polyfilled. 49 | useBuiltIns: semver.major(targetNodeVersion) >= 6, 50 | }, 51 | ], 52 | 53 | // function ( 54 | // arg1, 55 | // arg2, 56 | // ) { } 57 | LogicUtils.onlyIf( 58 | semver.major(targetNodeVersion) < 8, 59 | 'syntax-trailing-function-commas', 60 | ), 61 | 62 | // class { handleThing = () => { } } 63 | 'transform-class-properties', 64 | 65 | // Compiles import() to a deferred require() 66 | 'babel-plugin-dynamic-import-node', 67 | 68 | // Polyfills the runtime needed for async/await and generators. 69 | // async/await exists in Node 7.6.0 upwards 70 | LogicUtils.onlyIf( 71 | semver.lt(targetNodeVersion, '7.6.0'), 72 | 'babel-plugin-transform-runtime', 73 | ), 74 | 75 | // Replaces the React.createElement function with one that is 76 | // more optimized for production. 77 | LogicUtils.onlyIf( 78 | env === 'production', 79 | 'transform-react-inline-elements', 80 | ), 81 | 82 | // Hoists element creation to the top level for subtrees that 83 | // are fully static, which reduces call to React.createElement 84 | // and the resulting allocations. More importantly, it tells 85 | // React that the subtree hasn’t changed so React can completely 86 | // skip it when reconciling. 87 | LogicUtils.onlyIf( 88 | env === 'production', 89 | 'transform-react-constant-elements', 90 | ), 91 | 92 | // Removes PropTypes code as it's just dead weight for a production build. 93 | LogicUtils.onlyIf( 94 | env === 'production', 95 | 'babel-plugin-transform-react-remove-prop-types', 96 | ), 97 | 98 | // The following two plugins are currently necessary to make React warnings 99 | // include more valuable information. They are included here because they are 100 | // currently not enabled in babel-preset-react. See the below threads for more info: 101 | // https://github.com/babel/babel/issues/4702 102 | // https://github.com/babel/babel/pull/3540#issuecomment-228673661 103 | // https://github.com/facebookincubator/create-react-app/issues/989 104 | 105 | // Adds __self attribute to JSX which React will use for some warnings 106 | LogicUtils.onlyIf( 107 | env === 'development' || env === 'test', 108 | 'transform-react-jsx-self', 109 | ), 110 | 111 | // Adds component stack to warning messages 112 | LogicUtils.onlyIf( 113 | env === 'development' || env === 'test', 114 | 'transform-react-jsx-source', 115 | ), 116 | 117 | // If we are transpiling a node package then we inject some code to 118 | // include source maps support on the transpiled code. 119 | LogicUtils.onlyIf(env === 'development', 'inject-source-map-init'), 120 | ]), 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/lib/src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-use-before-define */ 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | import type { ChildProcess } from 'child_process' 6 | import type { Chalk } from 'chalk' 7 | 8 | export type LernaColaPluginConfig = 9 | | string 10 | | { 11 | name: string, 12 | options: Object, 13 | } 14 | 15 | export type CommandHooks = { 16 | pre: () => Promise, 17 | post: () => Promise, 18 | } 19 | 20 | export type LernaColaCommandHooksConfig = { 21 | clean?: CommandHooks, 22 | build?: CommandHooks, 23 | develop?: CommandHooks, 24 | deploy?: CommandHooks, 25 | } 26 | 27 | export type LernaColaPackageConfig = { 28 | srcDir: string, 29 | entryFile: string, 30 | outputDir: string, 31 | disableSrcWatching: boolean, 32 | cleanPlugin?: LernaColaPluginConfig, 33 | buildPlugin?: LernaColaPluginConfig, 34 | developPlugin?: LernaColaPluginConfig, 35 | deployPlugin?: LernaColaPluginConfig, 36 | } 37 | 38 | export type LernaColaConfig = { 39 | commandHooks?: LernaColaCommandHooksConfig, 40 | packages: { [key: string]: LernaColaPackageConfig }, 41 | packageSources?: Array, 42 | } 43 | 44 | export type PackageVersions = { [string]: string } 45 | 46 | export type PackageWatcher = { 47 | start: () => void, 48 | stop: () => void, 49 | } 50 | 51 | export type RunType = 'FIRST_RUN' | 'SELF_CHANGED' | 'DEPENDENCY_CHANGED' 52 | 53 | export type PackageConductor = { 54 | run: (type: RunType, changedDependency?: Package) => Promise, 55 | stop: () => Promise, 56 | } 57 | 58 | export type DevelopInstance = { 59 | kill: () => Promise, 60 | } 61 | 62 | /** 63 | * Paths for a Package 64 | */ 65 | export type PackagePaths = { 66 | monoRepoRoot: string, 67 | monoRepoRootNodeModules: string, 68 | packageBuildOutput: string, 69 | packageEntryFile: string, 70 | packageJson: string, 71 | packageLockJson: string, 72 | packageNodeModules: string, 73 | packageRoot: string, 74 | packageSrc: string, 75 | packageWebpackCache: string, 76 | } 77 | 78 | export type PluginArgs = { 79 | config: Config, 80 | } 81 | 82 | export type DevelopPluginArgs = PluginArgs & { 83 | runType: RunType, 84 | changedDependency?: Package, 85 | watcher: PackageWatcher, 86 | } 87 | 88 | export type CleanPlugin = { 89 | name: string, 90 | clean: (pkg: Package, options: Object, args: PluginArgs) => Promise, 91 | } 92 | 93 | export type BuildPlugin = { 94 | name: string, 95 | build: (pkg: Package, options: Object, args: PluginArgs) => Promise, 96 | } 97 | 98 | export type DeployPath = string 99 | 100 | export type DeployPlugin = { 101 | name: string, 102 | deploy: (pkg: Package, options: Object, args: PluginArgs) => Promise, 103 | } 104 | 105 | export type DevelopPlugin = { 106 | name: string, 107 | develop: ( 108 | pkg: Package, 109 | options: Object, 110 | args: DevelopPluginArgs, 111 | ) => Promise, 112 | } 113 | 114 | export type PackagePlugins = { 115 | cleanPlugin?: { 116 | plugin: CleanPlugin, 117 | options: Object, 118 | }, 119 | buildPlugin?: { 120 | plugin: BuildPlugin, 121 | options: Object, 122 | }, 123 | deployPlugin?: { 124 | plugin: DeployPlugin, 125 | options: Object, 126 | }, 127 | developPlugin?: { 128 | plugin: DevelopPlugin, 129 | options: Object, 130 | }, 131 | } 132 | 133 | export type Package = { 134 | name: string, 135 | config: Object, 136 | color: Chalk, 137 | disableSrcWatching: boolean, 138 | allDependants: Array, 139 | allDependencies: Array, 140 | dependants: Array, 141 | dependencies: Array, 142 | devDependencies: Array, 143 | packageJson: Object, 144 | paths: PackagePaths, 145 | plugins: PackagePlugins, 146 | version: string, 147 | } 148 | 149 | export type PackageMap = { [string]: Package } 150 | 151 | export type Config = { 152 | commandHooks: { 153 | clean: CommandHooks, 154 | build: CommandHooks, 155 | develop: CommandHooks, 156 | deploy: CommandHooks, 157 | }, 158 | packages: Array, 159 | packageMap: PackageMap, 160 | terminalLabelMinLength: number, 161 | } 162 | 163 | declare module 'execa' { 164 | declare type ExecaChildProcess = ChildProcess & Promise 165 | declare type Execa = ( 166 | cmd: string, 167 | args: ?Array, 168 | opts: ?Object, 169 | ) => ExecaChildProcess 170 | 171 | declare type ExecaStatics = { 172 | spawn: Execa, 173 | sync: ChildProcess, 174 | } 175 | 176 | declare type ExecaWithStatics = Execa & ExecaStatics 177 | 178 | declare module.exports: ExecaWithStatics 179 | } 180 | -------------------------------------------------------------------------------- /packages/lib/src/config/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-console */ 4 | /* eslint-disable global-require */ 5 | 6 | import type { LernaColaPluginConfig, Package } from '../types' 7 | 8 | const R = require('ramda') 9 | const toposort = require('toposort') 10 | const resolvePlugin = require('../plugins/resolve-plugin') 11 | 12 | const allDeps = (pkg: Package) => 13 | (pkg.dependencies || []).concat(pkg.devDependencies || []) 14 | 15 | const getDependencies = ( 16 | pkg: Package, 17 | packages: Array, 18 | dependencyType: 'dependencies' | 'devDependencies', 19 | ): Array => { 20 | const targetDependencies = R.path(['packageJson', dependencyType], pkg) 21 | if (!targetDependencies) { 22 | return [] 23 | } 24 | return Object.keys(targetDependencies).reduce((acc, cur) => { 25 | const match = packages.find(x => x.name === cur) 26 | return match ? [...acc, match.name] : acc 27 | }, []) 28 | } 29 | 30 | const getDependants = (pkg: Package, packages: Array): Array => 31 | packages.filter(x => R.contains(pkg.name, allDeps(x))).map(R.prop('name')) 32 | 33 | const orderByDependencies = (packages: Array): Array => { 34 | const packageDependencyGraph = (pkg: Package): Array> => 35 | R.pipe( 36 | allDeps, 37 | R.map(dependencyName => [dependencyName, pkg.name]), 38 | )(pkg) 39 | 40 | // $FlowFixMe 41 | const dependencyGraph = R.chain(packageDependencyGraph) 42 | 43 | const hasNoDependencies = ({ dependencies }: Package): boolean => 44 | dependencies.length === 0 45 | 46 | const packagesWithNoDependencies: Array = R.pipe( 47 | R.filter(hasNoDependencies), 48 | R.map(R.prop('name')), 49 | )(packages) 50 | 51 | const findPackageByName = (packageNames: Array): Array => 52 | packageNames.reduce((acc, name) => { 53 | const found = packages.find(x => x.name === name) 54 | if (found != null) { 55 | return [...acc, found] 56 | } 57 | return acc 58 | }, []) 59 | 60 | return R.pipe( 61 | dependencyGraph, 62 | toposort, 63 | R.without(packagesWithNoDependencies), 64 | R.concat(packagesWithNoDependencies), 65 | findPackageByName, 66 | )(packages) 67 | } 68 | 69 | const getAllDependants = ( 70 | pkg: Package, 71 | packages: Array, 72 | ): Array => { 73 | const findPackage = name => packages.find(x => x.name === name) 74 | 75 | const resolveDependants = dependantName => { 76 | const dependant = findPackage(dependantName) 77 | if (!dependant) { 78 | throw new Error( 79 | `Could not find dependant package "${dependantName || 80 | ''}" for package ${pkg.name}`, 81 | ) 82 | } 83 | return [ 84 | dependant.name, 85 | ...dependant.dependants, 86 | ...R.map(resolveDependants, dependant.dependants), 87 | ] 88 | } 89 | 90 | // $FlowFixMe 91 | const allDependants = R.chain(resolveDependants, pkg.dependants) 92 | 93 | // Let's get a sorted version of allDependants by filtering allPackages 94 | // which will already be in a safe build order. 95 | return packages 96 | .filter(x => !!R.find(R.equals(x.name), allDependants)) 97 | .map(R.prop('name')) 98 | } 99 | 100 | const getAllDependencies = ( 101 | pkg: Package, 102 | packages: Array, 103 | ): Array => { 104 | const findPackage = name => R.find(R.propEq('name', name), packages) 105 | 106 | const resolveDependencies = dependencyName => { 107 | const dependency = findPackage(dependencyName) 108 | if (!dependency) { 109 | throw new Error( 110 | `Could not find dependency package "${dependencyName || 111 | ''}" for package ${pkg.name}`, 112 | ) 113 | } 114 | return [ 115 | dependency.name, 116 | ...dependency.dependencies, 117 | ...R.map(resolveDependencies, dependency.dependencies), 118 | ] 119 | } 120 | 121 | // $FlowFixMe 122 | const allDependencies = R.chain(resolveDependencies, pkg.dependencies) 123 | 124 | // Let's get a sorted version of allDependencies by filtering allPackages 125 | // which will already be in a safe build order. 126 | return packages 127 | .filter(x => !!R.find(R.equals(x.name), allDependencies)) 128 | .map(R.prop('name')) 129 | } 130 | 131 | const getPlugin = ( 132 | packageName: string, 133 | pluginConfig: ?LernaColaPluginConfig, 134 | pluginType: string, 135 | ) => { 136 | if (pluginConfig == null) { 137 | return undefined 138 | } 139 | try { 140 | const config = 141 | typeof pluginConfig === 'string' 142 | ? { name: pluginConfig, options: {} } 143 | : pluginConfig 144 | return { 145 | plugin: resolvePlugin(config.name), 146 | options: config.options, 147 | } 148 | } catch (err) { 149 | console.error(`Failed to load "${pluginType}" for ${packageName}`) 150 | throw err 151 | } 152 | } 153 | 154 | module.exports = { 155 | getAllDependants, 156 | getAllDependencies, 157 | getDependants, 158 | getDependencies, 159 | getPlugin, 160 | orderByDependencies, 161 | } 162 | -------------------------------------------------------------------------------- /packages/lib/src/plugins/script/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ChildProcess } from 'child_process' 4 | import type { 5 | Package, 6 | CleanPlugin, 7 | BuildPlugin, 8 | DevelopPlugin, 9 | DeployPlugin, 10 | DevelopInstance, 11 | } from '../../types' 12 | 13 | const R = require('ramda') 14 | const TerminalUtils = require('../../terminal') 15 | const ChildProcessUtils = require('../../childProcess') 16 | const PluginUtils = require('../utils') 17 | const { PackageError } = require('../../errors') 18 | 19 | type Options = {| 20 | scriptName: string, 21 | runForEveryChange?: boolean, 22 | runSync?: boolean, 23 | |} 24 | 25 | type TaskName = 'build' | 'develop' | 'deploy' | 'clean' 26 | 27 | type Config = { 28 | managed: boolean, 29 | } 30 | 31 | type ChildProcessMap = { 32 | [key: TaskName]: { 33 | [key: string]: ChildProcess, 34 | }, 35 | } 36 | 37 | const childProcessMap: ChildProcessMap = { 38 | build: {}, 39 | clean: {}, 40 | develop: {}, 41 | deploy: {}, 42 | } 43 | 44 | const addChildProcess = ( 45 | pkg: Package, 46 | task: TaskName, 47 | processInstance: ChildProcess, 48 | ) => { 49 | childProcessMap[task][pkg.name] = processInstance 50 | } 51 | 52 | const removeChildProcess = (pkg: Package, task: TaskName) => { 53 | if (childProcessMap[task]) { 54 | delete childProcessMap[task][pkg.name] 55 | } 56 | } 57 | 58 | const getChildProcess = (pkg: Package, task: TaskName) => 59 | childProcessMap[task][pkg.name] 60 | 61 | const killChildProcessFor = (pkg: Package, task: TaskName) => { 62 | const childProcess = getChildProcess(pkg, task) 63 | if (!childProcess) { 64 | TerminalUtils.verbosePkg(pkg, `No running "${task}" script process to kill`) 65 | return Promise.resolve() 66 | } 67 | return PluginUtils.killChildProcess(pkg, childProcess).then(() => { 68 | TerminalUtils.verbosePkg( 69 | pkg, 70 | `Killed "${task}" script process successfully`, 71 | ) 72 | removeChildProcess(pkg, task) 73 | }) 74 | } 75 | 76 | const runScript = (task: TaskName, config: Config) => async ( 77 | pkg: Package, 78 | options: Options, 79 | ): Promise => { 80 | if (!options.scriptName || typeof options.scriptName !== 'string') { 81 | throw new Error( 82 | `No scriptName was provided for the develop configuration of ${ 83 | pkg.name 84 | }.`, 85 | ) 86 | } 87 | 88 | const scriptCmd = R.path(['scripts', options.scriptName], pkg.packageJson) 89 | if (!scriptCmd || R.isEmpty(scriptCmd)) { 90 | throw new Error( 91 | `Could not resolve script named "${options.scriptName}" on ${pkg.name}`, 92 | ) 93 | } 94 | 95 | if (config.managed) { 96 | const returnAPI: DevelopInstance = { 97 | kill: () => killChildProcessFor(pkg, task), 98 | } 99 | 100 | const existingProcess = getChildProcess(pkg, task) 101 | if (existingProcess && !options.runForEveryChange) { 102 | return task === 'develop' ? returnAPI : undefined 103 | } 104 | 105 | if (existingProcess) { 106 | await killChildProcessFor(pkg, task) 107 | } 108 | 109 | const execArgs = [ 110 | pkg, 111 | 'npm', 112 | ['run', options.scriptName], 113 | { 114 | cwd: pkg.paths.packageRoot, 115 | }, 116 | ] 117 | 118 | if (options.runSync) { 119 | ChildProcessUtils.execSyncPkg(...execArgs) 120 | } else { 121 | await new Promise((resolve, reject) => { 122 | TerminalUtils.infoPkg(pkg, `Executing script "${options.scriptName}"`) 123 | 124 | const childProcess = ChildProcessUtils.execPkg(...execArgs) 125 | 126 | childProcess.catch(err => { 127 | TerminalUtils.verbosePkg( 128 | pkg, 129 | `Error executing script "${options.scriptName}"`, 130 | ) 131 | reject(err) 132 | }) 133 | 134 | // Give the catch above a tick of space, so that it can resolve any 135 | // error that may have occurred 136 | process.nextTick(() => { 137 | childProcess.on('close', () => { 138 | TerminalUtils.verbosePkg( 139 | pkg, 140 | `Stopped script "${options.scriptName}" process`, 141 | ) 142 | // ensure that the process is removed 143 | removeChildProcess(pkg, task) 144 | }) 145 | addChildProcess(pkg, task, childProcess) 146 | resolve() 147 | }) 148 | }) 149 | } 150 | 151 | return returnAPI 152 | } 153 | 154 | TerminalUtils.infoPkg(pkg, `Executing script "${options.scriptName}"`) 155 | 156 | try { 157 | await ChildProcessUtils.execPkg(pkg, 'npm', ['run', options.scriptName], { 158 | cwd: pkg.paths.packageRoot, 159 | }) 160 | } catch (err) { 161 | throw new PackageError( 162 | pkg, 163 | `Error executing script "${options.scriptName}"`, 164 | err, 165 | ) 166 | } 167 | 168 | return undefined 169 | } 170 | 171 | const scriptPlugin: CleanPlugin & BuildPlugin & DevelopPlugin & DeployPlugin = { 172 | name: 'plugin-script', 173 | // $FlowFixMe 174 | build: runScript('build', { managed: false }), 175 | // $FlowFixMe 176 | clean: runScript('clean', { managed: false }), 177 | // $FlowFixMe 178 | develop: runScript('develop', { managed: true }), 179 | // $FlowFixMe 180 | deploy: runScript('deploy', { managed: false }), 181 | } 182 | 183 | module.exports = scriptPlugin 184 | -------------------------------------------------------------------------------- /packages/lib/src/terminal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-console */ 4 | 5 | import type { Package } from './types' 6 | 7 | const StringUtils = require('./strings') 8 | 9 | type MultiSelectChoice = { 10 | value: string, 11 | text: string, 12 | } 13 | 14 | type MultiSelectOptions = { 15 | choices: Array, 16 | selected?: Array, 17 | validate?: (Array) => boolean | string, 18 | } 19 | 20 | type SelectOptions = { 21 | choices: Array, 22 | selected?: string, 23 | validate?: string => boolean | string, 24 | } 25 | 26 | type InputOptions = { 27 | validate?: string => boolean | string, 28 | } 29 | 30 | type SingleValueAnswer = { 31 | type: string, 32 | value: string, 33 | } 34 | 35 | type ConfirmAnswer = { 36 | key: string, 37 | value: boolean, 38 | } 39 | 40 | const chalk = require('chalk') 41 | const inquirer = require('inquirer') 42 | const prettyFormat = require('pretty-format') 43 | 44 | const format = data => (typeof data === 'string' ? data : prettyFormat(data)) 45 | 46 | const formatVerbose = data => chalk.dim(format(data)) 47 | const formatError = data => chalk.red.bold(format(data)) 48 | const formatWarning = data => chalk.yellow(format(data)) 49 | const formatTitle = data => chalk.bold(format(data)) 50 | const formatInfo = data => format(data) 51 | const formatSuccess = data => chalk.green(format(data)) 52 | const formatHeader = data => chalk.bold(format(data)) 53 | 54 | function verbose(data: any): void { 55 | if (process.env.VERBOSE) { 56 | console.log(StringUtils.lernaColaMsg(formatVerbose(data))) 57 | } 58 | } 59 | 60 | function verbosePkg(pkg: Package, data: any): void { 61 | if (process.env.VERBOSE) { 62 | console.log(StringUtils.packageMsg(pkg, formatVerbose(data))) 63 | } 64 | } 65 | 66 | function error(data: string, err?: Error): void { 67 | console.log(StringUtils.lernaColaMsg(formatError(data))) 68 | if (err && err.stack) { 69 | console.log(StringUtils.lernaColaMsg(err.stack)) 70 | } 71 | } 72 | 73 | function errorPkg(pkg: Package, data: any, err?: Error): void { 74 | console.log(StringUtils.packageMsg(pkg, formatError(data))) 75 | if (err && err.stack) { 76 | console.log(StringUtils.packageMsg(pkg, err.stack)) 77 | } 78 | } 79 | 80 | function warning(data: any): void { 81 | console.log(StringUtils.lernaColaMsg(formatWarning(data))) 82 | } 83 | 84 | function warningPkg(pkg: Package, data: any): void { 85 | console.log(StringUtils.packageMsg(pkg, formatWarning(data))) 86 | } 87 | 88 | function title(data: any): void { 89 | console.log(StringUtils.lernaColaMsg(formatTitle(data))) 90 | } 91 | 92 | function titlePkg(pkg: Package, data: any): void { 93 | console.log(StringUtils.packageMsg(pkg, formatTitle(data))) 94 | } 95 | 96 | function info(data: any): void { 97 | console.log(StringUtils.lernaColaMsg(formatInfo(data))) 98 | } 99 | 100 | function infoPkg(pkg: Package, data: any): void { 101 | console.log(StringUtils.packageMsg(pkg, formatInfo(data))) 102 | } 103 | 104 | function success(data: any): void { 105 | console.log(StringUtils.lernaColaMsg(formatSuccess(data))) 106 | } 107 | 108 | function successPkg(pkg: Package, data: any): void { 109 | console.log(StringUtils.packageMsg(pkg, formatSuccess(data))) 110 | } 111 | 112 | function header(data: any): void { 113 | console.log(StringUtils.lernaColaMsg(formatHeader(data))) 114 | } 115 | 116 | function headerPkg(pkg: Package, data: any): void { 117 | console.log(StringUtils.packageMsg(pkg, formatHeader(data))) 118 | } 119 | 120 | function multiSelect( 121 | message: string, 122 | options: MultiSelectOptions, 123 | ): Promise> { 124 | const { choices, selected, validate } = options 125 | return inquirer 126 | .prompt([ 127 | { 128 | type: 'checkbox', 129 | name: 'prompt', 130 | message, 131 | choices, 132 | pageSize: choices.length, 133 | validate, 134 | default: selected, 135 | }, 136 | ]) 137 | .then(answers => answers.prompt) 138 | } 139 | 140 | function select( 141 | message: string, 142 | options: SelectOptions, 143 | ): Promise { 144 | const { choices, validate } = options 145 | return inquirer 146 | .prompt([ 147 | { 148 | type: 'list', 149 | name: 'prompt', 150 | message, 151 | choices, 152 | pageSize: choices.length, 153 | validate, 154 | }, 155 | ]) 156 | .then(answers => answers.prompt) 157 | } 158 | 159 | function input( 160 | message: string, 161 | options?: InputOptions = {}, 162 | ): Promise { 163 | const { validate } = options 164 | return inquirer 165 | .prompt([ 166 | { 167 | type: 'input', 168 | name: 'input', 169 | message, 170 | validate, 171 | }, 172 | ]) 173 | .then(answers => answers.input) 174 | } 175 | 176 | function confirm(message: string): Promise { 177 | return inquirer 178 | .prompt([ 179 | { 180 | type: 'expand', 181 | name: 'confirm', 182 | message, 183 | default: 2, // default to help in order to avoid clicking straight through 184 | choices: [ 185 | { key: 'y', name: 'Yes', value: true }, 186 | { key: 'n', name: 'No', value: false }, 187 | ], 188 | }, 189 | ]) 190 | .then(answers => answers.confirm) 191 | } 192 | 193 | module.exports = { 194 | confirm, 195 | error, 196 | errorPkg, 197 | header, 198 | headerPkg, 199 | info, 200 | infoPkg, 201 | input, 202 | multiSelect, 203 | select, 204 | success, 205 | successPkg, 206 | title, 207 | titlePkg, 208 | verbose, 209 | verbosePkg, 210 | warning, 211 | warningPkg, 212 | } 213 | -------------------------------------------------------------------------------- /packages/plugin-deploy-now/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Package, DeployPlugin } from '@lerna-cola/lib/build/types' 4 | 5 | const R = require('ramda') 6 | const pWhilst = require('p-whilst') 7 | const dedent = require('dedent') 8 | const chalk = require('chalk') 9 | const deepMerge = require('deepmerge') 10 | const tempWrite = require('temp-write') 11 | const fs = require('fs-extra') 12 | const { 13 | TerminalUtils, 14 | ChildProcessUtils, 15 | Errors: { PackageError }, 16 | } = require('@lerna-cola/lib') 17 | 18 | type NowSettings = { 19 | alias?: string, 20 | forwardNpm?: boolean, 21 | public?: boolean, 22 | } 23 | 24 | type Options = { 25 | disableRemovePrevious?: boolean, 26 | deployTimeoutMins?: number, 27 | passThroughEnvVars?: Array, 28 | settings?: NowSettings, 29 | } 30 | 31 | const nowDeployPlugin: DeployPlugin = { 32 | name: '@lerna-cola/plugin-now', 33 | build: (pkg: Package) => { 34 | throw new PackageError(pkg, '"build" not supported by "now" plugin') 35 | }, 36 | clean: (pkg: Package) => { 37 | throw new PackageError(pkg, '"clean" not supported by "now" plugin') 38 | }, 39 | develop: (pkg: Package) => { 40 | throw new PackageError(pkg, '"develop" not supported by "now" plugin') 41 | }, 42 | deploy: async (pkg: Package, options: Options) => { 43 | try { 44 | try { 45 | ChildProcessUtils.execSync('now', ['-v']) 46 | } catch (err) { 47 | TerminalUtils.errorPkg( 48 | pkg, 49 | 'You need to have the "now" CLI installed on your machine and available on your PATH in order to deploy to now.', 50 | ) 51 | throw err 52 | } 53 | 54 | const deploymentName = pkg.name 55 | 56 | const envVars = options.passThroughEnvVars 57 | ? options.passThroughEnvVars.reduce( 58 | (acc, cur) => 59 | process.env[cur] 60 | ? [...acc, '-e', `${cur}=${process.env[cur]}`] 61 | : acc, 62 | [], 63 | ) 64 | : [] 65 | 66 | const nowSettingsPath = tempWrite.sync() 67 | const nowSettings = deepMerge( 68 | // Defaults 69 | { 70 | forwardNpm: true, 71 | public: false, 72 | }, 73 | // User overrides 74 | options.settings || {}, 75 | ) 76 | fs.outputJsonSync(nowSettingsPath, nowSettings) 77 | TerminalUtils.verbosePkg(pkg, nowSettings) 78 | 79 | const args = [ 80 | 'deploy', 81 | '-n', 82 | deploymentName, 83 | ...envVars, 84 | '-A', 85 | nowSettingsPath, 86 | '-C', 87 | ] 88 | 89 | const deployResponse = await ChildProcessUtils.execPkg(pkg, 'now', args, { 90 | cwd: pkg.paths.packageRoot, 91 | }) 92 | const deploymentIdRegex = /(https:\/\/.+\.now\.sh)/g 93 | if (!deploymentIdRegex.test(deployResponse.stdout)) { 94 | throw new PackageError( 95 | pkg, 96 | 'No deployment id could be found, could not complete deployment', 97 | ) 98 | } 99 | const deploymentId = deployResponse.stdout.match(deploymentIdRegex)[0] 100 | TerminalUtils.infoPkg( 101 | pkg, 102 | `Waiting for deployment (${deploymentId}) to be ready...`, 103 | ) 104 | 105 | // Now we need to wait for the deployment to be ready. 106 | 107 | let ready = false 108 | 109 | setTimeout(() => { 110 | if (ready) { 111 | return 112 | } 113 | throw new PackageError( 114 | pkg, 115 | dedent(` 116 | The deployment process timed out. There may be an issue with your deployment or with "now". You could try a manually deployment using the following commands to gain more insight into the issue: 117 | 118 | ${chalk.blue(`cd ${pkg.paths.packageRoot}`)} 119 | ${chalk.blue(`now ${args.join(' ')}`)} 120 | `), 121 | ) 122 | }, (options.deployTimeoutMins || 15) * 60 * 1000) 123 | 124 | await pWhilst( 125 | () => !ready, 126 | async () => { 127 | // we will check the status for the deployment every 5 seconds 128 | await new Promise(resolve => setTimeout(resolve, 5 * 1000)) 129 | const status = ChildProcessUtils.execSync('now', ['ls', deploymentId]) 130 | if (/READY/.test(status)) { 131 | ready = true 132 | } else { 133 | TerminalUtils.infoPkg(pkg, '...') 134 | } 135 | }, 136 | ) 137 | 138 | const alias = R.path(['settings', 'alias'], options) 139 | 140 | if (alias != null) { 141 | TerminalUtils.infoPkg( 142 | pkg, 143 | `Setting up alias for new deployment to ${alias}....`, 144 | ) 145 | await ChildProcessUtils.execPkg(pkg, 'now', [ 146 | 'alias', 147 | 'set', 148 | deploymentId, 149 | alias, 150 | ]) 151 | 152 | // We need to do this at this point before attaching the rules as the rules 153 | // seem to indicate the deployment as not being aliased :-/ 154 | if (!options.disableRemovePrevious) { 155 | // Removes previous deployments 👍 156 | try { 157 | TerminalUtils.infoPkg( 158 | pkg, 159 | `Checking to see if there are any previous deployments to remove...`, 160 | ) 161 | await ChildProcessUtils.execPkg(pkg, 'now', [ 162 | 'rm', 163 | deploymentName, 164 | '--safe', 165 | '-y', 166 | ]) 167 | } catch (err) { 168 | TerminalUtils.infoPkg(pkg, 'No previous deployments to remove.') 169 | TerminalUtils.verbosePkg(pkg, err.stack) 170 | } 171 | } 172 | } 173 | 174 | TerminalUtils.successPkg(pkg, `Deployment successful`) 175 | } catch (err) { 176 | throw new PackageError(pkg, 'Failed to deploy', err) 177 | } 178 | }, 179 | } 180 | 181 | module.exports = nowDeployPlugin 182 | -------------------------------------------------------------------------------- /packages/plugin-build-babel/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | Package, 5 | CleanPlugin, 6 | BuildPlugin, 7 | } from '@lerna-cola/lib/build/types' 8 | 9 | const path = require('path') 10 | const babel = require('babel-core') 11 | const pify = require('pify') 12 | const pLimit = require('p-limit') 13 | const R = require('ramda') 14 | const fs = require('fs-extra') 15 | const globby = require('globby') 16 | const { FsUtils, TerminalUtils } = require('@lerna-cola/lib') 17 | 18 | // Having concurrent babel transpilations seems to break the sourcemap output. 19 | // Incorrect sources get mapped - I wonder if there is a shared global state 20 | // that references the "current" file being transpiled for reference in a 21 | // sourcemap. 22 | const maxConcurrentTranspiles = 1 23 | 24 | // :: (..args) => Promise 25 | const transformFile = pify(babel.transformFile) 26 | 27 | const ensureParentDirectoryExists = (filePath: string): void => { 28 | const dir = path.dirname(filePath) 29 | fs.ensureDirSync(dir) 30 | } 31 | 32 | type Options = { 33 | inputs?: Array, 34 | config?: string | Object, 35 | } 36 | 37 | type SanitisedOptions = { 38 | inputs: Array, 39 | config: Object, 40 | } 41 | 42 | const babelBuildPlugin: CleanPlugin & BuildPlugin = { 43 | name: '@lerna-cola/plugin-build-babel', 44 | build: async (pkg: Package, options: Options) => { 45 | try { 46 | const sanitiseOptions = (opts: Options): SanitisedOptions => { 47 | const { config, inputs } = opts 48 | 49 | let resolvedConfig 50 | 51 | const resolveConfig = (packageName: string): Object => { 52 | const module = FsUtils.resolvePackage(packageName) 53 | if (typeof module !== 'function' || typeof module !== 'object') { 54 | TerminalUtils.errorPkg( 55 | pkg, 56 | `The babel config "${packageName}" is an invalid package. Should export an object or a function.`, 57 | ) 58 | } 59 | // $FlowFixMe 60 | return typeof module === 'function' ? module(pkg, options) : module 61 | } 62 | 63 | if (config != null) { 64 | if (typeof config !== 'string' || typeof config === 'object') { 65 | TerminalUtils.errorPkg( 66 | pkg, 67 | 'A babel config package name or object must be provided as options', 68 | ) 69 | } 70 | 71 | resolvedConfig = 72 | typeof config === 'string' ? resolveConfig(config) : config 73 | } else { 74 | const packageBabelRc = path.resolve(pkg.paths.packageRoot, '.babelrc') 75 | const repoBabelRc = path.resolve(pkg.paths.monoRepoRoot, '.babelrc') 76 | const packageBabelRcJs = path.resolve( 77 | pkg.paths.packageRoot, 78 | '.babelrc.js', 79 | ) 80 | const repoBabelRcJs = path.resolve( 81 | pkg.paths.monoRepoRoot, 82 | '.babelrc.js', 83 | ) 84 | if (fs.existsSync(packageBabelRcJs)) { 85 | resolvedConfig = fs.readFileSync(packageBabelRc) 86 | } else if (fs.existsSync(repoBabelRcJs)) { 87 | resolvedConfig = fs.readFileSync(repoBabelRc) 88 | } else if (fs.existsSync(packageBabelRc)) { 89 | resolvedConfig = fs.readJsonSync(packageBabelRc) 90 | } else if (fs.existsSync(repoBabelRc)) { 91 | resolvedConfig = fs.readJsonSync(repoBabelRc) 92 | } else { 93 | TerminalUtils.errorPkg( 94 | pkg, 95 | 'No babel config supplied and no .babelrc found in package or root of monorepo.', 96 | ) 97 | } 98 | } 99 | 100 | if (!resolvedConfig) { 101 | TerminalUtils.errorPkg(pkg, 'Unexpected state') 102 | } 103 | 104 | return { 105 | // $FlowFixMe 106 | config: resolvedConfig, 107 | inputs: inputs || ['**/*.js', '**/*.jsx', '!__tests__', '!test.js'], 108 | } 109 | } 110 | 111 | const sanitisedOptions = sanitiseOptions(options) 112 | 113 | const patterns = sanitisedOptions.inputs.concat([ 114 | '!node_modules/**/*', 115 | `!${path.basename(pkg.paths.packageBuildOutput)}/**/*`, 116 | ]) 117 | 118 | // :: string -> Array 119 | const getJsFilePaths = () => 120 | globby(patterns, { 121 | cwd: pkg.paths.packageSrc, 122 | }) 123 | 124 | const transpileFile = filePath => { 125 | const writeTranspiledFile = result => { 126 | const outFile = path.resolve(pkg.paths.packageBuildOutput, filePath) 127 | ensureParentDirectoryExists(outFile) 128 | fs.writeFileSync(outFile, result.code, { encoding: 'utf8' }) 129 | fs.writeFileSync(`${outFile}.map`, JSON.stringify(result.map), { 130 | encoding: 'utf8', 131 | }) 132 | } 133 | const module = path.resolve(pkg.paths.packageSrc, filePath) 134 | return transformFile(module, sanitisedOptions.config).then( 135 | writeTranspiledFile, 136 | ) 137 | } 138 | 139 | const limit = pLimit(maxConcurrentTranspiles) 140 | const queueTranspile = filePath => limit(() => transpileFile(filePath)) 141 | const filePaths = await getJsFilePaths() 142 | return Promise.all(R.map(queueTranspile, filePaths)) 143 | } catch (err) { 144 | TerminalUtils.errorPkg(pkg, 'Unexpected error', err) 145 | } 146 | }, 147 | clean: (pkg: Package) => 148 | new Promise(resolve => { 149 | if (fs.pathExistsSync(pkg.paths.packageBuildOutput)) { 150 | fs.removeSync(pkg.paths.packageBuildOutput) 151 | } 152 | resolve() 153 | }), 154 | deploy: (pkg: Package) => { 155 | TerminalUtils.errorPkg(pkg, '"deploy" not supported by "babel" plugin') 156 | }, 157 | develop: (pkg: Package) => { 158 | TerminalUtils.errorPkg(pkg, '"develop" not supported by "babel" plugin') 159 | }, 160 | } 161 | 162 | module.exports = babelBuildPlugin 163 | -------------------------------------------------------------------------------- /packages/lib/src/config/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable global-require */ 4 | /* eslint-disable import/no-dynamic-require */ 5 | /* eslint-disable no-console */ 6 | /* eslint-disable no-param-reassign */ 7 | 8 | import type { 9 | Config, 10 | LernaColaConfig, 11 | LernaColaPackageConfig, 12 | Package, 13 | } from '../types' 14 | 15 | const path = require('path') 16 | const fs = require('fs-extra') 17 | const ObjectUtils = require('../objects') 18 | const getPackageRoots = require('./get-package-roots') 19 | const ColorUtils = require('../colors') 20 | const { 21 | getAllDependants, 22 | getAllDependencies, 23 | getDependencies, 24 | getDependants, 25 | getPlugin, 26 | orderByDependencies, 27 | } = require('./utils') 28 | 29 | let cache 30 | 31 | const defaultConfig = { 32 | commandHooks: { 33 | clean: { 34 | pre: () => Promise.resolve(), 35 | post: () => Promise.resolve(), 36 | }, 37 | build: { 38 | pre: () => Promise.resolve(), 39 | post: () => Promise.resolve(), 40 | }, 41 | develop: { 42 | pre: () => Promise.resolve(), 43 | post: () => Promise.resolve(), 44 | }, 45 | deploy: { 46 | pre: () => Promise.resolve(), 47 | post: () => Promise.resolve(), 48 | }, 49 | }, 50 | packageSources: undefined, 51 | packages: {}, 52 | } 53 | 54 | const defaultPackageConfig = { 55 | srcDir: 'src', 56 | entryFile: 'index.js', 57 | outputDir: 'build', 58 | disableSrcWatching: false, 59 | buildPlugin: undefined, 60 | developPlugin: undefined, 61 | deployPlugin: undefined, 62 | } 63 | 64 | const jsConfigPath = path.join(process.cwd(), './lerna-cola.js') 65 | const jsonConfigPath = path.join(process.cwd(), './lerna-cola.json') 66 | 67 | const config = () => { 68 | if (cache) { 69 | return cache 70 | } 71 | 72 | if (!fs.existsSync(jsonConfigPath) && !fs.existsSync(jsConfigPath)) { 73 | throw new Error( 74 | `No lerna-cola config was found. Please create either a lerna-cola.js or a lerna-cola.json configuration file`, 75 | ) 76 | } 77 | 78 | const lernaColaConfig: LernaColaConfig = ObjectUtils.mergeDeep( 79 | defaultConfig, 80 | fs.existsSync(jsonConfigPath) 81 | ? // $FlowFixMe 82 | require(jsonConfigPath) 83 | : // $FlowFixMe 84 | require(jsConfigPath), 85 | ) 86 | 87 | let packages: Array = getPackageRoots( 88 | lernaColaConfig.packageSources, 89 | ).map(packagePath => { 90 | const packageJsonPath = path.join(packagePath, './package.json') 91 | // $FlowFixMe 92 | const packageJson = require(packageJsonPath) 93 | const packageConfig: LernaColaPackageConfig = ObjectUtils.mergeDeep( 94 | defaultPackageConfig, 95 | lernaColaConfig.packages[packageJson.name] || {}, 96 | ) 97 | const plugins = { 98 | cleanPlugin: getPlugin( 99 | packageJson.name, 100 | packageConfig.cleanPlugin, 101 | 'cleanPlugin', 102 | ), 103 | buildPlugin: getPlugin( 104 | packageJson.name, 105 | packageConfig.buildPlugin, 106 | 'buildPlugin', 107 | ), 108 | developPlugin: getPlugin( 109 | packageJson.name, 110 | packageConfig.developPlugin, 111 | 'developPlugin', 112 | ), 113 | deployPlugin: getPlugin( 114 | packageJson.name, 115 | packageConfig.deployPlugin, 116 | 'deployPlugin', 117 | ), 118 | } 119 | 120 | // If we have a buildPlugin but no cleanPlugin then we will assign the 121 | // default cleanPlugin that will ensure the build output directory 122 | // gets cleaned between builds. 123 | if (!plugins.cleanPlugin && plugins.buildPlugin) { 124 | plugins.cleanPlugin = getPlugin( 125 | packageJson.name, 126 | 'plugin-clean-build', 127 | 'cleanPlugin', 128 | ) 129 | } 130 | 131 | // If we have a build plugin then we should be building our library 132 | // if it changes 133 | if (!plugins.developPlugin && plugins.buildPlugin) { 134 | plugins.developPlugin = getPlugin( 135 | packageJson.name, 136 | 'plugin-develop-build', 137 | 'developPlugin', 138 | ) 139 | } 140 | 141 | return { 142 | name: packageJson.name, 143 | color: ColorUtils.nextColorPair(), 144 | config: packageConfig, 145 | disableSrcWatching: packageConfig.disableSrcWatching, 146 | allDependants: [], 147 | allDependencies: [], 148 | dependants: [], 149 | dependencies: [], 150 | devDependencies: [], 151 | packageJson, 152 | paths: { 153 | monoRepoRoot: process.cwd(), 154 | monoRepoRootNodeModules: path.resolve(process.cwd(), './node_modules'), 155 | packageBuildOutput: path.resolve(packagePath, packageConfig.outputDir), 156 | packageSrc: path.resolve(packagePath, packageConfig.srcDir), 157 | packageEntryFile: path.resolve( 158 | packagePath, 159 | packageConfig.srcDir, 160 | packageConfig.entryFile, 161 | ), 162 | packageJson: packageJsonPath, 163 | packageLockJson: path.resolve(packagePath, './package-lock.json'), 164 | packageNodeModules: path.resolve(packagePath, './node_modules'), 165 | packageRoot: packagePath, 166 | packageWebpackCache: path.resolve(packagePath, './.webpackcache'), 167 | }, 168 | // $FlowFixMe 169 | plugins, 170 | version: packageJson.version || '0.0.0', 171 | } 172 | }) 173 | 174 | packages.forEach(pkg => { 175 | pkg.dependencies = getDependencies(pkg, packages, 'dependencies') 176 | pkg.devDependencies = getDependencies(pkg, packages, 'devDependencies') 177 | pkg.dependants = getDependants(pkg, packages) 178 | }) 179 | 180 | // Packages ordered based on their dependencies (via link or soft dep) 181 | // based order, which mean building them in order should be safe. 182 | packages = orderByDependencies(packages) 183 | 184 | packages.forEach(pkg => { 185 | // We get the full dependant list for this package traversing through 186 | // each dependant. This is so we can know which packages will all be 187 | // affect (either directly, or indirectly) when this package changes 188 | pkg.allDependants = getAllDependants(pkg, packages) 189 | pkg.allDependencies = getAllDependencies(pkg, packages) 190 | }) 191 | 192 | // Ensure there are no references to unknown packages 193 | Object.keys(lernaColaConfig.packages).forEach(packageName => { 194 | if (packages.find(x => x.name === packageName) == null) { 195 | throw new Error( 196 | `There is a lerna-cola configuration for "${packageName}", however, this package could not be resolved via the packageSources configuration.`, 197 | ) 198 | } 199 | }) 200 | 201 | const result: Config = { 202 | // $FlowFixMe 203 | commandHooks: lernaColaConfig.commandHooks, 204 | packages, 205 | packageMap: packages.reduce( 206 | (acc, pkg) => Object.assign(acc, { [pkg.name]: pkg }), 207 | {}, 208 | ), 209 | terminalLabelMinLength: Math.max( 210 | ...['lerna-cola'.length, ...packages.map(x => x.name.length)], 211 | ), 212 | } 213 | 214 | cache = result 215 | 216 | return result 217 | } 218 | 219 | module.exports = config 220 | -------------------------------------------------------------------------------- /packages/cli/src/development-service/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-use-before-define */ 4 | 5 | import type { 6 | RunType, 7 | Package, 8 | PackageConductor, 9 | PackageWatcher, 10 | } from '@lerna-cola/lib/build/types' 11 | 12 | const { config, TerminalUtils, PackageUtils } = require('@lerna-cola/lib') 13 | const createPackageConductor = require('./create-package-conductor') 14 | const createPackageWatcher = require('./create-package-watcher') 15 | const gracefulShutdownManager = require('./graceful-shutdown-manager') 16 | 17 | type QueueItem = { 18 | package: Package, 19 | runType: RunType, 20 | changedDependency?: Package, 21 | } 22 | 23 | type Options = {| 24 | packagesFilter?: Array, 25 | selectPackages?: boolean, 26 | exact?: boolean, 27 | |} 28 | 29 | module.exports = async function developmentService({ 30 | packagesFilter, 31 | selectPackages, 32 | exact, 33 | }: Options) { 34 | // Keep this message up here so it always comes before any others 35 | TerminalUtils.info('Press CTRL + C to exit') 36 | 37 | const preDevelopHook = config().commandHooks.develop.pre 38 | 39 | TerminalUtils.info('Running the pre develop hook') 40 | await preDevelopHook() 41 | 42 | let packages = PackageUtils.resolvePackages(packagesFilter, { 43 | strict: exact, 44 | }) 45 | 46 | if (selectPackages) { 47 | // Ask which packages to develop if the select option was enabled 48 | const selectedPackages = await TerminalUtils.multiSelect( 49 | 'Which packages would you like to develop?', 50 | { 51 | choices: packages.map(x => ({ 52 | value: x.name, 53 | text: `${x.name} (${x.version})`, 54 | })), 55 | }, 56 | ) 57 | packages = PackageUtils.resolvePackages(selectedPackages, { strict: exact }) 58 | } 59 | 60 | const packageMap = packages.reduce((acc, cur) => 61 | Object.assign( 62 | acc, 63 | { 64 | [cur.name]: cur, 65 | }, 66 | {}, 67 | ), 68 | ) 69 | 70 | TerminalUtils.verbose(`Developing packages:`) 71 | TerminalUtils.verbose(packages.map(x => x.name)) 72 | 73 | // Firstly clean build for all packages 74 | await PackageUtils.cleanPackages(packages) 75 | 76 | // Represents the current package being built 77 | let currentlyProcessing = null 78 | 79 | // Represents the build backlog queue. FIFO. 80 | // We will queue all the packages for the first run 81 | let toProcessQueue: Array = packages.map(pkg => ({ 82 | package: pkg, 83 | runType: 'FIRST_RUN', 84 | })) 85 | 86 | const packageHasDependant = (dependant: Package, pkg: Package): boolean => 87 | pkg.dependants.includes(dependant.name) 88 | 89 | const getPackageDependants = (pkg: Package): Array => 90 | pkg.dependants.map(name => packageMap[name]).filter(x => x != null) 91 | 92 | const onChange = (pkg: Package) => (): void => { 93 | queuePackageForProcessing(pkg, 'SELF_CHANGED') 94 | // If no active build running then we will call off to run next item in 95 | // the queue. 96 | if (!currentlyProcessing) { 97 | processNextInTheQueue() 98 | } 99 | } 100 | 101 | /** 102 | * Package watches are responsible for watching the source of the packages 103 | * and then notifying of changes so that packages can be queued to be 104 | * processed. 105 | */ 106 | const packageWatchers: { 107 | [key: string]: PackageWatcher, 108 | } = packages.reduce( 109 | (acc, pkg) => 110 | Object.assign(acc, { 111 | [pkg.name]: createPackageWatcher(onChange(pkg), pkg), 112 | }), 113 | {}, 114 | ) 115 | 116 | /** 117 | * Package develop conductors are responsible for calling the respective 118 | * develop plugins for packages when changes occur. They manage any 119 | * child processes that are spawned, ensuring there is only a single 120 | * child process per package. 121 | */ 122 | const packageDevelopConductors: { 123 | [key: string]: PackageConductor, 124 | } = packages.reduce( 125 | (acc, pkg) => 126 | Object.assign(acc, { 127 | [pkg.name]: createPackageConductor(pkg, packageWatchers[pkg.name]), 128 | }), 129 | {}, 130 | ) 131 | 132 | /** 133 | * Queues an item of processing. This will be sensitive to items already 134 | * in the queue. 135 | * 136 | * @param {*} packageToQueue 137 | * The package to queue 138 | * @param {*} runType 139 | * The change that cause the queueing of the item. 140 | * @param {*} changedDependency 141 | * If it was caused by a dependency changing then this is the dependency 142 | * that changed. 143 | */ 144 | const queuePackageForProcessing = ( 145 | packageToQueue: Package, 146 | runType: RunType, 147 | changedDependency?: Package, 148 | ): void => { 149 | TerminalUtils.verbose(`Attempting to queue ${packageToQueue.name}`) 150 | if ( 151 | currentlyProcessing !== null && 152 | packageHasDependant(packageToQueue, currentlyProcessing) 153 | ) { 154 | // Do nothing as the package currently being built will result in this 155 | // package being built via it's dependancy chain. 156 | TerminalUtils.verbosePkg( 157 | packageToQueue, 158 | `Skipping development service queue as represented by a queued package`, 159 | ) 160 | } else if ( 161 | toProcessQueue.some(queueItem => 162 | packageHasDependant(packageToQueue, queueItem.package), 163 | ) 164 | ) { 165 | // Do nothing as one of the queued packagesToDevelop will result in this package 166 | // getting built via it's dependancy chain. 167 | TerminalUtils.verbosePkg( 168 | packageToQueue, 169 | `Skipping development service queue as represented by a queued package`, 170 | ) 171 | } else { 172 | // Queue the package for building. 173 | TerminalUtils.verbose(`Queuing ${packageToQueue.name}`) 174 | const packageDependants = getPackageDependants(packageToQueue) 175 | // We'll assign the package to the build queue, removing any of the 176 | // package's dependants as they will be represented by the package being 177 | // added. 178 | toProcessQueue = toProcessQueue 179 | .filter( 180 | queueItem => 181 | !packageDependants.find(pkg => pkg.name === queueItem.package.name), 182 | ) 183 | .concat([{ package: packageToQueue, runType, changedDependency }]) 184 | TerminalUtils.verbose( 185 | `Queue: [${toProcessQueue.map(x => x.package.name).join(',')}]`, 186 | ) 187 | } 188 | } 189 | 190 | /** 191 | * Processes a package in the queue, and if successfully processed it will 192 | * queue the packages dependencies. 193 | */ 194 | const processQueueItem = (queueItem: QueueItem): void => { 195 | currentlyProcessing = queueItem.package 196 | const packageDevelopConductor = 197 | packageDevelopConductors[queueItem.package.name] 198 | if (!packageDevelopConductor) { 199 | TerminalUtils.error( 200 | `Did not run develop process for ${ 201 | queueItem.package.name 202 | } as there is no package develop conductor registered for it`, 203 | ) 204 | return 205 | } 206 | packageDevelopConductor 207 | // Kick off the develop of the package 208 | .run(queueItem.runType, queueItem.changedDependency) 209 | // Develop kickstart succeeded 🎉 210 | .then(() => ({ success: true })) 211 | // Or, failed 😭 212 | .catch(err => { 213 | TerminalUtils.errorPkg( 214 | queueItem.package, 215 | `An error occurred whilst trying to start development instance.`, 216 | err, 217 | ) 218 | return { success: false } 219 | }) 220 | // Finally... 221 | .then(({ success }) => { 222 | // Ensure any current is removed 223 | currentlyProcessing = null 224 | 225 | // If the build succeeded we will queue dependants 226 | if (success) { 227 | TerminalUtils.verbosePkg( 228 | queueItem.package, 229 | `Develop process ran successfully, queueing dependants...`, 230 | ) 231 | const packageDependants = getPackageDependants( 232 | queueItem.package, 233 | ).filter( 234 | dependant => 235 | !toProcessQueue.some( 236 | queued => queued.package.name === dependant.name, 237 | ), 238 | ) 239 | packageDependants.forEach(dep => 240 | queuePackageForProcessing( 241 | dep, 242 | 'DEPENDENCY_CHANGED', 243 | queueItem.package, 244 | ), 245 | ) 246 | } 247 | 248 | // We will call off the next item to be processe even if a failure 249 | // occurred. This is because any items in the queue likely are not 250 | // dependants of this failed package (due to the logic contained within 251 | // the queueing function). 252 | processNextInTheQueue() 253 | }) 254 | } 255 | 256 | const processNextInTheQueue = (): void => { 257 | if (currentlyProcessing) { 258 | TerminalUtils.error( 259 | `Tried to process the next Package in the queue even though there is a Package being processed: ${ 260 | currentlyProcessing.name 261 | }`, 262 | ) 263 | return 264 | } 265 | TerminalUtils.verbose('Popping the queue') 266 | if (toProcessQueue.length > 0) { 267 | // Pop the queue. 268 | const nextToProcess = toProcessQueue[0] 269 | toProcessQueue = toProcessQueue.slice(1) 270 | TerminalUtils.verbose(`Popped ${nextToProcess.package.name}`) 271 | processQueueItem(nextToProcess) 272 | } else { 273 | TerminalUtils.verbose('Nothing to pop') 274 | } 275 | } 276 | 277 | // READY, SET... 278 | Object.keys(packageWatchers).forEach(packageName => 279 | packageWatchers[packageName].start(), 280 | ) 281 | 282 | // GO! 🚀 283 | processNextInTheQueue() 284 | 285 | // Ensure graceful shutting down 286 | gracefulShutdownManager(packageDevelopConductors, packageWatchers) 287 | 288 | return new Promise(() => { 289 | // Never resolve as we are going to wait to CTRL + C to exit. 290 | }) 291 | } 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > **Archived / Deprecated** 4 | > 5 | > I no longer need to use this project as the awesome team Zeit give me everything I need with their `now dev` tool and cloud platform. So happy to sunset this project as maintaining this type of project is super hard work. 😀 6 | 7 | --- 8 | 9 | # Lerna Cola 🥤 10 | 11 | Superpowers for your [Lerna](https://lernajs.io/) monorepos. 12 | 13 | Clean, build, develop, and deploy your packages utilising a rich plugin ecosystem. 14 | 15 | ## TOC 16 | 17 | - [Introduction](#introduction) 18 | - [Requirements](#requirements) 19 | - [(Not Really) Requirements](#not-really-requirements) 20 | - [Getting Started](#getting-started) 21 | - [Sample Application](#sample-application) 22 | - [Video Walkthrough](#video-walkthrough) 23 | - [Configuration](#configuration) 24 | - [Example Configuration](#example-configuration) 25 | - [Configuration Schema](#configuration-schema) 26 | - [CLI Commands](#cli-commands) 27 | - [clean](#clean) 28 | - [build](#build) 29 | - [develop](#develop) 30 | - [deploy](#deploy) 31 | - [Plugins](#plugins) 32 | - [Core Plugins](#core-plugins) 33 | - [`plugin-clean-build`](#plugin-clean-build) 34 | - [`plugin-develop-server`](#plugin-develop-server) 35 | - [`plugin-script`](#plugin-script) 36 | - [Official Plugins](#official-plugins) 37 | - [`@lerna-cola/plugin-build-babel`](#lerna-cola-plugin-build-babel) 38 | - [`@lerna-cola/plugin-build-flow`](#lerna-cola-plugin-build-flow) 39 | - [`@lerna-cola/plugin-deploy-now`](#lerna-cola-plugin-deploy-now) 40 | - [3rd Party Plugins](#3rd-party-plugins) 41 | - [Plugin Development](#plugin-development) 42 | - [Clean Plugin](#clean-plugin) 43 | - [Build Plugin](#build-plugin) 44 | - [Develop Plugin](#develop-plugin) 45 | - [Deploy Plugin](#deploy-plugin) 46 | - [Schemas](#schemas) 47 | - [`Package` Schema](#package-schema) 48 | - [`Config` Schema](#package-schema) 49 | 50 | ## Introduction 51 | 52 | [Lerna](https://lernajs.io/) makes it crazy easy to manage cross package dependencies and provides sane methods to version them. It takes away the fear of creating and maintaining a wide set of packages, allowing us to fully embrace the module ethos by creating packages with isolated responsibilities. 53 | 54 | Lerna Cola wants to build on top of these core principles by providing the following additional features: 55 | 56 | - Easily **enrich your packages** with a **compilation/transpilation/bundling** step (babel/flow/typescript/reasonml/webpack/parcel/etc/etc/etc). 57 | - Take away the fear of building a wide set of **microservices/lambda packages** by providing a **rich development service** that handles **hot/auto reloading** of your packages. This allows for a **fluid development experience** reminiscent of old school single package/repository based development. 58 | - **Deploy** your packages with a simple command to a **cloud provider** of your choice. 59 | 60 | You access the features via one of the 4 CLI commands that Lerna Cola provides: [`clean`](#clean), [`build`](#build), [`develop`](#develop), and [`deploy`](#deploy). 61 | 62 | The commands utilise a rich plugin eco-system, allowing for 3rd party contributions. 63 | 64 | Lift your build, development and deployment to the root of your monorepo, keep your packages clean, and utilise the full benefits of a monorepo structure. 65 | 66 | ## Requirements 67 | 68 | - **Node >= 8** 69 | 70 | Version 8 was LTS at the time of writing this so a decision was made to run with it. 71 | 72 | ## (Not Really) Requirements 73 | 74 | - **[Lerna](https://lernajs.io/)** 75 | 76 | To tell you the truth, we don't strictly require that you use Lerna. You could very well use straight up Yarn workspaces or any other monorepo enabling tool, but in our opinion you would be missing out on some very cool features that Lerna provides. 77 | 78 | ## Getting started 79 | 80 | **First, you need to install the cli** 81 | 82 | ```bash 83 | yarn add @lerna-cola/cli -DW 84 | ``` 85 | 86 | _or, via NPM_: 87 | 88 | ```bash 89 | npm i @lerna-cola/cli -D 90 | ``` 91 | 92 | **Secondly, you'll need a `lerna-cola.js` [configuration](#configuration) file** 93 | 94 | You'll need to reference the [Configuration](#configuration) on how to do this. 95 | 96 | **Lastly, fire up the lern-cola CLI** 97 | 98 | You can run the help to see the help commands 99 | 100 | ```bash 101 | yarn lerna-cola help 102 | ``` 103 | 104 | _or, via NPX_ 105 | 106 | ``` 107 | npx lerna-cola help 108 | ``` 109 | 110 | ## Sample Application 111 | 112 | We definitely recommend that you read the all of the documentation first in order to gain a better understanding of Lerna Cola, however, we want to highlight early on that a sample application is maintained here: 113 | 114 | - https://github.com/ctrlplusb/lerna-cola-sample 115 | 116 | This provides a great way for you to quickly clone and run a non-trivial project in order to assess the benefits that Lerna Cola could bring to your monorepos. 117 | 118 | ## Video Walkthrough 119 | 120 | > Coming soon 121 | 122 | ## Configuration 123 | 124 | To use Lerna Cola you will need to create a configuration file named either `lerna-cola.json` or `lerna-cola.js` within the root of your monorepo. 125 | 126 | > Note: When creating a `.js` file ensure that you export the configuration object via `module.exports`. 127 | 128 | ### Example Configuration 129 | 130 | Before we describe the configuration schema, it may be helpful to review a "typical" configuration: 131 | 132 | ```javascript 133 | module.exports = { 134 | packages: { 135 | // The following two packages are microservices where we are using the babel 136 | // plugin to transpile them to support our current node version, and the 137 | // server develop plugin which will take care of executing and restarting 138 | // our microservices for any changes up their dependency trees. 139 | 140 | 'microservice-one': { 141 | buildPlugin: '@lerna-cola/plugin-build-babel', 142 | developPlugin: 'plugin-develop-server', 143 | }, 144 | 145 | 'microservice-two': { 146 | buildPlugin: '@lerna-cola/plugin-build-babel', 147 | developPlugin: 'plugin-develop-server', 148 | }, 149 | 150 | // The following package is a "Create React App" package which comes with 151 | // it's own build and develop (start) scripts. We will therefore use the 152 | // Lerna Cola script plugin to delegate to the CRA scripts. 153 | 154 | 'my-ui': { 155 | buildPlugin: { 156 | name: 'plugin-script', 157 | options: { 158 | scriptName: 'build', 159 | }, 160 | }, 161 | developPlugin: { 162 | name: 'plugin-script', 163 | options: { 164 | scriptName: 'start', 165 | }, 166 | }, 167 | }, 168 | }, 169 | } 170 | ``` 171 | 172 | Within this example we are providing a Lerna Cola configuration for 3 of our packages within our monorepo. We need not provide a configuration for every package within our monorepo; only the ones that we wish to execute Lerna Cola plugins against. Lerna Cola will still be aware of the other packages within our repo, for example providing hot reloading on our servers/apps when a change occurs on our shared utils package. 173 | 174 | In our example we have two microservices that will be Babel transpiled by the [`build`](#build) command, and will be treated as server process (with execution and hot reloading) by the [`develop`](#develop) command. The third package configuration utilities a special [Create React App](https://TODO) plugin that contains configuration for both the [`build`](#build) and [`develop`](#develop) commands. 175 | 176 | This is a very basic example, however, it is illustrative of how quickly you can provide a centralised and coordinated configuration for your monorepo packages. Plugins can of course be customised via options. We recommend you read the [plugin](#plugins) docs for detailed information on them. 177 | 178 | ### Configuration Schema 179 | 180 | The configuration is an Object/JSON structure that supports the following schema: 181 | 182 | - `commandHooks` (_Object_, **optional**) 183 | 184 | A set of hooks to allow you to execute a function pre/post each command. You can specify a set of hooks for each of the commands: 185 | 186 | - clean 187 | - build 188 | - develop 189 | - deploy 190 | 191 | Each of them support the following configuration _Object_: 192 | 193 | - `pre`: (_Function_, **optional**) 194 | 195 | Runs prior to the specifed command. Can return a `Promise`, which will be waited on to resolve before proceeding. 196 | 197 | - `post`: (_Function_, **optional**) 198 | 199 | Runs after to the specifed command completes/fails. Can return a `Promise`, which will be waited on to resolve before proceeding. 200 | 201 | Example: 202 | 203 | ```javascript 204 | // lerna-cola.js 205 | module.exports = { 206 | commandHooks: { 207 | build: { 208 | pre: () => Promise.resolve(), 209 | post: () => Promise.resolve(), 210 | }, 211 | }, 212 | } 213 | ``` 214 | 215 | - `packages` (_Object_, **_optional_**) 216 | 217 | An object where each key is the name of a package within your repository (matching the name of the package within the package's `package.json` file). The value against each key is considered a package configuration object, supporting the following schema: 218 | 219 | - `buildPlugin` (_string_ || _Object_, **optional**) 220 | 221 | The plugin to use when building the package. Please see the [Plugins](#plugins) documentation for more information. 222 | 223 | - `developPlugin` (_string_ || _Object_, **optional**) 224 | 225 | The plugin to use when developing the package. Please see the [Plugins](#plugins) documentation for more information. 226 | 227 | - `deployPlugin` (_string_ || _Object_, **optional**) 228 | 229 | The plugin to use when deploying the package. Please see the [Plugins](#plugins) documentation for more information. 230 | 231 | - `srcDir` (_string_, **optional**, **default**: 'src') 232 | 233 | The directory within your package containing the source. 234 | 235 | - `entryFile` (_string_, **optional**, **default**: 'index.js') 236 | 237 | The file within your `srcDir` that should be considered the "entry" file for your package. 238 | 239 | - `outputDir` (_string_, **optional**, **default**: 'build') 240 | 241 | The directory which should be targetted for build output by the build plugins. Also used by the clean plugin to know which directory should be removed. 242 | 243 | - `disableSrcWatching` (_boolean_, **optional**, **default**: false) 244 | 245 | Use this to disable watching of the source when running the develop command. This can be useful if you don't care about tracking changes for a specific package within your repository, or you have a seperate process that will already track source changes for the respective package (e.g. the `start` script within a Create React App project). 246 | 247 | - `packageSources` (_Array<string>_, **_optional_**, **default**: _see below_) 248 | 249 | An array of globs, paths where your packages are contained. By default it uses the same configuration as Lerna, i.e.: 250 | 251 | ```json 252 | "packageSources": [ 253 | "packages/*" 254 | ] 255 | ``` 256 | 257 | ## CLI Commands 258 | 259 | Below is an brief description of each command that the CLI provides. You can request for help via the CLI for all of the commands like so: 260 | 261 | ```bash 262 | lerna-cola help 263 | ``` 264 | 265 | Or for an individual command like so: 266 | 267 | ```bash 268 | lerna-cola build help 269 | ``` 270 | 271 | When executing commands all of the environment variables that are currently available (i.e. `process.env`) will be passed into the respective processes that are spawned for each package. 272 | 273 | ### clean 274 | 275 | Clean the output from the the [`build`](#build) command. 276 | 277 | By default the [`plugin-clean-build`](#plugin-clean-build) plugin is used, however, you can customise this within your [configuration](#configuration). 278 | 279 | ### build 280 | 281 | Take advantage of one of the wide array of plugins to babel/flow/typescript/reasonml/etc/etc/etc transpile/build your packages with the ability to easily share and manage configuration at the root of your monorepo. 282 | 283 | When executed it will run all of the configured plugins for all of your packages that have a `buildPlugin` configured within the `lerna-cola.json` [configuration](#configuration) file. The package build order is based upon on a topological sort against the dependency tree of your packages within the monorepo (i.e. they build in the correct order). 284 | 285 | ### develop 286 | 287 | Run a full auto-reloading development environment in which any changes to one package cascade through your package dependency tree. No more need to manually rebuild/restart servers during development allow for a blazingly hot development experience. 288 | 289 | All of your packages will be watched (even the ones without any explicit configuration within your [configuration](#configuration)), with any changes being propagated through the dependency tree subsequently executing any "develop" plugins that are configured against your projects. 290 | 291 | > Note: If you provide a "build" plugin against on your packages, but no explicit "develop" plugin, then the system will automatically execute the "build" plugin when handling changes. 292 | 293 | All of the logs/output from your packages will be "merged" and printed within the console window where you ran the `develop` command. The console output contains a uniquely colored column to the left allowing you to easily identify the output for each respective package. 294 | 295 | ### deploy 296 | 297 | Deploy your apps with a simple one line command to a cloud provider supported by the plugin system. 298 | 299 | When executing this command your packages will be built in topological sort order (based on their dependency tree between each other) and then will subsequently be deployed via their configured plugins. 300 | 301 | ## Plugins 302 | 303 | Lerna Cola is powered by a powerful plugin system that allows the build, develop, and deploy commands to support a multitude of targets. It is very easy to build your own plugin should you desire to so - please see the ["Plugin Development"](#plugin-development) section for more information. 304 | 305 | Plugins are split by "core" plugins, which are bundled with the main `@lerna-cola/cli` package, and "package" plugins which could either be official Lerna Cola packages, 3rd party packages, or private packages of your own making. 306 | 307 | You define plugins against each of your package configurations within your Lerna Cola [configuration](#configuration). 308 | 309 | Plugins allow two forms of assignment. 310 | 311 | **Form 1 - specify the plugin by name** 312 | 313 | ```javascript 314 | // lerna-cola.js 315 | module.exports = { 316 | packages: { 317 | 'my-package': { 318 | buildPlugin: '@lerna-cola/plugin-build-babel', 319 | }, 320 | }, 321 | } 322 | ``` 323 | 324 | **Form 2 - provide options to the plugin** 325 | 326 | ```javascript 327 | // lerna-cola.js 328 | module.exports = { 329 | packages: { 330 | 'my-package': { 331 | buildPlugin: { 332 | name: '@lerna-cola/plugin-build-babel', 333 | options: { 334 | config: { 335 | presets: ['babel-preset-env'], 336 | }, 337 | }, 338 | }, 339 | }, 340 | }, 341 | } 342 | ``` 343 | 344 | You can pass down any arbitrary set of options, which will be made available to the respective plugin. Please refer to the documentation for each plugin in terms of what options it supports. 345 | 346 | ### Core Plugins 347 | 348 | Below are the "core" plugins which will be immediately available when you add Lerna Cola to your repository. 349 | 350 | #### `plugin-clean-build` 351 | 352 | A [clean](#clean) command plugin. 353 | 354 | This plugin will be automatically assigned to any package with a `buildPlugin` defined. 355 | 356 | When executed it will remove the output directory targetted by your respective build plugin. 357 | 358 | #### `plugin-develop-server` 359 | 360 | A [develop](#develop) command plugin. 361 | 362 | #### `plugin-script` 363 | 364 | This is a special plugin that can be configured against any of your plugins for a package. i.e. `cleanPlugin`, `buildPlugin`, `developPlugin`, and `deployPlugin`. 365 | 366 | It allows you to delegate to a script defined within the `package.json` of your targetted package. 367 | 368 | This can be especially useful for packages that come with their own sets of scripts (e.g. A Create React App package). 369 | 370 | **Options** 371 | 372 | This plugin supports the following options: 373 | 374 | - `scriptName` (_string_, **required**) 375 | 376 | The name of the script to run within your target package. 377 | 378 | - `runForEveryChange` (_boolean_, **default**: false) 379 | 380 | Only used when assigned against the `developPlugin`. Setting this value to `true` will ensure that the your script will be executed any time a change is registered within your packages' source or against one of its dependencies. If your script spawns a long run child process (for example a Create React App server), then an existing running instance will be destroyed before the script is ran again. 381 | 382 | **Example** 383 | 384 | ```javascript 385 | // lerna-cola.js 386 | module.exports = { 387 | packages: { 388 | 'my-create-react-app': { 389 | buildPlugin: { 390 | name: 'script', 391 | options: { 392 | scriptName: 'build', 393 | }, 394 | }, 395 | }, 396 | }, 397 | } 398 | ``` 399 | 400 | ### Official Plugins 401 | 402 | #### `@lerna-cola/plugin-build-babel` 403 | 404 | A [build](#build) command plugin. 405 | 406 | This plugin will transpile your package's `srcDir` (see the [configuration](#configuration)) using [Babel](https://babeljs.io), outputing the results into your packages `outputDir` (see the [configuration](#configuration)). 407 | 408 | By default it will use a `.babelrc` found within your packages root directory, and if one is not found then it will look for a `.babelrc` within the root of your monorepo. You can alternatively provide a babel configuration via the plugin's options. 409 | 410 | **Options** 411 | 412 | This plugin supports the following options: 413 | 414 | - `config` (_string_ || _Object_, **optional**) 415 | 416 | The [babel configuration](https://babeljs.io/docs/en/api.md) to use for the transpilation. 417 | 418 | This can be one of two things: 419 | 420 | - The name of a package where the main export is a babel configuration object. 421 | - An object containing the babel configuration. 422 | 423 | > Note: Lerna Cola ships a simple babel config package which you could use. It is called `@lerna-cola/babel-config` 424 | 425 | We highly recommend that you enable sourcemaps output within your configuration to aid debugging. 426 | 427 | **Example** 428 | 429 | Specifying an inline config: 430 | 431 | ```javascript 432 | // lerna-cola.js 433 | module.exports = { 434 | packages: { 435 | 'my-lib': { 436 | buildPlugin: { 437 | name: '@lerna-cola/plugin-build-babel', 438 | options: { 439 | config: { 440 | presets: ['babel-preset-env'], 441 | }, 442 | }, 443 | }, 444 | }, 445 | }, 446 | } 447 | ``` 448 | 449 | Specifying a package containing the configuration: 450 | 451 | ```javascript 452 | // lerna-cola.js 453 | module.exports = { 454 | packages: { 455 | 'my-lib': { 456 | buildPlugin: { 457 | name: '@lerna-cola/plugin-build-babel', 458 | options: { 459 | config: 'my-babel-config-package', 460 | }, 461 | }, 462 | }, 463 | }, 464 | } 465 | ``` 466 | 467 | #### `@lerna-cola/plugin-build-flow` 468 | 469 | A [build](#build) command plugin. 470 | 471 | This plugin will transpile your package's `srcDir` (see the [configuration](#configuration)), stripping your source of any flow annotations, and outputing the result (along with \*.flow) files into your package's `outputDir` (see the [configuration](#configuration)). 472 | 473 | **Options** 474 | 475 | This plugin supports the following options: 476 | 477 | - `inputs` (_Array<string>_ , **optional**, **default**: _see below_) 478 | 479 | An array of glob strings which should be used to match the files that will be processed. 480 | 481 | Defaults to the following: 482 | 483 | ```json 484 | ["**/*.js", "!__tests__", "!test.js"] 485 | ``` 486 | 487 | **Example** 488 | 489 | ```javascript 490 | // lerna-cola.js 491 | module.exports = { 492 | packages: { 493 | 'my-lib': { 494 | buildPlugin: '@lerna-cola/plugin-build-flow', 495 | }, 496 | }, 497 | } 498 | ``` 499 | 500 | #### `@lerna-cola/plugin-deploy-now` 501 | 502 | A [deploy](#deploy) command plugin. 503 | 504 | This plugin allows your package to be deployed to Zeit's [now](https://zeit.co/now) cloud service. 505 | 506 | For this plugin to work you need to have installed the `now` CLI, i.e.: 507 | 508 | ```bash 509 | npm i -g now 510 | ``` 511 | 512 | You also need to ensure that you have logged into your `now` account: 513 | 514 | ```bash 515 | now login 516 | ``` 517 | 518 | **Options** 519 | 520 | This plugin supports the following options: 521 | 522 | - `settings` (_Object_, **optional**) 523 | 524 | Any [settings](https://zeit.co/docs/features/configuration#settings) officially supported by now. 525 | 526 | We highly recommend you set the `alias` setting. 527 | 528 | Some defaults are automatically applied by the plugin, but you can override them: 529 | 530 | ```javascript 531 | { 532 | forwardNpm: true, 533 | public: false 534 | } 535 | ``` 536 | 537 | - `disableRemovePrevious` (_boolean_, **optional**, **default**: false) 538 | 539 | If you provide an `alias` within the `settings` option then this plugin will by default remove any previous deployments that were deployed against the target alias. This avoids any unnecessary build up of old deployments, which can become tedious to manage. 540 | 541 | Set this to `true` to prevent the removal of prior deployments of an alias target. 542 | 543 | - `deployTimeoutMins` (_number_, **optional**, **default**: 15) 544 | 545 | The number of minutes to wait before timing out the deployment, subsequently stopping the deployment process with a failure indicated. 546 | 547 | - `passThroughEnvVars` (_Array<string>_ , **optional**) 548 | 549 | An array of strings, describing which environment variables (currently available on your `process.env`) should be passed into the now deployment. 550 | 551 | e.g. 552 | 553 | ```javascript 554 | // lerna-cola.js 555 | module.exports = { 556 | packages: { 557 | 'my-app': { 558 | deployPlugin: { 559 | name: '@lerna-cola/plugin-deploy-now', 560 | options: { 561 | alias: 'my-app.com', 562 | passThroughEnvVars: ['MY_DB_USERNAME', 'MY_DB_PASSWORD'], 563 | }, 564 | }, 565 | }, 566 | }, 567 | } 568 | ``` 569 | 570 | **Example** 571 | 572 | ```javascript 573 | // lerna-cola.js 574 | module.exports = { 575 | packages: { 576 | 'my-app': { 577 | buildPlugin: { 578 | name: '@lerna-cola/plugin-deploy-now', 579 | options: { 580 | settings: { 581 | alias: 'my-app.com', 582 | sfo1: { 583 | sfo1: { 584 | min: 1, 585 | max: 5, 586 | }, 587 | }, 588 | }, 589 | }, 590 | }, 591 | }, 592 | }, 593 | } 594 | ``` 595 | 596 | #### `@lerna-cola/plugin-build-typescript` 597 | 598 | > Coming soon 599 | 600 | #### `@lerna-cola/plugin-build-reasonml` 601 | 602 | > Coming soon 603 | 604 | #### `@lerna-cola/plugin-build-parcel` 605 | 606 | > Coming soon 607 | 608 | #### `@lerna-cola/plugin-build-rollup` 609 | 610 | > Coming soon 611 | 612 | ### 3rd Party Plugins 613 | 614 | Hopefully we will have some soon. 🤞 615 | 616 | Please submit a PR to add yours here. 617 | 618 | ## Plugin Development 619 | 620 | Developing plugins is very easy. You can create a plugin to support one or more of the 4 commands that Lerna Cola provides. 621 | 622 | To create a plugin simply create new package where the "main" on its package.json file points to the plugin module. 623 | 624 | Your newly created package can adopt the behaviour of any of the plugin types as long as provides implementation for the interface specified for each plugin type. For example below is a template for a plugin package that supports all the command types (clean/build/develop/deploy): 625 | 626 | ```javascript 627 | // my-plugin.js 628 | module.exports = { 629 | name: 'my-plugin', 630 | clean: (pkg, options, args) => Promise.resolve('todo'), 631 | build: (pkg, options, args) => Promise.resolve('todo'), 632 | develop: (pkg, options, args) => Promise.resolve('todo'), 633 | deploy: (pkg, options, args) => Promise.resolve('todo'), 634 | } 635 | ``` 636 | 637 | Looking at the above, you can see that a plugin is really just a simple object, containing functions that map to the command names. Each of the functions will receive a set of useful arguments, which will be described below, and need to return a `Promise` in order to indicate when they have completed. 638 | 639 | When running a command, the associated plugins will get executed for each package that they are configured against. The plugin functions will receive as an argument an object describing the package that it is running against, as well as the specific options that were defined when assigning the plugin to the package within the Lerna Cola [configuration](#configuration) file. 640 | 641 | Lets review the arguments that are provided to each of the plugin functions: 642 | 643 | - `pkg` (_Package_) 644 | 645 | The package for which the plugin command is getting executed against. This contains lots of really useful information about the package, such as it's dependency tree, src and build output paths etc. Please see the [Package Schema](#package-schema) for a full breakdown of what is available. 646 | 647 | - `options` (_Object_) 648 | 649 | Any options that were configured within the `lerna-cola.js` [configuration](#configuration) file will be provided here. 650 | 651 | For example: 652 | 653 | ```javascript 654 | // lerna-cola.js 655 | module.exports = { 656 | packages: { 657 | 'my-lib': { 658 | buildPlugin: { 659 | name: '@lerna-cola/plugin-build-babel', 660 | // 👇 this stuff here 661 | options: { 662 | config: 'my-babel-config-package', 663 | }, 664 | }, 665 | }, 666 | }, 667 | } 668 | ``` 669 | 670 | - `args` (_Object_) 671 | 672 | This will contain any other useful utils/data. Mostly plugin specific. Please read the docs for each plugin below. 673 | 674 | ### Clean Plugin 675 | 676 | Requires the following interface be satisfied: 677 | 678 | ```javascript 679 | module.exports = { 680 | name: 'my-plugin', 681 | clean: (pkg, options, args) => Promise.resolve('todo'), 682 | } 683 | ``` 684 | 685 | ### Build Plugin 686 | 687 | Requires the following interface be satisfied: 688 | 689 | ```javascript 690 | module.exports = { 691 | name: 'my-plugin', 692 | build: (pkg, options, args) => Promise.resolve('todo'), 693 | } 694 | ``` 695 | 696 | ### Develop Plugin 697 | 698 | Requires the following interface be satisfied: 699 | 700 | ```javascript 701 | module.exports = { 702 | name: 'my-plugin', 703 | develop: (pkg, options, args) => Promise.resolve('todo'), 704 | } 705 | ``` 706 | 707 | A special note on the develop plugin; it gets executed via the development service and will be run multiple times. Specifically the plugin will be run under the following conditions: 708 | 709 | - When the development service first starts 710 | - Any time a change is detected within the src directory of the package it is configured against 711 | - Any time one of the packages within the monorepo that it depends on changes. 712 | 713 | This allows you to know when to do restarting etc, and what type of behaviour you would like to implement for each scenario. 714 | 715 | The `args` parameter is also well used by the develop plugin. It will contain the following items: 716 | 717 | - `runType` (_string_) 718 | 719 | It will contain one of the following values: 720 | 721 | - 'FIRST_RUN' 722 | 723 | Indicates that this is the first time the develop plugin is being executed during by the development service. 724 | 725 | - 'SELF_CHANGED' 726 | 727 | Indicates that a file within the src directory of the package it is configured against changed. 728 | 729 | - 'DEPENDENCY_CHANGED' 730 | 731 | Indicates that a package that it depends upon within the monorepo changed. 732 | 733 | - `changedDependency` 734 | 735 | This will contain the [Package](#package-scheam) meta object describing the dependency that changed when the `runType` is "DEPENDENCY_CHANGED". 736 | 737 | ### Deploy Plugin 738 | 739 | Requires the following interface be satisfied: 740 | 741 | ```javascript 742 | module.exports = { 743 | name: 'my-plugin', 744 | develop: (pkg, options, args) => Promise.resolve('todo'), 745 | } 746 | ``` 747 | 748 | ### Schemas 749 | 750 | Below are schemas of the arguments that are provided to your plugins. 751 | 752 | #### `Package` Schema 753 | 754 | The holy grail of information for your plugins. The object contains the following: 755 | 756 | - `name` (_string_) 757 | 758 | The name of the package. 759 | 760 | - `config` (_Object_) 761 | 762 | The configuration that is being used (taken from the `lerna-cola.js` file with defaults applied) for it. 763 | 764 | - `color` (`chalk` package function) 765 | 766 | The `chalk` function representing the color we will use to uniquely identify console output for the package. 767 | 768 | - `dependants` (_Array<string>_) 769 | 770 | The names of the other packages within the monorepo that have a direct dependency on this package. 771 | 772 | - `dependencies` (_Array<string>_) 773 | 774 | The names of the other packages within the monorepo that this package has a direct dependency on. 775 | 776 | - `allDependants` (_Array<string>_) 777 | 778 | The names of the ALL other packages within the monorepo that directly, or indirectly (via dependency tree), depend on this package. 779 | 780 | - `devDependencies` (_Array<string>_) 781 | 782 | The names of the other packages within the monorepo that this package has a direct devDependency on. 783 | 784 | - `packageJson` (_Object_) 785 | 786 | The package.json for this package. 787 | 788 | - `paths` (_Object_) 789 | 790 | A set of paths for this package. Containing the following paths: 791 | 792 | - `monoRepoRoot` (_string_) 793 | 794 | The path to the root of the monorepo that this package belongs to. 795 | 796 | - `monoRepoRootNodeModules` (_string_) 797 | 798 | The path to the node_modules dir contained within the root of the monorepo that this package belongs to. 799 | 800 | - `packageBuildOutput` (_string_) 801 | 802 | The path to which build output should be emitted. 803 | 804 | - `packageEntryFile` (_string_) 805 | 806 | The source entry file for this package. 807 | 808 | - `packageJson` (_string_) 809 | 810 | The path to the package.json file for the package. 811 | 812 | - `packageNodeModules` (_string_) 813 | 814 | The path to the node_modules file for the package. 815 | 816 | - `packageRoot` (_string_) 817 | 818 | The path to the root dir of the package. 819 | 820 | - `packageSrc` (_string_) 821 | 822 | The path to the src dir of the package. 823 | 824 | #### `Config` Schema 825 | 826 | > TODO 827 | --------------------------------------------------------------------------------