/** * @copyright 2017-present, Charlike Mike Reagent * @license Apache-2.0 */ import arrayify from 'arrayify'; import collectMentions from 'collect-mentions'; export type CommitHeader = { type: string; scope?: string; subject: string; }; export type Commit = { header: CommitHeader; body?: string | null; footer?: string | null; increment?: string; isBreaking?: boolean; mentions?: Array; }; export type Plugin = (commit: Commit) => Commit; /** * * @param {string} commitMessage required, a whole commit message * @param {Array} plugins optional, a list of functions that get passed with `commit` object */ function parse(commitMessage: string, plugins: Array): Commit { const lines = commitMessage.split('\n'); const header = lines.shift() || ''; const body = lines.shift() || null; const footer = lines.join('\n').trim() || null; const parts = /^(\w+)(?:\((.+)\))?: (.+)$/.exec(header); if (!parts) { throw new Error('invalid message: [optional scope]: '); } const [type, scope, subject] = parts.slice(1); const commit: Commit = { header: { type, scope, subject }, body, footer }; return arrayify(plugins).reduce((acc: Commit, fn: Plugin): Commit => { const result = fn(Object.assign({}, acc)); return Object.assign({}, acc, result); }, commit); } /** * Mappers (plugins) for the `parseCommitMessage` function, * they get a `commit` object and should return an object. * It isn't needed to return the whole coming `commit` object, because * the return of plugins is merged with it automatically in any way. */ /** * * @param {Commit} commit, the commit object coming from `parseCommitMessage` */ function incrementMapper(commit: Commit) { const isBreaking = isBreakingChange(commit); let increment = null; if (/fix|bugfix|patch/.test(commit.header.type)) { increment = 'patch'; } if (/feat|feature|minor/.test(commit.header.type)) { increment = 'minor'; } if (/break|breaking|major/.test(commit.header.type) || isBreaking) { increment = 'major'; } return { increment, isBreaking }; } /* eslint-disable no-param-reassign */ /** * * @param {Object} commit, the commit object coming from `parseCommitMessage` */ function isBreakingChange({ header, body, footer }: Commit) { body = body || ''; footer = footer || ''; const re = /^BREAKING(?: CHANGES?)?:/; return re.test(header.subject) || re.test(body) || re.test(footer); } /** * > Collects all mentions for `subject`, `body` and `footer` * places of the commit message, into one single array of "mention" objects * like `{ mention, handle }`, see [collect-mentions][] package. * * @param {Commit} commit, the commit object coming from `parseCommitMessage` * @returns {Object<{ mentions: Array }>} with a `mentions` array property */ function mentionsMapper({ header, body, footer }: Commit) { const mentions = [] .concat(collectMentions(header.subject)) .concat(collectMentions(body)) .concat(collectMentions(footer)); return { mentions }; } /** * An object with all mappers, such as `plugins` array, but named. */ const mappers: { [key: string]: any } = { increment: incrementMapper, mentions: mentionsMapper }; /** * A list of all plugins, such as `mappers` but no names. */ const plugins: Array = Object.keys(mappers).map( (name) => mappers[name] ); /** * Expose everything */ export { mappers, plugins, parse };