Skip to main content

simple-excel

@jetstreamapp/simple-excel reads and writes .xlsx files in browsers and in Node, with no runtime dependencies.

It writes a workbook by pushing rows into a sink as they arrive and reads one by pulling rows through an async iterator. Nothing accumulates: there is no in-memory worksheet, no whole-sheet XML string and no unbounded shared string table. A one-million-row export costs about as much memory as a one-thousand-row export — measured, that is 77 MB of JS heap growth to write 1,000,000 × 20 columns of Salesforce-shaped data, against 45 MB for 100,000 rows of the same, where SheetJS throws RangeError: Invalid string length. (Those runs used the optional shared-string table, before inline strings became the default.) The whole browser entry is 29.0 KB brotli with no dependencies. Streaming and memory has the numbers and the method.

info

The library is at 0.1 on npm, and the API may still shift before 1.0. CHANGELOG.md records every release.

What it is for​

Exporting and importing tabular data — query results, record loads, report extracts — in files that have to open cleanly in Excel and come back out carrying the same values.

import { collectToBlob, createWorkbookWriter } from '@jetstreamapp/simple-excel';

const sink = collectToBlob();
const workbook = createWorkbookWriter(sink);
const sheet = workbook.addSheet('Accounts', { header: ['Id', 'Name', 'Created'], freeze: { rows: 1 } });

await sheet.writeRow(['001xx000003DGb2AAG', 'Acme', new Date(2024, 2, 10)]);

await sheet.close();
await workbook.close();
const blob = await sink.result();
import { openWorkbook } from '@jetstreamapp/simple-excel';

const workbook = await openWorkbook(file);
for await (const row of workbook.sheet(0).rows({ mode: 'object' })) {
console.log(row.Id, row.Name);
}
await workbook.close();

When to pick it​

Pick simple-excel when:

  • the file is large, or you cannot predict how large it will be;
  • it has to open in Excel without a repair prompt, and in Google Sheets, LibreOffice and Numbers too;
  • you are running in a browser tab, a Web Worker or an extension service worker, where a multi-gigabyte heap spike ends the session;
  • you want to know exactly what happens to a date, a 40,000-character cell or a control character, because the answer is written down;
  • adding a dependency tree to a code path that touches untrusted uploads is not acceptable.

Pick something else when:

  • you need to edit an existing workbook and preserve the parts you did not touch;
  • you need formulas, charts, pivot tables, conditional formatting or data validation;
  • you need to read .xls, .xlsb or .ods (simple-excel detects each and says what it is, but does not parse it);
  • you need Excel's number formats rendered to display text.

Comparison has the library-by-library reasoning.

How the pieces fit​

ConceptWhat it is
ByteSinkWhere written bytes go: a Blob, a byte array, a WritableStream, a file. See Writing
WorkbookWriterOwns the zip, the styles and, under strings: 'auto', the shared strings; hands out one SheetWriter at a time
SourceInputWhere read bytes come from: ArrayBuffer, Uint8Array, Blob/File, or a RandomAccessSource
Workbook / SheetLists sheets without touching sheet XML; streams rows on demand. See Reading
XlsxErrorEvery failure, carrying a stable code. See Errors

Where to go next​

  • Install — package, entry points, environment requirements
  • Writing — sinks, sheets, rows, styles, options
  • Reading — opening, listing sheets, array and object rows
  • Dates and values — the value model and its edge cases
  • Errors — every error code and when it fires
  • Streaming and memory — how both sides stay flat, and what to do with very large files
  • Compatibility — what was verified, against what
  • Node — the /node entry
  • Comparison — the longer version of the README table
  • Contributing — repo layout, commands, fixtures