xlsx-stream-writer
v1.3.2
Published
Create xlsx in streaming mode in browser and nodejs, with no dependencies
Maintainers
Readme
xlsx-stream-writer
Create .xlsx files in streaming mode, in the browser and in Node.js.
Built for one job: writing very large spreadsheets with simple formatting, without holding all the rows in memory.
No dependencies. The ZIP container is written by this package, compressing
through node:zlib on the server and CompressionStream in the browser.
Rewritten from the CoffeeScript node-xlsx-writer and changed to run in both environments. The API is completely different from that implementation.
Install
npm install xlsx-stream-writerRequires Node.js 20.19 or newer. In the browser it needs CompressionStream
(Chrome 80+, Firefox 113+, Safari 16.4+) and a bundler that honours the
browser field, which is all of them.
Projects that cannot take those requirements can stay on the 0.2 line, which carries the same corruption fixes and keeps the old behaviour:
npm install xlsx-stream-writer@legacyComing from 0.2.x? See docs/migrating-to-1.x.md — written to be followed step by step.
Rows from an array
const XlsxStreamWriter = require("xlsx-stream-writer");
const fs = require("fs");
const rows = [
["Name", "Location"],
["Alpha", "Adams"],
["Bravo", "Boston"],
["Charlie", "Chicago"],
];
const xlsx = new XlsxStreamWriter();
xlsx.addRows(rows);
xlsx.getFile().then(buffer => {
fs.writeFileSync("result.xlsx", buffer);
});getFile() resolves to a Buffer in Node.js and a Blob in the browser.
Rows from a stream
addRows accepts an array, a Node.js readable stream, a web ReadableStream, or
any iterable or async iterable of rows — so you can feed it a database cursor or
a generator directly:
const XlsxStreamWriter = require("xlsx-stream-writer");
const fs = require("fs");
async function* fetchRows() {
yield ["Name", "Location"];
for await (const record of database.stream("SELECT name, location FROM city")) {
yield [record.name, record.location];
}
}
const xlsx = new XlsxStreamWriter();
xlsx.addRows(fetchRows());
xlsx.getFile().then(buffer => {
fs.writeFileSync("result.xlsx", buffer);
});If the source fails part-way through, getFile() rejects with that error.
Streaming the workbook out
getFile() builds the whole archive in memory. For workbooks too large for
that, getStream() returns a ReadableStream of the archive bytes, so rows go
in while bytes come out:
const { Readable } = require("node:stream");
const { pipeline } = require("node:stream/promises");
const fs = require("node:fs");
const xlsx = new XlsxStreamWriter();
xlsx.addRows(generateRows());
await pipeline(
Readable.fromWeb(xlsx.getStream()),
fs.createWriteStream("large.xlsx"),
);Writing 500 000 rows × 4 columns this way produces a 12 MB file — 80 MB of
worksheet XML — in about 30 seconds, with peak memory around 240 MB. Most of
that is the shared-string table; with inlineStrings: true the same export peaks
near 120 MB and takes half the time, at the cost of a slightly larger file. If
your strings are mostly distinct, prefer inline strings.
Cell values
| Value | Written as |
| ---------------------------- | ------------------------------------------------- |
| string | shared string, or an inline string |
| number (finite), bigint | number |
| boolean | boolean — Excel shows TRUE / FALSE |
| Date | Excel date serial; apply a date format style to display it as a date |
| null, undefined, NaN | blank cell |
| Infinity, -Infinity | blank cell — Excel has no representation for them |
| anything else | its toString(); objects that have no meaningful one raise an error |
Characters that XML 1.0 cannot represent — most control characters, unpaired surrogates — are removed, since leaving them in produces a file Excel refuses to open.
A worksheet is limited to 1 048 576 rows and 16 384 columns. Exceeding either raises rather than producing a file that will not open.
Options
const xlsx = new XlsxStreamWriter({
// Write strings into the sheet directly instead of into a shared-string
// table. Larger output, but no string table to hold in memory.
inlineStrings: false,
// Cell formats, referenced by index from styleIdFunc. Index 0 is the
// implicit default, so the first entry here is style 1.
styles: [{ fill: "FFFF0000" }, { format: "dd.mm.yyyy" }],
// Choose a style per cell.
styleIdFunc: (value, columnIndex, rowIndex) => (rowIndex === 0 ? 1 : 0),
// Deflate level, 0-9. Node only; browsers expose no level control.
compressionLevel: 4,
});fill is an ARGB colour like FFFF0000. format is an Excel number format
string like 0.00 or dd.mm.yyyy.
Lifecycle
A writer builds one workbook: call addRows once, then getFile or getStream
once. Calling either again raises, because the row stream has been consumed.
Create a new XlsxStreamWriter for the next workbook.
Archives are reproducible — entry timestamps are fixed rather than "now", so the same rows always produce the same bytes.
Plans
Shipped items live in the changelog; what is left:
- A prebuilt browser bundle on a CDN. The package works through any bundler
today, but there is nothing to drop into a
<script>tag. - A smaller shared-string table. It is the memory ceiling on large exports —
500k rows peak around 240 MB with it and 120 MB with
inlineStrings. Worth either a cheaper structure or spilling it once it outgrows a threshold. - Multiple sheets. The most common request the current one-sheet shape cannot answer.
- Column widths and a frozen header row. Small, and the two bits of formatting people ask for immediately after the data lands.
TypeScript
The package is written in TypeScript and ships its own declarations — no
@types package needed:
import XlsxStreamWriter = require("xlsx-stream-writer");
const styles: XlsxStreamWriter.CellStyle[] = [{ format: "dd.mm.yyyy" }];
const rows: XlsxStreamWriter.Row[] = [["When"], [new Date()]];
const xlsx = new XlsxStreamWriter({ styles });
xlsx.addRows(rows);
const buffer = await xlsx.getFile();Contributing
npm ci
npm test # builds, then runs the suite and the type checks
npm run build # tsc onlyTests run against the compiled dist/, so they exercise the artifact that
actually ships — go through npm test rather than the test runner directly, or
you will be reading whatever was built last.
- docs/migrating-to-1.x.md — upgrading from 0.2.x
- AGENTS.md — commands and the conventions tooling does not enforce
- docs/architecture.md — why the ZIP writer and stream plumbing are shaped the way they are
- docs/verifying-output.md — how output correctness is established
- docs/RELEASING.md — the release runbook
Security
Reporting and threat model: SECURITY.md.
License
MIT
