diff --git a/site/src/lib/trending.ts b/site/src/lib/trending.ts new file mode 100644 index 0000000..122df56 --- /dev/null +++ b/site/src/lib/trending.ts @@ -0,0 +1,84 @@ +/** + * Trending data loader + schema normalizer. + * + * The gh_trending Lambda historically wrote two different shapes: + * - Legacy: `weekly`/`monthly` are objects `{ period, repos: Repo[] }` and + * repos have no `stars_this_period`. + * - Current: `weekly`/`monthly` are flat `Repo[]` arrays with `stars_this_period`. + * + * Archive pages render historical files, so both shapes must be tolerated or the + * static build crashes on old data (e.g. `data.weekly.map is not a function`). + * This module coerces either shape into the current flat `TrendingData`. + */ +import { readFileSync } from 'node:fs'; + +export interface Repo { + name: string; + url: string; + description: string; + language: string; + stars: number; + stars_this_period: number; + summary: string; +} + +export interface TrendingData { + updated_at: string; + weekly: Repo[]; + monthly: Repo[]; +} + +const EMPTY: TrendingData = { updated_at: '', weekly: [], monthly: [] }; + +function normalizeRepo(raw: unknown): Repo { + const r = (raw ?? {}) as Record; + return { + name: typeof r.name === 'string' ? r.name : '', + url: typeof r.url === 'string' ? r.url : '', + description: typeof r.description === 'string' ? r.description : '', + language: typeof r.language === 'string' ? r.language : '', + stars: typeof r.stars === 'number' ? r.stars : 0, + stars_this_period: typeof r.stars_this_period === 'number' ? r.stars_this_period : 0, + summary: typeof r.summary === 'string' ? r.summary : '', + }; +} + +/** Coerce a `weekly`/`monthly` field from either schema into `Repo[]`. */ +function normalizeBucket(value: unknown): Repo[] { + if (Array.isArray(value)) { + return value.map(normalizeRepo); + } + // Legacy shape: { period, repos: Repo[] } + if (value && typeof value === 'object' && Array.isArray((value as { repos?: unknown }).repos)) { + return ((value as { repos: unknown[] }).repos).map(normalizeRepo); + } + return []; +} + +/** Normalize a parsed trending JSON object (either schema) into `TrendingData`. */ +export function normalizeTrending(raw: unknown): TrendingData { + if (!raw || typeof raw !== 'object') { + return { ...EMPTY }; + } + const obj = raw as Record; + const updated_at = + typeof obj.updated_at === 'string' + ? obj.updated_at + : typeof obj.generated_at === 'string' + ? obj.generated_at + : ''; + return { + updated_at, + weekly: normalizeBucket(obj.weekly), + monthly: normalizeBucket(obj.monthly), + }; +} + +/** Read a trending JSON file and normalize it; returns empty data if missing/invalid. */ +export function loadTrending(path: string): TrendingData { + try { + return normalizeTrending(JSON.parse(readFileSync(path, 'utf-8'))); + } catch { + return { ...EMPTY }; + } +} diff --git a/site/src/pages/archive/gh-trending/[date].astro b/site/src/pages/archive/gh-trending/[date].astro index 34dd4a1..843de7b 100644 --- a/site/src/pages/archive/gh-trending/[date].astro +++ b/site/src/pages/archive/gh-trending/[date].astro @@ -1,23 +1,8 @@ --- import Base from '../../../layouts/Base.astro'; -import { readFileSync, readdirSync } from 'node:fs'; +import { readdirSync } from 'node:fs'; import { resolve } from 'node:path'; - -interface Repo { - name: string; - url: string; - description: string; - language: string; - stars: number; - stars_this_period: number; - summary: string; -} - -interface TrendingData { - updated_at: string; - weekly: Repo[]; - monthly: Repo[]; -} +import { loadTrending, type TrendingData } from '../../../lib/trending'; export function getStaticPaths() { const archiveDir = resolve(process.cwd(), '..', 'data', 'archive', 'gh-trending'); @@ -43,14 +28,8 @@ export function getStaticPaths() { const { date } = Astro.params; const { prevDate, nextDate } = Astro.props; -let data: TrendingData = { updated_at: '', weekly: [], monthly: [] }; - -try { - const dataPath = resolve(process.cwd(), '..', 'data', 'archive', 'gh-trending', `${date}.json`); - data = JSON.parse(readFileSync(dataPath, 'utf-8')); -} catch { - // data file not found -} +const dataPath = resolve(process.cwd(), '..', 'data', 'archive', 'gh-trending', `${date}.json`); +const data: TrendingData = loadTrending(dataPath); const languageColors: Record = { TypeScript: 'bg-blue-500', diff --git a/site/src/pages/rss/trending.xml.ts b/site/src/pages/rss/trending.xml.ts index faa8f88..db8633e 100644 --- a/site/src/pages/rss/trending.xml.ts +++ b/site/src/pages/rss/trending.xml.ts @@ -1,33 +1,11 @@ import rss from '@astrojs/rss'; import type { APIContext } from 'astro'; -import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; - -interface Repo { - name: string; - url: string; - description: string; - language: string; - stars: number; - stars_this_period: number; - summary: string; -} - -interface TrendingData { - updated_at: string; - weekly: Repo[]; - monthly: Repo[]; -} +import { loadTrending, type Repo } from '../../lib/trending'; export function GET(context: APIContext) { - let data: TrendingData = { updated_at: '', weekly: [], monthly: [] }; - - try { - const dataPath = resolve(process.cwd(), '..', 'data', 'gh-trending.json'); - data = JSON.parse(readFileSync(dataPath, 'utf-8')); - } catch { - // data file not found — use empty defaults - } + const dataPath = resolve(process.cwd(), '..', 'data', 'gh-trending.json'); + const data = loadTrending(dataPath); const pubDate = data.updated_at ? new Date(data.updated_at) : new Date(); diff --git a/site/src/pages/trending.astro b/site/src/pages/trending.astro index 918ea06..93ee2c8 100644 --- a/site/src/pages/trending.astro +++ b/site/src/pages/trending.astro @@ -1,32 +1,10 @@ --- import Base from '../layouts/Base.astro'; -import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; +import { loadTrending, type TrendingData } from '../lib/trending'; -interface Repo { - name: string; - url: string; - description: string; - language: string; - stars: number; - stars_this_period: number; - summary: string; -} - -interface TrendingData { - updated_at: string; - weekly: Repo[]; - monthly: Repo[]; -} - -let data: TrendingData = { updated_at: '', weekly: [], monthly: [] }; - -try { - const dataPath = resolve(process.cwd(), '..', 'data', 'gh-trending.json'); - data = JSON.parse(readFileSync(dataPath, 'utf-8')); -} catch { - // data file not found — use empty defaults -} +const dataPath = resolve(process.cwd(), '..', 'data', 'gh-trending.json'); +const data: TrendingData = loadTrending(dataPath); const languageColors: Record = { TypeScript: 'bg-blue-500',