xlvane 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DevXord
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,410 @@
1
- # Temporary Holding Version
1
+ # xlvane
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Read, inspect and write XLSX files; input files are treated as untrusted. ESM only, one runtime dependency ([fflate](https://github.com/101arrowz/fflate)). The core works with `Uint8Array` and has no Node.js dependencies; file helpers are in `xlvane/node`.
4
+
5
+ ```sh
6
+ npm install xlvane
7
+ ```
8
+
9
+ The package also contains `docs/` (error codes, JSON schemas, exceljs compatibility), the Agent Skill `skills/xlvane/SKILL.md` and `llms-full.txt` (all of it in one file), for reading without network access: after installing, they are in `node_modules/xlvane/`. Links below point to the same files on GitHub. The skill can be installed into a project with the [`skills`](https://github.com/vercel-labs/skills) CLI, offline: `npx skills add ./node_modules/xlvane/skills --skill xlvane`.
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { inspect, readTable, writeTable } from 'xlvane';
15
+ import { readWorkbookFile } from 'xlvane/node';
16
+
17
+ const workbook = await readWorkbookFile('orders.xlsx');
18
+ console.log(inspect(workbook).sheets.map((s) => [s.name, s.usedRange])); // what is in the file
19
+ const { rows } = readTable(workbook, { sheet: 'Orders' }); // [{ Customer: 'Ann', Total: 120.5 }, …]
20
+ const big = rows.filter((row) => typeof row['Total'] === 'number' && row['Total'] > 100);
21
+ const bytes = writeTable(big, { sheet: 'Big orders' }); // a formatted .xlsx file as a Uint8Array
22
+ ```
23
+
24
+ From a shell: `npx xlvane inspect orders.xlsx`, then `npx xlvane read orders.xlsx --sheet Orders`.
25
+
26
+ ## Contents
27
+
28
+ - [Command line in 5 minutes](#command-line-in-5-minutes)
29
+ - [Reading a workbook](#reading-a-workbook) · [Writing a workbook](#writing-a-workbook) (rich text, hyperlinks, notes, images, printing, [data validation and conditional formatting](#data-validation-and-conditional-formatting), [tables](#tables))
30
+ - [Tasks in one call](#tasks-in-one-call): tables of rows, CSV, JSON, editing files, diff · [inserting and deleting rows](#inserting-and-deleting-rows)
31
+ - [Large files: streaming](#large-files-streaming)
32
+ - [Inspecting a workbook](#inspecting-a-workbook) · [Errors and warnings](#errors-and-warnings) · [Untrusted input and limits](#untrusted-input-and-limits)
33
+ - [Limitations](#limitations) · [Package size](#package-size) · [Links](#links)
34
+ - exceljs users: [`@xlvane/compat`](https://www.npmjs.com/package/@xlvane/compat) and `xlvane migrate`
35
+
36
+ ## Tested environments
37
+
38
+ The core API (reading, writing, inspect, tables, CSV, editing, diff, streaming with Web Streams) and `@xlvane/compat` are tested in Node.js 22, 24 and 26, Bun, Deno, Chromium (Playwright) and the Cloudflare Workers runtime (workerd), with the same test bundle (`npm run test:runtimes`). In Deno, import from `npm:xlvane`; in browsers and Workers, through a bundler such as Vite, esbuild or webpack. `xlvane/node` and the command line need Node.js. Reading is tested on 340 files of the Apache POI test corpus (written by Excel for Windows, Mac and the web, LibreOffice, ONLYOFFICE and others, and damaged files) and on files saved by LibreOffice; see `tests/corpus/`.
39
+
40
+ ## Command line in 5 minutes
41
+
42
+ The package installs an `xlvane` command (Node.js 22 or later): `npm install xlvane` in a project and run `npx xlvane`, or `npm install -g xlvane`.
43
+
44
+ ```sh
45
+ xlvane # what the tool does and the common commands
46
+ xlvane inspect report.xlsx # sheets, used ranges, header rows, column types, warnings
47
+ xlvane read report.xlsx --sheet Sales # a sheet as a table (first 50 rows; --limit, --range)
48
+ xlvane convert report.xlsx sales.csv # XLSX to CSV or JSON, or CSV/JSON to XLSX
49
+ xlvane create data.json report.xlsx # a formatted report from JSON or CSV
50
+ xlvane edit report.xlsx --set "Sales!B2=1250" -o updated.xlsx
51
+ xlvane diff report.xlsx updated.xlsx # what changed; exit code 1 when they differ
52
+ xlvane validate report.xlsx --columns "Region:string,Revenue:number"
53
+ xlvane help edit # usage and examples of one command
54
+ ```
55
+
56
+ - Nothing is overwritten without `--force`. Commands that write accept `--dry-run` and end with the path they wrote.
57
+ - Errors say what happened, where, and what to do next, often with a ready-to-run `Try:` command; typos get "Did you mean …?".
58
+ - `--json` prints one JSON document per command with a documented schema ([docs/cli-json.md](https://github.com/DevXord/xlvane/blob/main/docs/cli-json.md)). Exit codes: 0 success, 1 a check found something (`diff`, `validate`), 2 usage error, 3 input or file error.
59
+ - Colour (red errors, yellow warnings, green results, bold headings) is used only on a terminal and never carries information alone; `NO_COLOR` or `TERM=dumb` turn it off, `FORCE_COLOR=1` turns it on. Control characters from files, such as an escape character in a cell, are shown as `\x1b` and never reach the terminal.
60
+ - Tables and summaries go to separate streams: `xlvane read report.xlsx > out.txt` writes only the table; the row count and warnings notes go to stderr.
61
+ - `xlvane migrate` prepares a project for `@xlvane/compat`.
62
+
63
+ ## Reading a workbook
64
+
65
+ ```ts
66
+ import { readWorkbook } from 'xlvane';
67
+
68
+ declare const bytes: Uint8Array; // the file's contents
69
+ const workbook = readWorkbook(bytes); // Uint8Array or ArrayBuffer
70
+ const sheet = workbook.getSheet('Sales');
71
+ console.log(sheet.cell('B2')?.value);
72
+ for (const row of sheet.rows()) {
73
+ console.log(row.map((cell) => cell.value));
74
+ }
75
+ ```
76
+
77
+ In Node.js:
78
+
79
+ ```ts
80
+ import { readWorkbookFile } from 'xlvane/node';
81
+
82
+ const workbook = await readWorkbookFile('report.xlsx');
83
+ ```
84
+
85
+ Cell values are numbers, strings, booleans, `Date`s (for date-formatted numbers and ISO date cells; the UTC fields of the `Date` hold the spreadsheet's date and time), error codes such as `"#DIV/0!"` (with `type: "error"`) or `null`. Formula cells carry `formula.text` and the cached result stored in the file; xlvane does not calculate formulas. Cells that have neither a value nor a formula are not part of `cells()`; those that hold only a style are listed by `sheet.styledCells()` (and `styledCells` of each streamed row), so tables, CSV and `inspect` see only data.
86
+
87
+ ## Writing a workbook
88
+
89
+ ```ts
90
+ import { createWorkbook, writeWorkbook } from 'xlvane';
91
+
92
+ const workbook = createWorkbook(); // { dateSystem: '1904' } for the 1904 date system
93
+ const sheet = workbook.addSheet('Sales');
94
+ const header = { font: { bold: true }, fill: { color: 'D9E1F2' }, border: { bottom: { style: 'thin' } } } as const;
95
+ sheet.set('A1', 'Region', header).set('B1', 'Revenue', header).set('C1', 'Date', header);
96
+ sheet.set('A2', 'North').set('B2', 1250.5, { numFmt: '#,##0.00' }).set('C2', new Date(Date.UTC(2024, 0, 31)));
97
+ sheet.set('B3', { formula: 'SUM(B2:B2)', result: 1250.5 });
98
+ sheet.column('A', { width: 20 }).row(1, { height: 24 });
99
+ sheet.merge('A5:C5').freeze({ rows: 1 }).autoFilter('A1:C2');
100
+ const bytes = writeWorkbook(workbook); // Uint8Array
101
+ ```
102
+
103
+ In Node.js, `writeWorkbookFile(workbook, 'sales.xlsx')` from `xlvane/node` writes the file; it never replaces an existing file unless you pass `{ overwrite: true }`.
104
+
105
+ - Values: numbers, strings, booleans, `Date`s (stored as serial numbers with a `yyyy-mm-dd` or `yyyy-mm-dd hh:mm:ss` format unless the style sets one), `{ error: '#N/A' }`, and `{ formula, result }`, also as an array formula (`{ formula, array: 'A1:A3' }`) or a shared formula (`{ formula, shared: 'B1:B9' }` on the first cell, `{ sharedFormula: 'B1' }` on the others). xlvane writes the formula text and the cached result you give; it does not calculate formulas.
106
+ - Styles: font (name, size, bold, italic, underline, strike, colour, superscript and subscript, family, scheme, charset, outline, shadow), fill (solid colour, pattern, or linear and path gradient), borders (top, right, bottom, left, diagonal), alignment (horizontal, vertical, wrap, shrink to fit, indent, rotation, reading order), cell protection (`locked`, `hidden`) and number format. Identical styles are stored once. `sheet.style(range, style)` merges a style into every cell of a range.
107
+ - Layout: column widths and hidden columns, row heights and hidden rows, outline levels of rows and columns (`row(n, { outlineLevel: 1, collapsed })`, `column('B', { outlineLevel: 1 })`), merged ranges, frozen rows and columns, autofilter, hidden and very hidden sheets. `sheet.properties({ defaultRowHeight, outlineLevelRow, outlineLevelCol, summaryBelow, summaryRight })` sets the sheet's format properties.
108
+ - Workbook: `workbook.views([{ x, y, width, height, firstSheet, activeTab, visibility }])` sets the workbook windows and `workbook.calculation({ fullCalcOnLoad: true })` asks applications to recalculate when the file is opened.
109
+ - Writer options: `writeWorkbook(workbook, { sharedStrings: false })` writes text inline instead of in a shared string table; `{ compressionLevel: 0 }` (0–9, default 6) stores or compresses the parts. The same options work for `writeWorkbookFile` and, with `compressionLevel`, `writeWorkbookStream`.
110
+ - Invalid input is rejected when it is set, with an `XlvError` saying what and where, for example `XLV_E_INVALID_SHEET_NAME` or `XLV_E_INVALID_STYLE` with the allowed values.
111
+ - The output is deterministic: the same workbook always gives the same bytes. The exception is a protection password (`protect({ password })`), whose verifier gets a random salt; pass a fixed salt through `hashPassword` for identical bytes. The test suite opens written files that use all of these features in LibreOffice and checks the values, styles and layout it sees.
112
+
113
+ Reading returns the same information: `cell.style`, `cell.formula`, `sheet.columns`, `sheet.rowInfo(row)`, `sheet.properties`, `sheet.freeze` and `sheet.autoFilter`.
114
+
115
+ ### Rich text, document properties, defined names, column styles
116
+
117
+ ```ts
118
+ import { createWorkbook, readWorkbook, writeWorkbook } from 'xlvane';
119
+
120
+ const workbook = createWorkbook({ properties: { title: 'Q3 sales', creator: 'Finance', created: new Date(Date.UTC(2026, 9, 1)) } });
121
+ const sheet = workbook.addSheet('Sales');
122
+ sheet.set('A1', { richText: [{ text: 'Total ', font: { bold: true } }, { text: '(EUR)', font: { color: '808080' } }] });
123
+ sheet.set('B1', 1250.5).set('C1', new Date(Date.UTC(2026, 9, 1)), { numFmt: 14 });
124
+ sheet.column('B', { width: 14, style: { numFmt: '#,##0.00' } });
125
+ workbook.defineName('Total', 'Sales!$B$1');
126
+
127
+ const read = readWorkbook(writeWorkbook(workbook));
128
+ console.log(read.properties.title, read.definedNames, read.getSheet('Sales').cell('A1')?.richText);
129
+ ```
130
+
131
+ - **Rich text:** `{ richText: [{ text, font? }] }` writes text whose parts have their own fonts. Reading keeps `cell.value` as the plain text and adds `cell.richText` with the runs. Run fonts take the same properties as cell fonts.
132
+ - **Document properties:** `properties` (`title`, `subject`, `creator`, `keywords`, `description`, `lastModifiedBy`, `category`, `created`, `modified`, `lastPrinted`, and a few more) are stored in `docProps/core.xml`, only when at least one is set. Read them from `workbook.properties`. `company`, `manager`, `application` and `appVersion` go to `docProps/app.xml`.
133
+ - **Defined names:** `workbook.defineName(name, refersTo, { sheet?, hidden?, comment? })`; `workbook.definedNames` lists the names of a file that was read, built-in names such as `_xlnm._FilterDatabase` included. Invalid names throw `XLV_E_INVALID_DEFINED_NAME`.
134
+ - **Column styles:** `column(c, { style })` stores a default style for the column. Applications use it for cells added later; xlvane does not apply it to the cells you set. Reading gives `sheet.columns[i].style`.
135
+ - **Built-in number formats:** `numFmt` may be a built-in format number such as `14` (a date shown in the reader's local date order). A format code string such as `'yyyy-mm-dd'` is shown the same everywhere.
136
+
137
+ ### Hyperlinks, notes, tab colour and view
138
+
139
+ ```ts
140
+ import { createWorkbook, readWorkbook, writeWorkbook } from 'xlvane';
141
+
142
+ const workbook = createWorkbook({ properties: { company: 'Example Ltd' } });
143
+ const sheet = workbook.addSheet('Links').tabColor('FF0070C0').view({ zoom: 120, showGridLines: false });
144
+ sheet.set('A1', 'Docs').link('A1', { url: 'https://example.com/docs', tooltip: 'Open the docs' });
145
+ sheet.set('A2', 'Back to top').link('A2', { location: 'Links!A1' });
146
+ sheet.set('B2', 1250).note('B2', { text: 'Estimate from March', author: 'Ann' });
147
+
148
+ const read = readWorkbook(writeWorkbook(workbook)).getSheet('Links');
149
+ console.log(read.hyperlinks, read.notes, read.cell('B2')?.note?.text, read.tabColor, read.view.zoom);
150
+ ```
151
+
152
+ - **Hyperlinks:** `link(range, { url | location, tooltip?, display? })`. A `url` (web page, `mailto:`, file) is stored as an external relationship of the sheet; a `location` (a place in the workbook) is stored without one. The cell text is set with `set`. Reading gives `sheet.hyperlinks` and `cell.hyperlink`; xlvane never opens a target, and `XLV_W_UNUSUAL_HYPERLINK` reports targets other than http(s) or mailto.
153
+ - **Notes (comments):** `note(cell, text | { text | runs, author?, box? })`; `box` sets the note box: `margins` in millimetres, `autoMargins`, `locked`, `lockText`, `anchor` (`'absolute'`, `'oneCell'`, `'twoCell'`); reading gives the same as `note.box`. The cell needs no value. Notes are written hidden, as spreadsheet applications show them when the pointer rests on the cell. Reading gives `sheet.notes` and `cell.note` (`text`, `author`, and `runs` for rich text). Threaded comments of current Excel versions are not read; their placeholder notes are, with `XLV_W_THREADED_COMMENTS`.
154
+ - **Tab colour and view:** `tabColor(color)`, and `view({ zoom, zoomScaleNormal, showGridLines, showRowColHeaders, showRuler, rightToLeft, layout, split, topLeftCell, activeCell })`. Reading gives `sheet.tabColor`, `sheet.view`, `sheet.properties`, `workbook.views` and `workbook.calculation`.
155
+ - **Streaming:** `writeWorkbookStream` takes `hyperlinks` and `notes` per row (`{ values, hyperlinks: [...], notes: [...] }`, by column position) and `tabColor`/`view` per sheet. `openWorkbookStream` sets `cell.note` on the rows it returns and lists a sheet's notes with `notes(sheet)`; it does not set `cell.hyperlink`.
156
+ - `editCells` and `appendRows` keep existing links and notes but cannot add new ones yet.
157
+
158
+ ### Images
159
+
160
+ ```ts
161
+ import { createWorkbook, readWorkbook, writeWorkbook } from 'xlvane';
162
+
163
+ declare const png: Uint8Array; // the bytes of a PNG, JPEG or GIF file
164
+ const workbook = createWorkbook();
165
+ const logo = workbook.addImage(png);
166
+ const sheet = workbook.addSheet('Report').set('A1', 'Quarterly report');
167
+ sheet.addImage(logo, 'E1:G4', { description: 'Company logo', hyperlink: 'https://example.com' });
168
+ sheet.addImage(logo, { from: 'A6', size: { width: 160, height: 90 } });
169
+
170
+ const read = readWorkbook(writeWorkbook(workbook));
171
+ for (const picture of read.getSheet('Report').images) console.log(picture.anchor, picture.image?.format);
172
+ ```
173
+
174
+ - **Adding:** `workbook.addImage(bytes)` takes PNG, JPEG or GIF bytes (recognised from the first bytes; other formats throw `XLV_E_INVALID_IMAGE`) and returns a number; the same image placed many times is stored once. `sheet.addImage(image, anchor, { name?, description?, hyperlink? })` places it over a range (`"E1:G4"`), between two corners (`{ from, to, editAs }`, each a cell or a 0-based `{ col, row }` with fractions) or at a corner with a size in pixels (`{ from, size }`). `sheet.background(image)` sets the sheet background. Pixels are converted at 96 per inch (9525 EMU); fractional positions use the sheet's column widths and row heights.
175
+ - **Reading:** `sheet.images` lists the pictures (`anchor` in EMU with the cell of each corner, `image` with `format`, `contentType` and `bytes`, `description`, `hyperlink`), `sheet.backgroundImage` the background, and `workbook.images` every image once. Charts, shapes and other drawing objects are not read; `XLV_W_DRAWING_CONTENT_NOT_READ` reports them, and `editCells` keeps them unchanged.
176
+ - **Rows:** `insertRows` and `deleteRows` move pictures with their rows. A deletion that would cut a picture, and a drawing with charts or shapes, are refused with `XLV_E_NOT_SUPPORTED`.
177
+ - **Streaming:** `writeWorkbookStream({ images: [png], sheets: [{ images: [{ image: 0, anchor: 'B2:D6' }], background: 0, rows }] })`.
178
+
179
+ ### Sheet rules and printing
180
+
181
+ ```ts
182
+ import { createWorkbook, readWorkbook, writeWorkbook } from 'xlvane';
183
+
184
+ const workbook = createWorkbook();
185
+ const sheet = workbook.addSheet('Report').set('A1', 'Quarterly report');
186
+ sheet.pageSetup({ orientation: 'landscape', paperSize: 9, margins: { left: 0.5, right: 0.5 }, fitToPage: true, fitToWidth: 1, fitToHeight: 0 });
187
+ sheet.headerFooter({ oddHeader: '&C&A', oddFooter: '&LPrinted &D&RPage &P of &N' });
188
+ sheet.printArea('A1:H40').printTitles({ rows: '1:2' }).pageBreaks({ rows: [21] });
189
+ sheet.protect({ sort: false, autoFilter: false });
190
+
191
+ const read = readWorkbook(writeWorkbook(workbook)).getSheet('Report');
192
+ console.log(read.pageSetup.orientation, read.printArea, read.printTitles, read.protection?.sheet);
193
+ ```
194
+
195
+ - **Page setup:** `pageSetup({ margins, orientation, paperSize, scale, fitToPage, fitToWidth, fitToHeight, pageOrder, blackAndWhite, draft, cellComments, errors, firstPageNumber, horizontalDpi, verticalDpi, copies, printOptions })`. Margins are in inches. Values outside the limits Office documents (for example margins of 49 inches or more, a scale other than 10–400 or 0) throw `XLV_E_INVALID_OPTION` with the allowed range.
196
+ - **Headers and footers:** `headerFooter({ oddHeader, oddFooter, evenHeader, evenFooter, firstHeader, firstFooter, differentOddEven, differentFirst, … })`, with the codes `&L`, `&C`, `&R` (sections), `&P` (page), `&N` (pages), `&D` (date), `&A` (sheet), `&F` (file), `&&` (an ampersand) and others; at most 255 characters each.
197
+ - **Print area, titles and page breaks:** `printArea(range | ranges)`, `printTitles({ rows: '1:2', columns: 'A' })` and `pageBreaks({ rows: [21], columns: ['F'] })` (the rows and columns that start a new page). The print area and titles are stored as the built-in names `_xlnm.Print_Area` and `_xlnm.Print_Titles`; `insertRows` and `deleteRows` move them.
198
+ - **Protection:** `protect(flags)` turns on sheet protection. Each flag is `true` when the action is locked, as in the file: `protect({ sort: false })` keeps sorting allowed. Flags not given keep the schema defaults. `protect({ password: 'secret' })` stores a SHA-512 password verifier (100,000 iterations, random salt; `hashPassword` gives a fixed salt or another algorithm), and `workbook.protect({ password })` protects the workbook structure. A protection password is not encryption: the content stays readable, and spreadsheet applications only refuse changes without it. Reading gives `sheet.protection` and `workbook.protection`; `verifyPassword(protection, password)` checks a password against them, including legacy 4-digit verifiers.
199
+ - **Reading** gives `sheet.pageSetup` (with defaults), `sheet.headerFooter`, `sheet.pageBreaks`, `sheet.printArea` and `sheet.printTitles`. `writeWorkbookStream` takes the same settings per sheet (`pageSetup`, `headerFooter`, `pageBreaks`, `printArea`, `printTitles`, `protect`).
200
+
201
+ ### Data validation and conditional formatting
202
+
203
+ ```ts
204
+ import { createWorkbook, readWorkbook, writeWorkbook } from 'xlvane';
205
+
206
+ const workbook = createWorkbook();
207
+ const sheet = workbook.addSheet('Orders').set('A1', 'Status').set('B1', 'Quantity').set('C1', 'Total');
208
+ sheet.validate('A2:A500', { type: 'list', formula1: '"Open,Shipped,Paid"', showErrorMessage: true, error: 'Pick a status from the list.' });
209
+ sheet.validate('B2:B500', { type: 'whole', operator: 'greaterThan', formula1: 0, showInputMessage: true, prompt: 'Quantity, at least 1' });
210
+ sheet.conditionalFormat('C2:C500', [
211
+ { type: 'cellIs', operator: 'greaterThan', formulas: [1000], style: { fill: { pattern: 'solid', fgColor: 'FFC7CE' } } },
212
+ { type: 'colorScale', colorScale: { cfvo: [{ type: 'min' }, { type: 'max' }], colors: ['F8696B', '63BE7B'] } },
213
+ ]);
214
+
215
+ const read = readWorkbook(writeWorkbook(workbook)).getSheet('Orders');
216
+ console.log(read.dataValidations.length, read.conditionalFormats.map((f) => f.sqref));
217
+ ```
218
+
219
+ - **Data validation:** `validate(range, rule)` with `type` `list`, `whole`, `decimal`, `date`, `time`, `textLength` or `custom`, an `operator`, `formula1`/`formula2`, input and error messages. `validate(range, null)` removes the validations added for exactly that range. Rules that Office would reject (a missing `formula1`, a message longer than the Office limit) throw `XLV_E_INVALID_OPTION` naming the property.
220
+ - **Conditional formatting:** `conditionalFormat(range, rules)` with `cellIs`, `expression`, `colorScale`, `dataBar`, `iconSet`, `top10`, `aboveAverage`, `containsText`, `timePeriod`, `duplicateValues` and the other rule types of the file format; `style` is a differential style (font, fill, border, number format). Priorities are numbered in the order the rules are added unless given.
221
+ - **Reading** gives `sheet.dataValidations` and `sheet.conditionalFormats`; `insertRows` and `deleteRows` move them with their rows, and `editCells` keeps them unchanged.
222
+
223
+ ### Tables
224
+
225
+ ```ts
226
+ import { createWorkbook, readTable, readWorkbook, writeWorkbook } from 'xlvane';
227
+
228
+ const workbook = createWorkbook();
229
+ workbook.addSheet('Data').addTable({
230
+ name: 'Sales',
231
+ ref: 'B2',
232
+ totalsRow: true,
233
+ style: { theme: 'TableStyleMedium9', showRowStripes: true },
234
+ columns: [{ name: 'Region', totalsRowLabel: 'Total' }, { name: 'Revenue', totalsRowFunction: 'sum' }],
235
+ rows: [['North', 1200], ['South', 980]],
236
+ });
237
+ const bytes = writeWorkbook(workbook);
238
+ console.log(readWorkbook(bytes).getSheet('Data').tables.map((t) => [t.displayName, t.ref]));
239
+ console.log(readTable(bytes, { table: 'Sales' }).rows); // [{ Region: 'North', Revenue: 1200 }, …]
240
+ ```
241
+
242
+ - **Writing:** `addTable({ name, displayName, ref, headerRow, totalsRow, style, columns, rows })` writes the header row (the column names), the data rows and the totals row into the cells, and the table part. `ref` is the top-left cell or the whole range. Totals use `SUBTOTAL` (`sum`, `average`, `count`, `countNums`, `min`, `max`, `stdDev`, `var`), a label (`totalsRowLabel`) or a custom formula; `calculatedColumnFormula` fills the empty data cells of a column. Styles are the 60 built-in table styles. Names follow the rules of defined names (no spaces) and are unique in the workbook together with the defined names; tables must not overlap other tables or merged cells; violations throw `XLV_E_INVALID_TABLE`.
243
+ - **Reading:** `sheet.tables` lists each table with its range, header and totals rows, columns (totals functions and formulas) and style. `readTable(bytes, { table: 'Sales' })` reads the data rows of a table by name; an unknown name throws `XLV_E_TABLE_NOT_FOUND` with the available names.
244
+ - **Streaming:** `writeWorkbookStream` takes `tables` per sheet with the whole range in `ref`; the header, data and totals values are streamed as rows.
245
+ - **Editing:** `editCells` keeps tables as they are; `insertRows` and `deleteRows` move, grow and shrink them with their rows and refuse to delete a table's header row, totals row or all of its data rows.
246
+
247
+ ## Tasks in one call
248
+
249
+ ```ts
250
+ import { readTable, writeTable, editCells, appendRows, diff, toCSV, fromCSV, toJSON, fromJSON } from 'xlvane';
251
+ import { z } from 'zod'; // or any Standard Schema library: Valibot, ArkType, …
252
+
253
+ declare const bytes: Uint8Array; // the file's contents
254
+
255
+ // Rows as objects, validated; problems come with cell addresses.
256
+ const { rows, issues } = readTable(bytes, { sheet: 'Orders', schema: z.object({ Customer: z.string(), Total: z.number() }) });
257
+
258
+ // A formatted report: bold header, frozen first row, filter, fitted widths.
259
+ const report = writeTable(rows, { sheet: 'Report', columns: ['Customer', { key: 'Total', numFmt: '#,##0.00' }] });
260
+
261
+ // Change cells or add rows in an existing file; everything else in the file stays as it was.
262
+ const { bytes: edited } = editCells(bytes, [{ sheet: 'Orders', cell: 'B7', value: 120.5 }]);
263
+ const { bytes: appended } = appendRows(bytes, 'Orders', [{ Customer: 'Dan', Total: 42 }]);
264
+
265
+ // What changed between two versions, as JSON.
266
+ console.log(diff(bytes, edited).sheets[0]?.cells);
267
+
268
+ // CSV and JSON in both directions. toCSV escapes text that spreadsheets would run as formulas.
269
+ const csv = toCSV(bytes, { sheet: 'Orders' });
270
+ const fromText = fromCSV('sku,stock\r\nA-1,12\r\n');
271
+ const json = toJSON(bytes).rows;
272
+ const fromData = fromJSON([{ sku: 'A-1', stock: 12 }]);
273
+ ```
274
+
275
+ - `readTable` finds the header row like `inspect()`, or takes `header: 3` or `header: false`. With a schema, `rows` has the schema's output type. Asynchronous schemas need `readTableAsync`.
276
+ - Warnings about the file (for example a shared formula that could not be expanded) are in `workbook.warnings`; `readTable`, `readTableAsync`, `toJSON` and `toCSV` also pass each of them to `onWarning`: `readTable(bytes, { onWarning: (w) => console.warn(w.code, w.message) })`.
277
+ - Dates: `readTable` returns `Date`s (their UTC fields hold the sheet's date and time); `dates: "iso"`, `toJSON` and `toCSV` give ISO 8601 text without a time zone, `2024-01-31` or `2024-01-31T13:45:00`.
278
+ - `editCells` and `appendRows` change only the cells they write, plus new entries in the shared strings and styles. Charts, comments, conditional formatting, data validation, defined names and every other part are kept byte for byte. xlvane does not calculate formulas. When the workbook has formulas, the result carries `XLV_W_FORMULAS_NOT_RECALCULATED` and the file asks spreadsheet applications to recalculate on load.
279
+ - `fromCSV` never creates formulas. It converts plain numbers, booleans and ISO dates (`types: "infer"`); numbers with leading zeros, whole numbers with more than 15 digits (IDs) and numbers a double cannot hold (`1e400`, `1e-400`) stay text. Every number `toCSV` writes reads back as the same number, except whole numbers above 15 digits. At most `maxCells` fields are read (default 5 000 000).
280
+ - Round trips: XLSX → CSV → XLSX keeps numbers, booleans and dates. XLSX → JSON → XLSX keeps numbers and booleans, but dates come back as text, because JSON has no date type and `fromJSON` does not guess; convert such fields to `Date` before `fromJSON` or `writeTable`.
281
+ - Shared formulas are translated to each cell when reading. If a formula cannot be translated safely, its `formula.text` is `null` and the reader reports `XLV_W_SHARED_FORMULA_NOT_EXPANDED`.
282
+
283
+ ### Inserting and deleting rows
284
+
285
+ ```ts
286
+ import { insertRows, deleteRows } from 'xlvane';
287
+
288
+ declare const bytes: Uint8Array;
289
+ // Two rows before row 5 of "Orders", formatted like row 4.
290
+ const { bytes: wider } = insertRows(bytes, { sheet: 'Orders', at: 5, count: 2, style: 'above' });
291
+ // Rows 10 to 12 of "Orders".
292
+ const { bytes: shorter } = deleteRows(wider, { sheet: 'Orders', at: 10, count: 3 });
293
+ ```
294
+
295
+ - Like `editCells`, they change an existing file in place and keep every other part as it was.
296
+ - References follow the rows as in a spreadsheet application: formulas on every sheet, merged cells, hyperlinks, notes, the autofilter, conditional formats, data validation, page breaks and defined names. A range grows when rows are inserted inside it and shrinks when some of its rows are deleted; a reference whose cells are all deleted becomes `#REF!`. The rules are in docs/facts/ref-adjust.md (observed in LibreOffice; not yet checked against Excel).
297
+ - Tables move, grow and shrink with their rows; deleting a table's header row, totals row or all of its data rows is refused.
298
+ - Pictures move with their rows. Drawings with charts, shapes or form controls, a deletion that would cut a picture, parts xlvane cannot rewrite (charts or pivot tables that may refer to the sheet), and formulas xlvane cannot rewrite safely (for example 3-D references such as `Jan:Mar!A1` that include the sheet) are refused with `XLV_E_NOT_SUPPORTED`. Data that would be pushed past row 1048576 gives `XLV_E_LIMIT_EXCEEDED`.
299
+ - They also refuse sheets whose conditional formats use the [MS-XLSX] extension, which xlvane writes for data bars with an extension setting (`gradient`, `borderColor`, `negativeFillColor`, `negativeBorderColor`, `axisPosition`, `axisColor`, `direction`, or an `autoMin`/`autoMax` threshold) and for the icon sets `3Stars`, `3Triangles`, `5Boxes` and `NoIcons`; insert the rows before adding such rules, or add the rules again after the change.
300
+ - Sheet views (selection, frozen panes) are not changed.
301
+ - `adjustFormula` and `adjustRange` apply the same rules to formula text you hold yourself.
302
+
303
+ ## Large files: streaming
304
+
305
+ ```ts
306
+ import { openWorkbookStream, writeWorkbookStream, readTableStream } from 'xlvane';
307
+ import { openWorkbookStreamFile, writeWorkbookStreamFile } from 'xlvane/node';
308
+ import { z } from 'zod';
309
+
310
+ declare const file: Blob; // a File from an <input>, or fs.openAsBlob(path) in Node.js
311
+ const schema = z.object({ id: z.number(), value: z.number() });
312
+
313
+ // Read row by row: worksheet cells are never all in memory. A Blob (File, fs.openAsBlob) also
314
+ // keeps the file itself out of memory.
315
+ const book = await openWorkbookStreamFile('large.xlsx');
316
+ for await (const row of book.rows('Data')) console.log(row.number, row.cells.map((c) => c.value));
317
+
318
+ // Rows as validated objects, one at a time.
319
+ for await (const { value, issues } of readTableStream(file, { schema })) { /* … */ }
320
+
321
+ // Write: rows are pulled from a (async) iterable only as fast as the output is consumed.
322
+ function* rows() {
323
+ yield { values: ['id', 'value'], style: { font: { bold: true } } };
324
+ for (let i = 1; i <= 1_000_000; i++) yield [i, Math.sqrt(i)];
325
+ }
326
+ await writeWorkbookStreamFile('out.xlsx', { sheets: [{ name: 'Data', freeze: { rows: 1 }, autoFilter: true, rows: rows() }] });
327
+ const stream = writeWorkbookStream({ sheets: [{ name: 'Data', rows: rows() }] }); // ReadableStream<Uint8Array>
328
+ ```
329
+
330
+ - Memory stays constant: the test suite reads and writes 50 000 and 400 000 rows and checks that the retained heap does not grow. It measured under 1 MB in both cases.
331
+ - Streamed text is stored in the cells (inline strings) by default. Pass `sharedStrings: true` for a shared string table, which is smaller when values repeat but kept in memory until the end.
332
+ - Rows come in file order. A shared formula whose master cell comes later in the file cannot be translated while streaming.
333
+ - `maxCells` does not apply when streaming; single parts may decompress to 4 GiB (`STREAM_LIMIT_DEFAULTS`). All other limits apply.
334
+ - `readTableStream` differs from `readTable` in what it can see in advance: it detects the header row among the first rows it reads and fixes the columns there (the columns of the header row, or of the first rows without one), so a column that first appears further down is not part of the objects. `readTable` takes the columns of the whole sheet. For files that fit in memory, `readTable` gives the complete table.
335
+
336
+ ## Benchmark
337
+
338
+ 100 000 rows × 20 columns (numbers, texts, dates, booleans). Median of 5 runs, each in a fresh Node.js 22.22.0 process. Machine: Intel Xeon @ 2.10 GHz, 4 logical cores, 15.7 GiB RAM, Linux (a cloud VM). Measured on 2026-10-30. Peak RSS includes Node.js and the imported libraries; the baseline with all three libraries imported is 135 MiB. Method, raw data and caveats: [bench/perf/RESULTS.md](https://github.com/DevXord/xlvane/blob/main/bench/perf/RESULTS.md). Reproduce with `npm run bench`.
339
+
340
+ | Write | Time, s | Peak RSS, MiB | | Read | Time, s | Peak RSS, MiB |
341
+ |---|---|---|---|---|---|---|
342
+ | xlvane `writeWorkbook` | 9.37 | 1005 | | xlvane `readWorkbook` | 4.52 | 690 |
343
+ | xlvane `writeWorkbookStream` | 4.91 | 170 | | xlvane `openWorkbookStream` | 5.32 | 309 |
344
+ | exceljs 4.4.0 `Workbook` | 18.72 | 2102 | | exceljs `xlsx.load` | 6.80 | 889 |
345
+ | exceljs `WorkbookWriter` (stream) | 6.31 | 194 | | exceljs `WorkbookReader` (stream) | 4.60 | 309 |
346
+ | SheetJS 0.18.5 `XLSX.write` | 10.96 | 1002 | | SheetJS `XLSX.read` | 9.04 | 838 |
347
+
348
+ The streaming reader of exceljs was faster than xlvane's in this run (4.60 s against 5.32 s). SheetJS is the npm release 0.18.5, which has CVE-2023-30533; the benchmark reads only files it generated itself. The results come from one machine and one data mix.
349
+
350
+ ## Inspecting a workbook
351
+
352
+ `inspect()` returns plain JSON data about the structure of a workbook: sheets and their visibility, used ranges, a detected header row, column types with example values, merged cells, formulas, rich-text cells, column styles, document properties, defined names and warnings. The schema is documented in [docs/inspect-json.md](https://github.com/DevXord/xlvane/blob/main/docs/inspect-json.md).
353
+
354
+ ```ts
355
+ import { inspect } from 'xlvane';
356
+ import { inspectFile } from 'xlvane/node';
357
+
358
+ declare const bytes: Uint8Array;
359
+ const report = inspect(bytes); // or: await inspectFile('report.xlsx')
360
+ console.log(JSON.stringify(report, null, 2));
361
+ ```
362
+
363
+ ## Errors and warnings
364
+
365
+ Problems with input files throw `XlvError` with a stable `code` (for example `XLV_E_SHEET_NOT_FOUND`), a `location` (part, sheet, cell), a `hint` saying what to do next and a `docsUrl`. `error.toJSON()` returns the same as data. Recoverable oddities are reported as warnings in `workbook.warnings`. All codes are described in [docs/errors.md](https://github.com/DevXord/xlvane/blob/main/docs/errors.md).
366
+
367
+ ## Untrusted input and limits
368
+
369
+ Every file is treated as untrusted:
370
+
371
+ - ZIP: sizes are measured on the bytes actually decompressed, not taken from headers; overlapping entries, duplicate names, encrypted entries, unsupported compression methods and multi-disk archives are rejected; CRC-32 is verified.
372
+ - XML: an own parser that rejects DTDs (so no external entities or entity expansion), recognises only the predefined entities, and does not recurse.
373
+ - Data from the file never becomes a key of a plain object, so names like `__proto__` are harmless.
374
+ - Macros are never run and external links are never followed; both are reported.
375
+
376
+ Limits can be changed per call, for example `readWorkbook(bytes, { limits: { maxCells: 50_000_000 } })`. Use `Infinity` to switch a limit off.
377
+
378
+ | Limit | Default | Protects against |
379
+ |---|---|---|
380
+ | `maxInputBytes` | 256 MiB | oversized input |
381
+ | `maxEntries` | 10 000 | archives with huge numbers of entries |
382
+ | `maxEntryBytes` | 256 MiB | one part decompressing to a huge size |
383
+ | `maxTotalBytes` | 512 MiB | all parts together decompressing to a huge size |
384
+ | `maxCompressionRatio` | 200 (checked after `compressionRatioMinBytes`, 1 MiB) | zip bombs |
385
+ | `maxXmlDepth` | 128 | deeply nested XML |
386
+ | `maxXmlAttributes` | 256 | elements with huge numbers of attributes |
387
+ | `maxXmlNameLength` | 1 024 | huge element or attribute names |
388
+ | `maxXmlTextLength` | 8 Mi characters | huge text nodes or attribute values |
389
+ | `maxXmlTreeElements` | 1 000 000 | huge workbook, styles or relationship parts (elements plus attributes) |
390
+ | `maxCells` | 5 000 000 | workbooks too large to hold in memory (measured: about 180 bytes per numeric cell, Node.js 26) |
391
+ | `maxSharedStringChars` | 512 Mi characters | many cells pointing at one huge shared string (a few MB that become many GB when values are copied) |
392
+
393
+ ## Package size
394
+
395
+ **Size budget:** 300 kB of gzip-compressed JavaScript for the library files in `dist/` (level 9, unminified as published; measured 209.1 kB for 1.0.0). **CLI size budget:** 40 kB for `dist/cli.js`, which is not loaded by `import 'xlvane'` (measured 27.3 kB for 1.0.0). `npm run check:pkg` fails when the budget is exceeded. The only dependency, fflate, adds about 22 kB gzip (its `esm/browser.js`, measured the same way).
396
+
397
+ ## Limitations
398
+
399
+ - xlvane does not calculate formulas. It reads and writes formula text and the cached results stored in the file; edited files ask spreadsheet applications to recalculate when they are opened.
400
+ - Password-protected (encrypted) workbooks and legacy `.xls` files are rejected with `XLV_E_UNSUPPORTED_CONTAINER`. A sheet protection password is not encryption and is supported.
401
+ - Charts, shapes, pivot tables, slicers and form controls are not modelled: reading skips them (with a warning for drawings), `editCells` keeps them unchanged, and row operations refuse sheets where they would have to move.
402
+ - The command line reads files, not standard input.
403
+
404
+ ## Links
405
+
406
+ - Changes in each version: [CHANGELOG.md](https://github.com/DevXord/xlvane/blob/main/CHANGELOG.md)
407
+ - Bugs and questions: [GitHub issues](https://github.com/DevXord/xlvane/issues)
408
+ - Reporting a vulnerability: [SECURITY.md](https://github.com/DevXord/xlvane/blob/main/SECURITY.md)
409
+
410
+ License: MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}