updated generate-changelog-page.ts
This commit is contained in:
1 file changed
+283
-47
@@ -10,12 +10,15 @@ import { join } from 'path';
|
|||||||
const ROOT_DIR = join(import.meta.dir, '..');
|
const ROOT_DIR = join(import.meta.dir, '..');
|
||||||
const CHANGELOG_PATH = join(ROOT_DIR, 'src/lib/data/changelog.json');
|
const CHANGELOG_PATH = join(ROOT_DIR, 'src/lib/data/changelog.json');
|
||||||
const INDEX_PATH = join(ROOT_DIR, 'webpage/index.html');
|
const INDEX_PATH = join(ROOT_DIR, 'webpage/index.html');
|
||||||
|
const RSS_PATH = join(ROOT_DIR, 'webpage/changelog.xml');
|
||||||
|
const SITE_URL = 'https://dockhand.pro';
|
||||||
|
|
||||||
interface ChangelogEntry {
|
interface ChangelogEntry {
|
||||||
version: string;
|
version: string;
|
||||||
date: string;
|
date: string;
|
||||||
changes: Array<{ type: 'feature' | 'fix'; text: string }>;
|
changes: Array<{ type: 'feature' | 'fix'; text: string }>;
|
||||||
imageTag: string;
|
imageTag: string;
|
||||||
|
comingSoon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG icons for change types
|
// SVG icons for change types
|
||||||
@@ -25,10 +28,26 @@ const FIX_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stro
|
|||||||
|
|
||||||
const TOGGLE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;
|
const TOGGLE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;
|
||||||
|
|
||||||
|
// Lucide rss icon for the "subscribe to release notes" link.
|
||||||
|
const RSS_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg>`;
|
||||||
|
|
||||||
const COPY_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
const COPY_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
||||||
|
|
||||||
function formatDate(dateStr: string): string {
|
const GITHUB_SVG = `<svg viewBox="0 0 24 24" fill="currentColor" style="width:10px;height:10px"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`;
|
||||||
|
|
||||||
|
const EXTLINK_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:10px;height:10px;vertical-align:middle"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
|
||||||
|
|
||||||
|
// Lucide git-pull-request-arrow — inline before PR refs so they're visually distinct from issues.
|
||||||
|
const PR_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:10px;height:10px;vertical-align:-1px;margin-right:1px"><circle cx="5" cy="6" r="3"/><path d="M5 9v12"/><path d="m15 9-3-3 3-3"/><path d="M12 6h5a3 3 0 0 1 3 3v10"/></svg>`;
|
||||||
|
|
||||||
|
function formatDate(dateStr: string, comingSoon?: boolean): string {
|
||||||
|
if (!dateStr || comingSoon) {
|
||||||
|
return 'Coming soon';
|
||||||
|
}
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return 'Coming soon';
|
||||||
|
}
|
||||||
return date.toLocaleDateString('en-US', {
|
return date.toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@@ -36,22 +55,117 @@ function formatDate(dateStr: string): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortChanges(changes: Array<{ type: 'feature' | 'fix'; text: string }>): Array<{ type: 'feature' | 'fix'; text: string }> {
|
||||||
|
return [...changes].sort((a, b) => {
|
||||||
|
if (a.type === b.type) return 0;
|
||||||
|
return a.type === 'feature' ? -1 : 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s: string): string {
|
||||||
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkifyIssues(text: string): string {
|
||||||
|
// Tokenize PR#N, #N, and @username. Collect each contiguous group of refs
|
||||||
|
// (separated only by whitespace, commas, or parens) into a single pill cluster
|
||||||
|
// prefixed by the GitHub octocat icon.
|
||||||
|
const PATTERN = /PR#(\d+)|#(\d+)|@([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}))/g;
|
||||||
|
|
||||||
|
type RefLink = { html: string };
|
||||||
|
const matches: Array<{ start: number; end: number; ref: RefLink }> = [];
|
||||||
|
|
||||||
|
for (const m of text.matchAll(PATTERN)) {
|
||||||
|
const start = m.index ?? 0;
|
||||||
|
const end = start + m[0].length;
|
||||||
|
let html: string;
|
||||||
|
if (m[1]) {
|
||||||
|
html = `<a href="https://github.com/Finsys/dockhand/pull/${m[1]}" target="_blank" rel="noopener noreferrer" class="changelog-issue-link" title="Pull request #${m[1]}">${PR_SVG}#${m[1]}</a>`;
|
||||||
|
} else if (m[2]) {
|
||||||
|
html = `<a href="https://github.com/Finsys/dockhand/issues/${m[2]}" target="_blank" rel="noopener noreferrer" class="changelog-issue-link" title="Issue #${m[2]}">#${m[2]}</a>`;
|
||||||
|
} else {
|
||||||
|
const name = m[3]!;
|
||||||
|
html = `<a href="https://github.com/${name}" target="_blank" rel="noopener noreferrer" class="changelog-issue-link" title="@${name} on GitHub">@${name}</a>`;
|
||||||
|
}
|
||||||
|
matches.push({ start, end, ref: { html } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) return escapeHtml(text);
|
||||||
|
|
||||||
|
// Group adjacent matches whose gap contains only [\s,()]
|
||||||
|
const GLUE_RE = /^[\s,()]*$/;
|
||||||
|
const groups: Array<{ start: number; end: number; refs: RefLink[] }> = [];
|
||||||
|
let current = { start: matches[0].start, end: matches[0].end, refs: [matches[0].ref] };
|
||||||
|
for (let i = 1; i < matches.length; i++) {
|
||||||
|
const gap = text.slice(current.end, matches[i].start);
|
||||||
|
if (GLUE_RE.test(gap)) {
|
||||||
|
current.refs.push(matches[i].ref);
|
||||||
|
current.end = matches[i].end;
|
||||||
|
} else {
|
||||||
|
groups.push(current);
|
||||||
|
current = { start: matches[i].start, end: matches[i].end, refs: [matches[i].ref] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups.push(current);
|
||||||
|
|
||||||
|
// Expand each group's bounds to swallow surrounding glue (parens, commas, spaces)
|
||||||
|
// so we don't end up with "foo · #123" or "foo (#123)" leftovers.
|
||||||
|
let out = '';
|
||||||
|
let lastIndex = 0;
|
||||||
|
for (const g of groups) {
|
||||||
|
let s = g.start;
|
||||||
|
while (s > lastIndex && /[\s(]/.test(text[s - 1])) s--;
|
||||||
|
let e = g.end;
|
||||||
|
while (e < text.length && /[\s),]/.test(text[e])) e++;
|
||||||
|
out += escapeHtml(text.slice(lastIndex, s));
|
||||||
|
out += ` <span class="changelog-issues">${GITHUB_SVG}${g.refs.map(r => r.html).join(' · ')}</span>`;
|
||||||
|
lastIndex = e;
|
||||||
|
}
|
||||||
|
out += escapeHtml(text.slice(lastIndex));
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function generateChangeItem(change: { type: 'feature' | 'fix'; text: string }): string {
|
function generateChangeItem(change: { type: 'feature' | 'fix'; text: string }): string {
|
||||||
const pillClass = change.type === 'feature' ? 'changelog-pill-feature' : 'changelog-pill-fix';
|
const pillClass = change.type === 'feature' ? 'changelog-pill-feature' : 'changelog-pill-fix';
|
||||||
const svg = change.type === 'feature' ? FEATURE_SVG : FIX_SVG;
|
const svg = change.type === 'feature' ? FEATURE_SVG : FIX_SVG;
|
||||||
const label = change.type === 'feature' ? 'New' : 'Fix';
|
const label = change.type === 'feature' ? 'New' : 'Fix';
|
||||||
return ` <li><span class="changelog-pill ${pillClass}">${svg}${label}</span>${change.text}</li>`;
|
return ` <li><span class="changelog-pill ${pillClass}">${svg}${label}</span><span class="changelog-text">${linkifyIssues(change.text)}</span></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function versionLink(version: string, comingSoon?: boolean): string {
|
||||||
|
if (comingSoon) return version;
|
||||||
|
return `<a href="https://github.com/Finsys/dockhand/releases/tag/${version}" target="_blank" rel="noopener noreferrer" class="changelog-version-link">${version} ${GITHUB_SVG}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateComingSoonEntry(entry: ChangelogEntry): string {
|
||||||
|
const changes = sortChanges(entry.changes).map(generateChangeItem).join('\n');
|
||||||
|
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
||||||
|
|
||||||
|
return ` <!-- ${version} (coming soon) -->
|
||||||
|
<div class="changelog-entry">
|
||||||
|
<div class="changelog-header">
|
||||||
|
<div class="changelog-version">
|
||||||
|
<h3>${versionLink(version, true)}</h3>
|
||||||
|
<span class="changelog-badge" style="background: linear-gradient(135deg, #f59e0b, #d97706); animation: pulseGlowAmber 2s ease-in-out infinite;">Coming soon</span>
|
||||||
|
</div>
|
||||||
|
<span class="changelog-date">${formatDate(entry.date, true)}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="changelog-changes">
|
||||||
|
${changes}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateLatestEntry(entry: ChangelogEntry): string {
|
function generateLatestEntry(entry: ChangelogEntry): string {
|
||||||
const changes = entry.changes.map(generateChangeItem).join('\n');
|
const changes = sortChanges(entry.changes).map(generateChangeItem).join('\n');
|
||||||
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
||||||
|
|
||||||
return ` <!-- ${version} -->
|
return ` <!-- ${version} -->
|
||||||
<div class="changelog-entry">
|
<div class="changelog-entry">
|
||||||
<div class="changelog-header">
|
<div class="changelog-header">
|
||||||
<div class="changelog-version">
|
<div class="changelog-version">
|
||||||
<h3>${version}</h3>
|
<h3>${versionLink(version)}</h3>
|
||||||
<span class="changelog-badge">Latest</span>
|
<span class="changelog-badge">Latest</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="changelog-date">${formatDate(entry.date)}</span>
|
<span class="changelog-date">${formatDate(entry.date)}</span>
|
||||||
@@ -71,14 +185,14 @@ ${changes}
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateCollapsibleEntry(entry: ChangelogEntry): string {
|
function generateCollapsibleEntry(entry: ChangelogEntry): string {
|
||||||
const changes = entry.changes.map(generateChangeItem).join('\n');
|
const changes = sortChanges(entry.changes).map(generateChangeItem).join('\n');
|
||||||
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
||||||
|
|
||||||
return ` <!-- ${version} (collapsible) -->
|
return ` <!-- ${version} (collapsible) -->
|
||||||
<div class="changelog-entry collapsible" data-version="${version}">
|
<div class="changelog-entry collapsible" data-version="${version}">
|
||||||
<div class="changelog-header">
|
<div class="changelog-header">
|
||||||
<div class="changelog-version">
|
<div class="changelog-version">
|
||||||
<h3>${version}</h3>
|
<h3>${versionLink(version)}</h3>
|
||||||
<span class="changelog-toggle">${TOGGLE_SVG}</span>
|
<span class="changelog-toggle">${TOGGLE_SVG}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="changelog-date">${formatDate(entry.date)}</span>
|
<span class="changelog-date">${formatDate(entry.date)}</span>
|
||||||
@@ -96,69 +210,191 @@ ${changes}
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateRecentEntry(entry: ChangelogEntry): string {
|
||||||
|
const changes = sortChanges(entry.changes).map(generateChangeItem).join('\n');
|
||||||
|
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
||||||
|
|
||||||
|
return ` <!-- ${version} -->
|
||||||
|
<div class="changelog-entry">
|
||||||
|
<div class="changelog-header">
|
||||||
|
<div class="changelog-version">
|
||||||
|
<h3>${versionLink(version)}</h3>
|
||||||
|
</div>
|
||||||
|
<span class="changelog-date">${formatDate(entry.date)}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="changelog-changes">
|
||||||
|
${changes}
|
||||||
|
</ul>
|
||||||
|
<div class="changelog-image-tag">
|
||||||
|
<span>Docker image:</span>
|
||||||
|
<code>${entry.imageTag}</code>
|
||||||
|
<button class="copy-btn" onclick="copyDockerImage(this, '${entry.imageTag}')" title="Copy to clipboard">${COPY_SVG}</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ARCHIVE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21 8-2 2-1.5-3.7A2 2 0 0 0 15.64 5H8.36a2 2 0 0 0-1.86 1.3L5 10l-2-2"/><path d="M3.1 11.2A2 2 0 0 0 3 12v6c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-6a2 2 0 0 0-.1-.8"/><path d="M10 14h4"/></svg>`;
|
||||||
|
|
||||||
function generateChangelogSection(entries: ChangelogEntry[]): string {
|
function generateChangelogSection(entries: ChangelogEntry[]): string {
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const [latest, ...rest] = entries;
|
// Separate coming soon entries from released entries
|
||||||
const latestHtml = generateLatestEntry(latest);
|
const comingSoonEntries = entries.filter(e => e.comingSoon);
|
||||||
const restHtml = rest.map(generateCollapsibleEntry).join('\n');
|
const releasedEntries = entries.filter(e => !e.comingSoon);
|
||||||
|
|
||||||
|
// Latest 3 released versions shown expanded
|
||||||
|
const RECENT_COUNT = 3;
|
||||||
|
const latestReleased = releasedEntries[0];
|
||||||
|
const recentEntries = releasedEntries.slice(1, RECENT_COUNT);
|
||||||
|
const olderEntries = releasedEntries.slice(RECENT_COUNT);
|
||||||
|
|
||||||
|
// Generate HTML for each section
|
||||||
|
const comingSoonHtml = comingSoonEntries.map(generateComingSoonEntry).join('\n');
|
||||||
|
const latestHtml = latestReleased ? generateLatestEntry(latestReleased) : '';
|
||||||
|
const recentHtml = recentEntries.map(generateRecentEntry).join('\n');
|
||||||
|
const olderHtml = olderEntries.map(generateCollapsibleEntry).join('\n');
|
||||||
|
|
||||||
|
// Build older releases wrapper if there are older entries
|
||||||
|
let olderSection = '';
|
||||||
|
if (olderEntries.length > 0) {
|
||||||
|
const firstOlder = olderEntries[0].version.startsWith('v') ? olderEntries[0].version : `v${olderEntries[0].version}`;
|
||||||
|
const lastOlder = olderEntries[olderEntries.length - 1].version.startsWith('v') ? olderEntries[olderEntries.length - 1].version : `v${olderEntries[olderEntries.length - 1].version}`;
|
||||||
|
olderSection = `
|
||||||
|
<!-- Older Releases -->
|
||||||
|
<div class="older-releases-wrapper" id="older-releases">
|
||||||
|
<div class="older-releases-header" onclick="toggleOlderReleases()">
|
||||||
|
<h4>${ARCHIVE_SVG}Older releases (${firstOlder} – ${lastOlder})</h4>
|
||||||
|
<svg class="older-releases-toggle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="older-releases-content">
|
||||||
|
${olderHtml}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
return ` <!-- Changelog Section -->
|
return ` <!-- Changelog Section -->
|
||||||
<section class="changelog" id="changelog">
|
<section class="changelog" id="changelog">
|
||||||
<div class="changelog-container">
|
<div class="changelog-container">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="section-label">Changelog</div>
|
<div class="section-label">Changelog</div>
|
||||||
<h2 class="section-title">Release history</h2>
|
<div class="changelog-title-row">
|
||||||
|
<h2 class="section-title">Release history</h2>
|
||||||
|
<a class="changelog-rss-link" href="/changelog.xml" title="Subscribe to release notes (RSS)">${RSS_SVG}<span>RSS</span></a>
|
||||||
|
</div>
|
||||||
<p class="section-subtitle">Track our progress and see what's new in each version. <span style="color: #fbbf24; white-space: nowrap;">Spoiler: it gets better every time.</span></p>
|
<p class="section-subtitle">Track our progress and see what's new in each version. <span style="color: #fbbf24; white-space: nowrap;">Spoiler: it gets better every time.</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="changelog-list">
|
<div class="changelog-list">
|
||||||
|
${comingSoonHtml}
|
||||||
${latestHtml}
|
${latestHtml}
|
||||||
${restHtml}
|
${recentHtml}${olderSection}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read changelog.json
|
// Strip ref tokens (#N, PR#N, @user) from changelog text for the plain-text RSS feed.
|
||||||
console.log('Reading changelog from:', CHANGELOG_PATH);
|
// A trailing "(#1313, PR#1339, @user)" cluster is removed whole; bare refs mid-text too.
|
||||||
const changelog: ChangelogEntry[] = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8'));
|
export function stripRefs(text: string): string {
|
||||||
console.log(`Found ${changelog.length} changelog entries`);
|
const ref = '(?:PR)?#\\d+|@[\\w-]+';
|
||||||
|
return text
|
||||||
// Read index.html
|
// Whole parenthesised ref cluster: "(#1, PR#2, @u)" -> ""
|
||||||
console.log('Reading index.html from:', INDEX_PATH);
|
.replace(new RegExp(`\\s*\\((?:\\s*(?:${ref})\\s*[,\\s]*)+\\)`, 'g'), '')
|
||||||
let indexHtml = readFileSync(INDEX_PATH, 'utf-8');
|
// Any leftover bare refs
|
||||||
|
.replace(new RegExp(ref, 'g'), '')
|
||||||
// Generate new changelog section
|
// Tidy ONLY separators orphaned by a removed ref (e.g. "a , , b" -> "a b"),
|
||||||
const newChangelogSection = generateChangelogSection(changelog);
|
// not legitimate prose punctuation like "(./config, ./data)".
|
||||||
|
.replace(/\s+,(\s|$)/g, '$1')
|
||||||
// Replace changelog section using regex
|
.replace(/\s{2,}/g, ' ')
|
||||||
// Match from "<!-- Changelog Section -->" to the closing "</section>" before "<!-- CTA -->"
|
.replace(/[,;]\s*$/g, '')
|
||||||
const changelogRegex = / <!-- Changelog Section -->[\s\S]*?<\/section>(?=\s*\n\s*<!-- CTA -->)/;
|
.trim();
|
||||||
|
|
||||||
if (!changelogRegex.test(indexHtml)) {
|
|
||||||
console.error('ERROR: Could not find changelog section in index.html');
|
|
||||||
console.error('Looking for pattern: <!-- Changelog Section --> ... </section> followed by <!-- CTA -->');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
indexHtml = indexHtml.replace(changelogRegex, newChangelogSection);
|
// Build an RSS 2.0 feed from the released changelog entries (coming-soon excluded).
|
||||||
|
function generateRssFeed(entries: ChangelogEntry[]): string {
|
||||||
|
const released = entries.filter((e) => !e.comingSoon && e.date);
|
||||||
|
const items = released
|
||||||
|
.map((e) => {
|
||||||
|
const ver = e.version.startsWith('v') ? e.version : `v${e.version}`;
|
||||||
|
const pubDate = new Date(`${e.date}T00:00:00Z`).toUTCString();
|
||||||
|
const body = e.changes
|
||||||
|
.map((c) => `${c.type === 'feature' ? 'Feature' : 'Fix'}: ${stripRefs(c.text)}`)
|
||||||
|
.join('\n');
|
||||||
|
return ` <item>
|
||||||
|
<title>Dockhand ${escapeHtml(ver)}</title>
|
||||||
|
<link>${SITE_URL}/#changelog</link>
|
||||||
|
<guid isPermaLink="false">dockhand-${escapeHtml(ver)}</guid>
|
||||||
|
<pubDate>${pubDate}</pubDate>
|
||||||
|
<description>${escapeHtml(body)}</description>
|
||||||
|
</item>`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
const lastBuild = released[0]?.date
|
||||||
|
? new Date(`${released[0].date}T00:00:00Z`).toUTCString()
|
||||||
|
: new Date().toUTCString();
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>Dockhand - Release notes</title>
|
||||||
|
<link>${SITE_URL}/#changelog</link>
|
||||||
|
<atom:link href="${SITE_URL}/changelog.xml" rel="self" type="application/rss+xml"/>
|
||||||
|
<description>New features and fixes in each Dockhand release.</description>
|
||||||
|
<language>en</language>
|
||||||
|
<lastBuildDate>${lastBuild}</lastBuildDate>
|
||||||
|
${items}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Also update softwareVersion in JSON-LD schema
|
// Guarded so the pure functions above (e.g. stripRefs) can be imported by unit tests
|
||||||
if (changelog.length > 0) {
|
// without triggering file reads/writes.
|
||||||
const latestVersion = changelog[0].version;
|
if (import.meta.main) {
|
||||||
// Match "softwareVersion": "X.X" or "softwareVersion": "X.X.X"
|
// Read changelog.json
|
||||||
const versionRegex = /"softwareVersion":\s*"[\d.]+"/;
|
console.log('Reading changelog from:', CHANGELOG_PATH);
|
||||||
if (versionRegex.test(indexHtml)) {
|
const changelog: ChangelogEntry[] = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8'));
|
||||||
indexHtml = indexHtml.replace(versionRegex, `"softwareVersion": "${latestVersion}"`);
|
console.log(`Found ${changelog.length} changelog entries`);
|
||||||
console.log(`Updated softwareVersion to: ${latestVersion}`);
|
|
||||||
|
// Read index.html
|
||||||
|
console.log('Reading index.html from:', INDEX_PATH);
|
||||||
|
let indexHtml = readFileSync(INDEX_PATH, 'utf-8');
|
||||||
|
|
||||||
|
// Generate new changelog section
|
||||||
|
const newChangelogSection = generateChangelogSection(changelog);
|
||||||
|
|
||||||
|
// Replace changelog section using regex
|
||||||
|
// Match from "<!-- Changelog Section -->" to the closing "</section>" before "<!-- CTA -->"
|
||||||
|
const changelogRegex = / <!-- Changelog Section -->[\s\S]*?<\/section>(?=\s*\n\s*<!-- CTA -->)/;
|
||||||
|
|
||||||
|
if (!changelogRegex.test(indexHtml)) {
|
||||||
|
console.error('ERROR: Could not find changelog section in index.html');
|
||||||
|
console.error('Looking for pattern: <!-- Changelog Section --> ... </section> followed by <!-- CTA -->');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Write back to index.html
|
indexHtml = indexHtml.replace(changelogRegex, newChangelogSection);
|
||||||
writeFileSync(INDEX_PATH, indexHtml);
|
|
||||||
console.log('');
|
// Also update softwareVersion in JSON-LD schema
|
||||||
console.log('Generated changelog in webpage/index.html');
|
if (changelog.length > 0) {
|
||||||
console.log(` - Latest version: v${changelog[0]?.version || 'unknown'}`);
|
const latestVersion = changelog[0].version;
|
||||||
console.log(` - Total entries: ${changelog.length}`);
|
// Match "softwareVersion": "X.X" or "softwareVersion": "X.X.X"
|
||||||
|
const versionRegex = /"softwareVersion":\s*"[\d.]+"/;
|
||||||
|
if (versionRegex.test(indexHtml)) {
|
||||||
|
indexHtml = indexHtml.replace(versionRegex, `"softwareVersion": "${latestVersion}"`);
|
||||||
|
console.log(`Updated softwareVersion to: ${latestVersion}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to index.html
|
||||||
|
writeFileSync(INDEX_PATH, indexHtml);
|
||||||
|
|
||||||
|
// Generate the RSS feed alongside the page
|
||||||
|
writeFileSync(RSS_PATH, generateRssFeed(changelog));
|
||||||
|
console.log('Generated RSS feed in webpage/changelog.xml');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('Generated changelog in webpage/index.html');
|
||||||
|
console.log(` - Latest version: v${changelog[0]?.version || 'unknown'}`);
|
||||||
|
console.log(` - Total entries: ${changelog.length}`);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user