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

@ledgermill/wasm

v0.1.2

Published

ledgermill ledger engines compiled to WebAssembly, with their wasm-bindgen glue.

Readme

ledgermill

Ledger engines for plain-text accounting: one for hledger journals, one for Beancount ledgers. They are written in Rust, compile to WebAssembly for browsers, and are built to be embedded in native apps later. They compute with exact decimals and answer through a typed interface.

Status: version 0.1.2. It gives the same answers as hledger 1.52.4 and Beancount 3.2.3 (beanquery 0.2.0) for everything its first user, a personal finance app, reads from a journal; what it refuses is listed in spec/hledger/syntax.md and spec/README.md. See CHANGELOG.md.

npm install @ledgermill/wasm   # WebAssembly and its JS glue
cargo add ledgermill           # the Rust core
import init, { load, format } from "@ledgermill/wasm";

await init(); // browsers; in Bun or Node pass the .wasm bytes: initSync({ module: bytes })

const ledger = load(JSON.stringify([{ name: "main.journal", content: journalText }]));
try {
  const findings = JSON.parse(ledger.check()); // [] when the journal is fine
  const postings = JSON.parse(ledger.postings());
  const holdings = JSON.parse(ledger.holdings(JSON.stringify({ commodity: "EUR", today: "2026-10-25" })));
} finally {
  ledger.free(); // the ledger lives in wasm memory
}

const tidy = format("main.journal", journalText); // whitespace only

Every answer is a JSON string. atCost(), value(date, commodity) and holdings(query) throw an Error whose message is the finding as JSON when ledgermill cannot answer as the reference tool does.

What it does

load(files) → ledger
ledger.transactions()   booked transactions with their file and line (Beancount: padding included)
ledger.postings()       one row per posting and commodity, with its transaction's header fields
ledger.prices()         the `P` directives, as `hledger prices` lists them; the Beancount `price` directives, as its `prices` table does
ledger.check()          problems found, as structured findings
ledger.atCost()         the transactions as hledger's `print --cost` gives them; every cost as Beancount's `cost(position)`
ledger.value(date, commodity)   the transactions as `print --value=DATE,COMMODITY` gives them; every posting as Beancount's `convert(position, V, date)`
ledger.holdings(query)  per account and commodity, the units, cost, value and cost value (hledger: real postings only)
ledger.free()

format(file) → text   realigns posting lines, whitespace only: hledger journals (4-space indent, amounts ending in one column per transaction), Beancount ledgers (amounts of the whole file in one column, the file's usual indent)

postings() gives, for hledger, what print --location -O csv lists: one row per posting and commodity (an elided posting gets a row for each commodity it was given), by date, with flag the transaction's status ("", "*" or "!"), account without the () or [] of a virtual posting and its kind, number the exact quantity (never rounded, with a point as decimal mark) and line the line the posting itself is written on, which hledger does not give. prices() gives each P directive with its date, commodity, rate (the commodity and the exact quantity, with the places written), file and line, sorted by date. A P line whose rate is followed by a price, a lot or a label is refused with an unsupported-syntax finding (price-annotation).

In Rust these are Ledger::at_cost() and Ledger::value(date, commodity); both return Result<Vec<Transaction>, Finding>. at_cost replaces every cost by an amount in the cost's commodity and drops the conversion postings; value converts every amount at the prices (P lines) known on date (YYYY-MM-DD), by the shortest path of prices, with the arithmetic hledger uses. A question ledgermill cannot answer as hledger does is an Err with an unsupported-syntax finding, not a guess: an at_cost on a transaction that has pairs of conversion postings together with several priced postings or a balance assignment, a value in no commodity, and, on a Beancount ledger, the first construct it refused (a Beancount file with a refused construct lists nothing, so a valuation would look complete and be empty). A date that is not YYYY-MM-DD is an invalid-argument finding. In the wasm binding the Err is thrown as an Error whose message is the finding as JSON.

holdings(query) takes { commodity, end, today } (JSON in the wasm binding, HoldingsQuery in Rust): the commodity V to value in, end an optional exclusive YYYY-MM-DD like -e, and today the date today, because the core has no clock. It gives one Holding per account and commodity held, ordered by account name then commodity: units and the lists cost, value and cost_value (the same name in JSON), exact, every sum rounded to ten places once. Only real postings count; no account-prefix filter and no symbol aliases (callers do those). It refuses like at_cost and value (unsupported-syntax) and answers an invalid-argument finding for an end or today that is not a date; see spec/hledger/holdings.md.

For Beancount, prices() lists the price directives by date (the same date as written, nothing dropped), and the three reads follow convert(), cost() and the sums of the queries Plainledger sends to bean-query (price-table, convert-rates, convert-positions, cost-of-position, holdings): rates come from price directives only, never from @, @@ or a cost; the latest price of a pair on or before the date, direct or inverse; no chain of pairs, except that a position with a cost goes through the cost's currency when both rates exist. holdings takes end as exclusive here too (the postings dated before it) but values at the latest prices there are, as convert(position, V) does, and does not use today. Beancount's numbers come with its own arithmetic: 28 significant digits, holdings strings in plain digits, value strings as number() prints them (1E-7).

For hledger, balance assertions (=, ==, =*, ==*) and assignments are read: check() reports a failed assertion (assertion-failed, with the account, commodity and the exact expected and actual amounts) and the transactions come with the amounts the assignments filled in. Its first error is the one hledger prints first (file and line), not the first by line. The automated (=) and periodic (~) rules are read, with the errors hledger gives for them, and left out: hledger only applies them with --auto and --forecast, so each one gets an ignored-syntax warning and changes nothing. A few things are refused with an unsupported-syntax error instead (include, timeclock, timedot and CSV files, dates with no year and no Y, and a few more); spec/hledger/syntax.md lists every construct and what ledgermill does with it.

For Beancount, check() gives the verdict bean-check gives, with the same first error (file and line): unknown or closed accounts, balance assertions and their tolerance, unused pads, currency constraints, duplicates, unbalanced transactions and more (spec/README.md lists the codes). Beancount pad is read and its padding transactions are built; include, plugin, unusual options and AVERAGE booking are refused with a finding.

format(file) realigns the posting lines of an hledger journal and changes nothing but whitespace; if the result would not read as the same ledger, it returns the file as it was. A Beancount file is formatted like bean-format does (postings, balance and price lines: the amounts of the whole file in one column, the file's usual indent), also whitespace only; one ledgermill refuses or cannot parse is returned as it was.

ledgermill is not a command-line clone of hledger or Beancount. It does not print their reports and does not copy their messages; it answers questions about a ledger.

How we know it is right

The real hledger and Beancount tools are our reference tools. A comparison harness runs them on journals we write ourselves, decodes their output, and compares it with ledgermill's answers value by value. Every rule ledgermill follows is written down as a spec note with the journal, command and output that show it (spec/).

Clean room

ledgermill is an independent implementation. Nobody working on it reads the source code of hledger, Beancount, rustledger or Ledger. Behaviour is learned by running those programs on journals we wrote and recording what they do. Their manuals may be read to learn rules and names, but no text, table or example is copied from them. The rules are in CONTRIBUTING.md; what each contributor has been exposed to is recorded in EXPOSURE.md; the remaining legal risk and our answer to a complaint are in RISK.md.

Licence

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.