@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.
- package/CHANGELOG.md +137 -0
- package/LICENSE +201 -0
- package/README.md +213 -0
- package/RELEASING.md +101 -0
- package/docs/parity-report.md +153 -0
- package/docs/templates/book.md +55 -0
- package/docs/templates/data-report.md +85 -0
- package/docs/templates/directory.md +73 -0
- package/docs/templates/monograph.md +50 -0
- package/docs/templates/report.md +55 -0
- package/docs/troubleshooting.md +159 -0
- package/package.json +62 -0
- package/src/catalog.js +23 -0
- package/src/cli.js +193 -0
- package/src/commands/compile.js +37 -0
- package/src/commands/create.js +165 -0
- package/src/commands/inspect.js +182 -0
- package/src/compile.js +165 -0
- package/src/config.js +128 -0
- package/src/content-loader.js +56 -0
- package/src/document-yml.js +20 -0
- package/src/errors.js +78 -0
- package/src/foundation-fetch.js +202 -0
- package/src/foundation-loader.js +229 -0
- package/src/foundations-data.js +122 -0
- package/src/index.js +8 -0
- package/src/orchestrator.js +150 -0
- package/src/scaffold.js +66 -0
- package/src/sinks/blob.js +25 -0
- package/src/sinks/typst.js +101 -0
- package/src/templates-data.js +63 -0
- package/src/typst/binary-manager.js +228 -0
- package/src/typst/versions.js +65 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Resolve a usable Typst binary path — either a caller-supplied override,
|
|
2
|
+
// a previously cached download, or a fresh fetch from typst/typst's
|
|
3
|
+
// GitHub releases.
|
|
4
|
+
//
|
|
5
|
+
// Contract: `resolveTypstBinary()` returns an absolute path to an
|
|
6
|
+
// executable `typst` (`typst.exe` on Windows). It does NOT spawn `typst
|
|
7
|
+
// compile` — that's the sink's job. It DOES spawn `<path> --version`
|
|
8
|
+
// as a sanity check: if the binary won't run, we'd rather fail here with
|
|
9
|
+
// a clear error than later with a spawn-from-sink error.
|
|
10
|
+
//
|
|
11
|
+
// Cache layout (per plan §12.3):
|
|
12
|
+
// $UNIPRESS_CACHE_DIR/typst/<version>/typst
|
|
13
|
+
// or the platform-default cache dir when $UNIPRESS_CACHE_DIR is unset.
|
|
14
|
+
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
|
+
import { spawn } from 'node:child_process'
|
|
17
|
+
import { mkdir, rm, rename, readdir, chmod, stat } from 'node:fs/promises'
|
|
18
|
+
import { createWriteStream, existsSync } from 'node:fs'
|
|
19
|
+
import { join, dirname, resolve, isAbsolute } from 'node:path'
|
|
20
|
+
import { homedir, tmpdir } from 'node:os'
|
|
21
|
+
import { Readable } from 'node:stream'
|
|
22
|
+
import { pipeline } from 'node:stream/promises'
|
|
23
|
+
import {
|
|
24
|
+
TYPST_VERSION,
|
|
25
|
+
detectPlatform,
|
|
26
|
+
buildReleaseUrl,
|
|
27
|
+
getChecksum
|
|
28
|
+
} from './versions.js'
|
|
29
|
+
import { TypstBinaryError } from '../errors.js'
|
|
30
|
+
|
|
31
|
+
const EXE = process.platform === 'win32' ? 'typst.exe' : 'typst'
|
|
32
|
+
|
|
33
|
+
// Platform-default cache directory.
|
|
34
|
+
// UNIPRESS_CACHE_DIR .......... absolute path, wins over everything.
|
|
35
|
+
// XDG_CACHE_HOME/unipress ..... Linux + anyone who set it.
|
|
36
|
+
// ~/Library/Caches/unipress ... macOS default.
|
|
37
|
+
// %LOCALAPPDATA%/unipress ..... Windows default.
|
|
38
|
+
// ~/.cache/unipress ........... fallback.
|
|
39
|
+
export function getCacheDir() {
|
|
40
|
+
const override = process.env.UNIPRESS_CACHE_DIR
|
|
41
|
+
if (override) {
|
|
42
|
+
if (!isAbsolute(override)) {
|
|
43
|
+
throw new TypstBinaryError(
|
|
44
|
+
`UNIPRESS_CACHE_DIR must be an absolute path (got '${override}')`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
return override
|
|
48
|
+
}
|
|
49
|
+
const xdg = process.env.XDG_CACHE_HOME
|
|
50
|
+
if (xdg) return join(xdg, 'unipress')
|
|
51
|
+
if (process.platform === 'darwin') {
|
|
52
|
+
return join(homedir(), 'Library', 'Caches', 'unipress')
|
|
53
|
+
}
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
const local = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local')
|
|
56
|
+
return join(local, 'unipress', 'Cache')
|
|
57
|
+
}
|
|
58
|
+
return join(homedir(), '.cache', 'unipress')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getBinaryPath(version = TYPST_VERSION) {
|
|
62
|
+
return join(getCacheDir(), 'typst', version, EXE)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Main entry point. Returns an absolute path to a working typst binary.
|
|
66
|
+
export async function resolveTypstBinary({ overridePath = null, version = TYPST_VERSION, onProgress = () => {} } = {}) {
|
|
67
|
+
if (overridePath) {
|
|
68
|
+
const absolute = resolve(overridePath)
|
|
69
|
+
if (!existsSync(absolute)) {
|
|
70
|
+
throw new TypstBinaryError(
|
|
71
|
+
`--typst-binary points at a file that does not exist: ${absolute}`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
await sanityCheck(absolute, { label: 'override' })
|
|
75
|
+
onProgress(`using typst binary (override): ${absolute}`)
|
|
76
|
+
return absolute
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cached = getBinaryPath(version)
|
|
80
|
+
if (existsSync(cached)) {
|
|
81
|
+
await sanityCheck(cached, { label: 'cache' })
|
|
82
|
+
onProgress(`using cached typst ${version}: ${cached}`)
|
|
83
|
+
return cached
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return downloadAndInstall({ version, onProgress })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function downloadAndInstall({ version, onProgress }) {
|
|
90
|
+
const platform = detectPlatform()
|
|
91
|
+
if (!platform.triple) {
|
|
92
|
+
throw new TypstBinaryError(
|
|
93
|
+
`no prebuilt typst binary available for ${platform.key}\n` +
|
|
94
|
+
`hint: install typst yourself and pass --typst-binary <path>, or\n` +
|
|
95
|
+
` set the UNIPRESS_CACHE_DIR so unipress can find an existing binary`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const checksum = getChecksum(platform.triple)
|
|
100
|
+
if (!checksum) {
|
|
101
|
+
throw new TypstBinaryError(
|
|
102
|
+
`no pinned SHA-256 for triple '${platform.triple}' at version ${version}\n` +
|
|
103
|
+
`hint: src/typst/versions.js is out of sync — see the bump procedure comment`
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const url = buildReleaseUrl({ version, triple: platform.triple, ext: platform.ext })
|
|
108
|
+
const destDir = join(getCacheDir(), 'typst', version)
|
|
109
|
+
const finalPath = join(destDir, EXE)
|
|
110
|
+
|
|
111
|
+
onProgress(`downloading typst ${version} for ${platform.triple}...`)
|
|
112
|
+
await mkdir(destDir, { recursive: true })
|
|
113
|
+
|
|
114
|
+
const archivePath = join(destDir, `typst-${platform.triple}.${platform.ext}`)
|
|
115
|
+
await fetchTo(url, archivePath)
|
|
116
|
+
|
|
117
|
+
onProgress('verifying checksum...')
|
|
118
|
+
const actual = await sha256OfFile(archivePath)
|
|
119
|
+
if (actual !== checksum) {
|
|
120
|
+
await rm(archivePath, { force: true })
|
|
121
|
+
throw new TypstBinaryError(
|
|
122
|
+
`sha256 mismatch for typst ${version} (${platform.triple})\n` +
|
|
123
|
+
` expected: ${checksum}\n` +
|
|
124
|
+
` actual: ${actual}\n` +
|
|
125
|
+
`hint: the download may be corrupt or the pinned checksum is stale — retry, or report this`
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onProgress('extracting...')
|
|
130
|
+
await extractArchive(archivePath, destDir)
|
|
131
|
+
await rm(archivePath, { force: true })
|
|
132
|
+
|
|
133
|
+
// Archive extracts into a typst-<triple>/ subdirectory containing the
|
|
134
|
+
// binary. Move the binary up one level and remove the leftover dir.
|
|
135
|
+
await liftBinary(destDir, platform.triple, finalPath)
|
|
136
|
+
|
|
137
|
+
if (process.platform !== 'win32') {
|
|
138
|
+
await chmod(finalPath, 0o755)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await sanityCheck(finalPath, { label: 'downloaded' })
|
|
142
|
+
onProgress(`installed typst ${version} at ${finalPath}`)
|
|
143
|
+
return finalPath
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function fetchTo(url, destPath) {
|
|
147
|
+
let response
|
|
148
|
+
try {
|
|
149
|
+
response = await fetch(url, { redirect: 'follow' })
|
|
150
|
+
} catch (err) {
|
|
151
|
+
throw new TypstBinaryError(
|
|
152
|
+
`failed to fetch ${url}\n` +
|
|
153
|
+
`cause: ${err.message}\n` +
|
|
154
|
+
`hint: check network access, or pre-populate the cache and retry`
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
if (!response.ok || !response.body) {
|
|
158
|
+
throw new TypstBinaryError(
|
|
159
|
+
`fetch failed: ${url}\n` +
|
|
160
|
+
`HTTP ${response.status} ${response.statusText}`
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(destPath))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function sha256OfFile(path) {
|
|
167
|
+
const { createReadStream } = await import('node:fs')
|
|
168
|
+
const hash = createHash('sha256')
|
|
169
|
+
await pipeline(createReadStream(path), hash)
|
|
170
|
+
return hash.digest('hex')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function extractArchive(archivePath, destDir) {
|
|
174
|
+
// `tar -xf` handles both tar.xz (via libarchive/GNU tar+xz) and .zip
|
|
175
|
+
// on macOS, Linux, and modern Windows (ships bsdtar since 2017).
|
|
176
|
+
await run('tar', ['-xf', archivePath, '-C', destDir], {
|
|
177
|
+
onError: (err) => new TypstBinaryError(
|
|
178
|
+
`failed to extract ${archivePath}\n` +
|
|
179
|
+
`cause: ${err.message}\n` +
|
|
180
|
+
`hint: is \`tar\` on PATH? (macOS/Linux ship it; Windows 10+ does too)`
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function liftBinary(destDir, triple, finalPath) {
|
|
186
|
+
const entries = await readdir(destDir, { withFileTypes: true })
|
|
187
|
+
const sub = entries.find(e => e.isDirectory() && e.name.startsWith('typst-'))
|
|
188
|
+
if (!sub) {
|
|
189
|
+
throw new TypstBinaryError(
|
|
190
|
+
`extracted archive has no typst-<triple>/ directory under ${destDir}`
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
const extractedBinary = join(destDir, sub.name, EXE)
|
|
194
|
+
if (!existsSync(extractedBinary)) {
|
|
195
|
+
throw new TypstBinaryError(
|
|
196
|
+
`expected ${extractedBinary} after extraction, but it is missing`
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
await rename(extractedBinary, finalPath)
|
|
200
|
+
await rm(join(destDir, sub.name), { recursive: true, force: true })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function sanityCheck(binaryPath, { label }) {
|
|
204
|
+
try {
|
|
205
|
+
await run(binaryPath, ['--version'])
|
|
206
|
+
} catch (err) {
|
|
207
|
+
throw new TypstBinaryError(
|
|
208
|
+
`typst ${label} binary at ${binaryPath} failed \`--version\`\n` +
|
|
209
|
+
`cause: ${err.message}`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function run(cmd, args, { onError } = {}) {
|
|
215
|
+
return new Promise((resolveProm, reject) => {
|
|
216
|
+
const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
217
|
+
let stderr = ''
|
|
218
|
+
proc.stderr.on('data', chunk => { stderr += chunk.toString() })
|
|
219
|
+
proc.on('error', err => reject(onError ? onError(err) : err))
|
|
220
|
+
proc.on('close', code => {
|
|
221
|
+
if (code === 0) resolveProm()
|
|
222
|
+
else {
|
|
223
|
+
const err = new Error(`\`${cmd}\` exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`)
|
|
224
|
+
reject(onError ? onError(err) : err)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Pinned Typst version + per-platform download metadata.
|
|
2
|
+
//
|
|
3
|
+
// Typst does NOT publish .sha256 sidecar files on its GitHub Releases
|
|
4
|
+
// (checked across 0.11-0.14). The plan originally assumed otherwise.
|
|
5
|
+
// Instead, GitHub's own release-assets API surfaces asset.digest for
|
|
6
|
+
// each uploaded artifact; we snapshot those SHA-256 digests here so the
|
|
7
|
+
// binary manager can verify downloads without a second fetch.
|
|
8
|
+
//
|
|
9
|
+
// --- Bumping to a new Typst version ---
|
|
10
|
+
// 1. Pick the new version number; update TYPST_VERSION below.
|
|
11
|
+
// 2. Fetch the release asset digests from GitHub:
|
|
12
|
+
// curl -sSL https://api.github.com/repos/typst/typst/releases/tags/v<VERSION> \
|
|
13
|
+
// | python3 -c "import json,sys; d=json.load(sys.stdin); \
|
|
14
|
+
// [print(a['name'], a['digest']) for a in d['assets']]"
|
|
15
|
+
// 3. Copy the per-platform digests into CHECKSUMS below. The asset names
|
|
16
|
+
// in GitHub's feed match the `{triple}.{ext}` suffixes here.
|
|
17
|
+
// 4. Smoke-test on at least one platform: delete the local cache
|
|
18
|
+
// ($UNIPRESS_CACHE_DIR/typst/<VERSION>) and run a compile that
|
|
19
|
+
// exercises the binary manager.
|
|
20
|
+
//
|
|
21
|
+
// Verified 2026-04-24 against
|
|
22
|
+
// https://api.github.com/repos/typst/typst/releases/tags/v0.14.2
|
|
23
|
+
|
|
24
|
+
export const TYPST_VERSION = '0.14.2'
|
|
25
|
+
|
|
26
|
+
// Maps (process.platform, process.arch) → { triple, ext }.
|
|
27
|
+
// Only the platforms we distribute unipress for. riscv64 / armv7 / etc.
|
|
28
|
+
// are technically downloadable from typst/typst but we don't test them,
|
|
29
|
+
// so we surface a clear "unsupported platform" error rather than trying
|
|
30
|
+
// to fetch something we can't smoke-test.
|
|
31
|
+
export const PLATFORM_TRIPLES = {
|
|
32
|
+
'darwin-arm64': { triple: 'aarch64-apple-darwin', ext: 'tar.xz' },
|
|
33
|
+
'darwin-x64': { triple: 'x86_64-apple-darwin', ext: 'tar.xz' },
|
|
34
|
+
'linux-x64': { triple: 'x86_64-unknown-linux-musl', ext: 'tar.xz' },
|
|
35
|
+
'linux-arm64': { triple: 'aarch64-unknown-linux-musl', ext: 'tar.xz' },
|
|
36
|
+
'win32-x64': { triple: 'x86_64-pc-windows-msvc', ext: 'zip' },
|
|
37
|
+
'win32-arm64': { triple: 'aarch64-pc-windows-msvc', ext: 'zip' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// SHA-256 digests of each release archive. Keyed by triple so the
|
|
41
|
+
// binary manager can look up the expected hash after it's picked a
|
|
42
|
+
// triple for the host.
|
|
43
|
+
export const CHECKSUMS = {
|
|
44
|
+
'aarch64-apple-darwin': '470aa49a2298d20b65c119a10e4ff8808550453e0cb4d85625b89caf0cedf048',
|
|
45
|
+
'x86_64-apple-darwin': '4e91d8e1e33ab164f949c5762e01ee3faa585c8615a2a6bd5e3677fa8506b249',
|
|
46
|
+
'x86_64-unknown-linux-musl': 'a6044cbad2a954deb921167e257e120ac0a16b20339ec01121194ff9d394996d',
|
|
47
|
+
'aarch64-unknown-linux-musl': '491b101aa40a3a7ea82a3f8a6232cabb4e6a7e233810082e5ac812d43fdcd47a',
|
|
48
|
+
'x86_64-pc-windows-msvc': '51353994ac83218c3497052e89b2c432c53b9d4439cdc1b361e2ea4798ebfc13',
|
|
49
|
+
'aarch64-pc-windows-msvc': '1c4aaa0de000ab1787dda354c34f4fa1fe3c2525d3d038e692a3d7daa333d551'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function detectPlatform() {
|
|
53
|
+
const key = `${process.platform}-${process.arch}`
|
|
54
|
+
const entry = PLATFORM_TRIPLES[key]
|
|
55
|
+
if (!entry) return { key, triple: null, ext: null }
|
|
56
|
+
return { key, ...entry }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildReleaseUrl({ version = TYPST_VERSION, triple, ext } = {}) {
|
|
60
|
+
return `https://github.com/typst/typst/releases/download/v${version}/typst-${triple}.${ext}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getChecksum(triple) {
|
|
64
|
+
return CHECKSUMS[triple] ?? null
|
|
65
|
+
}
|