Add organizations, research groups and repo stats to projects
This commit is contained in:
1 parent
3fefc814e1
commit
c820672803
25 files changed
+573
-32
No files matched your search
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user