| | import os from 'os' |
| | import fs from 'fs' |
| | import path from 'path' |
| |
|
| | import Parser from 'rss-parser' |
| |
|
| | import type { ChangelogItem } from '@/types' |
| |
|
| | const CHANGELOG_CACHE_FILE_PATH = process.env.CHANGELOG_CACHE_FILE_PATH |
| | |
| | const CHANGELOG_DISABLED = Boolean(JSON.parse(process.env.CHANGELOG_DISABLED || 'false')) |
| |
|
| | async function getRssFeed(url: string) { |
| | const parser = new Parser({ timeout: 5000 }) |
| | const feedUrl = `${url}/feed` |
| | let feed |
| |
|
| | try { |
| | feed = await parser.parseURL(feedUrl) |
| | } catch (err) { |
| | console.error(`cannot get ${feedUrl}: ${err instanceof Error ? err.message : err}`) |
| | return |
| | } |
| |
|
| | return feed |
| | } |
| |
|
| | export async function getChangelogItems( |
| | prefix: string | undefined, |
| | feedUrl: string, |
| | ignoreCache = false, |
| | ): Promise<ChangelogItem[] | undefined> { |
| | if (CHANGELOG_DISABLED) { |
| | if (process.env.NODE_ENV === 'development') { |
| | console.warn(`Downloading changelog (${feedUrl}) items is disabled.`) |
| | } |
| | return |
| | } |
| | if (!ignoreCache) { |
| | const fromCache = getChangelogItemsFromCache(prefix, feedUrl) |
| | if (fromCache) return fromCache |
| | } |
| |
|
| | const feed = await getRssFeed(feedUrl) |
| |
|
| | if (!feed || !feed.items) { |
| | console.log(feed) |
| | console.error('feed is not valid or has no items') |
| | return |
| | } |
| |
|
| | |
| | const changelog: ChangelogItem[] = feed.items.slice(0, 3).map((item) => { |
| | const rawTitle = item.title as string |
| | |
| | const title = prefix ? rawTitle.replace(new RegExp(`^${prefix}`), '') : rawTitle |
| | return { |
| | |
| | title: title.trim().charAt(0).toUpperCase() + title.slice(1), |
| | date: item.isoDate as string, |
| | href: item.link as string, |
| | } |
| | }) |
| |
|
| | |
| | |
| | |
| | setChangelogItemsCache(prefix, feedUrl, changelog) |
| |
|
| | return changelog |
| | } |
| |
|
| | const globalCache = new Map() |
| |
|
| | function getChangelogCacheKey(prefix: string | undefined, feedUrl: string) { |
| | |
| | |
| | return `${prefix || ''}${feedUrl}`.replace(/[^a-z]+/gi, '') |
| | } |
| |
|
| | function getDiskCachePath(prefix: string | undefined, feedUrl: string) { |
| | |
| | if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') { |
| | if (CHANGELOG_CACHE_FILE_PATH) { |
| | return CHANGELOG_CACHE_FILE_PATH |
| | } |
| | const cacheKey = getChangelogCacheKey(prefix, feedUrl) |
| | const date = new Date().toISOString().split('T')[0] |
| | const fileName = `changelogcache-${cacheKey}-${date}.json` |
| | return path.join(os.tmpdir(), fileName) |
| | } |
| | } |
| |
|
| | function getChangelogItemsFromCache(prefix: string | undefined, feedUrl: string) { |
| | const cacheKey = getChangelogCacheKey(prefix, feedUrl) |
| |
|
| | if (globalCache.get(cacheKey)) { |
| | return globalCache.get(cacheKey) |
| | } |
| |
|
| | const diskCachePath = getDiskCachePath(prefix, feedUrl) |
| | if (diskCachePath) { |
| | try { |
| | const payload = JSON.parse(fs.readFileSync(diskCachePath, 'utf-8')) |
| | if (process.env.NODE_ENV === 'development') |
| | console.log(`Changelog disk-cache HIT on ${diskCachePath}`) |
| | |
| | |
| | globalCache.set(cacheKey, payload) |
| | return payload |
| | } catch (err) { |
| | |
| | if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return |
| | |
| | |
| | if (err instanceof SyntaxError) { |
| | fs.unlinkSync(diskCachePath) |
| | return |
| | } |
| | throw err |
| | } |
| | } |
| | } |
| |
|
| | function setChangelogItemsCache( |
| | prefix: string | undefined, |
| | feedUrl: string, |
| | payload: ChangelogItem[], |
| | ) { |
| | const cacheKey = getChangelogCacheKey(prefix, feedUrl) |
| | globalCache.set(cacheKey, payload) |
| |
|
| | const diskCachePath = getDiskCachePath(prefix, feedUrl) |
| | |
| | |
| | if (diskCachePath) { |
| | fs.writeFileSync(diskCachePath, JSON.stringify(payload), 'utf-8') |
| | if (process.env.NODE_ENV === 'development') |
| | console.log(`Wrote changelog cache to disk ${diskCachePath}`) |
| | } |
| | } |
| |
|