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

pure-xlsx

v0.2.1

Published

Zero-dependency, lightweight .xlsx (Excel) generator for Node.js, written in TypeScript. JSON/CSV to styled Excel files, buffered or streaming.

Readme

pure-xlsx

Zero-dependency .xlsx writer for Node.js, written in TypeScript. It turns JSON, CSV or any (async) iterable into styled Excel files, either buffered or streamed with flat memory.

  • No runtime dependencies. It uses only node:zlib, node:stream and node:fs.
  • Styling: bold, italic, underline, strike, size, colour and font name; fills; borders; alignment and wrap; number formats; per column, per row or per cell. Identical styles are deduplicated.
  • Sheet features: header row, column widths (explicit or auto), freeze panes, auto filter, merged cells, row heights, tab colour, RTL, hidden sheets and columns, multiple sheets.
  • Values: numbers, text, booleans, Date, BigInt, formulas (with cached result), hyperlinks (external and internal) and per-cell styled values. NaN/Infinity become #NUM! and invalid dates become #VALUE!.
  • Safe by default:
    • Text is always written as text, so CSV-style formula injection (=cmd|...) is never executed.
    • XML-illegal characters are stripped.
    • Text longer than 32,767 characters is truncated with a warning, or rejected when strict is set.
    • Excel limits are enforced: 1,048,576 rows, 16,384 columns and sheet-name rules.
  • CSV:
    • Parsing follows RFC 4180 and is incremental.
    • The delimiter is detected automatically and a BOM is removed.
    • Types are inferred conservatively: leading-zero codes and numbers longer than 15 digits stay text.
  • Streaming:
    • Rows are pulled only as fast as the destination consumes bytes.
    • A ZIP64 archive is written automatically when it is needed.
    • The source iterator is closed if the consumer stops early.
  • Packaging: ESM and CommonJS builds with .d.ts types. Requires Node ≥ 20.

Install

npm install pure-xlsx

CommonJS

const { Workbook, jsonToXlsx } = require('pure-xlsx');

Both ESM and CommonJS builds ship with types; no @types package is needed.

Quick start

import { Workbook } from 'pure-xlsx';

const wb = new Workbook({ creator: 'My App' });
const sheet = wb.addSheet('Sales', {
  columns: [
    { header: 'Order', key: 'id', width: 8 },
    { header: 'Customer', key: 'customer' },          // auto width
    { header: 'Amount', key: 'amount', numFmt: '#,##0.00' },
    { header: 'Date', key: 'date' },                    // Date -> real Excel date
  ],
  headerStyle: { font: { bold: true, color: '#FFFFFF' }, fill: '#1F4E78', border: 'thin' },
  freeze: true,        // freeze the header row
  autoFilter: true,    // filter buttons on the header
});

sheet.addRow({ id: 1, customer: 'Ravi', amount: 1234.5, date: new Date() });
sheet.addRow({ id: 2, customer: 'सीता', amount: 99, date: new Date() }, { fill: '#F2F2F2' }); // row style
sheet.addRow(['', 'Total', { formula: 'SUM(C2:C3)', result: 1333.5 }]);

await wb.writeFile('sales.xlsx');          // or: await wb.toBuffer() / wb.toStream()

JSON → xlsx

import { jsonToXlsx } from 'pure-xlsx';

const buffer = await jsonToXlsx(users, {
  sheetName: 'Users',
  headerStyle: { font: { bold: true }, fill: '#DDEBF7' },
  freeze: true,
});

Object keys become columns in order of first appearance. Rows with missing keys leave the cell empty.

CSV → xlsx

import { csvToXlsx, csvToXlsxStream } from 'pure-xlsx';
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

const buffer = await csvToXlsx(csvText, { freeze: true });

// Large files: constant memory
await pipeline(csvToXlsxStream(createReadStream('big.csv')), createWriteStream('big.xlsx'));

Options:

  • delimiter: detected automatically when omitted.
  • header: default true.
  • inferTypes: default true. Numbers and ISO dates are converted, while 007 and 16-digit IDs stay text.
  • encoding, quote and maxRecordLength.

Streaming large data (DB cursor, generator, API pages)

import { xlsxStream, writeXlsx } from 'pure-xlsx';

// Express / HTTP
app.get('/export', (req, res) => {
  res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  res.setHeader('Content-Disposition', 'attachment; filename="export.xlsx"');
  xlsxStream({ name: 'Orders', rows: db.orders.cursor(), columns: ['id', 'customer', 'total'], freeze: true })
    .on('error', (err) => res.destroy(err))
    .pipe(res);
});

