@adinsure-ops/ops-cli
Version:
Operations CLI for working with AdInsure
206 lines (205 loc) • 9.46 kB
JavaScript
import { __awaiter } from "tslib";
import { Flags, ux } from '@oclif/core';
import fs from 'fs-extra';
import matter from 'gray-matter';
import _ from 'lodash-es';
import moment from 'moment';
import path from 'path';
import { rimraf } from 'rimraf';
import CommandBase from '../../command.base.js';
class Generate extends CommandBase {
constructor() {
super(...arguments);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._entryTypes = [
{ types: ['BreakingChange', 'breaking'], text: 'Breaking Changes' },
{ types: ['Feature', 'feature'], text: 'New Features' },
{ types: ['Improvement', 'improve'], text: 'Improvements' },
{ types: ['Fixed', 'fixed'], text: 'Fixed' },
];
this._mdFileTypeRegex = /\.md$/i;
this._changelogTitle = '# Changelog';
}
readChangelogs(dir) {
return __awaiter(this, void 0, void 0, function* () {
const result = [];
const files = yield this.getFilesRecursive(dir, dir, this._mdFileTypeRegex);
for (const fileName of files) {
const filePath = path.resolve(dir, fileName);
let fileComponent = path.basename(path.dirname(filePath));
fileComponent = fileComponent === 'unreleased' ? '' : fileComponent;
const contents = matter.read(filePath);
result.push({
type: contents.data.type,
issue: contents.data.issue,
component: fileComponent,
content: contents.content.trim(),
});
}
return result;
});
}
generateLog(entries_1, components_1) {
return __awaiter(this, arguments, void 0, function* (entries, components, headingLevel = 3) {
var _a;
let content = '';
// First group by component, then re-sort the components to get the desired output ordering
const componentGroups = _.groupBy(entries, 'component');
for (const group of components) {
const groupEntries = (_a = componentGroups[group]) !== null && _a !== void 0 ? _a : [];
if (group !== '') {
content += `${'#'.repeat(headingLevel)} ${_.capitalize(group)}\r\n\r\n`;
}
if (groupEntries.length === 0) {
content += `No changes were introduced in this version.\r\n\r\n`;
continue;
}
const typeHeadingLevel = group === '' ? headingLevel : headingLevel + 1;
for (const type of this._entryTypes) {
const batch = groupEntries.filter(e => _.indexOf(type.types, e.type) > -1);
if (batch.length === 0) {
continue;
}
content += `${'#'.repeat(typeHeadingLevel)} ${type.text} (${batch.length} changes)\r\n\r\n`;
for (const log of batch) {
content += `${this.formatEntry(log)}\r\n\r\n`;
}
}
}
// We only want 1 new line at the end of this changelog
return content.trimEnd() + '\r\n';
});
}
generateChangelog(version_1, entries_1, components_1) {
return __awaiter(this, arguments, void 0, function* (version, entries, components, currentContents = '', date = moment().format('YYYY-MM-DD')) {
const currentChangelog = this.stripChangelogTitle(currentContents);
const generatedChangelog = `${this._changelogTitle}\r\n\r\n## ${version} (${date})\r\n\r\n${yield this.generateLog(entries, components)}`;
return currentChangelog ? `${generatedChangelog}\r\n${currentChangelog}\r\n` : generatedChangelog;
});
}
formatEntry(log) {
const normalizedContent = this.normalizeLineEndings(log.content).trim();
const lines = normalizedContent.length === 0 ? [] : normalizedContent.split('\n').map(line => line.trimEnd());
const issueLine = `- [${log.issue}](https://jira.adacta-fintech.com/browse/${log.issue}):`;
if (lines.length === 0) {
return issueLine;
}
if (this.startsWithBlockContent(lines[0])) {
return `${issueLine}\r\n${this.indentMarkdownLines(lines)}`;
}
const [firstLine, ...remainingLines] = lines;
if (remainingLines.length === 0) {
return `${issueLine} ${firstLine}`;
}
return `${issueLine} ${firstLine}\r\n${this.indentMarkdownLines(remainingLines)}`;
}
indentMarkdownLines(lines) {
return lines.map(line => line.length === 0 ? '' : ` ${line}`).join('\r\n');
}
stripChangelogTitle(currentContents) {
const trimmedContents = currentContents.trimEnd();
if (trimmedContents.length === 0) {
return '';
}
const lines = this.normalizeLineEndings(trimmedContents.trimStart()).split('\n');
if (lines[0].trim().toLowerCase() !== this._changelogTitle.toLowerCase()) {
return trimmedContents;
}
lines.shift();
while (lines.length > 0 && lines[0].trim().length === 0) {
lines.shift();
}
return lines.join('\r\n').trimEnd();
}
normalizeLineEndings(content) {
return content.replace(/\r\n?/g, '\n');
}
startsWithBlockContent(line) {
return /^([-*+]\s|\d+\.\s|```|~~~|\||#{1,6}\s)/.test(line.trimStart());
}
run() {
const _super = Object.create(null, {
run: { get: () => super.run }
});
return __awaiter(this, void 0, void 0, function* () {
var _a;
_super.run.call(this);
const { flags } = yield this.parse(Generate);
const root = path.resolve(process.cwd());
const changelogFolder = yield this.getChangelogsRoot();
const version = (_a = flags.version) !== null && _a !== void 0 ? _a : yield this.getVersion();
const components = flags.components.split(',');
this.log(`Inspecting ${changelogFolder} ...`);
ux.action.start(`generating for ${version}`, undefined, { stdout: true });
const changelogs = yield this.readChangelogs(changelogFolder);
if (!flags['ignore-no-changelogs'] && changelogs.length === 0) {
ux.action.stop('done, no changelogs found.');
return;
}
if (flags.preview) {
fs.writeFileSync(path.resolve(root, 'preview.md'), yield this.generateChangelog(version, changelogs, components), 'utf8');
ux.action.stop('generated preview.md');
return;
}
this.warn(`The following action will update ${flags.file} and remove all unreleased changelogs in '${changelogFolder}'`);
const confirm = flags.force || (yield ux.confirm(`Do you want to continue? [y/n/yes/no]`));
if (confirm) {
const changelogFile = path.resolve(root, flags.file);
const currentContents = fs.readFileSync(changelogFile, { encoding: 'utf8', flag: 'a+' });
fs.writeFileSync(changelogFile, yield this.generateChangelog(version, changelogs, components, currentContents), { encoding: 'utf8', flag: 'w' });
// delete unreleased changelogs
for (const group of components) {
const componentChangelogFolder = path.join(changelogFolder, group);
this.log(`Removing unreleased changelogs from ${componentChangelogFolder}`);
const changelogFiles = yield fs.readdir(componentChangelogFolder);
for (const file of changelogFiles) {
if (this._mdFileTypeRegex.test(file)) {
rimraf.sync(path.resolve(componentChangelogFolder, file));
}
}
}
}
ux.action.stop('done');
});
}
}
Generate.aliases = ['log:gen'];
Generate.description = 'generates final changelog from unreleased notes';
Generate.usage = 'log:gen [options]';
Generate.examples = [
`$ ops log:gen`,
`$ ops log:gen -p`,
`$ ops log:gen -c Platform`,
`$ ops log:gen -c Platform,Configuration`,
`$ ops log:gen -c Implementation -f CHANGELOG.Implementation.md`,
];
Generate.flags = {
components: Flags.string({
char: 'c',
description: 'components separated by comma (,) to process for final changelog',
default: '',
}),
file: Flags.string({
char: 'f',
description: 'file to update with new changelogs',
default: 'CHANGELOG.md',
}),
force: Flags.boolean({
description: 'update target file without prompting for confirmation',
default: false,
}),
preview: Flags.boolean({
char: 'p',
description: 'generates a preview markdown of current unreleased changelogs',
default: false,
}),
version: Flags.string({
char: 'v',
description: 'version for which you\'re generating changelogs',
}),
'ignore-no-changelogs': Flags.boolean({
description: 'generate final changelog even if no unreleased changelogs exist',
default: false,
}),
};
export default Generate;