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

@mitresthen/excelents

v1.0.1

Published

Universal, tree-shakeable, TypeScript-first spreadsheet (xlsx/csv) library.

Readme

excelents

Universal, tree-shakeable, TypeScript-first spreadsheets — .xlsx and .csv, read, write, and stream. Zero runtime dependencies.

CI npm version node license

excelents is a ground-up rewrite of ExcelJS for the modern JavaScript ecosystem. The ZIP container, XML engine, and CSV codec are all implemented in-house on web-standard APIs (CompressionStream, Web Streams, TextEncoder/TextDecoder), so the same code runs unmodified in Node, browsers, Deno, Bun, and edge runtimes — with no runtime dependencies at all.

  • Tiny. The entire library — reader, writer, streaming, CSV — is ~31 KB gzipped. ExcelJS's minified browser bundle is ~252 KB gzipped. Subpath entries and sideEffects: false mean a write-only app never bundles the reader.
  • Universal. The core contains zero node: imports (enforced by a purity test in CI). Node-specific filesystem helpers live behind the separate ./node entry.
  • TypeScript-first. Written in TypeScript; every shipped type is generated from the source, never hand-maintained. Checked with publint and Are the Types Wrong in CI.
  • Streaming. The ./stream entry reads and writes xlsx/CSV row-by-row over Web Streams with backpressure, so memory stays bounded regardless of file size.
  • Verified against ExcelJS. Every serialize/parse path is validated by a conformance harness that XML-diffs our output against [email protected] and round-trips 36 real-world fixture files.

Contents

Install

npm install @mitresthen/excelents

Requirements: the package is ESM-only and targets Node ≥ 24 or any runtime with CompressionStream/DecompressionStream support (all evergreen browsers, Deno, Bun, edge runtimes). See Runtime support.

Quick start

import { createWorkbook, writeXlsx, readXlsx } from '@mitresthen/excelents'

const wb = createWorkbook()
const ws = wb.addSheet('Sheet1')
ws.cell('A1').value = 'Hello'
ws.cell('B1').value = 42

const bytes: Uint8Array = await writeXlsx(wb) // a complete .xlsx file

const restored = await readXlsx(bytes)
restored.sheets[0]?.cell('A1').value // 'Hello'

In the browser, turn the bytes into a download:

