Add organizations, research groups and repo stats to projects

This commit is contained in:
SirBlob committed 2026-10-31 21:17:42 -04:00
1 parent 3fefc814e1
commit c820672803
25 files changed
+573 -32

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import type { RepoStats } from '#lib/github/types';
const cacheDuration = 60 * 60 * 1000;
const githubRepoPattern = /^https:\/\/github\.com\/([^/]+\/[^/#?]+)/;
const statsCache = new Map<string, { stats: RepoStats; fetchedAt: number }>();
function githubRepoPath(url: string): string | null {
const match = url.match(githubRepoPattern);
return match ? match[1].replace(/\.git$/, '') : null;
}
async function fetchRepoStats(repoPath: string): Promise<RepoStats | null> {
const cached = statsCache.get(repoPath);
if (cached && Date.now() - cached.fetchedAt < cacheDuration) return cached.stats;
try {
const response = await fetch(`https://api.github.com/repos/${repoPath}`, {
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'portfolio-website' }
});
if (!response.ok) return cached?.stats ?? null;
const repo = await response.json();
const stats: RepoStats = { stars: repo.stargazers_count, forks: repo.forks_count };
statsCache.set(repoPath, { stats, fetchedAt: Date.now() });
return stats;
} catch {
return cached?.stats ?? null;
}
}
export async function getRepoStats(codeUrls: string[]): Promise<Record<string, RepoStats>> {
const results = await Promise.all(
codeUrls.map(async (codeUrl) => {
const repoPath = githubRepoPath(codeUrl);
const stats = repoPath ? await fetchRepoStats(repoPath) : null;
return [codeUrl, stats] as const;
})
);
return Object.fromEntries(results.filter((result): result is readonly [string, RepoStats] => result[1] !== null));
}
+62
View File
@@ -0,0 +1,62 @@
const cacheDuration = 60 * 60 * 1000;
const dayDuration = 24 * 60 * 60 * 1000;
const maxRangeDays = 540;
const npmPackagePattern = /^https:\/\/www\.npmjs\.com\/package\/((?:@[^/]+\/)?[^/#?]+)/;
const downloadsCache = new Map<string, { downloads: number; fetchedAt: number }>();
function npmPackageName(url: string): string | null {
return url.match(npmPackagePattern)?.[1] ?? null;
}
function formatDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
async function fetchCreatedDate(packageName: string): Promise<Date> {
const response = await fetch(`https://registry.npmjs.org/${packageName.replace('/', '%2f')}`);
if (!response.ok) throw new Error(`npm registry returned ${response.status} for ${packageName}`);
const metadata = await response.json();
return new Date(metadata.time.created);
}
async function fetchDownloadsBetween(packageName: string, start: Date, end: Date): Promise<number> {
const range = `${formatDate(start)}:${formatDate(end)}`;
const response = await fetch(`https://api.npmjs.org/downloads/point/${range}/${packageName}`);
if (!response.ok) throw new Error(`npm downloads returned ${response.status} for ${packageName}`);
const result = await response.json();
return result.downloads ?? 0;
}
async function fetchTotalDownloads(packageName: string): Promise<number | null> {
const cached = downloadsCache.get(packageName);
if (cached && Date.now() - cached.fetchedAt < cacheDuration) return cached.downloads;
try {
const today = new Date();
const rangeRequests: Promise<number>[] = [];
for (let rangeStart = await fetchCreatedDate(packageName); rangeStart <= today; ) {
const rangeEnd = new Date(Math.min(rangeStart.getTime() + (maxRangeDays - 1) * dayDuration, today.getTime()));
rangeRequests.push(fetchDownloadsBetween(packageName, rangeStart, rangeEnd));
rangeStart = new Date(rangeEnd.getTime() + dayDuration);
}
const downloads = (await Promise.all(rangeRequests)).reduce((total, count) => total + count, 0);
downloadsCache.set(packageName, { downloads, fetchedAt: Date.now() });
return downloads;
} catch (error) {
console.error(`npm download count failed for ${packageName}:`, error);
return cached?.downloads ?? null;
}
}
export async function getPackageDownloads(packageUrls: string[]): Promise<Record<string, number>> {
const results = await Promise.all(
packageUrls.map(async (packageUrl) => {
const packageName = npmPackageName(packageUrl);
const downloads = packageName ? await fetchTotalDownloads(packageName) : null;
return [packageUrl, downloads] as const;
})
);
return Object.fromEntries(results.filter((result): result is readonly [string, number] => result[1] !== null));
}