feat(quick-260916-dcz): CHANGELOG.md, Seite "Was ist neu" mit Kanalfilter, Versionszeile als Link, Bauzeit-Einbettung
- CHANGELOG.md (Wurzel): Unveroeffentlicht (bwo + dyv + diese Seite, 8 Punkte) und 1.0.0 – 2026-09-15 (11 Punkte aus den Handbuechern), Alltagssprache, echte Umlaute - next.config.ts liest CHANGELOG.md zur Bauzeit nach env.TESSERA_CHANGELOG_MD; .dockerignore-Ausnahme !CHANGELOG.md und COPY-Zeile im Web-Dockerfile - lib/changelog.ts: filterChangelogForChannel (live ohne Unveroeffentlicht, beta/dev mit "Noch nicht freigegeben (Beta)", leerer Abschnitt ausgeblendet), 10 Tests - Server-Seite /changelog (MDEditor.Markdown + rehype-sanitize, Hinweis auf Beta, Leer-Text), 3 Tests; Versionszeile ist Link mit aria-label, 2 Tests - de/en: sidebar.whatsNew und Namensraum changelog; Web-Suite 49/309, tsc 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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.<NAME>` 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user