@uekichinos/quire
A small, dependency-light .xlsx writer for Node and the browser.
- One runtime dependency —
fflate for zip packaging
- Writer only — no OOXML reader, so none of the parser-side CVE surface
(zip bombs, path traversal, prototype pollution) that affects reader libraries
- Worksheets · typed cells (string / number / boolean /
Date / formula) ·
a de-duplicated style pool · merged cells · column widths · freeze panes ·
auto-filter · row heights
- Returns a
Uint8Array (or Blob); identical in Node and the browser
- Deterministic output (stable bytes for stable input)
Install
npm install @uekichinos/quire
Usage
import { createWorkbook } from '@uekichinos/quire'
const wb = createWorkbook()
const sheet = wb.addWorksheet('Sales')
const header = { font: { bold: true, color: 'FFFFFF' }, fill: '2F5597' }
sheet.addRow(
[
{ value: 'Product', style: header },
{ value: 'Revenue', style: header },
{ value: 'Updated', style: header },
],
)
sheet.addRow(['Widget', { value: 15003.4, style: { numFmt: '#,##0.00' } }, new Date(2024, 2, 1)])
sheet.addRow(['Gadget', { value: 2450, style: { numFmt: '#,##0.00' } }, new Date(2024, 2, 3)])
sheet.addRow(
['Total', { value: { formula: 'SUM(B2:B3)', result: 17453.4 } }],
{ style: { font: { bold: true }, border: { top: { style: 'thin' } } } },
)
sheet.merge('A1:C1')
sheet.setColumn(1, { width: 24 })
sheet.freeze({ ySplit: 1 })
sheet.autoFilter('A1:C1')
sheet.addRow(['tall row'], { height: 30 })
sheet.setCell('E1', 'note', { align: { wrapText: true } })
wb.addWorksheet('Q2', { columns: [{ width: 20 }], freeze: { ySplit: 1 }, autoFilter: 'A1:C1' })
const bytes = wb.xlsx()
import { writeFileSync } from 'node:fs'
writeFileSync('sales.xlsx', bytes)
const url = URL.createObjectURL(wb.blob())
API
createWorkbook(): Workbook
addWorksheet(name, options?) | options: { columns?, freeze?, autoFilter? }. Name: 1–31 chars, unique, no \ / ? * [ ] : |
xlsx() → Uint8Array | deterministic |
blob() → Blob | spreadsheetml.sheet mime |
addRow(values, { style?, height? }) | values: CellInput[] — a bare value or { value, style } |
setCell(ref, value, style?) | write to any A1 reference |
setRow(i, { style?, height? }) | style/size a row even with no cells |
setColumn(i, { width?, hidden?, style? }) | 1-based; column style resolves column < row < cell |
merge(range) | 'A1:C1'; rejects single-cell / backwards / overlapping |
freeze({ xSplit?, ySplit? }) | frozen panes; freeze({}) clears |
autoFilter(range) | header filter dropdowns |
Cell values: string · number · boolean · Date · { formula, result? } · null.
Dates use the 1900 serial system (default format yyyy-mm-dd). null /
undefined cells are skipped but keep column position. Non-finite numbers and
pre-1900 dates throw.
CellStyle: font (name, size, bold, italic, underline, color)
· fill ('RRGGBB' / 'AARRGGBB') · align (horizontal, vertical,
wrapText, indent) · border (top/right/bottom/left/all →
{ style, color? }) · numFmt (format code or built-in id). Every distinct
style is interned once. Cell style merges over row style over column style, one
nested level deep.
Run node examples/hello.mjs (after pnpm build) for a full example.
Scope
quire is not a drop-in ExcelJS replacement. It targets the common
"export a styled spreadsheet" case with a tiny, auditable surface.
Not included (see PLAN.md): reading .xlsx, images, charts,
pivot tables, data validation, conditional formatting, rich text, a streaming
writer, .xls / .xlsb. A minimal reader for files from mainstream tools may
come later.
Performance
In-memory; ~100k rows × 5 cols serialise in ~1 second. Comfortable for typical
exports; for millions of rows, wait for a streaming writer or chunk across
sheets.
License
MIT © uekichinos. Portions adapted from
ExcelJS (MIT) — see NOTICE.