UNPKG

@adinsure-ops/ops-cli

Version:

Operations CLI for working with AdInsure

206 lines (205 loc) 9.46 kB
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;