I’ve seen private package management come up more frequently, so I thought I’d make the process I use more public with this post. A few disclaimers up front:
- you can’t use this with the web app
- the people using the private package namespace will need to know a bit of git
- the script I wrote to help with this is only tested on Linux
(The good thing about these limitations is that I don’t fear that this will impact Typst’s ability to monetize this more advanced use case for more general audiences.)
The goal is to have a number of packages available to import under some namespace, for example:
#import "@pria/invoice:0.1.1"
(I’m using PRIA as the example because it’s the real association I’m doing it for, and because why not)
For this, we need the package to ultimately land at the path {data-dir}/typst/packages/pria/invoice/0.1.1
, like described in the typst/packages repo.
My approach has the following parts:
- each package is a git repo, created from a fork of GitHub - typst-community/typst-package-template
- releases of these packages are not released to typst/packages, but our private “package registry” repo
- the registry repo contains a script that makes it easier to clone it to the right location and update it once it has been cloned
Since all related repos are private, I’ll just outline the structure below.
The package template
Private packages don’t go to the official registry, and don’t go into the @preview
namespace. This means I adapted the community’s typst-package-template repo.
Be sure to also check the template’s README and/or Typst packages - Why and How? | James’ Programming Blog since you need to create some Github tokens to use the release workflows.
Many of the changes are just trivial replacements:
Replace 'preview' with 'pria' in a few places
- # install the library with the "@preview" prefix (for pre-release testing)
- install-preview: (package "@preview")
+ # install the library with the "@pria" prefix (for pre-release testing)
+ install-pria: (package "@pria")
...
- # uninstalls the library from the "@preview" prefix (for pre-release testing)
- uninstall-preview: (remove "@preview")
+ # uninstalls the library from the "@pria" prefix (for pre-release testing)
+ uninstall-pria: (remove "@pria")
if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then
echo "package TARGET"
echo ""
echo "Packages all relevant files into a directory named '<name>/<version>'"
- echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package"
+ echo "at TARGET. If TARGET is set to @local or @pria, the local Typst package"
echo "directory will be used so that the package gets installed for local use."
echo "The name and version are read from 'typst.toml' in the project root."
echo ""
echo "Local package prefix: $DATA_DIR/typst/package/local"
- echo "Local preview package prefix: $DATA_DIR/typst/package/preview"
+ echo "Local pria package prefix: $DATA_DIR/typst/package/pria"
exit 1
fi
- TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")"
+ TARGET="$(resolve-target "${1:?Missing target path, @local or @pria}")"
if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then
echo "uninstall TARGET"
echo ""
echo "Removes the package installed into a directory named '<name>/<version>'"
- echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package"
+ echo "at TARGET. If TARGET is set to @local or @pria, the local Typst package"
echo "directory will be used so that the package gets installed for local use."
echo "The name and version are read from 'typst.toml' in the project root."
echo ""
echo "Local package prefix: $DATA_DIR/typst/package/local"
- echo "Local preview package prefix: $DATA_DIR/typst/package/preview"
+ echo "Local pria package prefix: $DATA_DIR/typst/package/pria"
exit 1
fi
- TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")"
+ TARGET="$(resolve-target "${1:?Missing target path, @local or @pria}")"
if [[ "$target" == "@local" ]]; then
echo "${DATA_DIR}/typst/packages/local"
- elif [[ "$target" == "@preview" ]]; then
- echo "${DATA_DIR}/typst/packages/preview"
+ elif [[ "$target" == "@pria" ]]; then
+ echo "${DATA_DIR}/typst/packages/pria"
else
echo "$target"
fi
The more interesting changes are around configuring the actual CI deployment:
private registry action
This action makes sure that, if a private package depends on other private packages, it has access to them when the package is tested in CI.
.github/setup-private-registry/action.yml
name: Setup private registry
description: Install private Typst package registry
inputs:
repository:
description: The registry repository to check out
token:
description: >
Personal access token (PAT) used to fetch the repository.
default: ${{ github.token }}
namespace:
description: The namespace under which to install the repository, e.g. `foo` for `@foo`
runs:
using: "composite"
steps:
- name: Checkout package registry
uses: actions/checkout@v4
with:
repository: ${{ inputs.repository }}
token: ${{ inputs.token }}
path: ${{ inputs.namespace }}-typst-packages
- name: Move package registry to local package location
shell: bash
run: |
if [[ "$OSTYPE" == "linux"* ]]; then
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}"
elif [[ "$OSTYPE" == "darwin"* ]]; then
DATA_DIR="$HOME/Library/Application Support"
else
DATA_DIR="${APPDATA}"
fi
TARGET="${DATA_DIR}/typst/packages/${{ inputs.namespace }}"
mkdir -p "$(dirname "$TARGET")"
mv "${{ inputs.namespace }}-typst-packages" "${TARGET}"
modified CI workflows
The public registry is not directly backed by its github repo, it’s only populated from there. As such, it has a different directory structure and packages go into packages/prefix
. Not so for the private registry: the repo itself is the namespace and directly in there are the packages.
release and test workflows
.github/workflows/release.yml
:
env:
# the repository to which to push the release version
# usually a fork of typst/packages (https://github.com/typst/packages/)
# that you have push privileges to
- REGISTRY_REPO: author/typst-packages
+ REGISTRY_REPO: PRIArobotics/typst-packages
# the path within that repo where the "<name>/<version>" directory should be put
# for the Typst package registry, keep this as is
- PATH_PREFIX: packages/preview
+ PATH_PREFIX: .
...
jobs:
release:
runs-on: ubuntu-latest
steps:
...
- name: Setup typst
uses: typst-community/setup-typst@v3
with:
typst-version: latest
+ - name: Setup PRIA package registry
+ uses: ./.github/setup-private-registry/
+ with:
+ repository: PRIArobotics/typst-packages
+ token: ${{ secrets.REGISTRY_TOKEN }}
+ namespace: pria
jobs:
tests:
...
steps:
...
- name: Setup typst
id: setup-typst
uses: typst-community/setup-typst@v3
with:
typst-version: ${{ matrix.typst-version }}
+ - name: Setup PRIA package registry
+ uses: ./.github/setup-private-registry/
+ with:
+ repository: PRIArobotics/typst-packages
+ token: ${{ secrets.REGISTRY_TOKEN }}
+ namespace: pria
The package registry
The hard part is done; the package registry repo is basically just a regular repo with a convenience script in it, and to which your Github Actions workflows get access. I will just paste the README and the script here:
README
PRIA-private Typst package repository
This repository contains Typst packages for PRIA. As described in the typst/packages repo, by placing packages in a specific folder they can be made available on a system. Against recommendation, we don’t limit ourselves to the local
namespace: this repository should be cloned into a directory named pria
so that packages can then be imported via
#import "@pria/<name>:<version>"
Maintaining this clone in the right location is done by the install.sh script - see below.
install.sh
This will (on a Linux system) either clone or pull from this repo, at the appropriate location on your system.
--force
updates the repository even if there are changes to it.
This would usually only happen when developing Typst packages.
--remove
Removes the repository. Currently, this does not require --force
even if there are changes.
Of course, this script can in principle be used in the curl ... | bash
way;
however, since this is a private repository, there is no stable link to the raw script that could be put in this README.
You can either just download the script, or do the following:
- initially, copy the raw link once and
curl ... | bash
it - you will see the output
cloned PRIA package repository into .../typst/packages/pria
- from now on, call the script as
.../typst/packages/pria/install.sh
If you should want to use arguments without downloading the script,
you can use /dev/stdin
as the script passed to bash
, e.g.:
curl ... | bash /dev/stdin --force
install.sh
#!/usr/bin/env bash
set -eu
# Local package directories per platform
if [[ "$OSTYPE" == "linux"* ]]; then
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}"
elif [[ "$OSTYPE" == "darwin"* ]]; then
DATA_DIR="$HOME/Library/Application Support"
else
DATA_DIR="${APPDATA}"
fi
PRIA_PACKAGES="$DATA_DIR/typst/packages/pria"
if [[ "${1:-""}" == "--remove" ]]; then
# uninstall package registry
if [[ -e "$PRIA_PACKAGES" ]]; then
rm -rf "$PRIA_PACKAGES"
echo "Removed PRIA package repository from $PRIA_PACKAGES"
exit 0
else
echo "PRIA package repository did not exist at $PRIA_PACKAGES"
exit 1
fi
elif [[ ! -e "$PRIA_PACKAGES" ]]; then
# the package repo does not exist; clone
git clone --depth=1 https://github.com/PRIArobotics/typst-packages "$PRIA_PACKAGES"
cd "$PRIA_PACKAGES"
echo "cloned PRIA package repository into $PRIA_PACKAGES"
else
cd "$PRIA_PACKAGES"
if git diff-index --quiet HEAD -- || [[ "${1:-""}" == "--force" ]]; then
if [[ "${1:-""}" == "--force" ]]; then
# force overwrite the package repo
git fetch origin main
git reset --hard origin/main
git clean -fd
else
# pull the package repo
git pull
fi
echo "Pulled updated PRIA package repository into $PRIA_PACKAGES"
else
# the package repo is dirty
git status
echo "the PRIA package repository at $PRIA_PACKAGES is not clean; see above"
echo "use --force to overwrite local changes"
exit 1
fi
fi
git log -1 --pretty=format:'PRIA packages are now at %h:%n%B'
Conclusion
This was a lot of code with little explanation, but I only have so much time today. If it helps you let me know; if you need a few more pointers to understand what’s going on or how to get this setup to work just ask.
Happy new year!