
Security News
Agent Baseline Publishes Security Reference Architecture for Software Factories
Agent Baseline’s new reference architecture shows how to secure software factories with four boundaries enforced outside the AI agent.
@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 /
hyperlink / rich text) · a de-duplicated style pool · merged cells · column widths ·
freeze panes · auto-filter · row heights · data validation · conditional formatting
(highlight rules, colour scales, data bars) · comments · print setup (orientation,
fit-to-page, margins, print area). Deterministic Uint8Array / Blob output, plus a
low-memory streaming writer for very large sheetsreadRows mode
streams a sheet row-by-row for lower peak memory on large filesnpm 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 \ / ? * [ ] : |
defineName(name, sheetName, range) | workbook-scoped named range, e.g. wb.defineName('SalesRange', 'Sales', 'A1:B10') |
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 |
setDataValidation(range, rule, options?) | dropdown, or a numeric/date/text-length rule |
addConditionalFormat(range, rule) | highlight matching cells, a colour scale, or a data bar |
setComment(ref, text, { author? }) | a cell comment/note, independent of the cell's value |
setPageSetup(options) | orientation, paper size, fit-to-page/scale, margins, print area |
Cell values: string · number · boolean · Date · { formula, result? } · { hyperlink, text?, tooltip? } · RichTextRun[] · 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.
Hyperlinks: { hyperlink: 'https://…', text?, tooltip? } as a cell value —
text (defaults to the URL itself) becomes the cell's string, hyperlink any
URI (http(s)://, mailto:, …) up to 2,079 characters. Re-setting the cell to
a non-hyperlink value drops it. sheet.setCell('A1', { hyperlink, text }, style)
combines it with a style.
Defined names: wb.defineName(name, sheetName, range) — sheetName must
already be added, range a single cell ('A1') or a range ('A1:C3'). Names
follow Excel's identifier rules (start with a letter/_/\, then letters,
digits, _ or .; can't read as a cell reference; can't use the reserved
_xlnm. prefix) and must be unique (case-insensitively). Workbook-scoped only
— sheet-scoped names aren't supported yet.
Row/column default styles: sheet.setRow(i, { style }) / setColumn(i, { style }) now also mark the row/column itself with that default style (not
just each populated cell), so empty cells in a styled row or column show the
right formatting in Excel too.
Rich text: a cell value can be RichTextRun[] — [{ text, font? }, …] —
for multiple differently-formatted spans in one string, e.g. a bold word
mid-sentence. font is a subset of CellStyle.font (name, size, bold,
italic, underline, color) applied per run; the cell's own style (fill,
border, align, numFmt) still applies normally. Must have at least one run.
Data validation: sheet.setDataValidation(range, rule, options?) restricts
a range to one rule:
// dropdown from fixed choices, or a range reference
sheet.setDataValidation('C2:C100', { type: 'list', list: ['Open', 'In Progress', 'Done'] })
sheet.setDataValidation('D2:D100', { type: 'list', list: 'Lookup!$A$1:$A$5' })
// numeric / date / text-length rules — `value` takes a pair for between/notBetween
sheet.setDataValidation('E2:E100', { type: 'whole', operator: 'between', value: [0, 100] })
sheet.setDataValidation('F2:F100', { type: 'date', operator: 'greaterThan', value: new Date(2024, 0, 1) })
sheet.setDataValidation('G2:G100', { type: 'textLength', operator: 'lessThanOrEqual', value: 50 })
// shared options (2nd arg for list, 3rd arg otherwise) — allowBlank, prompt/error text
sheet.setDataValidation('C2:C100', { type: 'list', list: ['Open', 'Done'] }, {
promptTitle: 'Status', promptMessage: 'Pick one', allowBlank: false,
})
For list, none of the fixed choices may contain a comma, and the joined list
must fit Excel's 255-character limit. For the other types, operator is one
of between · notBetween · equal · notEqual · greaterThan · lessThan · greaterThanOrEqual · lessThanOrEqual.
Conditional formatting: sheet.addConditionalFormat(range, rule) — three
rule kinds:
// highlight cells matching a condition
sheet.addConditionalFormat('B2:B100', {
type: 'cellIs',
operator: 'greaterThan',
formula: 100,
style: { font: { bold: true, color: 'FF0000' }, fill: 'FFF2CC' },
})
// 2- or 3-colour scale (thresholds are the range's own min/mid/max)
sheet.addConditionalFormat('C2:C100', { type: 'colorScale', colors: ['FF0000', 'FFFF00', '00FF00'] })
// data bar (length scales between the range's own min and max)
sheet.addConditionalFormat('D2:D100', { type: 'dataBar', color: '638EC6' })
cellIs styling is limited to font.bold/italic/color and fill (Excel's
differential-format record); operator takes the same set as data validation,
with formula as a pair for between/notBetween.
Comments: sheet.setComment(ref, text, { author? }) attaches a note to a
cell, independent of whatever value (or no value) is there. Uses the classic
comment format (xl/commentsN.xml + a VML drawing for the indicator/popup),
not modern threaded comments.
Print setup: sheet.setPageSetup(options):
sheet.setPageSetup({
orientation: 'landscape',
paperSize: 9, // Excel paper-size code — 9 = A4, 1 = Letter
fitToWidth: 1, // fit to 1 page wide
fitToHeight: 0, // any number of pages tall
margins: { left: 0.5 }, // inches; unset sides fall back to Excel's own defaults
printArea: 'A1:F50',
})
printArea is written as the sheet's own _xlnm.Print_Area workbook-level
defined name. In the streaming writer, setPageSetup must be called
before that sheet's first addRow(), same as setColumn/freeze.
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)[][]
wb.definedNames // [{ name, sheetName?, range?, refersTo, hidden? }]
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 })) — formula cells get the same
treatment from their cached result. A string cell with multiple
differently-formatted spans comes back as .value: ReadRichTextRun[] instead
of a plain string. Every cell also carries .numFmt (the resolved format
code) when it has one, .hyperlink ({ target, tooltip? }) when it carries
one — external targets resolved via the worksheet's own relationships,
in-workbook links via location — and .comment ({ text, author }) when it
has a note, whether or not the cell itself has a value. 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.
Under the same option, sheet.columnStyles / sheet.rowStyles (Map<number, ReadStyle>, 1-based) expose column/row default styles — what an empty
cell (no <c> element at all) in that column/row would look like.
Defined names — wb.definedNames lists user-defined named ranges
(_xlnm._FilterDatabase and other Excel-internal names are excluded). Each
entry resolves sheetName/range when the reference is a simple single-sheet
range; anything else (a formula, a multi-area reference, a named constant)
keeps refersTo with sheetName/range left undefined.
Data validation — sheet.dataValidations lists list / whole /
decimal / date / textLength rules (other types — custom formula, time,
etc. — are skipped). A list rule resolves list (inline choices) or
formula (range reference); the others resolve operator and values
(numbers, or Dates for type: 'date' — one entry, or two for
between/notBetween).
Conditional formatting — sheet.conditionalFormats lists cellIs,
colorScale and dataBar rules as { ref, rule } (other rule kinds — icon
sets, top/bottom N, etc. — are skipped). cellIs resolves operator,
formula (one or two, as written), and style (a ReadStyle with only
font.bold/italic/color and fill populated — the differential-format's
own limited scope) under { styles: true }. colorScale resolves colors;
dataBar resolves color.
Print setup — sheet.pageSetup resolves orientation, paperSize,
fitToWidth/fitToHeight, scale, margins, and printArea (from the
sheet's own _xlnm.Print_Area defined name) — undefined when the sheet has
neither a <pageSetup>/<pageMargins> block nor a print area.
Deliberately strict and small. The XML is parsed by a ~200-line in-house
tokenizer (not a dependency) that rejects <!DOCTYPE>, <!ENTITY>, <![CDATA[>
and unknown entities outright — the DOCTYPE / entity-expansion class that
accounts for most XML-parser CVEs does not apply. Only an allow-list of parts is
extracted, with uncompressed-size caps enforced before and after inflation.
Still: parse untrusted uploads inside a worker with an overall time/memory limit.
Not read: images, charts, pivot tables, sheet-scoped defined names, data
validation types beyond list/whole/decimal/date/textLength, conditional-format
types beyond cellIs/colorScale/dataBar, threaded (modern) comments. Reading is
aimed at files from mainstream tools (Excel, Google Sheets, LibreOffice,
openpyxl, exceljs, quire) — not corrupt files or every vendor quirk. See
PLAN-READER.md.
readWorkbook() materialises every cell of every loaded sheet in memory.
readRows() streams one sheet's rows through a callback instead, so peak
memory scales with row width, not row count:
import { readRows } from '@uekichinos/quire'
const meta = readRows(bytes, 'Sales', (cells, row) => {
db.insert(row, cells.map((c) => c?.value))
})
meta.dimension // { rows, cols }
meta.dataValidations // sheet-level metadata is still resolved up front
meta.pageSetup
cells[i] is undefined for a blank cell, matching ReadWorksheet.rows().
Sheet-level metadata (merges, columnStyles/rowStyles, dataValidations,
conditionalFormats, pageSetup) comes back in the return value alongside
the stream. Two honest limits: hyperlinks and comments aren't attached to
streamed rows (resolving them needs a second pass over the sheet's cells,
which this mode specifically avoids holding — use readWorkbook if you need
them), and it's still one synchronous parse pass under the hood, same as
readWorkbook for a single sheet — this reduces memory, not blocking; there's
no async/streaming variant, since the hand-rolled tokenizer isn't
interruptible mid-parse.
quire is not a drop-in ExcelJS replacement. The writer covers the
common "export a styled spreadsheet" case; the reader covers "import the
values from a normal .xlsx". Out of scope for both (see PLAN.md
and PLAN-READER.md): images, charts, pivot tables,
.xls / .xlsb.
createWorkbook()'s xlsx() holds every cell in memory until you call it —
fine up to a few hundred thousand rows, wasteful well beyond that.
createStreamingWorkbook() renders and DEFLATEs each row as it's added instead,
so memory stays flat regardless of row count:
import { createStreamingWorkbook } from '@uekichinos/quire'
const wb = createStreamingWorkbook()
const sheet = wb.addWorksheet('Big', { columns: [{ width: 20 }], freeze: { ySplit: 1 } })
sheet.addRow(['ID', 'Name', 'Created'])
for (let i = 1; i <= 2_000_000; i++) {
sheet.addRow([i, `Row ${i}`, new Date(2024, 0, 1)])
}
const bytes = await wb.finish() // Uint8Array — same as xlsx(), just async
Everything from the buffered writer works — hyperlinks, rich text, merges,
freeze panes, auto-filter, data validation, conditional formatting, comments,
print setup, defineName — with one constraint: it's one pass,
forward-only. There's no setCell/setRow random access, and
setColumn/freeze/setPageSetup must be called before that sheet's first
addRow() (they render into the header, which is flushed immediately).
merge/autoFilter/setDataValidation/addConditionalFormat/setComment
can be called any time before finish(). Multiple sheets can be written
interleaved or in any order.
In-memory (buffered) writing of ~100k rows × 5 cols takes ~1 s; reading is comparable. Past that, or for an unbounded/very large row count, use the streaming writer and/or the streaming reader.
MIT © uekichinos. Portions adapted from
ExcelJS (MIT) — see NOTICE.
FAQs
Small, dependency-light .xlsx read + write for Node and the browser. Worksheets, typed cells, styles, dates, formulas, merged cells — one dependency, no legacy bloat.
The npm package @uekichinos/quire receives a total of 34 weekly downloads. As such, @uekichinos/quire popularity was classified as not popular.
We found that @uekichinos/quire demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Agent Baseline’s new reference architecture shows how to secure software factories with four boundaries enforced outside the AI agent.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.