diff options
| author | adamelmore <[email protected]> | 2026-01-26 12:51:32 -0600 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-26 12:51:35 -0600 |
| commit | de3b654dcd195448f1d19e55562b2d84a2db7c91 (patch) | |
| tree | 864f23f276140c1d2830cb57065daea3d0016f45 /packages/console/app/src/lib | |
| parent | 8b17ac656cb428b1eee5f425deb195dc61f844ba (diff) | |
| download | opencode-de3b654dcd195448f1d19e55562b2d84a2db7c91.tar.gz opencode-de3b654dcd195448f1d19e55562b2d84a2db7c91.zip | |
chore: refactor changelog
Diffstat (limited to 'packages/console/app/src/lib')
| -rw-r--r-- | packages/console/app/src/lib/changelog.ts | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/packages/console/app/src/lib/changelog.ts b/packages/console/app/src/lib/changelog.ts new file mode 100644 index 000000000..93a0d423c --- /dev/null +++ b/packages/console/app/src/lib/changelog.ts @@ -0,0 +1,146 @@ +import { query } from "@solidjs/router" + +type Release = { + tag_name: string + name: string + body: string + published_at: string + html_url: string +} + +export type HighlightMedia = + | { type: "video"; src: string } + | { type: "image"; src: string; width: string; height: string } + +export type HighlightItem = { + title: string + description: string + shortDescription?: string + media: HighlightMedia +} + +export type HighlightGroup = { + source: string + items: HighlightItem[] +} + +export type ChangelogRelease = { + tag: string + name: string + date: string + url: string + highlights: HighlightGroup[] + sections: { title: string; items: string[] }[] +} + +export type ChangelogData = { + ok: boolean + releases: ChangelogRelease[] +} + +export async function loadChangelog(): Promise<ChangelogData> { + const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenCode-Console", + }, + cf: { + // best-effort edge caching (ignored outside Cloudflare) + cacheTtl: 60 * 5, + cacheEverything: true, + }, + } as RequestInit).catch(() => undefined) + + if (!response?.ok) return { ok: false, releases: [] } + + const data = await response.json().catch(() => undefined) + if (!Array.isArray(data)) return { ok: false, releases: [] } + + const releases = (data as Release[]).map((release) => { + const parsed = parseMarkdown(release.body || "") + return { + tag: release.tag_name, + name: release.name, + date: release.published_at, + url: release.html_url, + highlights: parsed.highlights, + sections: parsed.sections, + } + }) + + return { ok: true, releases } +} + +export const changelog = query(async () => { + "use server" + const result = await loadChangelog() + return result.releases +}, "changelog") + +function parseHighlights(body: string): HighlightGroup[] { + const groups = new Map<string, HighlightItem[]>() + const regex = /<highlight\s+source="([^"]+)">([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/<h2>([^<]+)<\/h2>/) + const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/) + const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="[^"]*"\s+src="([^"]+)"/) + const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m) + + const media = (() => { + if (videoMatch) return { type: "video", src: videoMatch[1] } satisfies HighlightMedia + if (imgMatch) { + return { + type: "image", + src: imgMatch[3], + width: imgMatch[1], + height: imgMatch[2], + } satisfies HighlightMedia + } + })() + + if (!titleMatch || !media) continue + + const item: HighlightItem = { + title: titleMatch[1], + description: pMatch?.[2] || "", + shortDescription: pMatch?.[1], + media, + } + + if (!groups.has(source)) groups.set(source, []) + groups.get(source)!.push(item) + } + + return Array.from(groups.entries()).map(([source, items]) => ({ source, items })) +} + +function parseMarkdown(body: string) { + const lines = body.split("\n") + const sections: { title: string; items: string[] }[] = [] + let current: { title: string; items: string[] } | null = null + let skip = false + + for (const line of lines) { + if (line.startsWith("## ")) { + if (current) sections.push(current) + current = { title: line.slice(3).trim(), items: [] } + skip = false + continue + } + + if (line.startsWith("**Thank you")) { + skip = true + continue + } + + if (line.startsWith("- ") && !skip) current?.items.push(line.slice(2).trim()) + } + + if (current) sections.push(current) + return { sections, highlights: parseHighlights(body) } +} |
