npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@typfolio/paramdoc

v0.0.1

Published

Experimental: fills a parametric document exported from a Typst template with data and writes tagged PDF/UA-1, without a compiler, in a few hundred microseconds; dependency-free, for browsers, Node and a pool of Web Workers

Readme

@typfolio/paramdoc

Experimental. Fills a parametric document with data and writes a tagged PDF/UA-1, without a compiler, in a few hundred microseconds per document. The document is a Typst template in the paramdoc slot dialect that @typfolio/paramdoc-export laid out once with Typst: static content as it was typeset, and holes (text slots, wrapping slots, bars, image slots, link slots, repeating bands with variants) that this runtime fills from the data by the names the template gave them.

The one rule: the runtime's output equals Typst's own layout of the same data, glyph for glyph, or the runtime refuses the data with a message that names the slot, the reason and the fix. It only does what was ported from Typst and proven against it (experiments/paramdoc in the repository: a Rust twin of this runtime, Typst references, random data, pixels, veraPDF); everything else is refused at export time, before a document exists, or here at fill time. Supported in slots: Latin with the extended blocks, Greek and Cyrillic, precomposed, one font, size and colour per slot; single lines with the error, clip, shrink and ellipsis policies; wrapping slots that break as Typst's simple breaker does, ragged or justified, web addresses, soft hyphens and middle dots included; integers, booleans and strings as values; PNG and JPEG images. The inventory of what is not supported and why is experiments/paramdoc/GAPS.md.

No dependencies; 39 kB minified, 15 kB gzipped on the calling thread, 41 kB and 16 kB as a worker with its protocol; browsers, Web Workers and Node 22 or later. The document format is unstable until 0.1: a document is read by the runtime version that matches the exporter that wrote it.

API

import { createRuntime, ParamdocError } from '@typfolio/paramdoc';

// Once per document (in a worker, once per worker): parses it and compresses the fonts.
const runtime = await createRuntime({ document: docJsonBytes, fonts: [regularBytes, boldBytes] });
runtime.names;                     // the names the data binds to: slots, bars, image slots, bands
const { pdf, pages } = await runtime.fill({ name: 'Ada Lovelace', score: 97, courses: [...] });

createRuntime throws ParamdocError with code: 'document' when the document, its fonts or its images cannot be used (a format version this runtime does not read, a font whose hash is not the document's); fill throws it with code: 'fill' when the data cannot be laid out as Typst would lay it out. The document and its fonts come from the exporter's output directory: doc.json and fonts/* in the order doc.json lists them.

In Web Workers

import { createWorkerClient, fillBatch } from '@typfolio/paramdoc';

// A pool of Web Workers, one runtime each, started lazily as fills arrive; the document and its fonts are copied
// into each worker as it starts.
const client = createWorkerClient({ document: { document: docJsonBytes, fonts }, workerCount: 4, timeoutMs: 60_000 });
const { pdf } = await client.fill(data, { signal });

// Many documents: at most workerCount fills in flight, outcomes as they settle, in completion order.
for await (const outcome of fillBatch(client, rows.map((data, id) => ({ id, data })), { signal })) {
  if (outcome.status === 'success') save(outcome.id, outcome.result.pdf);
  else if (outcome.status === 'error') console.error(outcome.id, outcome.error.code, outcome.error.message);
}
await client.dispose();

The client has the shape and the rules of @typfolio/workers' createWorkerClient and compileBatch, without Effect. Fills are queued in order and run one per worker; fill copies its data synchronously, so the object can be reused at once. Cancelling a waiting fill removes it from the queue; cancelling a running one rejects it at once and lets its worker finish quietly, since a fill takes a fraction of a millisecond and a worker restart (the fonts compressed again) takes far longer. A timeout, queue time included, terminates the worker it ran on and the next fill gets a fresh one. dispose rejects every fill with disposed and terminates every worker. Every rejection is a ParamdocError: the runtime's own document and fill codes, or worker, timeout, queue-full, aborted, disposed; in a batch, a refused document is that job's error outcome and the rest go on. collectBatch gathers the outcomes into input order. The worker itself is @typfolio/paramdoc/worker, started as new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }), which Vite ships as its own file; workerFactory makes the workers for other hosts (Node has no Web Worker: the package's tests drive the worker side in-process through workerFactory).

@typfolio/paramdoc/internal exposes the runtime as the experiment's proof uses it (the Runtime class with its slot trace, compare against a reference layout, the document's types); it is not part of the public API and changes with the document format.

Numbers

One thread, the benchmark report (a page with a table of up to eight rows, score bars and a three-line comment), M-series Mac: about 3,250 documents per second in Node, median 0.29 ms per document, 69 kB per PDF (the two subset fonts are embedded in every file). In the browser, through createWorkerClient and fillBatch on four workers, the demo in the repository (examples/pretext-hybrid, the performance page's engine switch) renders 200 of these reports in 0.1 s: about 2,100 per second with the four cold starts of 20 ms included, 1 ms median per fill.