mrkeyoor.com_
Thu 06 Aug 15:41 UTC
npmDataupdated 06 Aug 2026

xlsx

xlsx is the npm name for SheetJS Community Edition, a pure JavaScript reader and writer for spreadsheet files. It parses XLSX, XLSB, XLS, XLML, ODS, CSV, DBF, SYLK and a dozen other formats into one normalised workbook object, and writes most of them back out. The same code runs in Node, the browser, Deno, Bun and React Native, with no native dependency and no headless Excel anywhere. Its real value is the format coverage: someone will hand you a .xls that Excel 97 produced, and this is the library that opens it. The thing you must know before installing is that the npm package has not been updated since March 2022. SheetJS moved distribution off npm to their own CDN, so npm serves 0.18.5 while the current release is 0.20.3, and two high-severity advisories that were fixed in 0.19.3 and 0.20.2 have no fixed version available on npm at all.

Verdict

As a format reader SheetJS is still unmatched and worth having when users upload arbitrary spreadsheets. Do not install it from npm: pin the CDN tarball or the @e965 mirror, because the registry copy is four years stale and carries two high-severity advisories with no patched version.

API stability5/5read, write, readFile, writeFile and the utils namespace have kept the same shape for years, and code written against 0.16 generally runs on 0.20. Later releases added convenience overloads such as book_new(ws, name) without changing the existing calls.
Docs4/5docs.sheetjs.com is thorough: every option documented, a data-format reference explaining the cell object, and per-framework install guides for Node, Deno, Bun, Vite, Angular and React Native. It is also a sales surface, so the answer to styling questions is often the Pro edition, and the docs describe 0.20.x while npm serves 0.18.5, which quietly misleads anyone who installed from the registry.
Maintenance2/5The npm package has not been published since 24 March 2022 and the GitHub mirror was last pushed 18 April 2024, with its description redirecting to a self-hosted Gitea. Two high-severity advisories against the npm package still list no patched version. Upstream 0.20.3 exists and is fine; the distribution channel most people use has been abandoned.
Ecosystem4/5Around 12M weekly downloads and the default answer to 'read a spreadsheet in JavaScript' for a decade, so almost every question you have is already answered somewhere. The move off npm has started to fracture that: wrappers, mirrors and forks now disagree about which version is current.

Use it if

  • You have to accept whatever spreadsheet a user uploads: legacy .xls, .xlsb, .ods, a CSV with a Windows codepage, or an .xlsx that some Java library generated slightly wrong. Nothing else in JavaScript covers that spread
  • The same parsing code needs to run in the browser and on the server, for example validating an upload client-side before sending it
  • You are reading data out of sheets rather than producing formatted reports: sheet_to_json with a header row is a couple of lines and handles merged headers, sparse rows and dates once you set the options
  • You need a specific format conversion (XLSB to CSV, ODS to XLSX) as a one-off in a script, where install size and the CVE surface matter less
Skip it if

Setup reality

npm install xlsx gets you 0.18.5 and two unfixable audit findings. The install the SheetJS docs actually tell you to run is npm install https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz, which pins a tarball URL in your package.json: that gets you the patched code and zero dependencies instead of seven, at the cost of a lockfile entry your registry proxy, air-gapped mirror or Renovate config may refuse to handle. The community mirror @e965/xlsx publishes 0.20.3 to npm if you need a normal registry install, but it is a third party republishing someone else's code. Beyond that, bundler wiring is the usual friction: the package ships a CommonJS main and an .mjs module, and bundlers that tree-shake will strip the Node built-ins it lazily requires, so in Node ESM or a webpack target you often have to call XLSX.set_fs(fs) before readFile works and XLSX.stream.set_readable(Readable) before the stream helpers do. Reading is all-in-memory: readFile loads the whole workbook, so a 200 MB XLSB will use several times that in heap. Finally, dates arrive as Excel serial numbers unless you pass cellDates: true, and even then a file written by a Mac Excel with the 1904 epoch shifts everything by four years unless the workbook declares it.

