| import { readFileSync, readdirSync, statSync } from "node:fs"; |
| import path from "node:path"; |
| import matter from "gray-matter"; |
| import { |
| adConfig1, |
| adConfig2, |
| announcementConfig, |
| backgroundWallpaper, |
| commentConfig, |
| coverImageConfig, |
| fontConfig, |
| footerConfig, |
| friendsConfig, |
| friendsPageConfig, |
| licenseConfig, |
| live2dModelConfig, |
| musicPlayerConfig, |
| navBarConfig, |
| profileConfig, |
| sakuraConfig, |
| sidebarLayoutConfig, |
| siteConfig, |
| sponsorConfig, |
| spineModelConfig, |
| } from "@/config"; |
| import type { |
| AdminConfigBundle, |
| AdminImageRecord, |
| AdminPostRecord, |
| AdminSnapshot, |
| AdminSpecPageRecord, |
| } from "./types"; |
|
|
| const PROJECT_ROOT = process.cwd(); |
| const POSTS_DIR = path.join(PROJECT_ROOT, "src", "content", "posts"); |
| const SPEC_DIR = path.join(PROJECT_ROOT, "src", "content", "spec"); |
| const PUBLIC_DIR = path.join(PROJECT_ROOT, "public"); |
|
|
| const IMAGE_EXTENSIONS = new Set([ |
| ".png", |
| ".jpg", |
| ".jpeg", |
| ".webp", |
| ".gif", |
| ".svg", |
| ".avif", |
| ]); |
|
|
| const MARKDOWN_EXTENSIONS = new Set([".md", ".mdx"]); |
|
|
| function toUnixPath(value: string): string { |
| return value.replace(/\\/g, "/"); |
| } |
|
|
| function readUtf8(filePath: string): string { |
| return readFileSync(filePath, "utf8"); |
| } |
|
|
| function statSafe(filePath: string) { |
| try { |
| return statSync(filePath); |
| } catch { |
| return null; |
| } |
| } |
|
|
| function walkFiles(rootDir: string, matcher: (filePath: string) => boolean): string[] { |
| if (!statSafe(rootDir)?.isDirectory()) { |
| return []; |
| } |
|
|
| const results: string[] = []; |
| const stack = [rootDir]; |
|
|
| while (stack.length > 0) { |
| const currentDir = stack.pop(); |
| if (!currentDir) continue; |
|
|
| for (const entry of readdirSync(currentDir, { withFileTypes: true })) { |
| const absolutePath = path.join(currentDir, entry.name); |
| if (entry.isDirectory()) { |
| stack.push(absolutePath); |
| continue; |
| } |
| if (matcher(absolutePath)) { |
| results.push(absolutePath); |
| } |
| } |
| } |
|
|
| return results.sort((left, right) => left.localeCompare(right)); |
| } |
|
|
| function toPreviewUrl(origin: AdminImageRecord["origin"], relativeToRoot: string): string { |
| const encodedPath = relativeToRoot |
| .split("/") |
| .map((segment) => encodeURIComponent(segment)) |
| .join("/"); |
| return `/_admin/files/${origin}/${encodedPath}`; |
| } |
|
|
| function getPostRecords(): AdminPostRecord[] { |
| const markdownFiles = walkFiles(POSTS_DIR, (filePath) => |
| MARKDOWN_EXTENSIONS.has(path.extname(filePath).toLowerCase()), |
| ); |
|
|
| return markdownFiles |
| .map((filePath) => { |
| const raw = readUtf8(filePath); |
| const parsed = matter(raw); |
| const stats = statSync(filePath); |
| const relativePath = toUnixPath(path.relative(PROJECT_ROOT, filePath)); |
| const relativeToPosts = toUnixPath(path.relative(POSTS_DIR, filePath)); |
| const slug = relativeToPosts.replace(/\.(md|mdx)$/i, ""); |
| const data = parsed.data as Record<string, unknown>; |
|
|
| return { |
| slug, |
| filePath: relativePath, |
| title: String(data.title ?? slug), |
| published: String(data.published ?? ""), |
| updated: String(data.updated ?? ""), |
| description: String(data.description ?? ""), |
| image: String(data.image ?? ""), |
| tags: Array.isArray(data.tags) ? data.tags.map((item) => String(item)) : [], |
| category: String(data.category ?? ""), |
| lang: String(data.lang ?? ""), |
| draft: Boolean(data.draft ?? false), |
| pinned: Boolean(data.pinned ?? false), |
| comment: Boolean(data.comment ?? true), |
| author: String(data.author ?? ""), |
| sourceLink: String(data.sourceLink ?? ""), |
| licenseName: String(data.licenseName ?? ""), |
| licenseUrl: String(data.licenseUrl ?? ""), |
| body: parsed.content, |
| extension: path.extname(filePath).toLowerCase() === ".mdx" ? "mdx" : "md", |
| excerpt: parsed.content.trim().slice(0, 180), |
| modifiedAt: stats.mtime.toISOString(), |
| }; |
| }) |
| .sort((left, right) => { |
| const pinnedDiff = Number(right.pinned) - Number(left.pinned); |
| if (pinnedDiff !== 0) return pinnedDiff; |
| return right.published.localeCompare(left.published); |
| }); |
| } |
|
|
| function getSpecPageRecords(): AdminSpecPageRecord[] { |
| const markdownFiles = walkFiles(SPEC_DIR, (filePath) => |
| MARKDOWN_EXTENSIONS.has(path.extname(filePath).toLowerCase()), |
| ); |
|
|
| return markdownFiles.map((filePath) => ({ |
| slug: path.basename(filePath).replace(/\.(md|mdx)$/i, ""), |
| filePath: toUnixPath(path.relative(PROJECT_ROOT, filePath)), |
| body: readUtf8(filePath), |
| extension: path.extname(filePath).toLowerCase() === ".mdx" ? "mdx" : "md", |
| modifiedAt: statSync(filePath).mtime.toISOString(), |
| })); |
| } |
|
|
| function getImageRecords(): AdminImageRecord[] { |
| const imageRoots: Array<{ |
| dir: string; |
| origin: AdminImageRecord["origin"]; |
| }> = [ |
| { dir: PUBLIC_DIR, origin: "public" }, |
| { dir: POSTS_DIR, origin: "content" }, |
| ]; |
|
|
| return imageRoots.flatMap(({ dir, origin }) => { |
| return walkFiles(dir, (filePath) => |
| IMAGE_EXTENSIONS.has(path.extname(filePath).toLowerCase()), |
| ).map((filePath) => { |
| const stats = statSync(filePath); |
| const relativePath = toUnixPath(path.relative(PROJECT_ROOT, filePath)); |
| const relativeToRoot = toUnixPath(path.relative(dir, filePath)); |
|
|
| return { |
| id: relativePath, |
| name: path.basename(filePath), |
| path: relativePath, |
| url: toPreviewUrl(origin, relativeToRoot), |
| directory: toUnixPath(path.dirname(relativePath)), |
| origin, |
| size: stats.size, |
| updatedAt: stats.mtime.toISOString(), |
| sitePath: origin === "public" ? `/${relativeToRoot}` : relativeToRoot, |
| }; |
| }); |
| }); |
| } |
|
|
| function getConfigBundle(): AdminConfigBundle { |
| return { |
| siteConfig, |
| profileConfig, |
| navBarConfig, |
| sidebarConfig: sidebarLayoutConfig, |
| backgroundWallpaper, |
| announcementConfig, |
| footerConfig, |
| adConfig: { |
| adConfig1, |
| adConfig2, |
| }, |
| commentConfig, |
| musicConfig: musicPlayerConfig, |
| pioConfig: { |
| spineModelConfig, |
| live2dModelConfig, |
| }, |
| sponsorConfig, |
| friendsConfig: { |
| friendsPageConfig, |
| friendsConfig, |
| }, |
| licenseConfig, |
| coverImageConfig, |
| sakuraConfig, |
| fontConfig, |
| }; |
| } |
|
|
| export function getAdminSnapshot(): AdminSnapshot { |
| return { |
| generatedAt: new Date().toISOString(), |
| configs: getConfigBundle(), |
| posts: getPostRecords(), |
| specPages: getSpecPageRecords(), |
| images: getImageRecords(), |
| }; |
| }
|
|
|