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

safesheet

v1.1.0

Published

A secure, streaming-first .xlsx reader/writer for Node.js — a drop-in-friendly replacement for xlsx (SheetJS) with no unpatched CVEs and no whole-file-in-memory footgun.

Readme

safesheet

A secure, streaming-first .xlsx reader/writer for Node.js — built as a drop-in-friendly replacement for xlsx (SheetJS).

Why not just use xlsx (SheetJS)?

The free xlsx npm package has two unpatched high-severity vulnerabilities (ReDoS and prototype pollution) with no fix published to npm — the maintainer moved fixes behind a paid registry instead. On top of that, it loads the entire workbook into memory as one big object graph: open a sheet with a few hundred thousand rows and it can crash the process with no useful error, and round-tripping a file (open, resave) silently drops formatting and custom properties.

safesheet fixes the underlying causes, not just the symptoms:

  • Streams rows instead of building one big object. Reading never materializes the whole sheet — each row is handed to you as soon as it's parsed, so a 10-million-row sheet costs you one row's worth of memory, not ten million.
  • A decompression-bomb guard, on by default. .xlsx files are zip archives; a hostile file can be a few KB on disk and expand to gigabytes. Every entry safesheet decompresses is capped (maxDecompressedSheetBytes, maxSharedStringsBytes), and it aborts cleanly instead of exhausting memory.
  • No XML entity expansion. The XML parser (sax) only resolves the five predefined XML entities and numeric character references — never DOCTYPE-defined entities — which rules out "billion laughs"-style attacks by construction, not by a denylist.
  • Prototype-pollution-safe by construction. rowToObject() returns a null-prototype object, so a header column literally named __proto__ or constructor can't be used to pollute anything — there's no prototype chain to write onto.
  • Real backpressure. Both reading and writing are chunked so a slow consumer (e.g. writing rows to a database) pauses the producer instead of it buffering unboundedly ahead.
  • TypeScript-first. Full type definitions ship in the package.

Install

npm install safesheet

Quick start

Reading

import { Workbook, isSafeSheetError } from "safesheet";
import { readFile } from "node:fs/promises";

const buffer = await readFile("report.xlsx");
const wb = Workbook.open(buffer);

console.log(wb.sheetNames); // ["Sheet1", "Sheet2", ...]

// Recommended: stream rows, never hold the whole sheet in memory
for await (const row of wb.readRows("Sheet1")) {
  console.log(row); // [cellA, cellB, cellC, ...]
}

// Only for sheets you know are small
const rows = await wb.readSheetToArray("Sheet1");

Map rows onto header names without the prototype-pollution risk of a plain object:

import { rowToObject } from "safesheet";

const rows = wb.readRows("Sheet1");
const { value: headerRow } = await rows.next();
for await (const row of rows) {
  const record = rowToObject(headerRow as string[], row);
  console.log(record.name, record.email);
}

Writing

import { WorkbookWriter } from "safesheet";
import { writeFile } from "node:fs/promises";

const writer = new WorkbookWriter();
const sheet = writer.addSheet("Report");
sheet.writeRow(["name", "age"]);
sheet.writeRow(["Ada", 36]);

const buffer = await writer.finalize();
await writeFile("report.xlsx", buffer);

// Or stream compressed bytes straight to disk without buffering the
// whole workbook in memory:
import { createWriteStream } from "node:fs";
await writer.finalizeTo(createWriteStream("report.xlsx"));

API

Workbook.open(buffer, options?)

Parses workbook metadata (sheet names) synchronously; opens instantly regardless of sheet size since row data isn't touched until you read it.

  • .sheetNames: string[]
  • .readRows(nameOrIndex, options?): AsyncGenerator<CellValue[]> — the recommended way to read anything that might be large
  • .readSheetToArray(nameOrIndex, options?): Promise<CellValue[][]> — convenience for small sheets only

options (all optional, all have finite defaults — there's no "unlimited" mode):

| Option | Default | | ---------------------------- | -------- | | maxDecompressedSheetBytes | 512 MiB | | maxSharedStringsBytes | 64 MiB | | maxRows | 5,000,000 | | maxCellBytes | 1 MiB | | highWaterMark | 64 rows |

WorkbookWriter

  • .addSheet(name): SheetWriterSheetWriter.writeRow(values: CellValue[])
  • .finalize(): Promise<Buffer>
  • .finalizeTo(writable): Promise<void> — streams compressed output directly to a Node Writable

Errors

Every rejection is a SafeSheetError with a stable code: INVALID_WORKBOOK, SHEET_NOT_FOUND, LIMIT_DECOMPRESSED_SIZE, LIMIT_ENTRY_SIZE, LIMIT_ROW_COUNT, LIMIT_CELL_SIZE, XML_PARSE_ERROR. Use isSafeSheetError(err) to narrow the type.

What's out of scope for v1.1.0

  • Formulas are read as their cached value only (the formula text itself isn't exposed)
  • Cell styling/formatting is not read or written
  • Writing always uses inline strings rather than a shared-string table (simpler, fully valid, slightly less space-efficient for highly repetitive text)

License

MIT