
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@uekichinos/quire
Advanced tools
Small, dependency-light .xlsx read + write for Node and the browser. Worksheets, typed cells, styles, dates, formulas, merged cells — one dependency, no legacy bloat.
A small, dependency-light .xlsx reader + writer for Node and the browser.
fflate for zip packagingDate / formula) ·
a de-duplicated style pool · merged cells · column widths · freeze panes ·
auto-filter · row heights. Deterministic Uint8Array / Blob outputnpm install @uekichinos/quire
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' } } } },
)
// layout
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 } })
// …or up front
wb.addWorksheet('Q2', { columns: [{ width: 20 }], freeze: { ySplit: 1 }, autoFilter: 'A1:C1' })
const bytes = wb.xlsx() // Uint8Array
// Node
import { writeFileSync } from 'node:fs'
writeFileSync('sales.xlsx', bytes)
// Browser
const url = URL.createObjectURL(wb.blob())
createWorkbook(): WorkbookWorkbook | |
|---|---|
addWorksheet(name, options?) | options: { columns?, freeze?, autoFilter? }. Name: 1–31 chars, unique, no \ / ? * [ ] : |
xlsx() → Uint8Array | deterministic |
blob() → Blob | spreadsheetml.sheet mime |
Worksheet (all chainable) | |
|---|---|
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.
0.2.0)import { readWorkbook } from '@uekichinos/quire'
const wb = readWorkbook(bytes) // Uint8Array | ArrayBuffer
wb.sheetNames // ['Sales', …]
const s = wb.sheet('Sales') // by name or index
s.dimension // { rows, cols }
s.merges // ['A1:C1', …]
s.cell('B2') // { ref, row, col, type, value, formula? }
s.values() // (string|number|boolean|Date|null)[][]
for (const row of s.rows()) { // sparse: row[col-1]
console.log(row.map((c) => c?.value))
}
ReadCell.type is 'string' | 'number' | 'boolean' | 'date' | 'formula' | 'error' | 'empty'. Numbers with a date format become Date (opt out with
readWorkbook(bytes, { dates: false })). Every cell also carries .numFmt (the
resolved format code) when it has one. Formula cells carry both .formula (no
leading =) and .value (the cached result). { sheets: [name|index] } loads
a subset.
readWorkbookAsync(bytes, options?) returns the same ReadWorkbook but yields
to the event loop between sheets, so a large import doesn't block. s.values({ ragged: true }) keeps each row's own length instead of padding to the sheet
width.
Limits. readWorkbook(bytes, { limits }) caps maxTotalBytes (100 MB),
maxPartBytes (50 MB), maxCells (5 000 000) and maxSheets (256); the
archive is inflated part-by-part and aborted mid-stream when a cap trips. Over
the limit throws QuireError.
Styles on read — readWorkbook(bytes, { styles: true }) resolves a
ReadStyle onto each cell (font, fill, border, align, numFmt), mirror
of the writer's CellStyle. rgb colours resolve exactly; indexed colours use
the standard palette; theme colours are read from xl/theme (with the 0/1 swap
and an approximate tint). Off by default — most imports only need values.
Deliberately strict and small. The XML is parsed by a ~200-line in-house
tokenizer (not a dependency) that rejects <!DOCTYPE>, <!ENTITY>, <
Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.