Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@hmalinchock/excel

JSR CI

Read and write Excel .xlsx files in Deno. On JSR. Source: github.com/HoldenMalinchock/excel.

exceljs is the usual way to write .xlsx from JavaScript, but 4.4.0 is unmaintained and carries prototype-pollution, zip-path, and zip-bomb CVEs. This package is not a port of that API. It covers the jobs people actually use exceljs for: turn rows of data into a workbook, style it, make a table, read it back.

deno add jsr:@hmalinchock/excel
import { Workbook } from "@hmalinchock/excel";

const wb = new Workbook();
wb.sheet("Sales").from([
  { id: 1, name: "Ada", total: 42.5 },
  { id: 2, name: "Grace", total: 17 },
]).table({ name: "Sales", style: "TableStyleMedium9" });
await wb.write("./sales.xlsx");

Zero runtime dependencies. Names follow Excel’s own model: a workbook holds sheets, sheets hold cells, and a range of cells can be an Excel table.

Runnable copies of these snippets live in examples/.

Records to a sheet

const ws = wb.sheet("People");
ws.from([
  { id: 1, name: "Ada", role: "Engineer" },
  { id: 2, name: "Grace", role: "Admiral" },
]);
ws.width("name", 16);

from() writes a header row, bolds it, and adds an autofilter. Pass { headers: ["name", "id"] } to pick column order. Arrays write a raw grid instead:

wb.sheet("Raw").from([
  ["Id", "Name"],
  [1, "Ada"],
]);
ws.add(["next", "row"]);

Read it back:

const loaded = await Workbook.read("./sales.xlsx");
loaded.sheet("Sales").objects(); // records using row 1 as keys
loaded.sheet("Sales").values(); // 2d array
loaded.sheet("Sales").get("B2");

Convert records into an Excel table

An Excel table is the formatted, filterable object you get from Insert → Table. Call table() after from():

const records = [
  { region: "East", product: "Gadget", units: 10, revenue: 1200 },
  { region: "West", product: "Widget", units: 4, revenue: 360 },
];

wb.sheet("Sales")
  .from(records)
  .column("revenue", { width: 14, numFmt: "$#,##0.00", align: "right" })
  .table({
    name: "Sales",
    style: "TableStyleMedium9",
    totals: { units: "sum", revenue: "sum" },
  });

Or in one step: ws.from(records, { table: true }).

style is any built-in name (TableStyleLight121, TableStyleMedium128, TableStyleDark111). totals: true labels the first column “Total” and sums the rest.

See examples/as_table.ts.

Cell styling

style takes an A1 cell, a range, a whole column (C:C), or a whole row (2:2). Colors are hex.

ws.set("A1", "Q1 report");
ws.merge("A1:D1");
ws.style("A1:D1", {
  bold: true,
  size: 16,
  font: "Calibri",
  color: "#FFFFFF",
  fill: "#1F4E79",
  align: "center",
  border: true,
});
ws.style("A2:D2", { bold: true, fill: "#D6DCE4", border: "thin" });
ws.style("C:C", { numFmt: "$#,##0.00", align: "right" });

border: true is a thin box. Use "thin" | "medium" | "thick" | "dashed" and optional borderColor. vertical is "top" | "center" | "bottom". wrap: true wraps text.

Column styling

ws.column("revenue", { width: 14, numFmt: "$#,##0.00", align: "right" });
ws.column("C", { width: 12, hidden: false });
ws.column("note", { width: 24, wrap: true });

Keys from from() work ("revenue"), as do letters ("C"). Column style applies to existing cells in that column and is stored as the column default.

Worksheet styling

ws.sheetStyle({
  tabColor: "#1F4E79",
  defaultRowHeight: 18,
  defaultColWidth: 12,
  showGridLines: false,
});
ws.freeze(1); // freeze the header row

Formulas, dates, links, comments, CSV

ws.set("D2", { formula: "SUM(C2:C4)", result: 42.5 });
ws.set("E2", { hyperlink: "https://example.com", text: "link" });
ws.set("F2", new Date("2026-01-15T00:00:00Z"));
ws.comment("A2", "check this");
ws.csv(); // formula-like cells are quoted so they cannot execute
const wb = Workbook.fromCsv("id,name\n1,Ada\n");

See examples/styling.ts and examples/from_records.ts.

There is no addImage({ filename }), no lodash-style note merge, and no unbounded zip inflate.

Security vs exceljs 4.4.0

Issue exceljs 4.4.0 this package
Prototype pollution (CVE-2026-78207) deepMerge on cell notes copies __proto__ from() / set() copy only own, allow-listed keys onto prototype-free objects
Path traversal (CVE-2026-78208) addImage({ filename }) reads any path that API does not exist; zip entry names with .. are rejected on load
Zip bomb (CVE-2026-78206) every entry inflated, no cap declared size, ratio, and actual inflate are capped before XML parse

Load limits (overridable on Workbook.parse / Workbook.read): 10 000 entries, 128 MiB per entry, 512 MiB total uncompressed, compression ratio 1000:1.

License

MIT

About

Read and write Excel .xlsx files in Deno. JSR package @hmalinchock/excel.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages