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

@sebbebroman/exceljs

v0.2.0

Published

Builder-first Excel (xlsx) for modern bundlers. ESM TypeScript fork of ExcelJS.

Readme

@sebbebroman/exceljs

Read, manipulate, and write Excel workbooks (.xlsx) with a builder-first ESM API.

Fork of ExcelJS aimed at modern Node and bundlers (Vite, SvelteKit, Rollup, esbuild).

0.2.0 — builder-first public API (., ./node, and optional ./protection, ./csv). Write, load, CSV, advanced sheet features, Node streaming. Breaking changes: CHANGELOG.md. See MIGRATION.md and ARCHITECTURE.md.

Install

npm install @sebbebroman/exceljs
# or
pnpm add @sebbebroman/exceljs

Requirements: Node.js >= 22.

Quick start

import { workbook } from '@sebbebroman/exceljs';

const buffer = await workbook({ creator: 'Reports' })
  .sheet('Data')
  .row(['name', 'value'])
  .row(['alpha', 1])
  .style('A1:B1', { font: { bold: true } })
  .writeBuffer();

Node file write

import { workbook, writeFile } from '@sebbebroman/exceljs/node';

await writeFile(
  'out.xlsx',
  workbook().sheet('Data').rows([
    ['name', 'value'],
    ['alpha', 1],
  ]),
);

Nested sheets

const buffer = await workbook()
  .sheet('A', s => s.row([1]).row([2]))
  .sheet('B', s => s.cell('A1', 'hello'))
  .writeBuffer();

Title row + table

.columns([{ header }]) appends a header row after any rows already written, so a title-then-table chain is safe. Prefer SheetInit.title (or fluent .title()) for the common case:

const buffer = await workbook().sheet('Report', {
  title: {
    text: 'Q1 Revenue',
    style: { font: { bold: true, size: 16 } },
    merge: 'A1:B1',
  },
  columns: [
    { header: 'Product', key: 'p', width: 20 },
    { header: 'Revenue', key: 'r', width: 12 },
  ],
  rows: [
    { p: 'Widgets', r: 12000 },
    { p: 'Gadgets', r: 8000 },
  ],
}).writeBuffer();

// Equivalent fluent form:
// workbook().sheet('Report').title({ text: 'Q1 Revenue', merge: 'A1:B1' })
//   .columns([...]).rows([...])

Plain snapshot (no I/O)

const data = workbook().sheet('S').rows([[1, 2]]).build();
// { meta, sheets: [...] }

Load + edit

import { workbook, load } from '@sebbebroman/exceljs';

const data = await load(buffer); // plain { meta, sheets }
const out = await workbook(data)
  .sheet('Data')
  .cell('A1', 'updated')
  .writeBuffer();

Import CSV / xlsx — view + rows (recommended)

import { viewWorkbook } from '@sebbebroman/exceljs';
// writes are a separate import so pure readers tree-shake better:
import { workbook, writeBuffer } from '@sebbebroman/exceljs';

const data = reader.result as ArrayBuffer;
const view = await viewWorkbook(data, {
  format: 'auto',
  filename: file.name, // optional sniff (.xlsx / .xlsm); CSV uses viewCsv()
});

view.sheetNames; // ['Sheet1', ...]
const sheet = view.sheet(0);

// Dense string grid with optional row/col slice (1-based inclusive)
const rows = sheet.rows({
  start: 1,
  end: 100,
  cols: { start: 1, end: 4 }, // or cols: ['A', 'D']
  // values: 'cell' for CellValue[][]
});

// Header → objects
const records = sheet.records({ header: true });

// Opt-in write from the same view
const buf = await writeBuffer(workbook(view));

One-liner sugar (same as first sheet .rows({ values: 'string' })):

import { readRows } from '@sebbebroman/exceljs';
const rows = await readRows(data, { filename: file.name, start: 1, end: 50 });

Supports CSV and OOXML (.xlsx / .xlsm / …). Not .xls / .xlsb.

Read path choice: load() / readFile() parse workbook models (values, styles, merges, tables, …) for re-encode, with the round-trip limitations listed below. viewWorkbook() / readRows() are values-only (no styles, merges, formulas, hyperlinks, dates stay numeric) and must not be used for round-trip writes expecting fidelity — workbook(view).writeBuffer() keeps values only.

