/* eslint-disable no-console */ const fs = require('node:fs'); const path = require('node:path'); const { loadConfig } = require('../src/generator.js'); const { extractYearlyContributionsInnerHtml, } = require('../src/generator/utils/githubContributions'); const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger'); const log = createLogger('sync:heatmap'); const DEFAULT_SETTINGS = { enabled: true, cacheDir: 'dev', fetch: { timeoutMs: 10_000, userAgent: 'MeNavHeatmapSync/1.0', }, }; function parseBooleanEnv(value, fallback) { if (value === undefined || value === null || value === '') return fallback; const v = String(value).trim().toLowerCase(); if (v === '1' || v === 'true' || v === 'yes' || v === 'y') return true; if (v === '0' || v === 'false' || v === 'no' || v === 'n') return false; return fallback; } function parseIntegerEnv(value, fallback) { if (value === undefined || value === null || value === '') return fallback; const n = Number.parseInt(String(value), 10); return Number.isFinite(n) ? n : fallback; } function getSettings(config) { const fromConfig = config && config.site && config.site.github && typeof config.site.github === 'object' ? config.site.github : {}; const merged = { ...DEFAULT_SETTINGS, ...fromConfig, fetch: { ...DEFAULT_SETTINGS.fetch, ...(fromConfig.fetch || {}), }, }; merged.enabled = parseBooleanEnv(process.env.HEATMAP_ENABLED, merged.enabled); // 复用 projects 的 cacheDir 逻辑,保持所有 dev 缓存在同一目录 merged.cacheDir = process.env.PROJECTS_CACHE_DIR ? String(process.env.PROJECTS_CACHE_DIR) : process.env.HEATMAP_CACHE_DIR ? String(process.env.HEATMAP_CACHE_DIR) : merged.cacheDir; merged.fetch.timeoutMs = parseIntegerEnv( process.env.HEATMAP_FETCH_TIMEOUT, merged.fetch.timeoutMs ); merged.fetch.timeoutMs = Math.max(1_000, merged.fetch.timeoutMs); return merged; } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function findProjectsPages(config) { const pages = []; const nav = Array.isArray(config.navigation) ? config.navigation : []; nav.forEach((item) => { const pageId = item && item.id ? String(item.id) : ''; if (!pageId || !config[pageId]) return; const page = config[pageId]; const templateName = page && page.template ? String(page.template) : pageId; if (templateName !== 'projects') return; pages.push({ pageId, page }); }); return pages; } function getGithubUsernameFromConfig(config) { const username = config && config.site && config.site.github && config.site.github.username ? String(config.site.github.username).trim() : ''; return username; } async function fetchTextWithTimeout(url, { timeoutMs, headers }) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { method: 'GET', headers, signal: controller.signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.text(); } finally { clearTimeout(timer); } } async function main() { const elapsedMs = startTimer(); const config = loadConfig(); const settings = getSettings(config); log.info('开始'); if (!settings.enabled) { log.ok('heatmap 同步已禁用,跳过', { env: 'HEATMAP_ENABLED=false' }); return; } const username = getGithubUsernameFromConfig(config); if (!username) { log.ok('未配置 site.github.username,跳过'); return; } const pages = findProjectsPages(config); if (!pages.length) { log.ok('未找到 template=projects 的页面,跳过同步'); return; } const cacheBaseDir = path.isAbsolute(settings.cacheDir) ? settings.cacheDir : path.join(process.cwd(), settings.cacheDir); ensureDir(cacheBaseDir); const url = `https://github.com/users/${encodeURIComponent(username)}/contributions`; const headers = { 'user-agent': settings.fetch.userAgent, accept: 'text/html', }; let html; try { html = await fetchTextWithTimeout(url, { timeoutMs: settings.fetch.timeoutMs, headers }); } catch (error) { log.warn('获取 GitHub contributions 失败(best-effort)', { url, message: String(error && error.message ? error.message : error), }); return; } const innerHtml = extractYearlyContributionsInnerHtml(html); if (!innerHtml) { log.warn('解析 contributions HTML 失败(best-effort)', { url, username }); return; } for (const { pageId } of pages) { const payload = { version: '1.0', pageId, generatedAt: new Date().toISOString(), username, sourceUrl: url, html: innerHtml, }; const cachePath = path.join(cacheBaseDir, `${pageId}.heatmap-cache.json`); fs.writeFileSync(cachePath, JSON.stringify(payload, null, 2), 'utf8'); log.ok('写入 heatmap 缓存', { page: pageId, cache: cachePath }); } log.ok('完成', { ms: elapsedMs(), pages: pages.length, username }); } main().catch((error) => { log.error('执行异常(best-effort,不阻断后续 build)', { message: error && error.message ? error.message : String(error), }); if (isVerbose() && error && error.stack) console.error(error.stack); process.exitCode = 0; // best-effort:不阻断后续 build });