diff --git a/.gitignore b/.gitignore index 5e37fcfe85..56f9832b18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,9 @@ -# Logs -logs -*.log npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory +lerna-debug.log node_modules - -# Optional npm cache directory .npm - -# Optional REPL history -.node_repl_history - -# jsonlint-cli cache -.tmp - -# transpiled artifacts -distribution - .nyc_output +.dockerignore +.*.dockerfile +@commitlint/**/lib +@commitlint/**/package.json.lerna_backup diff --git a/.travis.yml b/.travis.yml index 8606782c5f..7d69d741ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,5 @@ language: node_js node_js: - - '7' + - '8' - '6' - '4' -before_install: - - git fetch --unshallow -before_script: - - npm run build -script: - - npm run travis:lint:commits - - npm test diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..abf8296a48 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "xo.enable": true +} diff --git a/@commitlint/cli/CHANGELOG.md b/@commitlint/cli/CHANGELOG.md new file mode 100644 index 0000000000..eac9f3ab4d --- /dev/null +++ b/@commitlint/cli/CHANGELOG.md @@ -0,0 +1,30 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + + + +# 3.0.0 (2017-07-10) + + +### Bug Fixes + +* **cli:** remove destructuring for node 4 support ([94437e8](https://github.com/marionebl/commitlint/commit/94437e8)) +* ensure node4 compat ([a5e658a](https://github.com/marionebl/commitlint/commit/a5e658a)) + + + + + +# 3.0.0 (2017-07-10) + + +### Bug Fixes + +* **cli:** remove destructuring for node 4 support ([94437e8](https://github.com/marionebl/commitlint/commit/94437e8)) +* ensure node4 compat ([a5e658a](https://github.com/marionebl/commitlint/commit/a5e658a)) diff --git a/@commitlint/cli/cli.js b/@commitlint/cli/cli.js new file mode 100755 index 0000000000..919ef5c40b --- /dev/null +++ b/@commitlint/cli/cli.js @@ -0,0 +1,118 @@ +#!/usr/bin/env node +require('babel-polyfill'); // eslint-disable-line import/no-unassigned-import + +// npm modules +const core = require('@commitlint/core'); +const chalk = require('chalk'); +const meow = require('meow'); +const pick = require('lodash').pick; +const stdin = require('get-stdin'); + +const pkg = require('./package'); +const help = require('./help'); + +/** + * Behavioural rules + */ +const rules = { + fromStdin: (input, flags) => input.length === 0 && + typeof flags.from !== 'string' && + typeof flags.to !== 'string' && + !flags.edit +}; + +const configuration = { + string: ['from', 'to', 'extends'], + boolean: ['edit', 'help', 'version', 'quiet', 'color'], + alias: { + c: 'color', + e: 'edit', + f: 'from', + t: 'to', + q: 'quiet', + h: 'help', + v: 'version', + x: 'extends' + }, + description: { + color: 'toggle colored output', + edit: 'read last commit message found in ./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' + }, + default: { + color: true, + edit: false, + from: null, + to: null, + quiet: false + }, + unknown(arg) { + throw new Error(`unknown flags: ${arg}`); + } +}; + +const cli = meow({ + help: `[input] reads from stdin if --edit, --from and --to are omitted\n${help(configuration)}`, + description: `${pkg.name}@${pkg.version} - ${pkg.description}` +}, configuration); + +const load = seed => core.load(seed); + +function main(options) { + const raw = options.input; + const flags = options.flags; + const fromStdin = rules.fromStdin(raw, flags); + + const range = pick(flags, 'edit', 'from', 'to'); + const input = fromStdin ? stdin() : core.read(range); + const fmt = new chalk.constructor({enabled: flags.color}); + + return input + .then(raw => Array.isArray(raw) ? raw : [raw]) + .then(messages => Promise.all(messages.map(commit => { + return load(getSeed(flags)) + .then(opts => core.lint(commit, opts.rules)) + .then(report => { + const formatted = core.format(report, {color: flags.color}); + + if (!flags.quiet) { + console.log(`${fmt.grey('⧗')} input: ${fmt.bold(commit.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; + } + return console.log(''); + }); + }) + )); +} + +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} : {}; +} + +// Start the engine +main(cli) + .catch(err => + setTimeout(() => { + if (err.type === pkg.name) { + process.exit(1); + } + throw err; + }) + ); + +// Catch unhandled rejections globally +process.on('unhandledRejection', (reason, promise) => { + console.log('Unhandled Rejection at: Promise ', promise, ' reason: ', reason); + throw reason; +}); diff --git a/@commitlint/cli/cli.test.js b/@commitlint/cli/cli.test.js new file mode 100644 index 0000000000..24573769b4 --- /dev/null +++ b/@commitlint/cli/cli.test.js @@ -0,0 +1,71 @@ +import path from 'path'; +import test from 'ava'; +import execa from 'execa'; +import stream from 'string-to-stream'; + +const here = path.join.bind(null, __dirname); + +const SIMPLE = here('fixtures/simple'); +const EXTENDS_ROOT = here('fixtures/extends-root'); +const EMPTY = here('fixtures/empty'); + +const cli = (input = '', args = [], opts = {}) => { + const c = execa(here('cli.js'), args, { + capture: ['stdout'], + cwd: opts.cwd + }); + stream(input).pipe(c.stdin); + return c; +}; + +test('should throw when called without [input]', t => { + t.throws(cli(), /Expected a raw commit/); +}); + +test('should reprint input from stdin', async t => { + const actual = await cli('foo: bar', [], {cwd: EMPTY}); + t.true(actual.stdout.includes('foo: bar')); +}); + +test('should produce no success output with --quiet flag', async t => { + const actual = await cli('foo: bar', ['--quiet'], {cwd: EMPTY}); + t.is(actual.stdout, ''); + t.is(actual.stderr, ''); +}); + +test('should produce no success output with -q flag', async t => { + const actual = await cli('foo: bar', ['-q'], {cwd: EMPTY}); + t.is(actual.stdout, ''); + t.is(actual.stderr, ''); +}); + +test('should succeed for input from stdin without rules', async t => { + const actual = await cli('foo: bar', [], {cwd: EMPTY}); + t.is(actual.code, 0); +}); + +test('should fail for input from stdin with rule from rc', async t => { + const actual = await t.throws(cli('foo: bar', [], {cwd: SIMPLE})); + t.true(actual.stdout.includes('scope must not be one of [foo]')); + t.is(actual.code, 1); +}); + +test('should fail for input from stdin with rule from js', async t => { + const actual = await t.throws(cli('foo: bar', ['--extends', './extended'], {cwd: EXTENDS_ROOT})); + t.true(actual.stdout.includes('scope must not be one of [foo]')); + t.is(actual.code, 1); +}); + +test('should produce no error output with --quiet flag', async t => { + const actual = await t.throws(cli('foo: bar', ['--quiet'], {cwd: SIMPLE})); + t.is(actual.stdout, ''); + t.is(actual.stderr, ''); + t.is(actual.code, 1); +}); + +test('should produce no error output with -q flag', async t => { + const actual = await t.throws(cli('foo: bar', ['-q'], {cwd: SIMPLE})); + t.is(actual.stdout, ''); + t.is(actual.stderr, ''); + t.is(actual.code, 1); +}); diff --git a/@commitlint/cli/fixtures/empty/commitlint.config.js b/@commitlint/cli/fixtures/empty/commitlint.config.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/@commitlint/cli/fixtures/empty/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/@commitlint/cli/fixtures/extends-root/extended.js b/@commitlint/cli/fixtures/extends-root/extended.js new file mode 100644 index 0000000000..b6775fef5b --- /dev/null +++ b/@commitlint/cli/fixtures/extends-root/extended.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'type-enum': [2, 'never', ['foo']] + } +}; diff --git a/@commitlint/cli/fixtures/simple/commitlint.config.js b/@commitlint/cli/fixtures/simple/commitlint.config.js new file mode 100644 index 0000000000..b6775fef5b --- /dev/null +++ b/@commitlint/cli/fixtures/simple/commitlint.config.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'type-enum': [2, 'never', ['foo']] + } +}; diff --git a/source/help.js b/@commitlint/cli/help.js similarity index 80% rename from source/help.js rename to @commitlint/cli/help.js index 6f5478c2c3..dbe2471527 100644 --- a/source/help.js +++ b/@commitlint/cli/help.js @@ -1,7 +1,8 @@ -export default configuration => { +module.exports = configuration => { const lines = Object.entries(configuration.description) .map(entry => { - const [name, desc] = 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]; @@ -11,14 +12,16 @@ export default configuration => { const longest = lines .map(line => { - const [flags] = line; + const flags = line[0]; return flags.reduce((sum, flag) => sum + flag.length, 0); }) .sort(Number)[0]; return lines .map(line => { - const [flags, desc, defaults] = 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); diff --git a/@commitlint/cli/package.json b/@commitlint/cli/package.json new file mode 100644 index 0000000000..340ceaecdd --- /dev/null +++ b/@commitlint/cli/package.json @@ -0,0 +1,58 @@ +{ + "name": "@commitlint/cli", + "version": "3.0.1", + "description": "Lint your commit messages", + "bin": { + "commitlint": "cli.js" + }, + "scripts": { + "build": "exit 0", + "clean": "exit 0", + "pretest": "dep-check", + "test": "ava", + "prepublish": "npm test" + }, + "ava": { + "files": [ + "cli.test.js" + ] + }, + "xo": false, + "engines": { + "node": ">=4" + }, + "repository": { + "type": "git", + "url": "https://github.com/marionebl/commitlint.git" + }, + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "keywords": [ + "conventional-changelog", + "commitlint", + "cli" + ], + "author": { + "name": "Mario Nebl", + "email": "hello@herebecode.com" + }, + "license": "MIT", + "devDependencies": { + "@commitlint/utils": "^3.0.0", + "ava": "^0.18.2", + "dependency-check": "^2.9.1", + "execa": "^0.7.0", + "string-to-stream": "^1.1.0", + "xo": "^0.18.2" + }, + "dependencies": { + "@commitlint/core": "^3.0.1", + "babel-polyfill": "^6.23.0", + "chalk": "^2.0.1", + "get-stdin": "^5.0.1", + "lodash": "^4.17.4", + "meow": "^3.7.0" + } +} diff --git a/@commitlint/cli/readme.md b/@commitlint/cli/readme.md new file mode 100644 index 0000000000..a0014014f8 --- /dev/null +++ b/@commitlint/cli/readme.md @@ -0,0 +1,12 @@ +> Lint commit messages + +# @commitlint/cli + +## Getting started + +```shell +npm install --save-dev @commitlint/cli @commitlint/config-angular +echo "module.exports = {extends: ['@commitlint/config-angular']};" > .commitlint.config.js +``` + +Consult [docs/cli](../../docs/cli) for comprehensive documentation. diff --git a/@commitlint/commitlint-config-angular/CHANGELOG.md b/@commitlint/commitlint-config-angular/CHANGELOG.md new file mode 100644 index 0000000000..503de2dde0 --- /dev/null +++ b/@commitlint/commitlint-config-angular/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* alias config packages ([67ba5c6](https://github.com/marionebl/commitlint/commit/67ba5c6)) + + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* alias config packages ([67ba5c6](https://github.com/marionebl/commitlint/commit/67ba5c6)) diff --git a/@commitlint/commitlint-config-angular/index.js b/@commitlint/commitlint-config-angular/index.js new file mode 100644 index 0000000000..350c9f08f2 --- /dev/null +++ b/@commitlint/commitlint-config-angular/index.js @@ -0,0 +1 @@ +module.exports = require('@commitlint/config-angular'); diff --git a/@commitlint/commitlint-config-angular/package.json b/@commitlint/commitlint-config-angular/package.json new file mode 100644 index 0000000000..4dba67f7f7 --- /dev/null +++ b/@commitlint/commitlint-config-angular/package.json @@ -0,0 +1,29 @@ +{ + "name": "commitlint-config-angular", + "version": "3.0.1", + "description": "Shareable commitlint config enforcing the angular commit convention", + "scripts": { + "test": "exit 0", + "clean": "exit 0" + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "conventional-changelog", + "commitlint", + "commitlint-config", + "angular" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "dependencies": { + "@commitlint/config-angular": "^3.0.0" + } +} diff --git a/@commitlint/commitlint-config-lerna-scopes/CHANGELOG.md b/@commitlint/commitlint-config-lerna-scopes/CHANGELOG.md new file mode 100644 index 0000000000..503de2dde0 --- /dev/null +++ b/@commitlint/commitlint-config-lerna-scopes/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* alias config packages ([67ba5c6](https://github.com/marionebl/commitlint/commit/67ba5c6)) + + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* alias config packages ([67ba5c6](https://github.com/marionebl/commitlint/commit/67ba5c6)) diff --git a/@commitlint/commitlint-config-lerna-scopes/index.js b/@commitlint/commitlint-config-lerna-scopes/index.js new file mode 100644 index 0000000000..ff8392cc1b --- /dev/null +++ b/@commitlint/commitlint-config-lerna-scopes/index.js @@ -0,0 +1 @@ +module.exports = require('@commitlint/config-lerna-scopes'); diff --git a/@commitlint/commitlint-config-lerna-scopes/package.json b/@commitlint/commitlint-config-lerna-scopes/package.json new file mode 100644 index 0000000000..4b529c3434 --- /dev/null +++ b/@commitlint/commitlint-config-lerna-scopes/package.json @@ -0,0 +1,29 @@ +{ + "name": "commitlint-config-lerna-scopes", + "version": "3.0.1", + "description": "Shareable commitlint config enforcing lerna package names as scopes", + "scripts": { + "test": "exit 0", + "clean": "exit 0" + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "conventional-changelog", + "commitlint", + "commitlint-config", + "angular" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "dependencies": { + "@commitlint/config-lerna-scopes": "^3.0.0" + } +} diff --git a/@commitlint/commitlint-config-patternplate/CHANGELOG.md b/@commitlint/commitlint-config-patternplate/CHANGELOG.md new file mode 100644 index 0000000000..503de2dde0 --- /dev/null +++ b/@commitlint/commitlint-config-patternplate/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* alias config packages ([67ba5c6](https://github.com/marionebl/commitlint/commit/67ba5c6)) + + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* alias config packages ([67ba5c6](https://github.com/marionebl/commitlint/commit/67ba5c6)) diff --git a/@commitlint/commitlint-config-patternplate/index.js b/@commitlint/commitlint-config-patternplate/index.js new file mode 100644 index 0000000000..609d2cc13d --- /dev/null +++ b/@commitlint/commitlint-config-patternplate/index.js @@ -0,0 +1 @@ +module.exports = require('@commitlint/config-patternplate'); diff --git a/@commitlint/commitlint-config-patternplate/package.json b/@commitlint/commitlint-config-patternplate/package.json new file mode 100644 index 0000000000..413f162e01 --- /dev/null +++ b/@commitlint/commitlint-config-patternplate/package.json @@ -0,0 +1,29 @@ +{ + "name": "commitlint-config-patternplate", + "version": "3.0.1", + "description": "Lint your commits, patternplate-style", + "scripts": { + "test": "exit 0", + "clean": "exit 0" + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "conventional-changelog", + "commitlint", + "commitlint-config", + "angular" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "dependencies": { + "@commitlint/config-patternplate": "^3.0.0" + } +} diff --git a/@commitlint/config-angular/CHANGELOG.md b/@commitlint/config-angular/CHANGELOG.md new file mode 100644 index 0000000000..f30746b685 --- /dev/null +++ b/@commitlint/config-angular/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +# 3.0.0 (2017-07-10) diff --git a/@commitlint/config-angular/index.js b/@commitlint/config-angular/index.js new file mode 100644 index 0000000000..24dd31573c --- /dev/null +++ b/@commitlint/config-angular/index.js @@ -0,0 +1,64 @@ +module.exports = { + rules: { + 'body-leading-blank': [1, + 'always' + ], + 'body-tense': [1, + 'always', + ['present-imperative'] + ], + 'footer-leading-blank': [1, + 'always' + ], + 'footer-tense': [1, + 'always', + ['present-imperative'] + ], + 'header-max-length': [2, + 'always', + 72 + ], + lang: [1, + 'always', + 'eng' + ], + 'scope-case': [2, + 'always', + 'lowerCase' + ], + 'subject-empty': [2, + 'never' + ], + 'subject-full-stop': [2, + 'never', + '.' + ], + 'subject-tense': [1, + 'always', + ['present-imperative'] + ], + 'type-case': [2, + 'always', + 'lowerCase' + ], + 'type-empty': [2, + 'never' + ], + 'type-enum': [2, + 'always', + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test' + ] + ] + } +}; diff --git a/@commitlint/config-angular/package.json b/@commitlint/config-angular/package.json new file mode 100644 index 0000000000..4befe4b1d3 --- /dev/null +++ b/@commitlint/config-angular/package.json @@ -0,0 +1,30 @@ +{ + "name": "@commitlint/config-angular", + "version": "3.0.0", + "description": "Shareable commitlint config enforcing the angular commit convention", + "scripts": { + "pretest": "dep-check", + "test": "exit 0", + "clean": "exit 0" + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "conventional-changelog", + "commitlint", + "commitlint-config", + "angular" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "devDependencies": { + "@commitlint/utils": "^3.0.0" + } +} diff --git a/@commitlint/config-angular/readme.md b/@commitlint/config-angular/readme.md new file mode 100644 index 0000000000..964e1df158 --- /dev/null +++ b/@commitlint/config-angular/readme.md @@ -0,0 +1,91 @@ +> Lint your commits, angular-style + +# @commitlint/config-angular +Shareable `commitlint` config enforcing the angular commit convention. + +## Getting started + +```sh +npm install --save-dev @commitlint/config-angular @commitlint/cli +echo "module.exports = {extends: ['@commitlint/config-angular']};" > .commitlint.config.js +``` + +## Rules +### Problems + +The following rules are considered problems for `@commitlint/config-angular` and will yield a non-zero exit code when not met. + +#### type-enum +* **condition**: `type` is found in value +* **rule**: `always` +* **value** + + ```js + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test' + ] + ``` + +#### type-case +* **description**: `type` is in case `value` +* **rule**: `always` +* **value** + ```js + 'lowerCase' + ``` + +#### type-empty +* **condition**: `type` is empty +* **rule**: `never` + +#### scope-case +* **condition**: `scope` is in case `value` +* **rule**: `always` +```js + 'lowerCase' +``` + +#### subject-empty +* **condition**: `subject` is empty +* **rule**: `never` + +#### subject-full-stop +* **condition**: `subject` ends with `value` +* **rule**: `never` +* **value** +```js + '.' +``` + +#### header-max-length +* **condition**: `header` has `value` or less characters +* **rule**: `always` +* **value** +```js + 72 +``` + +### Warnings +The following rules are considered warnings for `@commitlint/config-angular` and will print warning messages when not met. + +#### body-leading-blank +* **condition**: Body begins with blank line +* **rule**: `always` + +#### lang +* **condition**: `subject` is of language `value` +* **rule**: `always` +* **value** +```js + eng +``` diff --git a/@commitlint/config-lerna-scopes/CHANGELOG.md b/@commitlint/config-lerna-scopes/CHANGELOG.md new file mode 100644 index 0000000000..d219463af0 --- /dev/null +++ b/@commitlint/config-lerna-scopes/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +# 3.0.0 (2017-07-10) + + +### Features + +* **config-lerna-scopes:** support non-standard lerna repos ([903df4b](https://github.com/marionebl/commitlint/commit/903df4b)) diff --git a/@commitlint/config-lerna-scopes/index.js b/@commitlint/config-lerna-scopes/index.js new file mode 100644 index 0000000000..f08e564506 --- /dev/null +++ b/@commitlint/config-lerna-scopes/index.js @@ -0,0 +1,15 @@ +const Repository = require('lerna/lib/Repository'); + +module.exports = { + utils: {getPackages}, + rules: { + 'scope-enum': () => [2, 'always', getPackages()] + } +}; + +function getPackages() { + const repo = new Repository(process.cwd()); + return repo.packages + .map(pkg => pkg.name) + .map(name => name.charAt(0) === '@' ? name.split('/')[1] : name); +} diff --git a/@commitlint/config-lerna-scopes/package.json b/@commitlint/config-lerna-scopes/package.json new file mode 100644 index 0000000000..d60e4c24d1 --- /dev/null +++ b/@commitlint/config-lerna-scopes/package.json @@ -0,0 +1,33 @@ +{ + "name": "@commitlint/config-lerna-scopes", + "version": "3.0.0", + "description": "Shareable commitlint config enforcing lerna package names as scopes", + "scripts": { + "pretest": "dep-check", + "test": "exit 0", + "clean": "exit 0" + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "conventional-changelog", + "commitlint", + "commitlint-config", + "lerna" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "dependencies": { + "lerna": "^2.0.0" + }, + "devDependencies": { + "@commitlint/utils": "^3.0.0" + } +} diff --git a/@commitlint/config-lerna-scopes/readme.md b/@commitlint/config-lerna-scopes/readme.md new file mode 100644 index 0000000000..7bf094a9cb --- /dev/null +++ b/@commitlint/config-lerna-scopes/readme.md @@ -0,0 +1,39 @@ +> Lint your commits, angular-style + +# @commitlint/config-lerna-scopes +Shareable `commitlint` config enforcing lerna package names as scopes. + +## Getting started +```sh +npm install --save-dev @commitlint/config-lerna-scopes @commitlint/cli +echo "module.exports = {extends: ['@commitlint/config-lerna-scopes']};" > .commitlint.config.js +``` + +## Examples + +``` +❯ cat .commitlintrc +{ + "extends": ["lerna-scopes"] +} + +❯ tree packages + +packages +├── api +├── app +└── web + +❯ echo "chore(api): fix something in api's build" | commitlint +⧗ input: chore(api): fix something in api's build +✔ found 0 problems, 0 warnings + +❯ echo "chore(foo): this won't pass" | commitlint +⧗ input: chore(foo): this won't pass +✖ scope must be one of [api, app, web] [scope-enum] +✖ found 1 problems, 0 warnings + +❯ echo "chore: do some general maintenance" | commitlint +⧗ input: chore: do some general maintenance +✔ found 0 problems, 0 warnings +``` diff --git a/@commitlint/config-patternplate/CHANGELOG.md b/@commitlint/config-patternplate/CHANGELOG.md new file mode 100644 index 0000000000..f30746b685 --- /dev/null +++ b/@commitlint/config-patternplate/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +# 3.0.0 (2017-07-10) diff --git a/@commitlint/config-patternplate/index.js b/@commitlint/config-patternplate/index.js new file mode 100644 index 0000000000..916a27dbea --- /dev/null +++ b/@commitlint/config-patternplate/index.js @@ -0,0 +1,25 @@ +const path = require('path'); +const globby = require('globby'); +const merge = require('lodash').merge; + +function pathToId(root, filePath) { + const relativePath = path.relative(root, filePath); + return path.dirname(relativePath).split(path.sep).join('/'); +} + +function getPatternIDs() { + const root = path.resolve(process.cwd(), './patterns'); + const glob = path.resolve(root, '**/pattern.json'); + return globby(glob) + .then(results => results.map(result => pathToId(root, result))); +} + +module.exports = merge( + require('@commitlint/config-angular'), + { + rules: { + 'scope-enum': () => getPatternIDs() + .then(ids => [2, 'always', ids.concat(['system'])]) + } + } +); diff --git a/@commitlint/config-patternplate/package.json b/@commitlint/config-patternplate/package.json new file mode 100644 index 0000000000..ce856ba8d5 --- /dev/null +++ b/@commitlint/config-patternplate/package.json @@ -0,0 +1,35 @@ +{ + "name": "@commitlint/config-patternplate", + "version": "3.0.0", + "description": "Lint your commits, patternplate-style", + "scripts": { + "pretest": "dep-check", + "test": "exit 0", + "clean": "exit 0" + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "conventional-changelog", + "commitlint", + "commitlint-config", + "patternplate" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "dependencies": { + "@commitlint/config-angular": "^3.0.0", + "globby": "^4.0.0", + "lodash": "^4.5.1" + }, + "devDependencies": { + "@commitlint/utils": "^3.0.0" + } +} diff --git a/@commitlint/config-patternplate/readme.md b/@commitlint/config-patternplate/readme.md new file mode 100644 index 0000000000..5dc3e19b43 --- /dev/null +++ b/@commitlint/config-patternplate/readme.md @@ -0,0 +1,21 @@ +> Lint your commits, patternplate-style + +# @commitlint/config-patternplate +Shareable `commitlint` config enforcing the patternplate commit convention. + +## Getting started +```sh +npm install --save-dev @commitlint/config-patternplate @commitlint/cli +echo "module.exports = {extends: ['@commitlint/config-patternplate']};" > .commitlint.config.js +``` + +## Rules +`@commitlint/config-patternplate` extends the [shareable angular config](../config-angular#rules). Additionally these rules apply: + +### Problems +The following rules are considered problems for `@commitlint/config-patterplate` and will yield a non-zero exit code when not met. + +#### scope-enum +* **description**: `scope` is found in `value` +* **rule**: `always` +* **value**: determined based on pattern tree. `system` and all pattern ids present in `patterns` are allowed diff --git a/@commitlint/core/CHANGELOG.md b/@commitlint/core/CHANGELOG.md new file mode 100644 index 0000000000..28d27e2647 --- /dev/null +++ b/@commitlint/core/CHANGELOG.md @@ -0,0 +1,53 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + +### Bug Fixes + +* use conventional-changelog-angular again ([2bcc941](https://github.com/marionebl/commitlint/commit/2bcc941)) + + + + +# 3.0.0 (2017-07-10) + + +### Bug Fixes + +* enable recursive relative extends ([e715d86](https://github.com/marionebl/commitlint/commit/e715d86)) +* ensure node4 compat ([a5e658a](https://github.com/marionebl/commitlint/commit/a5e658a)) +* **core:** ignore version commits with leading whitespace ([ead20b6](https://github.com/marionebl/commitlint/commit/ead20b6)) +* **core:** resolve extends relative to config file ([2257a80](https://github.com/marionebl/commitlint/commit/2257a80)) + + +### Features + +* **core:** do not prefix relative extends ([dfb661f](https://github.com/marionebl/commitlint/commit/dfb661f)) +* **core:** readd support for .conventional-changelog-lintrc ([030298e](https://github.com/marionebl/commitlint/commit/030298e)) +* **core:** support conventional-changelog-lint-config-* ([3092ce5](https://github.com/marionebl/commitlint/commit/3092ce5)) + + + + + +# 3.0.0 (2017-07-10) + + +### Bug Fixes + +* enable recursive relative extends ([e715d86](https://github.com/marionebl/commitlint/commit/e715d86)) +* ensure node4 compat ([a5e658a](https://github.com/marionebl/commitlint/commit/a5e658a)) +* **core:** ignore version commits with leading whitespace ([ead20b6](https://github.com/marionebl/commitlint/commit/ead20b6)) +* **core:** resolve extends relative to config file ([2257a80](https://github.com/marionebl/commitlint/commit/2257a80)) + + +### Features + +* **core:** do not prefix relative extends ([dfb661f](https://github.com/marionebl/commitlint/commit/dfb661f)) +* **core:** readd support for .conventional-changelog-lintrc ([030298e](https://github.com/marionebl/commitlint/commit/030298e)) +* **core:** support conventional-changelog-lint-config-* ([3092ce5](https://github.com/marionebl/commitlint/commit/3092ce5)) diff --git a/fixtures/empty-file/.conventional-changelog-lintrc b/@commitlint/core/fixtures/empty-file/commitlint.config.js similarity index 100% rename from fixtures/empty-file/.conventional-changelog-lintrc rename to @commitlint/core/fixtures/empty-file/commitlint.config.js diff --git a/@commitlint/core/fixtures/empty-object-file/commitlint.config.js b/@commitlint/core/fixtures/empty-object-file/commitlint.config.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/@commitlint/core/fixtures/empty-object-file/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/@commitlint/core/fixtures/extends-empty/commitlint.config.js b/@commitlint/core/fixtures/extends-empty/commitlint.config.js new file mode 100644 index 0000000000..8461bcc039 --- /dev/null +++ b/@commitlint/core/fixtures/extends-empty/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [] +}; diff --git a/@commitlint/core/fixtures/extends-invalid/commitlint.config.js b/@commitlint/core/fixtures/extends-invalid/commitlint.config.js new file mode 100644 index 0000000000..547e81f5c3 --- /dev/null +++ b/@commitlint/core/fixtures/extends-invalid/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['____foooooo'] +}; diff --git a/@commitlint/core/fixtures/legacy/.conventional-changelog-lintrc b/@commitlint/core/fixtures/legacy/.conventional-changelog-lintrc new file mode 100644 index 0000000000..39911211f1 --- /dev/null +++ b/@commitlint/core/fixtures/legacy/.conventional-changelog-lintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "legacy": true + } +} diff --git a/@commitlint/core/fixtures/overridden-type-enums/commitlint.config.js b/@commitlint/core/fixtures/overridden-type-enums/commitlint.config.js new file mode 100644 index 0000000000..99dbaeda8a --- /dev/null +++ b/@commitlint/core/fixtures/overridden-type-enums/commitlint.config.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./extended'], + rules: { + 'type-enum': [2, 'always', ['a', 'b', 'c', 'd']] + } +}; diff --git a/@commitlint/core/fixtures/overridden-type-enums/extended.js b/@commitlint/core/fixtures/overridden-type-enums/extended.js new file mode 100644 index 0000000000..87103567fd --- /dev/null +++ b/@commitlint/core/fixtures/overridden-type-enums/extended.js @@ -0,0 +1,20 @@ +module.exports = { + rules: { + 'type-enum': [2, + 'always', + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test' + ] + ] + } +}; diff --git a/@commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc b/@commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc new file mode 100644 index 0000000000..39911211f1 --- /dev/null +++ b/@commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "legacy": true + } +} diff --git a/@commitlint/core/fixtures/overriden-legacy/commitlint.config.js b/@commitlint/core/fixtures/overriden-legacy/commitlint.config.js new file mode 100644 index 0000000000..0625ee6360 --- /dev/null +++ b/@commitlint/core/fixtures/overriden-legacy/commitlint.config.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + legacy: false + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends/commitlint.config.js b/@commitlint/core/fixtures/recursive-extends/commitlint.config.js new file mode 100644 index 0000000000..7564fdc432 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends/commitlint.config.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./first-extended'], + rules: { + zero: 0 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends/first-extended/index.js b/@commitlint/core/fixtures/recursive-extends/first-extended/index.js new file mode 100644 index 0000000000..4317428ad1 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends/first-extended/index.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./second-extended'], + rules: { + one: 1 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends/first-extended/second-extended/index.js b/@commitlint/core/fixtures/recursive-extends/first-extended/second-extended/index.js new file mode 100644 index 0000000000..d199d354da --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends/first-extended/second-extended/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + two: 2 + } +}; diff --git a/@commitlint/core/fixtures/trash-extend/commitlint.config.js b/@commitlint/core/fixtures/trash-extend/commitlint.config.js new file mode 100644 index 0000000000..3b41dace2b --- /dev/null +++ b/@commitlint/core/fixtures/trash-extend/commitlint.config.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./one'], + zero: '0', + rules: { + zero: 0 + } +}; diff --git a/@commitlint/core/fixtures/trash-extend/one.js b/@commitlint/core/fixtures/trash-extend/one.js new file mode 100644 index 0000000000..60f3a3530d --- /dev/null +++ b/@commitlint/core/fixtures/trash-extend/one.js @@ -0,0 +1,6 @@ +module.exports = { + one: 1, + rules: { + one: 1 + } +}; diff --git a/@commitlint/core/fixtures/trash-file/commitlint.config.js b/@commitlint/core/fixtures/trash-file/commitlint.config.js new file mode 100644 index 0000000000..a7a8e43bd8 --- /dev/null +++ b/@commitlint/core/fixtures/trash-file/commitlint.config.js @@ -0,0 +1,8 @@ +module.exports = { + foo: 'bar', + baz: 'bar', + rules: { + foo: 'bar', + baz: 'bar' + } +}; diff --git a/@commitlint/core/package.json b/@commitlint/core/package.json new file mode 100644 index 0000000000..03ead9dfa3 --- /dev/null +++ b/@commitlint/core/package.json @@ -0,0 +1,149 @@ +{ + "name": "@commitlint/core", + "version": "3.0.1", + "description": "Lint your commit messages", + "main": "lib/index.js", + "scripts": { + "pretest": "dep-check", + "test": "ava -c 4", + "build": "cross-env NODE_ENV=production babel src --out-dir lib", + "clean": "rimraf lib", + "prepublish": "npm run build" + }, + "ava": { + "files": [ + "src/**/*.test.js", + "!lib/**/*" + ], + "source": [ + "src/**/*.js", + "!lib/**/*" + ], + "babel": "inherit", + "require": [ + "babel-register", + "babel-polyfill" + ] + }, + "babel": { + "env": { + "development": { + "sourceMaps": "inline", + "plugins": [ + "add-module-exports", + "istanbul", + [ + "transform-runtime", + { + "polyfill": false, + "regenerator": true + } + ] + ] + }, + "production": { + "ignore": [ + "**/*.test.js" + ] + } + }, + "presets": [ + [ + "env", + { + "targets": { + "node": 4 + } + } + ], + "stage-0" + ], + "plugins": [ + "add-module-exports", + [ + "transform-runtime", + { + "polyfill": false, + "regenerator": true + } + ] + ] + }, + "xo": false, + "nyc": { + "all": true, + "sourceMap": false, + "instrument": false, + "include": [ + "source/**/*.js" + ] + }, + "engines": { + "node": ">=4" + }, + "repository": { + "type": "git", + "url": "https://github.com/marionebl/commitlint.git" + }, + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "keywords": [ + "conventional-changelog", + "commitlint", + "library", + "core" + ], + "author": { + "name": "Mario Nebl", + "email": "hello@herebecode.com" + }, + "license": "MIT", + "devDependencies": { + "@commitlint/utils": "^3.0.0", + "ansi-styles": "3.1.0", + "ava": "0.18.2", + "babel-cli": "^6.18.0", + "babel-plugin-add-module-exports": "0.2.1", + "babel-plugin-istanbul": "4.1.3", + "babel-plugin-transform-runtime": "6.23.0", + "babel-polyfill": "^6.20.0", + "babel-preset-env": "^1.2.1", + "babel-preset-stage-0": "^6.16.0", + "babel-register": "6.24.1", + "conventional-changelog-cli": "1.2.0", + "conventional-recommended-bump": "0.3.0", + "cross-env": "^5.0.1", + "cz-conventional-changelog-lint": "0.1.3", + "denodeify": "1.2.1", + "dependency-check": "2.7.0", + "eslint-plugin-flow-check": "1.1.1", + "execa": "0.6.3", + "globby": "6.1.0", + "has-ansi": "3.0.0", + "import-from": "2.1.0", + "nyc": "10.3.2", + "path-exists": "3.0.0", + "resolve-from": "3.0.0", + "rimraf": "2.6.1", + "xo": "0.18.2" + }, + "dependencies": { + "babel-runtime": "^6.23.0", + "chalk": "^2.0.1", + "conventional-changelog-angular": "^1.3.3", + "conventional-commits-parser": "^1.3.0", + "franc": "^2.0.0", + "git-raw-commits": "^1.1.2", + "git-toplevel": "^1.1.1", + "import-from": "^2.1.0", + "lodash": "^4.17.4", + "mz": "^2.6.0", + "path-exists": "^3.0.0", + "pos": "^0.4.2", + "rc": "^1.1.7", + "resolve-from": "^3.0.0", + "semver": "^5.3.0" + } +} diff --git a/@commitlint/core/readme.md b/@commitlint/core/readme.md new file mode 100644 index 0000000000..0dca919199 --- /dev/null +++ b/@commitlint/core/readme.md @@ -0,0 +1,11 @@ +> Lint commit messages + +# @commitlint/core + +## Getting started + +```shell +npm install --save-dev @commitlint/core +``` + +Consult [docs/api](../../docs/api) for comprehensive documentation. diff --git a/source/library/format.js b/@commitlint/core/src/format.js similarity index 93% rename from source/library/format.js rename to @commitlint/core/src/format.js index c8da5c6a7c..781e8b15ea 100644 --- a/source/library/format.js +++ b/@commitlint/core/src/format.js @@ -12,7 +12,7 @@ export default function format(report = {}, options = {}) { const sign = signs[problem.level] || ''; const color = colors[problem.level] || 'white'; const decoration = enabled ? chalk[color](sign) : sign; - const name = chalk.grey(`[${problem.name}]`); + const name = enabled ? chalk.grey(`[${problem.name}]`) : `[${problem.name}]`; return `${decoration} ${problem.message} ${name}`; }); diff --git a/source/library/format.test.js b/@commitlint/core/src/format.test.js similarity index 100% rename from source/library/format.test.js rename to @commitlint/core/src/format.test.js diff --git a/@commitlint/core/src/index.js b/@commitlint/core/src/index.js new file mode 100644 index 0000000000..fa9b907951 --- /dev/null +++ b/@commitlint/core/src/index.js @@ -0,0 +1,6 @@ +import format from './format'; +import lint from './lint'; +import load from './load'; +import read from './read'; + +export {format, load, read, lint}; diff --git a/@commitlint/core/src/index.test.js b/@commitlint/core/src/index.test.js new file mode 100644 index 0000000000..98b83f2cc7 --- /dev/null +++ b/@commitlint/core/src/index.test.js @@ -0,0 +1,21 @@ +import test from 'ava'; + +test('exports format method', t => { + const {format} = require('.'); + t.is(typeof format, 'function'); +}); + +test('exports lint method', t => { + const {lint} = require('.'); + t.is(typeof lint, 'function'); +}); + +test('exports load method', t => { + const {load} = require('.'); + t.is(typeof load, 'function'); +}); + +test('exports read method', t => { + const {read} = require('.'); + t.is(typeof read, 'function'); +}); diff --git a/source/library/ensure-case.js b/@commitlint/core/src/library/ensure-case.js similarity index 100% rename from source/library/ensure-case.js rename to @commitlint/core/src/library/ensure-case.js diff --git a/source/library/ensure-case.test.js b/@commitlint/core/src/library/ensure-case.test.js similarity index 100% rename from source/library/ensure-case.test.js rename to @commitlint/core/src/library/ensure-case.test.js diff --git a/source/library/ensure-enum.js b/@commitlint/core/src/library/ensure-enum.js similarity index 100% rename from source/library/ensure-enum.js rename to @commitlint/core/src/library/ensure-enum.js diff --git a/source/library/ensure-enum.test.js b/@commitlint/core/src/library/ensure-enum.test.js similarity index 100% rename from source/library/ensure-enum.test.js rename to @commitlint/core/src/library/ensure-enum.test.js diff --git a/source/library/ensure-language.js b/@commitlint/core/src/library/ensure-language.js similarity index 100% rename from source/library/ensure-language.js rename to @commitlint/core/src/library/ensure-language.js diff --git a/source/library/ensure-language.test.js b/@commitlint/core/src/library/ensure-language.test.js similarity index 100% rename from source/library/ensure-language.test.js rename to @commitlint/core/src/library/ensure-language.test.js diff --git a/source/library/ensure-max-length.js b/@commitlint/core/src/library/ensure-max-length.js similarity index 100% rename from source/library/ensure-max-length.js rename to @commitlint/core/src/library/ensure-max-length.js diff --git a/source/library/ensure-max-length.test.js b/@commitlint/core/src/library/ensure-max-length.test.js similarity index 100% rename from source/library/ensure-max-length.test.js rename to @commitlint/core/src/library/ensure-max-length.test.js diff --git a/source/library/ensure-min-length.js b/@commitlint/core/src/library/ensure-min-length.js similarity index 100% rename from source/library/ensure-min-length.js rename to @commitlint/core/src/library/ensure-min-length.js diff --git a/source/library/ensure-min-length.test.js b/@commitlint/core/src/library/ensure-min-length.test.js similarity index 100% rename from source/library/ensure-min-length.test.js rename to @commitlint/core/src/library/ensure-min-length.test.js diff --git a/source/library/ensure-not-empty.js b/@commitlint/core/src/library/ensure-not-empty.js similarity index 100% rename from source/library/ensure-not-empty.js rename to @commitlint/core/src/library/ensure-not-empty.js diff --git a/source/library/ensure-not-empty.test.js b/@commitlint/core/src/library/ensure-not-empty.test.js similarity index 100% rename from source/library/ensure-not-empty.test.js rename to @commitlint/core/src/library/ensure-not-empty.test.js diff --git a/source/library/ensure-tense.js b/@commitlint/core/src/library/ensure-tense.js similarity index 95% rename from source/library/ensure-tense.js rename to @commitlint/core/src/library/ensure-tense.js index 872d832a2c..158f5f247d 100644 --- a/source/library/ensure-tense.js +++ b/@commitlint/core/src/library/ensure-tense.js @@ -1,4 +1,5 @@ import {Lexer, Tagger} from 'pos'; +import {entries} from 'lodash'; const lexer = new Lexer(); const tagger = new Tagger(); @@ -49,7 +50,7 @@ export default (input, allowed, options = {}) => { .filter(Boolean) .map(verb => { const [lemma, tag] = verb; - const tense = Object.entries(tenses) + const tense = entries(tenses) .filter(item => { const [, tags] = item; return tags.indexOf(tag) > -1; diff --git a/source/library/ensure-tense.test.js b/@commitlint/core/src/library/ensure-tense.test.js similarity index 100% rename from source/library/ensure-tense.test.js rename to @commitlint/core/src/library/ensure-tense.test.js diff --git a/source/library/execute-rule.js b/@commitlint/core/src/library/execute-rule.js similarity index 100% rename from source/library/execute-rule.js rename to @commitlint/core/src/library/execute-rule.js diff --git a/source/library/execute-rule.test.js b/@commitlint/core/src/library/execute-rule.test.js similarity index 100% rename from source/library/execute-rule.test.js rename to @commitlint/core/src/library/execute-rule.test.js diff --git a/source/library/is-ignored.js b/@commitlint/core/src/library/is-ignored.js similarity index 91% rename from source/library/is-ignored.js rename to @commitlint/core/src/library/is-ignored.js index 97df674b59..df6f4d88d0 100644 --- a/source/library/is-ignored.js +++ b/@commitlint/core/src/library/is-ignored.js @@ -4,7 +4,7 @@ const WILDCARDS = [ c => c.match(/^(Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?))$)/), c => c.match(/^(R|r)evert (.*)/), c => c.match(/^(fixup|squash)!/), - c => semver.valid(c) + c => semver.valid(c.trim()) ]; export default function isIgnored(commit = '') { diff --git a/source/library/is-ignored.test.js b/@commitlint/core/src/library/is-ignored.test.js similarity index 96% rename from source/library/is-ignored.test.js rename to @commitlint/core/src/library/is-ignored.test.js index 132bb0263a..13fb2e1bab 100644 --- a/source/library/is-ignored.test.js +++ b/@commitlint/core/src/library/is-ignored.test.js @@ -39,6 +39,8 @@ test('should return true for npm version commits', t => { t.true(isIgnored(`0.0.1-some-crazy-tag.0`)); t.true(isIgnored(`0.0.1-some-crazy-tag.999`)); t.true(isIgnored(`0.0.1-1e69d54`)); + t.true(isIgnored(`v0.0.1`)); + t.true(isIgnored(` v3.0.0`)); }); test('should return true fixup commits', t => { diff --git a/@commitlint/core/src/library/parse.js b/@commitlint/core/src/library/parse.js new file mode 100644 index 0000000000..c86262b582 --- /dev/null +++ b/@commitlint/core/src/library/parse.js @@ -0,0 +1,17 @@ +import {sync} from 'conventional-commits-parser'; + +export default parse; + +async function parse(message, parser = sync) { + // Prevent conventional-changelog-angular from spamming startup + // TODO: Remove when https://github.com/conventional-changelog/conventional-changelog/pull/206 lands + const _error = console.error; + console.error = () => {}; + const opts = require('conventional-changelog-angular'); + console.error = _error; + + const {parserOpts} = await opts; + const parsed = parser(message, parserOpts); + parsed.raw = message; + return parsed; +} diff --git a/@commitlint/core/src/library/parse.test.js b/@commitlint/core/src/library/parse.test.js new file mode 100644 index 0000000000..03382ce84d --- /dev/null +++ b/@commitlint/core/src/library/parse.test.js @@ -0,0 +1,79 @@ +import test from 'ava'; +import parse from './parse'; + +test('throws when called without params', t => { + t.throws(parse(), /Expected a raw commit/); +}); + +test('throws when called with empty message', t => { + t.throws(parse(''), /Expected a raw commit/); +}); + +test('returns object with raw message', async t => { + const message = 'type(scope): subject'; + const actual = await parse(message); + t.is(actual.raw, message); +}); + +test('calls parser with message and passed options', t => { + const message = 'message'; + + parse(message, m => { + t.is(message, m); + return {}; + }); +}); + +test('passes object up from parser function', async t => { + const message = 'message'; + const result = {}; + const actual = await parse(message, () => result); + t.is(actual, result); +}); + +test('returns object with expected keys', async t => { + const message = 'message'; + const actual = await parse(message); + const expected = { + body: null, + footer: null, + header: 'message', + mentions: [], + merge: null, + notes: [], + raw: 'message', + references: [], + revert: null, + scope: null, + subject: null, + type: null + }; + t.deepEqual(actual, expected); +}); + +test('uses angular grammar', async t => { + const message = 'type(scope): subject'; + const actual = await parse(message); + const expected = { + body: null, + footer: null, + header: 'type(scope): subject', + mentions: [], + merge: null, + notes: [], + raw: 'type(scope): subject', + references: [], + revert: null, + scope: 'scope', + subject: 'subject', + type: 'type' + }; + t.deepEqual(actual, expected); +}); + +test('supports scopes with /', async t => { + const message = 'type(some/scope): subject'; + const actual = await parse(message); + t.is(actual.scope, 'some/scope'); + t.is(actual.subject, 'subject'); +}); diff --git a/@commitlint/core/src/library/resolve-extends.js b/@commitlint/core/src/library/resolve-extends.js new file mode 100644 index 0000000000..a97cc3879e --- /dev/null +++ b/@commitlint/core/src/library/resolve-extends.js @@ -0,0 +1,62 @@ +import path from 'path'; +import from from 'resolve-from'; +import {merge, omit} from 'lodash'; + +// Resolve extend configs +export default function resolveExtends(config = {}, context = {}) { + const {extends: e} = config; + const extended = loadExtends(config, context) + .reduceRight((r, c) => merge(r, omit(c, 'extends')), e ? {extends: e} : {}); + + // Remove deprecation warning in version 3 + if (typeof config === 'object' && 'wildcards' in config) { + console.warn(`'wildcards' found in top-level configuration ignored. Remove them from your config to silence this warning.`); + } + + return merge({}, extended, config); +} + +// (any, string, string, Function) => any[]; +function loadExtends(config = {}, context = {}) { + return (config.extends || []).reduce((configs, raw) => { + const load = context.require || require; + const resolved = resolveConfig(raw, context); + const c = load(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: path.dirname(resolved) + }); + + return [...configs, c, ...loadExtends(c, ctx)]; + }, []); +} + +function getId(raw = '', prefix = '') { + const first = raw.charAt(0); + const scoped = first === '@'; + const relative = first === '.'; + return (scoped || relative) ? raw : [prefix, raw].filter(String).join('-'); +} + +function resolveConfig(raw, context = {}) { + const resolve = context.resolve || resolveId; + const id = getId(raw, context.prefix); + + try { + return resolve(id, context); + } catch (err) { + const legacy = getId(raw, 'conventional-changelog-lint-config'); + const resolved = resolve(legacy, context); + console.warn(`Resolving ${raw} to legacy config ${legacy}. To silence this warning raise an issue at 'npm repo ${legacy}' to rename to ${id}.`); + return resolved; + } +} + +function resolveId(id, context = {}) { + return from(context.cwd || process.cwd(), id); +} diff --git a/@commitlint/core/src/library/resolve-extends.test.js b/@commitlint/core/src/library/resolve-extends.test.js new file mode 100644 index 0000000000..1ca4005062 --- /dev/null +++ b/@commitlint/core/src/library/resolve-extends.test.js @@ -0,0 +1,200 @@ +import test from 'ava'; +import resolveExtends from './resolve-extends'; + +const id = id => id; + +test('returns empty object when called without params', t => { + const actual = resolveExtends(); + t.deepEqual(actual, {}); +}); + +test('returns an equivalent object as passed in', t => { + const expected = {foo: 'bar'}; + const actual = resolveExtends(expected); + t.deepEqual(actual, expected); +}); + +test('uses empty prefix by default', t => { + const input = {extends: ['extender-name']}; + + resolveExtends(input, { + resolve: id, + require(id) { + t.is(id, 'extender-name'); + } + }); +}); + +test('uses prefix as configured', t => { + const input = {extends: ['extender-name']}; + + resolveExtends(input, { + prefix: 'prefix', + resolve: id, + require(id) { + t.is(id, 'prefix-extender-name'); + } + }); +}); + +test('ignores prefix for scoped extends', t => { + const input = {extends: ['@scope/extender-name']}; + + resolveExtends(input, { + prefix: 'prefix', + resolve: id, + require(id) { + t.is(id, '@scope/extender-name'); + } + }); +}); + +test('ignores prefix for relative extends', t => { + const input = {extends: ['./extender']}; + + resolveExtends(input, { + prefix: 'prefix', + resolve: id, + require(id) { + t.is(id, './extender'); + } + }); +}); + +test('propagates return value of require function', t => { + const input = {extends: ['extender-name']}; + const propagated = {foo: 'bar'}; + + const actual = resolveExtends(input, { + resolve: id, + require() { + return propagated; + } + }); + + t.is(actual.foo, 'bar'); +}); + +test('resolves extends recursively', t => { + const input = {extends: ['extender-name']}; + const actual = []; + + resolveExtends(input, { + resolve: id, + require(id) { + actual.push(id); + if (id === 'extender-name') { + return {extends: ['recursive-extender-name']}; + } + if (id === 'recursive-extender-name') { + return {foo: 'bar'}; + } + } + }); + + t.deepEqual(actual, ['extender-name', 'recursive-extender-name']); +}); + +test('uses prefix key recursively', t => { + const input = {extends: ['extender-name']}; + const actual = []; + + resolveExtends(input, { + prefix: 'prefix', + resolve: id, + require(id) { + actual.push(id); + if (id === 'prefix-extender-name') { + return {extends: ['recursive-extender-name']}; + } + if (id === 'prefix-recursive-extender-name') { + return {foo: 'bar'}; + } + } + }); + + t.deepEqual(actual, ['prefix-extender-name', 'prefix-recursive-extender-name']); +}); + +test('propagates contents recursively', t => { + const input = {extends: ['extender-name']}; + + const actual = resolveExtends(input, { + resolve: id, + require(id) { + if (id === 'extender-name') { + return {extends: ['recursive-extender-name'], foo: 'bar'}; + } + if (id === 'recursive-extender-name') { + return {baz: 'bar'}; + } + } + }); + + const expected = { + extends: ['extender-name'], + foo: 'bar', + baz: 'bar' + }; + + t.deepEqual(actual, expected); +}); + +test('extending contents should take precedence', t => { + const input = {extends: ['extender-name'], zero: 'root'}; + + const actual = resolveExtends(input, { + resolve: id, + require(id) { + if (id === 'extender-name') { + return {extends: ['recursive-extender-name'], zero: id, one: id}; + } + if (id === 'recursive-extender-name') { + return {extends: ['second-recursive-extender-name'], zero: id, one: id, two: id}; + } + if (id === 'second-recursive-extender-name') { + return {zero: id, one: id, two: id, three: id}; + } + } + }); + + const expected = { + extends: ['extender-name'], + zero: 'root', + one: 'extender-name', + two: 'recursive-extender-name', + three: 'second-recursive-extender-name' + }; + + t.deepEqual(actual, expected); +}); + +test('should fall back to conventional-changelog-lint-config prefix', t => { + const input = {extends: ['extender-name']}; + + const actual = resolveExtends(input, { + prefix: 'prefix', + resolve(id) { + if (id === 'conventional-changelog-lint-config-extender-name') { + return 'conventional-changelog-lint-config-extender-name'; + } + throw new Error(`Could not find module "*${id}"`); + }, + require(id) { + if (id === 'conventional-changelog-lint-config-extender-name') { + return { + rules: { + fallback: true + } + }; + } + } + }); + + t.deepEqual(actual, { + extends: ['extender-name'], + rules: { + fallback: true + } + }); +}); diff --git a/source/index.js b/@commitlint/core/src/lint.js similarity index 64% rename from source/index.js rename to @commitlint/core/src/lint.js index a4af1a0e7f..e29c394893 100644 --- a/source/index.js +++ b/@commitlint/core/src/lint.js @@ -1,16 +1,9 @@ -import ruleFunctions from './rules'; -import format from './library/format'; -import getConfiguration from './library/get-configuration'; -import getMessages from './library/get-messages'; -import getPreset from './library/get-preset'; +import {entries} from 'lodash'; import isIgnored from './library/is-ignored'; import parse from './library/parse'; +import implementations from './rules'; -export {format, getConfiguration, getMessages, getPreset}; - -export default async (message, options = {}) => { - const {configuration} = options; - +export default async (message, rules = {}) => { // Found a wildcard match, skip if (isIgnored(message)) { return { @@ -21,10 +14,10 @@ export default async (message, options = {}) => { } // Parse the commit message - const parsed = parse(message); + const parsed = await parse(message); // Validate against all rules - const results = Object.entries(configuration.rules) + const results = entries(rules) .filter(entry => { const [, [level]] = entry; return level > 0; @@ -38,7 +31,7 @@ export default async (message, options = {}) => { return null; } - const rule = ruleFunctions[name]; + const rule = implementations[name]; const [valid, message] = rule(parsed, when, value); return { diff --git a/@commitlint/core/src/lint.test.js b/@commitlint/core/src/lint.test.js new file mode 100644 index 0000000000..750e8cf739 --- /dev/null +++ b/@commitlint/core/src/lint.test.js @@ -0,0 +1,36 @@ +import test from 'ava'; +import lint from './lint'; + +test('throws without params', t => { + t.throws(lint()); +}); + +test('throws with empty message', t => { + t.throws(lint('')); +}); + +test('positive on stub message and no rule', async t => { + const actual = await lint('foo: bar'); + t.true(actual.valid); +}); + +test('positive on stub message and adhered rule', async t => { + const actual = await lint('foo: bar', { + 'type-enum': [2, 'always', ['foo']] + }); + t.true(actual.valid); +}); + +test('negative on stub message and broken rule', async t => { + const actual = await lint('foo: bar', { + 'type-enum': [2, 'never', ['foo']] + }); + t.false(actual.valid); +}); + +test('positive on ignored message and broken rule', async t => { + const actual = await lint('Revert "some bogus commit"', { + 'type-empty': [2, 'never'] + }); + t.true(actual.valid); +}); diff --git a/@commitlint/core/src/load.js b/@commitlint/core/src/load.js new file mode 100644 index 0000000000..7c7668e43b --- /dev/null +++ b/@commitlint/core/src/load.js @@ -0,0 +1,88 @@ +import path from 'path'; +import importFrom from 'import-from'; +import {entries, merge, mergeWith, pick} from 'lodash'; +import rc from 'rc'; +import resolveFrom from 'resolve-from'; + +import resolveExtends from './library/resolve-extends'; +import executeRule from './library/execute-rule'; + +const w = (a, b) => Array.isArray(b) ? b : undefined; +const valid = input => pick(input, 'extends', 'rules'); + +export default async (seed = {}) => { + // Obtain config from .rc files + const raw = file(); + + // Merge passed config with file based options + const config = valid(merge(raw, seed)); + const opts = merge({extends: [], rules: {}}, pick(config, 'extends')); + + // Resolve extends key + const extended = resolveExtends(opts, { + prefix: 'commitlint-config', + cwd: raw.config ? path.dirname(raw.config) : process.cwd() + }); + + const preset = valid(mergeWith({}, extended, config, w)); + + // Execute rule config functions if needed + const executed = await Promise.all(['rules'] + .map(key => { + return [key, preset[key]]; + }) + .map(async item => { + const [key, value] = item; + const executedValue = await Promise.all( + entries(value || {}) + .map(entry => executeRule(entry)) + ); + return [key, executedValue.reduce((registry, item) => { + const [key, value] = item; + return { + ...registry, + [key]: value + }; + }, {})]; + })); + + // Merge executed config keys into preset + return executed.reduce((registry, item) => { + const [key, value] = item; + return { + ...registry, + [key]: value + }; + }, preset); +}; + +function file() { + const legacy = rc('conventional-changelog-lint'); + const legacyFound = typeof legacy.config === 'string'; + + const found = resolveable('./commitlint.config'); + const raw = found ? importFrom(process.cwd(), './commitlint.config') : {}; + + if (legacyFound && !found) { + console.warn(`Using legacy ${path.relative(process.cwd(), legacy.config)}. Rename to commitlint.config.js to silence this warning.`); + } + + if (legacyFound && found) { + console.warn(`Ignored legacy ${path.relative(process.cwd(), legacy.config)} as commitlint.config.js superseeds it. Remove .conventional-changelog-lintrc to silence this warning.`); + } + + if (found) { + return raw; + } + + return legacy; +} + +function resolveable(id) { + try { + resolveFrom(process.cwd(), id); + return true; + } catch (err) { + return false; + } +} diff --git a/@commitlint/core/src/load.test.js b/@commitlint/core/src/load.test.js new file mode 100644 index 0000000000..5b955a99f3 --- /dev/null +++ b/@commitlint/core/src/load.test.js @@ -0,0 +1,104 @@ +import path from 'path'; +import test from 'ava'; + +import load from './load'; + +const cwd = process.cwd(); + +test.afterEach.always(t => { + t.context.back(); +}); + +test('extends-empty should have no rules', async t => { + t.context.back = chdir('fixtures/extends-empty'); + const actual = await load(); + t.deepEqual(actual.rules, {}); +}); + +test('uses seed as configured', async t => { + t.context.back = chdir('fixtures/extends-empty'); + const actual = await load({rules: {foo: 'bar'}}); + t.is(actual.rules.foo, 'bar'); +}); + +test('invalid extend should throw', t => { + t.context.back = chdir('fixtures/extends-invalid'); + t.throws(load()); +}); + +test('empty file should have no rules', async t => { + t.context.back = chdir('fixtures/empty-object-file'); + const actual = await load(); + t.deepEqual(actual.rules, {}); +}); + +test('empty file should extend nothing', async t => { + t.context.back = chdir('fixtures/empty-file'); + const actual = await load(); + t.deepEqual(actual.extends, []); +}); + +test('recursive extends', async t => { + t.context.back = chdir('fixtures/recursive-extends'); + const actual = await load(); + t.deepEqual(actual, { + extends: ['./first-extended'], + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('ignores unknow keys', async t => { + t.context.back = chdir('fixtures/trash-file'); + const actual = await load(); + t.deepEqual(actual, { + extends: [], + rules: { + foo: 'bar', + baz: 'bar' + } + }); +}); + +test('ignores unknow keys recursively', async t => { + t.context.back = chdir('fixtures/trash-extend'); + const actual = await load(); + t.deepEqual(actual, { + extends: ['./one'], + rules: { + zero: 0, + one: 1 + } + }); +}); + +test('supports legacy .conventional-changelog-lintrc', async t => { + t.context.back = chdir('fixtures/legacy'); + const actual = await load(); + t.deepEqual(actual, { + extends: [], + rules: { + legacy: true + } + }); +}); + +test('commitlint.config.js overrides .conventional-changelog-lintrc', async t => { + t.context.back = chdir('fixtures/overriden-legacy'); + const actual = await load(); + t.deepEqual(actual, { + extends: [], + rules: { + legacy: false + } + }); +}); + +function chdir(target) { + const to = path.resolve(cwd, target.split('/').join(path.sep)); + process.chdir(to); + return () => process.chdir(cwd); +} diff --git a/source/library/get-messages.js b/@commitlint/core/src/read.js similarity index 97% rename from source/library/get-messages.js rename to @commitlint/core/src/read.js index c9557aa666..3d284a631c 100644 --- a/source/library/get-messages.js +++ b/@commitlint/core/src/read.js @@ -29,7 +29,7 @@ async function getCommitMessages(settings) { } // Get commit messages from history -// Object => Promise> +// Object => Promise function getHistoryCommits(options) { return new Promise((resolve, reject) => { const data = []; diff --git a/source/library/get-messages.test.js b/@commitlint/core/src/read.test.js similarity index 89% rename from source/library/get-messages.test.js rename to @commitlint/core/src/read.test.js index 4be06cca50..e05f097fa0 100644 --- a/source/library/get-messages.test.js +++ b/@commitlint/core/src/read.test.js @@ -9,8 +9,8 @@ import {mkdir, writeFile} from 'mz/fs'; import exists from 'path-exists'; import rimraf from 'rimraf'; -import pkg from '../../package'; -import getMessages from './get-messages'; +import pkg from '../package'; +import read from './read'; const rm = denodeify(rimraf); @@ -32,7 +32,7 @@ test.serial('get edit commit message from git root', async t => { await execa('git', ['add', '.']); await execa('git', ['commit', '-m', 'alpha']); const expected = ['alpha\n\n']; - const actual = await getMessages({edit: true}); + const actual = await read({edit: true}); t.deepEqual(actual, expected); }); @@ -44,7 +44,7 @@ test.serial('get history commit messages', async t => { await execa('git', ['commit', '-m', 'remove alpha']); const expected = ['remove alpha\n\n', 'alpha\n\n']; - const actual = await getMessages({}); + const actual = await read({}); t.deepEqual(actual, expected); }); @@ -56,11 +56,11 @@ test.serial('get edit commit message from git subdirectory', async t => { await execa('git', ['commit', '-m', 'beta']); const expected = ['beta\n\n']; - const actual = await getMessages({edit: true}); + const actual = await read({edit: true}); t.deepEqual(actual, expected); }); -test.serial('get history commit messages from shallow clone', async t => { +test.serial.failing('get history commit messages from shallow clone', async t => { const [repo] = t.context.repos; await writeFile('alpha.txt', 'alpha'); @@ -70,7 +70,7 @@ test.serial('get history commit messages from shallow clone', async t => { const clone = await cloneRepository(pkg.repository.url, repo, '--depth', '1'); t.context.repos = [...t.context.repos, clone]; - const err = await t.throws(getMessages({from: 'master'})); + const err = await t.throws(read({from: 'master'})); t.true(err.message.indexOf('Could not get git history from shallow clone') > -1); }); diff --git a/source/rules/body-case.js b/@commitlint/core/src/rules/body-case.js similarity index 100% rename from source/rules/body-case.js rename to @commitlint/core/src/rules/body-case.js diff --git a/@commitlint/core/src/rules/body-case.test.js b/@commitlint/core/src/rules/body-case.test.js new file mode 100644 index 0000000000..d4b4f920f9 --- /dev/null +++ b/@commitlint/core/src/rules/body-case.test.js @@ -0,0 +1,89 @@ +import test from 'ava'; +import parse from '../library/parse'; +import bodyCase from './body-case'; + +const messages = { + empty: 'chore: subject', + lowercase: 'chore: subject\nbody', + mixedcase: 'chore: subject\nBody', + uppercase: 'chore: subject\nBODY' +}; + +const parsed = { + empty: parse(messages.empty), + lowercase: parse(messages.lowercase), + mixedcase: parse(messages.mixedcase), + uppercase: parse(messages.uppercase) +}; + +test('with empty body should succeed for "never lowercase"', async t => { + const [actual] = bodyCase(await parsed.empty, 'never', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with empty body should succeed for "always lowercase"', async t => { + const [actual] = bodyCase(await parsed.empty, 'always', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with empty body should succeed for "never uppercase"', async t => { + const [actual] = bodyCase(await parsed.empty, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with empty body should succeed for "always uppercase"', async t => { + const [actual] = bodyCase(await parsed.empty, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with lowercase body should fail for "never lowercase"', async t => { + const [actual] = bodyCase(await parsed.lowercase, 'never', 'lowercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with lowercase body should succeed for "always lowercase"', async t => { + const [actual] = bodyCase(await parsed.lowercase, 'always', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase body should succeed for "never lowercase"', async t => { + const [actual] = bodyCase(await parsed.mixedcase, 'never', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase body should fail for "always lowercase"', async t => { + const [actual] = bodyCase(await parsed.mixedcase, 'always', 'lowercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with mixedcase body should succeed for "never uppercase"', async t => { + const [actual] = bodyCase(await parsed.mixedcase, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase body should fail for "always uppercase"', async t => { + const [actual] = bodyCase(await parsed.mixedcase, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with uppercase body should fail for "never uppercase"', async t => { + const [actual] = bodyCase(await parsed.uppercase, 'never', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with lowercase body should succeed for "always uppercase"', async t => { + const [actual] = bodyCase(await parsed.uppercase, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); diff --git a/source/rules/body-empty.js b/@commitlint/core/src/rules/body-empty.js similarity index 100% rename from source/rules/body-empty.js rename to @commitlint/core/src/rules/body-empty.js diff --git a/@commitlint/core/src/rules/body-empty.test.js b/@commitlint/core/src/rules/body-empty.test.js new file mode 100644 index 0000000000..e3b34ff1e9 --- /dev/null +++ b/@commitlint/core/src/rules/body-empty.test.js @@ -0,0 +1,49 @@ +import test from 'ava'; +import parse from '../library/parse'; +import bodyEmpty from './body-empty'; + +const messages = { + empty: 'chore: subject', + filled: 'chore: subject\nbody' +}; + +const parsed = { + empty: parse(messages.empty), + filled: parse(messages.filled) +}; + +test('with empty body should succeed for empty keyword', async t => { + const [actual] = bodyEmpty(await parsed.empty); + const expected = true; + t.is(actual, expected); +}); + +test('with empty body should fail for "never"', async t => { + const [actual] = bodyEmpty(await parsed.empty, 'never'); + const expected = false; + t.is(actual, expected); +}); + +test('with empty body should succeed for "always"', async t => { + const [actual] = bodyEmpty(await parsed.empty, 'always'); + const expected = true; + t.is(actual, expected); +}); + +test('with body should fail for empty keyword', async t => { + const [actual] = bodyEmpty(await parsed.filled); + const expected = false; + t.is(actual, expected); +}); + +test('with body should succeed for "never"', async t => { + const [actual] = bodyEmpty(await parsed.filled, 'never'); + const expected = true; + t.is(actual, expected); +}); + +test('with body should fail for "always"', async t => { + const [actual] = bodyEmpty(await parsed.filled, 'always'); + const expected = false; + t.is(actual, expected); +}); diff --git a/source/rules/body-leading-blank.js b/@commitlint/core/src/rules/body-leading-blank.js similarity index 100% rename from source/rules/body-leading-blank.js rename to @commitlint/core/src/rules/body-leading-blank.js diff --git a/source/rules/body-leading-blank.test.js b/@commitlint/core/src/rules/body-leading-blank.test.js similarity index 55% rename from source/rules/body-leading-blank.test.js rename to @commitlint/core/src/rules/body-leading-blank.test.js index 273888999c..233452eb11 100644 --- a/source/rules/body-leading-blank.test.js +++ b/@commitlint/core/src/rules/body-leading-blank.test.js @@ -14,56 +14,56 @@ const parsed = { with: parse(messages.with) }; -test('with simple message should succeed for empty keyword', t => { - const [actual] = bodyLeadingBlank(parsed.simple); +test('with simple message should succeed for empty keyword', async t => { + const [actual] = bodyLeadingBlank(await parsed.simple); const expected = true; t.is(actual, expected); }); -test('with simple message should succeed for "never"', t => { - const [actual] = bodyLeadingBlank(parsed.simple, 'never'); +test('with simple message should succeed for "never"', async t => { + const [actual] = bodyLeadingBlank(await parsed.simple, 'never'); const expected = true; t.is(actual, expected); }); -test('with simple message should succeed for "always"', t => { - const [actual] = bodyLeadingBlank(parsed.simple, 'always'); +test('with simple message should succeed for "always"', async t => { + const [actual] = bodyLeadingBlank(await parsed.simple, 'always'); const expected = true; t.is(actual, expected); }); -test('without blank line before body should fail for empty keyword', t => { - const [actual] = bodyLeadingBlank(parsed.without); +test('without blank line before body should fail for empty keyword', async t => { + const [actual] = bodyLeadingBlank(await parsed.without); const expected = false; t.is(actual, expected); }); -test('without blank line before body should succeed for "never"', t => { - const [actual] = bodyLeadingBlank(parsed.without, 'never'); +test('without blank line before body should succeed for "never"', async t => { + const [actual] = bodyLeadingBlank(await parsed.without, 'never'); const expected = true; t.is(actual, expected); }); -test('without blank line before body should fail for "always"', t => { - const [actual] = bodyLeadingBlank(parsed.without, 'always'); +test('without blank line before body should fail for "always"', async t => { + const [actual] = bodyLeadingBlank(await parsed.without, 'always'); const expected = false; t.is(actual, expected); }); -test('with blank line before body should succeed for empty keyword', t => { - const [actual] = bodyLeadingBlank(parsed.with); +test('with blank line before body should succeed for empty keyword', async t => { + const [actual] = bodyLeadingBlank(await parsed.with); const expected = true; t.is(actual, expected); }); -test('with blank line before body should fail for "never"', t => { - const [actual] = bodyLeadingBlank(parsed.with, 'never'); +test('with blank line before body should fail for "never"', async t => { + const [actual] = bodyLeadingBlank(await parsed.with, 'never'); const expected = false; t.is(actual, expected); }); -test('with blank line before body should succeed for "always"', t => { - const [actual] = bodyLeadingBlank(parsed.with, 'always'); +test('with blank line before body should succeed for "always"', async t => { + const [actual] = bodyLeadingBlank(await parsed.with, 'always'); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/body-max-length.js b/@commitlint/core/src/rules/body-max-length.js similarity index 100% rename from source/rules/body-max-length.js rename to @commitlint/core/src/rules/body-max-length.js diff --git a/source/rules/body-max-length.test.js b/@commitlint/core/src/rules/body-max-length.test.js similarity index 64% rename from source/rules/body-max-length.test.js rename to @commitlint/core/src/rules/body-max-length.test.js index 1d5f319356..3b19e73fbb 100644 --- a/source/rules/body-max-length.test.js +++ b/@commitlint/core/src/rules/body-max-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should succeed', t => { - const [actual] = check(parsed.short, '', value); +test('with short should succeed', async t => { + const [actual] = check(await parsed.short, '', value); const expected = true; t.is(actual, expected); }); -test('with long should fail', t => { - const [actual] = check(parsed.long, '', value); +test('with long should fail', async t => { + const [actual] = check(await parsed.long, '', value); const expected = false; t.is(actual, expected); }); diff --git a/source/rules/body-min-length.js b/@commitlint/core/src/rules/body-min-length.js similarity index 100% rename from source/rules/body-min-length.js rename to @commitlint/core/src/rules/body-min-length.js diff --git a/source/rules/body-min-length.test.js b/@commitlint/core/src/rules/body-min-length.test.js similarity index 64% rename from source/rules/body-min-length.test.js rename to @commitlint/core/src/rules/body-min-length.test.js index 63cc7a7b90..a85b996e68 100644 --- a/source/rules/body-min-length.test.js +++ b/@commitlint/core/src/rules/body-min-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with simple should succeed', t => { - const [actual] = check(parsed.simple, '', value); +test('with simple should succeed', async t => { + const [actual] = check(await parsed.simple, '', value); const expected = true; t.is(actual, expected); }); -test('with short should fail', t => { - const [actual] = check(parsed.short, '', value); +test('with short should fail', async t => { + const [actual] = check(await parsed.short, '', value); const expected = false; t.is(actual, expected); }); -test('with long should succeed', t => { - const [actual] = check(parsed.long, '', value); +test('with long should succeed', async t => { + const [actual] = check(await parsed.long, '', value); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/body-tense.js b/@commitlint/core/src/rules/body-tense.js similarity index 100% rename from source/rules/body-tense.js rename to @commitlint/core/src/rules/body-tense.js diff --git a/@commitlint/core/src/rules/body-tense.test.js b/@commitlint/core/src/rules/body-tense.test.js new file mode 100644 index 0000000000..922e5e3cee --- /dev/null +++ b/@commitlint/core/src/rules/body-tense.test.js @@ -0,0 +1,114 @@ +import test from 'ava'; +import parse from '../library/parse'; +import footerTense from './body-tense'; + +const messages = { + empty: 'chore: \n', + presentImperative: `chore: \nwe implement things`, + presentParticiple: `chore: \nimplementing things`, + presentThirdPerson: `chore: \nimplements things`, + past: `chore: \nwe did implement things`, + mixed: `chore: \nimplement, implementing, implements, implemented` +}; + +const parsed = { + empty: parse(messages.empty), + presentImperative: parse(messages.presentImperative), + presentParticiple: parse(messages.presentParticiple), + presentThirdPerson: parse(messages.presentImperative), + past: parse(messages.past), + mixed: parse(messages.mixed) +}; + +test('empty succeeds', async t => { + const [actual] = footerTense(await parsed.empty, '', ['present-imperative']); + const expected = true; + t.is(actual, expected); +}); + +test('present succeeds "always present-imperative"', async t => { + const [actual] = footerTense(await parsed.presentImperative, 'always', ['present-imperative']); + const expected = true; + t.is(actual, expected); +}); + +test('present fails "never present-imperative"', async t => { + const [actual] = footerTense(await parsed.presentImperative, 'never', ['present-imperative']); + const expected = false; + t.is(actual, expected); +}); + +test('present succeeds "always present-participle"', async t => { + const [actual] = footerTense(await parsed.presentParticiple, 'always', ['present-participle']); + const expected = true; + t.is(actual, expected); +}); + +test('present fails "never present-participle"', async t => { + const [actual] = footerTense(await parsed.presentParticiple, 'never', ['present-participle']); + const expected = false; + t.is(actual, expected); +}); + +test('present succeeds "always present-third-person"', async t => { + const [actual] = footerTense(await parsed.presentThirdPerson, 'always', ['present-third-person']); + const expected = true; + t.is(actual, expected); +}); + +test('present fails "never present-third-person"', async t => { + const [actual] = footerTense(await parsed.presentThirdPerson, 'never', ['present-third-person']); + const expected = false; + t.is(actual, expected); +}); + +test('past should succedd "always past-tense"', async t => { + const [actual] = footerTense(await parsed.past, 'always', ['past-tense']); + const expected = true; + t.is(actual, expected); +}); + +test('past fails "never past-tense"', async t => { + const [actual] = footerTense(await parsed.past, 'never', ['past-tense']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed fails "always present-third-person"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-third-person']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed fails "always present-imperative"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-imperative']); + const expected = false; + t.is(actual, expected); +}); + +test('present fails "always present-participle"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-participle']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed fails "always past-tense"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['past-tense']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed succeeds "always present-third-person, present-imperative, present-participle, past-tense"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-third-person', 'present-imperative', 'present-participle', 'past-tense']); + const expected = true; + t.is(actual, expected); +}); + +test('mixed succeeds "never allowed: present-third-person" and matching ignored: implements', async t => { + const [actual] = footerTense(await parsed.mixed, 'never', { + allowed: ['present-third-person'], + ignored: ['implements'] + }); + const expected = true; + t.is(actual, expected); +}); diff --git a/source/rules/footer-empty.js b/@commitlint/core/src/rules/footer-empty.js similarity index 100% rename from source/rules/footer-empty.js rename to @commitlint/core/src/rules/footer-empty.js diff --git a/@commitlint/core/src/rules/footer-empty.test.js b/@commitlint/core/src/rules/footer-empty.test.js new file mode 100644 index 0000000000..f5e074c5a9 --- /dev/null +++ b/@commitlint/core/src/rules/footer-empty.test.js @@ -0,0 +1,69 @@ +import test from 'ava'; +import parse from '../library/parse'; +import footerEmpty from './footer-empty'; + +const messages = { + simple: 'chore: subject', + empty: 'chore: subject\nbody', + filled: 'chore: subject\nBREAKING CHANGE: something important' +}; + +const parsed = { + simple: parse(messages.simple), + empty: parse(messages.empty), + filled: parse(messages.filled) +}; + +test('with simple message should succeed for empty keyword', async t => { + const [actual] = footerEmpty(await parsed.simple); + const expected = true; + t.is(actual, expected); +}); + +test('with simple message should fail for "never"', async t => { + const [actual] = footerEmpty(await parsed.simple, 'never'); + const expected = false; + t.is(actual, expected); +}); + +test('with simple message should succeed for "always"', async t => { + const [actual] = footerEmpty(await parsed.simple, 'always'); + const expected = true; + t.is(actual, expected); +}); + +test('with empty footer should succeed for empty keyword', async t => { + const [actual] = footerEmpty(await parsed.empty); + const expected = true; + t.is(actual, expected); +}); + +test('with empty footer should fail for "never"', async t => { + const [actual] = footerEmpty(await parsed.empty, 'never'); + const expected = false; + t.is(actual, expected); +}); + +test('with empty footer should succeed for "always"', async t => { + const [actual] = footerEmpty(await parsed.empty, 'always'); + const expected = true; + t.is(actual, expected); +}); + +test('with footer should fail for empty keyword', async t => { + const [actual] = footerEmpty(await parsed.filled); + const expected = false; + t.is(actual, expected); +}); + +test('with footer should succeed for "never"', async t => { + const [actual] = footerEmpty(await parsed.filled, 'never'); + const expected = true; + t.is(actual, expected); +}); + +test('with footer should fail for "always"', async t => { + const [actual] = footerEmpty(await parsed.filled, 'always'); + const expected = false; + t.is(actual, expected); +}); diff --git a/source/rules/footer-leading-blank.js b/@commitlint/core/src/rules/footer-leading-blank.js similarity index 100% rename from source/rules/footer-leading-blank.js rename to @commitlint/core/src/rules/footer-leading-blank.js diff --git a/source/rules/footer-leading-blank.test.js b/@commitlint/core/src/rules/footer-leading-blank.test.js similarity index 51% rename from source/rules/footer-leading-blank.test.js rename to @commitlint/core/src/rules/footer-leading-blank.test.js index 6fa76fb81b..e14211cc4e 100644 --- a/source/rules/footer-leading-blank.test.js +++ b/@commitlint/core/src/rules/footer-leading-blank.test.js @@ -20,110 +20,110 @@ const parsed = { withMulitLine: parse(messages.withMulitLine) }; -test('with simple message should succeed for empty keyword', t => { - const [actual] = footerLeadingBlank(parsed.simple); +test('with simple message should succeed for empty keyword', async t => { + const [actual] = footerLeadingBlank(await parsed.simple); const expected = true; t.is(actual, expected); }); -test('with simple message should succeed for "never"', t => { - const [actual] = footerLeadingBlank(parsed.simple, 'never'); +test('with simple message should succeed for "never"', async t => { + const [actual] = footerLeadingBlank(await parsed.simple, 'never'); const expected = true; t.is(actual, expected); }); -test('with simple message should succeed for "always"', t => { - const [actual] = footerLeadingBlank(parsed.simple, 'always'); +test('with simple message should succeed for "always"', async t => { + const [actual] = footerLeadingBlank(await parsed.simple, 'always'); const expected = true; t.is(actual, expected); }); -test('with body message should succeed for empty keyword', t => { - const [actual] = footerLeadingBlank(parsed.body); +test('with body message should succeed for empty keyword', async t => { + const [actual] = footerLeadingBlank(await parsed.body); const expected = true; t.is(actual, expected); }); -test('with body message should succeed for "never"', t => { - const [actual] = footerLeadingBlank(parsed.body, 'never'); +test('with body message should succeed for "never"', async t => { + const [actual] = footerLeadingBlank(await parsed.body, 'never'); const expected = true; t.is(actual, expected); }); -test('with body message should succeed for "always"', t => { - const [actual] = footerLeadingBlank(parsed.body, 'always'); +test('with body message should succeed for "always"', async t => { + const [actual] = footerLeadingBlank(await parsed.body, 'always'); const expected = true; t.is(actual, expected); }); -test('with trailing message should succeed for empty keyword', t => { - const [actual] = footerLeadingBlank(parsed.trailing); +test('with trailing message should succeed for empty keyword', async t => { + const [actual] = footerLeadingBlank(await parsed.trailing); const expected = true; t.is(actual, expected); }); -test('with trailing message should succeed for "never"', t => { - const [actual] = footerLeadingBlank(parsed.trailing, 'never'); +test('with trailing message should succeed for "never"', async t => { + const [actual] = footerLeadingBlank(await parsed.trailing, 'never'); const expected = true; t.is(actual, expected); }); -test('with trailing message should succeed for "always"', t => { - const [actual] = footerLeadingBlank(parsed.trailing, 'always'); +test('with trailing message should succeed for "always"', async t => { + const [actual] = footerLeadingBlank(await parsed.trailing, 'always'); const expected = true; t.is(actual, expected); }); -test('without blank line before footer should fail for empty keyword', t => { - const [actual] = footerLeadingBlank(parsed.without); +test('without blank line before footer should fail for empty keyword', async t => { + const [actual] = footerLeadingBlank(await parsed.without); const expected = false; t.is(actual, expected); }); -test('without blank line before footer should succeed for "never"', t => { - const [actual] = footerLeadingBlank(parsed.without, 'never'); +test('without blank line before footer should succeed for "never"', async t => { + const [actual] = footerLeadingBlank(await parsed.without, 'never'); const expected = true; t.is(actual, expected); }); -test('without blank line before footer should fail for "always"', t => { - const [actual] = footerLeadingBlank(parsed.without, 'always'); +test('without blank line before footer should fail for "always"', async t => { + const [actual] = footerLeadingBlank(await parsed.without, 'always'); const expected = false; t.is(actual, expected); }); -test('with blank line before footer should succeed for empty keyword', t => { - const [actual] = footerLeadingBlank(parsed.with); +test('with blank line before footer should succeed for empty keyword', async t => { + const [actual] = footerLeadingBlank(await parsed.with); const expected = true; t.is(actual, expected); }); -test('with blank line before footer should fail for "never"', t => { - const [actual] = footerLeadingBlank(parsed.with, 'never'); +test('with blank line before footer should fail for "never"', async t => { + const [actual] = footerLeadingBlank(await parsed.with, 'never'); const expected = false; t.is(actual, expected); }); -test('with blank line before footer should succeed for "always"', t => { - const [actual] = footerLeadingBlank(parsed.with, 'always'); +test('with blank line before footer should succeed for "always"', async t => { + const [actual] = footerLeadingBlank(await parsed.with, 'always'); const expected = true; t.is(actual, expected); }); -test('with blank line before footer and multiline body should succeed for empty keyword', t => { - const [actual] = footerLeadingBlank(parsed.withMulitLine); +test('with blank line before footer and multiline body should succeed for empty keyword', async t => { + const [actual] = footerLeadingBlank(await parsed.withMulitLine); const expected = true; t.is(actual, expected); }); -test('with blank line before footer and multiline body should fail for "never"', t => { - const [actual] = footerLeadingBlank(parsed.withMulitLine, 'never'); +test('with blank line before footer and multiline body should fail for "never"', async t => { + const [actual] = footerLeadingBlank(await parsed.withMulitLine, 'never'); const expected = false; t.is(actual, expected); }); -test('with blank line before footer and multiline body should succeed for "always"', t => { - const [actual] = footerLeadingBlank(parsed.withMulitLine, 'always'); +test('with blank line before footer and multiline body should succeed for "always"', async t => { + const [actual] = footerLeadingBlank(await parsed.withMulitLine, 'always'); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/footer-max-length.js b/@commitlint/core/src/rules/footer-max-length.js similarity index 100% rename from source/rules/footer-max-length.js rename to @commitlint/core/src/rules/footer-max-length.js diff --git a/source/rules/footer-max-length.test.js b/@commitlint/core/src/rules/footer-max-length.test.js similarity index 63% rename from source/rules/footer-max-length.test.js rename to @commitlint/core/src/rules/footer-max-length.test.js index 13af836f40..703820fab6 100644 --- a/source/rules/footer-max-length.test.js +++ b/@commitlint/core/src/rules/footer-max-length.test.js @@ -21,26 +21,26 @@ const parsed = { long: parse(messages.long) }; -test('with simple should succeed', t => { - const [actual] = check(parsed.simple, '', value); +test('with simple should succeed', async t => { + const [actual] = check(await parsed.simple, '', value); const expected = true; t.is(actual, expected); }); -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should succeed', t => { - const [actual] = check(parsed.short, '', value); +test('with short should succeed', async t => { + const [actual] = check(await parsed.short, '', value); const expected = true; t.is(actual, expected); }); -test('with long should fail', t => { - const [actual] = check(parsed.long, '', value); +test('with long should fail', async t => { + const [actual] = check(await parsed.long, '', value); const expected = false; t.is(actual, expected); }); diff --git a/source/rules/footer-min-length.js b/@commitlint/core/src/rules/footer-min-length.js similarity index 100% rename from source/rules/footer-min-length.js rename to @commitlint/core/src/rules/footer-min-length.js diff --git a/source/rules/footer-min-length.test.js b/@commitlint/core/src/rules/footer-min-length.test.js similarity index 63% rename from source/rules/footer-min-length.test.js rename to @commitlint/core/src/rules/footer-min-length.test.js index 5d20465f2c..4092318949 100644 --- a/source/rules/footer-min-length.test.js +++ b/@commitlint/core/src/rules/footer-min-length.test.js @@ -21,26 +21,26 @@ const parsed = { long: parse(messages.long) }; -test('with simple should succeed', t => { - const [actual] = check(parsed.simple, '', value); +test('with simple should succeed', async t => { + const [actual] = check(await parsed.simple, '', value); const expected = true; t.is(actual, expected); }); -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should fail', t => { - const [actual] = check(parsed.short, '', value); +test('with short should fail', async t => { + const [actual] = check(await parsed.short, '', value); const expected = false; t.is(actual, expected); }); -test('with long should succeed', t => { - const [actual] = check(parsed.long, '', value); +test('with long should succeed', async t => { + const [actual] = check(await parsed.long, '', value); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/footer-tense.js b/@commitlint/core/src/rules/footer-tense.js similarity index 100% rename from source/rules/footer-tense.js rename to @commitlint/core/src/rules/footer-tense.js diff --git a/source/rules/footer-tense.test.js b/@commitlint/core/src/rules/footer-tense.test.js similarity index 57% rename from source/rules/footer-tense.test.js rename to @commitlint/core/src/rules/footer-tense.test.js index a96bb8198d..51020ec09a 100644 --- a/source/rules/footer-tense.test.js +++ b/@commitlint/core/src/rules/footer-tense.test.js @@ -20,92 +20,92 @@ const parsed = { mixed: parse(messages.mixed) }; -test('with empty footer should succeed', t => { - const [actual] = footerTense(parsed.empty, '', ['present-imperative']); +test('with empty footer should succeed', async t => { + const [actual] = footerTense(await parsed.empty, '', ['present-imperative']); const expected = true; t.is(actual, expected); }); -test('with present footer should succeed for "always present-imperative"', t => { - const [actual] = footerTense(parsed.presentImperative, 'always', ['present-imperative']); +test('with present footer should succeed for "always present-imperative"', async t => { + const [actual] = footerTense(await parsed.presentImperative, 'always', ['present-imperative']); const expected = true; t.is(actual, expected); }); -test('with present footer should fail for "never present-imperative"', t => { - const [actual] = footerTense(parsed.presentImperative, 'never', ['present-imperative']); +test('with present footer should fail for "never present-imperative"', async t => { + const [actual] = footerTense(await parsed.presentImperative, 'never', ['present-imperative']); const expected = false; t.is(actual, expected); }); -test('with present footer should succeed for "always present-participle"', t => { - const [actual] = footerTense(parsed.presentParticiple, 'always', ['present-participle']); +test('with present footer should succeed for "always present-participle"', async t => { + const [actual] = footerTense(await parsed.presentParticiple, 'always', ['present-participle']); const expected = true; t.is(actual, expected); }); -test('with present footer should fail for "never present-participle"', t => { - const [actual] = footerTense(parsed.presentParticiple, 'never', ['present-participle']); +test('with present footer should fail for "never present-participle"', async t => { + const [actual] = footerTense(await parsed.presentParticiple, 'never', ['present-participle']); const expected = false; t.is(actual, expected); }); -test('with present footer should succeed for "always present-third-person"', t => { - const [actual] = footerTense(parsed.presentThirdPerson, 'always', ['present-third-person']); +test('with present footer should succeed for "always present-third-person"', async t => { + const [actual] = footerTense(await parsed.presentThirdPerson, 'always', ['present-third-person']); const expected = true; t.is(actual, expected); }); -test('with present footer should fail for "never present-third-person"', t => { - const [actual] = footerTense(parsed.presentThirdPerson, 'never', ['present-third-person']); +test('with present footer should fail for "never present-third-person"', async t => { + const [actual] = footerTense(await parsed.presentThirdPerson, 'never', ['present-third-person']); const expected = false; t.is(actual, expected); }); -test('with past footer should succedd for "always past-tense"', t => { - const [actual] = footerTense(parsed.past, 'always', ['past-tense']); +test('with past footer should succedd for "always past-tense"', async t => { + const [actual] = footerTense(await parsed.past, 'always', ['past-tense']); const expected = true; t.is(actual, expected); }); -test('with past footer should fail for "never past-tense"', t => { - const [actual] = footerTense(parsed.past, 'never', ['past-tense']); +test('with past footer should fail for "never past-tense"', async t => { + const [actual] = footerTense(await parsed.past, 'never', ['past-tense']); const expected = false; t.is(actual, expected); }); -test('with mixed footer should fail for "always present-third-person"', t => { - const [actual] = footerTense(parsed.mixed, 'always', ['present-third-person']); +test('with mixed footer should fail for "always present-third-person"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-third-person']); const expected = false; t.is(actual, expected); }); -test('with mixed footer should fail for "always present-imperative"', t => { - const [actual] = footerTense(parsed.mixed, 'always', ['present-imperative']); +test('with mixed footer should fail for "always present-imperative"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-imperative']); const expected = false; t.is(actual, expected); }); -test('with present footer should fail for "always present-participle"', t => { - const [actual] = footerTense(parsed.mixed, 'always', ['present-participle']); +test('with present footer should fail for "always present-participle"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-participle']); const expected = false; t.is(actual, expected); }); -test('with mixed footer should fail for "always past-tense"', t => { - const [actual] = footerTense(parsed.mixed, 'always', ['past-tense']); +test('with mixed footer should fail for "always past-tense"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['past-tense']); const expected = false; t.is(actual, expected); }); -test('with mixed footer should succeed for "always present-third-person, present-imperative, present-participle, past-tense"', t => { - const [actual] = footerTense(parsed.mixed, 'always', ['present-third-person', 'present-imperative', 'present-participle', 'past-tense']); +test('with mixed footer should succeed for "always present-third-person, present-imperative, present-participle, past-tense"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-third-person', 'present-imperative', 'present-participle', 'past-tense']); const expected = true; t.is(actual, expected); }); -test('with mixed footer should succeed for "never allowed: present-third-person" and matching ignored: implements', t => { - const [actual] = footerTense(parsed.mixed, 'never', { +test('with mixed footer should succeed for "never allowed: present-third-person" and matching ignored: implements', async t => { + const [actual] = footerTense(await parsed.mixed, 'never', { allowed: ['present-third-person'], ignored: ['implements'] }); diff --git a/source/rules/header-max-length.js b/@commitlint/core/src/rules/header-max-length.js similarity index 100% rename from source/rules/header-max-length.js rename to @commitlint/core/src/rules/header-max-length.js diff --git a/source/rules/header-max-length.test.js b/@commitlint/core/src/rules/header-max-length.test.js similarity index 67% rename from source/rules/header-max-length.test.js rename to @commitlint/core/src/rules/header-max-length.test.js index 856d228290..08e3f4a2e8 100644 --- a/source/rules/header-max-length.test.js +++ b/@commitlint/core/src/rules/header-max-length.test.js @@ -17,14 +17,14 @@ const parsed = { long: parse(messages.long) }; -test('with short should succeed', t => { - const [actual] = check(parsed.short, '', value); +test('with short should succeed', async t => { + const [actual] = check(await parsed.short, '', value); const expected = true; t.is(actual, expected); }); -test('with long should fail', t => { - const [actual] = check(parsed.long, '', value); +test('with long should fail', async t => { + const [actual] = check(await parsed.long, '', value); const expected = false; t.is(actual, expected); }); diff --git a/source/rules/header-min-length.js b/@commitlint/core/src/rules/header-min-length.js similarity index 100% rename from source/rules/header-min-length.js rename to @commitlint/core/src/rules/header-min-length.js diff --git a/source/rules/header-min-length.test.js b/@commitlint/core/src/rules/header-min-length.test.js similarity index 68% rename from source/rules/header-min-length.test.js rename to @commitlint/core/src/rules/header-min-length.test.js index e63777991b..c10d8d5995 100644 --- a/source/rules/header-min-length.test.js +++ b/@commitlint/core/src/rules/header-min-length.test.js @@ -17,14 +17,14 @@ const parsed = { long: parse(messages.long) }; -test('with short should fail', t => { - const [actual] = check(parsed.short, '', value); +test('with short should fail', async t => { + const [actual] = check(await parsed.short, '', value); const expected = false; t.is(actual, expected); }); -test('with long should succeed', t => { - const [actual] = check(parsed.long, '', value); +test('with long should succeed', async t => { + const [actual] = check(await parsed.long, '', value); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/index.js b/@commitlint/core/src/rules/index.js similarity index 100% rename from source/rules/index.js rename to @commitlint/core/src/rules/index.js diff --git a/source/rules/index.test.js b/@commitlint/core/src/rules/index.test.js similarity index 92% rename from source/rules/index.test.js rename to @commitlint/core/src/rules/index.test.js index 0cac33dbe9..6d8ca6a007 100644 --- a/source/rules/index.test.js +++ b/@commitlint/core/src/rules/index.test.js @@ -1,6 +1,7 @@ import path from 'path'; import test from 'ava'; import globby from 'globby'; +import {values} from 'lodash'; import rules from '.'; test('exports all rules', async t => { @@ -10,7 +11,7 @@ test('exports all rules', async t => { }); test('rules export functions', t => { - const actual = Object.values(rules); + const actual = values(rules); t.true(actual.every(rule => typeof rule === 'function')); }); diff --git a/source/rules/lang.js b/@commitlint/core/src/rules/lang.js similarity index 100% rename from source/rules/lang.js rename to @commitlint/core/src/rules/lang.js diff --git a/@commitlint/core/src/rules/lang.test.js b/@commitlint/core/src/rules/lang.test.js new file mode 100644 index 0000000000..3e45b7811d --- /dev/null +++ b/@commitlint/core/src/rules/lang.test.js @@ -0,0 +1,75 @@ +import test from 'ava'; +import parse from '../library/parse'; +import check from './lang'; + +const messages = { + empty: '(): \n', + eng: '(): this is a serious subject', + deu: '(): Dies ist ein ernstes Subjekt' +}; + +const parsed = { + empty: parse(messages.empty), + eng: parse(messages.eng), + deu: parse(messages.deu) +}; + +test('empty succeeds', async t => { + const [actual] = check(await parsed.eng, '', 'eng'); + const expected = true; + t.is(actual, expected); +}); + +test('english against "eng" succeeds', async t => { + const [actual] = check(await parsed.eng, '', 'eng'); + const expected = true; + t.is(actual, expected); +}); + +test('english against "always eng" succeeds', async t => { + const [actual] = check(await parsed.eng, 'always', 'eng'); + const expected = true; + t.is(actual, expected); +}); + +test('english against "never eng" fails', async t => { + const [actual] = check(await parsed.eng, 'never', 'eng'); + const expected = false; + t.is(actual, expected); +}); + +test('english against "deu" fails', async t => { + const [actual] = check(await parsed.eng, '', 'deu+'); + const expected = false; + t.is(actual, expected); +}); + +test('english against "always deu" fails', async t => { + const [actual] = check(await parsed.eng, 'always', 'deu'); + const expected = false; + t.is(actual, expected); +}); + +test('english against "never deu" succeeds', async t => { + const [actual] = check(await parsed.eng, 'never', 'deu'); + const expected = true; + t.is(actual, expected); +}); + +test('german against "deu" succeeds', async t => { + const [actual] = check(await parsed.deu, '', 'deu'); + const expected = true; + t.is(actual, expected); +}); + +test('german against "always deu" succeeds', async t => { + const [actual] = check(await parsed.deu, 'always', 'deu'); + const expected = true; + t.is(actual, expected); +}); + +test('german against "never deu" fails', async t => { + const [actual] = check(await parsed.deu, 'never', 'deu'); + const expected = false; + t.is(actual, expected); +}); diff --git a/source/rules/scope-case.js b/@commitlint/core/src/rules/scope-case.js similarity index 100% rename from source/rules/scope-case.js rename to @commitlint/core/src/rules/scope-case.js diff --git a/source/rules/scope-case.test.js b/@commitlint/core/src/rules/scope-case.test.js similarity index 50% rename from source/rules/scope-case.test.js rename to @commitlint/core/src/rules/scope-case.test.js index 3f643a7833..c9adb7f482 100644 --- a/source/rules/scope-case.test.js +++ b/@commitlint/core/src/rules/scope-case.test.js @@ -16,74 +16,74 @@ const parsed = { uppercase: parse(messages.uppercase) }; -test('with empty scope should succeed for "never lowercase"', t => { - const [actual] = scopeCase(parsed.empty, 'never', 'lowercase'); +test('with empty scope should succeed for "never lowercase"', async t => { + const [actual] = scopeCase(await parsed.empty, 'never', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with empty scope should succeed for "always lowercase"', t => { - const [actual] = scopeCase(parsed.empty, 'always', 'lowercase'); +test('with empty scope should succeed for "always lowercase"', async t => { + const [actual] = scopeCase(await parsed.empty, 'always', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with empty scope should succeed for "never uppercase"', t => { - const [actual] = scopeCase(parsed.empty, 'never', 'uppercase'); +test('with empty scope should succeed for "never uppercase"', async t => { + const [actual] = scopeCase(await parsed.empty, 'never', 'uppercase'); const expected = true; t.is(actual, expected); }); -test('with empty scope should succeed for "always uppercase"', t => { - const [actual] = scopeCase(parsed.empty, 'always', 'uppercase'); +test('with empty scope should succeed for "always uppercase"', async t => { + const [actual] = scopeCase(await parsed.empty, 'always', 'uppercase'); const expected = true; t.is(actual, expected); }); -test('with lowercase scope should fail for "never lowercase"', t => { - const [actual] = scopeCase(parsed.lowercase, 'never', 'lowercase'); +test('with lowercase scope should fail for "never lowercase"', async t => { + const [actual] = scopeCase(await parsed.lowercase, 'never', 'lowercase'); const expected = false; t.is(actual, expected); }); -test('with lowercase scope should succeed for "always lowercase"', t => { - const [actual] = scopeCase(parsed.lowercase, 'always', 'lowercase'); +test('with lowercase scope should succeed for "always lowercase"', async t => { + const [actual] = scopeCase(await parsed.lowercase, 'always', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with mixedcase scope should succeed for "never lowercase"', t => { - const [actual] = scopeCase(parsed.mixedcase, 'never', 'lowercase'); +test('with mixedcase scope should succeed for "never lowercase"', async t => { + const [actual] = scopeCase(await parsed.mixedcase, 'never', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with mixedcase scope should fail for "always lowercase"', t => { - const [actual] = scopeCase(parsed.mixedcase, 'always', 'lowercase'); +test('with mixedcase scope should fail for "always lowercase"', async t => { + const [actual] = scopeCase(await parsed.mixedcase, 'always', 'lowercase'); const expected = false; t.is(actual, expected); }); -test('with mixedcase scope should succeed for "never uppercase"', t => { - const [actual] = scopeCase(parsed.mixedcase, 'never', 'uppercase'); +test('with mixedcase scope should succeed for "never uppercase"', async t => { + const [actual] = scopeCase(await parsed.mixedcase, 'never', 'uppercase'); const expected = true; t.is(actual, expected); }); -test('with mixedcase scope should fail for "always uppercase"', t => { - const [actual] = scopeCase(parsed.mixedcase, 'always', 'uppercase'); +test('with mixedcase scope should fail for "always uppercase"', async t => { + const [actual] = scopeCase(await parsed.mixedcase, 'always', 'uppercase'); const expected = false; t.is(actual, expected); }); -test('with uppercase scope should fail for "never uppercase"', t => { - const [actual] = scopeCase(parsed.uppercase, 'never', 'uppercase'); +test('with uppercase scope should fail for "never uppercase"', async t => { + const [actual] = scopeCase(await parsed.uppercase, 'never', 'uppercase'); const expected = false; t.is(actual, expected); }); -test('with lowercase scope should succeed for "always uppercase"', t => { - const [actual] = scopeCase(parsed.uppercase, 'always', 'uppercase'); +test('with lowercase scope should succeed for "always uppercase"', async t => { + const [actual] = scopeCase(await parsed.uppercase, 'always', 'uppercase'); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/scope-empty.js b/@commitlint/core/src/rules/scope-empty.js similarity index 100% rename from source/rules/scope-empty.js rename to @commitlint/core/src/rules/scope-empty.js diff --git a/@commitlint/core/src/rules/scope-empty.test.js b/@commitlint/core/src/rules/scope-empty.test.js new file mode 100644 index 0000000000..4cf59ca657 --- /dev/null +++ b/@commitlint/core/src/rules/scope-empty.test.js @@ -0,0 +1,69 @@ +import test from 'ava'; +import parse from '../library/parse'; +import scopeEmpty from './scope-empty'; + +const messages = { + plain: 'foo(bar): baz', + superfluous: 'foo(): baz', + empty: 'foo: baz' +}; + +const parsed = { + plain: parse(messages.plain), + superfluous: parse(messages.superfluous), + empty: parse(messages.empty) +}; + +test('with plain message it should succeed for empty keyword', async t => { + const [actual] = scopeEmpty(await parsed.plain); + const expected = true; + t.deepEqual(actual, expected); +}); + +test('with plain message it should succeed for "never"', async t => { + const [actual] = scopeEmpty(await parsed.plain, 'never'); + const expected = true; + t.deepEqual(actual, expected); +}); + +test('with plain message it should fail for "always"', async t => { + const [actual] = scopeEmpty(await parsed.plain, 'always'); + const expected = false; + t.deepEqual(actual, expected); +}); + +test('with superfluous message it should fail for empty keyword', async t => { + const [actual] = scopeEmpty(await parsed.superfluous); + const expected = false; + t.deepEqual(actual, expected); +}); + +test('with superfluous message it should fail for "never"', async t => { + const [actual] = scopeEmpty(await parsed.superfluous, 'never'); + const expected = false; + t.deepEqual(actual, expected); +}); + +test('with superfluous message it should fail for "always"', async t => { + const [actual] = scopeEmpty(await parsed.superfluous, 'always'); + const expected = true; + t.deepEqual(actual, expected); +}); + +test('with empty message it should fail for empty keyword', async t => { + const [actual] = scopeEmpty(await parsed.empty); + const expected = false; + t.deepEqual(actual, expected); +}); + +test('with empty message it should fail for "never"', async t => { + const [actual] = scopeEmpty(await parsed.empty, 'never'); + const expected = false; + t.deepEqual(actual, expected); +}); + +test('with empty message it should fail for "always"', async t => { + const [actual] = scopeEmpty(await parsed.empty, 'always'); + const expected = true; + t.deepEqual(actual, expected); +}); diff --git a/source/rules/scope-enum.js b/@commitlint/core/src/rules/scope-enum.js similarity index 100% rename from source/rules/scope-enum.js rename to @commitlint/core/src/rules/scope-enum.js diff --git a/source/rules/scope-enum.test.js b/@commitlint/core/src/rules/scope-enum.test.js similarity index 57% rename from source/rules/scope-enum.test.js rename to @commitlint/core/src/rules/scope-enum.test.js index c470bb8f6e..78a09fad8a 100644 --- a/source/rules/scope-enum.test.js +++ b/@commitlint/core/src/rules/scope-enum.test.js @@ -14,80 +14,80 @@ const parsed = { empty: parse(messages.empty) }; -test('scope-enum with plain message and always should succeed empty enum', t => { - const [actual] = scopeEnum(parsed.plain, 'always', []); +test('scope-enum with plain message and always should succeed empty enum', async t => { + const [actual] = scopeEnum(await parsed.plain, 'always', []); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with plain message and never should error empty enum', t => { - const [actual] = scopeEnum(parsed.plain, 'never', []); +test('scope-enum with plain message and never should error empty enum', async t => { + const [actual] = scopeEnum(await parsed.plain, 'never', []); const expected = false; t.deepEqual(actual, expected); }); -test('with plain message should succeed correct enum', t => { - const [actual] = scopeEnum(parsed.plain, 'always', ['bar']); +test('with plain message should succeed correct enum', async t => { + const [actual] = scopeEnum(await parsed.plain, 'always', ['bar']); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with plain message should error false enum', t => { - const [actual] = scopeEnum(parsed.plain, 'always', ['foo']); +test('scope-enum with plain message should error false enum', async t => { + const [actual] = scopeEnum(await parsed.plain, 'always', ['foo']); const expected = false; t.deepEqual(actual, expected); }); -test('scope-enum with plain message should error forbidden enum', t => { - const [actual] = scopeEnum(parsed.plain, 'never', ['bar']); +test('scope-enum with plain message should error forbidden enum', async t => { + const [actual] = scopeEnum(await parsed.plain, 'never', ['bar']); const expected = false; t.deepEqual(actual, expected); }); -test('scope-enum with plain message should succeed forbidden enum', t => { - const [actual] = scopeEnum(parsed.plain, 'never', ['foo']); +test('scope-enum with plain message should succeed forbidden enum', async t => { + const [actual] = scopeEnum(await parsed.plain, 'never', ['foo']); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with superfluous scope should succeed enum', t => { - const [actual] = scopeEnum(parsed.superfluous, 'always', ['bar']); +test('scope-enum with superfluous scope should succeed enum', async t => { + const [actual] = scopeEnum(await parsed.superfluous, 'always', ['bar']); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with superfluous scope and "never" should succeed', t => { - const [actual] = scopeEnum(parsed.superfluous, 'never', ['bar']); +test('scope-enum with superfluous scope and "never" should succeed', async t => { + const [actual] = scopeEnum(await parsed.superfluous, 'never', ['bar']); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with superfluous scope and always should succeed empty enum', t => { - const [actual] = scopeEnum(parsed.superfluous, 'always', []); +test('scope-enum with superfluous scope and always should succeed empty enum', async t => { + const [actual] = scopeEnum(await parsed.superfluous, 'always', []); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with superfluous scope and never should succeed empty enum', t => { - const [actual] = scopeEnum(parsed.superfluous, 'never', []); +test('scope-enum with superfluous scope and never should succeed empty enum', async t => { + const [actual] = scopeEnum(await parsed.superfluous, 'never', []); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with empty scope and always should succeed empty enum', t => { - const [actual] = scopeEnum(parsed.superfluous, 'always', []); +test('scope-enum with empty scope and always should succeed empty enum', async t => { + const [actual] = scopeEnum(await parsed.superfluous, 'always', []); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with empty scope and always should succeed filled enum', t => { - const [actual] = scopeEnum(parsed.superfluous, 'always', ['foo']); +test('scope-enum with empty scope and always should succeed filled enum', async t => { + const [actual] = scopeEnum(await parsed.superfluous, 'always', ['foo']); const expected = true; t.deepEqual(actual, expected); }); -test('scope-enum with empty scope and never should succeed empty enum', t => { - const [actual] = scopeEnum(parsed.superfluous, 'never', []); +test('scope-enum with empty scope and never should succeed empty enum', async t => { + const [actual] = scopeEnum(await parsed.superfluous, 'never', []); const expected = true; t.deepEqual(actual, expected); }); diff --git a/source/rules/scope-max-length.js b/@commitlint/core/src/rules/scope-max-length.js similarity index 100% rename from source/rules/scope-max-length.js rename to @commitlint/core/src/rules/scope-max-length.js diff --git a/source/rules/scope-max-length.test.js b/@commitlint/core/src/rules/scope-max-length.test.js similarity index 64% rename from source/rules/scope-max-length.test.js rename to @commitlint/core/src/rules/scope-max-length.test.js index 95222957a0..b2227c46db 100644 --- a/source/rules/scope-max-length.test.js +++ b/@commitlint/core/src/rules/scope-max-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should succeed', t => { - const [actual] = check(parsed.short, '', value); +test('with short should succeed', async t => { + const [actual] = check(await parsed.short, '', value); const expected = true; t.is(actual, expected); }); -test('with long should fail', t => { - const [actual] = check(parsed.long, '', value); +test('with long should fail', async t => { + const [actual] = check(await parsed.long, '', value); const expected = false; t.is(actual, expected); }); diff --git a/source/rules/scope-min-length.js b/@commitlint/core/src/rules/scope-min-length.js similarity index 100% rename from source/rules/scope-min-length.js rename to @commitlint/core/src/rules/scope-min-length.js diff --git a/source/rules/scope-min-length.test.js b/@commitlint/core/src/rules/scope-min-length.test.js similarity index 64% rename from source/rules/scope-min-length.test.js rename to @commitlint/core/src/rules/scope-min-length.test.js index c30c9d0399..f0090cf062 100644 --- a/source/rules/scope-min-length.test.js +++ b/@commitlint/core/src/rules/scope-min-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should fail', t => { - const [actual] = check(parsed.short, '', value); +test('with short should fail', async t => { + const [actual] = check(await parsed.short, '', value); const expected = false; t.is(actual, expected); }); -test('with long should succeed', t => { - const [actual] = check(parsed.long, '', value); +test('with long should succeed', async t => { + const [actual] = check(await parsed.long, '', value); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/subject-case.js b/@commitlint/core/src/rules/subject-case.js similarity index 100% rename from source/rules/subject-case.js rename to @commitlint/core/src/rules/subject-case.js diff --git a/source/rules/subject-case.test.js b/@commitlint/core/src/rules/subject-case.test.js similarity index 62% rename from source/rules/subject-case.test.js rename to @commitlint/core/src/rules/subject-case.test.js index 20da9dbe49..de0d5e71fe 100644 --- a/source/rules/subject-case.test.js +++ b/@commitlint/core/src/rules/subject-case.test.js @@ -16,74 +16,74 @@ const parsed = { uppercase: parse(messages.uppercase) }; -test('with empty subject should succeed for "never lowercase"', t => { - const [actual] = subjectCase(parsed.empty, 'never', 'lowercase'); +test('with empty subject should succeed for "never lowercase"', async t => { + const [actual] = subjectCase(await parsed.empty, 'never', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with empty subject should succeed for "always lowercase"', t => { - const [actual] = subjectCase(parsed.empty, 'always', 'lowercase'); +test('with empty subject should succeed for "always lowercase"', async t => { + const [actual] = subjectCase(await parsed.empty, 'always', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with empty subject should succeed for "never uppercase"', t => { - const [actual] = subjectCase(parsed.empty, 'never', 'uppercase'); +test('with empty subject should succeed for "never uppercase"', async t => { + const [actual] = subjectCase(await parsed.empty, 'never', 'uppercase'); const expected = true; t.is(actual, expected); }); -test('with empty subject should succeed for "always uppercase"', t => { - const [actual] = subjectCase(parsed.empty, 'always', 'uppercase'); +test('with empty subject should succeed for "always uppercase"', async t => { + const [actual] = subjectCase(await parsed.empty, 'always', 'uppercase'); const expected = true; t.is(actual, expected); }); -test('with lowercase subject should fail for "never lowercase"', t => { - const [actual] = subjectCase(parsed.lowercase, 'never', 'lowercase'); +test('with lowercase subject should fail for "never lowercase"', async t => { + const [actual] = subjectCase(await parsed.lowercase, 'never', 'lowercase'); const expected = false; t.is(actual, expected); }); -test('with lowercase subject should succeed for "always lowercase"', t => { - const [actual] = subjectCase(parsed.lowercase, 'always', 'lowercase'); +test('with lowercase subject should succeed for "always lowercase"', async t => { + const [actual] = subjectCase(await parsed.lowercase, 'always', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with mixedcase subject should succeed for "never lowercase"', t => { - const [actual] = subjectCase(parsed.mixedcase, 'never', 'lowercase'); +test('with mixedcase subject should succeed for "never lowercase"', async t => { + const [actual] = subjectCase(await parsed.mixedcase, 'never', 'lowercase'); const expected = true; t.is(actual, expected); }); -test('with mixedcase subject should fail for "always lowercase"', t => { - const [actual] = subjectCase(parsed.mixedcase, 'always', 'lowercase'); +test('with mixedcase subject should fail for "always lowercase"', async t => { + const [actual] = subjectCase(await parsed.mixedcase, 'always', 'lowercase'); const expected = false; t.is(actual, expected); }); -test('with mixedcase subject should succeed for "never uppercase"', t => { - const [actual] = subjectCase(parsed.mixedcase, 'never', 'uppercase'); +test('with mixedcase subject should succeed for "never uppercase"', async t => { + const [actual] = subjectCase(await parsed.mixedcase, 'never', 'uppercase'); const expected = true; t.is(actual, expected); }); -test('with mixedcase subject should fail for "always uppercase"', t => { - const [actual] = subjectCase(parsed.mixedcase, 'always', 'uppercase'); +test('with mixedcase subject should fail for "always uppercase"', async t => { + const [actual] = subjectCase(await parsed.mixedcase, 'always', 'uppercase'); const expected = false; t.is(actual, expected); }); -test('with uppercase subject should fail for "never uppercase"', t => { - const [actual] = subjectCase(parsed.uppercase, 'never', 'uppercase'); +test('with uppercase subject should fail for "never uppercase"', async t => { + const [actual] = subjectCase(await parsed.uppercase, 'never', 'uppercase'); const expected = false; t.is(actual, expected); }); -test('with lowercase subject should succeed for "always uppercase"', t => { - const [actual] = subjectCase(parsed.uppercase, 'always', 'uppercase'); +test('with lowercase subject should succeed for "always uppercase"', async t => { + const [actual] = subjectCase(await parsed.uppercase, 'always', 'uppercase'); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/subject-empty.js b/@commitlint/core/src/rules/subject-empty.js similarity index 100% rename from source/rules/subject-empty.js rename to @commitlint/core/src/rules/subject-empty.js diff --git a/@commitlint/core/src/rules/subject-empty.test.js b/@commitlint/core/src/rules/subject-empty.test.js new file mode 100644 index 0000000000..e0482d855a --- /dev/null +++ b/@commitlint/core/src/rules/subject-empty.test.js @@ -0,0 +1,49 @@ +import test from 'ava'; +import parse from '../library/parse'; +import subjectEmpty from './subject-empty'; + +const messages = { + empty: 'chore: \nbody', + filled: 'chore: subject\nbody' +}; + +const parsed = { + empty: parse(messages.empty), + filled: parse(messages.filled) +}; + +test('without subject should succeed for empty keyword', async t => { + const [actual] = subjectEmpty(await parsed.empty); + const expected = true; + t.is(actual, expected); +}); + +test('without subject should fail for "never"', async t => { + const [actual] = subjectEmpty(await parsed.empty, 'never'); + const expected = false; + t.is(actual, expected); +}); + +test('without subject should succeed for "always"', async t => { + const [actual] = subjectEmpty(await parsed.empty, 'always'); + const expected = true; + t.is(actual, expected); +}); + +test('with subject fail for empty keyword', async t => { + const [actual] = subjectEmpty(await parsed.filled); + const expected = false; + t.is(actual, expected); +}); + +test('with subject succeed for "never"', async t => { + const [actual] = subjectEmpty(await parsed.filled, 'never'); + const expected = true; + t.is(actual, expected); +}); + +test('with subject fail for "always"', async t => { + const [actual] = subjectEmpty(await parsed.filled, 'always'); + const expected = false; + t.is(actual, expected); +}); diff --git a/source/rules/subject-full-stop.js b/@commitlint/core/src/rules/subject-full-stop.js similarity index 100% rename from source/rules/subject-full-stop.js rename to @commitlint/core/src/rules/subject-full-stop.js diff --git a/@commitlint/core/src/rules/subject-full-stop.test.js b/@commitlint/core/src/rules/subject-full-stop.test.js new file mode 100644 index 0000000000..7a3d4413cf --- /dev/null +++ b/@commitlint/core/src/rules/subject-full-stop.test.js @@ -0,0 +1,51 @@ +import test from 'ava'; +import parse from '../library/parse'; +import check from './subject-full-stop'; + +const messages = { + empty: 'chore:\n', + with: `chore: subject.\n`, + without: `chore: subject\n` +}; + +const parsed = { + empty: parse(messages.empty), + with: parse(messages.with), + without: parse(messages.without) +}; + +test('empty against "always" should succeed', async t => { + const [actual] = check(await parsed.empty, 'always', '.'); + const expected = true; + t.is(actual, expected); +}); + +test('empty against "never ." should succeed', async t => { + const [actual] = check(await parsed.empty, 'never', '.'); + const expected = true; + t.is(actual, expected); +}); + +test('with against "always ." should succeed', async t => { + const [actual] = check(await parsed.with, 'always', '.'); + const expected = true; + t.is(actual, expected); +}); + +test('with against "never ." should fail', async t => { + const [actual] = check(await parsed.with, 'never', '.'); + const expected = false; + t.is(actual, expected); +}); + +test('without against "always ." should fail', async t => { + const [actual] = check(await parsed.without, 'always', '.'); + const expected = false; + t.is(actual, expected); +}); + +test('without against "never ." should succeed', async t => { + const [actual] = check(await parsed.without, 'never', '.'); + const expected = true; + t.is(actual, expected); +}); diff --git a/source/rules/subject-leading-capital.js b/@commitlint/core/src/rules/subject-leading-capital.js similarity index 100% rename from source/rules/subject-leading-capital.js rename to @commitlint/core/src/rules/subject-leading-capital.js diff --git a/@commitlint/core/src/rules/subject-leading-capital.test.js b/@commitlint/core/src/rules/subject-leading-capital.test.js new file mode 100644 index 0000000000..eb920ce345 --- /dev/null +++ b/@commitlint/core/src/rules/subject-leading-capital.test.js @@ -0,0 +1,69 @@ +import test from 'ava'; +import parse from '../library/parse'; +import check from './subject-leading-capital'; + +const messages = { + empty: 'chore:\n', + with: `chore: Subject\n`, + without: `chore: subject\n` +}; + +const parsed = { + empty: parse(messages.empty), + with: parse(messages.with), + without: parse(messages.without) +}; + +test('empty should succeed', async t => { + const [actual] = check(await parsed.empty); + const expected = true; + t.is(actual, expected); +}); + +test('empty against "always" should succeed', async t => { + const [actual] = check(await parsed.empty, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('empty against "never" should succeed', async t => { + const [actual] = check(await parsed.empty, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with should succeed', async t => { + const [actual] = check(await parsed.with); + const expected = true; + t.is(actual, expected); +}); + +test('with against "always" should succeed', async t => { + const [actual] = check(await parsed.with, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with against "never" should fail', async t => { + const [actual] = check(await parsed.with, 'never', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('without should fail', async t => { + const [actual] = check(await parsed.without, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('without against "always" should fail', async t => { + const [actual] = check(await parsed.without, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('without against "never" should succeed', async t => { + const [actual] = check(await parsed.without, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); diff --git a/source/rules/subject-max-length.js b/@commitlint/core/src/rules/subject-max-length.js similarity index 100% rename from source/rules/subject-max-length.js rename to @commitlint/core/src/rules/subject-max-length.js diff --git a/source/rules/subject-max-length.test.js b/@commitlint/core/src/rules/subject-max-length.test.js similarity index 64% rename from source/rules/subject-max-length.test.js rename to @commitlint/core/src/rules/subject-max-length.test.js index 7a50873d70..432ce73de0 100644 --- a/source/rules/subject-max-length.test.js +++ b/@commitlint/core/src/rules/subject-max-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should succeed', t => { - const [actual] = check(parsed.short, '', value); +test('with short should succeed', async t => { + const [actual] = check(await parsed.short, '', value); const expected = true; t.is(actual, expected); }); -test('with long should fail', t => { - const [actual] = check(parsed.long, '', value); +test('with long should fail', async t => { + const [actual] = check(await parsed.long, '', value); const expected = false; t.is(actual, expected); }); diff --git a/source/rules/subject-min-length.js b/@commitlint/core/src/rules/subject-min-length.js similarity index 100% rename from source/rules/subject-min-length.js rename to @commitlint/core/src/rules/subject-min-length.js diff --git a/source/rules/subject-min-length.test.js b/@commitlint/core/src/rules/subject-min-length.test.js similarity index 64% rename from source/rules/subject-min-length.test.js rename to @commitlint/core/src/rules/subject-min-length.test.js index b77ea43a24..b1df88d5ce 100644 --- a/source/rules/subject-min-length.test.js +++ b/@commitlint/core/src/rules/subject-min-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should fail', t => { - const [actual] = check(parsed.short, '', value); +test('with short should fail', async t => { + const [actual] = check(await parsed.short, '', value); const expected = false; t.is(actual, expected); }); -test('with long should succeed', t => { - const [actual] = check(parsed.long, '', value); +test('with long should succeed', async t => { + const [actual] = check(await parsed.long, '', value); const expected = true; t.is(actual, expected); }); diff --git a/source/rules/subject-tense.js b/@commitlint/core/src/rules/subject-tense.js similarity index 100% rename from source/rules/subject-tense.js rename to @commitlint/core/src/rules/subject-tense.js diff --git a/@commitlint/core/src/rules/subject-tense.test.js b/@commitlint/core/src/rules/subject-tense.test.js new file mode 100644 index 0000000000..4946fec824 --- /dev/null +++ b/@commitlint/core/src/rules/subject-tense.test.js @@ -0,0 +1,114 @@ +import test from 'ava'; +import parse from '../library/parse'; +import footerTense from './subject-tense'; + +const messages = { + empty: 'chore: \n', + presentImperative: `chore: we implement things`, + presentParticiple: `chore: implementing things`, + presentThirdPerson: `chore: implements things`, + past: `chore: we did implement things`, + mixed: `chore: implement, implementing, implements, implemented` +}; + +const parsed = { + empty: parse(messages.empty), + presentImperative: parse(messages.presentImperative), + presentParticiple: parse(messages.presentParticiple), + presentThirdPerson: parse(messages.presentImperative), + past: parse(messages.past), + mixed: parse(messages.mixed) +}; + +test('empty succeeds', async t => { + const [actual] = footerTense(await parsed.empty, '', ['present-imperative']); + const expected = true; + t.is(actual, expected); +}); + +test('present succeeds "always present-imperative"', async t => { + const [actual] = footerTense(await parsed.presentImperative, 'always', ['present-imperative']); + const expected = true; + t.is(actual, expected); +}); + +test('present fails "never present-imperative"', async t => { + const [actual] = footerTense(await parsed.presentImperative, 'never', ['present-imperative']); + const expected = false; + t.is(actual, expected); +}); + +test('present succeeds "always present-participle"', async t => { + const [actual] = footerTense(await parsed.presentParticiple, 'always', ['present-participle']); + const expected = true; + t.is(actual, expected); +}); + +test('present fails "never present-participle"', async t => { + const [actual] = footerTense(await parsed.presentParticiple, 'never', ['present-participle']); + const expected = false; + t.is(actual, expected); +}); + +test('present succeeds "always present-third-person"', async t => { + const [actual] = footerTense(await parsed.presentThirdPerson, 'always', ['present-third-person']); + const expected = true; + t.is(actual, expected); +}); + +test('present fails "never present-third-person"', async t => { + const [actual] = footerTense(await parsed.presentThirdPerson, 'never', ['present-third-person']); + const expected = false; + t.is(actual, expected); +}); + +test('past should succedd "always past-tense"', async t => { + const [actual] = footerTense(await parsed.past, 'always', ['past-tense']); + const expected = true; + t.is(actual, expected); +}); + +test('past fails "never past-tense"', async t => { + const [actual] = footerTense(await parsed.past, 'never', ['past-tense']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed fails "always present-third-person"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-third-person']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed fails "always present-imperative"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-imperative']); + const expected = false; + t.is(actual, expected); +}); + +test('present fails "always present-participle"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-participle']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed fails "always past-tense"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['past-tense']); + const expected = false; + t.is(actual, expected); +}); + +test('mixed succeeds "always present-third-person, present-imperative, present-participle, past-tense"', async t => { + const [actual] = footerTense(await parsed.mixed, 'always', ['present-third-person', 'present-imperative', 'present-participle', 'past-tense']); + const expected = true; + t.is(actual, expected); +}); + +test('mixed succeeds "never allowed: present-third-person" and matching ignored: implements', async t => { + const [actual] = footerTense(await parsed.mixed, 'never', { + allowed: ['present-third-person'], + ignored: ['implements'] + }); + const expected = true; + t.is(actual, expected); +}); diff --git a/source/rules/type-case.js b/@commitlint/core/src/rules/type-case.js similarity index 100% rename from source/rules/type-case.js rename to @commitlint/core/src/rules/type-case.js diff --git a/@commitlint/core/src/rules/type-case.test.js b/@commitlint/core/src/rules/type-case.test.js new file mode 100644 index 0000000000..1729f5686a --- /dev/null +++ b/@commitlint/core/src/rules/type-case.test.js @@ -0,0 +1,89 @@ +import test from 'ava'; +import parse from '../library/parse'; +import typeCase from './type-case'; + +const messages = { + empty: '(scope): subject', + lowercase: 'type: subject', + mixedcase: 'tYpE: subject', + uppercase: 'TYPE: subject' +}; + +const parsed = { + empty: parse(messages.empty), + lowercase: parse(messages.lowercase), + mixedcase: parse(messages.mixedcase), + uppercase: parse(messages.uppercase) +}; + +test('with empty type should succeed for "never lowercase"', async t => { + const [actual] = typeCase(await parsed.empty, 'never', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with empty type should succeed for "always lowercase"', async t => { + const [actual] = typeCase(await parsed.empty, 'always', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with empty type should succeed for "never uppercase"', async t => { + const [actual] = typeCase(await parsed.empty, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with empty type should succeed for "always uppercase"', async t => { + const [actual] = typeCase(await parsed.empty, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with lowercase type should fail for "never lowercase"', async t => { + const [actual] = typeCase(await parsed.lowercase, 'never', 'lowercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with lowercase type should succeed for "always lowercase"', async t => { + const [actual] = typeCase(await parsed.lowercase, 'always', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase type should succeed for "never lowercase"', async t => { + const [actual] = typeCase(await parsed.mixedcase, 'never', 'lowercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase type should fail for "always lowercase"', async t => { + const [actual] = typeCase(await parsed.mixedcase, 'always', 'lowercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with mixedcase type should succeed for "never uppercase"', async t => { + const [actual] = typeCase(await parsed.mixedcase, 'never', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); + +test('with mixedcase type should fail for "always uppercase"', async t => { + const [actual] = typeCase(await parsed.mixedcase, 'always', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with uppercase type should fail for "never uppercase"', async t => { + const [actual] = typeCase(await parsed.uppercase, 'never', 'uppercase'); + const expected = false; + t.is(actual, expected); +}); + +test('with lowercase type should succeed for "always uppercase"', async t => { + const [actual] = typeCase(await parsed.uppercase, 'always', 'uppercase'); + const expected = true; + t.is(actual, expected); +}); diff --git a/source/rules/type-empty.js b/@commitlint/core/src/rules/type-empty.js similarity index 100% rename from source/rules/type-empty.js rename to @commitlint/core/src/rules/type-empty.js diff --git a/@commitlint/core/src/rules/type-empty.test.js b/@commitlint/core/src/rules/type-empty.test.js new file mode 100644 index 0000000000..76c8c6860e --- /dev/null +++ b/@commitlint/core/src/rules/type-empty.test.js @@ -0,0 +1,49 @@ +import test from 'ava'; +import parse from '../library/parse'; +import typeEmpty from './type-empty'; + +const messages = { + empty: '(scope):', + filled: 'type: subject' +}; + +const parsed = { + empty: parse(messages.empty), + filled: parse(messages.filled) +}; + +test('without type should succeed for empty keyword', async t => { + const [actual] = typeEmpty(await parsed.empty); + const expected = true; + t.is(actual, expected); +}); + +test('without type should fail for "never"', async t => { + const [actual] = typeEmpty(await parsed.empty, 'never'); + const expected = false; + t.is(actual, expected); +}); + +test('without type should succeed for "always"', async t => { + const [actual] = typeEmpty(await parsed.empty, 'always'); + const expected = true; + t.is(actual, expected); +}); + +test('with type fail for empty keyword', async t => { + const [actual] = typeEmpty(await parsed.filled); + const expected = false; + t.is(actual, expected); +}); + +test('with type succeed for "never"', async t => { + const [actual] = typeEmpty(await parsed.filled, 'never'); + const expected = true; + t.is(actual, expected); +}); + +test('with type fail for "always"', async t => { + const [actual] = typeEmpty(await parsed.filled, 'always'); + const expected = false; + t.is(actual, expected); +}); diff --git a/source/rules/type-enum.js b/@commitlint/core/src/rules/type-enum.js similarity index 100% rename from source/rules/type-enum.js rename to @commitlint/core/src/rules/type-enum.js diff --git a/@commitlint/core/src/rules/type-enum.test.js b/@commitlint/core/src/rules/type-enum.test.js new file mode 100644 index 0000000000..698e7a0794 --- /dev/null +++ b/@commitlint/core/src/rules/type-enum.test.js @@ -0,0 +1,123 @@ +import test from 'ava'; +import parse from '../library/parse'; +import check from './type-enum'; + +const messages = { + empty: '(): \n', + a: 'a(): \n', + b: 'b(): \n' +}; + +const parsed = { + empty: parse(messages.empty), + a: parse(messages.a), + b: parse(messages.b) +}; + +test('empty succeeds', async t => { + const [actual] = check(await parsed.empty); + const expected = true; + t.is(actual, expected); +}); + +test('empty on "a" succeeds', async t => { + const [actual] = check(await parsed.empty, '', ['a']); + const expected = true; + t.is(actual, expected); +}); + +test('empty on "always a" succeeds', async t => { + const [actual] = check(await parsed.empty, 'always', ['a']); + const expected = true; + t.is(actual, expected); +}); + +test('empty on "never a" succeeds', async t => { + const [actual] = check(await parsed.empty, 'never', ['a']); + const expected = true; + t.is(actual, expected); +}); + +test('empty on "always a, b" succeeds', async t => { + const [actual] = check(await parsed.empty, 'always', ['a', 'b']); + const expected = true; + t.is(actual, expected); +}); + +test('empty on "never a, b" succeeds', async t => { + const [actual] = check(await parsed.empty, 'neber', ['a', 'b']); + const expected = true; + t.is(actual, expected); +}); + +test('a on "a" succeeds', async t => { + const [actual] = check(await parsed.a, '', ['a']); + const expected = true; + t.is(actual, expected); +}); + +test('a on "always a" succeeds', async t => { + const [actual] = check(await parsed.a, 'always', ['a']); + const expected = true; + t.is(actual, expected); +}); + +test('a on "never a" fails', async t => { + const [actual] = check(await parsed.a, 'never', ['a']); + const expected = false; + t.is(actual, expected); +}); + +test('b on "b" succeeds', async t => { + const [actual] = check(await parsed.b, '', ['b']); + const expected = true; + t.is(actual, expected); +}); + +test('b on "always b" succeeds', async t => { + const [actual] = check(await parsed.b, 'always', ['b']); + const expected = true; + t.is(actual, expected); +}); + +test('b on "never b" fails', async t => { + const [actual] = check(await parsed.b, 'never', ['b']); + const expected = false; + t.is(actual, expected); +}); + +test('a on "a, b" succeeds', async t => { + const [actual] = check(await parsed.a, '', ['a', 'b']); + const expected = true; + t.is(actual, expected); +}); + +test('a on "always a, b" succeeds', async t => { + const [actual] = check(await parsed.a, 'always', ['a', 'b']); + const expected = true; + t.is(actual, expected); +}); + +test('a on "never a, b" fails', async t => { + const [actual] = check(await parsed.a, 'never', ['a', 'b']); + const expected = false; + t.is(actual, expected); +}); + +test('b on "a, b" succeeds', async t => { + const [actual] = check(await parsed.b, '', ['a', 'b']); + const expected = true; + t.is(actual, expected); +}); + +test('b on "always a, b" succeeds', async t => { + const [actual] = check(await parsed.b, 'always', ['a', 'b']); + const expected = true; + t.is(actual, expected); +}); + +test('b on "never a, b" fails', async t => { + const [actual] = check(await parsed.b, 'never', ['a', 'b']); + const expected = false; + t.is(actual, expected); +}); diff --git a/source/rules/type-max-length.js b/@commitlint/core/src/rules/type-max-length.js similarity index 100% rename from source/rules/type-max-length.js rename to @commitlint/core/src/rules/type-max-length.js diff --git a/source/rules/type-max-length.test.js b/@commitlint/core/src/rules/type-max-length.test.js similarity index 63% rename from source/rules/type-max-length.test.js rename to @commitlint/core/src/rules/type-max-length.test.js index 1be87bb227..d3f3e7bfb6 100644 --- a/source/rules/type-max-length.test.js +++ b/@commitlint/core/src/rules/type-max-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should succeed', t => { - const [actual] = check(parsed.short, '', value); +test('with short should succeed', async t => { + const [actual] = check(await parsed.short, '', value); const expected = true; t.is(actual, expected); }); -test('with long should fail', t => { - const [actual] = check(parsed.long, '', value); +test('with long should fail', async t => { + const [actual] = check(await parsed.long, '', value); const expected = false; t.is(actual, expected); }); diff --git a/source/rules/type-min-length.js b/@commitlint/core/src/rules/type-min-length.js similarity index 100% rename from source/rules/type-min-length.js rename to @commitlint/core/src/rules/type-min-length.js diff --git a/source/rules/type-min-length.test.js b/@commitlint/core/src/rules/type-min-length.test.js similarity index 63% rename from source/rules/type-min-length.test.js rename to @commitlint/core/src/rules/type-min-length.test.js index 4f3cd59636..2371df2324 100644 --- a/source/rules/type-min-length.test.js +++ b/@commitlint/core/src/rules/type-min-length.test.js @@ -19,20 +19,20 @@ const parsed = { long: parse(messages.long) }; -test('with empty should succeed', t => { - const [actual] = check(parsed.empty, '', value); +test('with empty should succeed', async t => { + const [actual] = check(await parsed.empty, '', value); const expected = true; t.is(actual, expected); }); -test('with short should fail', t => { - const [actual] = check(parsed.short, '', value); +test('with short should fail', async t => { + const [actual] = check(await parsed.short, '', value); const expected = false; t.is(actual, expected); }); -test('with long should succeed', t => { - const [actual] = check(parsed.long, '', value); +test('with long should succeed', async t => { + const [actual] = check(await parsed.long, '', value); const expected = true; t.is(actual, expected); }); diff --git a/@commitlint/example-prompt/CHANGELOG.md b/@commitlint/example-prompt/CHANGELOG.md new file mode 100644 index 0000000000..d9d2d4caa7 --- /dev/null +++ b/@commitlint/example-prompt/CHANGELOG.md @@ -0,0 +1,18 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + + + +# 3.0.0 (2017-07-10) + + + + + +# 3.0.0 (2017-07-10) diff --git a/@commitlint/example-prompt/package.json b/@commitlint/example-prompt/package.json new file mode 100644 index 0000000000..59bf2a3077 --- /dev/null +++ b/@commitlint/example-prompt/package.json @@ -0,0 +1,22 @@ +{ + "name": "@commitlint/example-prompt", + "private": true, + "version": "3.0.1", + "description": "Example for prompt guide", + "scripts": { + "commit": "commit" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "devDependencies": { + "@commitlint/prompt-cli": "^3.0.1" + } +} diff --git a/@commitlint/prompt-cli/CHANGELOG.md b/@commitlint/prompt-cli/CHANGELOG.md new file mode 100644 index 0000000000..6ba43407f3 --- /dev/null +++ b/@commitlint/prompt-cli/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* **prompt-cli:** add standalone prompt interface ([b0239d2](https://github.com/marionebl/commitlint/commit/b0239d2)) + + + + + +# 3.0.0 (2017-07-10) + + +### Features + +* **prompt-cli:** add standalone prompt interface ([b0239d2](https://github.com/marionebl/commitlint/commit/b0239d2)) diff --git a/@commitlint/prompt-cli/cli.js b/@commitlint/prompt-cli/cli.js new file mode 100755 index 0000000000..0936d26df3 --- /dev/null +++ b/@commitlint/prompt-cli/cli.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node +const execa = require('execa'); +const meow = require('meow'); +const prompter = require('@commitlint/prompt').prompter; + +const HELP = ` + Usage + $ commit +`; + +const _ = undefined; +const prompt = () => prompter(_, commit); + +main(meow(HELP)) + .catch(err => { + setTimeout(() => { + throw err; + }); + }); + +function main() { + return prompt(); +} + +function commit(message) { + const c = execa('git', ['commit', '-m', message]); + c.stdout.pipe(process.stdout); + c.stderr.pipe(process.stderr); +} diff --git a/@commitlint/prompt-cli/package.json b/@commitlint/prompt-cli/package.json new file mode 100644 index 0000000000..3594503ef4 --- /dev/null +++ b/@commitlint/prompt-cli/package.json @@ -0,0 +1,36 @@ +{ + "name": "@commitlint/prompt-cli", + "version": "3.0.1", + "description": "commit prompt using .commitlintrc", + "bin": { + "commit": "./cli.js" + }, + "scripts": { + "clean": "rimraf lib", + "commit": "$npm_package_bin_commit", + "pretest": "dep-check" + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "commitlint", + "prompt" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "devDependencies": { + "@commitlint/utils": "^3.0.0" + }, + "dependencies": { + "@commitlint/prompt": "^3.0.1", + "execa": "^0.7.0", + "meow": "^3.7.0" + } +} diff --git a/@commitlint/prompt-cli/readme.md b/@commitlint/prompt-cli/readme.md new file mode 100644 index 0000000000..31e79b2b1f --- /dev/null +++ b/@commitlint/prompt-cli/readme.md @@ -0,0 +1,15 @@ +> commit prompt using .commitlintrc + +# @commitlint/prompt-cli + +## Getting started + +```bash +npm install --g @commitlint/prompt-cli @commitlint/config-angular +echo "module.exports = {extends: ['@commitlint/config-angular']};" > .commitlint.config.js +``` + +```bash +git add . +commit +``` diff --git a/@commitlint/prompt/CHANGELOG.md b/@commitlint/prompt/CHANGELOG.md new file mode 100644 index 0000000000..d9d2d4caa7 --- /dev/null +++ b/@commitlint/prompt/CHANGELOG.md @@ -0,0 +1,18 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + +## 3.0.1 (2017-07-11) + + + + +# 3.0.0 (2017-07-10) + + + + + +# 3.0.0 (2017-07-10) diff --git a/@commitlint/prompt/package.json b/@commitlint/prompt/package.json new file mode 100644 index 0000000000..f30d429132 --- /dev/null +++ b/@commitlint/prompt/package.json @@ -0,0 +1,114 @@ +{ + "name": "@commitlint/prompt", + "version": "3.0.1", + "description": "commitizen prompt using .commitlintrc", + "main": "./lib/index.js", + "scripts": { + "build": "cross-env NODE_ENV=production babel src --out-dir lib", + "clean": "rimraf lib", + "commit": "git-cz", + "pretest": "dep-check", + "test": "ava", + "prepublish": "npm run build" + }, + "ava": { + "babel": "inherit", + "require": [ + "babel-register" + ], + "files": [ + "src/**/*.test.js" + ], + "sources": [ + "src/**/*.js" + ] + }, + "babel": { + "env": { + "development": { + "sourceMaps": "inline", + "plugins": [ + "add-module-exports", + [ + "transform-runtime", + { + "polyfill": false, + "regenerator": true + } + ] + ] + }, + "production": { + "ignore": [ + "**/*.test.js" + ] + } + }, + "presets": [ + [ + "env", + { + "targets": { + "node": 4 + } + } + ], + "stage-0" + ], + "plugins": [ + "add-module-exports", + [ + "transform-runtime", + { + "polyfill": false, + "regenerator": true + } + ] + ] + }, + "config": { + "commitizen": { + "path": "./@commitlint/prompt" + } + }, + "xo": false, + "repository": { + "type": "git", + "url": "git+https://github.com/marionebl/commitlint.git" + }, + "keywords": [ + "conventional-changelog", + "commitlint", + "prompt", + "cz", + "commitizen" + ], + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/marionebl/commitlint/issues" + }, + "homepage": "https://github.com/marionebl/commitlint#readme", + "devDependencies": { + "@commitlint/utils": "^3.0.0", + "ava": "^0.20.0", + "babel-cli": "^6.24.1", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-env": "^1.6.0", + "babel-preset-stage-0": "^6.24.1", + "babel-register": "^6.24.1", + "commitizen": "^2.9.6", + "cross-env": "^5.0.1", + "throat": "^4.1.0" + }, + "dependencies": { + "@commitlint/core": "^3.0.1", + "babel-polyfill": "^6.23.0", + "babel-runtime": "^6.23.0", + "chalk": "^1.1.1", + "lodash": "^4.17.4", + "throat": "^4.1.0", + "vorpal": "^1.10.0" + } +} diff --git a/@commitlint/prompt/readme.md b/@commitlint/prompt/readme.md new file mode 100644 index 0000000000..b10fd2665f --- /dev/null +++ b/@commitlint/prompt/readme.md @@ -0,0 +1,29 @@ +> commitizen adapter using .commitlintrc + +# @commitlint/prompt + +## Getting started + +```bash +npm install --save @commitlint/prompt @commitlint/config-angular commitizen +echo "module.exports = {extends: ['@commitlint/config-angular']};" > .commitlint.config.js +``` + +In package.json +``` +{ + "scripts": { + "commit": "git-cz" + }, + "config": { + "commitizen": { + "path": "@commitlint/prompt" + } + } +} +``` + +```bash +git add . +npm run commit +``` diff --git a/@commitlint/prompt/src/index.js b/@commitlint/prompt/src/index.js new file mode 100644 index 0000000000..78d5ab2a17 --- /dev/null +++ b/@commitlint/prompt/src/index.js @@ -0,0 +1,14 @@ +import 'babel-polyfill'; // eslint-disable-line import/no-unassigned-import +import vorpal from 'vorpal'; +import input from './input'; + +/** + * Entry point for commitizen + * @param {object} _ inquirer instance passed by commitizen, unused + * @param {function} commit callback to execute with complete commit message + * @return {string} genersated commit message + */ +export const prompter = async (_, commit) => { // eslint-disable-line import/prefer-default-export + const message = await input(vorpal); + commit(message); +}; diff --git a/@commitlint/prompt/src/input.js b/@commitlint/prompt/src/input.js new file mode 100644 index 0000000000..d4779d9169 --- /dev/null +++ b/@commitlint/prompt/src/input.js @@ -0,0 +1,94 @@ +import {load} from '@commitlint/core'; +import throat from 'throat'; + +import format from './library/format'; +import getHasName from './library/get-has-name'; +import getPrompt from './library/get-prompt'; +import settings from './settings'; + +export default input; + +/* eslint-disable valid-jsdoc */ +/** + * Get user input by interactive prompt based on + * conventional-changelog-lint rules. + * @param {function} prompter + * @return {Promise} commit message + */ +async function input(prompter) { + const results = { + type: null, + scope: null, + subject: null, + body: null, + footer: null + }; + + const {rules} = await load(); + + await Promise.all(['type', 'scope', 'subject', 'body', 'footer'] + .map(throat(1, async input => { + const inputRules = getRules(input, rules); + const inputSettings = settings[input]; + + const isHeader = ['type', 'scope', 'subject'].indexOf(input) > -1; + + const headerLengthRule = getRules('header', rules) + .filter(getHasName('max-length'))[0]; + + if (isHeader && headerLengthRule) { + const [, [severity, applicable, length]] = headerLengthRule; + if (severity > 0 && applicable === 'always') { + inputSettings.header = { + length + }; + } + } + + results[input] = await getPrompt(input, { // eslint-disable-line no-await-in-loop + rules: inputRules, + settings: inputSettings, + results, + prompter + }); + }))) + .catch(err => { + console.error(err); + return ''; + }); + + // Return the results + return format(results); +} + +/** + * Get prefix for a given rule id + * @param {string} id of the rule + * @return {string} prefix of the rule + */ +function getRulePrefix(id) { + const fragments = id.split('-'); + const [prefix] = fragments; + return fragments.length > 1 ? + prefix : + null; +} + +/** + * Get a predecate matching rule definitions with a given prefix + * @param {[type]} name [description] + * @return {[type]} [description] + */ +function getHasPrefix(name) { + return rule => getRulePrefix(rule[0]) === name; +} + +/** + * Get rules for a given prefix + * @param {string} prefix to search in rule names + * @param {object} rules rules to search in + * @return {object} rules matching the prefix search + */ +function getRules(prefix, rules) { + return Object.entries(rules).filter(getHasPrefix(prefix)); +} diff --git a/@commitlint/prompt/src/library/enum-rule-is-active.js b/@commitlint/prompt/src/library/enum-rule-is-active.js new file mode 100644 index 0000000000..b20318ed21 --- /dev/null +++ b/@commitlint/prompt/src/library/enum-rule-is-active.js @@ -0,0 +1,14 @@ +import ruleIsApplicable from './rule-is-applicable'; +import ruleIsActive from './rule-is-active'; + +/** + * [enumRuleIsActive description] + * @param {[type]} rule [description] + * @return {[type]} [description] + */ +export default function enumRuleIsActive(rule) { + const [, [, , value]] = rule; + return ruleIsActive(rule) && + ruleIsApplicable(rule) && + value.length > 0; +} diff --git a/@commitlint/prompt/src/library/format.js b/@commitlint/prompt/src/library/format.js new file mode 100644 index 0000000000..19d3d5b48f --- /dev/null +++ b/@commitlint/prompt/src/library/format.js @@ -0,0 +1,33 @@ +import chalk from 'chalk'; +import {entries} from 'lodash'; + +export default format; + +/** + * Get formatted commit message + * @param {object} input object containing structured results + * @param {boolean} debug show debug information in commit message + * @return {string} formatted debug message + */ +function format(input, debug = false) { + const results = debug ? + entries(input) + .reduce((registry, item) => { + const [name, value] = item; + return { + ...registry, + [name]: value === null ? + chalk.grey(`<${name}>`) : + chalk.bold(value) + }; + }, {}) : + input; + + // Return formatted string + const {type, scope, subject, body, footer} = results; + return [ + `${type}${scope ? '(' : ''}${scope}${scope ? ')' : ''}${type || scope ? ':' : ''} ${subject}`, + body, + footer + ].filter(Boolean).join('\n'); +} diff --git a/@commitlint/prompt/src/library/get-forced-case-fn.js b/@commitlint/prompt/src/library/get-forced-case-fn.js new file mode 100644 index 0000000000..b5be3a239a --- /dev/null +++ b/@commitlint/prompt/src/library/get-forced-case-fn.js @@ -0,0 +1,26 @@ +import getForcedCase from './get-forced-case'; + +/** + * Get forced case for rule + * @param {object} rule to parse + * @return {fn} transform function applying the enforced case + */ +export default function getForcedCaseFn(rule) { + const noop = input => input; + const lowerCase = input => String.prototype.toLowerCase.call(input); + const upperCase = input => String.prototype.toUpperCase.call(input); + + if (!rule) { + return noop; + } + + const forcedCase = getForcedCase(rule); + + if (forcedCase === null) { + return noop; + } + + return forcedCase === 'lowerCase' ? + lowerCase : + upperCase; +} diff --git a/@commitlint/prompt/src/library/get-forced-case.js b/@commitlint/prompt/src/library/get-forced-case.js new file mode 100644 index 0000000000..8927a9f4ac --- /dev/null +++ b/@commitlint/prompt/src/library/get-forced-case.js @@ -0,0 +1,27 @@ +/** + * Get forced case for rule + * @param {object} rule to parse + * @return {string|null} transform function applying the enforced case + */ +export default function getForcedCase(rule) { + if (!rule) { + return null; + } + + const [, [severity, applicable, value]] = rule; + const negated = applicable === 'never'; + + if (severity === 0) { + return null; + } + + if (negated) { + return value === 'lowerCase' ? + 'upperCase' : + 'lowerCase'; + } + + return value === 'lowerCase' ? + 'lowerCase' : + 'upperCase'; +} diff --git a/@commitlint/prompt/src/library/get-forced-case.js-fn.js b/@commitlint/prompt/src/library/get-forced-case.js-fn.js new file mode 100644 index 0000000000..b5be3a239a --- /dev/null +++ b/@commitlint/prompt/src/library/get-forced-case.js-fn.js @@ -0,0 +1,26 @@ +import getForcedCase from './get-forced-case'; + +/** + * Get forced case for rule + * @param {object} rule to parse + * @return {fn} transform function applying the enforced case + */ +export default function getForcedCaseFn(rule) { + const noop = input => input; + const lowerCase = input => String.prototype.toLowerCase.call(input); + const upperCase = input => String.prototype.toUpperCase.call(input); + + if (!rule) { + return noop; + } + + const forcedCase = getForcedCase(rule); + + if (forcedCase === null) { + return noop; + } + + return forcedCase === 'lowerCase' ? + lowerCase : + upperCase; +} diff --git a/@commitlint/prompt/src/library/get-forced-leading-fn.js b/@commitlint/prompt/src/library/get-forced-leading-fn.js new file mode 100644 index 0000000000..8b304282ef --- /dev/null +++ b/@commitlint/prompt/src/library/get-forced-leading-fn.js @@ -0,0 +1,54 @@ +/** + * Get forced leading for rule + * @param {object} rule to parse + * @return {fn} transform function applying the leading + */ +export default function getForcedLeadingFn(rule) { + const noop = input => input; + const remove = input => { + const fragments = input.split('\n'); + return fragments[0] === '' ? + fragments.slice(1).join('\n') : + input; + }; + const lead = input => { + const fragments = input.split('\n'); + return fragments[0] === '' ? + input : + ['', ...fragments].join('\n'); + }; + + if (!rule) { + return noop; + } + + const leading = getForcedLeading(rule); + + if (leading === null) { + return noop; + } + + return leading ? + lead : + remove; +} + +/** + * Get forced leading for rule + * @param {object} rule to parse + * @return {boolean|null} transform function applying the leading + */ +function getForcedLeading(rule) { + if (!rule) { + return null; + } + + const [, [severity, applicable]] = rule; + const negated = applicable === 'never'; + + if (severity === 0) { + return null; + } + + return !negated; +} diff --git a/@commitlint/prompt/src/library/get-has-name.js b/@commitlint/prompt/src/library/get-has-name.js new file mode 100644 index 0000000000..1f7df8f102 --- /dev/null +++ b/@commitlint/prompt/src/library/get-has-name.js @@ -0,0 +1,10 @@ +import getRuleName from './get-rule-name'; + +/** + * Get a predecate matching rule definitions with a given name + * @param {[type]} name [description] + * @return {[type]} [description] + */ +export default function getHasName(name) { + return rule => getRuleName(rule[0]) === name; +} diff --git a/@commitlint/prompt/src/library/get-prompt.js b/@commitlint/prompt/src/library/get-prompt.js new file mode 100644 index 0000000000..3f5c4f843e --- /dev/null +++ b/@commitlint/prompt/src/library/get-prompt.js @@ -0,0 +1,244 @@ +import chalk from 'chalk'; + +import enumRuleIsActive from './enum-rule-is-active'; +import format from './format'; +import getForcedCase from './get-forced-case'; +import getForcedCaseFn from './get-forced-case-fn'; +import getForcedLeadingFn from './get-forced-leading-fn'; +import getHasName from './get-has-name'; +import meta from './meta'; + +export default getPrompt; + +/** + * Get a cli prompt based on rule configuration + * @param {string} type type of the data to gather + * @param {object} context rules to parse + * @return {object} prompt instance + */ +function getPrompt(type, context = {}) { + const {rules = [], settings = {}, results = {}, prompter} = context; + + if (typeof prompter !== 'function') { + throw new TypeError('Missing prompter function in getPrompt context'); + } + + const prompt = prompter(); + + if (typeof prompt.removeAllListeners !== 'function') { + throw new TypeError('getPrompt: prompt.removeAllListeners is not a function'); + } + + if (typeof prompt.command !== 'function') { + throw new TypeError('getPrompt: prompt.command is not a function'); + } + + if (typeof prompt.catch !== 'function') { + throw new TypeError('getPrompt: prompt.catch is not a function'); + } + + if (typeof prompt.addListener !== 'function') { + throw new TypeError('getPrompt: prompt.addListener is not a function'); + } + + if (typeof prompt.log !== 'function') { + throw new TypeError('getPrompt: prompt.log is not a function'); + } + + if (typeof prompt.delimiter !== 'function') { + throw new TypeError('getPrompt: prompt.delimiter is not a function'); + } + + if (typeof prompt.show !== 'function') { + throw new TypeError('getPrompt: prompt.show is not a function'); + } + + const enumRule = rules + .filter(getHasName('enum')) + .filter(enumRuleIsActive)[0]; + + const emptyRule = rules + .filter(getHasName('empty'))[0]; + + const mustBeEmpty = emptyRule ? + emptyRule[1][0] > 0 && + emptyRule[1][1] === 'always' : + false; + + const mayNotBeEmpty = emptyRule ? + emptyRule[1][0] > 0 && + emptyRule[1][1] === 'never' : + false; + + const mayBeEmpty = !mayNotBeEmpty; + + if (mustBeEmpty) { + prompt.removeAllListeners('keypress'); + prompt.removeAllListeners('client_prompt_submit'); + prompt.ui.redraw.done(); + return Promise.resolve(); + } + + const caseRule = rules + .filter(getHasName('case'))[0]; + + const forcedCase = getForcedCase(caseRule); + const forceCaseFn = getForcedCaseFn(caseRule); + + const leadingBlankRule = rules + .filter(getHasName('leading-blank'))[0]; + + const forceLeadingBlankFn = getForcedLeadingFn(leadingBlankRule); + + const maxLenghtRule = rules + .filter(getHasName('max-length'))[0]; + + const hasMaxLength = maxLenghtRule && maxLenghtRule[1][0] > 0; + + const inputMaxLength = hasMaxLength ? + maxLenghtRule[1][1] : + Infinity; + + const headerLength = settings.header ? + settings.header.length : + Infinity; + + const remainingHeaderLength = headerLength ? + headerLength - [ + results.type, + results.scope, + results.scope ? '()' : '', + results.type && results.scope ? ':' : '', + results.subject + ].join('').length : + Infinity; + + const maxLength = Math.min(inputMaxLength, remainingHeaderLength); + + return new Promise(resolve => { + // Add the defined enums as sub commands if applicable + if (enumRule) { + const [, [, , enums]] = enumRule; + + enums.forEach(enumerable => { + const enumSettings = (settings.enumerables || {})[enumerable] || {}; + prompt + .command(enumerable) + .description(enumSettings.description || '') + .action(() => { + prompt.removeAllListeners(); + prompt.ui.redraw.done(); + return resolve(forceLeadingBlankFn(forceCaseFn(enumerable))); + }); + }); + } else { + prompt + .catch('[text...]') + .action(parameters => { + const {text = ''} = parameters; + prompt.removeAllListeners(); + prompt.ui.redraw.done(); + return resolve(forceLeadingBlankFn(forceCaseFn(text.join(' ')))); + }); + } + + if (mayBeEmpty) { + // Add an easy exit command + prompt + .command(':skip') + .description('Skip the input if possible.') + .action(() => { + prompt.removeAllListeners(); + prompt.ui.redraw.done(); + resolve(''); + }); + } + + // Handle empty input + const onSubmit = input => { + if (input.length > 0) { + return; + } + + // Show help if enum is defined and input may not be empty + if (mayNotBeEmpty) { + prompt.ui.log(chalk.yellow(`⚠ ${chalk.bold(type)} may not be empty.`)); + } + + if (mayBeEmpty) { + prompt.ui.log(chalk.blue(`ℹ Enter ${chalk.bold(':skip')} to omit ${chalk.bold(type)}.`)); + } + + if (enumRule) { + prompt.exec('help'); + } + }; + + const drawRemaining = length => { + if (length < Infinity) { + const colors = [ + { + threshold: 5, + color: 'red' + }, + { + threshold: 10, + color: 'yellow' + }, + { + threshold: Infinity, + color: 'grey' + } + ]; + + const color = colors + .filter(item => { + return item.threshold >= length; + }) + .map(item => item.color)[0]; + + prompt.ui.redraw(chalk[color](`${length} characters left`)); + } + }; + + const onKey = event => { + const sanitized = forceCaseFn(event.value); + const cropped = sanitized.slice(0, maxLength); + + // We **could** do live editing, but there are some quirks to solve + /* const live = merge({}, results, { + [type]: cropped + }); + prompt.ui.redraw(`\n\n${format(live, true)}\n\n`); */ + + if (maxLength) { + drawRemaining(maxLength - cropped.length); + } + prompt.ui.input(cropped); + }; + + prompt.addListener('keypress', onKey); + prompt.addListener('client_prompt_submit', onSubmit); + + prompt.log(`\n\nPlease enter a ${chalk.bold(type)}: ${meta({ + optional: !mayNotBeEmpty, + required: mayNotBeEmpty, + 'tab-completion': typeof enumRule !== 'undefined', + header: typeof settings.header !== 'undefined', + case: forcedCase, + 'multi-line': settings.multiline + })}`); + + if (settings.description) { + prompt.log(chalk.grey(`${settings.description}\n`)); + } + + prompt.log(`\n\n${format(results, true)}\n\n`); + + drawRemaining(maxLength); + + prompt + .delimiter(`❯ ${type}:`) + .show(); + }); +} diff --git a/@commitlint/prompt/src/library/get-prompt.test.js b/@commitlint/prompt/src/library/get-prompt.test.js new file mode 100644 index 0000000000..fab4ec110e --- /dev/null +++ b/@commitlint/prompt/src/library/get-prompt.test.js @@ -0,0 +1,84 @@ +import test from 'ava'; +import getPrompt from './get-prompt'; + +test('throws without params', t => { + t.throws(() => getPrompt(), /Missing prompter function/); +}); + +test('throws with incompatible prompter', t => { + t.throws(() => getPrompt('type', { + prompter() { + return {}; + } + }), /prompt.removeAllListeners/); +}); + +test('returns input unaltered wihtout rules', async t => { + const message = await getPrompt('type', { + prompter: stub('foobar') + }); + + t.is(message, 'foobar'); +}); + +function stub(input = '') { + return stubPrompter; + + function stubPrompter() { + const called = []; + const actions = []; + + const instance = { + action(...args) { + actions.push(args[0]); + called.push([instance.action, args]); + }, + addListener(...args) { + called.push([instance.addListener, args]); + }, + catch(...args) { + called.push([instance.catch, args]); + return instance; + }, + command(...args) { + called.push([instance.command, args]); + return instance; + }, + description(...args) { + called.push([instance.description, args]); + return instance; + }, + delimiter(...args) { + called.push([instance.delimiter, args]); + return instance; + }, + log(...args) { + called.push([instance.log, args]); + return instance; + }, + removeAllListeners(...args) { + called.push([instance.removeAllListeners, args]); + }, + show(...args) { + called.push([instance.show, args]); + return instance; + }, + ui: { + redraw: { + done(...args) { + called.push([instance.ui.redraw.done, args]); + } + } + }, + called + }; + + setTimeout(() => { + actions[0]({ + text: Array.isArray(input) ? input : [input] + }); + }); + + return instance; + } +} diff --git a/@commitlint/prompt/src/library/get-rule-name.js b/@commitlint/prompt/src/library/get-rule-name.js new file mode 100644 index 0000000000..d644bedaeb --- /dev/null +++ b/@commitlint/prompt/src/library/get-rule-name.js @@ -0,0 +1,11 @@ +/** + * Get name for a given rule id + * @param {string} id of the rule + * @return {[type]} name of the rule + */ +export default function getRuleName(id) { + const fragments = id.split('-'); + return fragments.length > 1 ? + fragments.slice(1).join('-') : + fragments[0]; +} diff --git a/@commitlint/prompt/src/library/meta.js b/@commitlint/prompt/src/library/meta.js new file mode 100644 index 0000000000..71c73ffe02 --- /dev/null +++ b/@commitlint/prompt/src/library/meta.js @@ -0,0 +1,19 @@ +import chalk from 'chalk'; +import {entries} from 'lodash'; + +/** + * Get formatted meta hints for configuration + * @param {object} settings dictionary to parse + * @return {string} formatted meta information + */ +export default function meta(settings) { + return chalk.grey(entries(settings) + .filter(item => item[1]) + .map(item => { + const [name, value] = item; + return typeof value === 'boolean' ? + `[${name}]` : + `[${name}=${value}]`; + }) + .join(' ')); +} diff --git a/@commitlint/prompt/src/library/rule-is-active.js b/@commitlint/prompt/src/library/rule-is-active.js new file mode 100644 index 0000000000..aae811891a --- /dev/null +++ b/@commitlint/prompt/src/library/rule-is-active.js @@ -0,0 +1,9 @@ +/** + * Check if a rule definition is active + * @param {object} rule to check + * @return {boolean} if the rule definition is active + */ +export default function ruleIsActive(rule) { + const [, [severity]] = rule; + return severity > 0; +} diff --git a/@commitlint/prompt/src/library/rule-is-applicable.js b/@commitlint/prompt/src/library/rule-is-applicable.js new file mode 100644 index 0000000000..ec94d60bcc --- /dev/null +++ b/@commitlint/prompt/src/library/rule-is-applicable.js @@ -0,0 +1,9 @@ +/** + * Check if a rule definition is applicable + * @param {object} rule to check + * @return {boolean} if the rule definition is appliable + */ +export default function ruleIsApplicable(rule) { + const [, [, applicable]] = rule; + return applicable === 'always'; +} diff --git a/@commitlint/prompt/src/settings.js b/@commitlint/prompt/src/settings.js new file mode 100644 index 0000000000..387d7f88fc --- /dev/null +++ b/@commitlint/prompt/src/settings.js @@ -0,0 +1,45 @@ +export default { + type: { + description: ' holds information about the goal of a change.', + enumerables: { + feat: { + description: 'Adds a new feature.' + }, + fix: { + description: 'Solves a bug.' + }, + docs: { + description: 'Adds or alters documentation.' + }, + style: { + description: 'Improves formatting, white-space.' + }, + refactor: { + description: 'Rewrites code without feature, performance or bug changes.' + }, + perf: { + description: 'Improves performance.' + }, + test: { + description: 'Adds or modifies tests.' + }, + chore: { + description: 'Change build process, tooling or dependencies.' + } + } + }, + scope: { + description: ' marks which sub-component of the project is affected' + }, + subject: { + description: ' is a short, high-level description of the change' + }, + body: { + description: ' holds additional information about the change', + multline: true + }, + footer: { + description: '