feat: initialize VitePress CMS

This commit is contained in:
Coldsmile_7
2026-06-05 23:21:41 +08:00
commit 928f742d5e
45 changed files with 8214 additions and 0 deletions

283
vite.config.ts Normal file
View File

@@ -0,0 +1,283 @@
import { createReadStream, promises as fs } from "node:fs";
import { extname, join, normalize, relative, sep } from "node:path";
import type { IncomingMessage, ServerResponse } from "node:http";
import { defineConfig, type Plugin } from "vite";
import vue from "@vitejs/plugin-vue";
const siteRoot = join(process.cwd(), "test-vitepress");
interface LocalPagePayload {
path: string;
title: string;
description: string;
content: string;
}
interface LocalSiteSettingsPayload {
title: string;
description: string;
logo: string;
lastUpdated: boolean;
localSearch: boolean;
outline: boolean;
socialKind: string;
socialLink: string;
}
const configPath = join(siteRoot, ".vitepress", "config.ts");
function isMarkdownPath(filePath: string) {
return filePath.endsWith(".md") && !filePath.includes(`${sep}node_modules${sep}`);
}
function toPosixPath(filePath: string) {
return filePath.split(sep).join("/");
}
function resolveSitePath(relativePath: string) {
const normalizedPath = normalize(join(siteRoot, relativePath));
if (!normalizedPath.startsWith(siteRoot) || !normalizedPath.endsWith(".md")) {
throw new Error("Invalid site path");
}
return normalizedPath;
}
function parseFrontmatter(source: string) {
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
if (!match) {
return {
title: "",
description: "",
content: source,
};
}
const frontmatter = match[1];
const title = frontmatter.match(/^title:\s*(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
const description =
frontmatter.match(/^description:\s*(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
return {
title,
description,
content: source.slice(match[0].length),
};
}
function stringifyPage(page: LocalPagePayload) {
const frontmatter = [
"---",
`title: ${page.title || "未命名页面"}`,
`description: ${page.description || ""}`,
"---",
"",
].join("\n");
return `${frontmatter}${page.content.trimStart()}`;
}
function readStringValue(source: string, key: string, fallback = "") {
const pattern = new RegExp(`${key}:\\s*["'\`]([^"'\`]+)["'\`]`);
return source.match(pattern)?.[1] ?? fallback;
}
function readBooleanValue(source: string, key: string, fallback = false) {
const pattern = new RegExp(`${key}:\\s*(true|false)`);
const value = source.match(pattern)?.[1];
return value ? value === "true" : fallback;
}
function parseSiteSettings(source: string): LocalSiteSettingsPayload {
const socialLinksSource = source.match(/socialLinks:\s*\[([\s\S]*?)\]/)?.[1] ?? "";
return {
title: readStringValue(source, "title", "VitePress Site"),
description: readStringValue(source, "description"),
logo: readStringValue(source, "logo", "/logo.svg"),
lastUpdated: readBooleanValue(source, "lastUpdated", true),
localSearch: /search:\s*{[\s\S]*?provider:\s*["']local["'][\s\S]*?}/.test(source),
outline: readBooleanValue(source, "outline", true),
socialKind: readStringValue(socialLinksSource, "icon", "github"),
socialLink: readStringValue(socialLinksSource, "link", "https://github.com/example/vitepress-cms"),
};
}
function stringifySiteConfig(settings: LocalSiteSettingsPayload) {
const searchConfig = settings.localSearch
? ` search: {
provider: "local",
},`
: "";
return `import { defineConfig } from "vitepress";
export default defineConfig({
title: ${JSON.stringify(settings.title)},
description: ${JSON.stringify(settings.description)},
lastUpdated: ${settings.lastUpdated},
themeConfig: {
logo: ${JSON.stringify(settings.logo)},
outline: ${settings.outline},
nav: [
{ text: "首页", link: "/" },
{ text: "指南", link: "/guide/getting-started" },
{ text: "配置", link: "/guide/config" },
{ text: "更新日志", link: "/changelog" },
],
sidebar: {
"/guide/": [
{
text: "指南",
items: [
{ text: "快速开始", link: "/guide/getting-started" },
{ text: "页面管理", link: "/guide/pages" },
{ text: "配置说明", link: "/guide/config" },
],
},
],
},
socialLinks: [
{ icon: ${JSON.stringify(settings.socialKind)}, link: ${JSON.stringify(settings.socialLink)} },
],
footer: {
message: "Powered by VitePress-CMS",
copyright: "Copyright 2026",
},
${searchConfig}
},
});
`;
}
async function collectMarkdownFiles(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true });
const nestedFiles = await Promise.all(
entries.map(async (entry) => {
const entryPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === ".vitepress") {
return [];
}
return collectMarkdownFiles(entryPath);
}
return isMarkdownPath(entryPath) ? [entryPath] : [];
}),
);
return nestedFiles.flat();
}
function readRequestBody(request: IncomingMessage) {
return new Promise<string>((resolve, reject) => {
let body = "";
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function sendJson(response: ServerResponse, status: number, payload: unknown) {
response.statusCode = status;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.end(JSON.stringify(payload));
}
function localSiteApi(): Plugin {
return {
name: "vitepress-cms-local-site-api",
configureServer(server) {
server.middlewares.use("/api/local-site/pages", async (request, response) => {
try {
if (request.method === "GET") {
const files = await collectMarkdownFiles(siteRoot);
const contentFiles = files.filter((filePath) => {
const sitePath = toPosixPath(relative(siteRoot, filePath));
return sitePath !== "README.md";
});
const pages = await Promise.all(
contentFiles.sort().map(async (filePath) => {
const source = await fs.readFile(filePath, "utf-8");
const parsed = parseFrontmatter(source);
const path = toPosixPath(relative(siteRoot, filePath));
return {
id: path,
path,
title: parsed.title || path.replace(/\.md$/, ""),
description: parsed.description,
content: parsed.content,
};
}),
);
sendJson(response, 200, { pages });
return;
}
if (request.method === "PUT") {
const payload = JSON.parse(await readRequestBody(request)) as LocalPagePayload;
const filePath = resolveSitePath(payload.path);
await fs.writeFile(filePath, stringifyPage(payload), "utf-8");
sendJson(response, 200, { ok: true });
return;
}
sendJson(response, 405, { error: "Method not allowed" });
} catch (error) {
sendJson(response, 500, {
error: error instanceof Error ? error.message : "Unknown local site API error",
});
}
});
server.middlewares.use("/api/local-site/settings", async (request, response) => {
try {
if (request.method === "GET") {
const source = await fs.readFile(configPath, "utf-8");
sendJson(response, 200, { settings: parseSiteSettings(source) });
return;
}
if (request.method === "PUT") {
const settings = JSON.parse(await readRequestBody(request)) as LocalSiteSettingsPayload;
await fs.writeFile(configPath, stringifySiteConfig(settings), "utf-8");
sendJson(response, 200, { ok: true });
return;
}
sendJson(response, 405, { error: "Method not allowed" });
} catch (error) {
sendJson(response, 500, {
error: error instanceof Error ? error.message : "Unknown local site settings API error",
});
}
});
server.middlewares.use("/api/local-site/assets/", (request, response, next) => {
const url = request.url ?? "";
const filePath = normalize(join(siteRoot, "public", decodeURIComponent(url)));
if (!filePath.startsWith(join(siteRoot, "public"))) {
sendJson(response, 403, { error: "Forbidden" });
return;
}
response.setHeader("Content-Type", extname(filePath) === ".svg" ? "image/svg+xml" : "application/octet-stream");
createReadStream(filePath).on("error", next).pipe(response);
});
},
};
}
export default defineConfig({
plugins: [localSiteApi(), vue()],
});