// File
await writeXlsx('orders.xlsx', [
  { name: 'Orders', rows: ordersAsyncIterable },
  { name: 'Refunds', rows: refundsArray, rowStyle: (row, i) => (i % 2 ? undefined : ZEBRA) },
]);

How streaming behaves:

  • Text is written inline by default, so memory does not grow with unique strings. Pass { sharedStrings: true } for smaller files when text repeats a lot.
  • The first sampleRows rows (default 100) are read ahead to learn object keys and column widths. Pass columns when keys appear late in the data.
  • If generation fails, writeXlsx(path) deletes the partial file.

Recipes

Download from an Express route (buffered):

app.get('/report.xlsx', async (_req, res) => {
  const buf = await jsonToXlsx(await db.users.findAll(), { sheetName: 'Users', freeze: true });
  res.type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
     .set('Content-Disposition', 'attachment; filename="report.xlsx"')
     .send(buf);
});

Handle errors by type:

import { XlsxError, XlsxInputError, XlsxLimitError, XlsxTooLargeError } from 'pure-xlsx';

try { await csvToXlsx(upload); }
catch (e) {
  if (e instanceof XlsxTooLargeError) return res.status(413).end();   // input too big
  if (e instanceof XlsxLimitError || e instanceof XlsxInputError) return res.status(400).end(); // Excel limit / bad argument
  throw e;
}

Collect warnings instead of printing them (for example truncated cells):

const warnings: string[] = [];
const wb = new Workbook({ onWarning: (w) => warnings.push(String(w)) });

Several sheets from one dataset, each with its own columns:

await writeXlsx('report.xlsx', [
  { name: 'Orders', rows: orders, columns: ['id', 'customer', 'total'] },
  { name: 'Refunds', rows: refunds },
]);

Troubleshooting

| Symptom | Cause / fix | |---|---| | ID like 007 or a 16-digit number looks wrong | Pass it as a string (JSON) or keep inferTypes on for CSV: such values stay text on purpose | | A column is missing in a streamed export | Its key first appeared after sampleRows rows; pass columns explicitly | | Dates are shifted by hours | Dates are written in UTC; use dateMode: 'local' for server-local time | | XlsxLimitError on 1M+ rows | One sheet holds 1,048,576 rows; split the data across sheets | | Text starting with = shows as text | Intended (formula-injection protection). Use { formula: '...' } for real formulas |

Cell values

