fix: capture typst diagnostics emitted after the status line
Build and Deploy Verso / deploy (push) Has been cancelled

typst watch outputs the "[HH:MM:SS] compiled with errors" status line
FIRST, then the full diagnostic output (file:line:col, source snippets,
hints) AFTERWARDS. The previous code resolved the pending compile
promise as soon as COMPILE_DONE_RE fired, discarding all post-status
diagnostic lines. Those lines then got cleared by the next cycle's
COMPILE_START_RE, so output.log only ever contained the bare status
line — explaining the "zero verbosity" symptom.

Fix: introduce a two-phase buffering model. When COMPILE_DONE_RE fires,
enter "post-done" phase (storing doneResult) and keep accumulating into
currentLines. _finalizeCompile() is called either when the next
COMPILE_START_RE arrives (zero added latency) or after FLUSH_DELAY_MS
(150 ms fallback for the last compile). It concatenates pre-done and
post-done lines before resolving, so output.log now contains the full
diagnostic output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-08 12:25:43 +00:00
parent 7e6c8c30cc
commit b8543c8bb9
+63 -9
View File
@@ -37,6 +37,13 @@ const FILE_ID_EXHAUSTION_RE = /ran out of file ids/i
// Typst uses ~65 IDs per compile; 1000 compiles ≈ 65 000 — safely under 65 535.
const MAX_COMPILES_BEFORE_RESTART = 1000
// typst watch emits the "[HH:MM:SS] compiled with errors" status line FIRST,
// then the full diagnostic output (file:line:col, code snippets) AFTERWARDS.
// We buffer post-done lines and resolve after this delay if no new compile
// cycle starts sooner. 150 ms is well above the ~1 chunk latency for typst's
// diagnostic flush and imperceptible on top of a typical compile time.
const FLUSH_DELAY_MS = 150
// ---------------------------------------------------------------------------
// State (module-level, never exported)
// ---------------------------------------------------------------------------
@@ -53,7 +60,13 @@ const ProcessTable = {}
// compilationCount total successful compile cycles on this watcher
// restartPending flag to restart at the next runTypst call
// accumulator incomplete trailing line from the last data chunk
// currentLines lines accumulated since the last compile-done event
// currentLines lines accumulated in the current phase (pre-done or
// post-done, see _onWatcherData for the two-phase logic)
// doneResult { preLines, compiledWithErrors } held between the
// COMPILE_DONE_RE line and the post-done diagnostic flush;
// null when not in post-done phase
// flushTimeout timeout handle that finalises doneResult when no next
// compile cycle starts within FLUSH_DELAY_MS
// pendingResolvers Array<{resolve, reject, timeoutHandle}>
// pendingResult compile result cached when typst finished before a
// resolver was registered (race-condition safety net)
@@ -142,6 +155,8 @@ async function _startWatcher(compileName, options) {
restartPending: false,
accumulator: '',
currentLines: [],
doneResult: null,
flushTimeout: null,
pendingResolvers: [],
pendingResult: null,
}
@@ -187,6 +202,7 @@ async function _startWatcher(compileName, options) {
function _killWatchEntry(compileName) {
const entry = WatchTable[compileName]
if (!entry) return
clearTimeout(entry.flushTimeout)
delete WatchTable[compileName]
try {
_killedWatchPids.add(entry.proc.pid)
@@ -211,9 +227,15 @@ function _onWatcherData(compileName, chunk) {
for (const line of lines) {
if (COMPILE_START_RE.test(line)) {
// New compile cycle — discard any output left over from a previous
// failed compile that didn't emit a "compiled with errors" footer.
entry.currentLines = []
// A new compile cycle is starting. If we were in the post-done phase
// (collecting diagnostic lines that typst emits AFTER the status line),
// finalise the previous result now — all diagnostics have arrived.
if (entry.doneResult) {
_finalizeCompile(compileName)
}
// Start fresh for the new cycle.
entry.currentLines = [line]
continue
}
entry.currentLines.push(line)
@@ -228,8 +250,6 @@ function _onWatcherData(compileName, chunk) {
if (COMPILE_DONE_RE.test(line)) {
entry.compilationCount++
const stdout = entry.currentLines.join('\n')
entry.currentLines = []
if (entry.compilationCount >= MAX_COMPILES_BEFORE_RESTART) {
logger.info(
@@ -239,14 +259,48 @@ function _onWatcherData(compileName, chunk) {
entry.restartPending = true
}
_resolveAllPending(compileName, {
stdout,
// typst watch outputs the "[HH:MM:SS] compiled with errors" status
// line FIRST, then the full diagnostics (file:line:col, code snippets)
// AFTERWARDS. Enter post-done phase: keep accumulating into currentLines
// and flush after FLUSH_DELAY_MS (or immediately when the next compile
// cycle's COMPILE_START_RE arrives, whichever comes first).
entry.doneResult = {
preLines: entry.currentLines,
compiledWithErrors: /compiled with errors/.test(line),
})
}
entry.currentLines = []
clearTimeout(entry.flushTimeout)
entry.flushTimeout = setTimeout(
() => _finalizeCompile(compileName),
FLUSH_DELAY_MS
)
}
}
}
// Combines the pre-done lines (up to/including the status line) with any
// post-done diagnostic lines and resolves all pending waiters.
function _finalizeCompile(compileName) {
const entry = WatchTable[compileName]
if (!entry || !entry.doneResult) return
clearTimeout(entry.flushTimeout)
entry.flushTimeout = null
const { preLines, compiledWithErrors } = entry.doneResult
entry.doneResult = null
// Merge: status line(s) first, then the post-done diagnostics.
const allLines = preLines.concat(entry.currentLines)
entry.currentLines = []
_resolveAllPending(compileName, {
stdout: allLines.join('\n'),
compiledWithErrors,
})
}
// ---------------------------------------------------------------------------
// Resolver helpers
// ---------------------------------------------------------------------------