Files
Verso/services/clsi/app/js/TypstRunner.js
T
claudeandClaude Opus 4.8 d67bc77b0e
Build and Deploy Verso / deploy (push) Successful in 7m37s
Add a Typst compiler alongside Quarto and LaTeX
A project whose root file is a .typ file now compiles straight to PDF with
Typst, as a third engine beside Quarto (.qmd) and latexmk (.tex). Dispatch
stays purely extension-based.

CLSI:
- New TypstRunner.js: runs `quarto typst compile <main>.typ output.pdf` (reuses
  the Typst bundled in Quarto, so no extra binary / Docker change). stderr is
  merged into output.log.
- CompileManager: _isTypstFile + a TypstRunner branch in _getRunner, and
  TypstRunner added to the isRunning check and stopCompile kill list.
- RequestParser: 'typst' added to VALID_COMPILERS.

web:
- settings.defaults: 'typ' added to validRootDocExtensions and the text
  extensions (so .typ opens in the editor); 'typst' added to safeCompilers.
- output-files: the Quarto/Typst log parser (which already understands Typst
  `error:`/`warning:` + `┌─ file:line:col` diagnostics) now also handles .typ
  compiles, so their errors/warnings populate the log tabs.

Polish:
- New-project menu: "Blank Typst project" + "Example Typst project" in both the
  main and welcome dropdowns, backed by createBasicProject/createExampleProject
  flavour 'typst', a new mainbasic.typ template and an example-project-typst
  presentation (math, an image, a table, lists).
- Compiler dropdown gains a "Typst" option (cosmetic; dispatch is by extension).

README updated: three compilers side by side, with a Writing-a-Typst-document
section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:56:30 +00:00

105 lines
3.5 KiB
JavaScript

import Path from 'node:path'
import { promisify } from 'node:util'
import logger from '@overleaf/logger'
import CommandRunner from './CommandRunner.js'
import fs from 'node:fs'
// Maps currently-running Typst jobs: compileName → PID (or docker container id)
const ProcessTable = {}
// Compiles a standalone Typst document (.typ) straight to output.pdf. We reuse
// the Typst that ships inside Quarto via `quarto typst compile`, so there is no
// extra binary to install. This is deliberately the simplest of the three
// runners: Typst only ever produces a PDF, so there is no format detection,
// no HTML asset directory and no extension merging (cf. QuartoRunner).
function runTypst(compileName, options, callback) {
const { directory, mainFile, image, environment, compileGroup } = options
const timeout = options.timeout || 60000
logger.debug(
{ directory, timeout, mainFile, compileGroup },
'starting typst compile'
)
const command = _buildTypstCommand(mainFile)
ProcessTable[compileName] = CommandRunner.run(
compileName,
command,
directory,
image,
timeout,
environment || {},
compileGroup,
null,
function (error, output) {
delete ProcessTable[compileName]
// Propagate real process-level errors (killed, timed out) but NOT
// ordinary non-zero exit codes from Typst itself. A compile failure
// (exit code 1) is not a server error — the absence of output.pdf is
// enough for CompileController to return 'failure'.
if (error && (error.terminated || error.timedout)) {
return callback(error)
}
// On exit-code-1 errors LocalCommandRunner attaches stdout to the error
// object; merge it so _writeLogOutput can persist it.
const combined = output || (error ? { stdout: error.stdout || '' } : null)
_writeLogOutput(compileName, directory, combined, () =>
callback(null, combined)
)
}
)
}
function _buildTypstCommand(mainFile) {
// Run through a POSIX shell so stderr (where Typst writes its diagnostics)
// is merged into stdout (2>&1). LocalCommandRunner replaces $COMPILE_DIR
// before the shell sees it; the output path is relative because the shell
// CWD is already the compile directory.
const inputPath = `$COMPILE_DIR/${mainFile}`
const cmd = `quarto typst compile ${inputPath} output.pdf 2>&1`
return ['/bin/sh', '-c', cmd]
}
function _writeLogOutput(compileName, directory, output, callback) {
const content = (output && output.stdout) || ''
if (!content) return callback()
// Write to output.log so the PDF-preview log panel picks it up. Typst's
// `error:`/`warning:` + `┌─ file:line:col` diagnostics are understood by the
// Quarto/Typst log parser on the web side.
const logFile = Path.join(directory, 'output.log')
fs.unlink(logFile, () => {
fs.writeFile(logFile, content, { flag: 'wx' }, err => {
if (err) {
logger.error({ err, compileName, logFile }, 'error writing typst log')
}
callback()
})
})
}
function isRunning(compileName) {
return ProcessTable[compileName] != null
}
function killTypst(compileName, callback) {
logger.debug({ compileName }, 'killing running typst compile')
if (!isRunning(compileName)) {
logger.warn({ compileName }, 'no such compile to kill')
return callback(null)
}
CommandRunner.kill(ProcessTable[compileName], callback)
}
export default {
isRunning,
runTypst,
killTypst,
promises: {
runTypst: promisify(runTypst),
killTypst: promisify(killTypst),
},
}