hucre
Zero-dependency spreadsheet engine.
Read & write XLSX, CSV, ODS. Schema validation, streaming, round-trip preservation. Pure TypeScript, works everywhere.
npm install hucreimport { readXlsx, writeXlsx } from "hucre";
// Read an XLSX file
const workbook = await readXlsx(buffer);
console.log(workbook.sheets[0].rows);
// Write an XLSX file
const xlsx = await writeXlsx({
sheets: [
{
name: "Products",
columns: [
{ header: "Name", key: "name", width: 25 },
{ header: "Price", key: "price", width: 12, numFmt: "$#,##0.00" },
{ header: "Stock", key: "stock", width: 10 },
],
data: [
{ name: "Widget", price: 9.99, stock: 142 },
{ name: "Gadget", price: 24.5, stock: 87 },
],
},
],
});Import only what you need:
import { readXlsx, writeXlsx } from "hucre/xlsx"; // XLSX only (~14 KB gzipped)
import { parseCsv, writeCsv } from "hucre/csv"; // CSV only (~2 KB gzipped)| hucre | SheetJS | ExcelJS | read-excel-file | |
|---|---|---|---|---|
| Dependencies | 0 | 0* | 12 (CVEs) | 2 |
| Bundle (gzip) | ~18 KB | ~300 KB | ~500 KB | ~40 KB |
| ESM native | Yes | Partial | No (CJS) | Yes |
| TypeScript | Native | Bolted-on | Bolted-on | Yes |
| Edge runtime | Yes | No | No | No |
| CSP compliant | Yes | Yes | No (eval) | Yes |
| npm published | Yes | No (CDN only) | Stale | Yes |
| Read + Write | Yes | Yes (Pro $) | Yes | Separate pkgs |
* SheetJS removed itself from npm; must install from CDN tarball.
import { readXlsx } from "hucre/xlsx";
const wb = await readXlsx(uint8Array, {
sheets: [0, "Products"], // Filter sheets by index or name
readStyles: true, // Parse cell styles
dateSystem: "auto", // Auto-detect 1900/1904
});
for (const sheet of wb.sheets) {
console.log(sheet.name); // "Products"
console.log(sheet.rows); // CellValue[][]
console.log(sheet.merges); // MergeRange[]
}Supported cell types: strings, numbers, booleans, dates, formulas, rich text, errors, inline strings.
import { writeXlsx } from "hucre/xlsx";
const buffer = await writeXlsx({
sheets: [
{
name: "Report",
columns: [
{ header: "Date", key: "date", width: 15, numFmt: "yyyy-mm-dd" },
{ header: "Revenue", key: "revenue", width: 15, numFmt: "$#,##0.00" },
{ header: "Active", key: "active", width: 10 },
],
data: [
{ date: new Date("2026-01-15"), revenue: 12500, active: true },
{ date: new Date("2026-01-16"), revenue: 8900, active: false },
],
freezePane: { rows: 1 },
autoFilter: { range: "A1:C3" },
},
],
defaultFont: { name: "Calibri", size: 11 },
});Features: cell styles, auto column widths, merged cells, freeze/split panes, auto-filter with criteria, data validation, hyperlinks, images (PNG/JPEG/GIF/SVG/WebP), comments, tables, conditional formatting (cellIs/colorScale/dataBar/iconSet), named ranges, print settings, page breaks, sheet protection, workbook protection, rich text, shared/array/dynamic formulas, sparklines, textboxes, background images, number formats, hidden sheets, HTML/Markdown/JSON/TSV export, template engine.
const buffer = await writeXlsx({
sheets: [
{
name: "Products",
columns: [
{ header: "Name", key: "name", autoWidth: true },
{ header: "Price", key: "price", autoWidth: true, numFmt: "$#,##0.00" },
{ header: "SKU", key: "sku", autoWidth: true },
],
data: products,
},
],
});Calculates optimal column widths from cell content — font-aware, handles CJK double-width characters, number formats, min/max constraints.
const buffer = await writeXlsx({
sheets: [
{
name: "Sheet1",
rows: [
["Status", "Quantity"],
["active", 10],
],
dataValidations: [
{
type: "list",
values: ["active", "inactive", "draft"],
range: "A2:A100",
showErrorMessage: true,
errorTitle: "Invalid",
errorMessage: "Pick from the list",
},
{
type: "whole",
operator: "between",
formula1: "0",
formula2: "1000",
range: "B2:B100",
},
],
},
],
});const buffer = await writeXlsx({
sheets: [
{
name: "Links",
rows: [["Visit Google", "Go to Sheet2"]],
cells: new Map([
[
"0,0",
{
value: "Visit Google",
type: "string",
hyperlink: { target: "https://google.com", tooltip: "Open Google" },
},
],
[
"0,1",
{
value: "Go to Sheet2",
type: "string",
hyperlink: { target: "", location: "Sheet2!A1" },
},
],
]),
},
],
});Process large files row-by-row without loading everything into memory:
import { streamXlsxRows, XlsxStreamWriter } from "hucre/xlsx";
// Stream read — async generator yields rows one at a time
for await (const row of streamXlsxRows(buffer)) {
console.log(row.index, row.values);
}
// Stream write — add rows incrementally
const writer = new XlsxStreamWriter({
name: "BigData",
columns: [{ header: "ID" }, { header: "Value" }],
freezePane: { rows: 1 },
});
for (let i = 0; i < 100_000; i++) {
writer.addRow([i + 1, Math.random()]);
}
const buffer = await writer.finish();import { readOds, writeOds } from "hucre/ods";
const wb = await readOds(buffer);
const ods = await writeOds({ sheets: [{ name: "Sheet1", rows: [["Hello", 42]] }] });Open, modify, save — without losing charts, macros, or features hucre doesn't natively handle:
import { openXlsx, saveXlsx } from "hucre/xlsx";
const workbook = await openXlsx(buffer);
workbook.sheets[0].rows[0][0] = "Updated!";
const output = await saveXlsx(workbook); // Charts, VBA, themes preservedAuto-detect format and work with simple helpers:
import { read, write, readObjects, writeObjects } from "hucre";
// Auto-detect XLSX vs ODS
const wb = await read(buffer);
// Quick: file → array of objects
const products = await readObjects<{ name: string; price: number }>(buffer);
// Quick: objects → XLSX
const xlsx = await writeObjects(products, { sheetName: "Products" });npx hucre convert input.xlsx output.csv
npx hucre convert input.csv output.xlsx
npx hucre inspect file.xlsx
npx hucre inspect file.xlsx --sheet 0
npx hucre validate data.xlsx --schema schema.jsonManipulate sheet data in memory:
import { insertRows, deleteRows, cloneSheet, moveSheet } from "hucre";
insertRows(sheet, 5, 3); // Insert 3 rows at position 5
deleteRows(sheet, 0, 1); // Delete first row
const copy = cloneSheet(sheet, "Copy"); // Deep clone
moveSheet(workbook, 0, 2); // Reorder sheetsimport { toHtml, toMarkdown } from "hucre";
const html = toHtml(workbook.sheets[0], {
headerRow: true,
styles: true,
classes: true,
});
const md = toMarkdown(workbook.sheets[0]);
// | Name | Price | Stock |
// |--------|-------:|------:|
// | Widget | 9.99 | 142 |import { formatValue } from "hucre";
formatValue(1234.5, "#,##0.00"); // "1,234.50"
formatValue(0.15, "0%"); // "15%"
formatValue(44197, "yyyy-mm-dd"); // "2021-01-01"
formatValue(1234, "$#,##0"); // "$1,234"
formatValue(0.333, "# ?/?"); // "1/3"import { parseCellRef, cellRef, colToLetter, rangeRef } from "hucre";
parseCellRef("AA15"); // { row: 14, col: 26 }
cellRef(14, 26); // "AA15"
colToLetter(26); // "AA"
rangeRef(0, 0, 9, 3); // "A1:D10"Fluent method-chaining interface:
import { WorkbookBuilder } from "hucre";
const xlsx = await WorkbookBuilder.create()
.addSheet("Products")
.columns([
{ header: "Name", key: "name", autoWidth: true },
{ header: "Price", key: "price", numFmt: "$#,##0.00" },
])
.row(["Widget", 9.99])
.row(["Gadget", 24.5])
.freeze(1)
.done()
.build();Fill {{placeholders}} in existing XLSX templates:
import { openXlsx, saveXlsx, fillTemplate } from "hucre";
const workbook = await openXlsx(templateBuffer);
fillTemplate(workbook, {
company: "Acme Inc",
date: new Date(),
total: 12500,
});
const output = await saveXlsx(workbook);import { toJson } from "hucre";
toJson(sheet, { format: "objects" }); // [{Name:"Widget", Price:9.99}, ...]
toJson(sheet, { format: "columns" }); // {Name:["Widget"], Price:[9.99]}
toJson(sheet, { format: "arrays" }); // {headers:[...], data:[[...]]}import { parseCsv, parseCsvObjects, writeCsv, detectDelimiter } from "hucre/csv";
// Parse — auto-detects delimiter, handles RFC 4180 edge cases
const rows = parseCsv(csvString, { typeInference: true });
// Parse with headers — returns typed objects
const { data, headers } = parseCsvObjects(csvString, { header: true });
// Write
const csv = writeCsv(rows, { delimiter: ";", bom: true });
// Detect delimiter
detectDelimiter(csvString); // "," or ";" or "\t" or "|"Validate imported data with type coercion, pattern matching, and error collection:
import { validateWithSchema } from "hucre";
import { parseCsv } from "hucre/csv";
const rows = parseCsv(csvString);
const result = validateWithSchema(
rows,
{
"Product Name": { type: "string", required: true },
Price: { type: "number", required: true, min: 0 },
SKU: { type: "string", pattern: /^[A-Z]{3}-\d{4}$/ },
Stock: { type: "integer", min: 0, default: 0 },
Status: { type: "string", enum: ["active", "inactive", "draft"] },
},
{ headerRow: 1 },
);
console.log(result.data); // Validated & coerced objects
console.log(result.errors); // [{ row: 3, field: "Price", message: "...", value: "abc" }]Schema field options:
| Option | Type | Description |
|---|---|---|
type |
"string" | "number" | "integer" | "boolean" | "date" |
Target type (with coercion) |
required |
boolean |
Reject null/empty values |
pattern |
RegExp |
Regex validation (strings) |
min |
number |
Min value (numbers) or length (strings) |
max |
number |
Max value (numbers) or length (strings) |
enum |
unknown[] |
Allowed values |
default |
unknown |
Default for null/empty |
validate |
(v) => boolean | string |
Custom validator |
transform |
(v) => unknown |
Post-validation transform |
column |
string |
Column header name |
columnIndex |
number |
Column index (0-based) |
Timezone-safe Excel date serial number conversion:
import { serialToDate, dateToSerial, isDateFormat, formatDate } from "hucre";
serialToDate(44197); // 2021-01-01T00:00:00.000Z
dateToSerial(new Date("2021-01-01")); // 44197
isDateFormat("yyyy-mm-dd"); // true
isDateFormat("#,##0.00"); // false
formatDate(new Date(), "yyyy-mm-dd"); // "2026-03-24"Handles the Lotus 1-2-3 bug (serial 60), 1900/1904 date systems, and time fractions correctly.
hucre works everywhere — no Node.js APIs (fs, crypto, Buffer) in core.
| Runtime | Status |
|---|---|
| Node.js 18+ | Full support |
| Deno | Full support |
| Bun | Full support |
| Modern browsers | Full support |
| Cloudflare Workers | Full support |
| Vercel Edge Functions | Full support |
| Web Workers | Full support |
hucre (~37 KB gzipped)
├── zip/ Zero-dep DEFLATE/inflate + ZIP read/write
├── xml/ SAX parser + XML writer (CSP-compliant, no eval)
├── xlsx/
│ ├── reader Shared strings, styles, worksheets, relationships
│ ├── writer Styles, shared strings, drawing, tables, comments
│ ├── roundtrip Open → modify → save with preservation
│ ├── stream-* Streaming reader (AsyncGenerator) + writer
│ └── auto-width Font-aware column width calculation
├── ods/ OpenDocument Spreadsheet read/write
├── csv/ RFC 4180 parser/writer + streaming
├── export/ HTML, Markdown, JSON, TSV output + HTML import
├── hucre Unified read/write API, format auto-detect
├── builder Fluent WorkbookBuilder / SheetBuilder API
├── template {{placeholder}} template engine
├── sheet-ops Insert/delete/move/sort/find/replace, clone, copy
├── cell-utils parseCellRef, colToLetter, parseRange, isInRange
├── image imageFromBase64 utility
├── worker Web Worker serialization helpers
├── _date Timezone-safe serial ↔ Date, Lotus bug, 1900/1904
├── _format Number format renderer (locale-aware)
├── _schema Schema validation, type coercion, error collection
└── cli Convert, inspect, validate (citty + consola)
Zero dependencies. Pure TypeScript. The ZIP engine uses CompressionStream/DecompressionStream Web APIs with a pure TS fallback.
| Function | Description |
|---|---|
read(input, options?) |
Auto-detect format (XLSX/ODS), returns Workbook |
write(options) |
Write XLSX or ODS (via format option) |
readObjects(input, options?) |
File → array of objects (first row = headers) |
writeObjects(data, options?) |
Objects → XLSX/ODS |
| Function | Description |
|---|---|
readXlsx(input, options?) |
Parse XLSX from Uint8Array | ArrayBuffer |
writeXlsx(options) |
Generate XLSX, returns Uint8Array |
openXlsx(input, options?) |
Open for round-trip (preserves unknown parts) |
saveXlsx(workbook) |
Save round-trip workbook back to XLSX |
streamXlsxRows(input, options?) |
AsyncGenerator yielding rows one at a time |
XlsxStreamWriter |
Class for incremental row-by-row XLSX writing |
| Function | Description |
|---|---|
readOds(input, options?) |
Parse ODS (OpenDocument Spreadsheet) |
writeOds(options) |
Generate ODS |
streamOdsRows(input) |
AsyncGenerator yielding ODS rows |
| Function | Description |
|---|---|
parseCsv(input, options?) |
Parse CSV string → CellValue[][] |
parseCsvObjects(input, options?) |
Parse CSV with headers → { data, headers } |
writeCsv(rows, options?) |
Write CellValue[][] → CSV string |
writeCsvObjects(data, options?) |
Write objects → CSV string |
detectDelimiter(input) |
Auto-detect delimiter character |
streamCsvRows(input, options?) |
Generator yielding CSV rows |
CsvStreamWriter |
Class for incremental CSV writing |
writeTsv(rows, options?) |
Write TSV (tab-separated) |
fetchCsv(url, options?) |
Fetch and parse CSV from URL |
| Function | Description |
|---|---|
insertRows(sheet, index, count) |
Insert rows, shift down |
deleteRows(sheet, index, count) |
Delete rows, shift up |
insertColumns(sheet, index, count) |
Insert columns, shift right |
deleteColumns(sheet, index, count) |
Delete columns, shift left |
moveRows(sheet, from, count, to) |
Move rows |
cloneSheet(sheet, name) |
Deep clone a sheet |
copySheetToWorkbook(sheet, wb, name?) |
Copy sheet between workbooks |
copyRange(sheet, source, target) |
Copy cell range within sheet |
moveSheet(wb, from, to) |
Reorder sheets |
removeSheet(wb, index) |
Remove a sheet |
sortRows(sheet, col, order?) |
Sort rows by column |
findCells(sheet, predicate) |
Find cells by value or function |
replaceCells(sheet, find, replace) |
Find and replace values |
| Function | Description |
|---|---|
toHtml(sheet, options?) |
HTML <table> with styles, a11y, dark/light CSS |
toMarkdown(sheet, options?) |
Markdown table with auto-alignment |
toJson(sheet, options?) |
JSON (objects, arrays, or columns format) |
fromHtml(html, options?) |
Parse HTML table string → Sheet |
| Function | Description |
|---|---|
WorkbookBuilder.create() |
Fluent API for building workbooks |
fillTemplate(workbook, data) |
Replace {{placeholders}} in templates |
| Function | Description |
|---|---|
formatValue(value, numFmt, options?) |
Apply Excel number format (locale-aware) |
validateWithSchema(rows, schema, options?) |
Validate & coerce data with schema |
serialToDate(serial, is1904?) |
Excel serial → Date (UTC) |
dateToSerial(date, is1904?) |
Date → Excel serial |
isDateFormat(numFmt) |
Check if format string is date |
formatDate(date, format) |
Format Date with Excel format string |
parseCellRef(ref) |
"AA15" → { row: 14, col: 26 } |
cellRef(row, col) |
(14, 26) → "AA15" |
colToLetter(col) |
26 → "AA" |
rangeRef(r1, c1, r2, c2) |
(0,0,9,3) → "A1:D10" |
| Function | Description |
|---|---|
serializeWorkbook(wb) |
Convert Workbook for postMessage (Maps → objects, Dates → strings) |
deserializeWorkbook(data) |
Restore Workbook from serialized form |
WORKER_SAFE_FUNCTIONS |
List of all hucre functions safe for Web Workers (all of them) |
pnpm install
pnpm dev # vitest watch
pnpm test # lint + typecheck + test
pnpm build # obuild (minified, tree-shaken)
pnpm lint:fix # oxlint + oxfmt
pnpm typecheck # tsgoContributions are welcome! Please open an issue or submit a PR.
127 of 135 tracked features are implemented. See the issue tracker for the v2 roadmap (XLS BIFF, encryption, charts, pivot tables).
MIT — Made by productdevbook