@@ -5,6 +5,19 @@ export interface WhatsNewInfo {
55 date ?: string ;
66}
77
8+ type FetchLike = ( input : string | URL | Request , init ?: RequestInit ) => Promise < Response > ;
9+
10+ interface UpdateManifestResponse {
11+ version ?: unknown ;
12+ notes ?: unknown ;
13+ pub_date ?: unknown ;
14+ }
15+
16+ interface GitHubReleaseResponse {
17+ body ?: unknown ;
18+ published_at ?: unknown ;
19+ }
20+
821interface WhatsNewStorageState {
922 current ?: WhatsNewInfo ;
1023 pending ?: WhatsNewInfo ;
@@ -41,6 +54,96 @@ export function buildWhatsNewMarkdown(info: WhatsNewInfo): string {
4154 return lines . join ( "\n" ) ;
4255}
4356
57+ function releaseTag ( version : string ) : string {
58+ return `v${ version } ` ;
59+ }
60+
61+ function updateChannel ( version : string ) : "stable" | "preview" {
62+ return version . includes ( "-preview." ) ? "preview" : "stable" ;
63+ }
64+
65+ function readText ( value : unknown ) : string | undefined {
66+ return typeof value === "string" && value . trim ( ) ? value : undefined ;
67+ }
68+
69+ function normalizeDate ( value : string | undefined ) : string | undefined {
70+ if ( ! value ) {
71+ return undefined ;
72+ }
73+
74+ return value . slice ( 0 , 10 ) ;
75+ }
76+
77+ async function readJson ( response : Response ) : Promise < unknown > {
78+ if ( ! response . ok ) {
79+ return null ;
80+ }
81+
82+ try {
83+ return await response . json ( ) ;
84+ } catch {
85+ return null ;
86+ }
87+ }
88+
89+ async function fetchManifestInfo ( info : WhatsNewInfo , fetchImpl : FetchLike ) : Promise < WhatsNewInfo > {
90+ const response = await fetchImpl ( `https://athas.dev/api/update/${ updateChannel ( info . version ) } ` , {
91+ cache : "no-store" ,
92+ } ) ;
93+ const manifest = ( await readJson ( response ) ) as UpdateManifestResponse | null ;
94+
95+ if ( manifest ?. version !== info . version ) {
96+ return info ;
97+ }
98+
99+ return {
100+ ...info ,
101+ body : info . body || readText ( manifest . notes ) ,
102+ date : info . date || normalizeDate ( readText ( manifest . pub_date ) ) ,
103+ } ;
104+ }
105+
106+ async function fetchGitHubReleaseInfo (
107+ info : WhatsNewInfo ,
108+ fetchImpl : FetchLike ,
109+ ) : Promise < WhatsNewInfo > {
110+ const response = await fetchImpl (
111+ `https://api.github.com/repos/athasdev/athas/releases/tags/${ releaseTag ( info . version ) } ` ,
112+ { cache : "no-store" } ,
113+ ) ;
114+ const release = ( await readJson ( response ) ) as GitHubReleaseResponse | null ;
115+
116+ return {
117+ ...info ,
118+ body : info . body || readText ( release ?. body ) ,
119+ date : info . date || normalizeDate ( readText ( release ?. published_at ) ) ,
120+ } ;
121+ }
122+
123+ export async function resolveWhatsNewInfo (
124+ info : WhatsNewInfo ,
125+ fetchImpl : FetchLike = fetch ,
126+ ) : Promise < WhatsNewInfo > {
127+ if ( info . body ?. trim ( ) ) {
128+ return info ;
129+ }
130+
131+ try {
132+ const manifestInfo = await fetchManifestInfo ( info , fetchImpl ) ;
133+ if ( manifestInfo . body ?. trim ( ) ) {
134+ return manifestInfo ;
135+ }
136+ } catch {
137+ // Keep the local fallback available when release metadata cannot be fetched.
138+ }
139+
140+ try {
141+ return await fetchGitHubReleaseInfo ( info , fetchImpl ) ;
142+ } catch {
143+ return info ;
144+ }
145+ }
146+
44147function readState ( ) : WhatsNewStorageState {
45148 if ( typeof window === "undefined" ) {
46149 return { } ;
@@ -79,6 +182,14 @@ export function queuePendingWhatsNew(info: WhatsNewInfo) {
79182 } ) ;
80183}
81184
185+ export function storeCurrentWhatsNew ( info : WhatsNewInfo ) {
186+ const state = readState ( ) ;
187+ writeState ( {
188+ ...state ,
189+ current : info ,
190+ } ) ;
191+ }
192+
82193export function hydrateWhatsNew ( currentVersion : string ) : {
83194 info : WhatsNewInfo ;
84195 shouldAutoOpen : boolean ;
0 commit comments