@uniweb/unipress 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ // Typst → PDF sink.
2
+ //
3
+ // The foundation's compileSubtree returns a Typst source bundle (a zip
4
+ // of main.typ, preamble.typ, template.typ, meta.typ, content.typ, and
5
+ // assets/). The `typst` CLI has no mode that reads a zip directly, so we
6
+ // extract into a temp dir, spawn `typst compile <tempdir>/main.typ
7
+ // <outPath>`, and clean up.
8
+ //
9
+ // On success: temp dir is removed. On failure: temp dir is kept iff
10
+ // keepTemp — so the user can open main.typ and reproduce the typst
11
+ // error themselves. Path is surfaced in the thrown error so they know
12
+ // where to look.
13
+
14
+ import { spawn } from 'node:child_process'
15
+ import { mkdtemp, mkdir, rm, writeFile, stat } from 'node:fs/promises'
16
+ import { tmpdir } from 'node:os'
17
+ import { dirname, join } from 'node:path'
18
+ import { TypstBinaryError, OutputWriteError } from '../errors.js'
19
+ import { resolveTypstBinary } from '../typst/binary-manager.js'
20
+
21
+ export async function writePdfViaTypst(blob, outPath, { typstBinaryPath = null, typstVersion = null, keepTemp = false, onProgress = () => {} } = {}) {
22
+ const bytes = Buffer.from(await blob.arrayBuffer())
23
+
24
+ const workDir = await mkdtemp(join(tmpdir(), 'unipress-typst-'))
25
+ onProgress(`temp dir: ${workDir}`)
26
+
27
+ let succeeded = false
28
+ try {
29
+ const archivePath = join(workDir, 'source.zip')
30
+ await writeFile(archivePath, bytes)
31
+
32
+ onProgress('extracting source bundle...')
33
+ await runCapture('tar', ['-xf', archivePath, '-C', workDir])
34
+ await rm(archivePath, { force: true })
35
+
36
+ const mainTyp = join(workDir, 'main.typ')
37
+ const mainStat = await statOrNull(mainTyp)
38
+ if (!mainStat) {
39
+ throw new TypstBinaryError(
40
+ `typst source bundle has no main.typ — extracted to ${workDir}`
41
+ )
42
+ }
43
+
44
+ const binary = await resolveTypstBinary({
45
+ overridePath: typstBinaryPath,
46
+ ...(typstVersion ? { version: typstVersion } : {}),
47
+ onProgress
48
+ })
49
+
50
+ await mkdir(dirname(outPath), { recursive: true })
51
+
52
+ onProgress(`running typst compile...`)
53
+ await runCapture(binary, ['compile', mainTyp, outPath], {
54
+ onFail: (code, stderr) => new TypstBinaryError(
55
+ `typst exited with code ${code}${keepTemp ? ` (temp dir kept at ${workDir})` : ''}\n` +
56
+ (stderr ? `--- typst stderr ---\n${stderr.trim()}\n` : '') +
57
+ (keepTemp ? '' : 'hint: pass --keep-temp to inspect the source bundle\n')
58
+ )
59
+ })
60
+
61
+ const outStat = await statOrNull(outPath)
62
+ if (!outStat) {
63
+ throw new OutputWriteError(
64
+ `typst compile succeeded but no file at ${outPath}`
65
+ )
66
+ }
67
+ succeeded = true
68
+ onProgress(` wrote ${outStat.size} bytes`)
69
+ return { outPath, bytes: outStat.size }
70
+ } finally {
71
+ if (succeeded || !keepTemp) {
72
+ await rm(workDir, { recursive: true, force: true })
73
+ }
74
+ }
75
+ }
76
+
77
+ async function statOrNull(path) {
78
+ try {
79
+ return await stat(path)
80
+ } catch {
81
+ return null
82
+ }
83
+ }
84
+
85
+ function runCapture(cmd, args, { onFail } = {}) {
86
+ return new Promise((resolvePromise, reject) => {
87
+ const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] })
88
+ let stdout = ''
89
+ let stderr = ''
90
+ proc.stdout.on('data', c => { stdout += c.toString() })
91
+ proc.stderr.on('data', c => { stderr += c.toString() })
92
+ proc.on('error', err => reject(err))
93
+ proc.on('close', code => {
94
+ if (code === 0) resolvePromise({ stdout, stderr })
95
+ else {
96
+ if (onFail) reject(onFail(code, stderr))
97
+ else reject(new Error(`\`${cmd}\` exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`))
98
+ }
99
+ })
100
+ })
101
+ }