Node
Everything in the core entry works in Node. The /node entry adds the things that need node: modules: a
file-backed source, two file-backed sinks, and a zlib deflater with a compression level.
import { createWorkbookWriter, fromFile, nodeDeflater, openWorkbook, toFile, toWritable } from '@jetstreamapp/simple-excel/node';
The /node entry re-exports the whole core surface, so you never need both imports.
fromFile(path)
function fromFile(path: string): Promise<FileSource>;
A RandomAccessSource over a file, using positional reads on a FileHandle. The reader pulls only the ranges it
needs — the central directory, the package parts, then each sheet as it is iterated — so opening a 2 GB workbook
does not read 2 GB.
const source = await fromFile('/tmp/upload.xlsx');
const workbook = await openWorkbook(source);
for await (const row of workbook.sheet(0).rows({ mode: 'object' })) {
await handle(row);
}
await workbook.close(); // closes the file handle
workbook.close() closes the source. If you never open a workbook over it, call source.close() yourself.
toFile(path)
function toFile(path: string): ByteSink;
Streams to a file through fs.createWriteStream, honouring back-pressure — a slow disk slows the row loop rather
than filling memory. workbook.close() ends the stream and resolves once the file is flushed.
const workbook = createWorkbookWriter(toFile('accounts.xlsx'));
const sheet = workbook.addSheet('Accounts', { header: ['Id', 'Name'] });
await sheet.writeRows(rows);
await sheet.close();
await workbook.close();
toWritable(writable)
function toWritable(writable: NodeWritableLike): ByteSink;
The same sink over any Node Writable: an HTTP response, a socket, a PassThrough, a gzip stream. The
parameter is structurally typed (write, end, destroy, once, off), so the core types stay free of Node
types and you can pass a test double.
app.get('/export.xlsx', async (request, response) => {
response.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
response.setHeader('Content-Disposition', 'attachment; filename="export.xlsx"');
const workbook = createWorkbookWriter(toWritable(response), { deflater: nodeDeflater(1) });
const sheet = workbook.addSheet('Export', { header });
try {
for await (const record of query()) {
await sheet.writeRow(toRow(record));
}
await sheet.close();
await workbook.close();
} catch (error) {
await workbook.abort(error);
throw error;
}
});
Streaming a response like this means the browser starts downloading immediately and the server never holds the
file. A stream error is sticky: the sink remembers it and every later call rejects with the original reason, so
you find out at the first writeRow rather than at the end.
nodeDeflater(level?)
function nodeDeflater(level?: number): DeflaterFactory;
A deflater over zlib.createDeflateRaw, so you get a compression level — which the platform CompressionStream
does not expose (ADR-005). Levels are zlib's 1–9.
const fast = createWorkbookWriter(toFile('big.xlsx'), { deflater: nodeDeflater(1) });
const small = createWorkbookWriter(toFile('small.xlsx'), { deflater: nodeDeflater(9) });
Level 1 is roughly 2–3× faster than the default on xlsx-shaped XML, which is highly repetitive and compresses well even at the low setting. For a large export that is usually the right trade.
nodeDeflater also honours compression: 'store': with that option the factory returns a pass-through and the
level is ignored.
When you need it
The core entry works in Node without this one — collectToBytes() and fromWritableStream(Writable.toWeb(...))
both do the job. Reach for /node when you want:
| Want | Use |
|---|---|
| To read a file without loading it into memory | fromFile |
| To write a file without buffering it | toFile |
| To stream into a response or socket | toWritable |
| A compression level | nodeDeflater(level) |
Node version
package.json declares engines.node >= 20. Node's own CompressionStream only accepts the deflate-raw
format from 21.2, so on Node 20 the writer detects that and falls back to stored (uncompressed) parts — pass
nodeDeflater(), which goes through zlib and works everywhere, to keep files compressed. That is the right default for server-side writes regardless, since it is the only way
to choose a compression level.