├── .gitignore ├── LICENSE ├── README.md ├── bin ├── yarnhack └── yerna ├── circle.yml ├── lib ├── config.js ├── error-handling.js ├── filtering.js ├── linking.js ├── logger.js ├── packages.js ├── project.js ├── taskrunning.js └── yarn-with-package-json-mangling.js ├── package.json ├── test ├── fixture-project │ ├── child │ │ └── .gitkeep │ └── yerna.json └── project.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .vscode/ 4 | .idea/ 5 | .yarnrc 6 | *.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Kevin Verdieck 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yerna 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/github/nomcopter/yerna/master.svg)](https://circleci.com/gh/nomcopter/yerna) 4 | [![npm](https://img.shields.io/npm/v/yernapkg.svg)](https://www.npmjs.com/package/yernapkg) 5 | 6 | > Yarn + Lerna = Yerna 7 | 8 | Yerna is a monorepo management tool in the style of [Lerna](https://lernajs.io/), 9 | but stripped down and using [Yarn](https://yarnpkg.com/) as the package manager. 10 | 11 | ## Disclaimer 12 | 13 | The idea for Yerna stemmed from [a Lerna ticket suggesting Yarn integration](https://github.com/lerna/lerna/issues/371). 14 | At the time, doing so in Lerna was nontrivial (see also [Lerna issue #605](https://github.com/lerna/lerna/pull/605)), 15 | and long story short, I built [this giant bag of hacks](#how-it-works) to try it out myself. 16 | It turns out that the benefits outweighed the costs pretty dramatically for my exact use case, so I cleaned it up for release, and here we are. 17 | 18 | While I use Yerna heavily myself, it is still fundamentally a hack-based stopgap/overgrown experiment. 19 | I am happy to discuss the feature set and its pros/cons in contrast to those of Lerna and Yarn, but note that **I will not be prioritizing feature requests or performing nontrivial maintenance** if it does not affect my own workflow. 20 | 21 | Furthermore, the code is pretty sloppy in places, and I apologize in advance to any sensibilities that may be offended. 22 | This is basically a glorified proof of concept, submitted to you for discussion and experimentation. 23 | 24 | ## Installation 25 | 26 | **Note:** `yarn` is expected to already be installed and to exist on your `PATH` as Yerna will shell out to it. 27 | 28 | ```sh 29 | yarn global add yernapkg 30 | ``` 31 | 32 | Also ensure that Yarn's globally-installed binaries are accessible on your `PATH`. 33 | 34 | ## Usage 35 | 36 | **Note:** Please also read the [Caveats/Known Issues](#caveatsknown-issues)! 37 | 38 | Yerna provides two binaries: `yerna` itself and a helper `yarnhack`. 39 | 40 | ### `yerna` 41 | 42 | `yerna` is a tool for running tasks over one or more local packages, using Yarn, in parallel. `yerna` provides a few different commands, listed briefly below. Note that all commands: 43 | 44 | - respect the package-selection flags (`--include`, etc.) unless otherwise noted 45 | - respect the dependency ordering of packages when running tasks 46 | 47 | Run `yerna --help` for more information on the supported commands and flags. 48 | 49 | 50 | command | description | notes 51 | ---------------- | ------------------------------------------------------ | ----- 52 | install | install all external packages, link all local packages | 53 | link | link all local packages without installing | ignores flags 54 | list | list selected packages | useful for testing flag combinations 55 | run \ | run the npm script \ in packages | 56 | exec \ | run shell command \ in packages | 57 | 58 | ### `yarnhack` 59 | 60 | `yarnhack` is an executable that wraps Yarn and mangles `package.json` to prevent Yarn from trying to install packages that don't exist on the registry. Otherwise, it forwards directly to the system-installed `yarn` and understands all commands and flags defined there. 61 | 62 | ### Usage with Lerna 63 | 64 | Yerna is backwards-compatible with Lerna, in that it puts the repo into a valid state for Lerna. You can continue to use Lerna for features missing from Yerna (such as publishing), though be sure to [read the caveats/known issues](#caveatsknown-issues), in particular, the [behavior around symlinks](#symlinks). 65 | 66 | Yerna does not read or write any Lerna-specific files on the filesystem (except for a logfile); in particular, it does not read `lerna.json`. 67 | 68 | You can customise the paths where Yerna looks for packages by adding a `yerna.json` file: 69 | 70 | ```json 71 | { 72 | "packages": [ 73 | "packages/*", 74 | "elsewhere/*" 75 | ] 76 | } 77 | ``` 78 | 79 | This works in the same way as Lerna. The configuration file does not support any other properties. 80 | 81 | ### Caveats/Known Issues 82 | 83 | #### Parallel `install` failures 84 | 85 | `yerna install` can sometimes die with an error mentioning a failure to find/write/unlink files in the Yarn cache directory. An example: 86 | 87 | ``` 88 | yarn install v0.21.3 89 | [1/4] Resolving packages... 90 | [2/4] Fetching packages... 91 | error Couldn't find a package.json file in "/home/ubuntu/.cache/yarn/" 92 | info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. 93 | ``` 94 | 95 | This can happen when multiple Yarn processes are installing and at least one is writing to the cache, i.e., you have a lot of new packages to install. A temporary workaround is to serialize the installation with `--parallel 1`. Further installs should be read-heavy, since the cache is populated, and not run into the same issue (at least, until your cache gets old again and hits the same issue). 96 | 97 | #### `package.json` mangling 98 | 99 | See [How It Works](#how-it-works) for details on how Yerna makes Yarn work, but in short, it involves mangling `package.json`s on the filesystem temporarily. In most cases, these manglings should be transparent, but problems could arise if you e.g. do git operations while `yerna` or `yarnhack` are running. A severe fatal error could also cause one of these tools to abort without cleaning up after itself. 100 | 101 | #### Symlinks 102 | 103 | Both `yerna` and `yarnhack` assume that all local packages should _always_ be symlinked. This means that they will: 104 | 105 | - remove anything that's in the way of placing a symlink, even if it's a directory or regular file 106 | - automatically generate symlinks before/after most operations as a convenience (so you almost never have to run `yerna link`) 107 | 108 | ### npm Lifecycle Scripts 109 | 110 | npm install-related lifecycle scripts (namely `preinstall`, `postinstall` and `prepublish`) will not work properly, or at all, if they depend on local packages. Yarn will attempt to run these tasks, but because local package references were removed, such scripts will fail to run. There is no way that I know of using Yarn in this manner that would allow symlinking before the tasks are run short of prefixing `yerna link` before every such script. 111 | 112 | ## Motivation 113 | 114 | Originally, Yerna was written because Lerna had no Yarn support. Now that it does, the gap is smaller, but the primary improvements that Yerna offers are the following: 115 | 116 | - inclusion of `yarnhack`, which is necessary for adding/removing packages if you don't want Yarn to overwrite your symlinks every time 117 | - `--dependencies` and `--dependents` flags for all task types 118 | - `--force` flag for ignoring task failures 119 | - dedicated `link` task 120 | - support for multiply-specified `--include` (Lerna: `--scope`) and `--exclude` (Lerna: `--ignore`) 121 | - sectioned/labeled stdout so you can tell which packages are generating what output 122 | - automatic re-symlinking on task start/completion 123 | - improved throughput for (always-on) topological sorting and cycle detection 124 | 125 | I've [filed a few issues on Lerna](https://github.com/lerna/lerna/issues/created_by/seansfkelley) to track adding some of these features. 126 | 127 | The goal is for Yerna to serve as a reasonable stop-gap until [Lerna gets merged into Yarn](https://github.com/yarnpkg/yarn/issues/946#issuecomment-264597575), at which point it should be mostly or entirely obsoleted by vanilla Yarn. 128 | 129 | ## How it Works 130 | 131 | Hacks. Filthy, awful hacks. 132 | 133 | Yarn demands complete control over `node_modules`. This is reasonable; the only reason that Lerna worked "seamlessly" is because npm was lenient about the structure and content of `node_modules` (as long as it appeared to satify the constraints in `package.json`). Lerna was free to make all kinds of symlinks and npm would happily chug along ignoring them. Yarn, in its strictness, will clear out these symlinks _and_ will attempt to download packages that we know to be local-only. 134 | 135 | The workaround is to wrap Yarn in a task that mangles the `package.json`s to remove all local references before running Yarn. In practice, this works great, but the failure mode can be very confusing for those who aren't intimately familiar with how `node_modules`, npm and Yarn work. It also means that Yarn is free to delete all your symlinks and pretty much any time (as a convenience, `yerna` and `yarnhack` will re-link packages any time Yarn was run). 136 | 137 | ## Contributors 138 | 139 | Original Author: [@seansfkelley](https://github.com/seansfkelley) 140 | -------------------------------------------------------------------------------- /bin/yarnhack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const chalk = require('chalk'); 4 | const _ = require('lodash'); 5 | const fs = require('fs'); 6 | 7 | const { PROJECT_ROOT } = require('../lib/project'); 8 | const { readConfig } = require('../lib/config'); 9 | const { linkPackages } = require('../lib/linking'); 10 | const { getPackages } = require('../lib/packages'); 11 | const { runYarnWithPackageJsonMangling } = require('../lib/yarn-with-package-json-mangling'); 12 | 13 | if (!fs.existsSync('package.json')) { 14 | console.error(chalk.red('yarnhack: no package.json in the current directory, aborting...')); 15 | } else { 16 | const CONFIG = readConfig(PROJECT_ROOT); 17 | const PACKAGES = getPackages(PROJECT_ROOT, CONFIG); 18 | 19 | const child = runYarnWithPackageJsonMangling(process.argv.slice(2), { 20 | cwd: process.cwd(), 21 | env: process.env, 22 | stdio: 'inherit' 23 | }, PACKAGES); 24 | 25 | child.on('exit', (code, signal) => { 26 | linkPackages(PACKAGES); 27 | console.error(`yarnhack: re-linked ${chalk.cyan(_.size(PACKAGES))} local packages`); 28 | process.exitCode = code; 29 | }); 30 | 31 | process.on('SIGINT', () => { 32 | console.error(chalk.red('yarnhack: received sigint, terminating...')); 33 | child.kill(); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /bin/yerna: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/error-handling').attachGlobalErrorHandling(); 4 | process.exitCode = 0; 5 | 6 | const chalk = require('chalk'); 7 | const path = require('path'); 8 | const _ = require('lodash'); 9 | const commander = require('commander'); 10 | const child_process = require('child_process'); 11 | const fs = require('fs'); 12 | const Promise = require('bluebird'); 13 | 14 | const { PROJECT_ROOT } = require('../lib/project'); 15 | const { readConfig } = require('../lib/config'); 16 | const { getPackages } = require('../lib/packages'); 17 | const { linkPackages, WithLinking } = require('../lib/linking'); 18 | const { runYarnWithPackageJsonMangling, runYarn } = require('../lib/yarn-with-package-json-mangling'); 19 | const { logger, deleteLogFile } = require('../lib/logger'); 20 | const { filterPackages, logFlagFeedback } = require('../lib/filtering'); 21 | const { createTaskRunner, runPackagesToposorted, abort, WithAbort } = require('../lib/taskrunning'); 22 | 23 | const CONFIG = readConfig(PROJECT_ROOT); 24 | const PACKAGES = getPackages(PROJECT_ROOT, CONFIG); 25 | 26 | if (_.size(PACKAGES) === 0) { 27 | logger.error(chalk.red(`could not find any packages under ${CONFIG.packages}`)); 28 | process.exitCode = 1; 29 | } 30 | 31 | let didInit = false; 32 | function WithInit(fn) { 33 | return function() { 34 | if (didInit) { 35 | throw new Error('cannot initialize twice!'); 36 | } 37 | didInit = true; 38 | 39 | logger.transports.console.level = commander.loglevel; 40 | 41 | if (!commander.color) { 42 | chalk.enabled = false; 43 | } 44 | 45 | // 10 for default, then one for each of the 'exit' handlers we know will be attached by child processes. 46 | // All this does is prevent warnings, but it's good practice. 47 | process.setMaxListeners(10 + commander.parallel); 48 | 49 | deleteLogFile(); 50 | 51 | const startTime = Date.now(); 52 | 53 | function logTiming(verbiage) { 54 | logger.info(`yerna: ${verbiage} ${((Date.now() - startTime) / 1000).toFixed(2)}s`); 55 | } 56 | 57 | process.on('exit', () => { 58 | if (process.exitCode === 0) { 59 | deleteLogFile(); 60 | logTiming('took'); 61 | } else { 62 | logTiming('failed after'); 63 | } 64 | }); 65 | 66 | process.on('SIGINT', () => { 67 | logger.error(chalk.red('yerna: received sigint, terminating...')); 68 | abort(null, { userInitiated: true }); 69 | }); 70 | 71 | if (process.exitCode === 0) { 72 | return fn.apply(this, arguments); 73 | } else { 74 | // Do nothing; wait for process to exit... 75 | } 76 | } 77 | } 78 | 79 | function decorate(fn, ...decorators) { 80 | return decorators.slice().reverse().reduce((decoratedFn, decorator) => decorator(decoratedFn), fn); 81 | } 82 | 83 | const performLink = decorate(function() { 84 | linkPackages(PACKAGES); 85 | logger.info(`yerna: linked ${chalk.cyan(_.size(PACKAGES))} local packages`); 86 | }, WithAbort, WithInit); 87 | 88 | const performInstall = decorate(function(moreArgs) { 89 | const filteredPackages = filterPackages(PACKAGES, commander); 90 | const yarnArgs = [ 'install' ].concat(moreArgs); 91 | logFlagFeedback(commander, filteredPackages, 'yarn ' + yarnArgs.join(' ')); 92 | return runPackagesToposorted( 93 | filteredPackages, 94 | createTaskRunner(spawnArgs => runYarnWithPackageJsonMangling(yarnArgs, spawnArgs, PACKAGES), commander.force), 95 | commander.parallel 96 | ); 97 | }, WithAbort, WithInit, WithLinking(PACKAGES)); 98 | 99 | const performList = decorate(function() { 100 | const filteredPackages = filterPackages(PACKAGES, commander); 101 | logFlagFeedback(commander, filteredPackages); 102 | return runPackagesToposorted( 103 | filteredPackages, 104 | (pkg) => { 105 | logger.info(pkg.name); 106 | return Promise.resolve(); 107 | }, 108 | commander.parallel 109 | ); 110 | }, WithAbort, WithInit); 111 | 112 | const performRun = decorate(function(scriptName, moreArgs, options) { 113 | const filteredPackages = filterPackages(PACKAGES, commander, pkg => !!pkg.scripts[scriptName]); 114 | const yarnArgs = [ 'run', scriptName ].concat(moreArgs); 115 | const executableName = options.mangle ? 'yarnhack' : 'yarn'; 116 | logFlagFeedback(commander, filteredPackages, executableName + ' ' + yarnArgs.join(' '), [ `have a ${chalk.magenta(scriptName)} script` ]); 117 | const spawnTask = options.mangle 118 | ? spawnArgs => runYarnWithPackageJsonMangling(yarnArgs, spawnArgs, PACKAGES) 119 | : spawnArgs => runYarn(yarnArgs, spawnArgs); 120 | return runPackagesToposorted( 121 | filteredPackages, 122 | createTaskRunner(spawnTask, commander.force), 123 | commander.parallel 124 | ); 125 | }, WithAbort, WithInit, WithLinking(PACKAGES)); 126 | 127 | const performExec = decorate(function(binaryName, moreArgs) { 128 | const filteredPackages = filterPackages(PACKAGES, commander); 129 | logFlagFeedback(commander, filteredPackages, [ binaryName ].concat(moreArgs).join(' ')); 130 | return runPackagesToposorted( 131 | filteredPackages, 132 | createTaskRunner(spawnArgs => child_process.spawn(binaryName, moreArgs, spawnArgs), commander.force), 133 | commander.parallel 134 | ); 135 | }, WithAbort, WithInit, WithLinking(PACKAGES)); 136 | 137 | function optParallel(n) { 138 | if (n == null || +n <= 0 || Math.round(n) !== +n) { 139 | console.error(chalk.red('yerna: parallel option must be a positive integer')); 140 | process.exitCode = 1; 141 | } else { 142 | return +n; 143 | } 144 | } 145 | 146 | function concatOpt(opt, allOpts) { 147 | return allOpts.concat([ opt ]); 148 | } 149 | 150 | function optEnum(values, optName) { 151 | return function(opt) { 152 | if (values.indexOf(opt) === -1) { 153 | console.error(chalk.red(`yerna: ${optName} option must be one of '${values.join('\', \'')}'`)); 154 | process.exitCode = 1; 155 | } else { 156 | return opt; 157 | } 158 | }; 159 | } 160 | 161 | commander 162 | .description('Yerna: run tasks over one or more packages in parallel') 163 | .usage(' [yerna options] [-- delegate options]') 164 | .version(require('../package.json').version) 165 | .option('-i, --include ', 'run command only on packages matching ; can be specified multiple times (default: all packages)', concatOpt, []) 166 | .option('-x, --exclude ', 'run command except on packages matching ; can be specified multiple times (default: no excludes)', concatOpt, []) 167 | .option('--dependents', 'additionally include all transitive dependents of selected packages (even when excluded)', false) 168 | .option('--dependencies', 'additionally include all transitive dependencies of selected packages (even when excluded)', false) 169 | .option('-p, --parallel ', 'run up to processes in parallel (default: 4)', optParallel, 4) 170 | .option('-l, --loglevel ', 'set the log level for Yerna', optEnum([ 'debug', 'verbose', 'info', 'warn', 'error' ], 'loglevel'), 'info') 171 | .option('-f, --force', 'don\'t abort if a task exits non-zero or is interrupted', false) 172 | .option('-c, --no-color', 'disable colorized output', false); 173 | 174 | commander.on('--help', () =>{ 175 | [ 176 | ' Notes:', 177 | '', 178 | ' - Use -- to separate arguments intended for Yarn. See examples.', 179 | ' - Most tasks automatically relink local dependencies; `yerna link` usage should be rare.', 180 | '', 181 | ' Examples:', 182 | '', 183 | ' $ yerna install', 184 | ' $ yerna install --include foo', 185 | ' $ yerna install -- --pure-lockfile', 186 | ' $ yerna install --include foo --include bar -- --pure-lockfile', 187 | ' $ yerna run taskname --include foo --exclude bar', 188 | ' $ yerna run taskname -i "^foo(bar)?" --mangle', 189 | ' $ yerna exec ls -- -la', 190 | '' 191 | ].forEach(line => console.log(line)); 192 | }); 193 | 194 | commander 195 | .command('install -- [args...]') 196 | .description('`yarn install` for the specified packages') 197 | .action(performInstall); 198 | 199 | commander 200 | .command('link') 201 | .description('symlink all local package dependencies (ignores all flags)') 202 | .action(performLink); 203 | 204 | commander 205 | .command('list') 206 | .description('list package names matching all provided flags') 207 | .action(performList); 208 | 209 | commander 210 | .command('run -- [args...]') 211 | .option('--mangle', 'mangle package.json before running the script in each package (default: false)', false) 212 | .description('`yarn run ` for the specified packages') 213 | .action(performRun); 214 | 215 | commander 216 | .command('exec -- [args...]') 217 | .description('run ` ` for the specified packages') 218 | .action(performExec); 219 | 220 | commander.parse(process.argv); 221 | 222 | if (!didInit) { 223 | commander.outputHelp(); 224 | process.exitCode = 2; 225 | } 226 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 8.10.0 4 | dependencies: 5 | pre: 6 | - npm install -g yarn 7 | cache_directories: 8 | - "~/.cache/yarn" 9 | - "node_modules" 10 | override: 11 | - yarn install --frozen-lockfile 12 | test: 13 | override: 14 | - yarn test 15 | deployment: 16 | npm-publish: 17 | tag: /v[0-9]+(\.[0-9]+){2}(-.*[0-9]+)?/ 18 | owner: nomcopter 19 | commands: 20 | - touch .npmrc 21 | - printf "\n$NPM_REGISTRY_CREDS\n" >> .npmrc 22 | - npm publish 23 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const defaultConfig = { 5 | packages: ['packages/*'] 6 | }; 7 | 8 | function readConfig(gitRoot) { 9 | try { 10 | const config = JSON.parse(fs.readFileSync(path.join(gitRoot, 'yerna.json'))); 11 | return Object.assign({}, defaultConfig, config); 12 | } catch (e) { 13 | if (e.code !== 'ENOENT') { 14 | throw e; 15 | } 16 | return defaultConfig; 17 | } 18 | } 19 | 20 | module.exports = { 21 | readConfig 22 | }; 23 | -------------------------------------------------------------------------------- /lib/error-handling.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | // ************************************************ 4 | // THIS FILE SHOULD HAVE NO SIDE EFFECTS 5 | // 6 | // It must be safe to import and call this file from 7 | // anywhere so the user can get error messages even 8 | // if everything is on fire. 9 | // ************************************************ 10 | 11 | function attachGlobalErrorHandling() { 12 | function logAndDie(e) { 13 | console.error(chalk.bgRed(formatErrorForConsole(e))); 14 | console.error(chalk.bgRed('yerna: unexpected error, exiting suddenly!')); 15 | console.error(chalk.bgRed('yerna: this is probably a bug in Yerna itself, please file an issue!')); 16 | console.error(chalk.bgRed('yerna: NOTE: package.jsons or links may be in an inconsistent state')); 17 | console.error(chalk.bgRed('yerna: NOTE: child processes may be abandoned')); 18 | process.exit(1); 19 | } 20 | 21 | process.on('uncaughtException', logAndDie); 22 | process.on('unhandledRejection', logAndDie); 23 | } 24 | 25 | function formatErrorForConsole(e) { 26 | if (e) { 27 | return (e.stack ? e.stack : e.toString()).split('\n').map(line => 'yerna: ' + line).join('\n'); 28 | } else { 29 | return 'yerna: '; 30 | } 31 | } 32 | 33 | module.exports = { 34 | attachGlobalErrorHandling, 35 | formatErrorForConsole 36 | } 37 | -------------------------------------------------------------------------------- /lib/filtering.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const _ = require('lodash'); 3 | const fs = require('fs'); 4 | const { logger } = require('./logger'); 5 | 6 | function findTransitiveDependentsOrDependencies(packagesByName, rootPackageName, typeKey) { 7 | const selectedPackageNames = []; 8 | let packageQueue = [ packagesByName[rootPackageName] ]; 9 | while (packageQueue.length) { 10 | const currentPackage = packageQueue.shift(); 11 | if (selectedPackageNames.indexOf(currentPackage.name) === -1) { 12 | selectedPackageNames.push(currentPackage.name); 13 | packageQueue = packageQueue.concat(currentPackage[typeKey].map(packageName => packagesByName[packageName])) 14 | } 15 | } 16 | return selectedPackageNames.map(packageName => packagesByName[packageName]); 17 | } 18 | 19 | function maybeIncludeDependentsAndDependencies(packagesByName, pkg, { dependents, dependencies }) { 20 | let packages = [ pkg ]; 21 | 22 | if (dependents) { 23 | packages = packages.concat(_.flatMap(packages, pkg => findTransitiveDependentsOrDependencies(packagesByName, pkg.name, 'localDependents'))); 24 | } 25 | 26 | if (dependencies) { 27 | packages = packages.concat(_.flatMap(packages, pkg => findTransitiveDependentsOrDependencies(packagesByName, pkg.name, 'localDependencies'))); 28 | } 29 | 30 | return _.uniqBy(packages, 'name'); 31 | } 32 | 33 | function applyIncludeExclude(pkg, { include, exclude }) { 34 | return ( 35 | (include.length === 0 || include.some(regex => new RegExp(regex).test(pkg.name))) && 36 | (exclude.length === 0 || !exclude.some(regex => new RegExp(regex).test(pkg.name))) 37 | ); 38 | }; 39 | 40 | function filterPackages(packagesByName, commander, additionalFilter = () => true) { 41 | const filteredPackages = _.chain(packagesByName) 42 | .values() 43 | .filter(pkg => applyIncludeExclude(pkg, commander)) 44 | .flatMap(pkg => maybeIncludeDependentsAndDependencies(packagesByName, pkg, commander)) 45 | .uniqBy('name') 46 | .sortBy('name') 47 | .filter(additionalFilter) 48 | .value(); 49 | 50 | return filteredPackages; 51 | } 52 | 53 | function formatList(strings, conjunction = 'or', oxfordComma = false) { 54 | if (oxfordComma) { 55 | throw new Error(`yerna: oops, don't use an Oxford comma!`); 56 | } 57 | 58 | if (strings.length <= 1) { 59 | return strings.join(''); 60 | } else { 61 | return `${strings.slice(0, strings.length - 1).join(', ')} ${conjunction} ${strings[strings.length - 1]}` 62 | } 63 | } 64 | 65 | function logFlagFeedback(commander, filteredPackages, commandPhrase, additionalFilters = []) { 66 | const include = commander.include.length ? formatList(commander.include.map(r => chalk.magenta(r))) : null; 67 | const exclude = commander.exclude.length ? formatList(commander.exclude.map(r => chalk.magenta(r))) : null; 68 | const { dependents, dependencies } = commander; 69 | 70 | logger.info(`yerna: ${commandPhrase ? 'running ' + chalk.cyan(commandPhrase) + ' for ' : ''}${chalk.cyan(filteredPackages.length)} package(s)`); 71 | 72 | if (include) { 73 | logger.info(`yerna: - that match ${include}`); 74 | } 75 | 76 | if (exclude) { 77 | logger.info(`yerna: - that do not match ${exclude}`); 78 | } 79 | 80 | additionalFilters.forEach(filter => { 81 | logger.info(`yerna: - that ${filter}`); 82 | }); 83 | 84 | if (dependents || dependencies) { 85 | let logline = ''; 86 | 87 | if (dependents && dependencies) { 88 | logline = `yerna: - including ${chalk.magenta('all transitive dependents and their dependencies')}`; 89 | } else if (dependents) { 90 | logline = `yerna: - including ${chalk.magenta('all transitive dependents')}`; 91 | } else if (dependencies) { 92 | logline = `yerna: - including ${chalk.magenta('all transitive dependencies')}`; 93 | } 94 | 95 | if (exclude) { 96 | logline += ` (even if they match --exclude)`; 97 | } 98 | 99 | logger.info(logline); 100 | } 101 | 102 | logger.verbose('yerna: selected packages are:'); 103 | filteredPackages.forEach(pkg => logger.verbose(`yerna: - ${pkg.name}`)); 104 | } 105 | 106 | module.exports = { 107 | filterPackages, 108 | logFlagFeedback 109 | }; 110 | -------------------------------------------------------------------------------- /lib/linking.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const _ = require('lodash'); 3 | const path = require('path'); 4 | const mkdirp = require('mkdirp'); 5 | const fs = require('fs'); 6 | const { logger } = require('./logger'); 7 | const rimraf = require('rimraf'); 8 | 9 | function symlink(symlinkContent, symlinkPath) { 10 | mkdirp.sync(path.dirname(symlinkPath)); 11 | let stats; 12 | try { 13 | stats = fs.lstatSync(symlinkPath); 14 | } catch (e) {} 15 | if (stats) { 16 | if (stats.isSymbolicLink()) { 17 | logger.debug(`yerna: removing existing symlink at ${symlinkPath}`); 18 | fs.unlinkSync(symlinkPath); 19 | } else { 20 | logger.warn(chalk.yellow(`yerna: removing unexpected non-symlink at ${symlinkPath}`)); 21 | rimraf.sync(symlinkPath); 22 | } 23 | } else { 24 | logger.debug(`yerna: nothing in the way of a symlink at ${symlinkPath}`); 25 | } 26 | logger.debug(`yerna: symlinking ${symlinkPath} to ${symlinkContent}`); 27 | fs.symlinkSync(symlinkContent, symlinkPath); 28 | } 29 | 30 | function linkPackages(packagesByName) { 31 | _.forEach(packagesByName, (package, packageName) => { 32 | package.localDependencies.forEach(dependencyName => { 33 | const dependency = packagesByName[dependencyName]; 34 | if (!dependency) { 35 | throw new Error(`programmer error: cannot link ${packageName} to ${dependencyName}`); 36 | } 37 | 38 | const symlinkPath = path.resolve(package.path, 'node_modules', dependencyName); 39 | const symlinkDirectory = path.dirname(symlinkPath); 40 | symlink(dependency.path, symlinkPath); 41 | 42 | if (dependency.bin) { 43 | const binaryRoot = path.resolve(package.path, 'node_modules', '.bin'); 44 | _.forEach(dependency.bin, (relativeBinaryPath, binaryName) => { 45 | const binarySymlinkPath = path.resolve(binaryRoot, binaryName); 46 | symlink(path.resolve(dependency.path, relativeBinaryPath), binarySymlinkPath); 47 | }); 48 | } 49 | }); 50 | }); 51 | } 52 | 53 | function WithLinking(packagesByName) { 54 | function prelink() { 55 | linkPackages(packagesByName); 56 | logger.verbose(`yerna: linked all ${chalk.cyan(_.size(packagesByName))} package(s) before running tasks`); 57 | } 58 | 59 | function postlink() { 60 | linkPackages(packagesByName); 61 | logger.verbose(`yerna: re-linked all ${chalk.cyan(_.size(packagesByName))} package(s) after running tasks`); 62 | } 63 | 64 | return function (fn) { 65 | return function() { 66 | prelink(); 67 | 68 | const returnValue = fn.apply(this, arguments); 69 | 70 | if (returnValue && typeof returnValue.then === 'function') { 71 | return returnValue.tap(postlink); 72 | } else { 73 | postlink(); 74 | return returnValue; 75 | } 76 | }; 77 | }; 78 | } 79 | 80 | module.exports = { 81 | linkPackages, 82 | WithLinking 83 | }; 84 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const LOG_FILENAME = path.resolve('yerna.log'); 6 | 7 | const ANSI_COLOR_CODE_REGEX = /\u001b\[(\d+(;\d+)*)?m/g; 8 | 9 | function stripColors(s) { 10 | return s ? s.replace(ANSI_COLOR_CODE_REGEX, '') : s; 11 | } 12 | 13 | const logger = new winston.Logger({ 14 | transports: [ 15 | new winston.transports.Console({ 16 | showLevel: false 17 | }), 18 | new winston.transports.File({ 19 | filename: LOG_FILENAME, 20 | json: false, 21 | level: 'debug', 22 | formatter: ({ timestamp, message, level }) => `${(new Date).toISOString()} - ${level}: ${stripColors(message)}` 23 | }) 24 | ] 25 | }); 26 | 27 | function deleteLogFile() { 28 | if (fs.existsSync(LOG_FILENAME)) { 29 | fs.unlinkSync(LOG_FILENAME); 30 | } 31 | } 32 | 33 | module.exports = { 34 | logger, 35 | deleteLogFile, 36 | LOG_FILENAME 37 | }; 38 | -------------------------------------------------------------------------------- /lib/packages.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const glob = require('glob'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | function getPackages(root, config) { 7 | const packagesByName = _.chain(config.packages) 8 | .map(packageGlob => path.join(root, packageGlob, 'package.json')) 9 | .flatMap(packageJsonGlob => glob.sync(packageJsonGlob)) 10 | .map(filename => { 11 | const packageJson = JSON.parse(fs.readFileSync(filename)); 12 | return { 13 | name: packageJson.name, 14 | path: path.resolve(path.dirname(filename)), 15 | bin: packageJson.bin || {}, 16 | scripts: packageJson.scripts || {}, 17 | allDependencies: _.chain([ 'dependencies', 'devDependencies' ]) 18 | .map(key => packageJson[key]) 19 | .compact() 20 | .map(dependencies => _.keys(dependencies)) 21 | .flatten() 22 | .uniq() 23 | .value(), 24 | localDependents: [] 25 | }; 26 | }) 27 | .keyBy('name') 28 | .value(); 29 | 30 | _.forEach(packagesByName, (pkg, name) => { 31 | pkg.localDependencies = pkg.allDependencies.filter(dependency => !!packagesByName[dependency]); 32 | delete pkg.allDependencies; 33 | }); 34 | 35 | _.forEach(packagesByName, (pkg, name) => { 36 | pkg.localDependencies.forEach(dependencyName => { 37 | packagesByName[dependencyName].localDependents.push(name); 38 | }) 39 | }); 40 | 41 | _.forEach(packagesByName, pkg => { 42 | pkg.localDependencies = _.sortBy(pkg.localDependencies); 43 | pkg.localDependents = _.sortBy(pkg.localDependents); 44 | }); 45 | 46 | return packagesByName; 47 | } 48 | 49 | module.exports = { 50 | getPackages 51 | }; 52 | -------------------------------------------------------------------------------- /lib/project.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const child_process = require('child_process'); 3 | const findUp = require('find-up'); 4 | 5 | let root; 6 | 7 | const file = jsonProjectRoot(); 8 | if (file) { 9 | root = path.dirname(file); 10 | } else { 11 | try { 12 | root = gitProjectRoot(); 13 | } catch(_) { 14 | throw new Error(`yerna: Can't find project root from ${process.cwd()}. Looking for git repository, yerna.json, or lerna.json.`); 15 | } 16 | } 17 | 18 | module.exports = { 19 | PROJECT_ROOT: root 20 | }; 21 | 22 | function gitProjectRoot() { 23 | return child_process.execSync('git rev-parse --show-toplevel', { 24 | timeout: 5000, 25 | stdio: 'pipe' 26 | }).toString().trim(); 27 | } 28 | 29 | function jsonProjectRoot() { 30 | return findUp.sync(['yerna.json', 'lerna.json']); 31 | } 32 | -------------------------------------------------------------------------------- /lib/taskrunning.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | const _ = require('lodash'); 4 | const Promise = require('bluebird'); 5 | const async = require('async'); 6 | 7 | const { logger, LOG_FILENAME } = require('./logger'); 8 | const { formatErrorForConsole } = require('./error-handling'); 9 | 10 | const KILL_SIGNAL = 'SIGTERM'; 11 | 12 | let spawnedProcesses = []; 13 | let isAborting = false; 14 | 15 | function trimLastNewline(s) { 16 | return s[s.length - 1] === '\n' ? s.slice(0, s.length - 1) : s; 17 | } 18 | 19 | function createTaskRunner(spawnChild, ignoreFailures = false) { 20 | return function(pkg, { incrementAndGetCompletedPackageCount, getTotalPackageCount }) { 21 | return new Promise((resolve, reject) => { 22 | const spawnArgs = { 23 | cwd: pkg.path, 24 | env: process.env, 25 | stdio: [ 'ignore', 'pipe', 'pipe' ], 26 | detached: true 27 | }; 28 | 29 | const child = spawnChild(spawnArgs); 30 | const spawnTime = Date.now(); 31 | 32 | logger.debug(`yerna: package ${chalk.cyan(pkg.name)} began task with child pid ${child.pid}`); 33 | 34 | spawnedProcesses.push(child); 35 | 36 | const mergedOutput = []; 37 | 38 | child.stdout.on('data', chunk => { 39 | mergedOutput.push({ 40 | level: 'info', 41 | loglines: trimLastNewline(chunk.toString()).split('\n') 42 | }); 43 | }); 44 | 45 | child.stderr.on('data', chunk => { 46 | mergedOutput.push({ 47 | level: 'error', 48 | loglines: trimLastNewline(chunk.toString()).split('\n') 49 | }); 50 | }); 51 | 52 | child.on('exit', (code, signal) => { 53 | logger.debug(`yerna: package ${chalk.cyan(pkg.name)} task with child pid ${child.pid} exited with code ${code} and signal ${signal} after ${((Date.now() - spawnTime) / 1000).toFixed(2)}s`); 54 | 55 | if (mergedOutput.length) { 56 | logger.info(`yerna: output for ${chalk.cyan(pkg.name)} (${incrementAndGetCompletedPackageCount()}/${getTotalPackageCount()})`); 57 | mergedOutput.forEach(({ level, loglines }) => loglines.forEach(l => logger[level](l))); 58 | } else { 59 | const verbiage = isAborting ? 'aborted' : 'completed'; 60 | logger.info(`yerna: ${verbiage} ${chalk.cyan(pkg.name)} with no output (${incrementAndGetCompletedPackageCount()}/${getTotalPackageCount()})`); 61 | } 62 | 63 | if (isAborting) { 64 | logger.info('yerna: -- ABORTED --'); 65 | } 66 | 67 | spawnedProcesses = _.without(spawnedProcesses, child); 68 | 69 | const didExitFail = code != null && code !== 0; 70 | const didGetSignal = signal != null; 71 | if (didExitFail || didGetSignal) { 72 | const reasons = _.compact([ 73 | didGetSignal ? `task received signal ${signal}` : null, 74 | didExitFail ? `task exited with code ${code}` : null 75 | ]).join('; '); 76 | if (ignoreFailures) { 77 | // If someone else kills this child with this signal, oh well. --force is mostly a best effort anyway. 78 | if (signal === KILL_SIGNAL) { 79 | logger.debug(`yerna: failing on package ${chalk.cyan(pkg.name)} even though we're ignoring failures because it probably came from a user-initiated abort`); 80 | reject(reasons); 81 | } else { 82 | logger.warn(chalk.yellow(`yerna: ignoring failure (${reasons}) for ${chalk.cyan(pkg.name)} and continuing`)); 83 | resolve(); 84 | } 85 | } else { 86 | reject(reasons); 87 | } 88 | } else { 89 | resolve(); 90 | } 91 | }); 92 | }); 93 | } 94 | } 95 | 96 | function abort(cause, { userInitiated } = {}) { 97 | logger.debug(`yerna: received a request to abort`); 98 | 99 | if (isAborting) { 100 | logger.debug('yerna: already aborting, ignoring request'); 101 | return; 102 | } 103 | 104 | if (cause) { 105 | logger.error(chalk.bgRed(formatErrorForConsole(cause))); 106 | } else { 107 | logger.debug('yerna: no abort cause was given'); 108 | } 109 | 110 | if (!userInitiated) { 111 | logger.error(chalk.bgRed('yerna: errors while running tasks!')); 112 | logger.error(chalk.red(`yerna: check the logfile for more logging: ${LOG_FILENAME}`)); 113 | } 114 | 115 | let childProcessCheckInterval; 116 | 117 | isAborting = true; 118 | Promise.delay(100) 119 | .then(() => { 120 | if (spawnedProcesses.length) { 121 | logger.warn(chalk.red(`yerna: waiting for ${spawnedProcesses.length} child process(es) to exit`)); 122 | } 123 | 124 | logger.debug(`yerna: killing ${spawnedProcesses.length} processes, pids: ${spawnedProcesses.map(child => child.pid).join(', ')}`); 125 | spawnedProcesses.forEach(child => { process.kill(-child.pid, KILL_SIGNAL); }) 126 | 127 | return new Promise((resolve, reject) => { 128 | childProcessCheckInterval = setInterval(() => { 129 | if (spawnedProcesses.length === 0) { 130 | logger.debug('yerna: child processes appear to have exited, continuing with abort'); 131 | resolve(); 132 | } 133 | }, 200); 134 | }); 135 | }) 136 | .catch(e => { 137 | logger.error(chalk.bgRed(formatErrorForConsole(e))); 138 | logger.error(chalk.bgRed('yerna: package.jsons may still be mangled on the file system!')); 139 | if (spawnedProcesses.length) { 140 | logger.error(chalk.bgRed(`yerna: ${spawnedProcesses.length} process(es) were still running at exit time and might be abandoned (pids: ${spawnedProcesses.map(child => child.pid).join(', ')})`)); 141 | } 142 | logger.error(chalk.bgRed('yerna: unexpected error during cleanup, exiting suddenly!')); 143 | }) 144 | .finally(() => { 145 | clearInterval(childProcessCheckInterval); 146 | process.exitCode = 1; 147 | }); 148 | } 149 | 150 | function WithAbort(fn) { 151 | return function() { 152 | let returnValue; 153 | try { 154 | returnValue = fn.apply(this, arguments); 155 | } catch (e) { 156 | abort(e); 157 | return; 158 | } 159 | 160 | if (returnValue && typeof returnValue.then === 'function') { 161 | return returnValue.catch(abort); 162 | } else { 163 | return returnValue; 164 | } 165 | }; 166 | } 167 | 168 | function runPackagesToposorted(packages, runTask, parallel) { 169 | if (packages.length === 0) { 170 | logger.debug('yerna: no packages to run tasks for; early-terminating taskrunning'); 171 | return Promise.resolve(); 172 | } 173 | 174 | const packageByName = _.keyBy(packages, 'name'); 175 | 176 | const pendingDependencies = _.mapValues(packageByName, pkg => 177 | pkg.localDependencies.filter(pkgName => packageByName[pkgName]).length 178 | ); 179 | 180 | function enqueueAvailablePackages({ breakCycles }) { 181 | if (_.size(pendingDependencies) > 0) { 182 | logger.debug(`${_.size(pendingDependencies)} packages not yet enqueued`); 183 | let freePackages =_.keys(_.pickBy(pendingDependencies, value => value === 0)); 184 | 185 | if (breakCycles && freePackages.length === 0) { 186 | logger.warn(chalk.red('yerna: encountered a cycle in the dependency graph; will best-effort break it...')); 187 | logger.warn(chalk.red(`yerna: packages blocked by the cycle are${[ '' ].concat(_.keys(pendingDependencies)).join('\nyerna: - ')}`)); 188 | const packageWithMostPendingDependents = _.maxBy(_.keys(pendingDependencies), pkgName => 189 | packageByName[pkgName].localDependents.filter(depName => !!pendingDependencies[depName]).length 190 | ); 191 | logger.warn(chalk.red(`yerna: breaking cycle with package '${packageWithMostPendingDependents}'`)); 192 | freePackages = [ packageWithMostPendingDependents ]; 193 | } 194 | 195 | logger.debug(`yerna: will enqueue ${freePackages.length} packages`); 196 | freePackages.forEach(pkgName => { 197 | logger.debug(`yerna: enqueuing package '${pkgName}'`); 198 | q.push(packageByName[pkgName]); 199 | delete pendingDependencies[pkgName]; 200 | }); 201 | } else { 202 | logger.debug('yerna: asked to enqueue but there are no packages to enqueue; doing nothing'); 203 | } 204 | } 205 | 206 | // This is strictly to report to the user; don't depend on this value for queueing or correctness. 207 | let completedPackageCount = 0; 208 | 209 | function incrementAndGetCompletedPackageCount() { 210 | return ++completedPackageCount; 211 | } 212 | 213 | function getTotalPackageCount() { 214 | return packages.length; 215 | } 216 | 217 | const q = async.queue((pkg, callback) => { 218 | logger.debug(`yerna: running task for ${pkg.name}`) 219 | return runTask(pkg, { incrementAndGetCompletedPackageCount, getTotalPackageCount }) 220 | .then(() => { 221 | logger.debug(`yerna: finished task for ${pkg.name}; attempting to enqueue more packages`); 222 | pkg.localDependents 223 | // This filter does 2 things: 224 | // - ensures we pick only packages in the subset 225 | // - avoids nonsensically decrementing values when a cycle-breaking task finishes 226 | .filter(pkgName => pendingDependencies[pkgName] != null) 227 | .forEach(pkgName => { 228 | pendingDependencies[pkgName]--; 229 | }); 230 | enqueueAvailablePackages({ breakCycles: false }) 231 | }) 232 | .asCallback(callback); 233 | }, parallel); 234 | 235 | enqueueAvailablePackages({ breakCycles: true }); 236 | 237 | return new Promise((resolve, reject) => { 238 | q.drain = () => { 239 | if (_.size(pendingDependencies) === 0) { 240 | logger.debug('yerna: taskrunning queue empty and no pending packages left; taskrunning complete'); 241 | resolve(); 242 | } else { 243 | logger.debug('yerna: taskrunning queue empty but packages remain; attempting to enqueue with cycle-breaking allowed'); 244 | enqueueAvailablePackages({ breakCycles: true }); 245 | } 246 | }; 247 | q.error = (e) => { 248 | q.error = () => {}; 249 | q.kill(); 250 | reject(e); 251 | }; 252 | }); 253 | } 254 | 255 | module.exports = { 256 | createTaskRunner, 257 | runPackagesToposorted, 258 | abort, 259 | WithAbort 260 | }; 261 | -------------------------------------------------------------------------------- /lib/yarn-with-package-json-mangling.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const child_process = require('child_process'); 5 | const { logger } = require('./logger'); 6 | 7 | function alphabetizeKeys(object) { 8 | const alphabetizedObject = {}; 9 | _.chain(object) 10 | .keys() 11 | .sortBy() 12 | .forEach(k => alphabetizedObject[k] = object[k]) 13 | .value(); 14 | return alphabetizedObject; 15 | } 16 | 17 | function manglePackageJson(packageJsonPath, packagesByName) { 18 | const packageJson = fs.readFileSync(packageJsonPath); 19 | 20 | let mangledPackageJson; 21 | try { 22 | mangledPackageJson = JSON.parse(packageJson); 23 | } catch (e) { 24 | logger.error('yerna: Could not parse ' + packageJsonPath); 25 | throw e; 26 | } 27 | 28 | const removedDependencies = {}; 29 | [ 'dependencies', 'devDependencies' ].forEach(key => { 30 | if (mangledPackageJson[key]) { 31 | removedDependencies[key] = {}; 32 | _.forEach(mangledPackageJson[key], (version, packageName) => { 33 | if (packagesByName[packageName]) { 34 | delete mangledPackageJson[key][packageName]; 35 | removedDependencies[key][packageName] = version; 36 | } 37 | }); 38 | } 39 | }); 40 | fs.writeFileSync(packageJsonPath, JSON.stringify(mangledPackageJson, null, 2) + '\n'); 41 | 42 | return removedDependencies; 43 | } 44 | 45 | function unmanglePackageJson(packageJsonPath, mangleToken) { 46 | const packageJson = fs.readFileSync(packageJsonPath); 47 | const unmangledPackageJson = JSON.parse(packageJson); 48 | _.merge(unmangledPackageJson, mangleToken); 49 | [ 'dependencies', 'devDependencies' ].forEach(key => { 50 | if (unmangledPackageJson[key]) { 51 | unmangledPackageJson[key] = alphabetizeKeys(unmangledPackageJson[key]); 52 | } 53 | }); 54 | fs.writeFileSync(packageJsonPath, JSON.stringify(unmangledPackageJson, null, 2) + '\n'); 55 | } 56 | 57 | function runYarn(args, spawnArgs, onChildExit) { 58 | const child = child_process.spawn('yarn', args, spawnArgs); 59 | 60 | const killOnExit = () => { 61 | logger.debug(`yerna: (yarn wrapper) killing child ${child.pid} because parent process is exiting`); 62 | child.kill(); 63 | }; 64 | 65 | process.on('exit', killOnExit); 66 | 67 | child.once('exit', (code, signal) => { 68 | if (onChildExit) { 69 | onChildExit(); 70 | } 71 | process.removeListener('exit', killOnExit); 72 | logger.debug(`yerna: (yarn wrapper) child pid ${child.pid} exited with code ${code} and signal ${signal}`); 73 | }); 74 | 75 | return child; 76 | } 77 | 78 | function runYarnWithPackageJsonMangling(args, spawnArgs, packagesByName) { 79 | const packageJsonPath = path.resolve(spawnArgs.cwd || process.cwd(), 'package.json'); 80 | const mangleToken = manglePackageJson(packageJsonPath, packagesByName); 81 | return runYarn(args, spawnArgs, () => unmanglePackageJson(packageJsonPath, mangleToken)); 82 | } 83 | 84 | module.exports = { 85 | runYarnWithPackageJsonMangling, 86 | runYarn, 87 | }; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yernapkg", 3 | "version": "0.6.0", 4 | "description": "Yarn + Lerna = Yerna", 5 | "license": "Apache-2.0", 6 | "scripts": { 7 | "test": "tape 'test/**/*.js'" 8 | }, 9 | "bin": { 10 | "yarnhack": "./bin/yarnhack", 11 | "yerna": "./bin/yerna" 12 | }, 13 | "dependencies": { 14 | "async": "^2.1.4", 15 | "bluebird": "^3.4.6", 16 | "chalk": "^1.1.3", 17 | "commander": "^2.9.0", 18 | "find-up": "^2.1.0", 19 | "glob": "^7.1.1", 20 | "lodash": "^4.17.2", 21 | "mkdirp": "^0.5.1", 22 | "rimraf": "^2.6.1", 23 | "winston": "^2.3.1" 24 | }, 25 | "keywords": [ 26 | "lerna", 27 | "yarn", 28 | "yerna" 29 | ], 30 | "devDependencies": { 31 | "mock-bin": "^1.0.1", 32 | "proxyquire": "^1.7.11", 33 | "tape": "^4.6.3", 34 | "tmp": "0.0.31" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/fixture-project/child/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seansfkelley/yerna/572b1b66ead681794a6415fc5ebb47ca84fb4b41/test/fixture-project/child/.gitkeep -------------------------------------------------------------------------------- /test/fixture-project/yerna.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/project.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const test = require('tape'); 4 | const proxyquire = require('proxyquire'); 5 | const tmp = require('tmp') 6 | const mockBin = require('mock-bin') 7 | 8 | const originalCwd = process.cwd() 9 | 10 | test('git root', (t) => { 11 | t.plan(1); 12 | 13 | const {PROJECT_ROOT} = proxyquire('../lib/project', {}); 14 | t.equal(PROJECT_ROOT, path.join(__dirname, '..')); 15 | }); 16 | 17 | test('yerna.json root', (t) => { 18 | t.plan(1); 19 | 20 | mockBin('git', 'node', 'process.exit(1)').then((unmock) => { 21 | const projectDir = path.join(__dirname, 'fixture-project'); 22 | const childDir = path.join(projectDir, 'child'); 23 | 24 | process.chdir(childDir); 25 | 26 | const {PROJECT_ROOT} = proxyquire('../lib/project', {}); 27 | t.equal(PROJECT_ROOT, projectDir); 28 | 29 | process.chdir(originalCwd); 30 | unmock(); 31 | }); 32 | }); 33 | 34 | test('error if no git root or yerna.json', (t) => { 35 | t.plan(1); 36 | 37 | mockBin('git', 'node', 'process.exit(1)').then((unmock) => { 38 | const dir = tmp.dirSync({unsafeCleanup: true}).name; 39 | process.chdir(dir); 40 | 41 | t.throws(() => { 42 | proxyquire('../lib/project', {}); 43 | }, new RegExp('Can\'t find project root')); 44 | 45 | process.chdir(originalCwd); 46 | unmock(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ansi-regex@^2.0.0: 6 | version "2.1.1" 7 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 8 | 9 | ansi-styles@^2.2.1: 10 | version "2.2.1" 11 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 12 | 13 | async@^2.1.4: 14 | version "2.3.0" 15 | resolved "https://registry.yarnpkg.com/async/-/async-2.3.0.tgz#1013d1051047dd320fe24e494d5c66ecaf6147d9" 16 | dependencies: 17 | lodash "^4.14.0" 18 | 19 | async@~1.0.0: 20 | version "1.0.0" 21 | resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" 22 | 23 | balanced-match@^0.4.1: 24 | version "0.4.2" 25 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 26 | 27 | bluebird@^3.4.6: 28 | version "3.5.0" 29 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" 30 | 31 | brace-expansion@^1.0.0: 32 | version "1.1.7" 33 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" 34 | dependencies: 35 | balanced-match "^0.4.1" 36 | concat-map "0.0.1" 37 | 38 | chalk@^1.1.3: 39 | version "1.1.3" 40 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 41 | dependencies: 42 | ansi-styles "^2.2.1" 43 | escape-string-regexp "^1.0.2" 44 | has-ansi "^2.0.0" 45 | strip-ansi "^3.0.0" 46 | supports-color "^2.0.0" 47 | 48 | cmd-shim@^2.0.2: 49 | version "2.0.2" 50 | resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" 51 | dependencies: 52 | graceful-fs "^4.1.2" 53 | mkdirp "~0.5.0" 54 | 55 | colors@1.0.x: 56 | version "1.0.3" 57 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" 58 | 59 | commander@^2.9.0: 60 | version "2.9.0" 61 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 62 | dependencies: 63 | graceful-readlink ">= 1.0.0" 64 | 65 | concat-map@0.0.1: 66 | version "0.0.1" 67 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 68 | 69 | cycle@1.0.x: 70 | version "1.0.3" 71 | resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" 72 | 73 | deep-equal@~1.0.1: 74 | version "1.0.1" 75 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" 76 | 77 | define-properties@^1.1.2: 78 | version "1.1.2" 79 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" 80 | dependencies: 81 | foreach "^2.0.5" 82 | object-keys "^1.0.8" 83 | 84 | defined@~1.0.0: 85 | version "1.0.0" 86 | resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" 87 | 88 | es-abstract@^1.5.0: 89 | version "1.7.0" 90 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" 91 | dependencies: 92 | es-to-primitive "^1.1.1" 93 | function-bind "^1.1.0" 94 | is-callable "^1.1.3" 95 | is-regex "^1.0.3" 96 | 97 | es-to-primitive@^1.1.1: 98 | version "1.1.1" 99 | resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" 100 | dependencies: 101 | is-callable "^1.1.1" 102 | is-date-object "^1.0.1" 103 | is-symbol "^1.0.1" 104 | 105 | escape-string-regexp@^1.0.2: 106 | version "1.0.5" 107 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 108 | 109 | eyes@0.1.x: 110 | version "0.1.8" 111 | resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" 112 | 113 | fill-keys@^1.0.2: 114 | version "1.0.2" 115 | resolved "https://registry.yarnpkg.com/fill-keys/-/fill-keys-1.0.2.tgz#9a8fa36f4e8ad634e3bf6b4f3c8882551452eb20" 116 | dependencies: 117 | is-object "~1.0.1" 118 | merge-descriptors "~1.0.0" 119 | 120 | find-up@^2.1.0: 121 | version "2.1.0" 122 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" 123 | dependencies: 124 | locate-path "^2.0.0" 125 | 126 | for-each@~0.3.2: 127 | version "0.3.2" 128 | resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" 129 | dependencies: 130 | is-function "~1.0.0" 131 | 132 | foreach@^2.0.5: 133 | version "2.0.5" 134 | resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" 135 | 136 | fs.realpath@^1.0.0: 137 | version "1.0.0" 138 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 139 | 140 | function-bind@^1.0.2, function-bind@^1.1.0, function-bind@~1.1.0: 141 | version "1.1.0" 142 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" 143 | 144 | glob@^7.0.0, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: 145 | version "7.1.1" 146 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" 147 | dependencies: 148 | fs.realpath "^1.0.0" 149 | inflight "^1.0.4" 150 | inherits "2" 151 | minimatch "^3.0.2" 152 | once "^1.3.0" 153 | path-is-absolute "^1.0.0" 154 | 155 | graceful-fs@^4.1.2: 156 | version "4.1.11" 157 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 158 | 159 | "graceful-readlink@>= 1.0.0": 160 | version "1.0.1" 161 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 162 | 163 | has-ansi@^2.0.0: 164 | version "2.0.0" 165 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 166 | dependencies: 167 | ansi-regex "^2.0.0" 168 | 169 | has@^1.0.1, has@~1.0.1: 170 | version "1.0.1" 171 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" 172 | dependencies: 173 | function-bind "^1.0.2" 174 | 175 | inflight@^1.0.4: 176 | version "1.0.6" 177 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 178 | dependencies: 179 | once "^1.3.0" 180 | wrappy "1" 181 | 182 | inherits@2, inherits@~2.0.3: 183 | version "2.0.3" 184 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 185 | 186 | interpret@^1.0.0: 187 | version "1.0.2" 188 | resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.2.tgz#f4f623f0bb7122f15f5717c8e254b8161b5c5b2d" 189 | 190 | is-callable@^1.1.1, is-callable@^1.1.3: 191 | version "1.1.3" 192 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" 193 | 194 | is-date-object@^1.0.1: 195 | version "1.0.1" 196 | resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" 197 | 198 | is-function@~1.0.0: 199 | version "1.0.1" 200 | resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" 201 | 202 | is-object@~1.0.1: 203 | version "1.0.1" 204 | resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" 205 | 206 | is-regex@^1.0.3: 207 | version "1.0.4" 208 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" 209 | dependencies: 210 | has "^1.0.1" 211 | 212 | is-symbol@^1.0.1: 213 | version "1.0.1" 214 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" 215 | 216 | isstream@0.1.x: 217 | version "0.1.2" 218 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 219 | 220 | locate-path@^2.0.0: 221 | version "2.0.0" 222 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" 223 | dependencies: 224 | p-locate "^2.0.0" 225 | path-exists "^3.0.0" 226 | 227 | lodash@^4.14.0, lodash@^4.17.2: 228 | version "4.17.4" 229 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 230 | 231 | merge-descriptors@~1.0.0: 232 | version "1.0.1" 233 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 234 | 235 | minimatch@^3.0.2: 236 | version "3.0.3" 237 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" 238 | dependencies: 239 | brace-expansion "^1.0.0" 240 | 241 | minimist@0.0.8: 242 | version "0.0.8" 243 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 244 | 245 | minimist@~1.2.0: 246 | version "1.2.0" 247 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 248 | 249 | mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: 250 | version "0.5.1" 251 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 252 | dependencies: 253 | minimist "0.0.8" 254 | 255 | mock-bin@^1.0.1: 256 | version "1.0.1" 257 | resolved "https://registry.yarnpkg.com/mock-bin/-/mock-bin-1.0.1.tgz#1a41c86d052c1036eb81cda1fd1a76dec22851e0" 258 | dependencies: 259 | cmd-shim "^2.0.2" 260 | pify "^2.3.0" 261 | pinkie-promise "^2.0.1" 262 | shebang-regex "^2.0.0" 263 | shelljs "^0.7.0" 264 | temp-write "^2.1.0" 265 | 266 | module-not-found-error@^1.0.0: 267 | version "1.0.1" 268 | resolved "https://registry.yarnpkg.com/module-not-found-error/-/module-not-found-error-1.0.1.tgz#cf8b4ff4f29640674d6cdd02b0e3bc523c2bbdc0" 269 | 270 | object-inspect@~1.2.1: 271 | version "1.2.2" 272 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.2.2.tgz#c82115e4fcc888aea14d64c22e4f17f6a70d5e5a" 273 | 274 | object-keys@^1.0.8: 275 | version "1.0.11" 276 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" 277 | 278 | once@^1.3.0: 279 | version "1.4.0" 280 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 281 | dependencies: 282 | wrappy "1" 283 | 284 | os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: 285 | version "1.0.2" 286 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 287 | 288 | p-limit@^1.1.0: 289 | version "1.1.0" 290 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" 291 | 292 | p-locate@^2.0.0: 293 | version "2.0.0" 294 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" 295 | dependencies: 296 | p-limit "^1.1.0" 297 | 298 | path-exists@^3.0.0: 299 | version "3.0.0" 300 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 301 | 302 | path-is-absolute@^1.0.0: 303 | version "1.0.1" 304 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 305 | 306 | pify@^2.2.0, pify@^2.3.0: 307 | version "2.3.0" 308 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 309 | 310 | pinkie-promise@^2.0.0, pinkie-promise@^2.0.1: 311 | version "2.0.1" 312 | resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" 313 | dependencies: 314 | pinkie "^2.0.0" 315 | 316 | pinkie@^2.0.0: 317 | version "2.0.4" 318 | resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" 319 | 320 | proxyquire@^1.7.11: 321 | version "1.7.11" 322 | resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-1.7.11.tgz#13b494eb1e71fb21cc3ebe3699e637d3bec1af9e" 323 | dependencies: 324 | fill-keys "^1.0.2" 325 | module-not-found-error "^1.0.0" 326 | resolve "~1.1.7" 327 | 328 | rechoir@^0.6.2: 329 | version "0.6.2" 330 | resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" 331 | dependencies: 332 | resolve "^1.1.6" 333 | 334 | resolve@^1.1.6, resolve@~1.1.7: 335 | version "1.1.7" 336 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" 337 | 338 | resumer@~0.0.0: 339 | version "0.0.0" 340 | resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" 341 | dependencies: 342 | through "~2.3.4" 343 | 344 | rimraf@^2.6.1: 345 | version "2.6.1" 346 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" 347 | dependencies: 348 | glob "^7.0.5" 349 | 350 | shebang-regex@^2.0.0: 351 | version "2.0.0" 352 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-2.0.0.tgz#f500bf6851b61356236167de2cc319b0fd7f0681" 353 | 354 | shelljs@^0.7.0: 355 | version "0.7.7" 356 | resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" 357 | dependencies: 358 | glob "^7.0.0" 359 | interpret "^1.0.0" 360 | rechoir "^0.6.2" 361 | 362 | stack-trace@0.0.x: 363 | version "0.0.9" 364 | resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" 365 | 366 | string.prototype.trim@~1.1.2: 367 | version "1.1.2" 368 | resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" 369 | dependencies: 370 | define-properties "^1.1.2" 371 | es-abstract "^1.5.0" 372 | function-bind "^1.0.2" 373 | 374 | strip-ansi@^3.0.0: 375 | version "3.0.1" 376 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 377 | dependencies: 378 | ansi-regex "^2.0.0" 379 | 380 | supports-color@^2.0.0: 381 | version "2.0.0" 382 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 383 | 384 | tape@^4.6.3: 385 | version "4.6.3" 386 | resolved "https://registry.yarnpkg.com/tape/-/tape-4.6.3.tgz#637e77581e9ab2ce17577e9bd4ce4f575806d8b6" 387 | dependencies: 388 | deep-equal "~1.0.1" 389 | defined "~1.0.0" 390 | for-each "~0.3.2" 391 | function-bind "~1.1.0" 392 | glob "~7.1.1" 393 | has "~1.0.1" 394 | inherits "~2.0.3" 395 | minimist "~1.2.0" 396 | object-inspect "~1.2.1" 397 | resolve "~1.1.7" 398 | resumer "~0.0.0" 399 | string.prototype.trim "~1.1.2" 400 | through "~2.3.8" 401 | 402 | temp-write@^2.1.0: 403 | version "2.1.0" 404 | resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-2.1.0.tgz#59890918e0ef09d548aaa342f4bd3409d8404e96" 405 | dependencies: 406 | graceful-fs "^4.1.2" 407 | mkdirp "^0.5.0" 408 | os-tmpdir "^1.0.0" 409 | pify "^2.2.0" 410 | pinkie-promise "^2.0.0" 411 | uuid "^2.0.1" 412 | 413 | through@~2.3.4, through@~2.3.8: 414 | version "2.3.8" 415 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 416 | 417 | tmp@0.0.31: 418 | version "0.0.31" 419 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" 420 | dependencies: 421 | os-tmpdir "~1.0.1" 422 | 423 | uuid@^2.0.1: 424 | version "2.0.3" 425 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" 426 | 427 | winston@^2.3.1: 428 | version "2.3.1" 429 | resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.1.tgz#0b48420d978c01804cf0230b648861598225a119" 430 | dependencies: 431 | async "~1.0.0" 432 | colors "1.0.x" 433 | cycle "1.0.x" 434 | eyes "0.1.x" 435 | isstream "0.1.x" 436 | stack-trace "0.0.x" 437 | 438 | wrappy@1: 439 | version "1.0.2" 440 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 441 | --------------------------------------------------------------------------------