Patterns

Get a version that is not four years oldinstall-a-patched-build

# what the SheetJS docs tell you to run
npm install --save https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz

# package.json ends up with:
#   "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"

# or, if a registry URL is required by your tooling:
npm install @e965/xlsx    # community mirror of 0.20.3

npm install xlsx gives you 0.18.5 and two high advisories with no fix available. The CDN tarball is the upstream-supported path and also drops the 7 transitive dependencies, but a URL dependency breaks offline mirrors, some private registries, and most automated update bots. Decide which pain you prefer before it is in a shared lockfile.

Read a workbook from diskread-file-node

import * as XLSX from 'xlsx';
import * as fs from 'node:fs';

XLSX.set_fs(fs);   // needed under Node ESM and most bundlers

const wb = XLSX.readFile('report.xlsx', { cellDates: true });
const ws = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json(ws);

The lazy require of node:fs inside the package does not survive ESM or bundling, so readFile throws 'Cannot access file system' until you call set_fs. wb.SheetNames preserves tab order; do not assume Object.keys(wb.Sheets) matches it. readFile reads the entire workbook into memory.

Parse a file the user pickedread-browser-upload

input.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const buf = await file.arrayBuffer();
  const wb = XLSX.read(buf, { type: 'array', cellDates: true });
  const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
  console.table(rows);
});

type: 'array' with an ArrayBuffer is the path that works everywhere; 'binary' with FileReader.readAsBinaryString is the old advice and mangles bytes in some browsers. Parsing happens on the main thread and blocks it, so move anything over a few megabytes into a Web Worker.

Control how rows come outsheet-to-json-options

// objects keyed by the first row
const objects = XLSX.utils.sheet_to_json(ws, { defval: null, raw: false });

// array of arrays, header row included, blanks preserved
const matrix = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '', blankrows: false });

// force your own column names regardless of the file
const named = XLSX.utils.sheet_to_json(ws, {
  header: ['sku', 'qty', 'price'],
  range: 1,   // skip the file's own header row
});

Without defval, empty cells are omitted from the object entirely, so row.price is undefined for some rows and present for others, which breaks every downstream .map(). raw: false runs each cell through its number format and gives you strings, which is what you want for display and wrong for arithmetic. header: 1 is the only mode that guarantees a rectangular result.

Stop dates arriving as 45123handle-dates

const wb = XLSX.read(buf, { type: 'array', cellDates: true });

// without cellDates you convert manually:
const cell = ws['B2'];
if (cell && cell.t === 'n' && cell.z && XLSX.SSF.is_date(cell.z)) {
  const parsed = XLSX.SSF.parse_date_code(cell.v);
  // { y, m, d, H, M, S }
}

Excel stores dates as serial numbers counted from an epoch, so a date column parses as numbers by default. cellDates: true converts them to JS Date objects at parse time, which is almost always what you want. The values are naive local times with no timezone, so a date typed in Sydney and parsed in a UTC container can shift by a day. Workbooks written by older Mac Excel use a 1904 epoch and are off by roughly four years if that flag is not in the file.

Build a workbook from JSON and save itwrite-workbook

const rows = [
  { sku: 'A-1', qty: 3, price: 9.99 },
  { sku: 'B-2', qty: 1, price: 24.5 },
];

const ws = XLSX.utils.json_to_sheet(rows);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Orders');
XLSX.writeFile(wb, 'orders.xlsx');

In 0.18.5 book_new() takes no arguments; the book_new(ws, name) shorthand you see in newer docs only exists from 0.20.3. Sheet names are capped at 31 characters and cannot contain : \ / ? * [ ], and book_append_sheet throws rather than truncating. Keys of the first object decide the column order, so objects with inconsistent shapes produce missing columns.

Trigger a download without touching the filesystemwrite-in-browser

