From 3aa908fdce308110f20a451270faaa5212c064d4 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 23 Jan 2026 22:42:14 +0000 Subject: [PATCH] Cleaning up Signed-off-by: Charles de Dreuille --- docs-ui/scripts/sync-changelog.mjs | 16 +- docs-ui/src/components/Changelog/index.tsx | 157 +-------------- docs-ui/src/components/Changelog/utils.tsx | 185 ++++++++++++++++++ .../components/ChangelogComponent/index.tsx | 38 ++-- 4 files changed, 218 insertions(+), 178 deletions(-) create mode 100644 docs-ui/src/components/Changelog/utils.tsx diff --git a/docs-ui/scripts/sync-changelog.mjs b/docs-ui/scripts/sync-changelog.mjs index 0c51a67f6b..414826a7e7 100644 --- a/docs-ui/scripts/sync-changelog.mjs +++ b/docs-ui/scripts/sync-changelog.mjs @@ -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'; } /** diff --git a/docs-ui/src/components/Changelog/index.tsx b/docs-ui/src/components/Changelog/index.tsx index 7f671adcb8..c1f3a5660b 100644 --- a/docs-ui/src/components/Changelog/index.tsx +++ b/docs-ui/src/components/Changelog/index.tsx @@ -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 ( - - {children} - - ); -}; - -const BreakingBadge = () => Breaking; +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); - - // 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 => `${toTitleCase(c)}`, - ) - .join(' ') + ' ' - : ''; - - // Add breaking badge if this is a breaking change - const breakingBadge = e.breaking ? ' ' : ''; - - // 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 ( { + 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 ( + + {children} + + ); +}; + +export const BreakingBadge = () => Breaking; + +// 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 => { + return entries.reduce((acc, entry) => { + if (!acc[entry.version]) { + acc[entry.version] = []; + } + acc[entry.version].push(entry); + return acc; + }, {} as Record); +}; + +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 => `${toTitleCase(c)}`) + .join(' ') + ' ' + : ''; + + // Add breaking badge if this is a breaking change + const breakingBadge = entry.breaking ? ' ' : ''; + + // 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; +}; diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx index ff86433268..92068d04f8 100644 --- a/docs-ui/src/components/ChangelogComponent/index.tsx +++ b/docs-ui/src/components/ChangelogComponent/index.tsx @@ -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 ( { - 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} /> ); };