3h-version
Version:
A package version manager.
292 lines (255 loc) • 7.79 kB
JavaScript
#!/usr/bin/env node
const Version = require('./lib'),
path = require('path'),
fs = require('fs'),
readline = require('readline'),
Time = require('3h-time'),
CLI = require('3h-cli');
const { check, increase } = Version;
const defaultTimeFormat = 'YYYY-MM-DD',
defaultTabSize = '4',
defaultFile = 'package.json',
defaultEncoding = 'utf8',
defaultLogFile = 'CHANGELOG.md',
defaultHeadingGap = ' - ';
/**
* Pick the value from v1 if v1 isn't undefined, otherwise, return v2.
* @param {string[] | undefined} v1
* @param {string} v2
* @returns {string}
*/
function pick(v1, v2) {
return v1 && v1.length ? v1[0] : v2;
}
/**
* Print the error message and exit 1.
* @param {string} msg
* @returns {never}
*/
function logErrorAndExit(msg) {
console.error(msg);
process.exit(1);
}
/**
* Create the validation message.
* @param {string} prefix
* @param {string} curVer
* @param {boolean} valid
* @returns {string}
*/
function createValidationMsg(prefix, curVer, valid) {
return `${prefix} version("${curVer}") is ${valid ? 'valid' : 'invalid'}.`;
}
/**
* Stringify the object and write it to the file.
* @param {string} file
* @param {string} encoding
* @param {{ version: string }} object
* @param {number} tabSize
*/
function writeJson(file, encoding, object, tabSize) {
fs.writeFileSync(file, JSON.stringify(object, undefined, tabSize), encoding);
}
/**
* Log version change.
* @param {string} from
* @param {string} to
*/
function logVersionChange(from, to) {
console.log(`Update version: "${from}"->"${to}".`);
}
/**
* Executor
* @param {Map<string,string[]>} args
*/
const executor = args => {
if (args.has('h') || args.size === 0) {
cli.help();
}
const file = path.join(process.cwd(), pick(args.get('f'), defaultFile));
if (!fs.existsSync(file)) {
logErrorAndExit(`File not found: "${file}"!`);
}
const encoding = pick(args.get('e'), defaultEncoding),
content = fs.readFileSync(file, encoding),
object = JSON.parse(content);
if (!('version' in object)) {
logErrorAndExit('Version property not found!');
}
let curVer = object.version;
if (args.has('g')) {
console.log(`Current version is: "${curVer}".`);
}
const valid = check(curVer),
validationMsg = createValidationMsg('Current', curVer, valid);
if (args.has('c')) {
console.log(validationMsg);
}
const rawTabSize = pick(args.get('t'), defaultTabSize),
tabSize = Number(rawTabSize);
if (Number.isNaN(tabSize)) {
logErrorAndExit(`Invalid tab size: "${rawTabSize}"!`);
}
let tag = '';
if (args.has('p')) {
tag = pick(args.get('p'), 'beta');
if (!['alpha', 'beta', 'gamma'].includes(tag)) {
logErrorAndExit(`Invalid tag: "${tag}"!`);
}
tag = '-' + tag;
}
if (args.has('s')) {
const target = args.get('s');
if (!target || target.length === 0) {
logErrorAndExit('Target version missed!');
}
const targetVer = target[0] + tag;
if (!check(targetVer)) {
logErrorAndExit(createValidationMsg('Target', targetVer, false));
}
object.version = targetVer;
writeJson(file, encoding, object, tabSize);
logVersionChange(curVer, targetVer);
curVer = object.version;
}
if (!valid) {
logErrorAndExit(validationMsg);
}
const level = pick(args.get('i'), 'patch'),
headingLevel = Version.getHeadingLevel(level);
if (args.has('i')) {
if (headingLevel === -1) {
logErrorAndExit(`Invalid level: "${level}"!`);
}
let targetVer = increase(curVer, level) + tag;
object.version = targetVer;
writeJson(file, encoding, object, tabSize);
logVersionChange(curVer, targetVer);
curVer = object.version;
}
if (args.has('l')) {
const logs = args.get('l'),
logFile = pick(args.get('-log-file'), defaultLogFile),
timeFormat = pick(args.get('-time-format'), defaultTimeFormat),
headingGap = pick(args.get('-heading-gap'), defaultHeadingGap),
logFileExists = fs.existsSync(logFile);
if (logs.length > 0) {
log();
} else {
console.log(
'Please input the changelogs:\n' +
'( End with an empty line. )\n'
);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.setPrompt('- ');
rl.prompt();
rl.on('line', line => {
if (line.length > 0) {
logs.push(line);
rl.prompt();
} else {
readline.moveCursor(process.stdout, 2, -1);
console.log('(end)\n');
log();
rl.close();
}
});
}
function log() {
let newContent = '';
logs.reverse().forEach(log => {
newContent = '- ' + log + '\n' + newContent;
});
newContent = '#'.repeat(level === args.get('i') ? headingLevel : Version.getHeadingLevelFromVersion(curVer)) + ' ' +
curVer + headingGap + Time.get(timeFormat) +
'\n\n' + newContent;
fs.writeFileSync(logFile, newContent + (logFileExists ? '\n' : '') + (logFileExists ? fs.readFileSync(logFile, encoding) : ''), encoding);
console.log(`Changelogs are written into "${logFile}":\n` + newContent);
}
}
};
const cli = CLI.create({
name: '3h-version',
title: 'A package version manager.',
tabSize: 2,
nameSize: 16,
gapSize: 12,
lineGapSize: 1
}).arg({
name: 'h',
alias: ['-help'],
help: 'Show this help info.'
}).arg({
name: 'f',
alias: ['-file'],
val: 'file',
help: 'The file to operate on.\n' +
'Default: ' + defaultFile
}).arg({
name: 'e',
alias: ['-enc'],
val: 'encoding',
help: 'The encoding of the file.\n' +
'Default: ' + defaultEncoding
}).arg({
name: 'g',
alias: ['-get'],
help: 'Get current version.'
}).arg({
name: 's',
alias: ['-set'],
val: 'version',
help: 'Set the version.'
}).arg({
name: 'c',
alias: ['-chk'],
help: 'Check current version.'
}).arg({
name: 'i',
alias: ['-inc'],
val: 'level',
help: 'Increase the version by <level>,\n' +
'where <level> is "major",\n' +
'"minor" or "patch"(default).'
}).arg({
name: 'p',
alias: ['-pre'],
val: 'tag',
help: 'Pre-release tag. (If this arg\n' +
'appears, but the <tag> is not\n' +
'specified, then the <tag> will\n' +
'be "beta".)'
}).arg({
name: 't',
alias: ['-tab-size'],
val: 'size',
help: `The tab size. (Default: ${defaultTabSize})`
}).arg({
name: 'l',
alias: ['-log'],
val: 'logs',
help: 'Changelogs. (e.g. -l "..." "...")\n' +
'If this arg is followed by nothing,\n' +
'then changelogs will be read from\n' +
'the command line.'
}).arg({
name: '-log-file',
val: 'file',
help: 'The changelog file.\n' +
'Default: ' + defaultLogFile
}).arg({
name: '-time-format',
val: 'format',
help: 'Time format passed to `3h-time`.\n' +
'Default: ' + defaultTimeFormat
}).arg({
name: '-heading-gap',
val: 'gap',
help: `Heading gap. (Default: "${defaultHeadingGap}")`
}).on('extra', key => {
console.error(`Unknown arg "${key}"!`);
process.exit(1);
}).on('exec', executor).exec(process.argv);