rspace-online/modules/pubs/typst-compile.ts

96 lines
2.4 KiB
TypeScript

/**
* Typst compiler — spawns the Typst CLI to generate PDFs.
*
* Ported from pocket-press/lib/typst.ts.
* Uses Bun.spawn() instead of Node execFile().
*/
import { writeFile, readFile, mkdir, unlink } from "node:fs/promises";
import { join, resolve } from "node:path";
import { randomUUID } from "node:crypto";
import type { ParsedDocument } from "./parse-document";
const TYPST_DIR = resolve(import.meta.dir, "typst");
export interface CompileOptions {
document: ParsedDocument;
formatId: string;
}
export interface CompileResult {
pdf: Buffer;
pageCount: number;
}
export async function compileDocument(options: CompileOptions): Promise<CompileResult> {
const { document, formatId } = options;
const jobId = randomUUID();
const tmpDir = join("/tmp", `rpubs-${jobId}`);
await mkdir(tmpDir, { recursive: true });
const dataPath = join(tmpDir, "data.json");
const driverPath = join(tmpDir, "main.typ");
const outputPath = join(tmpDir, "output.pdf");
try {
// Write the content data as JSON
await writeFile(dataPath, JSON.stringify(document, null, 2));
// Write the Typst driver file that imports format + template
const formatImport = join(TYPST_DIR, "formats", `${formatId}.typ`);
const templateImport = join(TYPST_DIR, "templates", "pocket-book.typ");
const driverContent = `
#import "${formatImport}": page-setup
#show: page-setup
#include "${templateImport}"
`;
await writeFile(driverPath, driverContent);
// Compile with Typst CLI using Bun.spawn
const proc = Bun.spawn(
[
"typst",
"compile",
driverPath,
outputPath,
"--root",
"/",
"--input",
`data-path=${dataPath}`,
"--font-path",
join(TYPST_DIR, "fonts"),
],
{
stdout: "pipe",
stderr: "pipe",
}
);
const exitCode = await proc.exited;
if (exitCode !== 0) {
const stderr = await new Response(proc.stderr).text();
throw new Error(`Typst compilation failed: ${stderr}`);
}
const pdf = Buffer.from(await readFile(outputPath));
// Count pages (rough: look for /Type /Page in PDF)
const pdfStr = pdf.toString("latin1");
const pageCount = (pdfStr.match(/\/Type\s*\/Page[^s]/g) || []).length;
return { pdf, pageCount };
} finally {
// Clean up temp files
await Promise.allSettled([
unlink(dataPath),
unlink(driverPath),
unlink(outputPath),
]).catch(() => {});
// Remove temp dir (best effort)
import("node:fs/promises").then(({ rmdir }) => rmdir(tmpDir).catch(() => {}));
}
}