diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..8f132b8 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,14 @@ +version: "2" +checks: + method-complexity: + config: + threshold: 50 + method-lines: + config: + threshold: 100 +plugins: + eslint: + enabled: true + channel: "eslint-4" + config: + config: .eslintrc \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0cdec1a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +node_modules +*.log +*.json +*.lock +*.md \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..75dd586 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,102 @@ +{ + "env": { + "browser": false, + "node": true, + "es6": true, + "mocha": true + }, + "rules": { + //# Possible Errors + //# http://eslint.org/docs/rules/#possible-errors + "comma-dangle": [2, "only-multiline"], + "no-control-regex": 0, + "no-debugger": 2, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty-character-class": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-extra-semi": 2, + "no-func-assign": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-obj-calls": 2, + "no-proto": 2, + //"no-template-curly-in-string": 2, + "no-unexpected-multiline": 2, + "no-unreachable": 2, + //"no-unsafe-negation": 2, + "use-isnan": 2, + "valid-typeof": 2, + + //# Best Practices + //# http://eslint.org/docs/rules/#best-practices + "no-fallthrough": 2, + //"no-global-assign": 2, + "no-multi-spaces": 2, + "no-octal": 2, + "no-redeclare": 2, + "no-self-assign": 2, + "no-unused-labels": 2, + "max-lines": ["error", 1000], + + //# Strict Mode + //# http://eslint.org/docs/rules/#strict-mode + "strict": [2, "global"], + + //# Variables + //# http://eslint.org/docs/rules/#variables + "no-delete-var": 2, + "no-undef": 2, + "no-unused-vars": [2, {"args": "none"}], + + //# Node.js and CommonJS + //# http://eslint.org/docs/rules/#nodejs-and-commonjs + "no-mixed-requires": 2, + "no-new-require": 2, + "no-path-concat": 2, + "no-restricted-modules": [2, "sys", "_linklist"], + + //# Stylistic Issues + //# http://eslint.org/docs/rules/#stylistic-issues + "brace-style": [2, "1tbs", {"allowSingleLine": true}], + "comma-spacing": 2, + "eol-last": 2, + //"func-call-spacing": 2, + "key-spacing": [2, {"mode": "minimum"}], + "keyword-spacing": 2, + "max-len": [2, 150, 2], + "indent": ["error", 4], + "new-parens": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [2, {"max": 2}], + "no-trailing-spaces": 2, + "quotes": [2, "single", "avoid-escape"], + "semi": 2, + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "never"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": 2, + + //# ECMAScript 6 + //# http://eslint.org/docs/rules/#ecmascript-6 + "arrow-spacing": [2, {"before": true, "after": true}], + "constructor-super": 2, + "no-class-assign": 2, + "no-confusing-arrow": ["error", {"allowParens": true}], + "no-const-assign": 2, + "no-dupe-class-members": 2, + "no-new-symbol": 2, + "no-this-before-super": 2, + "prefer-const": 2, + "template-curly-spacing": 2 + }, + "globals": { + "util": true, + "i18n": true, + "container": true + } +} \ No newline at end of file diff --git a/README.md b/README.md index 21b60b0..288463f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) +[![npm version](https://badge.fury.io/js/serverless-step-functions-offline.svg)](https://badge.fury.io/js/serverless-step-functions-offline) +[![Known Vulnerabilities](https://snyk.io/test/github/vkkis93/serverless-step-functions-offline/badge.svg?targetFile=package.json)](https://snyk.io/test/github/vkkis93/serverless-step-functions-offline?targetFile=package.json) +[![Dependency Status](https://david-dm.org/vkkis93/serverless-step-functions-offline.svg)](https://david-dm.org/vkkis93/serverless-step-functions-offline) -# serverless-step-offline +# serverless-step-functions-offline -:warning: **Currently in beta** :warning: +## Documentation -# IMPORTANT: This plugin only works with *node.js* runtime and only with callback. It does not work with context object. +- [Install](#install) +- [Setup](#setup) +- [Requirements](#requirements) +- [Usage](#usage) +- [Run plugin](#run-plugin) +- [What does plugin support](#what-does-plugin-support) # Install Using NPM: @@ -103,12 +111,14 @@ By default `process.env.STEP_IS_OFFLINE = true`. | ***Pass*** | * | | ***Parallel*** | Only *Branches* -### Future plans - - Support context object - - Support fields *Retry*, *Catch* - - Support other languages except node.js - - Improve performance - - Fixing bugs +### TODOs + - [x] Support context object + - [x] Improve performance + - [x] Fixing bugs + - [ ] Add unit tests - to make plugin stable (next step) + - [ ] Support fields *Retry*, *Catch* + - [ ] Support other languages except node.js + If you have any questions, feel free to contact me: vkkis1993@gmail.com diff --git a/build.js b/build.js index eed1ef7..9ff2742 100644 --- a/build.js +++ b/build.js @@ -4,9 +4,9 @@ const moment = require('moment'); const _ = require('lodash'); const Promise = require('bluebird'); const enumList = require('./enum'); -let steps; module.exports = { + findFunctionsPathAndHandler() { for (const functionName in this.variables) { const functionHandler = this.variables[functionName]; @@ -17,8 +17,8 @@ module.exports = { }, _findFunctionPathAndHandler(functionHandler) { - const dir = path.dirname(functionHandler) - const handler = path.basename(functionHandler) + const dir = path.dirname(functionHandler); + const handler = path.basename(functionHandler); const splitHandler = handler.split('.'); const filePath = `${dir}/${splitHandler[0]}.js`; const handlerName = `${splitHandler[1]}`; @@ -28,13 +28,11 @@ module.exports = { buildStepWorkFlow() { this.cliLog('Building StepWorkFlow'); - - steps = []; - const states = this.stateDefinition.States; + this.contextObject = this.createContextObject(); + this.states = this.stateDefinition.States; return Promise.resolve() - .then(() => this._findNextStep(states, states[this.stateDefinition.StartAt], this.stateDefinition.StartAt)) - .then(() => this._run(steps[0].f(), this.eventFile, 0)) + .then(() => this.process(this.states[this.stateDefinition.StartAt], this.stateDefinition.StartAt, this.eventFile)) .then(() => this.cliLog('Serverless step function offline: Finished')) .catch(err => { console.log('OOPS', err.stack); @@ -43,110 +41,53 @@ module.exports = { }); }, - _run(f, event, index) { - // console.log('steps', steps); - return new Promise((resolve, reject) => { - if (!f) { - // end of states - return resolve(); - } - - f(event, null, (err, result) => { - if (err) { - throw `Error in function "${steps[index].name}": ${err}`; - } - - this._runNextStepFunction(result, index + 1, resolve); - }); - }).catch(err => { - throw err; - }); - }, - - _runNextStepFunction(result, index, resolve) { - if (!steps[index]) { - // end of states - return resolve(); + process(state, stateName, event) { + if (state && state.Type === 'Parallel') { + this.eventForParallelExecution = event; } - - if (steps[index].choice) { - // type: Choice - this._runChoice(steps[index], result, resolve, index); - } else if (steps[index].waitState) { - //type: Wait - return resolve(this._run(steps[index].f(result), result, index)); + const data = this._findStep(state, stateName); + if (!data || data instanceof Promise) {return data;} + if (data.choice) { + return this._runChoice(data, event); } else { - return resolve(this._run(steps[index].f(), result, index)); - } - }, - - _runChoice(typeChoice, result, resolve, index) { - let existsAnyMatches = false; - - //look through choice and find appropriate - _.forEach(typeChoice.choice, choice => { - //check if result from previous function has of value which described in Choice - if (!_.isNil(result[choice.variable])) { - //check condition - const isConditionTrue = choice.checkFunction(result[choice.variable], choice.compareWithValue); - if (isConditionTrue) { - existsAnyMatches = true; - // if exists run appropriate function - if (!choice.f) { - console.log('PLEASE LOOK HERE'); - // return resolve(this._run(steps[index + 1], result, index + 1)); - } - const indexFunction = _.findIndex(steps, (step) => step.name === choice.f.name); - if (indexFunction > -1) { - return resolve(this._run(steps[indexFunction].f(), result, indexFunction)); - } else { - return resolve(this._run(choice.f.f(), result, index)); - - } - } - } - }); - if (!existsAnyMatches && typeChoice.defaultFunction) { - const indexFunction = _.findIndex(steps, (step) => step.name === typeChoice.defaultFunction.name); - if (indexFunction > -1) { - return resolve(this._run(steps[indexFunction].f(), result, indexFunction)); - } else { - return resolve(this._run(typeChoice.defaultFunction.f(), result, index)); - } + return this._run(data.f(event), event); } }, - _findNextStep(allStates, currentState, currentStateName) { + _findStep(currentState, currentStateName) { // it means end of states - if (!currentState) return Promise.resolve(); - let nextStateName = currentState.Next; - if (this._switcherByType(allStates, currentState, currentStateName)) { - steps.push(this._switcherByType(allStates, currentState, currentStateName)); - if (currentState.Type === 'Choice') { - const stateNames = Object.keys(allStates); - var index = stateNames.indexOf(currentStateName); - const nextStateName = stateNames[index + 1]; - return this._findNextStep(allStates, allStates[nextStateName], nextStateName); - } - } - return this._findNextStep(allStates, allStates[nextStateName], nextStateName); + if (!currentState) {return;} + this.currentState = currentState; + return this._switcherByType(currentState, currentStateName); }, - _switcherByType(allStates, currentState, currentStateName) { + _run(f, event) { + return new Promise((resolve, reject) => { + if (!f) return Promise.resolve();// end of states + f(event, this.contextObject, this.contextObject.done); + }).catch(err => { + throw err; + }); + }, + + _switcherByType(currentState, currentStateName) { switch (currentState.Type) { case 'Task': // just push task to general array return { name: currentStateName, f: () => require(path.join(process.cwd(), this.variables[currentStateName].filePath))[this.variables[currentStateName].handler] }; - // return this.caseTask(allStates, currentState, currentStateName); - break; case 'Parallel': // look through branches and push all of them + this.eventParallelResult = []; _.forEach(currentState.Branches, (branch) => { - this._findNextStep(branch.States, branch.States[branch.StartAt], branch.StartAt); + this.parallelBranch = branch; + return this.process(branch.States[branch.StartAt], branch.StartAt, this.eventForParallelExecution); }); - break; + this.process(this.states[currentState.Next], currentState.Next, this.eventParallelResult); + delete this.parallelBranch; + delete this.eventParallelResult; + return; case 'Choice': //push all choices. but need to store information like // 1) on which variable need to look: ${variable} @@ -172,15 +113,14 @@ module.exports = { checkFunction, compareWithValue }; - choiceObj.f = this._switcherByType(allStates, allStates[choice.Next], choice.Next); + choiceObj.choiceFunction = choice.Next; choiceConditional.choice.push(choiceObj); }); // if exists default function - store it if (currentState.Default) { - choiceConditional.defaultFunction = this._switcherByType(allStates, allStates[currentState.Default], currentState.Default); + choiceConditional.defaultFunction = currentState.Default; } return choiceConditional; - break; case 'Wait': // Wait State // works with parameter: seconds, timestamp, timestampPath, secondsPath; @@ -192,24 +132,46 @@ module.exports = { return (arg1, arg2, cb) => { setTimeout(() => { cb(null, event); - }, waitTimer * 1000) - } + }, waitTimer * 1000); + }; } }; - break; case 'Pass': - return; - break; + return {f: () => this.cliLog('PASS STATE')}; } return; }, + _runChoice(data, result) { + let existsAnyMatches = false; + + //look through choice and find appropriate + _.forEach(data.choice, choice => { + //check if result from previous function has of value which described in Choice + if (!_.isNil(result[choice.variable])) { + //check condition + const isConditionTrue = choice.checkFunction(result[choice.variable], choice.compareWithValue); + if (isConditionTrue) { + existsAnyMatches = true; + return this.process(this.states[choice.choiceFunction], choice.choiceFunction, result); + } + } + }); + if (!existsAnyMatches && data.defaultFunction) { + const fName = data.defaultFunction; + return this.process(this.states[fName], fName, result); + } + }, + _waitState(event, currentState, currentStateName) { let waitTimer = 0, targetTime, timeDiff; const currentTime = moment(); - const waitField = _.omit(currentState, 'Type', 'Next'); - if (!_.has(waitField, ['Seconds', 'Timestamp', 'TimestampPath', 'SecondsPath'])) { - this.cliLog('!!!WAIT STATE!!!!') + const waitListKeys = ['Seconds', 'Timestamp', 'TimestampPath', 'SecondsPath']; + const waitField = _.omit(currentState, 'Type', 'Next', 'Result'); + const waitKey = Object.keys(waitField)[0]; + if (!waitListKeys.includes(waitKey)) { + this.cliLog(`Plugin does not support wait operator "${waitKey}"`); + process.exit(); } switch (Object.keys(waitField)[0]) { case 'Seconds': @@ -223,7 +185,10 @@ module.exports = { case 'TimestampPath': const timestampPath = waitField['TimestampPath'].split('$.')[1]; if (!event[timestampPath]) { - this.cliLog(`An error occurred while executing the state ${currentStateName}. The TimestampPath parameter does not reference an input value: ${waitField['TimestampPath']}`); + this.cliLog( + `An error occurred while executing the state ${currentStateName}. + The TimestampPath parameter does not reference an input value: ${waitField['TimestampPath']}` + ); process.exit(1); } targetTime = moment(event[timestampPath]); @@ -234,7 +199,10 @@ module.exports = { const secondsPath = waitField['SecondsPath'].split('$.')[1]; const waitSeconds = event[secondsPath]; if (!waitSeconds) { - this.cliLog(`An error occurred while executing the state ${currentStateName}. The TimestampPath parameter does not reference an input value: ${waitField['SecondsPath']}`); + this.cliLog(` + An error occurred while executing the state ${currentStateName}. + The TimestampPath parameter does not reference an input value: ${waitField['SecondsPath']}` + ); process.exit(1); } waitTimer = waitSeconds; @@ -242,4 +210,28 @@ module.exports = { } return waitTimer; }, + + createContextObject() { + const cb = (err, result) => { + return new Promise((resolve, reject) => { + if (err) { + throw `Error in function "${this.currentState.name}": ${JSON.stringify(err)}`; //;TODO NAME + } + let state = this.states; + if (this.parallelBranch && this.parallelBranch.States) { + state = this.parallelBranch.States; + if (!this.currentState.Next) this.eventParallelResult.push(result); //it means the end of execution of branch + } + this.process(state[this.currentState.Next], this.currentState.Next, result); + }); + }; + + return { + cb: cb, + done: cb, + succeed: (result) => cb(null, result), + fail: (err) => cb(err) + }; + + } }; diff --git a/enum.js b/enum.js index 3e5b0aa..80951c3 100644 --- a/enum.js +++ b/enum.js @@ -1,61 +1,63 @@ 'use strict'; const _ = require('lodash'); -module.exports.comparisonOperators = [ - 'And', - 'BooleanEquals', - 'Not', - 'NumericEquals', - 'NumericGreaterThan', - 'NumericGreaterThanEquals', - 'NumericLessThan', - 'NumericLessThanEquals', - 'Or', - 'StringEquals', - 'StringGreaterThan', - 'StringGreaterThanEquals', - 'StringLessThan', - 'StringLessThanEquals', - 'TimestampEquals', - 'TimestampGreaterThan', - 'TimestampGreaterThanEquals', - 'TimestampLessThan', - 'TimestampLessThanEquals' -]; +module.exports = { + comparisonOperators: [ + 'And', + 'BooleanEquals', + 'Not', + 'NumericEquals', + 'NumericGreaterThan', + 'NumericGreaterThanEquals', + 'NumericLessThan', + 'NumericLessThanEquals', + 'Or', + 'StringEquals', + 'StringGreaterThan', + 'StringGreaterThanEquals', + 'StringLessThan', + 'StringLessThanEquals', + 'TimestampEquals', + 'TimestampGreaterThan', + 'TimestampGreaterThanEquals', + 'TimestampLessThan', + 'TimestampLessThanEquals' + ], -module.exports.supportedComparisonOperator = [ - 'BooleanEquals', - 'NumericEquals', - 'NumericGreaterThan', - 'NumericGreaterThanEquals', - 'NumericLessThan', - 'NumericLessThanEquals', - 'StringEquals', - 'StringGreaterThan', - 'StringGreaterThanEquals', - 'StringLessThan', - 'StringLessThanEquals', - 'TimestampEquals', - 'TimestampGreaterThan', - 'TimestampGreaterThanEquals', - 'TimestampLessThan', - 'TimestampLessThanEquals' -]; + supportedComparisonOperator: [ + 'BooleanEquals', + 'NumericEquals', + 'NumericGreaterThan', + 'NumericGreaterThanEquals', + 'NumericLessThan', + 'NumericLessThanEquals', + 'StringEquals', + 'StringGreaterThan', + 'StringGreaterThanEquals', + 'StringLessThan', + 'StringLessThanEquals', + 'TimestampEquals', + 'TimestampGreaterThan', + 'TimestampGreaterThanEquals', + 'TimestampLessThan', + 'TimestampLessThanEquals' + ], -module.exports.convertOperator = { - 'BooleanEquals': (value, other) => _.eq(value, other), - 'NumericEquals': (value, other) => _.eq(value, other), - 'NumericGreaterThan': (value, other) => _.gt(value, other), - 'NumericGreaterThanEquals': (value, other) => _.gte(value, other), - 'NumericLessThan': (value, other) => _.lt(value, other), - 'NumericLessThanEquals': (value, other) => _.lte(value, other), - 'StringEquals': (value, other) => _.eq(value, other), - 'StringGreaterThan': (value, other) => _.gt(value, other), - 'StringGreaterThanEquals': (value, other) => _.gte(value, other), - 'StringLessThan': (value, other) => _.lt(value, other), - 'StringLessThanEquals': (value, other) => _.lte(value, other), - 'TimestampEquals': (value, other) => _.eq(value, other), - 'TimestampGreaterThan': (value, other) => _.gt(value, other), - 'TimestampGreaterThanEquals': (value, other) => _.gte(value, other), - 'TimestampLessThan': (value, other) => _.lt(value, other), - 'TimestampLessThanEquals': (value, other) => _.lte(value, other) -}; \ No newline at end of file + convertOperator: { + 'BooleanEquals': (value, other) => _.eq(value, other), + 'NumericEquals': (value, other) => _.eq(value, other), + 'NumericGreaterThan': (value, other) => _.gt(value, other), + 'NumericGreaterThanEquals': (value, other) => _.gte(value, other), + 'NumericLessThan': (value, other) => _.lt(value, other), + 'NumericLessThanEquals': (value, other) => _.lte(value, other), + 'StringEquals': (value, other) => _.eq(value, other), + 'StringGreaterThan': (value, other) => _.gt(value, other), + 'StringGreaterThanEquals': (value, other) => _.gte(value, other), + 'StringLessThan': (value, other) => _.lt(value, other), + 'StringLessThanEquals': (value, other) => _.lte(value, other), + 'TimestampEquals': (value, other) => _.eq(value, other), + 'TimestampGreaterThan': (value, other) => _.gt(value, other), + 'TimestampGreaterThanEquals': (value, other) => _.gte(value, other), + 'TimestampLessThan': (value, other) => _.lt(value, other), + 'TimestampLessThanEquals': (value, other) => _.lte(value, other) + } +}; diff --git a/index.js b/index.js index 199eb77..098faae 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ +'use strict'; const parse = require('./parse'); const build = require('./build'); const _ = require('lodash'); -const fs = require('fs'); const path = require('path'); class StepFunctionsOfflinePlugin { @@ -21,7 +21,7 @@ class StepFunctionsOfflinePlugin { ); this.commands = { 'step-functions-offline': { - usage: "Will run your step function locally", + usage: 'Will run your step function locally', lifecycleEvents: [ 'start', 'isInstalledPluginSLSStepFunctions', @@ -79,7 +79,7 @@ class StepFunctionsOfflinePlugin { this.stateDefinition = this._findState(yaml, this.stateMachine); }).catch(err => { throw new this.serverless.classes.Error(err); - }) + }); } isInstalledPluginSLSStepFunctions() { @@ -93,7 +93,7 @@ class StepFunctionsOfflinePlugin { loadEventFile() { if (!this.eventFile) { - return this.eventFile = {}; + return this.eventFile = {}; } try { diff --git a/package.json b/package.json index 5f665ac..edaa382 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-step-functions-offline", - "version": "0.0.8", + "version": "1.0.0-rc.1", "description": "Serverlesss plugin to support step function offline", "main": "index.js", "repository": { @@ -8,10 +8,14 @@ "url": "git@github.com:vkkis93/serverless-step-functions-offline.git" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "lint": "eslint ./** --ext .js" }, "keywords": [ - "serverless", "step functions", "serverless-step-functions-offline" + "aws", + "lambda", + "serverless", + "step functions", + "serverless-step-functions-offline" ], "author": "viktor.kis ", "bugs": { @@ -22,6 +26,12 @@ "dependencies": { "bluebird": "^3.5.1", "lodash": "^4.17.4", - "moment": "^2.19.2" - } + "moment": "^2.20.1" + }, + "devDependencies": { + "pre-commit": "^1.1.3" + }, + "pre-commit": [ + "lint" + ] } diff --git a/yarn.lock b/yarn.lock index 25bab31..92e19b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6,10 +6,132 @@ bluebird@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" +concat-stream@^1.4.7: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + lodash@^4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + moment@^2.19.2: version "2.19.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" + +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + +pre-commit@^1.1.3: + version "1.2.2" + resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6" + dependencies: + cross-spawn "^5.0.1" + spawn-sync "^1.0.15" + which "1.2.x" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +readable-stream@^2.2.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +which@1.2.x: + version "1.2.14" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" + dependencies: + isexe "^2.0.0" + +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"