redhot
Version:
TypeScript Monorepo Management
231 lines (212 loc) • 7.25 kB
JavaScript
const wrap = require('word-wrap')
const longest = require('longest')
const rightPad = require('right-pad')
const temp = require('temp').track()
const fs = require('fs')
const editor = require('editor')
// This can be any kind of SystemJS compatible module.
// We use Commonjs here, but ES6 or AMD would do just
// fine.
module.exports = function (options) {
const types = options.types
const scopes = options.scopes
const length = longest(Object.keys(types)).length + 1
const choices = Object.keys(types).map(function (key) {
const type = types[key]
return {
name: rightPad(key + ':', length) + ' ' + type.description,
value: key
}
})
choices.push({ name: rightPad('WIP:', length) + 'A Work In Progress', value: 'WIP' })
return {
// When a user runs `git cz`, prompter will
// be executed. We pass you cz, which currently
// is just an instance of inquirer.js. Using
// this you can ask questions and get answers.
//
// The commit callback should be executed when
// you're ready to send back a commit template
// to git.
//
// By default, we'll de-indent your commit
// template and will keep empty lines.
prompter: function (cz, commit) {
console.log('\nLine 1 will be cropped at 100 characters. All other lines will be wrapped after 100 characters.\n')
// Let's ask some questions of the user
// so that we can populate our commit
// template.
//
// See inquirer.js docs for specifics.
// You can also opt to use another input
// collection library if you prefer.
cz.prompt([
{
type: 'list',
name: 'type',
message: 'Select the type of change that you\'re committing:',
choices: choices
}, {
type: 'list',
name: 'scope',
message: 'Select the package this change affects:\n',
choices: scopes.concat([
new cz.Separator(),
{ name: 'META - Overall monorepo', value: 'META' },
{ name: 'empty', value: false },
{ name: 'custom', value: 'custom' }
])
}, {
type: 'input',
name: 'scope',
message: 'Denote the SCOPE of this change:',
when: function (answers) {
return answers.scope === 'custom'
},
validate: function (value) {
return !!value
}
}, {
type: 'input',
name: 'subject',
message: 'Write a short, imperative tense description of the change:\n',
validate: function (value) {
return !!value
}
}, {
type: 'input',
name: 'body',
message: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n'
}, {
type: 'expand',
name: 'breakingChange',
choices: [
{ key: 'y', name: 'Yes', value: 'yes' },
{ key: 'n', name: 'No', value: 'no' }
],
message: function (answers) {
return 'Does this commit introduce BREAKING CHANGES?'
},
validate: function (value) {
return !!value
}
},
{
type: 'input',
name: 'before',
message: function (answers) {
return 'What did the API look like BEFORE this commit?'
},
when: function (answers) {
return answers.breakingChange === 'yes'
}
},
{
type: 'input',
name: 'after',
message: function (answers) {
return 'What does the API look like with this commit?'
},
when: function (answers) {
return answers.breakingChange === 'yes'
}
},
{
type: 'input',
name: 'footer',
message: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n',
when: answers => answers.type !== 'WIP'
},
{
type: 'expand',
name: 'confirmCommit',
choices: [
{ key: 'y', name: 'Yes', value: 'yes' },
{ key: 'n', name: 'Abort commit', value: 'no' },
{ key: 'e', name: 'Edit message', value: 'edit' }
],
message: function (answers) {
const SEP = '###--------------------------------------------------------###'
console.info('\n' + SEP + '\n' + buildCommit(answers) + '\n' + SEP + '\n')
return 'Are you sure you want to proceed with the commit above?'
},
validate: function (value) {
return !!value
}
}
]).then(function (answers) {
if (answers.confirmCommit === 'edit') {
temp.open(null, function (err, info) {
/* istanbul ignore else */
if (!err) {
fs.write(info.fd, buildCommit(answers))
fs.close(info.fd, function (err) {
if (err) { throw err }
editor(info.path, function (code, sig) {
if (code === 0) {
var commitStr = fs.readFileSync(info.path, { encoding: 'utf8' })
commit(commitStr)
} else {
console.info('Editor returned non zero value. Commit message was:\n' + buildCommit(answers))
}
})
})
}
})
} else if (answers.confirmCommit === 'yes') {
commit(buildCommit(answers))
} else {
console.info('Commit has been canceled.')
}
})
}
}
}
function buildCommit (answers) {
const maxLineWidth = 100
const wrapOptions = {
trim: true,
newline: '\n',
indent: '',
width: maxLineWidth
}
function addScope (scope) {
if (!scope) return ': ' // it could be type === WIP. So there is no scope
return '(' + scope.trim() + '): '
}
function addSubject (subject) {
return subject.trim()
}
function escapeSpecialChars (result) {
const specialChars = ['`']
specialChars.map(function (item) {
// For some strange reason, we have to pass additional '\' slash to commitizen. Total slashes are 4.
// If user types "feat: `string`", the commit preview should show "feat: `\\string\\`".
// Don't worry. The git log will be "feat: `string`"
result = result.replace(new RegExp(item, 'g'), '\\\\`')
})
return result
}
// Hard limit this line
const head = (answers.type + addScope(answers.scope) + addSubject(answers.subject)).slice(0, maxLineWidth)
// Wrap these lines at 100 characters
let body = wrap(answers.body, wrapOptions) || ''
body = body.split('|').join('\n')
let breaking = `BREAKING CHANGE:
Before: ${answers.before}
After: ${answers.after}
`
breaking = wrap(breaking, wrapOptions)
const footer = wrap(answers.footer, wrapOptions)
let result = head
if (body) {
result += '\n\n' + body
}
if (answers.breaking && answers.breaking === 'yes') {
result += '\n\n' + breaking
}
if (footer) {
result += '\n\nISSUES CLOSED: ' + footer
}
return escapeSpecialChars(result)
}