dashy/plugins/github-data.js

240 lines
8 KiB
JavaScript

const REPO = 'lissy93/dashy';
const MAX_TAG_DATE_FETCHES = 20;
function stripMarkdown(text) {
return text
.replace(/#{1,6}\s*/g, '')
.replace(/[*_~`>]/g, '')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
.replace(/\n+/g, ' ')
.trim();
}
async function fetchJson(url, headers) {
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return { json: await res.json(), headers: res.headers };
}
async function fetchAllContributors(headers) {
const allContributors = [];
let page = 1;
const perPage = 100;
while (true) {
const { json: data } = await fetchJson(
`https://api.github.com/repos/${REPO}/contributors?per_page=${perPage}&page=${page}`,
headers,
);
if (!Array.isArray(data) || data.length === 0) break;
allContributors.push(...data);
if (data.length < perPage) break;
page++;
}
return allContributors;
}
module.exports = function githubDataPlugin(context) {
return {
name: 'github-data',
async loadContent() {
const token = process.env.GITHUB_TOKEN || '';
const headers = { 'User-Agent': 'dashy-docs' };
if (token) headers['Authorization'] = `token ${token}`;
const data = {
releases: null,
tags: null,
commits: null,
contributors: null,
sponsors: null,
starCount: null,
dockerPulls: null,
contributorCount: null,
latestTag: null,
};
// Fetch all sources in parallel, each wrapped in try/catch
const [
releasesResult,
tagsResult,
commitsResult,
contributorsResult,
sponsorsResult,
repoResult,
dockerResult,
contributorCountResult,
] = await Promise.allSettled([
fetchJson(`https://api.github.com/repos/${REPO}/releases?per_page=100`, headers),
fetchJson(`https://api.github.com/repos/${REPO}/tags?per_page=100`, headers),
fetchJson(`https://api.github.com/repos/${REPO}/commits?per_page=30&page=1`, headers),
fetchAllContributors(headers),
fetchJson('https://github-sponsors-api.as93.net/lissy93', headers),
fetchJson(`https://api.github.com/repos/${REPO}`, headers),
fetchJson('https://hub.docker.com/v2/repositories/lissy93/dashy/', headers),
fetch(`https://api.github.com/repos/${REPO}/contributors?per_page=1&anon=true`, { headers }),
]);
// Releases — trim to only what components need
if (releasesResult.status === 'fulfilled') {
data.releases = releasesResult.value.json.map(r => ({
tag_name: r.tag_name,
name: r.name,
published_at: r.published_at,
body: r.body ? stripMarkdown(r.body).slice(0, 200) : '',
html_url: r.html_url,
author_login: r.author?.login,
author_avatar: r.author?.avatar_url,
}));
}
// Tags — resolve dates for non-release tags
if (tagsResult.status === 'fulfilled') {
const rawTags = tagsResult.value.json;
const releaseTagNames = data.releases
? new Set(data.releases.map(r => r.tag_name))
: new Set();
const nonReleaseTags = rawTags
.filter(t => !releaseTagNames.has(t.name))
.slice(0, MAX_TAG_DATE_FETCHES);
const tagDateResults = await Promise.allSettled(
nonReleaseTags.map(t =>
fetchJson(`https://api.github.com/repos/${REPO}/commits/${t.commit.sha}`, headers)
.then(({ json: c }) => ({
name: t.name,
date: c.commit.author.date,
}))
)
);
data.tags = tagDateResults
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
// Latest tag + date (for UpdateBanner) — reuse already-resolved date if possible
if (rawTags.length > 0) {
const latestTag = rawTags[0];
const resolvedInBatch = data.tags?.find(t => t.name === latestTag.name);
const matchingRelease = data.releases?.find(r => r.tag_name === latestTag.name);
if (resolvedInBatch) {
data.latestTag = { name: latestTag.name, date: resolvedInBatch.date };
} else if (matchingRelease) {
data.latestTag = { name: latestTag.name, date: matchingRelease.published_at };
} else {
try {
const { json: commitData } = await fetchJson(
`https://api.github.com/repos/${REPO}/commits/${latestTag.commit.sha}`,
headers,
);
data.latestTag = {
name: latestTag.name,
date: commitData.commit.author.date,
};
} catch {
data.latestTag = { name: latestTag.name, date: null };
}
}
}
}
// Commits — fetch page 1 from the parallel batch, then paginate for more
const allRawCommits = [];
if (commitsResult.status === 'fulfilled') {
allRawCommits.push(...commitsResult.value.json);
// Fetch up to 7 more pages sequentially
if (commitsResult.value.json.length >= 30) {
for (let page = 2; page <= 5; page++) {
try {
const { json } = await fetchJson(
`https://api.github.com/repos/${REPO}/commits?per_page=30&page=${page}`,
headers,
);
if (!Array.isArray(json) || json.length === 0) break;
allRawCommits.push(...json);
if (json.length < 30) break;
} catch {
break; // keep what we have so far
}
}
}
data.commits = allRawCommits.map(c => ({
sha: c.sha,
message: (c.commit?.message || '').split('\n')[0],
date: c.commit?.author?.date || c.commit?.committer?.date,
author_login: c.author?.login || c.commit?.author?.name,
author_avatar: c.author?.avatar_url,
html_url: c.html_url,
}));
}
// Contributors — trim to what Authors component needs
if (contributorsResult.status === 'fulfilled') {
const allContribs = contributorsResult.value;
data.contributors = allContribs
.filter(c => c.type === 'User' && !c.login.endsWith('[bot]'))
.map(c => ({
id: c.id,
login: c.login,
avatar_url: c.avatar_url,
html_url: c.html_url,
contributions: c.contributions,
type: c.type,
}));
}
// Sponsors
if (sponsorsResult.status === 'fulfilled') {
const sponsorsData = sponsorsResult.value.json;
if (Array.isArray(sponsorsData) && sponsorsData.length > 0) {
data.sponsors = sponsorsData.map(s => ({
login: s.login,
name: s.name,
avatarUrl: s.avatarUrl,
}));
}
}
// Star count
if (repoResult.status === 'fulfilled') {
data.starCount = repoResult.value.json.stargazers_count || null;
}
// Docker pulls
if (dockerResult.status === 'fulfilled') {
data.dockerPulls = dockerResult.value.json.pull_count || null;
}
// Contributor count from Link header
if (contributorCountResult.status === 'fulfilled') {
try {
const linkHeader = contributorCountResult.value.headers.get('Link');
if (linkHeader) {
const lastMatch = linkHeader.match(/&page=(\d+)>;\s*rel="last"/);
if (lastMatch) {
data.contributorCount = parseInt(lastMatch[1], 10);
}
}
} catch {}
}
const fetched = Object.entries(data)
.filter(([, v]) => v != null)
.map(([k, v]) => Array.isArray(v) ? `${k}(${v.length})` : `${k}`)
.join(', ');
console.log(`[github-data] Build-time data: ${fetched || 'none (API rate-limited?)'}`);
return data;
},
async contentLoaded({ content, actions }) {
actions.setGlobalData(content);
},
};
};