import { unstable_cache } from "next/cache";import { githubFetch, githubHeaders } from "@/apis/githubFetch";import { REPO_OWNER, REPO_NAME, REPO_BRANCH, CACHE_CONTENT_TAG, CACHE_CONTENT_REVALIDATE,} from "@/utilities/githubRepo";const CONTENT_PREFIX = "content/";export type ContentMap = Record<string, string>;function chooseSource(): "local" | "github" { const override = process.env.CONTENT_SOURCE; if (override === "local" || override === "github") return override; return process.env.NODE_ENV === "production" ? "github" : "local";}export const getContentMap = async (): Promise<ContentMap> => { return chooseSource() === "local" ? readLocalContent() : getCachedGithubContent();};async function readLocalContent(): Promise<ContentMap> { const { readdir, readFile } = await import("node:fs/promises"); const { join } = await import("node:path"); const root = join(process.cwd(), "content"); const out: ContentMap = {}; async function walk(dir: string, base = ""): Promise<void> { for (const entry of await readdir(dir, { withFileTypes: true })) { if (entry.name === ".DS_Store") continue; const rel = base ? `${base}/${entry.name}` : entry.name; if (entry.isDirectory()) await walk(join(dir, entry.name), rel); else if (entry.isFile()) out[rel] = await readFile(join(dir, entry.name), "utf-8"); } } await walk(root); return out;}const FETCH_CONCURRENCY = 12;async function mapLimit<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> { const results: R[] = new Array(items.length); let cursor = 0; async function worker(): Promise<void> { while (cursor < items.length) { const index = cursor++; results[index] = await fn(items[index]); } } await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)); return results;}const getCachedGithubContent = unstable_cache( async (): Promise<ContentMap> => { const paths = await listContentPaths(); const entries = await mapLimit( paths, FETCH_CONCURRENCY, async (rel) => [rel, await fetchRawFile(rel)] as const, ); return Object.fromEntries(entries); }, ["content-map"], { tags: [CACHE_CONTENT_TAG], revalidate: CACHE_CONTENT_REVALIDATE });async function listContentPaths(): Promise<string[]> { const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/git/trees/${REPO_BRANCH}?recursive=1`; const res = await githubFetch(url, { headers: await githubHeaders("cms"), }); if (!res.ok) throw new Error(`GitHub tree fetch failed: ${res.status} ${res.statusText}`); const data = (await res.json()) as { tree?: { path: string; type: string }[] }; return (data.tree ?? []) .filter((node) => node.type === "blob" && node.path.startsWith(CONTENT_PREFIX)) .map((node) => node.path.slice(CONTENT_PREFIX.length));}async function fetchRawFile(rel: string): Promise<string> { const encoded = rel.split("/").map(encodeURIComponent).join("/"); const url = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}/${CONTENT_PREFIX}${encoded}`; const res = await githubFetch(url, {}); if (!res.ok) throw new Error(`GitHub raw fetch failed (${res.status}): ${rel}`); return res.text();}