xlvane 1.0.0 → 1.1.0

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.
@@ -47,7 +47,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
47
47
 
48
48
  | API | Status | Notes | Source in exceljs |
49
49
  |---|---|---|---|
50
- | `name`, `id`, `state` | partly | Ids are given as in exceljs (the first free id), but the file gets `sheetId` 1, 2, 3… in sheet order and a loaded workbook numbers its sheets 1, 2, 3… in file order, because xlvane's core neither writes given sheet ids nor reports them on reading. exceljs writes `id` as `sheetId` and keeps it on loading, so after removing a sheet, `getWorksheet(id)` can give another sheet than with exceljs; look sheets up by name to be safe. | `lib/doc/worksheet.js` L33; `lib/doc/workbook.js` L43–51; `lib/xlsx/xform/book/sheet-xform.js` L7, L18 |
50
+ | `name`, `id`, `state` | partly | Ids are given as in exceljs (the first free id), written as `sheetId` and kept on loading and by the `model` setter, as exceljs does, so `getWorksheet(id)` finds the same sheet after removing a sheet, writing and loading (D-F03). Differences: an id outside 1 to 65534, the range Office uses (W2-20), is written as the lowest unused `sheetId`; a file's missing, repeated or larger than 65534 `sheetId` gives the worksheet the lowest free id (exceljs reads `NaN`, lets the later sheet replace the earlier one, or uses the large number as an array index); the streaming `WorkbookReader` numbers worksheets 1, 2, 3… in file order, where exceljs uses the number N of the part `xl/worksheets/sheetN.xml` (`lib/stream/xlsx/workbook-reader.js` L107-109, L293-297). | `lib/doc/worksheet.js` L33; `lib/doc/workbook.js` L43–51; `lib/xlsx/xform/book/sheet-xform.js` L7, L18 |
51
51
  | `columns` (get / set `{ header, key, width, style }`) | supported | As in exceljs, a column exists for every column a cell was created in, also after `addRow` and on a loaded file without `<cols>`, so `columns.forEach(c => { c.eachCell(…); c.width = … })` works; columns without width, style, hidden flag or outline level are not written (D-R09). An empty sheet gives `[]` where exceljs gives `null`. | `lib/doc/worksheet.js` L203, L209, L243–261; `lib/doc/row.js` L48 |
52
52
  | `getColumn(key \| letter \| number)` | supported | | `lib/doc/worksheet.js` L243 |
53
53
  | `addRow(values \| object, style?)`, `addRows` | supported | Style `'i'` / `'i+'` copies the style of the row above, as in exceljs. | `lib/doc/worksheet.js` L406, L414 |
@@ -58,7 +58,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
58
58
  | `rowCount`, `actualRowCount`, `columnCount` | supported | | `lib/doc/worksheet.js` L374, L378, L313 |
59
59
  | `mergeCells(range \| t, l, b, r)`, `unMergeCells` | supported | | `lib/doc/worksheet.js` L602, L656 |
60
60
  | `mergeCellsWithoutStyle(...)`, `findRows(start, length)`, `lastColumn`, `fillFormula(range, formula, results, shareType)`, `model` (get / set), `destroy()` | supported | `fillFormula` writes the formula into the top-left cell and `{ sharedFormula }` (or the same array formula) into the others, with results from a function, a flat array or a 2-D array, as in exceljs (X4-29). `findRows` returns `_rows.slice(start - 1, start - 1 + length)` as exceljs does (D-E06). `model` is xlvane's own plain snapshot (see `workbook.model`). | `lib/doc/worksheet.js` L181, L309, L370, L607, L678–721, L842–903 |
