96 lines
2.4 KiB
TypeScript
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(() => {}));
|
|
}
|
|
}
|