|
1 | 1 | import type { Metadata } from "next"; |
| 2 | +import { XMLParser } from "fast-xml-parser"; |
2 | 3 | import Image from "next/image"; |
3 | 4 | import SectionWrapper from "@/components/section-wrapper"; |
4 | 5 | import { H1, P } from "@/components/text"; |
5 | | -import { fetchLatestGhosttyVersion } from "@/lib/fetch-latest-ghostty-version"; |
6 | 6 | import SVGIMG from "../../../public/ghostty-logo.svg"; |
7 | | -import ReleaseDownloadPage from "./release-download-page"; |
8 | | -import TipDownloadPage from "./tip-download-page"; |
| 7 | +import ReleaseDownloadPage from "./ReleaseDownloadPage"; |
| 8 | +import TipDownloadPage from "./TipDownloadPage"; |
9 | 9 | import s from "./DownloadPage.module.css"; |
10 | 10 |
|
11 | | -export const dynamic = "force-static"; |
| 11 | +type AppcastItem = { |
| 12 | + "sparkle:version": string; |
| 13 | + "sparkle:shortVersionString": string; |
| 14 | +}; |
12 | 15 |
|
| 16 | +type Appcast = { |
| 17 | + rss?: { |
| 18 | + channel?: { |
| 19 | + item?: AppcastItem | AppcastItem[]; |
| 20 | + }; |
| 21 | + }; |
| 22 | +}; |
| 23 | + |
| 24 | +/** Metadata for the download page. */ |
13 | 25 | export const metadata: Metadata = { |
14 | 26 | title: "Download Ghostty", |
15 | 27 | description: |
16 | 28 | "Ghostty is a fast, feature-rich, and cross-platform terminal emulator that uses platform-native UI and GPU acceleration.", |
17 | 29 | }; |
18 | 30 |
|
19 | | -async function loadPageData(): Promise<{ |
20 | | - latestVersion: string; |
21 | | -}> { |
22 | | - return { |
23 | | - latestVersion: await fetchLatestGhosttyVersion(), |
24 | | - }; |
| 31 | +/** Fetches and parses the appcast to determine the latest released Ghostty version. */ |
| 32 | +async function fetchLatestGhosttyVersion(): Promise<string> { |
| 33 | + const response = await fetch( |
| 34 | + "https://release.files.ghostty.org/appcast.xml", |
| 35 | + { |
| 36 | + cache: "force-cache", |
| 37 | + }, |
| 38 | + ); |
| 39 | + if (!response.ok) { |
| 40 | + throw new Error(`Failed to fetch XML: ${response.statusText}`); |
| 41 | + } |
| 42 | + |
| 43 | + const xmlContent = await response.text(); |
| 44 | + const parser = new XMLParser({ |
| 45 | + ignoreAttributes: false, |
| 46 | + }); |
| 47 | + const parsedXml = parser.parse(xmlContent) as Appcast; |
| 48 | + |
| 49 | + const items = parsedXml.rss?.channel?.item; |
| 50 | + if (!items) { |
| 51 | + throw new Error("Failed to parse appcast XML: no items found"); |
| 52 | + } |
| 53 | + |
| 54 | + const itemsArray = Array.isArray(items) ? items : [items]; |
| 55 | + const latestItem = itemsArray.reduce((maxItem, currentItem) => { |
| 56 | + const currentVersion = Number.parseInt(currentItem["sparkle:version"], 10); |
| 57 | + const maxVersion = Number.parseInt(maxItem["sparkle:version"], 10); |
| 58 | + return currentVersion > maxVersion ? currentItem : maxItem; |
| 59 | + }); |
| 60 | + |
| 61 | + return latestItem["sparkle:shortVersionString"]; |
25 | 62 | } |
26 | 63 |
|
| 64 | +/** Renders the download page for either stable releases or the tip build. */ |
27 | 65 | export default async function DownloadPage() { |
28 | | - const { latestVersion } = await loadPageData(); |
| 66 | + const latestVersion = await fetchLatestGhosttyVersion(); |
29 | 67 | const isTip = process.env.GIT_COMMIT_REF === "tip"; |
30 | 68 |
|
31 | 69 | return ( |
|
0 commit comments