import type { AppChannel } from './app-version'; /** * Aenderungsliste fuer die Seite "Was ist neu" (quick-260916-dcz). * * Quelle ist CHANGELOG.md im Wurzelverzeichnis; `apps/web/next.config.ts` liest * die Datei zur Bauzeit und legt den Text als `env.TESSERA_CHANGELOG_MD` ab. * Dieses Modul darf NUR von der Server-Seite (`app/(portal)/changelog/page.tsx`) * importiert werden — sonst landet der Text in oeffentlich abrufbaren * Client-Chunks unter /_next/static. * * Kanalregel (`filterChangelogForChannel`): auf `live` fehlt der Abschnitt * "Unveröffentlicht" vollstaendig; auf `beta` und `dev` bleibt er und traegt * die Ueberschrift "Noch nicht freigegeben (Beta)" (uebersetzbar). Ein leerer * Abschnitt (ohne Listenpunkt) wird auf allen Kanaelen ausgeblendet. H1 und * Vorspann vor der ersten `## `-Ueberschrift entfallen — die Seite hat ihren * eigenen Titel. Reine Funktion ohne React/Next, deshalb direkt testbar. */ export type ChangelogChannel = AppChannel; export interface FilteredChangelog { markdown: string; hasUnreleased: boolean; } export const UNRELEASED_HEADING = 'Unveröffentlicht'; const DEFAULT_UNRELEASED_LABEL = 'Noch nicht freigegeben (Beta)'; /** * Wie in app-version.ts: Next.js ersetzt `process.env.` zur Bauzeit nur, * wenn der Ausdruck woertlich mit vollem Namen im Code steht — kein * Destructuring, kein `process.env[name]`. */ export const changelogMarkdown: string = process.env.TESSERA_CHANGELOG_MD ?? ''; const SECTION_RE = /^## /; const UNRELEASED_RE = new RegExp(`^## ${UNRELEASED_HEADING}\\s*$`); const LIST_ITEM_RE = /^\s*[-*] /; function splitSections(lines: string[]): string[][] { const sections: string[][] = []; let current: string[] | null = null; for (const line of lines) { if (SECTION_RE.test(line)) { current = [line]; sections.push(current); } else if (current) { current.push(line); } // Zeilen vor der ersten `## `-Ueberschrift (H1, Vorspann) werden verworfen. } return sections; } export function filterChangelogForChannel( markdown: string, channel: AppChannel, options?: { unreleasedHeading?: string }, ): FilteredChangelog { const normalized = markdown.replace(/\r\n?/g, '\n'); const sections = splitSections(normalized.split('\n')); let hasUnreleased = false; const kept: string[][] = []; for (const section of sections) { if (!UNRELEASED_RE.test(section[0])) { kept.push(section); continue; } const isEmpty = !section.slice(1).some((line) => LIST_ITEM_RE.test(line)); if (isEmpty || channel === 'live') { continue; } hasUnreleased = true; const label = options?.unreleasedHeading ?? DEFAULT_UNRELEASED_LABEL; kept.push([`## ${label}`, ...section.slice(1)]); } const joined = kept.map((section) => section.join('\n')).join('\n').trim(); return { markdown: joined ? `${joined}\n` : '', hasUnreleased }; }