const blob = new Blob([await writeXlsx(wb)], {
  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
// hand `URL.createObjectURL(blob)` to an <a download> ...

Feature tour

Cell values

A cell's value is a plain discriminated union — string, number, boolean, Date, null, or one of three object shapes:

ws.cell('A1').value = new Date('2026-01-01')                    // date (serial-date encoded)
ws.cell('A2').value = { formula: 'SUM(B1:B9)', result: 42 }     // formula + cached result
ws.cell('A3').value = { text: 'docs', hyperlink: 'https://example.com' }
ws.cell('A4').value = {
  richText: [{ text: 'bold', font: { bold: true } }, { text: ' plain' }],
}

readXlsx restores the same shapes, and cell.type tells you which one you got ('string' | 'number' | 'boolean' | 'date' | 'formula' | 'richText' | 'hyperlink' | 'null').

Styling

Styles are one plain object per cell — fonts, fills, borders, alignment, and number formats:

ws.cell('A1').style = {
  font: { name: 'Calibri', size: 12, bold: true, color: 'FFFFFFFF' },
  fill: { type: 'pattern', pattern: 'solid', fgColor: 'FF4472C4' },
  border: { bottom: { style: 'thin', color: 'FF000000' } },
  alignment: { horizontal: 'right', indent: 1, wrapText: true },
  numberFormat: '#,##0.00',
}

Layout: merges, sizing, freezing, filters

ws.merge('A1:C1')               // merged cells
ws.column(1).width = 24         // column width (column A)
ws.getRow(1).height = 30        // row height
ws.freeze({ rows: 1, cols: 1 }) // frozen panes
ws.setAutoFilter('A1:F1')       // autoFilter over the header range

Note: freeze, setAutoFilter, alignment indent, and images are write-side features — they serialize into the file, but readXlsx does not currently parse them back (see Roadmap). Merges, sizing, values, and the rest of the styles round-trip fully.

Images

Embed PNG/JPEG/GIF images (write-side), anchored to a cell and sized in pixels:

const logo = wb.addImage({ data: pngBytesOrBase64, extension: 'png' }) // -> image id
ws.placeImage(logo, { tl: 'F1', size: { width: 180, height: 101 } })

Tables, data validation & defined names

ws.addTable({
  name: 'Sales',
  ref: 'A1:C4',
  columns: ['Region', 'Q1', 'Q2'],
  styleName: 'TableStyleMedium2',
})

ws.addDataValidation({
  sqref: 'B2:B100',
  type: 'list',
  formula1: '"Small,Medium,Large"', // in-cell dropdown
})

wb.defineName('TaxRate', 'Sheet1!$B$1')

All three survive a writeXlsxreadXlsx round-trip.

CSV

RFC 4180 reader and writer in a dedicated ~0.1 KB entry:

import { writeCsv, readCsv } from '@mitresthen/excelents/csv'

const csv = writeCsv(wb, { bom: true })        // pass a Workbook or a Worksheet
const parsed = readCsv('name,qty\nwidget,42')  // -> Workbook (numbers/booleans inferred)

Both take options for delimiters, quoting, BOM, sheet selection, and inference (CsvWriteOptions / CsvReadOptions).

Streaming (@mitresthen/excelents/stream)

For large spreadsheets, the ./stream entry produces and consumes rows incrementally, so memory stays bounded regardless of file size. It's a separate, tree-shakeable entry that does not pull in the buffered codecs.

Write

Functional form — rows is any (async) iterable of cell-value arrays:

import { writeXlsxStream } from '@mitresthen/excelents/stream'

async function* rows() {
  yield ['name', 'qty']
  yield ['widget', 42]
}

const stream: ReadableStream<Uint8Array> = writeXlsxStream(rows(), { sheet: 'Data' })
// pipe `stream` to a file or HTTP response

Builder form — push rows imperatively; awaiting addRow applies backpressure:

import { createXlsxStreamWriter } from '@mitresthen/excelents/stream'

const writer = createXlsxStreamWriter({ sheet: 'Data' })
// writer.readable: ReadableStream<Uint8Array>
await writer.addRow(['name', 'qty'])
await writer.addRow(['widget', 42])
await writer.close()

Read

readXlsxRows async-iterates worksheet rows without building the whole workbook. The source may be a Uint8Array, a Blob, or a ReadableStream<Uint8Array>:

import { readXlsxRows } from '@mitresthen/excelents/stream'

for await (const { sheet, rowNumber, cells } of readXlsxRows(bytes)) {
  // cells: CellValue[] indexed by column (index 0 = column A)
  console.log(sheet, rowNumber, cells)
}

Streaming CSV

import { writeCsvStream, readCsvRows } from '@mitresthen/excelents/stream'

const csvStream = writeCsvStream(rows()) // ReadableStream<Uint8Array>

for await (const row of readCsvRows(csvStream)) {
  // row: CellValue[]  (readCsvRows also accepts a plain string)
}

Node filesystem helpers (@mitresthen/excelents/node)

The core never touches the filesystem. In Node, use the adapter to read/write files and to bridge Node streams to Web Streams:

import { createWorkbook, writeXlsx, readXlsx } from '@mitresthen/excelents'
import { writeXlsxStream } from '@mitresthen/excelents/stream'
import { nodeFileSystem } from '@mitresthen/excelents/node'

// buffered
await nodeFileSystem.writeFile('report.xlsx', await writeXlsx(wb))
const wb2 = await readXlsx(await nodeFileSystem.readFile('report.xlsx'))

// streaming, bounded memory
await writeXlsxStream(rows()).pipeTo(nodeFileSystem.createWritable('big.xlsx'))

Entry points

| Import | Contents | Size (gzip) | | --- | --- | --- | | @mitresthen/excelents | createWorkbook, writeXlsx, readXlsx, the model classes, all public types, version | ~8 KB + shared chunks | | @mitresthen/excelents/csv | writeCsv, readCsv | ~0.1 KB + shared chunks | | @mitresthen/excelents/stream | writeXlsxStream, createXlsxStreamWriter, readXlsxRows, writeCsvStream, readCsvRows | ~7 KB | | @mitresthen/excelents/node | nodeFileSystem (Node FS ↔ Web Streams adapter) | ~0.3 KB |

Everything is named exports with sideEffects: false, so bundlers drop whatever you don't use — per-entry gzip budgets are asserted in CI on every commit.

Performance

100,000 rows × 10 columns (5 strings + 5 numbers per row), Apple M4 Max, Node 24. Each case runs in a fresh process; peak RSS sampled in-process. Reproduce with pnpm build && pnpm bench.

| Scenario | excelents | exceljs | excelents peak RSS | exceljs peak RSS | | --- | --- | --- | --- | --- | | buffered write | 7.3 s | 3.3 s | 991 MB | 1457 MB | | buffered read | 0.8 s | 1.5 s | 605 MB | 793 MB | | streaming write | 1.6 s | 0.7 s | 124 MB | 161 MB | | streaming read | 0.8 s | 1.4 s | 225 MB | 358 MB |

Honest summary: reads are ~2× faster and every path uses meaningfully less memory, but ExcelJS currently wins on raw write throughput — write-path optimization is on the roadmap. For large exports the streaming writer's bounded memory (124 MB vs ~1 GB buffered) matters more than the seconds.

Runtime support

| Runtime | Status | | --- | --- | | Node.js ≥ 24 | ✅ Tested in CI | | Evergreen browsers | ✅ Tested in CI (Vitest browser mode, real Chromium) | | Deno / Bun / edge (Workers, etc.) | ✅ Expected to work — the core uses only web-standard APIs |

The only platform capabilities required are CompressionStream/DecompressionStream with deflate-raw, Web Streams, and TextEncoder/TextDecoder.

The package ships ESM only (since v0.2.0). If you're on CommonJS, use dynamic import().

Coming from ExcelJS?

excelents began as a fork of [email protected] (unmaintained upstream since 2023) and was rewritten from scratch: new architecture, new public API, no CommonJS, no dependencies. It is not a drop-in replacement, but the model maps closely and correctness is continuously verified against ExcelJS itself. The complete mapping — including style-model subsets and behavioral gotchas — is in the migration guide; the short version follows.

Start by scanning your codebase — it reports every ExcelJS call site as auto-mappable, needing restructuring, or without an equivalent (add --json for tooling and coding agents):

npx @mitresthen/excelents .

What's supported

Cell values (strings, numbers, booleans, dates, formulas with cached results, rich text, hyperlinks) · styling (fonts, fills, borders, alignment, number formats) · merged cells · row heights & column widths · tables · data validation · defined names · frozen panes, autoFilter, alignment indent & embedded images (write-side) · CSV · streaming read/write.

What was dropped from the fork

These ExcelJS features were deliberately cut and are not planned:

| Dropped | Notes | | --- | --- | | Cell comments / notes | Rarely round-trips cleanly; cut to keep the model small | | Pivot tables | ExcelJS support was partial/experimental to begin with | | Encryption / password protection | Out of scope | | Page setup, print options, headers/footers | Not carried over — open an issue if you need this | | Worksheet/workbook protection | Not carried over | | CommonJS build | ESM-only since v0.2.0 |

Still on the roadmap (in scope, not yet shipped): conditional formatting, and parsing the write-side features (images, frozen panes, autoFilter, alignment indent) back out of workbooks on read.

API mapping

| ExcelJS | excelents | | --- | --- | | new Excel.Workbook() | createWorkbook() | | wb.addWorksheet('Data') | wb.addSheet('Data') | | ws.getCell('A1').value = 1 | ws.cell('A1').value = 1 | | ws.getCell('A1').font = {...} | ws.cell('A1').style = { font: {...} } | | cell.numFmt = '0.00%' | cell.style = { numberFormat: '0.00%' } | | ws.mergeCells('A1:B2') | ws.merge('A1:B2') | | ws.getColumn(1).width = 20 | ws.column(1).width = 20 | | ws.autoFilter = 'A1:C1' | ws.setAutoFilter('A1:C1') | | ws.views = [{ state: 'frozen', ySplit: 1 }] | ws.freeze({ rows: 1 }) | | wb.xlsx.writeBuffer() | writeXlsx(wb) | | wb.xlsx.load(buffer) | readXlsx(bytes) | | wb.xlsx.writeFile(path) | nodeFileSystem.writeFile(path, await writeXlsx(wb)) | | wb.csv.writeBuffer() | writeCsv(wb) | | new Excel.stream.xlsx.WorkbookWriter(...) | createXlsxStreamWriter(...) / writeXlsxStream(...) |

The general pattern: workbooks are built through a small model API, and serialization is free functions (writeXlsx, readXlsx, writeCsv, …) instead of methods on the workbook — that's what makes the unused codecs tree-shake away.

FAQ

Does it evaluate formulas? No — like ExcelJS, it stores formula text plus an optional cached result. Excel recalculates on open.

Why Node ≥ 24? That's what CI tests against. The core is pure web-standard code, so earlier Node versions with deflate-raw support will likely work, but they're not verified.

How do I know the output is correct? Every codec path runs through a conformance harness in CI: workbooks are built with both excelents and exceljs, unzipped, canonicalized, and XML-diffed part by part; the read path parses 36 real-world fixture workbooks with both libraries and compares the resulting models.

Buffered or streaming — which do I pick? writeXlsx/readXlsx build the whole workbook in memory and support every feature (styles, images, tables, …). The ./stream entry handles arbitrarily large files with bounded memory, but works row-by-row with plain values. Reports → buffered; exports/imports of millions of rows → streaming.

Is the API stable? Yes — since 1.0.0 the package follows semver: breaking changes only land in major versions. Roadmap features (conditional formatting, write-side read-back) arrive as minors. See the CHANGELOG and GitHub Releases.

Roadmap

  • Conditional formatting (write + read)
  • Write-path performance (currently slower than ExcelJS — see Performance)
  • Parse the write-side features (images, frozen panes, autoFilter, indent) back out on read

Contributing

git clone https://github.com/mitresthen/excelents
cd excelents
pnpm install
pnpm test        # vitest (node + browser projects), incl. the conformance suite

| Script | What it does | | --- | --- | | pnpm build | Build dist/ with tsdown (Rolldown) | | pnpm typecheck | tsc --noEmit | | pnpm lint / pnpm lint:fix | oxlint (type-aware) | | pnpm format / pnpm format:fix | oxfmt | | pnpm test | Vitest (node project), including the ExcelJS conformance oracle | | pnpm test:browser | The public-API suite in real Chromium (needs pnpm exec playwright install chromium once) | | pnpm size | Assert per-entry gzip budgets (size-budget.json) | | pnpm smoke | Pack the tarball and import it from a scratch project |

Design documents for each subsystem live in docs/superpowers. Releases are automated via npm trusted publishing — see RELEASING.md.

License

MIT

excelents descends from ExcelJS by Guyon Roche and contributors — the fixture suite and the conformance oracle stand on their work. 🙏