Node file read

import { readFile, writeFile, workbook } from '@sebbebroman/exceljs/node';

const data = await readFile('in.xlsx');
await writeFile('out.xlsx', workbook(data).sheet('Sheet1').cell('A1', 'x'));

Node streaming write (large row counts)

Prefer streamWrite when rows are produced incrementally or the sheet is huge — rows are committed as they are written (bounded memory). Not available from the browser entry.

import { streamWrite, streamRead } from '@sebbebroman/exceljs/node';

// Declarative: async iterable / array / generator
await streamWrite('big.xlsx', {
  useSharedStrings: true,
  sheets: [
    {
      name: 'Data',
      columns: [
        { header: 'Id', key: 'id', width: 10 },
        { header: 'Name', key: 'name', width: 24 },
      ],
      rows: (async function* () {
        for (let i = 1; i <= 100_000; i++) {
          yield { id: i, name: `row-${i}` };
        }
      })(),
    },
  ],
});

// Callback: full control over multi-sheet flow
await streamWrite('report.xlsx', async w => {
  const sheet = w.sheet('Events', {
    columns: [{ header: 'Ts' }, { header: 'Msg' }],
  });
  await sheet.rows(eventSource());
});

// Optional: stream rows back without a full plain model
for await (const { sheetName, rowNumber, values } of streamRead('big.xlsx')) {
  // values[1] is column A (one-based sparse layout)
  console.log(sheetName, rowNumber, values[1]);
}

CSV

import {workbook} from '@sebbebroman/exceljs';
import {csv} from '@sebbebroman/exceljs/csv';

// Parse text → sheet → xlsx
const init = await csv.parse('name,value\nalpha,1');
const buffer = await workbook().sheet('Data', init).writeBuffer();

// Stringify the first sheet (use sheetName to select another)
const text = await csv.stringify(workbook()
  .sheet('Data')
  .rows([
    ['name', 'value'],
    ['alpha', 1],
  ]));

Node:

import { readCsvFile, writeCsvFile } from '@sebbebroman/exceljs/node';

const data = await readCsvFile('in.csv'); // plain Workbook (one sheet)
await writeCsvFile('out.csv', data);

Entry points

| Import | Purpose | |--------|---------| | @sebbebroman/exceljs | Builder, writeBuffer, load, enums (browser-safe) | | @sebbebroman/exceljs/csv | Optional CSV parse/stringify, viewCsv, and readCsvRows | | @sebbebroman/exceljs/protection | Optional synchronous sheetProtection(password, options) factory | | @sebbebroman/exceljs/node | + writeFile / readFile / streamWrite / streamRead / readCsvFile / writeCsvFile |

The supported runtime entry points are the main entry, /node, /protection, and /csv; ./package.json is also exported. There is no ./stream/xlsx, or default ExcelJS class. Types resolve to excel.d.ts and node.d.ts. The mutable document API has been removed; stream coordinators are internal.

Password hashing is opt-in, so unused protection crypto can be tree-shaken out even in single-file browser bundles:

import {workbook} from '@sebbebroman/exceljs';
import {sheetProtection} from '@sebbebroman/exceljs/protection';

const bytes = await workbook().sheet('Data').row([1, 2])
  .protect(sheetProtection('secret'))
  .writeBuffer();

.protect() now takes a prepared protection model; the previous password/options overload has been removed. For passwordless protection, use .protect({sheet: true}). Hashing runs synchronously when the optional factory is called. Loaded hashes can be re-encoded through the core without importing crypto.

Pipeline overview: ARCHITECTURE.md (op-log → materialize → XLSX).

CSV views and dense rows use viewCsv(data) and readCsvRows(data) from /csv; core viewWorkbook and readRows accept XLSX inputs.

Browser / bundlers

Core path uses fflate for zip. No readable-stream / npm buffer polyfills required for writeBuffer.

import { workbook } from '@sebbebroman/exceljs';

const buffer = await workbook()
  .sheet('Sheet1')
  .row(['a', 1])
  .writeBuffer();
// download via Blob…

