diff --git a/@commitlint/cz-commitlint/package.json b/@commitlint/cz-commitlint/package.json index fca37a0664..788d02e1c3 100644 --- a/@commitlint/cz-commitlint/package.json +++ b/@commitlint/cz-commitlint/package.json @@ -37,6 +37,7 @@ } }, "dependencies": { + "@commitlint/ensure": "^13.2.0", "@commitlint/load": "^13.2.1", "@commitlint/types": "^13.2.0", "chalk": "^4.1.0", @@ -48,6 +49,7 @@ "inquirer": "^8.0.0" }, "devDependencies": { - "@types/inquirer": "^8.0.0" + "@types/inquirer": "^8.0.0", + "commitizen": "^4.2.4" } } diff --git a/@commitlint/cz-commitlint/src/Question.test.ts b/@commitlint/cz-commitlint/src/Question.test.ts index 7a3cfb7d5b..18516eda48 100644 --- a/@commitlint/cz-commitlint/src/Question.test.ts +++ b/@commitlint/cz-commitlint/src/Question.test.ts @@ -15,6 +15,11 @@ const QUESTION_CONFIG = { messages: MESSAGES, }; +const caseFn = (input: string | string[], delimiter?: string) => + (Array.isArray(input) ? input : [input]) + .map((segment) => segment[0].toUpperCase() + segment.slice(1)) + .join(delimiter); + describe('name', () => { test('should throw error when name is not a meaningful string', () => { expect( @@ -47,7 +52,7 @@ describe('name', () => { }); describe('type', () => { - test('should return "list" type when enumList is array', () => { + test('should return "list" type when enumList is array and multipleSelectDefaultDelimiter is undefined', () => { const question = new Question('scope', { ...QUESTION_CONFIG, enumList: ['cli', 'core'], @@ -57,6 +62,17 @@ describe('type', () => { expect(question).not.toHaveProperty('transformer'); }); + test('should return "checkbox" type when enumList is array and multipleSelectDefaultDelimiter is defined', () => { + const question = new Question('scope', { + ...QUESTION_CONFIG, + enumList: ['cli', 'core'], + multipleSelectDefaultDelimiter: ',', + }).question; + expect(question).toHaveProperty('type', 'checkbox'); + expect(question).toHaveProperty('choices', ['cli', 'core']); + expect(question).not.toHaveProperty('transformer'); + }); + test('should contain "skip" list item when enumList is array and skip is true', () => { const question = new Question('scope', { ...QUESTION_CONFIG, @@ -184,13 +200,46 @@ describe('filter', () => { test('should auto fix case and full-stop', () => { const question = new Question('body', { ...QUESTION_CONFIG, - caseFn: (input: string) => input[0].toUpperCase() + input.slice(1), + caseFn, fullStopFn: (input: string) => input + '!', }).question; expect(question.filter?.('xxxx', {})).toBe('Xxxx!'); }); + test('should transform each item with same case when input is array', () => { + const question = new Question('body', { + ...QUESTION_CONFIG, + caseFn, + fullStopFn: (input: string) => input + '!', + }).question; + + expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx,Yyyy!'); + }); + + test('should concat items with multipleSelectDefaultDelimiter when input is array', () => { + const question = new Question('body', { + ...QUESTION_CONFIG, + caseFn, + fullStopFn: (input: string) => input + '!', + multipleSelectDefaultDelimiter: '|', + }).question; + + expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx|Yyyy!'); + }); + + test('should split the string to items when multipleValueDelimiters is defined', () => { + const question = new Question('body', { + ...QUESTION_CONFIG, + caseFn, + fullStopFn: (input: string) => input + '!', + multipleValueDelimiters: /,|\|/g, + }).question; + + expect(question.filter?.('xxxx,yyyy|zzzz', {})).toBe('Xxxx,Yyyy|Zzzz!'); + expect(question.filter?.('xxxx-yyyy-zzzz', {})).toBe('Xxxx-yyyy-zzzz!'); + }); + test('should works well when does not pass caseFn/fullStopFn', () => { const question = new Question('body', { ...QUESTION_CONFIG, @@ -252,7 +301,7 @@ describe('transformer', () => { test('should auto transform case and full-stop', () => { const question = new Question('body', { ...QUESTION_CONFIG, - caseFn: (input: string) => input[0].toUpperCase() + input.slice(1), + caseFn, fullStopFn: (input: string) => input + '!', }).question; diff --git a/@commitlint/cz-commitlint/src/Question.ts b/@commitlint/cz-commitlint/src/Question.ts index 109db59ef8..5ed72223a4 100644 --- a/@commitlint/cz-commitlint/src/Question.ts +++ b/@commitlint/cz-commitlint/src/Question.ts @@ -16,6 +16,8 @@ export type QuestionConfig = { name: string; value: string; }> | null; + multipleValueDelimiters?: RegExp; + multipleSelectDefaultDelimiter?: string; fullStopFn?: FullStopFn; caseFn?: CaseFn; }; @@ -29,6 +31,8 @@ export default class Question { private title: string; private caseFn: CaseFn; private fullStopFn: FullStopFn; + private multipleValueDelimiters?: RegExp; + private multipleSelectDefaultDelimiter?: string; constructor( name: PromptName, { @@ -42,6 +46,8 @@ export default class Question { caseFn, maxLength, minLength, + multipleValueDelimiters, + multipleSelectDefaultDelimiter, }: QuestionConfig ) { if (!name || typeof name !== 'string') @@ -53,11 +59,16 @@ export default class Question { this.title = title ?? ''; this.skip = skip ?? false; this.fullStopFn = fullStopFn ?? ((_: string) => _); - this.caseFn = caseFn ?? ((_: string) => _); + this.caseFn = + caseFn ?? + ((input: string | string[], delimiter?: string) => + Array.isArray(input) ? input.join(delimiter) : input); + this.multipleValueDelimiters = multipleValueDelimiters; + this.multipleSelectDefaultDelimiter = multipleSelectDefaultDelimiter; if (enumList && Array.isArray(enumList)) { this._question = { - type: 'list', + type: multipleSelectDefaultDelimiter ? 'checkbox' : 'list', choices: skip ? [ ...enumList, @@ -140,8 +151,25 @@ export default class Question { return true; } - protected filter(input: string): string { - return this.caseFn(this.fullStopFn(input)); + protected filter(input: string | string[]): string { + let toCased; + if (Array.isArray(input)) { + toCased = this.caseFn(input, this.multipleSelectDefaultDelimiter); + } else if (this.multipleValueDelimiters) { + const segments = input.split(this.multipleValueDelimiters); + const casedString = this.caseFn(segments, ','); + const casedSegments = casedString.split(','); + toCased = input.replace( + new RegExp(`[^${this.multipleValueDelimiters.source}]+`, 'g'), + (segment) => { + return casedSegments[segments.indexOf(segment)]; + } + ); + } else { + toCased = this.caseFn(input); + } + + return this.fullStopFn(toCased); } protected transformer(input: string, _answers: Answers): string { @@ -154,7 +182,7 @@ export default class Question { output.length <= this.maxLength && output.length >= this.minLength ? chalk.green : chalk.red; - return color('(' + output.length + ') ' + input); + return color('(' + output.length + ') ' + output); } protected decorateMessage(_answers: Answers): string { diff --git a/@commitlint/cz-commitlint/src/SectionHeader.test.ts b/@commitlint/cz-commitlint/src/SectionHeader.test.ts index 90fe7358be..6da978ff92 100644 --- a/@commitlint/cz-commitlint/src/SectionHeader.test.ts +++ b/@commitlint/cz-commitlint/src/SectionHeader.test.ts @@ -1,7 +1,16 @@ import {RuleConfigSeverity} from '@commitlint/types'; -import {combineCommitMessage, getQuestions} from './SectionHeader'; +import { + combineCommitMessage, + getQuestions, + getQuestionConfig, +} from './SectionHeader'; import {setPromptConfig} from './store/prompts'; import {setRules} from './store/rules'; + +beforeEach(() => { + setRules({}); + setPromptConfig({}); +}); describe('getQuestions', () => { test("should contain 'type','scope','subject'", () => { const questions = getQuestions(); @@ -36,6 +45,31 @@ describe('getQuestions', () => { }); }); +describe('getQuestionConfig', () => { + test("should 'scope' supports multiple items separated with ',\\/'", () => { + const config = getQuestionConfig('scope'); + expect(config).toEqual( + expect.objectContaining({ + multipleValueDelimiters: /\/|\\|,/g, + }) + ); + }); + + test("should 'scope' supports multiple select separated with settings.scopeEnumSeparator", () => { + setPromptConfig({ + settings: { + scopeEnumSeparator: '/', + }, + }); + const config = getQuestionConfig('scope'); + expect(config).toEqual( + expect.objectContaining({ + multipleSelectDefaultDelimiter: '/', + }) + ); + }); +}); + describe('combineCommitMessage', () => { test('should return correct string when type,scope,subject are not empty', () => { const commitMessage = combineCommitMessage({ diff --git a/@commitlint/cz-commitlint/src/SectionHeader.ts b/@commitlint/cz-commitlint/src/SectionHeader.ts index 9905ddcc33..dfc71d80de 100644 --- a/@commitlint/cz-commitlint/src/SectionHeader.ts +++ b/@commitlint/cz-commitlint/src/SectionHeader.ts @@ -2,6 +2,7 @@ import {PromptName, RuleField} from '@commitlint/types'; import {Answers, DistinctQuestion} from 'inquirer'; import Question, {QuestionConfig} from './Question'; import getRuleQuestionConfig from './services/getRuleQuestionConfig'; +import {getPromptSettings} from './store/prompts'; export class HeaderQuestion extends Question { headerMaxLength: number; @@ -47,8 +48,13 @@ export function getQuestions(): Array { } headerRuleFields.forEach((name) => { - const questionConfig = getRuleQuestionConfig(name); + const questionConfig = getQuestionConfig(name); if (questionConfig) { + if (name === 'scope') { + questionConfig.multipleSelectDefaultDelimiter = + getPromptSettings()['scopeEnumSeparator']; + questionConfig.multipleValueDelimiters = /\/|\\|,/g; + } const instance = new HeaderQuestion( name, questionConfig, @@ -60,3 +66,19 @@ export function getQuestions(): Array { }); return questions; } + +export function getQuestionConfig( + name: RuleField +): ReturnType { + const questionConfig = getRuleQuestionConfig(name); + + if (questionConfig) { + if (name === 'scope') { + questionConfig.multipleSelectDefaultDelimiter = + getPromptSettings()['scopeEnumSeparator']; + questionConfig.multipleValueDelimiters = /\/|\\|,/g; + } + } + + return questionConfig; +} diff --git a/@commitlint/cz-commitlint/src/store/defaultPromptConfigs.ts b/@commitlint/cz-commitlint/src/store/defaultPromptConfigs.ts index 99c649a1e5..f2824dd2cf 100644 --- a/@commitlint/cz-commitlint/src/store/defaultPromptConfigs.ts +++ b/@commitlint/cz-commitlint/src/store/defaultPromptConfigs.ts @@ -1,4 +1,7 @@ export default { + settings: { + scopeEnumSeparator: ',', + }, messages: { skip: '(press enter to skip)', max: '(max %d chars)', diff --git a/@commitlint/cz-commitlint/src/store/prompts.test.ts b/@commitlint/cz-commitlint/src/store/prompts.test.ts index b1584d6d4b..799eb69d08 100644 --- a/@commitlint/cz-commitlint/src/store/prompts.test.ts +++ b/@commitlint/cz-commitlint/src/store/prompts.test.ts @@ -2,10 +2,12 @@ import * as prompts from './prompts'; let getPromptQuestions: typeof prompts.getPromptQuestions; let getPromptMessages: typeof prompts.getPromptMessages; +let getPromptSettings: typeof prompts.getPromptSettings; let setPromptConfig: typeof prompts.setPromptConfig; beforeEach(() => { jest.resetModules(); + getPromptSettings = require('./prompts').getPromptSettings; getPromptMessages = require('./prompts').getPromptMessages; getPromptQuestions = require('./prompts').getPromptQuestions; setPromptConfig = require('./prompts').setPromptConfig; @@ -106,4 +108,26 @@ describe('setPromptConfig', () => { }); expect(getPromptMessages()).toEqual(initialMessages); }); + + test('should settings scopeEnumSeparator be set when value is ",\\/"', () => { + setPromptConfig({ + settings: { + scopeEnumSeparator: '/', + }, + }); + expect(getPromptSettings()).toEqual({ + scopeEnumSeparator: '/', + }); + + const processExit = jest + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + setPromptConfig({ + settings: { + scopeEnumSeparator: '-', + }, + }); + expect(processExit).toHaveBeenCalledWith(1); + processExit.mockClear(); + }); }); diff --git a/@commitlint/cz-commitlint/src/store/prompts.ts b/@commitlint/cz-commitlint/src/store/prompts.ts index 649cd8d804..8e97f8ff89 100644 --- a/@commitlint/cz-commitlint/src/store/prompts.ts +++ b/@commitlint/cz-commitlint/src/store/prompts.ts @@ -11,7 +11,7 @@ const store: { }; export function setPromptConfig(newPromptConfig: UserPromptConfig): void { - const {messages, questions} = newPromptConfig; + const {settings, messages, questions} = newPromptConfig; if (messages) { const requiredMessageKeys = Object.keys(defaultPromptConfigs.messages); requiredMessageKeys.forEach((key: string) => { @@ -25,6 +25,22 @@ export function setPromptConfig(newPromptConfig: UserPromptConfig): void { if (questions && isPlainObject(questions)) { store[storeKey]['questions'] = questions; } + + if (settings && isPlainObject(settings)) { + if ( + settings['scopeEnumSeparator'] && + !/^\/|\\|,$/.test(settings['scopeEnumSeparator']) + ) { + console.log( + `prompt.settings.scopeEnumSeparator must be one of ',', '\\', '/'.` + ); + process.exit(1); + } + store[storeKey]['settings'] = { + ...defaultPromptConfigs.settings, + ...settings, + }; + } } export function getPromptMessages(): Readonly { @@ -34,3 +50,7 @@ export function getPromptMessages(): Readonly { export function getPromptQuestions(): Readonly { return (store[storeKey] && store[storeKey]['questions']) ?? {}; } + +export function getPromptSettings(): Readonly { + return (store[storeKey] && store[storeKey]['settings']) ?? {}; +} diff --git a/@commitlint/cz-commitlint/src/utils/case-fn.test.ts b/@commitlint/cz-commitlint/src/utils/case-fn.test.ts index 038f70589f..a2ff502ba0 100644 --- a/@commitlint/cz-commitlint/src/utils/case-fn.test.ts +++ b/@commitlint/cz-commitlint/src/utils/case-fn.test.ts @@ -1,7 +1,7 @@ import {RuleConfigSeverity} from '@commitlint/types'; import getCaseFn from './case-fn'; -test('should not apply', () => { +test('should not transform when rule is disabled', () => { let rule = getCaseFn([RuleConfigSeverity.Disabled]); expect(rule('test')).toBe('test'); expect(rule('test-foo')).toBe('test-foo'); @@ -19,29 +19,17 @@ test('should not apply', () => { expect(rule('test-foo')).toBe('test-foo'); expect(rule('testFoo')).toBe('testFoo'); expect(rule('TEST_FOO')).toBe('TEST_FOO'); - - rule = getCaseFn([ - RuleConfigSeverity.Warning, - 'always', - ['camel-case', 'lowercase'], - ]); - expect(rule('test')).toBe('test'); - expect(rule('test-foo')).toBe('test-foo'); - expect(rule('testFoo')).toBe('testFoo'); - expect(rule('TEST_FOO')).toBe('TEST_FOO'); }); test('should throw error on invalid casing', () => { - expect(() => getCaseFn([RuleConfigSeverity.Warning, 'always'])).toThrow( - 'Unknown target case "undefined"' - ); + let rule = getCaseFn([RuleConfigSeverity.Warning, 'always']); + expect(() => rule('test')).toThrow('Unknown target case "undefined"'); - expect(() => - getCaseFn([RuleConfigSeverity.Warning, 'always', 'foo']) - ).toThrow('Unknown target case "foo"'); + rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'foo']); + expect(() => rule('test')).toThrow('Unknown target case "foo"'); }); -test('should convert text correctly', () => { +test('should transform text correctly with single case', () => { let rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'camel-case']); expect(rule('TEST_FOOBar-baz baz')).toBe('testFooBarBazBaz'); @@ -64,10 +52,10 @@ test('should convert text correctly', () => { expect(rule('TEST_FOOBar-baz baz')).toBe('TEST_FOOBAR-BAZ BAZ'); rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'sentence-case']); - expect(rule('TEST_FOOBar-baz baz')).toBe('Test_foobar-baz baz'); + expect(rule('tEST_FOOBar-baz baz')).toBe('TEST_FOOBar-baz baz'); rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'sentencecase']); - expect(rule('TEST_FOOBar-baz baz')).toBe('Test_foobar-baz baz'); + expect(rule('tEST_FOOBar-baz baz')).toBe('TEST_FOOBar-baz baz'); rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lower-case']); expect(rule('TEST_FOOBar-baz baz')).toBe('test_foobar-baz baz'); @@ -77,4 +65,27 @@ test('should convert text correctly', () => { rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lowerCase']); expect(rule('TEST_FOOBar-baz baz')).toBe('test_foobar-baz baz'); + + rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lowerCase']); + expect(rule(['TEST_FOOBar-baz', 'bAz'])).toBe('test_foobar-baz,baz'); + + rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lowerCase']); + expect(rule(['TEST_FOOBar-baz', 'bAz'], '|')).toBe('test_foobar-baz|baz'); +}); + +test('should transform text correctly with multiple cases', () => { + const rule = getCaseFn([ + RuleConfigSeverity.Warning, + 'always', + ['camel-case', 'lowercase'], + ]); + expect(rule('test')).toBe('test'); + expect(rule('test-foo')).toBe('test-foo'); + expect(rule('testFoo')).toBe('testFoo'); + expect(rule('TEST_FOO')).toBe('testFoo'); + + expect(rule(['testFoo', 'test_foo'])).toBe('testFoo,testFoo'); + expect(rule(['TEST_foo', 'test_foo'])).toBe('test_foo,test_foo'); + expect(rule(['TEST_FOO', 'Test_foo'])).toBe('testFoo,testFoo'); + expect(rule(['TEST_FOO', 'Test_foo'], '|')).toBe('testFoo|testFoo'); }); diff --git a/@commitlint/cz-commitlint/src/utils/case-fn.ts b/@commitlint/cz-commitlint/src/utils/case-fn.ts index 0bb098c7bf..f51b39c7be 100644 --- a/@commitlint/cz-commitlint/src/utils/case-fn.ts +++ b/@commitlint/cz-commitlint/src/utils/case-fn.ts @@ -1,12 +1,15 @@ +import _ from 'lodash'; import camelCase from 'lodash/camelCase'; import kebabCase from 'lodash/kebabCase'; import snakeCase from 'lodash/snakeCase'; import startCase from 'lodash/startCase'; import upperFirst from 'lodash/upperFirst'; -import {Rule} from '../types'; import {ruleIsActive, ruleIsNotApplicable} from './rules'; +import {TargetCaseType} from '@commitlint/types'; +import {case as ensureCase} from '@commitlint/ensure'; +import {Rule} from '../types'; -export type CaseFn = (input: string) => string; +export type CaseFn = (input: string | string[], delimiter?: string) => string; /** * Get forced case for rule @@ -14,40 +17,59 @@ export type CaseFn = (input: string) => string; * @return transform function applying the enforced case */ export default function getCaseFn(rule?: Rule): CaseFn { - const noop = (input: string) => input; + const noop = (input: string | string[], delimiter?: string) => + Array.isArray(input) ? input.join(delimiter) : input; if (!rule || !ruleIsActive(rule) || ruleIsNotApplicable(rule)) { return noop; } - const target = rule[2]; + const value = rule[2]; - if (Array.isArray(target)) { - return noop; - } + const caseList = Array.isArray(value) ? value : [value]; + + return (input: string | string[], delimiter?: string) => { + let matchedCase: TargetCaseType = caseList[0]; + const segments = Array.isArray(input) ? input : [input]; + + for (const segment of segments) { + const check = caseList.find((a) => ensureCase(segment, a)); + if (check) { + matchedCase = check; + break; + } + } + + return segments + .map((segment) => { + return toCase(segment, matchedCase); + }) + .join(delimiter); + }; +} +function toCase(input: string, target: TargetCaseType): string { switch (target) { case 'camel-case': - return (input: string) => camelCase(input); + return camelCase(input); case 'kebab-case': - return (input: string) => kebabCase(input); + return kebabCase(input); case 'snake-case': - return (input: string) => snakeCase(input); + return snakeCase(input); case 'pascal-case': - return (input: string) => upperFirst(camelCase(input)); + return upperFirst(camelCase(input)); case 'start-case': - return (input: string) => startCase(input); + return startCase(input); case 'upper-case': case 'uppercase': - return (input: string) => input.toUpperCase(); + return input.toUpperCase(); case 'sentence-case': case 'sentencecase': - return (input: string) => - `${input.charAt(0).toUpperCase()}${input.substring(1).toLowerCase()}`; + return input.charAt(0).toUpperCase() + input.slice(1); case 'lower-case': case 'lowercase': case 'lowerCase': // Backwards compat config-angular v4 - return (input: string) => input.toLowerCase(); + return input.toLowerCase(); default: throw new TypeError(`Unknown target case "${target}"`); } diff --git a/@commitlint/types/src/prompt.ts b/@commitlint/types/src/prompt.ts index 28bea77b1b..6e623ade87 100644 --- a/@commitlint/types/src/prompt.ts +++ b/@commitlint/types/src/prompt.ts @@ -16,6 +16,9 @@ export type PromptName = | 'issues'; export type PromptConfig = { + settings: { + scopeEnumSeparator: string; + }; messages: PromptMessages; questions: Partial< Record< diff --git a/docs/reference-prompt.md b/docs/reference-prompt.md index 0c4794ffd7..3d18f4fd0a 100644 --- a/docs/reference-prompt.md +++ b/docs/reference-prompt.md @@ -2,7 +2,13 @@ Prompt Config is used by `@commitlint/cz-commitlint`. -There are two fields: `messages` and `questions` +There are three fields: `settings`, `messages` and `questions` + +## `settings` + +Set optional options. + +- scopeEnumSeparator: Commitlint supports [multiple scopes](./concepts-commit-conventions.md?id=multiple-scopes), you can specify the delimiter. ## `messages` diff --git a/yarn.lock b/yarn.lock index a52523aa22..924b36e0ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3821,7 +3821,7 @@ commander@~2.20.3: resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commitizen@4.2.4: +commitizen@4.2.4, commitizen@^4.2.4: version "4.2.4" resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.2.4.tgz#a3e5b36bd7575f6bf6e7aa19dbbf06b0d8f37165" integrity sha512-LlZChbDzg3Ir3O2S7jSo/cgWp5/QwylQVr59K4xayVq8S4/RdKzSyJkghAiZZHfhh5t4pxunUoyeg0ml1q/7aw== @@ -7412,6 +7412,11 @@ lodash@4.17.15, lodash@^3.3.1, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19 resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^3.3.1: + version "3.10.1" + resolved "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= + log-update@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"