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

exceljs-stream-utils

v0.1.6

Published

Streaming Excel read/write utilities built on top of exceljs

Downloads

434

Readme

exceljs-stream-utils

Streaming Excel helpers built on top of @protobi/exceljs, a maintained ExcelJS-based streaming implementation.

  • Read .xlsx as async objects
  • Write iterable or async-iterable rows to .xlsx
  • Process large files in bounded batches
  • Built-in timezone-aware date handling for read/write flows

Requirements

  • Node.js >= 18

Install

npm install exceljs-stream-utils

@protobi/exceljs is installed automatically as a dependency of this package.

This package builds on top of:

API

The package now has two entrypoints:

  • exceljs-stream-utils: existing behavior, kept stable for compatibility
  • exceljs-stream-utils/v2: improved behavior for new code

Read

  • readXlsxRows(input, options)

Write

  • writeXlsxRows(output, rows, options)

Processing

  • processXlsxRows(input, handler, options)

Compatibility aliases are still exported:

  • xlsxStreamToObjects
  • objectsToXlsxStream
  • processXlsxLarge

V2

Import from exceljs-stream-utils/v2 to opt into the safer API without affecting existing users.

import {
  readXlsxRows,
  writeXlsxRows,
  processXlsxRows,
} from 'exceljs-stream-utils/v2';

v2 currently improves a few edge cases:

  • writeXlsxRows(output, rows) now works without requiring an options object
  • Formula result cells are normalized through the same date handling path as other cells
  • Duplicate headers are detected by default instead of silently overwriting earlier columns
  • processXlsxRows drains in-flight work before surfacing batch errors

Automated Bun tests currently cover:

  • file path input and output
  • Node readable and writable stream transport
  • timezone date round-trips
  • array parsing, trimming, and header normalization
  • root API compatibility and legacy aliases
  • a realistic 10,000-row write-and-process scenario

Parameters

Unless noted otherwise, the parameter tables below describe the /v2 entrypoint. The root entrypoint keeps the older compatibility behavior.

readXlsxRows(input, options?)

input types:

  • string (file path)
  • Node Readable
  • Web ReadableStream<Uint8Array>

options (XlsxReadOptions, all optional):

| Name | Type | Default | Description | | ----------------------- | ------------------------------- | --------------------------------- | --------------------------------------------------------------- | | sheetName | string | first matching sheet | Read only this sheet name | | headerRowNumber | number | 1 | Row number used as header row | | trimHeaders | boolean | true | Trim header cell text | | trimTextValues | boolean | false | Trim all string cell values | | trimTextColumns | string[] | [] | Trim only these columns (used when trimTextValues is false) | | arrayColumns | string[] | [] | Convert these string columns to arrays | | arrayDelimiter | string | "," | Delimiter for arrayColumns | | trimArrayItems | boolean | true | Trim each array item after split | | removeEmptyArrayItems | boolean | true | Remove empty items from split arrays | | skipEmptyRows | boolean | true | Skip rows where all cells are empty | | normalizeHeader | (header, index) => string | identity | Custom header normalizer | | duplicateHeaders | 'error' \| 'suffix' | 'error' | /v2 only. Error on duplicate headers or suffix them as _2, _3, etc. | | parseDates | boolean | true | Convert date-like numeric cells to Date | | date1904 | boolean | false | Use Excel 1904 epoch mode | | timeZone | string | undefined | Reinterpret read date wall-clock in this IANA time zone |

writeXlsxRows(output, rows, options?)

output types:

  • string (output file path)
  • Node Writable

rows types:

  • Iterable<Record<string, unknown>>
  • AsyncIterable<Record<string, unknown>>

options (XlsxWriteOptions, all optional):

| Name | Type | Default | Description | | ------------------ | --------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------- | | sheetName | string | 'Sheet1' | Worksheet name | | columns | { header: string; key: string; width?: number }[] | inferred from first row | Explicit column order and labels | | useStyles | boolean | false | Enable ExcelJS style writing | | useSharedStrings | boolean | false | Enable ExcelJS shared strings writing | | timeZone | string | undefined | Convert Date values to target time zone wall-clock | | dateColumns | string[] | [] | Restrict timezone conversion to these date columns; empty means all Date fields |

processXlsxRows(input, handler, options?)

Parameters:

  • input (required): same accepted types as readXlsxRows
  • handler (required): (row) => Promise<void> | void
  • options (optional): all XlsxReadOptions plus:

| Name | Type | Default | Description | | ------------- | -------- | ------- | -------------------------------------- | | batchSize | number | 2000 | Rows per processing batch | | concurrency | number | 8 | Max concurrent handlers within a batch |

Quick start

import { readXlsxRows, writeXlsxRows } from 'exceljs-stream-utils';