61
- | `views` | partly | The first view is written: frozen panes, split panes (`xSplit`/`ySplit` in 1/20 point), `zoomScale` (10–400), `zoomScaleNormal`, `showGridLines`, `showRowColHeaders`, `showRuler`, `rightToLeft`, `style` (`pageBreakPreview`, `pageLayout`), `activeCell`, and `topLeftCell` of a normal view (X4-28). Differences: further views are not written, because xlvane's core writes one `sheetView` per worksheet; `workbookViewId` is always 0; frozen panes get the cell right of and below the panes as `topLeftCell`, a split view none (exceljs's split `topLeftCell` is the top-left cell of the bottom-right pane, `pane@topLeftCell`, which xlvane's core does not write), but its `activeCell` is written and read back as the selection of the active pane; `activePane` is the pane the split implies; `tabSelected` is written for the first visible worksheet, whatever the view says. Every worksheet reads back with one view shaped as exceljs reads it. | `lib/doc/worksheet.js` L114; `lib/xlsx/xform/sheet/sheet-view-xform.js`; `index.d.ts` L33–149 |
61
+ | `views` | partly | The first view is written: frozen panes, split panes (`xSplit`/`ySplit` in 1/20 point), `zoomScale` (10–400), `zoomScaleNormal`, `showGridLines`, `showRowColHeaders`, `showRuler`, `rightToLeft`, `style` (`pageBreakPreview`, `pageLayout`), `activeCell`, and `topLeftCell` of a normal view (X4-28). Differences: further views are not written, because xlvane's core writes one `sheetView` per worksheet; `workbookViewId` is always 0; frozen panes always get the cell right of and below the panes as `topLeftCell` (exceljs writes a frozen view's own `topLeftCell` when given, sheet-view-xform.js L50-53); a split view's `topLeftCell` is written as `pane@topLeftCell` and read back, as in exceljs (L72-85, L118-124; D-F02), and its `activeCell` is written and read back as the selection of the active pane; `activePane` is the pane the split implies; `tabSelected` is written for the first visible worksheet, whatever the view says. Every worksheet reads back with one view shaped as exceljs reads it. | `lib/doc/worksheet.js` L114; `lib/xlsx/xform/sheet/sheet-view-xform.js`; `index.d.ts` L33–149 |
62
62
  | `autoFilter` (range string) | supported | | `lib/doc/worksheet.js` L116 |
63
63
  | `insertRow(pos, values, style?)`, `insertRows(pos, values, style?)` | partly | Style options `n`, `i`, `i+`, `o`, `o+` as in exceljs. Built on `spliceRows`, so the differences below apply. | `lib/doc/worksheet.js` L422, L428, L444 |
