diff --git a/@alias/commitlint-config-lerna-scopes/package.json b/@alias/commitlint-config-lerna-scopes/package.json index 2ff6090607..c8d508106b 100644 --- a/@alias/commitlint-config-lerna-scopes/package.json +++ b/@alias/commitlint-config-lerna-scopes/package.json @@ -1,6 +1,6 @@ { "name": "commitlint-config-lerna-scopes", - "version": "5.2.6", + "version": "5.3.0-1", "description": "Shareable commitlint config enforcing lerna package names as scopes", "scripts": { "test": "exit 0", @@ -24,6 +24,6 @@ }, "homepage": "https://github.com/marionebl/commitlint#readme", "dependencies": { - "@commitlint/config-lerna-scopes": "^5.2.6" + "@commitlint/config-lerna-scopes": "^5.3.0-1" } } diff --git a/@alias/commitlint/package.json b/@alias/commitlint/package.json index 8094287801..47f75d9bd3 100644 --- a/@alias/commitlint/package.json +++ b/@alias/commitlint/package.json @@ -1,6 +1,6 @@ { "name": "commitlint", - "version": "5.2.6", + "version": "5.3.0-1", "description": "Lint your commit messages", "bin": { "commitlint": "cli.js" @@ -28,7 +28,7 @@ }, "license": "MIT", "dependencies": { - "@commitlint/cli": "^5.2.6", + "@commitlint/cli": "^5.3.0-1", "read-pkg": "3.0.0", "resolve-pkg": "1.0.0" } diff --git a/@commitlint/cli/package.json b/@commitlint/cli/package.json index 5c899dfa07..c3d7d63cc2 100644 --- a/@commitlint/cli/package.json +++ b/@commitlint/cli/package.json @@ -1,6 +1,6 @@ { "name": "@commitlint/cli", - "version": "5.2.6", + "version": "5.3.0-1", "description": "Lint your commit messages", "bin": { "commitlint": "./lib/cli.js" @@ -50,7 +50,7 @@ }, "license": "MIT", "devDependencies": { - "@commitlint/test": "^5.2.6", + "@commitlint/test": "^5.3.0-1", "@commitlint/utils": "^5.1.1", "ava": "0.18.2", "babel-cli": "6.26.0", @@ -69,8 +69,9 @@ "xo": "0.18.2" }, "dependencies": { - "@commitlint/core": "^5.2.6", + "@commitlint/core": "^5.3.0-1", "babel-polyfill": "6.26.0", + "babel-runtime": "^6.26.0", "chalk": "2.3.0", "get-stdin": "5.0.1", "lodash": "4.17.4", diff --git a/@commitlint/cli/src/cli.js b/@commitlint/cli/src/cli.js index 082608d5eb..d896b1f572 100755 --- a/@commitlint/cli/src/cli.js +++ b/@commitlint/cli/src/cli.js @@ -1,68 +1,95 @@ #!/usr/bin/env node require('babel-polyfill'); // eslint-disable-line import/no-unassigned-import -const core = require('@commitlint/core'); -const chalk = require('chalk'); const meow = require('meow'); -const {merge, pick} = require('lodash'); -const stdin = require('get-stdin'); +const merge = require('lodash/merge'); const pkg = require('../package'); -const help = require('./help'); - -const configuration = { - string: ['cwd', 'from', 'to', 'edit', 'extends', 'parser-preset'], - boolean: ['help', 'version', 'quiet', 'color'], - alias: { - c: 'color', - d: 'cwd', - e: 'edit', - f: 'from', - t: 'to', - q: 'quiet', - h: 'help', - v: 'version', - x: 'extends', - p: 'parser-preset' - }, - description: { - color: 'toggle colored output', - cwd: 'directory to execute in', - edit: - 'read last commit message from the specified file or fallbacks to ./.git/COMMIT_EDITMSG', - extends: 'array of shareable configurations to extend', - from: 'lower end of the commit range to lint; applies if edit=false', - to: 'upper end of the commit range to lint; applies if edit=false', - quiet: 'toggle console output', - 'parser-preset': - 'configuration preset to use for conventional-commits-parser' - }, - default: { - color: true, - cwd: process.cwd(), - edit: false, - from: null, - to: null, - quiet: false - }, - unknown(arg) { - throw new Error(`unknown flags: ${arg}`); - } -}; +const commands = require('./commands'); + +const FORMATS = ['commitlint', 'json']; +const COMMANDS = ['config']; + +const HELP = ` +Commands +commitlint lint commits, [input] reads from stdin if --edit, --from and --to are omitted + +Options +--cwd, -d directory to execute in, defaults to: process.cwd() +--extends, -x array of shareable configurations to extend +--format, -o format to use, defaults to "commitlint". available: "commitlint", "json" +--parser-preset, -p configuration preset to use for conventional-commits-parser +--quiet, -q toggle console output + +commitlint + --color, -c toggle colored output, defaults to: true + --edit, -e read last commit message from the specified file or falls back to ./.git/COMMIT_EDITMSG + --from, -f lower end of the commit range to lint; applies if edit=false + --to, -t upper end of the commit range to lint; applies if edit=false + +Usage +$ echo "some commit" | commitlint +$ commitlint --to=master +$ commitlint --from=HEAD~1 +`; const cli = meow( { - help: `[input] reads from stdin if --edit, --from and --to are omitted\n${help( - configuration - )}`, + help: HELP, description: `${pkg.name}@${pkg.version} - ${pkg.description}` }, - configuration + { + string: ['cwd', 'format', 'from', 'to', 'edit', 'extends', 'parser-preset'], + boolean: ['help', 'version', 'quiet', 'color'], + alias: { + c: 'color', + d: 'cwd', + e: 'edit', + f: 'from', + h: 'help', + o: 'format', + p: 'parser-preset', + q: 'quiet', + t: 'to', + v: 'version', + x: 'extends' + }, + default: { + color: true, + cwd: process.cwd(), + edit: false, + from: null, + to: null, + quiet: false + }, + unknown(arg) { + if (COMMANDS.includes(arg)) { + return; + } + + console.log(HELP); + + if (!arg.startsWith('-') && !COMMANDS.includes(arg)) { + console.log(` must be on of: [config], received "${arg}"`); + } else { + console.log(`unknown flags: ${arg}`); + } + + process.exit(1); + } + } ); main(cli).catch(err => setTimeout(() => { + if (err.quiet) { + process.exit(1); + } + if (err.help) { + console.log(`${cli.help}\n`); + } if (err.type === pkg.name) { + console.log(err.message); process.exit(1); } throw err; @@ -70,124 +97,75 @@ main(cli).catch(err => ); async function main(options) { - const raw = options.input; - const flags = normalizeFlags(options.flags); - const fromStdin = checkFromStdin(raw, flags); + const raw = Array.isArray(options.input) ? options.input : []; + const [command] = raw; - const range = pick(flags, 'edit', 'from', 'to'); - const fmt = new chalk.constructor({enabled: flags.color}); - - const input = await (fromStdin - ? stdin() - : core.read(range, {cwd: flags.cwd})); - - const messages = (Array.isArray(input) ? input : [input]) - .filter(message => typeof message === 'string') - .filter(Boolean); + const flags = normalizeFlags(options.flags); - if (messages.length === 0 && !checkFromRepository(flags)) { - const err = new Error( - '[input] is required: supply via stdin, or --edit or --from and --to' - ); - err.type = pkg.name; - console.log(`${cli.help}\n`); - console.log(err.message); - throw err; + if (!command) { + return commands.lint(raw, flags); } - return Promise.all( - messages.map(async message => { - const loaded = await core.load(getSeed(flags), {cwd: flags.cwd}); - const parserOpts = selectParserOpts(loaded.parserPreset); - const opts = parserOpts ? {parserOpts} : {parserOpts: {}}; - - // Strip comments if reading from `.git/COMMIT_EDIT_MSG` - if (range.edit) { - opts.parserOpts.commentChar = '#'; - } - - const report = await core.lint(message, loaded.rules, opts); - const formatted = core.format(report, {color: flags.color}); - - if (!flags.quiet) { - console.log( - `${fmt.grey('⧗')} input: ${fmt.bold(message.split('\n')[0])}` - ); - console.log(formatted.join('\n')); - } - - if (report.errors.length > 0) { - const error = new Error(formatted[formatted.length - 1]); - error.type = pkg.name; - throw error; - } - console.log(''); - }) - ); -} - -function checkFromStdin(input, flags) { - return input.length === 0 && !checkFromRepository(flags); -} - -function checkFromRepository(flags) { - return checkFromHistory(flags) || checkFromEdit(flags); -} - -function checkFromEdit(flags) { - return Boolean(flags.edit); + switch (command) { + case 'config': + return commands.config(raw, { + cwd: flags.cwd, + extends: flags.extends, + format: flags.format, + parserPreset: flags.parserPreset + }); + default: { + const err = new Error( + ` must be on of: [config], received "${command}"` + ); + err.help = true; + err.type = pkg.name; + throw err; + } + } } -function checkFromHistory(flags) { - return typeof flags.from === 'string' || typeof flags.to === 'string'; -} +function normalizeFlags(raw) { + const flags = merge({}, raw); -function normalizeFlags(flags) { // The `edit` flag is either a boolean or a string but we are only allowed // to specify one of them in minimist - const edit = flags.edit === '' ? true : normalizeEdit(flags.edit); - return merge({}, flags, {edit, e: edit}); -} - -function normalizeEdit(edit) { - if (typeof edit === 'boolean') { - return edit; + if (flags.edit === '') { + merge(flags, {edit: true, e: true}); } + // The recommended method to specify -e with husky is commitlint -e $GIT_PARAMS // This does not work properly with win32 systems, where env variable declarations // use a different syntax // See https://github.com/marionebl/commitlint/issues/103 for details - if (edit === '$GIT_PARAMS' || edit === '%GIT_PARAMS%') { + if (flags.edit === '$GIT_PARAMS' || flags.edit === '%GIT_PARAMS%') { if (!('GIT_PARAMS' in process.env)) { throw new Error( - `Received ${edit} as value for -e | --edit, but GIT_PARAMS is not available globally.` + `Received ${ + flags.edit + } as value for -e | --edit, but GIT_PARAMS is not available globally.` ); } return process.env.GIT_PARAMS; } - return edit; -} -function getSeed(seed) { - const e = Array.isArray(seed.extends) ? seed.extends : [seed.extends]; - const n = e.filter(i => typeof i === 'string'); - return n.length > 0 - ? {extends: n, parserPreset: seed.parserPreset} - : {parserPreset: seed.parserPreset}; -} - -function selectParserOpts(parserPreset) { - if (typeof parserPreset !== 'object') { - return undefined; + if (!('format' in flags)) { + flags.format = 'commitlint'; } - const opts = parserPreset.opts; - - if (typeof opts !== 'object') { - return undefined; + if (!FORMATS.includes(flags.format)) { + const err = new Error( + `--format must be on of: [${FORMATS.join(',')}], received "${ + flags.format + }".` + ); + err.quiet = flags.quiet; + err.help = true; + err.type = pkg.name; + throw err; } - return opts.parserOpts; + return flags; } // Catch unhandled rejections globally diff --git a/@commitlint/cli/src/cli.test.js b/@commitlint/cli/src/cli.test.js index 5d5712b4ed..483cf63b5e 100644 --- a/@commitlint/cli/src/cli.test.js +++ b/@commitlint/cli/src/cli.test.js @@ -32,6 +32,15 @@ test('should reprint input from stdin', async t => { t.true(actual.stdout.includes('foo: bar')); }); +test('should throw for unknown command', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['foo'], {cwd})(); + t.is(actual.code, 1); + t.true( + actual.stdout.includes(' must be on of: [config], received "foo"') + ); +}); + test('should produce no success output with --quiet flag', async t => { const cwd = await git.bootstrap('fixtures/empty'); const actual = await cli(['--quiet'], {cwd})('foo: bar'); @@ -82,6 +91,67 @@ test('should produce no error output with -q flag', async t => { t.is(actual.code, 1); }); +test('should throw for unknown --format', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['--format=foo'], {cwd})('foo: bar'); + t.is(actual.code, 1); + t.true( + actual.stdout.includes( + '--format must be on of: [commitlint,json], received "foo"' + ) + ); +}); + +test('should produce json output with --format=json', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['--format=json'], {cwd})('foo: bar'); + t.notThrows(() => JSON.parse(actual.stdout)); +}); + +test('should produce no output when --quiet and --format=json', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['--format=json', '--quiet'], {cwd})('foo: bar'); + t.is(actual.stdout, ''); + t.is(actual.stderr, ''); +}); + +test('should produce json with expected truthy valid key', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['--format=json'], {cwd})('foo: bar'); + const data = JSON.parse(actual.stdout); + t.is(data.valid, true); +}); + +test('should produce json with expected falsy valid key', async t => { + const cwd = await git.bootstrap('fixtures/simple'); + const actual = await cli(['--format=json'], {cwd})('foo: bar'); + const data = JSON.parse(actual.stdout); + t.is(data.valid, false); +}); + +test('should produce json with results array', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['--format=json'], {cwd})('foo: bar'); + const data = JSON.parse(actual.stdout); + t.is(Array.isArray(data.results), true); +}); + +test('results array has one report for one message', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['--format=json'], {cwd})('foo: bar'); + const data = JSON.parse(actual.stdout); + t.is(data.results.length, 1); +}); + +test('report has expected schema', async t => { + const cwd = await git.bootstrap('fixtures/empty'); + const actual = await cli(['--format=json'], {cwd})('foo: bar'); + const data = JSON.parse(actual.stdout); + const result = data.results[0]; + t.is('report' in result, true); + t.is('input' in result, true); +}); + test('should work with husky commitmsg hook and git commit', async () => { const cwd = await git.bootstrap('fixtures/husky/integration'); await writePkg({scripts: {commitmsg: `${bin} -e`}}, {cwd}); diff --git a/@commitlint/cli/src/commands/cmd-config.js b/@commitlint/cli/src/commands/cmd-config.js new file mode 100644 index 0000000000..c434e148ac --- /dev/null +++ b/@commitlint/cli/src/commands/cmd-config.js @@ -0,0 +1,20 @@ +const core = require('@commitlint/core'); +const getSeed = require('./get-seed').getSeed; + +module.exports.config = config; + +async function config(_, flags) { + const loaded = await core.load(getSeed(flags), {cwd: flags.cwd}); + switch (flags.format) { + case 'commitlint': + return console.log(loaded); + case 'json': + return console.log(JSON.stringify(loaded)); + default: { + const err = new Error(`unknown format: ${flags.format}`); + err.type = 'commitlint'; + err.help = true; + throw err; + } + } +} diff --git a/@commitlint/cli/src/commands/cmd-lint.js b/@commitlint/cli/src/commands/cmd-lint.js new file mode 100644 index 0000000000..216e2414e5 --- /dev/null +++ b/@commitlint/cli/src/commands/cmd-lint.js @@ -0,0 +1,114 @@ +const core = require('@commitlint/core'); +const chalk = require('chalk'); +const pick = require('lodash/pick'); +const stdin = require('get-stdin'); + +const pkg = require('../../package'); +const getSeed = require('./get-seed').getSeed; + +module.exports.lint = lint; + +async function lint(rawInput, flags) { + const fromStdin = checkFromStdin(rawInput, flags); + + const range = pick(flags, 'edit', 'from', 'to'); + + const input = await (fromStdin + ? stdin() + : core.read(range, {cwd: flags.cwd})); + + const messages = (Array.isArray(input) ? input : [input]) + .filter(message => typeof message === 'string') + .filter(Boolean); + + if (messages.length === 0 && !checkFromRepository(flags)) { + throw error( + '[input] is required: supply via stdin, or --edit or --from and --to', + { + quiet: flags.quiet, + help: true, + type: pkg.name + } + ); + } + + const loaded = await core.load(getSeed(flags), {cwd: flags.cwd}); + + // Strip comments if reading from `.git/COMMIT_EDIT_MSG` + if (range.edit) { + loaded.parserPreset.opts.commentChar = '#'; + } + + const results = await all(messages, async msg => { + return { + report: await core.lint(msg, loaded.rules, { + parserOpts: loaded.parserPreset.opts + }), + input: msg + }; + }); + + const valid = results.every(result => result.report.valid); + + if (flags.quiet && valid) { + return; + } + + if (flags.quiet && !valid) { + throw error('linting failed', {type: pkg.name, quiet: true}); + } + + switch (flags.format) { + case 'commitlint': { + const fmt = new chalk.constructor({enabled: flags.color}); + const icon = fmt.grey('⧗'); + const formatted = results.map(result => { + result.formatted = core.format(result.report, {color: flags.color}); + return result; + }); + formatted.forEach(result => { + const subject = fmt.bold(result.input.split('\n')[0]); + console.log( + `${icon} input: ${subject}\n${result.formatted.join('\n')}\n` + ); + }); + break; + } + case 'json': + console.log(JSON.stringify({valid, results})); + break; + default: { + throw error(`unknown format: ${flags.format}`); + } + } + + if (!valid) { + throw error('linting failed', {type: pkg.name, quiet: true}); + } +} + +function all(things, predecate) { + return Promise.all(things.map(thing => predecate(thing))); +} + +function error(message, opts = {}) { + const err = new Error(message); + Object.assign(err, opts); + return err; +} + +function checkFromStdin(input, flags) { + return input.length === 0 && !checkFromRepository(flags); +} + +function checkFromRepository(flags) { + return checkFromHistory(flags) || checkFromEdit(flags); +} + +function checkFromEdit(flags) { + return Boolean(flags.edit); +} + +function checkFromHistory(flags) { + return typeof flags.from === 'string' || typeof flags.to === 'string'; +} diff --git a/@commitlint/cli/src/commands/get-seed.js b/@commitlint/cli/src/commands/get-seed.js new file mode 100644 index 0000000000..a127c1d2b7 --- /dev/null +++ b/@commitlint/cli/src/commands/get-seed.js @@ -0,0 +1,9 @@ +module.exports.getSeed = getSeed; + +function getSeed(seed) { + const e = Array.isArray(seed.extends) ? seed.extends : [seed.extends]; + const n = e.filter(i => typeof i === 'string'); + return n.length > 0 + ? {extends: n, parserPreset: seed.parserPreset} + : {parserPreset: seed.parserPreset}; +} diff --git a/@commitlint/cli/src/commands/index.js b/@commitlint/cli/src/commands/index.js new file mode 100644 index 0000000000..0d0c479710 --- /dev/null +++ b/@commitlint/cli/src/commands/index.js @@ -0,0 +1,4 @@ +module.exports = { + config: require('./cmd-config').config, + lint: require('./cmd-lint').lint +}; diff --git a/@commitlint/cli/src/help.js b/@commitlint/cli/src/help.js deleted file mode 100644 index 8ac853ef31..0000000000 --- a/@commitlint/cli/src/help.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = configuration => { - const lines = Object.entries(configuration.description).map(entry => { - const name = entry[0]; - const desc = entry[1]; - const alias = Object.entries(configuration.alias) - .find(entry => entry[1] === name) - .map(entry => entry[0])[0]; - const defaults = configuration.default[name]; - return [[name, alias].filter(Boolean), desc, defaults].filter(Boolean); - }); - - const longest = lines - .map(line => { - const flags = line[0]; - return flags.reduce((sum, flag) => sum + flag.length, 0); - }) - .sort(Number)[0]; - - return lines - .map(line => { - const flags = line[0]; - const desc = line[1]; - const defaults = line[2]; - const fs = flags.map( - flag => (flag.length > 1 ? `--${flag}` : ` -${flag}`) - ); - const ds = defaults ? `, defaults to: ${defaults}` : ''; - const length = flags.reduce((sum, flag) => sum + flag.length, 0); - return `${fs.join(',')}${' '.repeat(4 + longest - length)}${desc}${ds}`; - }) - .join('\n'); -}; diff --git a/@commitlint/config-lerna-scopes/package.json b/@commitlint/config-lerna-scopes/package.json index b18b510a4e..a7a689a38d 100644 --- a/@commitlint/config-lerna-scopes/package.json +++ b/@commitlint/config-lerna-scopes/package.json @@ -1,6 +1,6 @@ { "name": "@commitlint/config-lerna-scopes", - "version": "5.2.6", + "version": "5.3.0-1", "description": "Shareable commitlint config enforcing lerna package names as scopes", "scripts": { "clean": "exit 0", @@ -39,7 +39,7 @@ "lerna": "2.5.1" }, "devDependencies": { - "@commitlint/test": "^5.2.6", + "@commitlint/test": "^5.3.0-1", "ava": "0.22.0", "xo": "0.18.2" } diff --git a/@commitlint/core/package.json b/@commitlint/core/package.json index 8eeaad6358..3723e647f2 100644 --- a/@commitlint/core/package.json +++ b/@commitlint/core/package.json @@ -1,6 +1,6 @@ { "name": "@commitlint/core", - "version": "5.2.6", + "version": "5.3.0-1", "description": "Lint your commit messages", "main": "lib/index.js", "scripts": { @@ -55,7 +55,7 @@ }, "license": "MIT", "devDependencies": { - "@commitlint/test": "^5.2.6", + "@commitlint/test": "^5.3.0-1", "@commitlint/utils": "^5.1.1", "ava": "0.22.0", "babel-cli": "6.26.0", diff --git a/@commitlint/core/src/library/parse.test.js b/@commitlint/core/src/library/parse.test.js index 1c731174c7..f1f0d8a20f 100644 --- a/@commitlint/core/src/library/parse.test.js +++ b/@commitlint/core/src/library/parse.test.js @@ -1,4 +1,7 @@ import importFrom from 'import-from'; +import {sync} from '@marionebl/conventional-commits-parser'; +import angular from 'conventional-changelog-angular'; + import test from 'ava'; import parse from './parse'; @@ -13,30 +16,40 @@ test('throws when called with empty message', async t => { }); test('returns object with raw message', async t => { + const {parserOpts} = await angular; const message = 'type(scope): subject'; - const actual = await parse(message); + const actual = await parse(message, sync, parserOpts); t.is(actual.raw, message); }); test('calls parser with message and passed options', async t => { const message = 'message'; + const {parserOpts} = await angular; - await parse(message, m => { - t.is(message, m); - return {}; - }); + await parse( + message, + m => { + t.is(message, m); + return {}; + }, + parserOpts + ); }); test('passes object up from parser function', async t => { const message = 'message'; + const {parserOpts} = await angular; + const result = {}; - const actual = await parse(message, () => result); + const actual = await parse(message, () => result, parserOpts); t.is(actual, result); }); test('returns object with expected keys', async t => { const message = 'message'; - const actual = await parse(message); + const {parserOpts} = await angular; + + const actual = await parse(message, sync, parserOpts); const expected = { body: null, footer: null, @@ -56,7 +69,9 @@ test('returns object with expected keys', async t => { test('uses angular grammar', async t => { const message = 'type(scope): subject'; - const actual = await parse(message); + const {parserOpts} = await angular; + + const actual = await parse(message, sync, parserOpts); const expected = { body: null, footer: null, @@ -100,18 +115,18 @@ test('uses custom opts parser', async t => { test('supports scopes with /', async t => { const message = 'type(some/scope): subject'; - const actual = await parse(message); + const {parserOpts} = await angular; + + const actual = await parse(message, sync, parserOpts); t.is(actual.scope, 'some/scope'); t.is(actual.subject, 'subject'); }); test('ignores comments', async t => { const message = 'type(some/scope): subject\n# some comment'; - const changelogOpts = await importFrom( - process.cwd(), - 'conventional-changelog-angular' - ); - const opts = Object.assign({}, changelogOpts.parserOpts, {commentChar: '#'}); + const {parserOpts} = await angular; + + const opts = Object.assign({}, parserOpts, {commentChar: '#'}); const actual = await parse(message, undefined, opts); t.is(actual.body, null); t.is(actual.footer, null); @@ -121,11 +136,8 @@ test('ignores comments', async t => { test('registers inline #', async t => { const message = 'type(some/scope): subject #reference\n# some comment\nthings #reference'; - const changelogOpts = await importFrom( - process.cwd(), - 'conventional-changelog-angular' - ); - const opts = Object.assign({}, changelogOpts.parserOpts, {commentChar: '#'}); + const {parserOpts} = await angular; + const opts = Object.assign({}, parserOpts, {commentChar: '#'}); const actual = await parse(message, undefined, opts); t.is(actual.subject, 'subject #reference'); t.is(actual.body, 'things #reference'); diff --git a/@commitlint/core/src/library/resolve-extends.js b/@commitlint/core/src/library/resolve-extends.js index 2e0593a399..86f40f8956 100644 --- a/@commitlint/core/src/library/resolve-extends.js +++ b/@commitlint/core/src/library/resolve-extends.js @@ -5,9 +5,10 @@ import resolveFrom from 'resolve-from'; import {merge, omit} from 'lodash'; // Resolve extend configs -export default function resolveExtends(config = {}, context = {}) { +export default async function resolveExtends(config = {}, context = {}) { const {extends: e} = config; - const extended = loadExtends(config, context).reduceRight( + + const extended = (await loadExtends(config, context)).reduceRight( (r, c) => merge(r, omit(c, 'extends')), e ? {extends: e} : {} ); @@ -23,23 +24,15 @@ export default function resolveExtends(config = {}, context = {}) { } // (any, string, string, Function) => any[]; -function loadExtends(config = {}, context = {}) { - return (config.extends || []).reduce((configs, raw) => { +async function loadExtends(config = {}, context = {}) { + return (config.extends || []).reduce(async (accing, raw) => { const load = context.require || require; const resolved = resolveConfig(raw, context); const c = load(resolved); const cwd = path.dirname(resolved); - // Remove deprecation warning in version 3 - if (typeof c === 'object' && 'wildcards' in c) { - console.warn( - `'wildcards' found in '${raw}' ignored. To silence this warning raise an issue at 'npm repo ${raw}' to remove the wildcards.` - ); - } - const ctx = merge({}, context, {cwd}); - // Resolve parser preset if none was present before if ( !context.parserPreset && typeof c === 'object' && @@ -52,15 +45,18 @@ function loadExtends(config = {}, context = {}) { path: `./${path.relative(process.cwd(), resolvedParserPreset)}` .split(path.sep) .join('/'), - opts: require(resolvedParserPreset) + opts: (await require(resolvedParserPreset)).parserOpts }; ctx.parserPreset = parserPreset; config.parserPreset = parserPreset; } - return [...configs, c, ...loadExtends(c, ctx)]; - }, []); + const acc = await accing; + acc.push(c); + acc.push(...(await loadExtends(c, ctx))); + return acc; + }, Promise.resolve([])); } function getId(raw = '', prefix = '') { diff --git a/@commitlint/core/src/library/resolve-extends.test.js b/@commitlint/core/src/library/resolve-extends.test.js index 0eb90d2a85..09890be93d 100644 --- a/@commitlint/core/src/library/resolve-extends.test.js +++ b/@commitlint/core/src/library/resolve-extends.test.js @@ -6,14 +6,14 @@ import resolveExtends from './resolve-extends'; const id = id => id; -test('returns empty object when called without params', t => { - const actual = resolveExtends(); +test('returns empty object when called without params', async t => { + const actual = await resolveExtends(); t.deepEqual(actual, {}); }); -test('returns an equivalent object as passed in', t => { +test('returns an equivalent object as passed in', async t => { const expected = {foo: 'bar'}; - const actual = resolveExtends(expected); + const actual = await resolveExtends(expected); t.deepEqual(actual, expected); }); @@ -38,12 +38,12 @@ test.serial('falls back to global install', async t => { ]); const expected = {extends: ['@commitlint/config-angular']}; - t.notThrows(() => resolveExtends(expected)); + t.notThrows(async () => resolveExtends(expected)); process.env.PREFIX = prev; }); -test.serial('fails for missing extends', async t => { +test.serial.failing('fails for missing extends', async t => { const prev = process.env.PREFIX; const cwd = await fix.bootstrap('fixtures/missing-install'); @@ -54,17 +54,17 @@ test.serial('fails for missing extends', async t => { const input = {extends: ['@commitlint/foo-bar']}; t.throws( - () => resolveExtends(input, {cwd}), + resolveExtends(input, {cwd}), /Cannot find module "@commitlint\/foo-bar" from/ ); process.env.PREFIX = prev; }); -test('uses empty prefix by default', t => { +test('uses empty prefix by default', async t => { const input = {extends: ['extender-name']}; - resolveExtends(input, { + await resolveExtends(input, { resolve: id, require(id) { t.is(id, 'extender-name'); @@ -72,10 +72,10 @@ test('uses empty prefix by default', t => { }); }); -test('uses prefix as configured', t => { +test('uses prefix as configured', async t => { const input = {extends: ['extender-name']}; - resolveExtends(input, { + await resolveExtends(input, { prefix: 'prefix', resolve: id, require(id) { @@ -84,10 +84,10 @@ test('uses prefix as configured', t => { }); }); -test('ignores prefix for scoped extends', t => { +test('ignores prefix for scoped extends', async t => { const input = {extends: ['@scope/extender-name']}; - resolveExtends(input, { + await resolveExtends(input, { prefix: 'prefix', resolve: id, require(id) { @@ -96,10 +96,10 @@ test('ignores prefix for scoped extends', t => { }); }); -test('ignores prefix for relative extends', t => { +test('ignores prefix for relative extends', async t => { const input = {extends: ['./extender']}; - resolveExtends(input, { + await resolveExtends(input, { prefix: 'prefix', resolve: id, require(id) { @@ -108,11 +108,11 @@ test('ignores prefix for relative extends', t => { }); }); -test('propagates return value of require function', t => { +test('propagates return value of require function', async t => { const input = {extends: ['extender-name']}; const propagated = {foo: 'bar'}; - const actual = resolveExtends(input, { + const actual = await resolveExtends(input, { resolve: id, require() { return propagated; @@ -122,11 +122,11 @@ test('propagates return value of require function', t => { t.is(actual.foo, 'bar'); }); -test('resolves extends recursively', t => { +test('resolves extends recursively', async t => { const input = {extends: ['extender-name']}; const actual = []; - resolveExtends(input, { + await resolveExtends(input, { resolve: id, require(id) { actual.push(id); @@ -142,11 +142,11 @@ test('resolves extends recursively', t => { t.deepEqual(actual, ['extender-name', 'recursive-extender-name']); }); -test('uses prefix key recursively', t => { +test('uses prefix key recursively', async t => { const input = {extends: ['extender-name']}; const actual = []; - resolveExtends(input, { + await resolveExtends(input, { prefix: 'prefix', resolve: id, require(id) { @@ -166,10 +166,10 @@ test('uses prefix key recursively', t => { ]); }); -test('propagates contents recursively', t => { +test('propagates contents recursively', async t => { const input = {extends: ['extender-name']}; - const actual = resolveExtends(input, { + const actual = await resolveExtends(input, { resolve: id, require(id) { if (id === 'extender-name') { @@ -190,10 +190,10 @@ test('propagates contents recursively', t => { t.deepEqual(actual, expected); }); -test('extending contents should take precedence', t => { +test('extending contents should take precedence', async t => { const input = {extends: ['extender-name'], zero: 'root'}; - const actual = resolveExtends(input, { + const actual = await resolveExtends(input, { resolve: id, require(id) { if (id === 'extender-name') { @@ -224,10 +224,10 @@ test('extending contents should take precedence', t => { t.deepEqual(actual, expected); }); -test('should fall back to conventional-changelog-lint-config prefix', t => { +test('should fall back to conventional-changelog-lint-config prefix', async t => { const input = {extends: ['extender-name']}; - const actual = resolveExtends(input, { + const actual = await resolveExtends(input, { prefix: 'prefix', resolve(id) { if (id === 'conventional-changelog-lint-config-extender-name') { diff --git a/@commitlint/core/src/load.js b/@commitlint/core/src/load.js index 9a121d5bdf..fbd4757c5d 100644 --- a/@commitlint/core/src/load.js +++ b/@commitlint/core/src/load.js @@ -6,6 +6,10 @@ import resolveFrom from 'resolve-from'; import executeRule from './library/execute-rule'; import resolveExtends from './library/resolve-extends'; +const DEFAULT_NAME = 'conventional-changelog-angular'; +const DEFAULT_RESOLVED = require.resolve(DEFAULT_NAME); +const DEFAULT_PRESET = require(DEFAULT_RESOLVED); + const w = (a, b) => (Array.isArray(b) ? b : undefined); const valid = input => pick(input, 'extends', 'rules', 'parserPreset'); @@ -24,12 +28,12 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { config.parserPreset = { name: config.parserPreset, path: resolvedParserPreset, - opts: require(resolvedParserPreset) + opts: (await require(resolvedParserPreset)).parserOpts }; } // Resolve extends key - const extended = resolveExtends(opts, { + const extended = await resolveExtends(opts, { prefix: 'commitlint-config', cwd: base, parserPreset: config.parserPreset @@ -37,12 +41,13 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { const preset = valid(mergeWith(extended, config, w)); - // Await parser-preset if applicable - if ( - typeof preset.parserPreset === 'object' && - typeof preset.parserPreset.opts === 'object' - ) { - preset.parserPreset.opts = await preset.parserPreset.opts; + // Resolve to default preset if none set + if (typeof preset.parserPreset === 'undefined') { + preset.parserPreset = { + name: DEFAULT_NAME, + path: DEFAULT_RESOLVED, + opts: (await DEFAULT_PRESET).parserOpts + }; } // Execute rule config functions if needed diff --git a/@commitlint/core/src/load.test.js b/@commitlint/core/src/load.test.js index 3fa9da29bc..79e7de97bb 100644 --- a/@commitlint/core/src/load.test.js +++ b/@commitlint/core/src/load.test.js @@ -27,12 +27,16 @@ test('uses seed with parserPreset', async t => { t.is(actual.name, './conventional-changelog-custom'); t.deepEqual(actual.opts, { - parserOpts: { - headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/ - } + headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/ }); }); +test('defaults to expected preset', async t => { + const cwd = await git.bootstrap('fixtures/extends-empty'); + const actual = await load({}, {cwd}); + t.is(actual.parserPreset.name, 'conventional-changelog-angular'); +}); + test('invalid extend should throw', async t => { const cwd = await git.bootstrap('fixtures/extends-invalid'); await t.throws(load({}, {cwd})); @@ -53,25 +57,22 @@ test('empty file should extend nothing', async t => { test('respects cwd option', async t => { const cwd = await git.bootstrap('fixtures/recursive-extends/first-extended'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: ['./second-extended'], - rules: { - one: 1, - two: 2 - } + t.deepEqual(actual.extends, ['./second-extended']); + t.deepEqual(actual.rules, { + one: 1, + two: 2 }); }); test('recursive extends', async t => { const cwd = await git.bootstrap('fixtures/recursive-extends'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: ['./first-extended'], - rules: { - zero: 0, - one: 1, - two: 2 - } + + t.deepEqual(actual.extends, ['./first-extended']); + t.deepEqual(actual.rules, { + zero: 0, + one: 1, + two: 2 }); }); @@ -79,13 +80,11 @@ test('recursive extends with json file', async t => { const cwd = await git.bootstrap('fixtures/recursive-extends-json'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: ['./first-extended'], - rules: { - zero: 0, - one: 1, - two: 2 - } + t.deepEqual(actual.extends, ['./first-extended']); + t.deepEqual(actual.rules, { + zero: 0, + one: 1, + two: 2 }); }); @@ -93,13 +92,11 @@ test('recursive extends with yaml file', async t => { const cwd = await git.bootstrap('fixtures/recursive-extends-yaml'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: ['./first-extended'], - rules: { - zero: 0, - one: 1, - two: 2 - } + t.deepEqual(actual.extends, ['./first-extended']); + t.deepEqual(actual.rules, { + zero: 0, + one: 1, + two: 2 }); }); @@ -107,13 +104,11 @@ test('recursive extends with js file', async t => { const cwd = await git.bootstrap('fixtures/recursive-extends-js'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: ['./first-extended'], - rules: { - zero: 0, - one: 1, - two: 2 - } + t.deepEqual(actual.extends, ['./first-extended']); + t.deepEqual(actual.rules, { + zero: 0, + one: 1, + two: 2 }); }); @@ -121,13 +116,11 @@ test('recursive extends with package.json file', async t => { const cwd = await git.bootstrap('fixtures/recursive-extends-package'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: ['./first-extended'], - rules: { - zero: 0, - one: 1, - two: 2 - } + t.deepEqual(actual.extends, ['./first-extended']); + t.deepEqual(actual.rules, { + zero: 0, + one: 1, + two: 2 }); }); @@ -138,10 +131,7 @@ test('parser preset overwrites completely instead of merging', async t => { t.is(actual.parserPreset.name, './custom'); t.is(typeof actual.parserPreset.opts, 'object'); t.deepEqual(actual.parserPreset.opts, { - b: 'b', - parserOpts: { - headerPattern: /.*/ - } + headerPattern: /.*/ }); }); @@ -152,7 +142,7 @@ test('recursive extends with parserPreset', async t => { t.is(actual.parserPreset.name, './conventional-changelog-custom'); t.is(typeof actual.parserPreset.opts, 'object'); t.deepEqual( - actual.parserPreset.opts.parserOpts.headerPattern, + actual.parserPreset.opts.headerPattern, /^(\w*)(?:\((.*)\))?-(.*)$/ ); }); @@ -161,12 +151,10 @@ test('ignores unknow keys', async t => { const cwd = await git.bootstrap('fixtures/trash-file'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: [], - rules: { - foo: 'bar', - baz: 'bar' - } + t.deepEqual(actual.extends, []); + t.deepEqual(actual.rules, { + foo: 'bar', + baz: 'bar' }); }); @@ -174,12 +162,10 @@ test('ignores unknow keys recursively', async t => { const cwd = await git.bootstrap('fixtures/trash-extend'); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: ['./one'], - rules: { - zero: 0, - one: 1 - } + t.deepEqual(actual.extends, ['./one']); + t.deepEqual(actual.rules, { + zero: 0, + one: 1 }); }); @@ -190,13 +176,11 @@ test('find up from given cwd', async t => { const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: [], - rules: { - child: true, - inner: false, - outer: false - } + t.deepEqual(actual.extends, []); + t.deepEqual(actual.rules, { + child: true, + inner: false, + outer: false }); }); @@ -205,12 +189,10 @@ test('find up config from outside current git repo', async t => { const cwd = await git.init(path.join(outer, 'inner-scope')); const actual = await load({}, {cwd}); - t.deepEqual(actual, { - extends: [], - rules: { - child: false, - inner: false, - outer: true - } + t.deepEqual(actual.extends, []); + t.deepEqual(actual.rules, { + child: false, + inner: false, + outer: true }); }); diff --git a/@commitlint/prompt-cli/package.json b/@commitlint/prompt-cli/package.json index c74aba28e0..3a5ee8ed8c 100644 --- a/@commitlint/prompt-cli/package.json +++ b/@commitlint/prompt-cli/package.json @@ -1,6 +1,6 @@ { "name": "@commitlint/prompt-cli", - "version": "5.2.6", + "version": "5.3.0-1", "description": "commit prompt using commitlint.config.js", "bin": { "commit": "./cli.js" @@ -31,7 +31,7 @@ "xo": "0.18.2" }, "dependencies": { - "@commitlint/prompt": "^5.2.6", + "@commitlint/prompt": "^5.3.0-1", "execa": "0.8.0", "meow": "3.7.0" } diff --git a/@commitlint/prompt/package.json b/@commitlint/prompt/package.json index c95f5c71a3..c5da8ee00c 100644 --- a/@commitlint/prompt/package.json +++ b/@commitlint/prompt/package.json @@ -1,6 +1,6 @@ { "name": "@commitlint/prompt", - "version": "5.2.6", + "version": "5.3.0-1", "description": "commitizen prompt using commitlint.config.js", "main": "./lib/index.js", "scripts": { @@ -66,7 +66,7 @@ "xo": "0.18.2" }, "dependencies": { - "@commitlint/core": "^5.2.6", + "@commitlint/core": "^5.3.0-1", "babel-runtime": "^6.23.0", "chalk": "^2.0.0", "lodash": "^4.17.4", diff --git a/@commitlint/travis-cli/package.json b/@commitlint/travis-cli/package.json index 823eb9a9ac..669e7043cd 100644 --- a/@commitlint/travis-cli/package.json +++ b/@commitlint/travis-cli/package.json @@ -1,6 +1,6 @@ { "name": "@commitlint/travis-cli", - "version": "5.2.6", + "version": "5.3.0-1", "description": "Lint all relevant commits for a change or PR on Travis CI", "bin": { "commitlint-travis": "./lib/cli.js" @@ -54,7 +54,7 @@ }, "license": "MIT", "devDependencies": { - "@commitlint/test": "^5.2.6", + "@commitlint/test": "^5.3.0-1", "@commitlint/utils": "^5.1.1", "ava": "0.18.2", "babel-cli": "6.26.0", @@ -64,7 +64,7 @@ "which": "1.3.0" }, "dependencies": { - "@commitlint/cli": "^5.2.6", + "@commitlint/cli": "^5.3.0-1", "babel-runtime": "6.26.0", "execa": "0.8.0" } diff --git a/@packages/example-prompt/package.json b/@packages/example-prompt/package.json index 2ed61116d7..1a69720e86 100644 --- a/@packages/example-prompt/package.json +++ b/@packages/example-prompt/package.json @@ -1,7 +1,7 @@ { "name": "@commitlint/example-prompt", "private": true, - "version": "5.2.6", + "version": "5.3.0-1", "description": "Example for prompt guide", "scripts": { "commit": "commit" @@ -17,6 +17,6 @@ }, "homepage": "https://github.com/marionebl/commitlint#readme", "devDependencies": { - "@commitlint/prompt-cli": "^5.2.6" + "@commitlint/prompt-cli": "^5.3.0-1" } } diff --git a/@packages/test/package.json b/@packages/test/package.json index 8e53a98b92..811953e7a7 100644 --- a/@packages/test/package.json +++ b/@packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@commitlint/test", - "version": "5.2.6", + "version": "5.3.0-1", "description": "test utilities for @commitlint", "main": "lib/", "private": true, diff --git a/lerna.json b/lerna.json index f51bd96e6c..6a497998b3 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "lerna": "2.5.1", "npmClient": "yarn", "useWorkspaces": true, - "version": "5.2.6" + "version": "5.3.0-1" }