Managing dependencies¶
This is the workflow for a project that uses packages: a thesis, a report, a template repository. It covers depending on a git repository, reproducing that dependency on another machine, and taking it away again.
Three commands do the work — add, sync and remove — and they all operate
on the same two files.
Depending on a repository¶
Typst resolves an import from one of two places: @preview, the official
registry, or a namespace somebody filled in by hand. Neither lets a document say
"I depend on that repository at that version" and have it come back on someone
else's machine.
A project using gotpm keeps two files next to each other:
| File | What it owns |
|---|---|
typst.toml |
which packages the project wants, listed under [tool.gotpm] |
gotpm.lock |
where each one comes from, and the exact commit it was fetched at |
Packages are installed under the @gotpm namespace, which keeps them apart from
anything you placed in @local yourself.
A worked example¶
Add a repository. Everything it depends on comes along:
$ gotpm add github.com/user/cetz
info: added @gotpm/cetz:0.3.1 from github.com/user/cetz
info: @gotpm/oxifmt:0.2.1 (via @gotpm/cetz:0.3.1)
Without --rev, the newest release tag is used — or the current HEAD when the
repository has no release tags. Pin something else explicitly:
$ gotpm add github.com/user/cetz -t v0.3.0
$ gotpm add github.com/user/cetz -t 4f2a1c9
$ gotpm add git@github.com:user/private-pkg.git
Only the package you asked for is written to typst.toml; the rest are recorded
in the lock as transitive:
[tool.gotpm]
dependencies = [
"@gotpm/cetz:0.3.1",
]
while gotpm.lock gets the part that makes it reproducible — the repository, the
commit, and which package asked for it:
{
"version": 1,
"packages": [
{
"import": "@gotpm/cetz:0.3.1",
"name": "cetz",
"version": "0.3.1",
"namespace": "gotpm",
"url": "github.com/user/cetz",
"revision": "v0.3.1",
"hash": "abc123def4567890abc123def4567890abc123de",
"subdir": "",
"direct": true,
"required_by": null
}
]
}
Import it the way the dependency list spells it:
#import "@gotpm/cetz:0.3.1": *
Packages in a monorepo¶
A repository may hold several packages, each in its own directory with its own
typst.toml. Name the one you want by its package path — the directory
relative to the repository root — separated from the repository by //:
$ gotpm add github.com/user/monorepo//packages/common
info: added @gotpm/common:0.1.0 from github.com/user/monorepo//packages/common
The // works with every spelling of a repository, including
https://…/monorepo.git//packages/common and
git@github.com:user/monorepo.git//packages/common, and with gotpm install -r.
The whole repository is cloned, but only the package path is installed. The lock
records the source with its //, which gotpm 0.5.2 and older refuse as an
invalid repository url rather than misread.
Each package in a monorepo is a project of its own: its dependencies are read
from the gotpm.lock beside its typst.toml, not from one at the repository
root. Revisions are still the repository's — -t picks a tag or commit of the
whole repository, and the version is whatever the package's typst.toml says
at that commit.
Point at the repository root by mistake and gotpm reminds you of the //:
$ gotpm add github.com/user/monorepo
error: not a typst package: github.com/user/monorepo has no typst.toml at its root
note: name a package in a subdirectory by its package path, e.g. github.com/user/monorepo//path/to/package
Commit gotpm.lock¶
gotpm.lock is not a cache. It belongs in version control, for two reasons:
- It is what makes a checkout reproducible.
gotpm syncinstalls the commit the lock names, not whatever the tag points at today, so a tag moved upstream cannot silently change your document. - It is the only record of where a dependency's own dependencies live. A
dependency string such as
@gotpm/cetz:0.3.1names a package, never a repository — so when your package is itself added by someone else, the lock you committed is what tells their gotpm where to fetchcetzfrom. A package that declares gotpm dependencies without committing its lock cannot be added at all.
Reproducing it elsewhere¶
Anyone who clones the project gets the same packages from the two committed files:
$ git clone https://github.com/user/my-doc && cd my-doc
$ gotpm sync
info: installed @gotpm/oxifmt:0.2.1
info: installed @gotpm/cetz:0.3.1
sync also drops lock entries that nothing in typst.toml requires any more,
which is how a lock stays honest across branches.
In CI¶
Use --frozen. A lock that disagrees with typst.toml then fails the run
instead of being quietly repaired, which is usually a change somebody forgot to
commit:
- name: Install gotpm
run: curl -sSfL https://github.com/npikall/gotpm/releases/latest/download/install.sh | sh
- name: Install dependencies
run: gotpm sync --frozen
- name: Compile
run: typst compile thesis.typ
Upgrading, and two versions at once¶
add never rewrites an existing entry, because that would invalidate the
#import statements already in your source. Adding a second version installs it
alongside the first and lists both:
[tool.gotpm]
dependencies = [
"@gotpm/cetz:0.3.1",
"@gotpm/cetz:0.4.0",
]
Typst installs every version at its own path, so both remain importable. Migrate
your #import statements file by file, at your own pace. Once your source no
longer mentions the old one, take it out:
$ gotpm remove @gotpm/cetz:0.3.1
Removing a dependency¶
remove takes the import string exactly as it appears in typst.toml, so the
version being removed is never in doubt. It also drops the transitive
dependencies nothing else needs any more — a package another dependency still
requires stays:
$ gotpm remove @gotpm/cetz:0.3.1
info: removed @gotpm/cetz:0.3.1
info: no longer needed: @gotpm/oxifmt:0.2.1
info: the package files are still in the package directory; pass --prune to delete them
The files are left behind on purpose: the package
directory is shared by every project on
the machine, and another one may import the same version. --prune deletes them
anyway.
Things that will bite you¶
A dependency added to typst.toml by hand cannot be synced¶
An import statement names a package, never the repository it comes from, so only you know where it should be fetched from:
$ gotpm sync
ERROR
Declared dependency is missing from the lock: @gotpm/cetz:0.3.1
note: gotpm.lock records where a package comes from; run 'gotpm add <url>' to add it properly.
Use gotpm add <repository> instead. It writes both files.
The same coordinate from a different repository¶
The package directory is shared by every project on your machine. If
@gotpm/cetz:0.3.1 is already installed from a different repository — or from a
different package path of the same one — add
refuses rather than overwrite it, since that would change what your other
projects import. --force overrides that, deliberately.
A dependency without a committed lock¶
If a repository you are adding declares gotpm dependencies but did not commit
its gotpm.lock, gotpm has no way to learn where those dependencies live, and
the add fails. That is a bug in the package you are adding — the fix is upstream
committing its lock.
Checking that imports will resolve¶
gotpm check reads a .typ file and reports whether every package it imports
will resolve at compile time:
$ gotpm check thesis.typ
A non-Universe package has to be present in the package directory; a @preview
package need only exist in the Universe index, because the compiler downloads it
and gotpm does not interfere.