64
64
  | `spliceRows(start, count, ...inserts)` | partly | xlvane moves rows as a spreadsheet application does; exceljs 4.4.0 moves only values, styles and heights (X4-15). Differences: row objects keep their identity and their `number` follows them (exceljs copies values between row objects); the hidden flag, notes and links move with the row (exceljs leaves hidden/outline at the position and drops notes); merged ranges move, grow when rows are inserted inside them (inserted cells get a copy of the top-left cell's style, the other cells keep their own), shrink when rows are removed, and are dropped when their top-left cell is removed or one cell or nothing is left, as LibreOffice does (R-14; exceljs keeps stale ranges and dissolves merges on removal); the autofilter range moves and shrinks, and stops at the last row or column (exceljs leaves it); cells inserted inside a defined name join it (exceljs leaves a gap), and names follow whatever the case of their sheet name (exceljs compares it exactly); styles are copied, never shared; removing more rows than follow the removed block works (exceljs leaves rows behind). Formulas in every worksheet follow the splice as in a spreadsheet application (xlvane `adjustFormula`, D-B01; exceljs leaves them unchanged); formulas given in the inserted rows keep the text the caller wrote. If a formula cannot be adjusted safely, the call throws `XLV_E_NOT_SUPPORTED` before anything changes. | `lib/doc/worksheet.js` L483–548 |
@@ -151,6 +151,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
151
151
  | Named ESM imports | extra | Not available with exceljs 4.4.0. |
152
152
  | `exceljs/dist/es5`, `exceljs/dist/es5/index.js`, `exceljs/dist/exceljs.js`, `exceljs/dist/exceljs.min.js`, `exceljs/dist/exceljs.bare.js`, `exceljs/dist/exceljs.bare.min.js` | supported | Mapped to the main entry in the package's `exports` (an ES module; `require()` needs Node.js 22.12+). exceljs's files there are an ES5 build and UMD browser bundles; the API is the same (X4-26). |
153
153
  | Other deep imports (`exceljs/lib/...`) | not supported | Internal files of exceljs; not offered. |
154
+ | `ModelContainer` | not supported | exceljs exports it as a holder of a workbook model with an `xlsx` property (`lib/exceljs.nodejs.js` L3, `lib/doc/modelcontainer.js`). The class is on the module object so that code referring to it loads; constructing it throws `XLV_E_NOT_SUPPORTED`. `xlvane migrate` lists its uses. |
154
155
  | `RelationshipType`, `DocumentType`, `ReadingOrder` | supported | The enums of `lib/doc/enums.js`, on the module object and as named exports. |
155
156
  | `PaperSize` | supported | exceljs declares it as a `const enum` in index.d.ts (inlined by TypeScript, absent at run time); @xlvane/compat exports it as an object and on the default export. |
156
157
  | TypeScript types (`ExcelJS.Worksheet`, `ExcelJS.Fill`, `ExcelJS.stream.xlsx.WorkbookWriterOptions`, …) | supported | The type names of exceljs's index.d.ts are exported and usable through `import * as ExcelJS`, `import ExcelJS from`, and `import ExcelJS = require('exceljs')`, under `moduleResolution` bundler, nodenext (ESM and CommonJS) and `module: commonjs` (`tests/exceljs/types-compat.test.ts`, which type-checks code from the exceljs README). In a CommonJS project TypeScript refuses `import ExcelJS = require(...)` of an ES module under `module: node16` (TS1471, checked with TypeScript 5.9.3); `module: nodenext` and `module: commonjs` accept it. `Buffer` is `Uint8Array`. Enums (`ValueType`, `FormulaType`, `ErrorValue`, `PaperSize`) are also types. `WorkbookStreamReaderOptions` is an alias of `WorkbookReaderOptions`. As in exceljs's declarations, `getTable(name)` returns `Table`, `workbook.getImage(id)` returns `Image`, `getImages()[i].range` is required and `fillFormula`'s `results` is `(row, col) => value`, a flat or a 2-D array (index.d.ts L1327, L1342–1346, L1365, L1780); at run time an unknown table name or image id gives `undefined`, as in exceljs. The module object is frozen: adding or replacing its properties (monkey-patching) has no effect. |
@@ -34,6 +34,7 @@
34
34
  |---|---|---|
35
35
  | `name` | string | sheet name |
36
36
  | `index` | number | 0-based position |
37
+ | `sheetId` | number or `null` | the sheet's id in the file (`sheet@sheetId`), independent of its position; `null` when the file has none, an invalid one or a repeated one |
37
38
  | `kind` | `"worksheet" \| "chartsheet" \| "dialogsheet" \| "macrosheet"` | only worksheets have cells |
38
39
  | `state` | `"visible" \| "hidden" \| "veryHidden"` | visibility |
39
40
  | `dimension` | string or `null` | range declared in the file, e.g. `"A1:D20"` |
@@ -112,6 +113,7 @@ Values in `samples` are JSON-safe: numbers, booleans, strings (cut to 100 charac
112
113
  {
113
114
  "name": "Sales",
114
115
  "index": 0,
116
+ "sheetId": 1,
115
117
  "kind": "worksheet",
116
118
  "state": "visible",
117
119
  "dimension": "A1:C3",
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xlvane 1.0.0: documentation in one file
1
+ # xlvane 1.1.0: documentation in one file
2
2
 
3
3
  This file joins the README of the xlvane package, the Agent Skill and the documentation in docs/. It is generated by scripts/package-docs.mjs; the separate files are in the same package.
4
4
 
@@ -12,7 +12,7 @@ Read, inspect and write XLSX files; input files are treated as untrusted. ESM on
12
12
  npm install xlvane
13
13
  ```
14
14
 
15
- 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.
15
+ 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`.
16
16
 
17
17
  ## Quick start
18
18
 
@@ -36,12 +36,12 @@ From a shell: `npx xlvane inspect orders.xlsx`, then `npx xlvane read orders.xls
36
36
  - [Tasks in one call](#tasks-in-one-call): tables of rows, CSV, JSON, editing files, diff · [inserting and deleting rows](#inserting-and-deleting-rows)
37
37
  - [Large files: streaming](#large-files-streaming)
38
38
  - [Inspecting a workbook](#inspecting-a-workbook) · [Errors and warnings](#errors-and-warnings) · [Untrusted input and limits](#untrusted-input-and-limits)
39
- - [Limitations](#limitations) · [Package size](#package-size)
39
+ - [Limitations](#limitations) · [Package size](#package-size) · [Links](#links)
40
40
  - exceljs users: [`@xlvane/compat`](https://www.npmjs.com/package/@xlvane/compat) and `xlvane migrate`
41
41
 
42
42
  ## Tested environments
43
43
 
44
- 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`). 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/`.
44
+ 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/`.
45
45
 
46
46
  ## Command line in 5 minutes
47
47
 
@@ -111,7 +111,7 @@ In Node.js, `writeWorkbookFile(workbook, 'sales.xlsx')` from `xlvane/node` write
111
111
  - 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.
112
112
  - 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.
113
113
  - 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.
114
- - 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.
114
+ - Workbook: `workbook.views([{ x, y, width, height, firstSheet, activeTab, visibility }])` sets the workbook windows (`activeTab` and `firstSheet` are checked against the sheets when the workbook is written) and `workbook.calculation({ fullCalcOnLoad: true })` asks applications to recalculate when the file is opened. `addSheet(name, { sheetId })` sets the sheet's id in the file (1 to 65534, unique; other sheets get the lowest unused ids), and reading gives `sheet.sheetId`.
115
115
  - 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`.
116
116
  - 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.
117
117
  - 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.
@@ -157,7 +157,7 @@ console.log(read.hyperlinks, read.notes, read.cell('B2')?.note?.text, read.tabCo
157
157
 
158
158
  - **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.
159
159
  - **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`.
160
- - **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`.
160
+ - **Tab colour and view:** `tabColor(color)`, and `view({ zoom, zoomScaleNormal, showGridLines, showRowColHeaders, showRuler, rightToLeft, layout, split, topLeftCell, activeCell })`; a split window takes `split: { x, y, topLeftCell }` in points, where `topLeftCell` is the first cell shown in the bottom-right pane. Reading gives `sheet.tabColor`, `sheet.view`, `sheet.properties`, `workbook.views` and `workbook.calculation`.
161
161
  - **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`.
162
162
  - `editCells` and `appendRows` keep existing links and notes but cannot add new ones yet.
163
163
 
@@ -299,10 +299,10 @@ const { bytes: shorter } = deleteRows(wider, { sheet: 'Orders', at: 10, count: 3
299
299
  ```
300
300
 
301
301
  - Like `editCells`, they change an existing file in place and keep every other part as it was.
302
- - 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).
302
+ - References follow the rows as in a spreadsheet application: formulas on every sheet, merged cells, hyperlinks, notes, the autofilter, conditional formats (with their thresholds), 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).
303
303
  - Tables move, grow and shrink with their rows; deleting a table's header row, totals row or all of its data rows is refused.
304
304
  - 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`.
305
- - 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.
305
+ - Conditional formats and data validations of the [MS-XLSX] extension (extended data bars, the icon sets `3Stars`, `3Triangles`, `5Boxes` and `NoIcons`, and rules written by Excel) move with the rows too, and so do thresholds that refer to cells (`{ type: 'formula', value: '$C$12' }`). Other extended content that refers to cells, such as sparklines or slicers, is refused.
306
306
  - Sheet views (selection, frozen panes) are not changed.
307
307
  - `adjustFormula` and `adjustRange` apply the same rules to formula text you hold yourself.
308
308
 
@@ -341,7 +341,7 @@ const stream = writeWorkbookStream({ sheets: [{ name: 'Data', rows: rows() }] })
341
341
 
342
342
  ## Benchmark
343
343
 
344
- 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](../../bench/perf/RESULTS.md). Reproduce with `npm run bench`.
344
+ 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`.
345
345
 
346
346
  | Write | Time, s | Peak RSS, MiB | | Read | Time, s | Peak RSS, MiB |
347
347
  |---|---|---|---|---|---|---|
@@ -398,7 +398,7 @@ Limits can be changed per call, for example `readWorkbook(bytes, { limits: { max
398
398
 
399
399
  ## Package size
400
400
 
401
- **Size budget:** 300 kB of gzip-compressed JavaScript for the library files in `dist/` (level 9, unminified as published; measured 208.6 kB on 2026-10-31). **CLI size budget:** 40 kB for `dist/cli.js`, which is not loaded by `import 'xlvane'` (measured 25.7 kB on 2026-10-31). `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).
401
+ **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).
402
402
 
403
403
  ## Limitations
404
404
 
@@ -407,6 +407,12 @@ Limits can be changed per call, for example `readWorkbook(bytes, { limits: { max
407
407
  - 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.
408
408
  - The command line reads files, not standard input.
409
409
 
410
+ ## Links
411
+
412
+ - Changes in each version: [CHANGELOG.md](https://github.com/DevXord/xlvane/blob/main/CHANGELOG.md)
413
+ - Bugs and questions: [GitHub issues](https://github.com/DevXord/xlvane/issues)
414
+ - Reporting a vulnerability: [SECURITY.md](https://github.com/DevXord/xlvane/blob/main/SECURITY.md)
415
+
410
416
  License: MIT
411
417
 
412
418
  <!-- file: skills/xlvane/SKILL.md -->
@@ -580,7 +586,7 @@ const { bytes: wider } = insertRows(bytes, { sheet: 'Sales', at: 5, count: 2, st
580
586
  const { bytes: shorter } = deleteRows(wider, { sheet: 'Sales', at: 5, count: 2 });
581
587
  ```
582
588
 
583
- Formulas on every sheet, merged cells, links, notes, conditional formats, 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, 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`.
589
+ 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`.
584
590
 
585
591
  `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.
586
592
 
@@ -881,7 +887,7 @@ The CSV text given to `fromCSV` has a quoted field that is never closed, text ri
881
887
  `hashPassword` and `verifyPassword` throw it for a hash algorithm other than SHA-1, SHA-256, SHA-384 and SHA-512, and `hashPassword` without a `salt` throws it in a runtime that has no `crypto.getRandomValues` (pass your own random salt there; SR-50, D-C08).
882
888
  `editCells` throws it for an array or shared formula in an edit (it writes plain formulas only), and for a cell inside an array formula's range or the master of a shared formula with other cells in its range (D-E02); `details.cell` names it.
883
889
  `appendRows` throws it when a table ends with a totals row on the sheet's last used row (the new rows would fall below the totals row; the hint gives the `insertRows` call that grows the table instead), or when growing a table for the new rows would overlap another table (D-R03).
884
- `insertRows` and `deleteRows` (and `xlvane edit --insert-rows` / `--delete-rows`) also throw it when the sheet has content whose cell references xlvane cannot move yet: tables, charts, shapes and other drawing objects (pictures alone are moved, and deleting rows that would cut a picture is refused), pivot tables, form controls, embedded objects, custom sheet views, threaded comments, extended content such as data bars or sparklines, a chart or pivot cache elsewhere that names the sheet, a data table, an array formula the change would split, or a formula or defined name that cannot be adjusted safely (for example a 3-D reference such as `Jan:Mar!A1` that includes the sheet, D-B01). The message names the part or the cell, and `details` gives the element, relationship type or formula.
890
+ `insertRows` and `deleteRows` (and `xlvane edit --insert-rows` / `--delete-rows`) also throw it when the sheet has content whose cell references xlvane cannot move yet: tables, charts, shapes and other drawing objects (pictures alone are moved, and deleting rows that would cut a picture is refused), pivot tables, form controls, embedded objects, custom sheet views, threaded comments, extended content such as sparklines or slicers (the conditional formats and data validations of the [MS-XLSX] extension are moved, D-F04), a chart or pivot cache elsewhere that names the sheet, a data table, an array formula the change would split, or a formula or defined name that cannot be adjusted safely (for example a 3-D reference such as `Jan:Mar!A1` that includes the sheet, D-B01). The message names the part or the cell, and `details` gives the element, relationship type or formula.
885
891
  **Fix:** for `@xlvane/compat`, see docs/exceljs-compat.md for the supported subset; remove the call, or keep using exceljs for files that need the feature.
886
892
  For `insertRows`/`deleteRows`, insert or delete the rows in a spreadsheet application, or remove the named content first.
887
893
 
@@ -1001,6 +1007,7 @@ A worksheet contains a row whose number is not a whole number from 1 to 1048576,
1001
1007
  |---|---|---|
1002
1008
  | `name` | string | sheet name |
1003
1009
  | `index` | number | 0-based position |
1010
+ | `sheetId` | number or `null` | the sheet's id in the file (`sheet@sheetId`), independent of its position; `null` when the file has none, an invalid one or a repeated one |
1004
1011
  | `kind` | `"worksheet" \| "chartsheet" \| "dialogsheet" \| "macrosheet"` | only worksheets have cells |
1005
1012
  | `state` | `"visible" \| "hidden" \| "veryHidden"` | visibility |
1006
1013
  | `dimension` | string or `null` | range declared in the file, e.g. `"A1:D20"` |
@@ -1079,6 +1086,7 @@ Values in `samples` are JSON-safe: numbers, booleans, strings (cut to 100 charac
1079
1086
  {
1080
1087
  "name": "Sales",
1081
1088
  "index": 0,
1089
+ "sheetId": 1,
1082
1090
  "kind": "worksheet",
1083
1091
  "state": "visible",
1084
1092
  "dimension": "A1:C3",
@@ -1244,7 +1252,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
1244
1252
 
1245
1253
  | API | Status | Notes | Source in exceljs |
1246
1254
  |---|---|---|---|
1247
- | `name`, `id`, `state` | partly | Ids are given as in exceljs (the first free id), but the file gets `sheetId` 1, 2, 3… in sheet order and a loaded workbook numbers its sheets 1, 2, 3… in file order, because xlvane's core neither writes given sheet ids nor reports them on reading. exceljs writes `id` as `sheetId` and keeps it on loading, so after removing a sheet, `getWorksheet(id)` can give another sheet than with exceljs; look sheets up by name to be safe. | `lib/doc/worksheet.js` L33; `lib/doc/workbook.js` L43–51; `lib/xlsx/xform/book/sheet-xform.js` L7, L18 |
1255
+ | `name`, `id`, `state` | partly | Ids are given as in exceljs (the first free id), written as `sheetId` and kept on loading and by the `model` setter, as exceljs does, so `getWorksheet(id)` finds the same sheet after removing a sheet, writing and loading (D-F03). Differences: an id outside 1 to 65534, the range Office uses (W2-20), is written as the lowest unused `sheetId`; a file's missing, repeated or larger than 65534 `sheetId` gives the worksheet the lowest free id (exceljs reads `NaN`, lets the later sheet replace the earlier one, or uses the large number as an array index); the streaming `WorkbookReader` numbers worksheets 1, 2, 3… in file order, where exceljs uses the number N of the part `xl/worksheets/sheetN.xml` (`lib/stream/xlsx/workbook-reader.js` L107-109, L293-297). | `lib/doc/worksheet.js` L33; `lib/doc/workbook.js` L43–51; `lib/xlsx/xform/book/sheet-xform.js` L7, L18 |
1248
1256
  | `columns` (get / set `{ header, key, width, style }`) | supported | As in exceljs, a column exists for every column a cell was created in, also after `addRow` and on a loaded file without `<cols>`, so `columns.forEach(c => { c.eachCell(…); c.width = … })` works; columns without width, style, hidden flag or outline level are not written (D-R09). An empty sheet gives `[]` where exceljs gives `null`. | `lib/doc/worksheet.js` L203, L209, L243–261; `lib/doc/row.js` L48 |
1249
1257
  | `getColumn(key \| letter \| number)` | supported | | `lib/doc/worksheet.js` L243 |
1250
1258
  | `addRow(values \| object, style?)`, `addRows` | supported | Style `'i'` / `'i+'` copies the style of the row above, as in exceljs. | `lib/doc/worksheet.js` L406, L414 |
@@ -1255,7 +1263,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
1255
1263
  | `rowCount`, `actualRowCount`, `columnCount` | supported | | `lib/doc/worksheet.js` L374, L378, L313 |
1256
1264
  | `mergeCells(range \| t, l, b, r)`, `unMergeCells` | supported | | `lib/doc/worksheet.js` L602, L656 |
1257
1265
  | `mergeCellsWithoutStyle(...)`, `findRows(start, length)`, `lastColumn`, `fillFormula(range, formula, results, shareType)`, `model` (get / set), `destroy()` | supported | `fillFormula` writes the formula into the top-left cell and `{ sharedFormula }` (or the same array formula) into the others, with results from a function, a flat array or a 2-D array, as in exceljs (X4-29). `findRows` returns `_rows.slice(start - 1, start - 1 + length)` as exceljs does (D-E06). `model` is xlvane's own plain snapshot (see `workbook.model`). | `lib/doc/worksheet.js` L181, L309, L370, L607, L678–721, L842–903 |
1258
- | `views` | partly | The first view is written: frozen panes, split panes (`xSplit`/`ySplit` in 1/20 point), `zoomScale` (10–400), `zoomScaleNormal`, `showGridLines`, `showRowColHeaders`, `showRuler`, `rightToLeft`, `style` (`pageBreakPreview`, `pageLayout`), `activeCell`, and `topLeftCell` of a normal view (X4-28). Differences: further views are not written, because xlvane's core writes one `sheetView` per worksheet; `workbookViewId` is always 0; frozen panes get the cell right of and below the panes as `topLeftCell`, a split view none (exceljs's split `topLeftCell` is the top-left cell of the bottom-right pane, `pane@topLeftCell`, which xlvane's core does not write), but its `activeCell` is written and read back as the selection of the active pane; `activePane` is the pane the split implies; `tabSelected` is written for the first visible worksheet, whatever the view says. Every worksheet reads back with one view shaped as exceljs reads it. | `lib/doc/worksheet.js` L114; `lib/xlsx/xform/sheet/sheet-view-xform.js`; `index.d.ts` L33–149 |
1266
+ | `views` | partly | The first view is written: frozen panes, split panes (`xSplit`/`ySplit` in 1/20 point), `zoomScale` (10–400), `zoomScaleNormal`, `showGridLines`, `showRowColHeaders`, `showRuler`, `rightToLeft`, `style` (`pageBreakPreview`, `pageLayout`), `activeCell`, and `topLeftCell` of a normal view (X4-28). Differences: further views are not written, because xlvane's core writes one `sheetView` per worksheet; `workbookViewId` is always 0; frozen panes always get the cell right of and below the panes as `topLeftCell` (exceljs writes a frozen view's own `topLeftCell` when given, sheet-view-xform.js L50-53); a split view's `topLeftCell` is written as `pane@topLeftCell` and read back, as in exceljs (L72-85, L118-124; D-F02), and its `activeCell` is written and read back as the selection of the active pane; `activePane` is the pane the split implies; `tabSelected` is written for the first visible worksheet, whatever the view says. Every worksheet reads back with one view shaped as exceljs reads it. | `lib/doc/worksheet.js` L114; `lib/xlsx/xform/sheet/sheet-view-xform.js`; `index.d.ts` L33–149 |
1259
1267
  | `autoFilter` (range string) | supported | | `lib/doc/worksheet.js` L116 |
1260
1268
  | `insertRow(pos, values, style?)`, `insertRows(pos, values, style?)` | partly | Style options `n`, `i`, `i+`, `o`, `o+` as in exceljs. Built on `spliceRows`, so the differences below apply. | `lib/doc/worksheet.js` L422, L428, L444 |
1261
1269
  | `spliceRows(start, count, ...inserts)` | partly | xlvane moves rows as a spreadsheet application does; exceljs 4.4.0 moves only values, styles and heights (X4-15). Differences: row objects keep their identity and their `number` follows them (exceljs copies values between row objects); the hidden flag, notes and links move with the row (exceljs leaves hidden/outline at the position and drops notes); merged ranges move, grow when rows are inserted inside them (inserted cells get a copy of the top-left cell's style, the other cells keep their own), shrink when rows are removed, and are dropped when their top-left cell is removed or one cell or nothing is left, as LibreOffice does (R-14; exceljs keeps stale ranges and dissolves merges on removal); the autofilter range moves and shrinks, and stops at the last row or column (exceljs leaves it); cells inserted inside a defined name join it (exceljs leaves a gap), and names follow whatever the case of their sheet name (exceljs compares it exactly); styles are copied, never shared; removing more rows than follow the removed block works (exceljs leaves rows behind). Formulas in every worksheet follow the splice as in a spreadsheet application (xlvane `adjustFormula`, D-B01; exceljs leaves them unchanged); formulas given in the inserted rows keep the text the caller wrote. If a formula cannot be adjusted safely, the call throws `XLV_E_NOT_SUPPORTED` before anything changes. | `lib/doc/worksheet.js` L483–548 |
@@ -1348,6 +1356,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
1348
1356
  | Named ESM imports | extra | Not available with exceljs 4.4.0. |
1349
1357
  | `exceljs/dist/es5`, `exceljs/dist/es5/index.js`, `exceljs/dist/exceljs.js`, `exceljs/dist/exceljs.min.js`, `exceljs/dist/exceljs.bare.js`, `exceljs/dist/exceljs.bare.min.js` | supported | Mapped to the main entry in the package's `exports` (an ES module; `require()` needs Node.js 22.12+). exceljs's files there are an ES5 build and UMD browser bundles; the API is the same (X4-26). |
1350
1358
  | Other deep imports (`exceljs/lib/...`) | not supported | Internal files of exceljs; not offered. |
1359
+ | `ModelContainer` | not supported | exceljs exports it as a holder of a workbook model with an `xlsx` property (`lib/exceljs.nodejs.js` L3, `lib/doc/modelcontainer.js`). The class is on the module object so that code referring to it loads; constructing it throws `XLV_E_NOT_SUPPORTED`. `xlvane migrate` lists its uses. |
1351
1360
  | `RelationshipType`, `DocumentType`, `ReadingOrder` | supported | The enums of `lib/doc/enums.js`, on the module object and as named exports. |
1352
1361
  | `PaperSize` | supported | exceljs declares it as a `const enum` in index.d.ts (inlined by TypeScript, absent at run time); @xlvane/compat exports it as an object and on the default export. |
1353
1362
  | TypeScript types (`ExcelJS.Worksheet`, `ExcelJS.Fill`, `ExcelJS.stream.xlsx.WorkbookWriterOptions`, …) | supported | The type names of exceljs's index.d.ts are exported and usable through `import * as ExcelJS`, `import ExcelJS from`, and `import ExcelJS = require('exceljs')`, under `moduleResolution` bundler, nodenext (ESM and CommonJS) and `module: commonjs` (`tests/exceljs/types-compat.test.ts`, which type-checks code from the exceljs README). In a CommonJS project TypeScript refuses `import ExcelJS = require(...)` of an ES module under `module: node16` (TS1471, checked with TypeScript 5.9.3); `module: nodenext` and `module: commonjs` accept it. `Buffer` is `Uint8Array`. Enums (`ValueType`, `FormulaType`, `ErrorValue`, `PaperSize`) are also types. `WorkbookStreamReaderOptions` is an alias of `WorkbookReaderOptions`. As in exceljs's declarations, `getTable(name)` returns `Table`, `workbook.getImage(id)` returns `Image`, `getImages()[i].range` is required and `fillFormula`'s `results` is `(row, col) => value`, a flat or a 2-D array (index.d.ts L1327, L1342–1346, L1365, L1780); at run time an unknown table name or image id gives `undefined`, as in exceljs. The module object is frozen: adding or replacing its properties (monkey-patching) has no effect. |
@@ -2198,6 +2207,13 @@ A workbook loaded with `xlsx.load()` is written again by `xlsx.writeBuffer()` (`
2198
2207
  "type": "integer",
2199
2208
  "minimum": 0
2200
2209
  },
2210
+ "sheetId": {
2211
+ "type": [
2212
+ "integer",
2213
+ "null"
2214
+ ],
2215
+ "minimum": 0
2216
+ },
2201
2217
  "kind": {
2202
2218
  "enum": [
2203
2219
  "worksheet",
@@ -2548,6 +2564,7 @@ A workbook loaded with `xlsx.load()` is written again by `xlsx.writeBuffer()` (`
2548
2564
  "required": [
2549
2565
  "name",
2550
2566
  "index",
2567
+ "sheetId",
2551
2568
  "kind",
2552
2569
  "state",
2553
2570
  "dimension",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xlvane",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Read, inspect and write XLSX files; input is treated as untrusted. ESM only, one runtime dependency (fflate).",
5
5
  "keywords": [
6
6
  "xlsx",
@@ -167,7 +167,7 @@ const { bytes: wider } = insertRows(bytes, { sheet: 'Sales', at: 5, count: 2, st
167
167
  const { bytes: shorter } = deleteRows(wider, { sheet: 'Sales', at: 5, count: 2 });
168
168
  ```
169
169
 
170
- Formulas on every sheet, merged cells, links, notes, conditional formats, 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, 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`.
170
+ 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`.
171
171
 
172
172
  `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.
173
173