Cleaning up

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-01-23 22:42:14 +00:00
parent 0e1c72576e
commit 3aa908fdce
4 changed files with 218 additions and 178 deletions
+12 -4
View File
@@ -379,6 +379,9 @@ async function parseListItem(
/**
* Infer change type from section and description
* Breaking change rules (semver):
* - version >= 1.0.0: Major changes are breaking
* - version < 1.0.0: Major and Minor changes are breaking
*/
function isBreakingChange(section, description, version) {
// Mark as breaking if explicitly mentioned in description
@@ -386,12 +389,17 @@ function isBreakingChange(section, description, version) {
return true;
}
// Or if it's in the Major or Minor Changes section
if (section === 'Major Changes' || section === 'Minor Changes') {
return true;
// Parse version to determine breaking change rules
const normalizedVersion = normalizeVersion(version);
const [major] = normalizedVersion.split('.').map(Number);
// Version >= 1.0.0: Only Major Changes are breaking
if (major >= 1) {
return section === 'Major Changes';
}
return false;
// Version < 1.0.0: Both Major and Minor Changes are breaking
return section === 'Major Changes' || section === 'Minor Changes';
}
/**
+3 -154
View File
@@ -1,164 +1,13 @@
import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
const Badge = ({
children,
variant = 'gray',
}: {
children: React.ReactNode;
variant?: 'red' | 'gray';
}) => {
const colors = {
red: {
backgroundColor: 'var(--badge-red-bg)',
color: 'var(--badge-red-color)',
},
gray: {
backgroundColor: 'var(--badge-gray-bg)',
color: 'var(--badge-gray-color)',
},
};
return (
<span
style={{
display: 'inline-block',
...colors[variant],
padding: '0.125rem 0.375rem',
borderRadius: '0.25rem',
fontSize: '0.75rem',
fontWeight: 600,
marginRight: '0.25rem',
lineHeight: 1.3,
}}
>
{children}
</span>
);
};
const BreakingBadge = () => <Badge variant="red">Breaking</Badge>;
import { Badge, BreakingBadge, generateChangelogMarkdown } from './utils';
export function Changelog() {
// Convert kebab-case to Title Case
const toTitleCase = (kebabCase: string) => {
return kebabCase
.split('-')
.map(word => word.charAt(0).toLocaleUpperCase('en-US') + word.slice(1))
.join(' ');
};
// Group changelog entries by version
const groupedChangelog = changelog.reduce((acc, entry) => {
if (!acc[entry.version]) {
acc[entry.version] = [];
}
acc[entry.version].push(entry);
return acc;
}, {} as Record<string, typeof changelog>);
// Sort versions in descending order (semantic versioning)
const sortedVersions = Object.keys(groupedChangelog).sort((a, b) => {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (bParts[i] !== aParts[i]) {
return bParts[i] - aParts[i]; // Descending order
}
}
return 0;
const content = generateChangelogMarkdown(changelog, {
showComponentBadges: true,
});
const content = sortedVersions
.map(version => {
const entries = groupedChangelog[version];
// Group entries: Breaking vs Everything Else
const breakingChanges = entries.filter(e => e.breaking);
const otherChanges = entries.filter(e => !e.breaking);
const sections = [];
// Breaking changes section
if (breakingChanges.length > 0) {
sections.push({
title: 'Breaking Changes',
entries: breakingChanges,
});
}
// All other changes section
if (otherChanges.length > 0) {
sections.push({
title: 'Changes',
entries: otherChanges,
});
}
const bumpSections = sections
.map(({ title, entries: bumpEntries }) => {
const sectionTitle = title;
const entriesMarkdown = bumpEntries
.map(e => {
const prs =
e.prs.length > 0
? e.prs
.map(
pr =>
`[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
)
.join(', ')
: '';
// Prepend component names as badges if available
const componentBadges =
e.components.length > 0
? e.components
.map(
c => `<Badge variant="gray">${toTitleCase(c)}</Badge>`,
)
.join(' ') + ' '
: '';
// Add breaking badge if this is a breaking change
const breakingBadge = e.breaking ? '<BreakingBadge /> ' : '';
// Remove **BREAKING**: text from description since we show it as a badge
const description = e.description.replace(
/\*\*BREAKING\*\*:?\s*/,
'',
);
// Description already has proper indentation from CHANGELOG
let entry = `- ${componentBadges}${breakingBadge}${description}`;
if (prs) {
entry += ` ${prs}`;
}
// Add migration if present (should already be in description, but check)
if (e.migration && !e.description.includes('**Migration:**')) {
entry += `\n\n Migration Guide:\n\n ${e.migration
.split('\n')
.join('\n ')}`;
}
return entry;
})
.join('\n\n');
return `### ${sectionTitle}\n\n${entriesMarkdown}`;
})
.join('\n\n');
return `## Version ${version}
${bumpSections}`;
})
.join('\n');
return (
<MDXRemote
components={{
+185
View File
@@ -0,0 +1,185 @@
import type { ChangelogProps } from '@/utils/types';
// Badge Components
export const Badge = ({
children,
variant = 'gray',
}: {
children: React.ReactNode;
variant?: 'red' | 'gray';
}) => {
const colors = {
red: {
backgroundColor: 'var(--badge-red-bg)',
color: 'var(--badge-red-color)',
},
gray: {
backgroundColor: 'var(--badge-gray-bg)',
color: 'var(--badge-gray-color)',
},
};
return (
<span
style={{
display: 'inline-block',
...colors[variant],
padding: '0.125rem 0.375rem',
borderRadius: '0.25rem',
fontSize: '0.75rem',
fontWeight: 600,
marginRight: '0.25rem',
lineHeight: 1.3,
}}
>
{children}
</span>
);
};
export const BreakingBadge = () => <Badge variant="red">Breaking</Badge>;
// Utility Functions
export const toTitleCase = (kebabCase: string): string => {
return kebabCase
.split('-')
.map(word => word.charAt(0).toLocaleUpperCase('en-US') + word.slice(1))
.join(' ');
};
export const groupByVersion = (
entries: ChangelogProps[],
): Record<string, ChangelogProps[]> => {
return entries.reduce((acc, entry) => {
if (!acc[entry.version]) {
acc[entry.version] = [];
}
acc[entry.version].push(entry);
return acc;
}, {} as Record<string, ChangelogProps[]>);
};
export const sortVersions = (versions: string[]): string[] => {
return versions.sort((a, b) => {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (bParts[i] !== aParts[i]) {
return bParts[i] - aParts[i]; // Descending order
}
}
return 0;
});
};
export const formatPRLinks = (prs: string[]): string => {
if (prs.length === 0) return '';
return prs
.map(pr => `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`)
.join(', ');
};
export const generateEntryMarkdown = (
entry: ChangelogProps,
options: { showComponentBadges?: boolean } = {},
): string => {
const { showComponentBadges = true } = options;
const prs = formatPRLinks(entry.prs);
// Prepend component names as badges if available and requested
const componentBadges =
showComponentBadges && entry.components.length > 0
? entry.components
.map(c => `<Badge variant="gray">${toTitleCase(c)}</Badge>`)
.join(' ') + ' '
: '';
// Add breaking badge if this is a breaking change
const breakingBadge = entry.breaking ? '<BreakingBadge /> ' : '';
// Remove **BREAKING**: text from description since we show it as a badge
const description = entry.description.replace(/\*\*BREAKING\*\*:?\s*/, '');
// Build the entry
let entryMarkdown = `- ${componentBadges}${breakingBadge}${description}`;
if (prs) {
entryMarkdown += ` ${prs}`;
}
// Add migration if present (should already be in description, but check)
if (entry.migration && !entry.description.includes('**Migration:**')) {
entryMarkdown += `\n\n Migration Guide:\n\n ${entry.migration
.split('\n')
.join('\n ')}`;
}
return entryMarkdown;
};
export interface GenerateChangelogOptions {
showComponentBadges?: boolean;
headingLevel?: number;
}
export const generateChangelogMarkdown = (
entries: ChangelogProps[],
options: GenerateChangelogOptions = {},
): string => {
const { showComponentBadges = true, headingLevel = 2 } = options;
// Group changelog entries by version
const groupedChangelog = groupByVersion(entries);
// Sort versions in descending order (semantic versioning)
const sortedVersions = sortVersions(Object.keys(groupedChangelog));
// Generate heading prefix based on level (e.g., "##" for level 2, "###" for level 3)
const versionHeading = '#'.repeat(headingLevel);
const sectionHeading = '#'.repeat(headingLevel + 1);
const content = sortedVersions
.map(version => {
const versionEntries = groupedChangelog[version];
// Group entries: Breaking vs Everything Else
const breakingChanges = versionEntries.filter(e => e.breaking);
const otherChanges = versionEntries.filter(e => !e.breaking);
const sections = [];
// Breaking changes section
if (breakingChanges.length > 0) {
sections.push({
title: 'Breaking Changes',
entries: breakingChanges,
});
}
// All other changes section
if (otherChanges.length > 0) {
sections.push({
title: 'Changes',
entries: otherChanges,
});
}
const bumpSections = sections
.map(({ title, entries: sectionEntries }) => {
const entriesMarkdown = sectionEntries
.map(e => generateEntryMarkdown(e, { showComponentBadges }))
.join('\n\n');
return `${sectionHeading} ${title}\n\n${entriesMarkdown}`;
})
.join('\n\n');
return `${versionHeading} Version ${version}
${bumpSections}`;
})
.join('\n');
return content;
};
@@ -2,34 +2,32 @@ import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
import type { Component } from '@/utils/changelog';
import {
Badge,
BreakingBadge,
generateChangelogMarkdown,
} from '../Changelog/utils';
export const ChangelogComponent = ({ component }: { component: Component }) => {
const componentChangelog = changelog.filter(c =>
c.components.includes(component),
);
const content = `## Changelog
${generateChangelogMarkdown(componentChangelog, {
showComponentBadges: false,
headingLevel: 3,
})}`;
return (
<MDXRemote
components={formattedMDXComponents}
source={`
## Changelog
${componentChangelog
?.map(change => {
const prs =
change.prs.length > 0
? change.prs
.map(
pr =>
`[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
)
.join(', ')
: '';
return `- \`${change.version}\` - ${change.description}${
prs ? ` ${prs}` : ''
}`;
})
.join('\n')}`}
components={{
...formattedMDXComponents,
Badge,
BreakingBadge,
}}
source={content}
/>
);
};