/** * 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 { 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(() => {})); } }