feat: initialize VitePress CMS
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
import type { DocPage, GitHubConnection, SiteSettings } from "../types";
|
||||
import { parseFrontmatter, parseSiteSettings, stringifyMarkdownPage, stringifySiteConfig } from "../utils/siteContent";
|
||||
|
||||
interface GitTreeResponse {
|
||||
tree: Array<{
|
||||
path: string;
|
||||
type: "blob" | "tree";
|
||||
sha: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface GitHubContentResponse {
|
||||
content: string;
|
||||
encoding: string;
|
||||
sha: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function githubHeaders(connection: GitHubConnection) {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
};
|
||||
|
||||
if (connection.token.trim()) {
|
||||
headers.Authorization = `Bearer ${connection.token.trim()}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function usesSessionProxy(connection: GitHubConnection) {
|
||||
return !connection.token.trim();
|
||||
}
|
||||
|
||||
function siteParams(connection: GitHubConnection) {
|
||||
return new URLSearchParams({
|
||||
owner: connection.owner,
|
||||
repo: connection.repo,
|
||||
branch: connection.branch,
|
||||
siteRoot: connection.siteRoot,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeSiteRoot(siteRoot: string) {
|
||||
return siteRoot.trim().replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function toRepoPath(connection: GitHubConnection, path: string) {
|
||||
const siteRoot = normalizeSiteRoot(connection.siteRoot);
|
||||
return siteRoot ? `${siteRoot}/${path}` : path;
|
||||
}
|
||||
|
||||
function fromRepoPath(connection: GitHubConnection, path: string) {
|
||||
const siteRoot = normalizeSiteRoot(connection.siteRoot);
|
||||
return siteRoot && path.startsWith(`${siteRoot}/`) ? path.slice(siteRoot.length + 1) : path;
|
||||
}
|
||||
|
||||
function decodeBase64Content(content: string) {
|
||||
const binary = atob(content.replace(/\n/g, ""));
|
||||
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
function encodeBase64Content(content: string) {
|
||||
const bytes = new TextEncoder().encode(content);
|
||||
let binary = "";
|
||||
bytes.forEach((byte) => {
|
||||
binary += String.fromCharCode(byte);
|
||||
});
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function githubFetch<T>(connection: GitHubConnection, path: string, init?: RequestInit) {
|
||||
const response = await fetch(`https://api.github.com${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
...githubHeaders(connection),
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const details = await response.text();
|
||||
throw new Error(`GitHub 请求失败:${response.status} ${details}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async function getContent(connection: GitHubConnection, repoPath: string) {
|
||||
const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/");
|
||||
return githubFetch<GitHubContentResponse>(
|
||||
connection,
|
||||
`/repos/${connection.owner}/${connection.repo}/contents/${encodedPath}?ref=${encodeURIComponent(connection.branch)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadGitHubPages(connection: GitHubConnection) {
|
||||
if (usesSessionProxy(connection)) {
|
||||
const response = await fetch(`/api/github/site/pages?${siteParams(connection).toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`读取 GitHub 页面失败:${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { pages: DocPage[] };
|
||||
return data.pages;
|
||||
}
|
||||
|
||||
const tree = await githubFetch<GitTreeResponse>(
|
||||
connection,
|
||||
`/repos/${connection.owner}/${connection.repo}/git/trees/${encodeURIComponent(connection.branch)}?recursive=1`,
|
||||
);
|
||||
const siteRoot = normalizeSiteRoot(connection.siteRoot);
|
||||
const prefix = siteRoot ? `${siteRoot}/` : "";
|
||||
const markdownFiles = tree.tree
|
||||
.filter((entry) => entry.type === "blob")
|
||||
.filter((entry) => entry.path.endsWith(".md"))
|
||||
.filter((entry) => !entry.path.includes("/node_modules/"))
|
||||
.filter((entry) => entry.path !== `${prefix}README.md`)
|
||||
.filter((entry) => !prefix || entry.path.startsWith(prefix));
|
||||
|
||||
return Promise.all(
|
||||
markdownFiles.sort((a, b) => a.path.localeCompare(b.path)).map(async (entry) => {
|
||||
const file = await getContent(connection, entry.path);
|
||||
const source = decodeBase64Content(file.content);
|
||||
const parsed = parseFrontmatter(source);
|
||||
const path = fromRepoPath(connection, entry.path);
|
||||
|
||||
return {
|
||||
id: path,
|
||||
path,
|
||||
sha: file.sha,
|
||||
title: parsed.title || path.replace(/\.md$/, ""),
|
||||
description: parsed.description,
|
||||
content: parsed.content,
|
||||
} satisfies DocPage;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveGitHubPage(connection: GitHubConnection, page: DocPage) {
|
||||
if (usesSessionProxy(connection)) {
|
||||
const response = await fetch("/api/github/site/pages", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
site: {
|
||||
owner: connection.owner,
|
||||
repo: connection.repo,
|
||||
branch: connection.branch,
|
||||
siteRoot: connection.siteRoot,
|
||||
},
|
||||
page,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`保存 GitHub 页面失败:${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { sha: string };
|
||||
return data.sha;
|
||||
}
|
||||
|
||||
const repoPath = toRepoPath(connection, page.path);
|
||||
const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/");
|
||||
const response = await githubFetch<{ content: { sha: string } }>(
|
||||
connection,
|
||||
`/repos/${connection.owner}/${connection.repo}/contents/${encodedPath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: `docs: update ${page.path} from VitePress-CMS`,
|
||||
content: encodeBase64Content(stringifyMarkdownPage(page)),
|
||||
branch: connection.branch,
|
||||
sha: page.sha,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return response.content.sha;
|
||||
}
|
||||
|
||||
export async function loadGitHubSettings(connection: GitHubConnection) {
|
||||
if (usesSessionProxy(connection)) {
|
||||
const response = await fetch(`/api/github/site/settings?${siteParams(connection).toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`读取 GitHub 配置失败:${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as {
|
||||
settings: SiteSettings;
|
||||
sha: string;
|
||||
};
|
||||
}
|
||||
|
||||
const configPath = toRepoPath(connection, ".vitepress/config.ts");
|
||||
const file = await getContent(connection, configPath);
|
||||
const source = decodeBase64Content(file.content);
|
||||
|
||||
return {
|
||||
settings: parseSiteSettings(source),
|
||||
sha: file.sha,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveGitHubSettings(
|
||||
connection: GitHubConnection,
|
||||
settings: SiteSettings,
|
||||
sha?: string,
|
||||
) {
|
||||
if (usesSessionProxy(connection)) {
|
||||
const response = await fetch("/api/github/site/settings", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
site: {
|
||||
owner: connection.owner,
|
||||
repo: connection.repo,
|
||||
branch: connection.branch,
|
||||
siteRoot: connection.siteRoot,
|
||||
},
|
||||
settings,
|
||||
sha,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`保存 GitHub 配置失败:${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { sha: string };
|
||||
return data.sha;
|
||||
}
|
||||
|
||||
const repoPath = toRepoPath(connection, ".vitepress/config.ts");
|
||||
const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/");
|
||||
const response = await githubFetch<{ content: { sha: string } }>(
|
||||
connection,
|
||||
`/repos/${connection.owner}/${connection.repo}/contents/${encodedPath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: "docs: update VitePress config from VitePress-CMS",
|
||||
content: encodeBase64Content(stringifySiteConfig(settings)),
|
||||
branch: connection.branch,
|
||||
sha,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return response.content.sha;
|
||||
}
|
||||
Reference in New Issue
Block a user