v0.2.0 · 1e731ad

No prose in this file.

import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
import { publish } from "./publish.js";

let root: string;

function git(...args: string[]): string {
  return execFileSync("git", args, { cwd: root, encoding: "utf-8" }).trim();
}

/** The files on a branch, and one file's text. */
const onBranch = (ref = "prose") => git("ls-tree", "-r", "--name-only", ref).split("\n");
const show = (path: string, ref = "prose") => git("show", `${ref}:${path}`);

beforeAll(() => {
  root = mkdtempSync(join(tmpdir(), "prose-publish-repo-"));
  const files: Record<string, string> = {
    ".gitignore": ".prose/\n",
    "README.md": "# Demo\n",
    "src/a.ts": "/** @prose\n * Does a.\n */\nexport const a = 1;\n",
  };
  for (const [path, text] of Object.entries(files)) {
    mkdirSync(join(root, path, ".."), { recursive: true });
    writeFileSync(join(root, path), text);
  }
  git("init", "-q", "-b", "main");
  git("config", "user.name", "t");
  git("config", "user.email", "t@t");
  git("add", "-A");
  git("commit", "-q", "-m", "first");
});
afterAll(() => rmSync(root, { recursive: true, force: true }));

describe("publish", () => {
  it("commits the site to an orphan branch, leaving the checkout alone", async () => {
    writeFileSync(join(root, "scratch.ts"), "export const s = 1;\n");
    const head = git("rev-parse", "HEAD");
    const status = git("status", "--porcelain");
    const done = await publish(root);
    expect(done).toMatchObject({ branch: "prose", changed: true, domain: null });
    expect(git("rev-parse", "prose")).toBe(done.commit);
    expect(git("rev-list", "--count", "prose")).toBe("1");
    expect(onBranch().sort()).toEqual(
      [
        ".gitignore.html",
        ".nojekyll",
        "404.html",
        "README.md.html",
        "index.html",
        "src/a.ts.html",
        "src/index.html",
      ].sort(),
    );
    expect(show("src/a.ts.html")).toContain("Does a.");
    expect(git("log", "-1", "--format=%B", "prose")).toContain(`Built from ${head}.`);
    // The checkout: same branch, same commit, same changes, and the index untouched.
    expect(git("symbolic-ref", "--short", "HEAD")).toBe("main");
    expect(git("rev-parse", "HEAD")).toBe(head);
    expect(git("status", "--porcelain")).toBe(status);
    rmSync(join(root, "scratch.ts"));
  });

  it("makes no commit when nothing changed, and one on top when something did", async () => {
    const before = git("rev-parse", "prose");
    const same = await publish(root);
    expect(same).toMatchObject({ changed: false, commit: before });
    writeFileSync(
      join(root, "src/a.ts"),
      "/** @prose\n * Does a, now.\n */\nexport const a = 2;\n",
    );
    git("commit", "-q", "-am", "second");
    const next = await publish(root);
    expect(next.changed).toBe(true);
    expect(git("rev-parse", "prose^")).toBe(before);
    expect(git("diff", "--name-only", "prose^", "prose").split("\n")).toContain("src/a.ts.html");
  });

  it("writes a domain once and carries it forward, until it's replaced or dropped", async () => {
    expect((await publish(root, { domain: "docs.example.com" })).domain).toBe("docs.example.com");
    expect(show("CNAME")).toBe("docs.example.com");
    writeFileSync(join(root, "README.md"), "# Demo, again\n");
    git("commit", "-q", "-am", "third");
    expect((await publish(root)).domain).toBe("docs.example.com");
    expect(show("CNAME")).toBe("docs.example.com");
    await publish(root, { domain: "read.example.com" });
    expect(show("CNAME")).toBe("read.example.com");
    await publish(root, { domain: null });
    expect(onBranch()).not.toContain("CNAME");
  });

  it("publishes to another branch, and refuses a checked-out branch or a bad domain", async () => {
    await publish(root, { branch: "gh-pages" });
    expect(onBranch("gh-pages")).toContain("index.html");
    await expect(publish(root, { branch: "main" })).rejects.toThrow("is checked out");
    await expect(publish(root, { branch: "a..b" })).rejects.toThrow("valid branch name");
    await expect(publish(root, { domain: "https://x.com/" })).rejects.toThrow("isn't a domain");
  });
});