const out = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([out], {
  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), { href: url, download: 'orders.xlsx' });
a.click();
URL.revokeObjectURL(url);

XLSX.writeFile does work in the browser and does the same dance internally, but doing it yourself is what you need as soon as you want to upload the bytes instead of saving them. revokeObjectURL matters: without it every export leaks the whole workbook until the tab closes.

Write a header row and size the columnsheaders-and-column-widths

const ws = XLSX.utils.aoa_to_sheet([['SKU', 'Quantity', 'Unit price']]);
XLSX.utils.sheet_add_json(ws, rows, { origin: 'A2', skipHeader: true });

ws['!cols'] = [{ wch: 12 }, { wch: 10 }, { wch: 14 }];
ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }];   // optional title row span

Column widths in wch are character counts, not pixels. Widths, row heights, merges and autofilter are about the extent of what the Community Edition writes; bold headers, fills and borders are not, because cell styles are a SheetJS Pro feature and setting cell.s on a free build is silently dropped on write. Frozen panes are not writable either.

Add rows to a sheet that already has dataappend-rows

XLSX.utils.sheet_add_json(ws, newRows, {
  origin: -1,        // first empty row at the bottom
  skipHeader: true,
});

// same for arrays of arrays
XLSX.utils.sheet_add_aoa(ws, [['total', total]], { origin: -1 });

origin: -1 means append below the last row of the current !ref range. If the sheet has stray formatting far down the grid, !ref covers it and your append lands hundreds of rows lower than expected; recompute !ref yourself if the source file is untrustworthy.

Walk the grid when sheet_to_json is not enoughiterate-cells

const range = XLSX.utils.decode_range(ws['!ref']);
for (let r = range.s.r; r <= range.e.r; r++) {
  for (let c = range.s.c; c <= range.e.c; c++) {
    const addr = XLSX.utils.encode_cell({ r, c });
    const cell = ws[addr];
    if (!cell) continue;              // sparse: missing means empty
    // cell.t type, cell.v raw value, cell.w formatted text, cell.f formula
    console.log(addr, cell.t, cell.v, cell.f);
  }
}

A worksheet is a plain object keyed by A1 addresses, and keys starting with ! are metadata rather than cells. Cells are only present if they have content, so always null-check. cell.w exists only when the file carried a formatted string or you parsed with cellNF.

Emit a large sheet without building one giant stringstream-csv-out

import { Readable } from 'node:stream';
import { createWriteStream } from 'node:fs';

XLSX.stream.set_readable(Readable);

XLSX.stream
  .to_csv(ws, { FS: ',', RS: '\n' })
  .pipe(createWriteStream('out.csv'));

The stream helpers are output only: to_csv, to_html and to_json all take a worksheet you have already parsed into memory, so they cap the size of the output string, not the size of the input. There is no streaming XLSX reader or writer in the Community Edition, which is the real ceiling on how big a file this library can handle.

Read only what you need from a big uploadlimit-what-you-parse

const wb = XLSX.read(buf, {
  type: 'array',
  sheetRows: 1000,       // stop after 1000 rows per sheet
  sheets: ['Data'],      // parse only this tab
  cellFormula: false,    // skip formula text
  cellHTML: false,
  cellStyles: false,
});

sheetRows is the cheapest defence against a user uploading a million-row file: it truncates during parse rather than after. Turning off cellFormula, cellHTML and cellStyles measurably cuts both time and memory on wide sheets. None of this protects you from the ReDoS advisory, which is why the version you install still matters.

Alternatives

PackageRegistryPick it when
exceljsnpmYou write .xlsx files that need styling, formulas or column formats, or you need a streaming writer for very large sheets
@e965/xlsxnpmYou want the patched 0.20.3 SheetJS code from a normal npm install rather than a CDN tarball URL
papaparsenpmThe files are really CSV and you want a fast streaming parser with worker support instead of a whole spreadsheet engine