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

calamine

v0.1.0

Published

Fast xlsx/xls/xlsb/ods reader — calamine compiled to WebAssembly. One artifact for Node, Bun, Deno, browsers and edge workers.

Downloads

1,197

Readme

calamine

CI

The Rust spreadsheet reader calamine, compiled to WebAssembly, so the same artifact reads .xlsx / .xls / .xlsb / .ods in Node, Bun, Deno, browsers and edge runtimes without a native addon or a per-platform prebuild.

npm install calamine

This is an unofficial binding. calamine is tafia's and does the actual parsing; this repo only decides what the parsed values look like in JavaScript and how one wasm binary reaches five runtimes. The npm package is named for what it wraps, but it is maintained here, so please report problems here first — the CONTRIBUTING guide explains how to tell a binding bug from an upstream one before you file either.

Using it

import { sheetNames, toCsv, readCellsParsed, ready } from "calamine";

const bytes = new Uint8Array(await file.arrayBuffer());

await ready();                        // no-op outside browsers; see below

sheetNames(bytes);                    // ["Sheet1", "Data"]
toCsv(bytes);                         // "name,when\na,2020-01-01T00:00:00.000\n"
readCellsParsed(bytes);               // [["name", "when"], ["a", "2020-01-01T00:00:00.000"]]

Every function takes the file as bytes. Nothing here touches a filesystem, which is what lets one artifact serve Node, Bun, Deno, browsers and edge workers.

That snippet is examples/js/quickstart.ts, runnable and type-checked in CI so it cannot drift from the API.

The full surface

| | returns | |---|---| | sheetNames(bytes) | string[] — workbook order. Only reads metadata, so it is cheap. | | readCellsParsed(bytes, o?) | Cell[][], or TaggedCell[][] with tagged: true | | toJsonParsed(bytes, o?) | Row[] keyed by the header row, or Cell[][] with header: "none" | | toCsv(bytes, o?) | RFC 4180 string | | toMarkdown(bytes, o?) | GitHub-flavoured table | | readCells / toJson | the same data as an unparsed JSON string | | ready() | Promise<void> — resolves once the wasm is usable |

Options are { sheet, dates, tagged }, plus header on the JSON outputs and delimiter on CSV. sheet defaults to the first one.

readCells and toJson return a string because only one value crosses the wasm boundary that way, however large the sheet. The …Parsed variants are JSON.parse and nothing more — reach for them unless you are about to hand the JSON straight to something else, in which case not parsing it at all is the faster path.

An unknown option is an error rather than a silent no-op, so { sheets: "Data" } tells you instead of quietly reading the wrong sheet.

Types

tagged: true turns each cell into { t, v } with t one of num, str, bool, date, dur, err. That is what makes a date cell distinguishable from text that happens to read 2020-01-01. Empty cells are null either way. Without tags the distinction is genuinely lost — a documented tradeoff, and the cheaper payload.

readCellsParsed(bytes, { tagged: true });
// [[{ t: "str", v: "name" }], [{ t: "date", v: "2020-01-01T00:00:00.000" }]]

The return type follows the flag, so neither shape needs a cast.

Waiting for the wasm

On Node, Bun, Deno and Cloudflare Workers the wasm loads synchronously and the functions work the moment you import them. In a browser or a bundled app it loads asynchronously, so await ready() first — that call is already resolved everywhere else, so awaiting it always is portable.

Why bother

SheetJS is the incumbent and it is slow on anything large. Reading a 23 MB / 1.8M-cell sheet:

| | small (5k cells) | medium (400k) | large (1.8M cells, 23 MB) | |---|---|---|---| | toCsv | 2 ms | 188 ms | 865 ms (5.3x) | | readCells values | 3 ms | 190 ms | 981 ms (4.7x) | | readCells tagged | 4 ms | 326 ms | 1.53 s (3.0x) | | SheetJS sheet_to_json | 8 ms | 786 ms | 4.61 s | | SheetJS sheet_to_csv | 8 ms | — | 4.57 s |

