atproto utils for zig zat.dev
atproto sdk zig
at main 6.5 kB view raw
1import { 2 readdir, 3 readFile, 4 mkdir, 5 rm, 6 cp, 7 writeFile, 8 access, 9} from "node:fs/promises"; 10import path from "node:path"; 11import { execFile } from "node:child_process"; 12import { promisify } from "node:util"; 13 14const repoRoot = path.resolve(new URL("..", import.meta.url).pathname); 15const docsDir = path.join(repoRoot, "docs"); 16const devlogDir = path.join(repoRoot, "devlog"); 17const siteSrcDir = path.join(repoRoot, "site"); 18const outDir = path.join(repoRoot, "site-out"); 19const outDocsDir = path.join(outDir, "docs"); 20 21const execFileAsync = promisify(execFile); 22 23async function exists(filePath) { 24 try { 25 await access(filePath); 26 return true; 27 } catch { 28 return false; 29 } 30} 31 32function isMarkdown(filePath) { 33 return filePath.toLowerCase().endsWith(".md"); 34} 35 36async function listMarkdownFiles(dir, prefix = "") { 37 const entries = await readdir(dir, { withFileTypes: true }); 38 const out = []; 39 for (const e of entries) { 40 if (e.name.startsWith(".")) continue; 41 const rel = path.join(prefix, e.name); 42 const abs = path.join(dir, e.name); 43 if (e.isDirectory()) { 44 out.push(...(await listMarkdownFiles(abs, rel))); 45 } else if (e.isFile() && isMarkdown(e.name)) { 46 out.push(rel.replaceAll(path.sep, "/")); 47 } 48 } 49 return out.sort((a, b) => a.localeCompare(b)); 50} 51 52function titleFromMarkdown(md, fallback) { 53 const lines = md.split(/\r?\n/); 54 for (const line of lines) { 55 const m = /^#\s+(.+)\s*$/.exec(line); 56 if (m) return m[1].trim(); 57 } 58 return fallback.replace(/\.md$/i, ""); 59} 60 61function normalizeTitle(title) { 62 let t = String(title || "").trim(); 63 // Strip markdown links: [text](url) -> text 64 t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); 65 // If pages follow a "zat - ..." style, drop the redundant prefix in the nav. 66 t = t.replace(/^zat\s*-\s*/i, ""); 67 // Cheaply capitalize (keeps the rest as-authored). 68 if (t.length) t = t[0].toUpperCase() + t.slice(1); 69 return t; 70} 71 72async function getBuildId() { 73 try { 74 const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { 75 cwd: repoRoot, 76 }); 77 const full = String(stdout || "").trim(); 78 if (full) return full.slice(0, 12); 79 } catch { 80 // ignore 81 } 82 return String(Date.now()); 83} 84 85async function main() { 86 await rm(outDir, { recursive: true, force: true }); 87 await mkdir(outDir, { recursive: true }); 88 89 // Copy static site shell 90 await cp(siteSrcDir, outDir, { recursive: true }); 91 92 // Cache-bust immutable assets on Wisp by appending a per-commit query string. 93 const buildId = await getBuildId(); 94 const outIndex = path.join(outDir, "index.html"); 95 if (await exists(outIndex)) { 96 let html = await readFile(outIndex, "utf8"); 97 html = html.replaceAll('href="./style.css"', `href="./style.css?v=${buildId}"`); 98 html = html.replaceAll( 99 'src="./vendor/marked.min.js"', 100 `src="./vendor/marked.min.js?v=${buildId}"`, 101 ); 102 html = html.replaceAll( 103 'src="./app.js"', 104 `src="./app.js?v=${buildId}"`, 105 ); 106 html = html.replaceAll( 107 'href="./favicon.svg"', 108 `href="./favicon.svg?v=${buildId}"`, 109 ); 110 await writeFile(outIndex, html, "utf8"); 111 } 112 113 // Copy docs 114 await mkdir(outDocsDir, { recursive: true }); 115 116 const pages = []; 117 118 // Prefer an explicit docs homepage if present; otherwise use repo README as index. 119 const docsIndex = path.join(docsDir, "index.md"); 120 if (!(await exists(docsIndex))) { 121 const readme = path.join(repoRoot, "README.md"); 122 if (await exists(readme)) { 123 let md = await readFile(readme, "utf8"); 124 // Strip docs/ prefix from links since we're now inside the docs context. 125 md = md.replace(/\]\(docs\//g, "]("); 126 await writeFile(path.join(outDocsDir, "index.md"), md, "utf8"); 127 pages.push({ 128 path: "index.md", 129 title: normalizeTitle(titleFromMarkdown(md, "index.md")), 130 }); 131 } 132 } 133 134 const changelog = path.join(repoRoot, "CHANGELOG.md"); 135 const docsChangelog = path.join(docsDir, "changelog.md"); 136 if ((await exists(changelog)) && !(await exists(docsChangelog))) { 137 const md = await readFile(changelog, "utf8"); 138 await writeFile(path.join(outDocsDir, "changelog.md"), md, "utf8"); 139 pages.push({ 140 path: "changelog.md", 141 title: normalizeTitle(titleFromMarkdown(md, "changelog.md")), 142 }); 143 } 144 145 const mdFiles = (await exists(docsDir)) ? await listMarkdownFiles(docsDir) : []; 146 147 // Copy all markdown under docs/ (including archives), but only include non-archive 148 // paths in the sidebar manifest. 149 for (const rel of mdFiles) { 150 const src = path.join(docsDir, rel); 151 const dst = path.join(outDocsDir, rel); 152 await mkdir(path.dirname(dst), { recursive: true }); 153 await cp(src, dst); 154 155 const md = await readFile(src, "utf8"); 156 if (!rel.startsWith("archive/")) { 157 pages.push({ path: rel, title: normalizeTitle(titleFromMarkdown(md, rel)) }); 158 } 159 } 160 161 // Copy devlog files to docs/devlog/ and generate an index 162 const devlogFiles = (await exists(devlogDir)) ? await listMarkdownFiles(devlogDir) : []; 163 const devlogEntries = []; 164 165 for (const rel of devlogFiles) { 166 const src = path.join(devlogDir, rel); 167 const dst = path.join(outDocsDir, "devlog", rel); 168 await mkdir(path.dirname(dst), { recursive: true }); 169 await cp(src, dst); 170 171 const md = await readFile(src, "utf8"); 172 devlogEntries.push({ 173 path: `devlog/${rel}`, 174 title: titleFromMarkdown(md, rel), 175 }); 176 } 177 178 // Generate devlog index listing all entries (newest first by filename) 179 if (devlogEntries.length > 0) { 180 devlogEntries.sort((a, b) => b.path.localeCompare(a.path)); 181 const indexMd = [ 182 "# devlog", 183 "", 184 ...devlogEntries.map((e) => `- [${e.title}](${e.path})`), 185 "", 186 ].join("\n"); 187 await writeFile(path.join(outDocsDir, "devlog", "index.md"), indexMd, "utf8"); 188 } 189 190 // Stable nav order: README homepage, then roadmap, then changelog, then the rest. 191 pages.sort((a, b) => { 192 const order = (p) => { 193 if (p === "index.md") return 0; 194 if (p === "roadmap.md") return 1; 195 if (p === "changelog.md") return 2; 196 return 3; 197 }; 198 const ao = order(a.path); 199 const bo = order(b.path); 200 if (ao !== bo) return ao - bo; 201 return a.title.localeCompare(b.title); 202 }); 203 204 await writeFile( 205 path.join(outDir, "manifest.json"), 206 JSON.stringify({ pages }, null, 2) + "\n", 207 "utf8", 208 ); 209 210 process.stdout.write( 211 `Built Wisp docs site: ${pages.length} markdown file(s) -> ${outDir}\n`, 212 ); 213} 214 215await main();