nx
Version:
340 lines (339 loc) • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const semver_1 = require("semver");
const conventional_commits_1 = require("../../src/command-line/release/config/conventional-commits");
const github_1 = require("../../src/command-line/release/utils/github");
// axios types and values don't seem to match
const _axios = require("axios");
const axios = _axios;
class DefaultChangelogRenderer {
/**
* A ChangelogRenderer class takes in the determined changes and other relevant metadata
* and returns a string, or a Promise of a string of changelog contents (usually markdown).
*
* @param {Object} config The configuration object for the ChangelogRenderer
* @param {ChangelogChange[]} config.changes The collection of changes to show in the changelog
* @param {string} config.changelogEntryVersion The version for which we are rendering the current changelog entry
* @param {string | null} config.project The name of specific project to generate a changelog entry for, or `null` if the overall workspace changelog
* @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated
* @param {boolean} config.isVersionPlans Whether or not Nx release version plans are the source of truth for the changelog entry
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the change data
* @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository
* @param {NxReleaseConfig['conventionalCommits'] | null} config.conventionalCommitsConfig The configuration for conventional commits, or null if version plans are being used
*/
constructor(config) {
this.changes = this.filterChanges(config.changes, config.project);
this.changelogEntryVersion = config.changelogEntryVersion;
this.project = config.project;
this.entryWhenNoChanges = config.entryWhenNoChanges;
this.isVersionPlans = config.isVersionPlans;
this.changelogRenderOptions = config.changelogRenderOptions;
this.dependencyBumps = config.dependencyBumps;
this.repoData = config.repoData;
this.conventionalCommitsConfig = config.conventionalCommitsConfig;
this.relevantChanges = [];
this.breakingChanges = [];
this.additionalChangesForAuthorsSection = [];
}
filterChanges(changes, project) {
if (project === null) {
return changes;
}
return changes.filter((c) => c.affectedProjects &&
(c.affectedProjects === '*' || c.affectedProjects.includes(project)));
}
async render() {
const sections = [];
this.preprocessChanges();
if (this.shouldRenderEmptyEntry()) {
return this.renderEmptyEntry();
}
sections.push([this.renderVersionTitle()]);
const changesByType = this.renderChangesByType();
if (changesByType.length > 0) {
sections.push(changesByType);
}
if (this.hasBreakingChanges()) {
sections.push(this.renderBreakingChanges());
}
if (this.hasDependencyBumps()) {
sections.push(this.renderDependencyBumps());
}
if (this.shouldRenderAuthors()) {
sections.push(await this.renderAuthors());
}
// Join sections with double newlines, and trim any extra whitespace
return sections
.filter((section) => section.length > 0)
.map((section) => section.join('\n').trim())
.join('\n\n')
.trim();
}
preprocessChanges() {
this.relevantChanges = [...this.changes];
this.breakingChanges = [];
this.additionalChangesForAuthorsSection = [];
// Filter out reverted changes
for (let i = this.relevantChanges.length - 1; i >= 0; i--) {
const change = this.relevantChanges[i];
if (change.type === 'revert' && change.revertedHashes) {
for (const revertedHash of change.revertedHashes) {
const revertedCommitIndex = this.relevantChanges.findIndex((c) => c.shortHash && revertedHash.startsWith(c.shortHash));
if (revertedCommitIndex !== -1) {
this.relevantChanges.splice(revertedCommitIndex, 1);
this.relevantChanges.splice(i, 1);
i--;
break;
}
}
}
}
if (this.isVersionPlans) {
this.conventionalCommitsConfig = {
types: {
feat: conventional_commits_1.DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.feat,
fix: conventional_commits_1.DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.fix,
},
};
for (let i = this.relevantChanges.length - 1; i >= 0; i--) {
if (this.relevantChanges[i].isBreaking) {
const change = this.relevantChanges[i];
this.additionalChangesForAuthorsSection.push(change);
const line = this.formatChange(change);
this.breakingChanges.push(line);
this.relevantChanges.splice(i, 1);
}
}
}
else {
for (const change of this.relevantChanges) {
if (change.isBreaking) {
const breakingChangeExplanation = this.extractBreakingChangeExplanation(change.body);
this.breakingChanges.push(breakingChangeExplanation
? `- ${change.scope ? `**${change.scope.trim()}:** ` : ''}${breakingChangeExplanation}`
: this.formatChange(change));
}
}
}
}
shouldRenderEmptyEntry() {
return (this.relevantChanges.length === 0 &&
this.breakingChanges.length === 0 &&
!this.hasDependencyBumps());
}
renderEmptyEntry() {
if (this.hasDependencyBumps()) {
return [
this.renderVersionTitle(),
'',
...this.renderDependencyBumps(),
].join('\n');
}
else if (this.entryWhenNoChanges) {
return `${this.renderVersionTitle()}\n\n${this.entryWhenNoChanges}`;
}
return '';
}
renderVersionTitle() {
const isMajorVersion = `${(0, semver_1.major)(this.changelogEntryVersion)}.0.0` ===
this.changelogEntryVersion.replace(/^v/, '');
let maybeDateStr = '';
if (this.changelogRenderOptions.versionTitleDate) {
const dateStr = new Date().toISOString().slice(0, 10);
maybeDateStr = ` (${dateStr})`;
}
return isMajorVersion
? `# ${this.changelogEntryVersion}${maybeDateStr}`
: `## ${this.changelogEntryVersion}${maybeDateStr}`;
}
renderChangesByType() {
const markdownLines = [];
const typeGroups = this.groupChangesByType();
const changeTypes = this.conventionalCommitsConfig.types;
for (const type of Object.keys(changeTypes)) {
const group = typeGroups[type];
if (!group || group.length === 0) {
continue;
}
markdownLines.push('', `### ${changeTypes[type].changelog.title}`, '');
if (this.project === null) {
const changesGroupedByScope = this.groupChangesByScope(group);
const scopesSortedAlphabetically = Object.keys(changesGroupedByScope).sort();
for (const scope of scopesSortedAlphabetically) {
const changes = changesGroupedByScope[scope];
for (const change of changes.reverse()) {
const line = this.formatChange(change);
markdownLines.push(line);
if (change.isBreaking && !this.isVersionPlans) {
const breakingChangeExplanation = this.extractBreakingChangeExplanation(change.body);
this.breakingChanges.push(breakingChangeExplanation
? `- ${change.scope ? `**${change.scope.trim()}:** ` : ''}${breakingChangeExplanation}`
: line);
}
}
}
}
else {
// For project-specific changelogs, maintain the original order
for (const change of group) {
const line = this.formatChange(change);
markdownLines.push(line);
if (change.isBreaking && !this.isVersionPlans) {
const breakingChangeExplanation = this.extractBreakingChangeExplanation(change.body);
this.breakingChanges.push(breakingChangeExplanation
? `- ${change.scope ? `**${change.scope.trim()}:** ` : ''}${breakingChangeExplanation}`
: line);
}
}
}
}
return markdownLines;
}
hasBreakingChanges() {
return this.breakingChanges.length > 0;
}
renderBreakingChanges() {
const uniqueBreakingChanges = Array.from(new Set(this.breakingChanges));
return ['### ⚠️ Breaking Changes', '', ...uniqueBreakingChanges];
}
hasDependencyBumps() {
return this.dependencyBumps && this.dependencyBumps.length > 0;
}
renderDependencyBumps() {
const markdownLines = ['', '### 🧱 Updated Dependencies', ''];
this.dependencyBumps.forEach(({ dependencyName, newVersion }) => {
markdownLines.push(`- Updated ${dependencyName} to ${newVersion}`);
});
return markdownLines;
}
shouldRenderAuthors() {
return this.changelogRenderOptions.authors;
}
async renderAuthors() {
const markdownLines = [];
const _authors = new Map();
for (const change of [
...this.relevantChanges,
...this.additionalChangesForAuthorsSection,
]) {
if (!change.authors) {
continue;
}
for (const author of change.authors) {
const name = this.formatName(author.name);
if (!name || name.includes('[bot]')) {
continue;
}
if (_authors.has(name)) {
const entry = _authors.get(name);
entry.email.add(author.email);
}
else {
_authors.set(name, { email: new Set([author.email]) });
}
}
}
if (this.repoData &&
this.changelogRenderOptions.mapAuthorsToGitHubUsernames) {
await Promise.all([..._authors.keys()].map(async (authorName) => {
const meta = _authors.get(authorName);
for (const email of meta.email) {
if (email.endsWith('@users.noreply.github.com')) {
const match = email.match(/^(\d+\+)?([^@]+) \.noreply\.github\.com$/);
if (match && match[2]) {
meta.github = match[2];
break;
}
}
const { data } = await axios
.get(`https://ungh.cc/users/find/${email}`)
.catch(() => ({ data: { user: null } }));
if (data?.user) {
meta.github = data.user.username;
break;
}
}
}));
}
const authors = [..._authors.entries()].map((e) => ({
name: e[0],
...e[1],
}));
if (authors.length > 0) {
markdownLines.push('', '### ' + '❤️ Thank You', '', ...authors
.sort((a, b) => a.name.localeCompare(b.name))
.map((i) => {
const github = i.github ? ` @${i.github}` : '';
return `- ${i.name}${github}`;
}));
}
return markdownLines;
}
formatChange(change) {
let description = change.description;
let extraLines = [];
let extraLinesStr = '';
if (description.includes('\n')) {
[description, ...extraLines] = description.split('\n');
const indentation = ' ';
extraLinesStr = extraLines
.filter((l) => l.trim().length > 0)
.map((l) => `${indentation}${l}`)
.join('\n');
}
let changeLine = '- ' +
(!this.isVersionPlans && change.isBreaking ? '⚠️ ' : '') +
(!this.isVersionPlans && change.scope
? `**${change.scope.trim()}:** `
: '') +
description;
if (this.repoData && this.changelogRenderOptions.commitReferences) {
changeLine += (0, github_1.formatReferences)(change.githubReferences, this.repoData);
}
if (extraLinesStr) {
changeLine += '\n\n' + extraLinesStr;
}
return changeLine;
}
groupChangesByType() {
const typeGroups = {};
for (const change of this.relevantChanges) {
typeGroups[change.type] = typeGroups[change.type] || [];
typeGroups[change.type].push(change);
}
return typeGroups;
}
groupChangesByScope(changes) {
const scopeGroups = {};
for (const change of changes) {
const scope = change.scope || '';
scopeGroups[scope] = scopeGroups[scope] || [];
scopeGroups[scope].push(change);
}
return scopeGroups;
}
extractBreakingChangeExplanation(message) {
if (!message) {
return null;
}
const breakingChangeIdentifier = 'BREAKING CHANGE:';
const startIndex = message.indexOf(breakingChangeIdentifier);
if (startIndex === -1) {
return null;
}
const startOfBreakingChange = startIndex + breakingChangeIdentifier.length;
const endOfBreakingChange = message.indexOf('\n', startOfBreakingChange);
if (endOfBreakingChange === -1) {
return message.substring(startOfBreakingChange).trim();
}
return message.substring(startOfBreakingChange, endOfBreakingChange).trim();
}
formatName(name = '') {
return name
.split(' ')
.map((p) => p.trim())
.join(' ');
}
}
exports.default = DefaultChangelogRenderer;