Node 22.18, macOS arm64, median of N runs. Bun 1.3.5 runs the identical artifact and pulls further ahead, because SheetJS degrades harder there. Run them yourself with bun run bench.

Speed is the least interesting difference, though. docs/comparison.md puts the readers side by side on correctness — dates, cell types, error values — and is honest about where this library loses.

The full parse costs 634 ms of that large-file number, so the JS/wasm boundary is 35-55% on top — which was the surprise. The boundary is not the bottleneck, so the API does not need to be contorted into a columnar or batched-iterator shape to be fast.

Artifact size: 370 KB gzipped (720 KB raw) plus 3.2 KB of JS glue.

Dates, carefully

Excel has no timezone concept, so a spreadsheet date is a civil (wall-clock) value while a JS Date is an instant. Converting one to the other has to invent an offset, and that invention is where most libraries go wrong. This one refuses to guess and makes it a policy:

| dates | serial 45943.541 becomes | |---|---| | "iso" (default) | "2025-10-13T12:59:02.400" — no offset, lossless | | "serial" | 45943.541 — raw | | "epoch-millis" | 1760360342400 — asserts the civil time is UTC |

Durations are not dates: a cell formatted [h]:mm:ss holding 1.5 is PT36H0M0S, not a day in 1900. docs/dates.md has the rest, including the 1900 leap-year bug and the two places the conversion is genuinely lossy.

Documentation

| | | |---|---| | docs/dates.md | the date policies, the sharp edges, and why SheetJS disagrees | | docs/packaging.md | five entry points, the exports conditions, and what each runtime needs | | docs/testing.md | the differential method, the corpus, adversarial inputs, CI | | docs/comparison.md | measured against SheetJS, ExcelJS and others — including where we lose | | docs/roadmap.md | what is not done yet | | CONTRIBUTING.md | building it, and the rules for changing cell behaviour |

Which entry point you get

One wasm binary behind five entries, chosen by exports conditions. They differ only in how the bytes reach the glue, which is the one thing every runtime does differently.

| entry | condition | how the wasm loads | |---|---|---| | dist/node.js | node | node:fs + initSync — synchronous | | dist/workerd.js | workerd | import wasm from "./…wasm" — Workers want a Module | | dist/streaming.js | browser, import | fetch + instantiateStreaming | | calamine/inline | — | base64 in the JS, for builds with no companion asset | | calamine/slim | — | you supply the bytes or Module |

Most callers import calamine and never think about it. See docs/packaging.md for the two that are opt-in and why.

Layout

src/lib.rs                      the wasm surface: functions and their options
src/cells.rs                    the cell model — values vs tagged
src/dates.rs                    date policies, civil time, ISO parsing
src/output.rs                   objects, CSV, Markdown
types/                          the hand-written .d.ts, copied into dist/
scripts/build.mjs               assembles dist/ from the wasm-pack output
examples/dump_native.rs         reference dumper: raw calamine Data, no opinions applied
examples/js/                    runnable versions of the snippets in this README

bench/                          benchmarks and their large fixtures
test/support/                   shared helpers, fixture builders, paths
test/node/                      suites that run under Node
test/browser/                   Playwright, against a real engine over real HTTP
test/worker/                    Cloudflare Workers — not implemented, see its README

Tests are organised by where the code runs, because that is what actually varies — the parser is identical everywhere, and what differs is how the wasm reaches it. test/README.md has the detail.

Building it

bun install
bun run build      # wasm-pack, then assembles dist/
bun run fixtures   # ~44 MB, fetched and generated, not committed
bun run test

Needs Rust (pinned in rust-toolchain.toml), wasm-pack, and binaryen 131+ for wasm-opt. CONTRIBUTING.md has the detail, including why that binaryen version matters.

Licence

MIT. The published .wasm statically links calamine and ~50 other crates; their notices are in THIRD-PARTY-LICENSES.md, regenerated from Cargo.lock and checked in CI.