| JS value | Excel | |---|---| | number | number (NaN/Infinity → #NUM!) | | string | text (never a formula) | | boolean | TRUE / FALSE | | Date | date serial + date format (yyyy-mm-dd, or yyyy-mm-dd hh:mm:ss when it has a time) | | bigint | number if it is a safe integer, otherwise text | | null / undefined | empty | | { formula: 'SUM(A1:A9)', result?: 45 } | formula (Excel recalculates on open) | | { link: 'https://…', text?: 'Open', tooltip? } | hyperlink; link: "#'Sheet 2'!A1" is internal | | { value, style } | value with its own style | | other objects / arrays | JSON text |

Dates are read in UTC by default, which is deterministic on servers. Use new Workbook({ dateMode: 'local' }) for server-local time. Dates before 1900-03-01, which Excel cannot represent, are written as ISO text.

Styles

const style = {
  font: { bold: true, italic: true, underline: true, strike: false, size: 12, color: '#C00000', name: 'Arial' },
  fill: '#FFF2CC',
  border: 'thin',                                   // or { style: 'medium', color: '#999' } or { bottom: 'double' }
  alignment: { horizontal: 'center', vertical: 'center', wrapText: true, indent: 1, textRotation: 45 },
  numFmt: '#,##0.00',                               // '0%', 'yyyy-mm-dd', '"₹"#,##0' ...
};

Styles layer in this order: column → row → cell. Each layer only overrides the parts it sets. Text containing line breaks gets wrapText automatically.

API summary

  • new Workbook(options?)
    • .addSheet(name?, sheetOptions?), .getSheet(name), .sheets
    • .toBuffer(), .toStream(), .writeFile(path)
  • Worksheet
    • .addRow(row, style?), .addRows(rows, style?)
    • .setCell('B3' | row, col, value, style?)
    • .mergeCells('A1:C1'), .setRowHeight(row, pt), .setColumnWidth(col, width), .rowCount
  • xlsxStream(sheets, options?) → Readable; writeXlsx(pathOrWritable, sheets, options?)
  • jsonToXlsx(rows, options?), csvToXlsx(text, options?), csvToXlsxStream(input, options?)
  • parseCsv(text, options?), parseCsvStream(input, options?), CsvParser, inferCsvValue, countCsvRecords(input, options?) (exact row count without parsing)
  • LIMITS: the library's capacity, as constants
  • Errors: XlsxError, XlsxLimitError (Excel limits), XlsxTooLargeError (input over maxInputBytes) and XlsxInputError (bad arguments)

Workbook options

  • Document properties: creator, title, subject, company, created.
  • Dates: dateMode, dateFormat, dateTimeFormat.
  • Output: strict, compression (0–9), sharedStrings.
  • Font and warnings: fontName, fontSize, onWarning.

Sheet options

  • Columns and header: columns, header, headerStyle.
  • View: freeze, autoFilter, merges, tabColor, rightToLeft, showGridLines, hidden.
  • Sizes: autoWidth, minColumnWidth, maxColumnWidth, defaultRowHeight.
  • Streaming only: rowStyle, sampleRows, headerRow.

Limits and safety

| Limit | Behaviour | |---|---| | CSV string/Buffer input: 100 MB (LIMITS.csvMaxBytes) | XlsxTooLargeError immediately; nothing is parsed | | Streamed CSV input: 500 MB (LIMITS.csvStreamMaxBytes) | XlsxTooLargeError as soon as the stream crosses the limit; the rest is never read | | jsonToXlsx array longer than 1,048,576 rows | XlsxLimitError before the workbook is built | | csvToXlsx CSV with more than 1,048,576 rows | XlsxLimitError before parsing (rows are counted in one fast pass, ~0.3 s for 40 MB) | | 1,048,576 rows / 16,384 columns per sheet | XlsxLimitError (split big exports over several sheets) | | 32,767 characters per cell | truncated with one warning per sheet (strict: true throws) | | 65,530 hyperlinks per sheet | extra links are written as plain text, one summary warning | | 64,000 distinct cell styles | XlsxLimitError; reuse style objects instead of creating one per row | | Sheet names | ≤ 31 chars, unique, no \\ / ? * : [ ], control characters or "History" | | Hyperlink schemes | javascript:, vbscript:, data: links are written as plain text | | Text values | always text, never formulas, so formula injection is not possible |

Both CSV limits can be changed per call with maxInputBytes (Infinity turns the check off). XlsxTooLargeError has code: 'ERR_XLSX_TOO_LARGE', maxBytes and receivedBytes; map it to HTTP 413. All capacities are exported as LIMITS, so your app can reject oversized uploads before the library sees them:

import multer from 'multer';
import { csvToXlsx, LIMITS, XlsxTooLargeError } from 'pure-xlsx';

const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: LIMITS.csvMaxBytes } }); // nothing written to disk

app.post('/convert', upload.single('file'), async (req, res) => {
  try {
    const xlsx = await csvToXlsx(req.file.buffer, { freeze: true });
    res.type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet').send(xlsx);
  } catch (err) {
    res.status(err instanceof XlsxTooLargeError ? 413 : 400).json({ error: err.message });
  }
});

Warnings go to onWarning when it is given, otherwise to process.emitWarning. If generation fails, writeFile / writeXlsx(path) delete the partial file.

Tip: for 300k rows, compression: 1 was about 35% faster than the default (level 6) and the file was ~15% bigger. compression: 0 was about 2.5× faster, but the file was ~8× bigger.

Live HTTP example

node examples/server.mjs starts a server with three endpoints:

  • /report: a buffered report.
  • /export?rows=1000000: a streamed export. If the client disconnects, the source is closed.
  • POST /csv: an uploaded CSV body comes back as .xlsx.

Performance

Measured on an Apple Silicon Mac with Node 20.19 (node --expose-gc bench/bench.mjs 1000000). Each row has 6 columns: number, text, Hindi text, amount, date and boolean.

| Mode | Rows | Time | File | Peak memory (heap + buffers) | |---|---|---|---|---| | xlsxStream | 1,000,000 | ~4.3 s | 34 MB | ~25 MB (flat) | | csvToXlsx (49 MB CSV Buffer) | 1,000,000 | ~4.5 s | 28 MB | ~100 MB (includes the 28 MB output) | | Workbook.toBuffer | 200,000 | ~1.0 s | 6.5 MB | ~110 MB |

Use Workbook for reports that fit in memory and xlsxStream / writeXlsx for exports of any size.

Development

npm test          # build (ESM + CJS) and run node:test suites
node examples/demo.mjs
node --expose-gc bench/bench.mjs 1000000

License

MIT