xlvane: XLSX files
xlvane reads, writes and edits .xlsx files. It treats input files as untrusted (size and ZIP limits, no external entities, macros are never run, links are never followed). It does not calculate formulas: it reads the cached results stored in the file and writes the formula text you give.
Three ways to use it, with the same behaviour:
| Way | When | Start |
|---|---|---|
Library xlvane | You write JavaScript or TypeScript code | npm install xlvane |
Command line xlvane | You run shell commands | npx xlvane help |
MCP server @xlvane/mcp | Your client calls MCP tools | npx @xlvane/mcp --root <dir> |
Recommended order of work
- Look before reading.
inspectlists the sheets (including hidden ones), used ranges, the detected header row, column types with sample values, merged cells, formulas and warnings. Use it to pick the sheet and header row. - Read rows as objects with
readTable; pass a Standard Schema (Zod, Valibot, ArkType) to validate them. Problems come back as a list with the cell address. - Write a new report with
writeTable, or change an existing file witheditCells/appendRows, which keep everything else in the file as it was. - Check the result with
diffbefore handing the file over.
Library
import { readFile, writeFile } from 'node:fs/promises';
import { appendRows, diff, editCells, inspect, readTable, writeTable } from 'xlvane';
const bytes = await readFile('report.xlsx');
// 1. Structure: sheets, header rows, column types, warnings.
const report = inspect(bytes);
const sales = report.sheets.find((s) => s.name === 'Sales');
console.log(sales?.headerRow, sales?.columns.map((c) => `${c.header}: ${c.dominantType}`));
// 2. Rows as objects keyed by the header row; dates are Date objects.
const table = readTable(bytes, { sheet: 'Sales', header: 'auto' });
console.log(table.rows.length, table.issues);
// 3a. A new formatted report (bold header, frozen first row, autofilter, column widths).
await writeFile('summary.xlsx', writeTable([{ region: 'North', total: 1250 }], { sheet: 'Summary' }));
// 3b. Change cells and append rows; styles, formulas and other sheets stay as they were.
const edited = editCells(bytes, [{ sheet: 'Sales', cell: 'B2', value: 1300 }]);
const appended = appendRows(edited.bytes, 'Sales', [{ Region: 'West', Revenue: 700 }]);
await writeFile('updated.xlsx', appended.bytes);
// 4. What changed.
const changes = diff(bytes, appended.bytes);
console.log(changes.summary);
With a schema:
import { readTable } from 'xlvane';
import { z } from 'zod';
declare const bytes: Uint8Array;
const Row = z.object({ Region: z.string(), Revenue: z.number() });
const { rows, issues } = readTable(bytes, { sheet: 'Sales', schema: Row });
// rows: { Region: string; Revenue: number }[]; issues name the cell of every invalid value.
console.log(rows.length, issues.map((i) => `${i.cell ?? '?'}: ${i.message}`));
Rich text, document properties, defined names and column styles:
import { createWorkbook, readWorkbook } from 'xlvane';
declare const bytes: Uint8Array;
const workbook = readWorkbook(bytes);
console.log(workbook.properties.title, workbook.definedNames.map((n) => `${n.name}=${n.refersTo}`));
console.log(workbook.sheets[0]?.cell('A1')?.richText); // runs with fonts; cell.value is the plain text
const out = createWorkbook({ properties: { title: 'Report' } });
out.addSheet('Data').set('A1', { richText: [{ text: 'Net ', font: { bold: true } }, { text: 'sales' }] }).column('B', { style: { numFmt: '#,##0.00' } });
out.defineName('Net', 'Data!$B$2');
Hyperlinks, notes (comments), tab colour and view:
import { createWorkbook, readWorkbook } from 'xlvane';
declare const bytes: Uint8Array;
const sheet = readWorkbook(bytes).getSheet('Sales');
console.log(sheet.hyperlinks.map((l) => `${l.ref} → ${l.target ?? l.location ?? ''}`), sheet.notes.map((n) => `${n.ref}: ${n.text}`));
const out = createWorkbook({ properties: { company: 'Example Ltd' } });
out.addSheet('Data').tabColor('FF0070C0').set('A1', 'Docs').link('A1', { url: 'https://example.com' }).note('B2', { text: 'Check', author: 'Ann', box: { margins: { left: 2, top: 1, right: 2, bottom: 1 }, lockText: false } });
Link targets are never opened. Notes are written hidden and need no cell value. editCells keeps existing links and notes but cannot add new ones.
Images (PNG, JPEG, GIF):
import { createWorkbook, readWorkbook } from 'xlvane';
declare const bytes: Uint8Array;
declare const png: Uint8Array;
const pictures = readWorkbook(bytes).getSheet('Report').images; // anchor (EMU, with cells), image.format, image.bytes
const out = createWorkbook();
out.addSheet('Report').addImage(out.addImage(png), 'B2:D6', { description: 'Logo' });
Charts and shapes are not read (XLV_W_DRAWING_CONTENT_NOT_READ); editCells keeps them unchanged.
Printing and protection:
import { createWorkbook, readWorkbook } from 'xlvane';
declare const bytes: Uint8Array;
const sheet = readWorkbook(bytes).getSheet('Sales');
console.log(sheet.pageSetup.orientation, sheet.printArea, sheet.protection?.sheet);
const out = createWorkbook();
out.addSheet('Report').pageSetup({ orientation: 'landscape', margins: { left: 0.5 } }).headerFooter({ oddFooter: '&RPage &P of &N' }).printArea('A1:H40').protect({ sort: false });
Protection flags are true when the action is locked. protect({ password: 'secret' }) stores a SHA-512 verifier; it is not encryption. verifyPassword(sheet.protection, 'secret') checks a password.
Data validation and conditional formatting:
import { createWorkbook, readWorkbook } from 'xlvane';
declare const bytes: Uint8Array;
const sheet = readWorkbook(bytes).getSheet('Orders');
console.log(sheet.dataValidations.length, sheet.conditionalFormats.map((f) => f.sqref));
const out = createWorkbook();
out.addSheet('Orders')
.validate('A2:A500', { type: 'list', formula1: '"Open,Shipped,Paid"', showErrorMessage: true, error: 'Pick a status.' })
.conditionalFormat('C2:C500', [{ type: 'cellIs', operator: 'greaterThan', formulas: [1000], style: { fill: { pattern: 'solid', fgColor: 'FFC7CE' } } }]);
Rules Office would reject throw XLV_E_INVALID_OPTION naming the property (for example rules[0].operator).
Tables:
import { createWorkbook, readTable } from 'xlvane';
declare const bytes: Uint8Array;
const { rows } = readTable(bytes, { table: 'Sales' }); // data rows of the table "Sales", keyed by its header
const out = createWorkbook();
out.addSheet('Data').addTable({ name: 'Sales', ref: 'A1', totalsRow: true, columns: [{ name: 'Region', totalsRowLabel: 'Total' }, { name: 'Revenue', totalsRowFunction: 'sum' }], rows: [['North', 1200]] });
sheet.tables lists the tables of a sheet. Table names have no spaces and are unique in the workbook; overlapping tables or merged cells throw XLV_E_INVALID_TABLE.
Inserting and deleting rows
import { deleteRows, insertRows } from 'xlvane';
declare const bytes: Uint8Array;
const { bytes: wider } = insertRows(bytes, { sheet: 'Sales', at: 5, count: 2, style: 'above' });
const { bytes: shorter } = deleteRows(wider, { sheet: 'Sales', at: 5, count: 2 });
Formulas on every sheet, merged cells, links, notes, conditional formats (also extended data bars and icon sets, and thresholds that refer to cells), data validation and defined names follow the rows; references to deleted cells become #REF!. Tables and pictures move with their rows. Drawings with charts or shapes, sparklines and slicers, and a deletion that would cut a picture or remove a table's header row, totals row or all of its data rows, are refused with XLV_E_NOT_SUPPORTED.
inspect() reports the same items (properties, definedNames, and per sheet richTextCellCount, styledColumns, hyperlinkCount and noteCount). A column style is a default for cells added later; xlvane does not apply it to cells you set.
In Node.js, xlvane/node adds readWorkbookFile, inspectFile and writeWorkbookFile (which never replaces an existing file unless { overwrite: true } is passed). toCSV, fromCSV, toJSON and fromJSON convert tables. For very large files use openWorkbookStream and writeWorkbookStream.
Code written for the exceljs API can run on xlvane through @xlvane/compat ("exceljs": "npm:@xlvane/compat@^1" in package.json); unsupported exceljs features throw XLV_E_NOT_SUPPORTED.
Command line
xlvane inspect report.xlsx # structure and warnings
xlvane read report.xlsx --sheet Sales --limit 20 # a sheet as a table
xlvane convert report.xlsx sales.csv # XLSX to CSV/JSON, or CSV/JSON to XLSX
xlvane create data.json report.xlsx # a formatted report from JSON or CSV
xlvane edit report.xlsx --set "Sales!B2=1300" -o updated.xlsx
xlvane edit report.xlsx --sheet Sales --insert-rows 5 --count 2 -o wider.xlsx
xlvane diff report.xlsx updated.xlsx # exit code 1 when they differ
xlvane validate report.xlsx --columns "Region:string,Revenue:number"
Add --json to any command for one JSON document with a documented schema (docs/cli-json.md in the package). Exit codes: 0 success, 1 a check found something, 2 usage error, 3 input or file error. Nothing is overwritten without --force; commands that write accept --dry-run.
MCP server
npx @xlvane/mcp --root ./data serves the tools inspect_workbook, read_table, diff and validate for files inside ./data only. With --allow-write it also serves write_table, edit_cells and convert; they create new files and replace an existing file only when the call passes "overwrite": true. Paths outside the roots, also through .. or symbolic links, are refused with XLV_E_PATH_NOT_ALLOWED. A result is at most 1 Mi characters of JSON (--max-response-chars): read_table then returns the rows that fit, with truncated: true and nextOffset for the next call.
Errors
Every error is an XlvError with a stable code (XLV_E_*), a location (sheet, cell, part), a hint that says what to do, and details (for example the list of available sheets). Read the hint and correct the call; the codes are described in docs/errors.md in the package. Warnings (XLV_W_*) never stop reading; check workbook.warnings or inspect(...).warnings.
Common cases:
XLV_E_SHEET_NOT_FOUND:details.availablelists the sheets, and the hint names a close match when there is one.XLV_E_HEADER_NOT_FOUND: pass the header row number, orheader: falseto use column letters.XLV_E_FILE_EXISTS: choose another output path, or overwrite explicitly.XLV_E_LIMIT_EXCEEDED: the file is larger than a limit; raise the limit only for trusted files.
Things to know
- Formulas are not calculated. After
editCells, cached results that depend on edited cells are out of date until a spreadsheet program recalculates them (the file asks it to on opening). - Dates in spreadsheets have no time zone; xlvane returns them as
Dateobjects whose UTC fields hold the spreadsheet's date and time. As text (toCSV,toJSON,readTable({ dates: 'iso' }),inspect,xlvane read --json) they are ISO 8601 without a zone:2024-01-31or2024-01-31T13:45:00. - JSON has no date type:
fromJSONkeeps ISO date strings as text. Convert them toDatebeforefromJSON/writeTableif they should be dates.fromCSVdoes recognise ISO dates. xlvane read --jsonreturns the first 50 rows; checktruncatedand use--limit allfor every row. Warnings found while reading are inresult.warnings..xls(the old binary format) and encrypted files are not supported and are reported with an error code.
Source: skills/xlvane/SKILL.md (version 1.2.2)