(Meet our mascot — a freshly baked T-shaped bread!)
I wanted to use Typst as a fully self-contained, embedded engine inside a Rust application — no network access, no external files at runtime. typst-as-lib is a great library, but for this specific use case, embedding packages took some extra work: manually downloading every package and its transitive dependencies, bundling them with include_dir!, copying them to a temp folder at runtime, and wiring them through the filesystem resolver.
At compile time, typst-bake automatically scans your .typ files for package imports, downloads and resolves all dependencies, compresses everything with zstd, and embeds it all into your binary. At runtime, generating a PDF is just:
let doc = typst_bake::document!("main.typ");
let pdf = doc.to_pdf()?;
Key features
Simple API - Set template-dir and fonts-dir in Cargo.toml, then generate documents with just document!("main.typ").to_pdf()
Multi-Format Output - Generate PDF, SVG, or PNG from the same template
Self-Contained Binary - Templates, fonts, and packages are all embedded into the binary at compile time. No external files or internet connection needed at runtime
Automatic Package Resolution - Just use #import "@preview/..." as in Typst. Packages are resolved automatically using Typst’s own cache and data directories
Runtime Inputs - Pass dynamic data from Rust structs to Typst via IntoValue / IntoDict derive macros
Optimized Binary Size - Embedded resources are deduplicated and compressed automatically
Architecture
Click the diagram for a detailed PDF version.
Examples
Each example is a runnable project with a PDF that explains its own usage in detail:
That looks really cool! I won’t necessarily need this soon, but if I were to use it, this feature would be great (and if you don’t add it yourself I’d PR it once I need it, so no pressure):
The flow chart shows that you skip downloading if you find packages in cache. In principle, that means that you could support non-Universe packages as long as they’re downloaded in advance. You’d just need to look in the local packages folder in addition to the cache folder, and would need to not limit package search to the @preview namespace.
You’re right — currently typst-bake uses its own separate cache path ({cache-dir}/typst-bake/packages/), which means it doesn’t benefit from packages Typst has already downloaded, and there’s no way to use local packages either.
I’m going to align it with Typst’s official package resolution — so it’ll check the data directory first for local packages, then fall back to the shared cache directory, and only download if needed. I’ll also lift the @preview namespace restriction so that any namespace (like @local) works as you’d expect.
This should make the whole experience feel much more consistent with Typst itself. Thanks for nudging me in this direction!
Updated the architecture diagram and implementation to reflect your suggestion — typst-bake now checks local packages first and supports all namespaces. Thanks again!
This looks like a really cool project, thanks! Given how you deep-dived into package resolution here, maybe you are the right person to ask for input for this issue, see also this forum post I made. Maybe you have some ideas there ;)
Thanks for the kind words and for sharing those links!
I’m not really an expert on WASM or browser-side package resolution, but I did some research (with AI assistance) and here’s what I found:
First, good news: packages.typst.org already serves Access-Control-Allow-Origin: *, so a fully static-hosted solution should be feasible.
The overall flow for a browser-based implementation would look something like:
Parse the Typst source to find #import "@namespace/name:version" statements
fetch() the tar.gz from packages.typst.org/{namespace}/{name}-{version}.tar.gz
Decompress in memory (e.g. with pako + an untar library)
Parse typst.toml from the package to resolve transitive dependencies
Recursively repeat for all dependencies
Mount everything to a virtual filesystem and pass it to the Typst WASM compiler
For caching, you could store downloaded packages in IndexedDB to avoid re-fetching on every session.
This is roughly the same package resolution flow that typst-bake does at compile time, but done at runtime in the browser. The typst-bake source (especially the package resolution module) might serve as a reference for the dependency walking logic. Feel free to take a look!