import type { Metadata } from "next";
import fs from "fs";
import path from "path";
export const metadata: Metadata = {
title: "Changelog",
description:
"cmux release notes and version history. New features, bug fixes, and changes for the native macOS terminal.",
};
interface ChangelogSection {
heading: string;
items: string[];
}
interface ChangelogVersion {
version: string;
date: string;
intro?: string;
sections: ChangelogSection[];
}
function parseChangelog(markdown: string): ChangelogVersion[] {
const versions: ChangelogVersion[] = [];
let current: ChangelogVersion | null = null;
let currentSection: ChangelogSection | null = null;
for (const line of markdown.split("\n")) {
const versionMatch = line.match(/^## \[(.+?)\] - (.+)$/);
if (versionMatch) {
if (current) versions.push(current);
current = {
version: versionMatch[1],
date: versionMatch[2],
sections: [],
};
currentSection = null;
continue;
}
if (!current) continue;
const sectionMatch = line.match(/^### (.+)$/);
if (sectionMatch) {
currentSection = { heading: sectionMatch[1], items: [] };
current.sections.push(currentSection);
continue;
}
const itemMatch = line.match(/^- (.+)$/);
if (itemMatch) {
if (currentSection) {
currentSection.items.push(itemMatch[1]);
} else {
// Items without a ### heading (e.g. 1.0.x initial release)
if (!current.sections.length) {
currentSection = { heading: "", items: [] };
current.sections.push(currentSection);
}
current.sections[current.sections.length - 1].items.push(
itemMatch[1]
);
}
continue;
}
// Non-empty lines that aren't headings or items (intro text)
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
current.intro = trimmed;
}
}
if (current) versions.push(current);
return versions;
}
function InlineMarkdown({ text }: { text: string }) {
const parts = text.split(/(`[^`]+`|\[[^\]]+\]\([^)]+\))/g);
return (
<>
{parts.map((part, i) => {
if (part.startsWith("`") && part.endsWith("`")) {
return {part.slice(1, -1)};
}
const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
if (linkMatch) {
return (
{linkMatch[1]}
);
}
return {part};
})}
>
);
}
export default function ChangelogPage() {
const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md");
const markdown = fs.readFileSync(changelogPath, "utf-8");
const versions = parseChangelog(markdown);
return (
<>
All notable changes to cmux are documented here.
{versions.map((v) => ({v.intro}
} {v.sections.map((section, i) => (