for await (const row of readXlsxRows('/tmp/input.xlsx')) {
  console.log(row);
}

await writeXlsxRows('/tmp/output.xlsx', [{ id: 1, name: 'John' }], {
  sheetName: 'Sheet1',
});

How to use

1) Read from file

import { readXlsxRows } from 'exceljs-stream-utils';

for await (const row of readXlsxRows('/tmp/input.xlsx', {
  headerRowNumber: 1,
  trimHeaders: true,
  trimTextValues: true,
  parseDates: true,
  timeZone: 'Asia/Manila',
  arrayColumns: ['tags'],
  arrayDelimiter: ',',
})) {
  console.log(row);
}

2) Write to file

import { writeXlsxRows } from 'exceljs-stream-utils';

await writeXlsxRows(
  '/tmp/out.xlsx',
  [
    { accountNumber: '1234', amount: 100, dueDate: new Date() },
    { accountNumber: '5678', amount: 200, dueDate: new Date() },
  ],
  {
    sheetName: 'Debtors',
    columns: [
      { header: 'Account #', key: 'accountNumber' },
      { header: 'Amount', key: 'amount' },
      { header: 'Due Date', key: 'dueDate' },
    ],
    timeZone: 'Asia/Manila',
    dateColumns: ['dueDate'],
  },
);

3) Write from async iterator

import { writeXlsxRows } from 'exceljs-stream-utils';

async function* debtorRows() {
  yield { accountNumber: '1234', amount: 100, dueDate: new Date() };
  yield { accountNumber: '5678', amount: 200, dueDate: new Date() };
}

await writeXlsxRows('/tmp/streamed.xlsx', debtorRows(), {
  sheetName: 'Debtors',
  columns: [
    { header: 'Account #', key: 'accountNumber' },
    { header: 'Amount', key: 'amount' },
    { header: 'Due Date', key: 'dueDate' },
  ],
});

4) Stream directly to S3

import { PassThrough } from 'node:stream';
import { Upload } from '@aws-sdk/lib-storage';
import { writeXlsxRows } from 'exceljs-stream-utils';

const pass = new PassThrough();

const upload = new Upload({
  client: s3Client,
  params: {
    Bucket: 'my-bucket',
    Key: 'exports/debtors.xlsx',
    Body: pass,
  },
});

const uploadPromise = upload.done();

await writeXlsxRows(pass, debtorRows(), {
  sheetName: 'Debtors',
  timeZone: 'Asia/Manila',
  dateColumns: ['dueDate'],
});

await uploadPromise;

5) Process very large files in batches

import { processXlsxRows } from 'exceljs-stream-utils';

await processXlsxRows(
  '/tmp/input.xlsx',
  async (row) => {
    // handle one row
  },
  {
    batchSize: 2000,
    concurrency: 8,
    trimTextValues: true,
    parseDates: true,
  },
);

6) Export a realistic large dataset

import { writeXlsxRows } from 'exceljs-stream-utils/v2';

async function* debtorExportRows() {
  for (let id = 1; id <= 10000; id += 1) {
    yield {
      debtorId: `D-${id.toString().padStart(5, '0')}`,
      email: `user${id}@example.com`,
      balanceCents: id * 125,
      dueDate: new Date('2024-01-01T00:00:00.000Z'),
      tags: id % 2 === 0 ? 'priority,renewal' : 'standard',
    };
  }
}

await writeXlsxRows('/tmp/debtors.xlsx', debtorExportRows(), {
  sheetName: 'Debtors',
  columns: [
    { header: 'Debtor ID', key: 'debtorId' },
    { header: 'Email', key: 'email' },
    { header: 'Balance', key: 'balanceCents' },
    { header: 'Due Date', key: 'dueDate' },
    { header: 'Tags', key: 'tags' },
  ],
  timeZone: 'Asia/Manila',
  dateColumns: ['dueDate'],
});

Notes

  • writeXlsxRows supports both Iterable and AsyncIterable rows.
  • columns is optional; if omitted, columns are inferred from the first row.
  • Set timeZone in read/write options to enable built-in timezone date conversion.
  • writeXlsxRows accepts either a file path or a Node Writable and handles writable backpressure.

Type Behavior

  • String cells are returned as strings.
  • Boolean cells are returned as booleans.
  • Integer and decimal numeric cells are returned as numbers.
  • Empty cells are returned as null.
  • Date cells are returned as Date objects when date parsing is enabled.

Excel date values are commonly stored as numeric serials plus cell formatting metadata. Because of that, automatic date parsing depends on the workbook indicating that a numeric cell is date-like, usually through its Excel number format. This is a normal Excel parsing constraint, not a Node-specific quirk.

If you want raw numeric serials instead of parsed Date values, set:

{ parseDates: false }

Build and publish

bun run build

Release flow follows the local npm-publish skill process.