feat: publish orchestrator wiring transforms to GitClient
Co-Authored-By: Claude
This commit is contained in:
65
src/publish.test.ts
Normal file
65
src/publish.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { expect, test, vi } from "vitest";
|
||||||
|
import { publish } from "./publish";
|
||||||
|
import { DEFAULT_SETTINGS } from "./settings";
|
||||||
|
import type { GitClient, GitFile } from "./git";
|
||||||
|
|
||||||
|
function fakeGit() {
|
||||||
|
const calls: { files: GitFile[]; message?: string } = { files: [] };
|
||||||
|
const git: GitClient = {
|
||||||
|
syncClone: vi.fn(async () => {}),
|
||||||
|
writeFiles: vi.fn(async (files) => { calls.files = files; }),
|
||||||
|
commitAndPush: vi.fn(async (o) => { calls.message = o.message; }),
|
||||||
|
};
|
||||||
|
return { git, calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("writes post + resolved image, rewrites body, reports counts", async () => {
|
||||||
|
const { git, calls } = fakeGit();
|
||||||
|
const result = await publish(
|
||||||
|
{
|
||||||
|
noteText: "---\nlayout: post\n---\n\n![[shot.png]]\nbody",
|
||||||
|
frontmatterPairs: [{ key: "layout", value: "post" }],
|
||||||
|
slug: "my-post",
|
||||||
|
date: "2026-06-19",
|
||||||
|
strategy: "flat-slug",
|
||||||
|
commitMessage: "Publish: My Post",
|
||||||
|
},
|
||||||
|
{ ...DEFAULT_SETTINGS, remoteUrl: "ssh://x/y.git" },
|
||||||
|
git,
|
||||||
|
async () => Buffer.from([9]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.postPath).toBe("_posts/2026-06-19-my-post.md");
|
||||||
|
expect(result.imageCount).toBe(1);
|
||||||
|
expect(result.unresolved).toEqual([]);
|
||||||
|
const post = calls.files.find((f) => f.repoPath === "_posts/2026-06-19-my-post.md")!;
|
||||||
|
expect(post.data).toContain("");
|
||||||
|
expect(calls.files.some((f) => f.repoPath === "assets/img/my-post.png")).toBe(true);
|
||||||
|
expect(calls.message).toBe("Publish: My Post");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unresolved images are reported and left in the body", async () => {
|
||||||
|
const { git } = fakeGit();
|
||||||
|
const result = await publish(
|
||||||
|
{
|
||||||
|
noteText: "![[missing.png]]",
|
||||||
|
frontmatterPairs: [],
|
||||||
|
slug: "p", date: "2026-06-19", strategy: "flat-slug", commitMessage: "m",
|
||||||
|
},
|
||||||
|
{ ...DEFAULT_SETTINGS, remoteUrl: "ssh://x" },
|
||||||
|
git,
|
||||||
|
async () => null,
|
||||||
|
);
|
||||||
|
expect(result.unresolved).toEqual(["missing.png"]);
|
||||||
|
expect(result.imageCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when remoteUrl is empty", async () => {
|
||||||
|
const { git } = fakeGit();
|
||||||
|
await expect(
|
||||||
|
publish(
|
||||||
|
{ noteText: "x", frontmatterPairs: [], slug: "p", date: "2026-06-19", strategy: "flat-slug", commitMessage: "m" },
|
||||||
|
DEFAULT_SETTINGS, git, async () => null,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(/remote/i);
|
||||||
|
});
|
||||||
72
src/publish.ts
Normal file
72
src/publish.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { parseNote, Pair } from "./frontmatter";
|
||||||
|
import { buildPost } from "./buildPost";
|
||||||
|
import { postFilename } from "./slug";
|
||||||
|
import { findImageRefs, planImages, Strategy } from "./images";
|
||||||
|
import { GitClient, GitFile } from "./git";
|
||||||
|
import { JekyllPublishSettings } from "./settings";
|
||||||
|
|
||||||
|
export interface PublishInput {
|
||||||
|
noteText: string;
|
||||||
|
frontmatterPairs: Pair[];
|
||||||
|
slug: string;
|
||||||
|
date: string;
|
||||||
|
strategy: Strategy;
|
||||||
|
commitMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImageResolver = (linktext: string) => Promise<Buffer | null>;
|
||||||
|
|
||||||
|
export interface PublishResult {
|
||||||
|
postPath: string;
|
||||||
|
imageCount: number;
|
||||||
|
unresolved: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publish(
|
||||||
|
input: PublishInput,
|
||||||
|
settings: JekyllPublishSettings,
|
||||||
|
git: GitClient,
|
||||||
|
resolveImage: ImageResolver
|
||||||
|
): Promise<PublishResult> {
|
||||||
|
if (!settings.remoteUrl.trim()) throw new Error("No git remote URL configured");
|
||||||
|
|
||||||
|
const { body } = parseNote(input.noteText);
|
||||||
|
const refs = findImageRefs(body);
|
||||||
|
|
||||||
|
const resolved: { linktext: string; data: Buffer }[] = [];
|
||||||
|
const unresolved: string[] = [];
|
||||||
|
for (const ref of refs) {
|
||||||
|
if (resolved.some((r) => r.linktext === ref.linktext)) continue;
|
||||||
|
const data = await resolveImage(ref.linktext);
|
||||||
|
if (data) resolved.push({ linktext: ref.linktext, data });
|
||||||
|
else unresolved.push(ref.linktext);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usableRefs = refs.filter((r) => resolved.some((x) => x.linktext === r.linktext));
|
||||||
|
const { rewrittenBody, plan } = planImages(usableRefs, {
|
||||||
|
slug: input.slug,
|
||||||
|
strategy: input.strategy,
|
||||||
|
imagesDir: settings.imagesDir,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const postText = buildPost({ frontmatterPairs: input.frontmatterPairs, body: rewrittenBody });
|
||||||
|
const postPath = `${settings.postsDir}/${postFilename({ date: input.date, slug: input.slug })}`;
|
||||||
|
|
||||||
|
const files: GitFile[] = [{ repoPath: postPath, data: postText }];
|
||||||
|
for (const item of plan) {
|
||||||
|
const r = resolved.find((x) => x.linktext === item.linktext)!;
|
||||||
|
files.push({ repoPath: item.repoPath, data: r.data });
|
||||||
|
}
|
||||||
|
|
||||||
|
await git.syncClone({ url: settings.remoteUrl, branch: settings.branch });
|
||||||
|
await git.writeFiles(files);
|
||||||
|
await git.commitAndPush({
|
||||||
|
message: input.commitMessage,
|
||||||
|
branch: settings.branch,
|
||||||
|
authorName: settings.authorName || undefined,
|
||||||
|
authorEmail: settings.authorEmail || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { postPath, imageCount: plan.length, unresolved };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user