Tree-shaking

  • Named exports only; CSV has a separate opt-in entry.
  • "sideEffects": false (importing the package does not extend dayjs; CSV helpers extend it on first use).
  • Import @sebbebroman/exceljs/node only in Node code paths.
  • writeBuffer and load are separate modules (read does not pull write).
  • CSV lives in /csv; use csv.stringify(builder) rather than a builder method.

Advanced XLSX features are detected and loaded automatically. Dynamic imports keep their code out of the initial download when the bundler supports code splitting; single-file bundles still include them.

Dense export optimizations

When the builder op-log is rectangular only (.sheet / .columns / .row / .rows — no random .cell patches, styles, or merges), materialize uses a bulk addRows path and keeps compact cell storage (no full Cell class graph per value). Consecutive .row() calls are fused into one .rows op; same-address .cell writes are last-write-wins. If the builder never applied styles, writeBuffer defaults useStyles: false (override with { useStyles: true }).

Benchmarks (vs exceljs@4)

exceljs is a devDependency for head-to-head e2e benches (write / read / round-trip) via mitata:

pnpm bench                 # Node: full suite (default 5000×8)
pnpm bench:write
pnpm bench:read
pnpm bench:roundtrip
pnpm bench -- --rows 20000
pnpm bench -- --filter write --rows 1000
pnpm bench:dense           # internal dense-write microbench
pnpm bench:browser         # Chrome headless: size + write/read vs exceljs browser build
pnpm bench:browser -- --rows 1000 --runs 5

Contenders (Node): @sebbebroman/exceljs builder, exceljs@4 (npm).

Contenders (browser): @sebbebroman/exceljs esbuild browser bundle vs exceljs official dist/exceljs.min.js (their browser field). Node often favors exceljs; browser compares the polyfill-heavy UMD build against the ESM/fflate path. Needs Chrome (CHROME_PATH override supported).

Status (0.2.0)

| Phase / feature | Status | |-----------------|--------| | Phase 1–2: builder + writeBuffer / Node writeFile | ✅ | | Phase 3: dense write path / size-minded materialize | ✅ | | Phase 4: load / readFile + edit loop | ✅ | | Phase 5: views, pageSetup, headerFooter, validations, CF, notes, protect, tables, images, defined names | ✅ | | Phase 6: named csv, Node streamWrite / streamRead | ✅ | | Phase 7: public API cleanup (builder-only exports + docs) | ✅ | | Builder write (rows, cells, styles, merges, columns) | ✅ | | CSV (/csv parse/stringify/views / Node file helpers) | ✅ | | Streaming (streamWrite / streamRead on ./node) | ✅ | | Sheet protection | ✅ write; load re-encodes hash (password not recoverable) | | Tables / images / defined names | ✅ write; load best-effort | | Pivot builder API | later (no .pivot() yet — read/write of pivot tables is best-effort via load) | | Drop internal DocWorkbook bridge | ✅ buffered and streaming paths; mutable document API removed |

Column headers: ColumnInput.header as an array uses only the first line — multi-row headers are not supported and extra lines are dropped consistently across build() / writeBuffer() / streamWrite().

Browser bundle sizes (indicative)

Measured by pnpm test:bundle-size using esbuild browser ESM single-file minification. Advanced XLSX features remain automatic; CSV and password hashing are optional imports.

| Entry | Minified | Gzip | | --- | ---: | ---: | | Basic write | ~157.1 KiB | ~45.6 KiB | | Full load | ~130.2 KiB | ~36.9 KiB | | readRows | ~33.8 KiB | ~13.6 KiB | | viewWorkbook | ~33.3 KiB | ~13.5 KiB |

Sizes depend on the imported API, bundler and options. Regression budgets also cover the complete initial static import closure and reachable async chunks. pnpm test:work-budgets checks deterministic work counts without timing thresholds.

Re-run after encoder changes. Details: ARCHITECTURE.md.

API

Public types: excel.d.ts. Migration from 4.x: MIGRATION.md. Architecture: ARCHITECTURE.md.

Attribution

Derived from ExcelJS, originally created by Guyon Roche and maintained by exceljs/exceljs contributors.

License

MIT

Copyright (c) 2014–2019 Guyon Roche.

ExcelJS contributions by the ExcelJS team and contributors.

Fork modifications copyright (c) 2026 SebbeBroman and contributors.