Onykia Docs

Packages

Typst documents pull in libraries with #import. To resolve them you give the engine two things:

  • package handler - given (namespace, name, version), return the package's .tar.gz bytes. Called lazily, on import.
  • setRemotePackages - feed an index so autocomplete and bare-version resolution know what exists before any import is written. Optional.

@preview from the public registry

The @preview namespace maps to the official Typst package registry. Wire the handler to fetch tarballs from it:

const core = new Core({
  wasm,
  package: async (namespace, name, version) => {
    const url = `https://packages.typst.org/${namespace}/${name}-${version}.tar.gz`;
    const res = await fetch(url);
    if (!res.ok) throw new Error(`package fetch failed: ${url}`);
    return new Uint8Array(await res.arrayBuffer());
  },
});
 
// optional: populate the IDE catalog so completions list available packages
const res = await fetch('https://packages.typst.org/preview/index.json');
await core.setRemotePackages(new Uint8Array(await res.arrayBuffer()), []);

Index loading is best-effort: if it fails, imports still resolve through the package handler - only the autocomplete catalog goes dark. The index decoder accepts JSON only.

Cache the tarballs. The engine ships indexedDbCache() and withCache():

import { indexedDbCache, withCache } from '@mudomi/onykia-engine';
const cache = indexedDbCache();
const fetchPkg = withCache(rawFetch, cache, (ns, n, v) => `pkg:${ns}/${n}-${v}`);
new Core({ wasm, package: fetchPkg });

Private namespaces

setRemotePackages(index, privateNamespaces) takes a second argument - your own namespaces, each with its own index:

await core.setRemotePackages(previewIndexBytes, [
  { namespace: 'acme', data: acmeIndexBytes },
]);

#import "@acme/letterhead:1.0.0" then routes through the same package handler - branch on namespace there to serve private tarballs from your backend.

Cross-origin note

Fetching tarballs and the index from a third-party host must pass cross-origin isolation. If the registry sends CORS but not a Cross-Origin-Resource-Policy header, serve your app with Cross-Origin-Embedder-Policy: credentialless instead of require-corp (see Vite & frameworks).

On this page