Command-line tool · Rust

Keep your Typst package imports current.

typst-updater scans a project for @preview package imports, checks them against the Typst Universe package index, shows you which ones are out of date and rewrites just the version numbers once you say yes.

~/my-thesis
$ typst-updater
Scanning 2 Typst file(s) in .
Fetching package index from https://packages.typst.org/preview/index.json

Available updates:

  chapters/ch1.typ
       L4  cetz       0.1.0 → 0.10.12  breaking

  main.typ
       L1  cetz       0.1.0 → 0.10.12  breaking
       L3  tablex     0.0.1 → 0.0.9  breaking

Apply 3 update(s)? [y/N] y

  ✔ chapters/ch1.typ
      line 4: @preview/cetz 0.1.0 → 0.10.12

  ✔ main.typ
      line 1: @preview/cetz 0.1.0 → 0.10.12
      line 3: @preview/tablex 0.0.1 → 0.0.9

Updated 3 import(s) across 2 file(s).

Why it exists

Versions are pinned

Typst package imports always name an exact version, such as "@preview/cetz:0.1.0". Nothing tells you when a newer release lands, so documents quietly fall behind.

Imports are scattered

A thesis or a set of templates can pull the same package into dozens of files. Finding and editing each import by hand is tedious and easy to get wrong.

Edits stay surgical

Only the version text inside each import string is replaced. Formatting, comments and everything else in the file are left exactly as they were, and a file you edit while the prompt is open is skipped rather than overwritten.

Usage

Run it from the root of a Typst project, or point it somewhere else with --path.

~/my-thesis
$ typst-updater --help
Find Typst package imports and update them to their latest versions

Usage: typst-updater [OPTIONS]

Options:
  -p, --path <PATH>   Directory to search for *.typ files [default: .]
  -y, --yes           Apply updates without prompting [alias: --force]
      --include-docs  Also update imports in comments and raw blocks, such as the examples in a package's documentation
  -h, --help          Print help
  -V, --version       Print version
  1. Walk the directory for .typ files, skipping hidden directories such as .git.
  2. Fetch the package index from packages.typst.org and keep the highest version of each package.
  3. Compare every @preview import against that index.
  4. Report the available updates, grouped by file with line numbers. Updates that may break your document under semver, such as 0.1 to 0.2 or 1.x to 2.0, are marked breaking.
  5. Rewrite the version numbers after you confirm, or straight away with --yes.

--yes (or its older name --force) skips the prompt, which suits CI jobs and scripted upgrades. Commit first so you can review the result with git diff.

Imports inside comments and raw blocks are left alone, since they are usually examples rather than code. Pass --include-docs to update them too, for instance in a package's own documentation.

Examples

Each example below is generated from a unit test in src/main.rs, so the behaviour shown here is the behaviour the test suite checks.

Finds imports and their line numbers

finds_imports_with_line_numbers
#import "@preview/cetz:0.1.0": canvas

#import "@preview/tablex:0.0.1"
LinePackageVersion
1cetz0.1.0
3tablex0.0.1

Handles multi-line and code-mode imports

finds_multiline_and_code_mode_imports
#import "@preview/headstamp:0.1.0": (
  a, b,
)
#{ import "@preview/cetz:0.1.0" }

Both headstamp and cetz are found. The import list can span several lines, and an import inside a #{ … } code block counts too.

Leaves everything else alone

ignores_non_package_and_non_preview_imports
#import "custom-theme.typ": custom
#import "@local/mine:0.1.0"
#include "@preview/cetz:0.1.0"
#let s = "@preview/cetz:0.1.0"

None of these lines match. Local files, the @local namespace, #include and plain strings that only look like a package spec are all ignored.

Only reports real upgrades

find_updates_only_reports_newer_known_packages
#import "@preview/old:0.1.0"
#import "@preview/current:1.0.0"
#import "@preview/ahead:2.0.0"
#import "@preview/unknown:0.1.0"
ImportIndexResult
old 0.1.0 0.2.0 update to 0.2.0
current 1.0.0 1.0.0 up to date
ahead 2.0.0 1.0.0 never downgraded
unknown 0.1.0 — not in index, skipped

Compares versions as semver, not text

latest_versions_picks_highest_regardless_of_order
[
  { "name": "cetz",   "version": "0.2.0" },
  { "name": "cetz",   "version": "0.10.0" },
  { "name": "cetz",   "version": "0.9.1" },
  { "name": "tablex", "version": "0.0.9" },
  { "name": "cetz",   "version": "not-a-version" }
]

The latest cetz is 0.10.0 : as text, "0.9.1" would sort above it, but semver ordering doesn't. The order of entries in the index doesn't matter, and an entry that isn't a valid version is skipped (latest_versions_skips_unparseable_versions).

Rewrites only the version text

rewrite_changes_only_versions_and_handles_length_changes
before
#import "@preview/cetz:0.1.0": canvas
// keep this
#import "@preview/tablex:0.0.1"
after
#import "@preview/cetz:0.10.12": canvas
// keep this
#import "@preview/tablex:0.0.9"

Comments and import lists are untouched. A version that changes length, like 0.1.0 becoming 0.10.12, doesn't shift any later edits, because edits are applied from the end of the file backwards.

Scans recursively, skipping hidden directories

finds_typ_files_recursively_skipping_hidden_dirs
my-thesis/
├── .git/
│   └── x.typ     skipped: hidden directory
├── chapters/
│   └── ch1.typ   scanned
├── main.typ      scanned
└── notes.txt     skipped: not a .typ file

Files are visited in name order, so they are reported as chapters/ch1.typ then main.typ.

Install

Prebuilt binaries

Each release ships archives for Linux (x86_64, aarch64), macOS (Apple silicon, Intel) and Windows (x86_64). A rolling snapshot prerelease tracks main.

From source

cargo install --locked \
  --git https://github.com/talios/typst-updater

From a checkout

mise run install

Runs the tests, builds a release binary and installs it into ~/.cargo/bin.