Buckets:
| 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) } | |
| } | |
Xet Storage Details
- Size:
- 3.92 kB
- Xet hash:
- 1bc751c1bdbc1f518986f91240b2b7d6a225963bcdca1319adcf810c27f80ca4
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.