import { expect, test } from "bun:test"; import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { build, forget, globals, guard, jsonScript, type Output, type Site, type Spec } from "./build.ts"; /* SITE */ const site = (config: Site["config"] = {}) => ({ config: { title: "Demo", root: "https://demo.test", llms: { about: "A demo tree.", links: [ { href: "/raw/README.md", name: "README", note: "the readme" }, { href: "/git/", name: "Code", note: "the tree" }, { href: "/faq/", name: "FAQ" }, { href: "/nowhere/", name: "Nowhere" }, ], }, ...config, }, routes: [ { route: "/", name: "Home", at: "2026-01-01" }, { route: "/404.html", kind: "missing", hidden: true }, { route: "/cart/", kind: "cart", hidden: true }, { route: "/faq/", kind: "page", name: "FAQ", at: "2026-04-04" }, { route: "/git/", kind: "gitdir", name: "demo", sitemap: true, at: "2026-02-02" }, { route: "/git/README.md", kind: "gitfile", name: "README.md", hidden: true, sitemap: true, at: "2026-03-03", urls: [{ route: "/git/README.md" }, { route: "/raw/README.md" }], }, ], copies: [], }) as unknown as Site; const made = async (one = site(), spec = {} as unknown as Spec) => { const out = await globals(one, spec); const find = (path: string) => out.find((item: Output) => item.path === path)?.bytes as string | undefined; return { out, sitemap: find("sitemap.xml")!, robots: find("robots.txt")!, llms: find("llms.txt") }; }; /* SITEMAP */ test("the sitemap carries every shown route and every raw path with its own date and no hidden one", async () => { const { sitemap } = await made(); expect(sitemap).toContain("https://demo.test/2026-01-01"); expect(sitemap).toContain("https://demo.test/faq/2026-04-04"); expect(sitemap).toContain("https://demo.test/git/2026-02-02"); expect(sitemap).toContain("https://demo.test/git/README.md2026-03-03"); expect(sitemap).toContain("https://demo.test/raw/README.md2026-03-03"); expect(sitemap).not.toContain("404.html"); expect(sitemap).not.toContain("/cart/"); expect(sitemap.match(//g)!.length).toBe(5); }); /* ROBOTS */ test("robots names the crawlers it welcomes and points at the sitemap", async () => { const { robots } = await made(); for (const agent of ["GPTBot", "ClaudeBot", "Claude-Web", "CCBot", "Google-Extended", "anthropic-ai", "PerplexityBot"]) { expect(robots).toContain(`User-agent: ${agent}\nAllow: /`); } expect(robots).toContain("User-agent: *\nAllow: /"); expect(robots).toContain("Sitemap: https://demo.test/sitemap.xml"); }); /* LLMS */ test("llms.txt says what the site is and links only what the site publishes", async () => { const { llms } = await made(); expect(llms).toContain("# Demo"); expect(llms).toContain("A demo tree."); expect(llms).toContain("- [README](https://demo.test/raw/README.md): the readme"); expect(llms).toContain("- [Code](https://demo.test/git/): the tree"); expect(llms).toContain("- [FAQ](https://demo.test/faq/)"); expect(llms).not.toContain("Nowhere"); }); test("no llms block in site.json means no llms.txt", async () => { const { llms } = await made(site({ llms: undefined })); expect(llms).toBeUndefined(); }); /* JSON-LD */ test("a headline that closes a script tag cannot close the ld+json block", () => { const out = jsonScript({ headline: "" }); expect(out).toBe(''); }); /* ICONS */ test("icons come through the spec: a glyph grid draws five files and no grid draws none", async () => { const rows = ["10101", "01010", "10101", "01010", "10101"]; const { out } = await made(site(), { icons: { rows, svg: "" } } as unknown as Spec); const paths = out.map((one) => one.path); for (const path of ["favicon.svg", "favicon.png", "apple-touch-icon.png", "icon-192.png", "icon-512.png"]) expect(paths).toContain(path); expect(out.find((one) => one.path === "favicon.svg")!.bytes).toBe(""); const png = out.find((one) => one.path === "favicon.png")!.bytes as Uint8Array; expect([...png.subarray(0, 8)]).toEqual([137, 80, 78, 71, 13, 10, 26, 10]); expect((await made()).out.map((one) => one.path)).not.toContain("favicon.png"); }); /* GUARD */ test("the guard passes a listed inline script, json data and a raw mirror, and throws on an unlisted one", () => { const known = new Set(["const a=1"]); expect(() => guard("index.html", "", known)).not.toThrow(); expect(() => guard("index.html", '', known)).not.toThrow(); expect(() => guard("index.html", '', known)).not.toThrow(); expect(() => guard("raw/site/demos/index.html", "", known)).not.toThrow(); expect(() => guard("about/index.html", "", known)).toThrow(/boot list/); }); /* BUILD */ const fresh = (name: string) => { const home = join(tmpdir(), `kitssg-${name}-${process.pid}`); rmSync(home, { recursive: true, force: true }); mkdirSync(home, { recursive: true }); return home; }; test("prepare runs before the scan reads a bundle", async () => { const home = fresh("prepare"); const out = join(home, "dist"); const done = await build({ root: home, out, config: { assets: [{ path: "ui", out: "ui", hash: true, files: ["a.css"] }] }, prepare: () => { mkdirSync(join(home, "ui"), { recursive: true }); writeFileSync(join(home, "ui", "a.css"), "b{}"); }, collect: () => ({ routes: [] }), render: () => [], }); expect(done.site.styles).toEqual([done.site.asset("a.css")]); expect(existsSync(join(out, done.site.asset("a.css")))).toBe(true); rmSync(home, { recursive: true, force: true }); }); test("a second build drops a dead route's outputs, prunes their empty folders and sweeps a stale asset", async () => { const home = fresh("sweep"); const out = join(home, "dist"); mkdirSync(join(home, "ui"), { recursive: true }); writeFileSync(join(home, "ui", "a.css"), "b{}"); const spec = (routes: string[]): Spec => ({ root: home, out, config: { assets: [{ path: "ui", out: "ui", hash: true }] }, collect: () => ({ routes: routes.map((route) => ({ route })) }), render: (_site, route) => [{ path: `${route.route.slice(1)}index.html`, bytes: "

x

" }], }); const first = await build(spec(["/deep/page/"]), { manifest: "manifest.json" }); const was = first.site.asset("a.css"); expect(existsSync(join(out, "deep/page/index.html"))).toBe(true); writeFileSync(join(home, "ui", "a.css"), "c{}"); forget(); const second = await build(spec([]), { manifest: "manifest.json" }); expect(existsSync(join(out, was))).toBe(false); expect(existsSync(join(out, second.site.asset("a.css")))).toBe(true); expect(existsSync(join(out, "deep"))).toBe(false); expect(second.removed).toBe(2); rmSync(home, { recursive: true, force: true }); }); /* GIT OFF */ test("a spec with no git hooks still collects the repo and renders none of it", async () => { const home = fresh("gitoff"); const out = join(home, "dist"); writeFileSync(join(home, "README.md"), "# off\n"); const done = await build({ root: home, out, config: { git: { root: "." } }, collect: () => ({ routes: [] }), render: () => [] }); expect(done.site.routes.map((one) => one.route).sort()).toEqual(["/git/", "/git/README.md"]); expect(Object.keys(done.manifest)).toEqual([]); expect(existsSync(join(out, "git/README.md"))).toBe(false); expect(existsSync(join(out, "raw/README.md"))).toBe(false); rmSync(home, { recursive: true, force: true }); });