@xlsxflow/core 0.0.0-stage → 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.
- package/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/README.md +341 -2
- package/dist/index.cjs +6135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +601 -0
- package/dist/index.d.ts +601 -0
- package/dist/index.mjs +6099 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +69 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `SheetWriter`: workbook properties (title, author, company and so on), defined names, hidden and very hidden sheets, and sheet views (zoom, gridlines, headings, right-to-left). `SheetReader.readWorkbook` reads them back.
|
|
8
|
+
- `{ formatted: true }` reports each cell's text as Excel shows it, from its number format.
|
|
9
|
+
- `SheetEditor.insertRows`, `deleteRows`, `insertColumns` and `deleteColumns`. Everything that points at the moved cells moves with them: formulas on all sheets, defined names, merges, conditional formats, validations, hyperlinks, filters, page breaks, column widths, tables, pictures, notes, sparklines, data tables, chart series and pivot sources.
|
|
10
|
+
- `SheetEditor.setCells` edits and restyles cells of existing files; `addSheet` and `deleteSheet` add and remove sheets.
|
|
11
|
+
- `parseCsv` streams CSV rows, which `SheetWriter.addSheet` turns into a sheet.
|
|
12
|
+
- `SheetReader` reads Excel 97-2003 `.xls` files and OpenDocument `.ods` files: values, dates, formula results, merges, hidden rows, columns and sheets, frozen panes, defined names and document properties. `.ods` also gives formulas, hyperlinks and notes.
|
|
13
|
+
- `OdsWriter` writes `.ods` files: values, dates, formulas, merges, column widths, frozen panes and hidden sheets.
|
|
14
|
+
- Password-protected `.xlsx` files are rejected with an error that points to `decryptWorkbook` in `@xlsxflow/pro`, instead of a ZIP error.
|
|
15
|
+
- Cell notes on write (plain or formatted text) and `getComments()` on read.
|
|
16
|
+
- Conditional formats `cellIs`, `expression`, `top10`, `aboveAverage`, text rules, `duplicateValues`/`uniqueValues` and `iconSet`.
|
|
17
|
+
- Excel tables, sheet protection, page setup (margins, header and footer, print area and titles), row heights, hidden rows and columns, outline grouping, tab colour, and validation operators and messages.
|
|
18
|
+
- `Date` cell values, hyperlinks (URLs and locations in the workbook), `autoFilter`, and an optional shared string table on write.
|
|
19
|
+
- Reading returns formulas (`formulas: true`, shared formulas expanded), styles (`styles: true`) and rich text (`richText: true`). Theme and indexed colours are resolved to ARGB.
|
|
20
|
+
- PNG, JPEG and GIF images (`images` sheet option), anchored to a cell or stretched over a range. `getImages()` reads them back.
|
|
21
|
+
- Low-level parts are exported for add-ons: the ZIP reader and writer, `resolveWorkbookParts`, `readSharedStrings`, and `mapFormulaRefs`/`shiftFormula`.
|
|
22
|
+
- Tested: `.xlsm` files keep their macros through `SheetEditor`; the library runs in Cloudflare Workers without `nodejs_compat` (`scripts/workers` checks it).
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
Fixed crashes, hangs and memory blowups caused by crafted files:
|
|
27
|
+
|
|
28
|
+
- A ZIP directory pointing outside the file crashed Node.
|
|
29
|
+
- Unclosed tags and elements made the parser quadratic. The XML tokenizer is now linear on giant tags, text nodes and CDATA.
|
|
30
|
+
- Repeated hidden columns, far-right cells, fraction formats with long denominators and large style tables were slow or used unbounded memory.
|
|
31
|
+
- One deflated entry could be read many times under different names.
|
|
32
|
+
|
|
33
|
+
Also:
|
|
34
|
+
|
|
35
|
+
- `SheetEditor.edit` takes `maxUncompressedBytes`.
|
|
36
|
+
- Option values typed as enums are escaped, and appended rows encode control characters.
|
|
37
|
+
- `sheetToJson` keeps a `__proto__` header as an ordinary key.
|
|
38
|
+
- In-memory parts are capped at 1 GiB uncompressed by default.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Sheets with frozen panes were all marked as selected, so Excel opened them grouped.
|
|
43
|
+
- The reader dropped or corrupted cells at stream chunk boundaries.
|
|
44
|
+
- Styles pointed at the wrong font, fill or border, and styles used only by `AsyncIterable` rows were missing from `styles.xml`.
|
|
45
|
+
- Number formats with quoted text, escapes or colours (`#,##0.00 "USD"`, `[Red]0.0`) were taken for date formats.
|
|
46
|
+
- Reading a corrupt file without calling `getMetadata()` caused an unhandled promise rejection, which is fatal in Node.
|
|
47
|
+
- `addSheet` accepted sheet names Excel refuses to open. It now throws.
|
|
48
|
+
- Files Excel would repair:
|
|
49
|
+
- gradient stops were written as `<gradientStop>`;
|
|
50
|
+
- `vertical: 'middle'` was written as is instead of `center`;
|
|
51
|
+
- conditional formats came after validations.
|
|
52
|
+
- Data bar `minValue`/`maxValue` were ignored, every conditional format had priority 1, and colours and ranges were not XML-escaped.
|
|
53
|
+
- Strings with `_xHHHH_`, control characters, CR, or leading and trailing spaces now survive a round trip. `NaN` and `Infinity` are written as `#NUM!`.
|
|
54
|
+
- Reader:
|
|
55
|
+
- Strict OOXML and non-standard part names work;
|
|
56
|
+
- empty `<v/>` reads as empty and `-0` as `0`;
|
|
57
|
+
- out-of-order cells land in the right column;
|
|
58
|
+
- time-only values are no longer a day off, and datetimes keep milliseconds.
|
|
59
|
+
- frozen panes saved by Excel as `frozenSplit` (frozen after a split) are reported.
|
|
60
|
+
- `sheetToJson` ignored its `headerRowIndex` argument.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- The package ships compiled ESM and CommonJS builds with type declarations.
|
|
65
|
+
- Browser bundles no longer try to resolve Node's `fs`.
|
|
66
|
+
- `[Content_Types].xml` is written last in the ZIP.
|
|
67
|
+
- Real backpressure in the ZIP writer and worksheet stream. Producer errors now error the output stream instead of hanging it.
|
|
68
|
+
- About 2.5× faster reads, and a table-driven CRC-32 for faster writes.
|
|
69
|
+
- Styles, formulas and conditional formats are part of the MIT core. Paid add-ons are in the separate `@xlsxflow/pro` package.
|
|
70
|
+
- Benchmarks corrected: the earlier ExcelJS write time came from a cold first run, and write-excel-file was run with an old API.
|
|
71
|
+
|
|
72
|
+
## 1.0.0
|
|
73
|
+
|
|
74
|
+
- `SheetWriter` and `SheetEditor` stream the ZIP with data descriptors instead of buffering the file in memory.
|
|
75
|
+
- Dates are returned as ISO-8601 strings, detected from each cell's number format.
|
|
76
|
+
|
|
77
|
+
## 0.3.0-beta
|
|
78
|
+
|
|
79
|
+
- Workbooks with several sheets.
|
|
80
|
+
- Fixed a buffer bug that made parsing quadratic.
|
|
81
|
+
|
|
82
|
+
## 0.2.0-beta
|
|
83
|
+
|
|
84
|
+
- Deflate compression with `CompressionStream`: a 1M-cell file went from 30 MB to 2.9 MB.
|
|
85
|
+
- Inline and rich text strings are read.
|
|
86
|
+
|
|
87
|
+
## 0.1.0-beta
|
|
88
|
+
|
|
89
|
+
- First release: streaming reader, writer, and shared strings.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omkar Palika
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,342 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/xlsxflow/xlsxflow/main/assets/logo-wordmark-dark.svg" />
|
|
4
|
+
<img src="https://raw.githubusercontent.com/xlsxflow/xlsxflow/main/assets/logo-wordmark.svg" alt="XlsxFlow" width="320" />
|
|
5
|
+
</picture>
|
|
6
|
+
<p><strong>Streaming .xlsx reader, writer and editor for JavaScript. Also reads .xls and .ods, and writes .ods.</strong></p>
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@xlsxflow/core)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
10
|
|
|
3
|
-
|
|
11
|
+
<p>
|
|
12
|
+
<a href="#features">Features</a> •
|
|
13
|
+
<a href="#installation">Installation</a> •
|
|
14
|
+
<a href="#quick-start">Quick Start</a> •
|
|
15
|
+
<a href="#styles-formulas--conditional-formats">Styles & Formulas</a> •
|
|
16
|
+
<a href="#compared-with-sheetjs-and-exceljs">Comparison</a> •
|
|
17
|
+
<a href="#benchmarks">Benchmarks</a> •
|
|
18
|
+
<a href="#free-and-pro">Free and Pro</a>
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
**XlsxFlow** is a zero-dependency streaming reader, writer and editor for OpenXML (`.xlsx`) files. Built on native Web APIs (like `TransformStream` and `CompressionStream`), it handles millions of cells in flat memory.
|
|
25
|
+
|
|
26
|
+
Rows are read and written one at a time instead of loading the whole workbook, so it suits browsers, servers and edge runtimes alike.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **No dependencies**: TypeScript on Web APIs (`ReadableStream`, `CompressionStream`, `Blob`).
|
|
31
|
+
- **Streaming**: rows are read and written one at a time, so memory stays flat as files grow (10M cells written with about 1 MB of extra heap; see [Benchmarks](#benchmarks)).
|
|
32
|
+
- **Runs anywhere with Web APIs**: tested on Node 20.12+, Bun, browsers and Cloudflare Workers (without `nodejs_compat`). Deno provides the same APIs but is not tested yet.
|
|
33
|
+
- **Read, write and edit**: stream rows out of a file, generate one on the fly, or change cells, rows, columns and sheets of an existing file while keeping everything else in it.
|
|
34
|
+
- **Styles and formulas**: fonts, fills, borders, alignment, number formats, conditional formats, validations, tables, notes, hyperlinks, autofilters, images, protection and page setup. Formulas and styles read back too.
|
|
35
|
+
- **Older and open formats**: the same reader opens Excel 97-2003 `.xls` files and OpenDocument `.ods` files, and `OdsWriter` writes `.ods`.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# npm
|
|
41
|
+
npm install @xlsxflow/core
|
|
42
|
+
|
|
43
|
+
# pnpm
|
|
44
|
+
pnpm add @xlsxflow/core
|
|
45
|
+
|
|
46
|
+
# yarn
|
|
47
|
+
yarn add @xlsxflow/core
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### Parsing an Excel File (Streaming)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { SheetReader, createBlobReader } from '@xlsxflow/core';
|
|
56
|
+
|
|
57
|
+
// Browser / Edge: any Blob or File (e.g. from <input type="file">)
|
|
58
|
+
const blob = await fetch('https://example.com/data.xlsx').then(r => r.blob());
|
|
59
|
+
|
|
60
|
+
const reader = new SheetReader();
|
|
61
|
+
// Omit sheetName to read the first tab
|
|
62
|
+
const rows = await reader.parse(createBlobReader(blob), { sheetName: 'Sheet1' });
|
|
63
|
+
|
|
64
|
+
for await (const row of rows) {
|
|
65
|
+
console.log(row.rowNumber, row.cells); // 1 ['ID', 'Name', 'Amount']
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const meta = await rows.getMetadata(); // merged cells, hidden rows/cols, freeze panes
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Dates come back as ISO-8601 strings. Opt in to more detail, each indexed like `row.cells`:
|
|
72
|
+
|
|
73
|
+
- `{ formulas: true }` gives `row.formulas`, with shared formulas expanded per cell.
|
|
74
|
+
- `{ styles: true }` gives `row.styles`, as `CellStyle` objects (the same shape the writer takes). Theme and palette colours are resolved to ARGB.
|
|
75
|
+
- `{ richText: true }` gives `row.richText`, the formatted runs of cells that have them. `row.cells` still holds the plain text.
|
|
76
|
+
- `{ formatted: true }` gives `row.formatted`, each cell's text as Excel (en-US) shows it: `1,234.50`, `25.6%`, `(42)`, `08-Oct-2026 2:05 PM`. It covers sections, conditions, dates and elapsed times, fractions, scientific notation, currency and text formats. Repeat fills (`*`) and colours are left out, and other locales are shown as en-US.
|
|
77
|
+
|
|
78
|
+
`await reader.readWorkbook(createBlobReader(blob))` lists the sheets with their visibility, the defined names and the document properties, without reading any sheet.
|
|
79
|
+
|
|
80
|
+
Hyperlinks are in `(await rows.getMetadata()).hyperlinks`, as `{ ref, hyperlink, tooltip? }` in the writer's format. `await rows.getImages()` returns the sheet's pictures in the writer's `images` format too (bytes included, read on that call), so they can be written back unchanged. Charts and shapes are skipped. `await rows.getComments()` returns the sheet's notes as `{ ref, text, author? }`.
|
|
81
|
+
|
|
82
|
+
Parts held in memory (workbook, shared strings, styles) are capped at 1 GiB uncompressed each, to stop zip bombs. The streamed worksheet is uncapped. Change both with `maxUncompressedBytes` (`Infinity` disables); `SheetEditor.edit(reader, { maxUncompressedBytes })` takes the same limit.
|
|
83
|
+
|
|
84
|
+
**Reading files from untrusted users** (uploads on a server): set `maxUncompressedBytes` to what you expect, such as `50_000_000`. Corrupt or crafted ZIPs (overlapping entries, entries that inflate past their stated size, directories pointing outside the file) are rejected either way. The limit also bounds the empty cells added before far-right cells, since one tiny cell in column XFD pads its row to 16,384 values. Values are returned as written: hyperlinks may be `javascript:` URLs and text may start with `=`, so check them before putting them in a web page or a CSV that a spreadsheet will open.
|
|
85
|
+
|
|
86
|
+
`sheetToJson(rows, headerRowIndex = 0)` turns the rows into objects keyed by the header row, and `streamToCsv(rows)` returns the sheet as CSV text. Both hold the whole result in memory.
|
|
87
|
+
|
|
88
|
+
In Node.js, read straight from disk with `XlsxFlow.readFile`, or pass `await createFileReader(path)` to any function that takes a reader:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { XlsxFlow } from '@xlsxflow/core';
|
|
92
|
+
|
|
93
|
+
for await (const row of await XlsxFlow.readFile('./data.xlsx')) {
|
|
94
|
+
console.log(row.cells);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Writing an Excel File
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { SheetWriter } from '@xlsxflow/core';
|
|
102
|
+
|
|
103
|
+
const writer = new SheetWriter();
|
|
104
|
+
|
|
105
|
+
writer.addSheet('Report', [
|
|
106
|
+
['Header 1', 'Header 2', 'Header 3'],
|
|
107
|
+
[1, 2, 3],
|
|
108
|
+
['Data', 'More Data', 'Even More Data'],
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
// write() returns a ReadableStream<Uint8Array> of the .xlsx file
|
|
112
|
+
const blob = await new Response(writer.write()).blob();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Rows can also be an `AsyncIterable<Row>`, so millions of rows can be generated lazily; the writer only pulls rows as fast as the output is consumed.
|
|
116
|
+
|
|
117
|
+
Document properties and named ranges go to the constructor; visibility and view settings go to each sheet:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const writer = new SheetWriter({
|
|
121
|
+
properties: { title: 'Q3 sales', creator: 'Finance', company: 'ACME' }, // File > Info in Excel
|
|
122
|
+
definedNames: [
|
|
123
|
+
{ name: 'TaxRate', ref: '0.18' },
|
|
124
|
+
{ name: 'Sales', ref: 'Data!$B$2:$B$100' },
|
|
125
|
+
{ name: 'Total', ref: 'Data!$B$101', sheet: 'Data' }, // scoped to one sheet
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
writer.addSheet('Lookup', lookupRows, { state: 'hidden' }); // or 'veryHidden'
|
|
129
|
+
writer.addSheet('Data', rows, { view: { zoom: 90, showGridLines: false, rightToLeft: false } });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Excel opens on the first visible sheet. Invalid or duplicate names, and a workbook with no visible sheet, are rejected before anything is written.
|
|
133
|
+
|
|
134
|
+
### Converting CSV
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { SheetWriter, parseCsv } from '@xlsxflow/core';
|
|
138
|
+
|
|
139
|
+
// parseCsv streams rows from a string or a ReadableStream<Uint8Array>, e.g. file.stream()
|
|
140
|
+
const xlsx = new SheetWriter().addSheet('Data', parseCsv(csvStream)).write();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Quoted fields can hold delimiters, line breaks and `""`. Unquoted numbers and `TRUE`/`FALSE` become numbers and booleans, and empty fields become empty cells; `{ convert: false }` keeps everything as text, and `{ delimiter: ';' }` sets the delimiter. Dates stay text, since CSV files do not say which date order they use.
|
|
144
|
+
|
|
145
|
+
### .xls and .ods Files
|
|
146
|
+
|
|
147
|
+
`SheetReader` tells the format from the file's contents, so `parse` and `readWorkbook` work the same on `.xls`
|
|
148
|
+
(Excel 97-2003) and `.ods` (LibreOffice, Google Sheets, Excel's OpenDocument export) as on `.xlsx`:
|
|
149
|
+
values, dates, formula results, merged cells, hidden rows, columns and sheets, frozen panes, defined names
|
|
150
|
+
and document properties. `formatted: true` works on both; `formulas: true` works on `.ods`.
|
|
151
|
+
|
|
152
|
+
- `.xls` files are read whole into memory (capped by `maxUncompressedBytes`, 1 GiB by default). Formula text,
|
|
153
|
+
styles, hyperlinks and notes are not read from them, and files older than Excel 97 (BIFF5 and earlier) are
|
|
154
|
+
not supported.
|
|
155
|
+
- `.ods` files stream like `.xlsx`. Hyperlinks and notes are read too; `formatted` returns the text the file
|
|
156
|
+
stores for each cell.
|
|
157
|
+
- A file saved with a password is rejected with an error that points to `decryptWorkbook` in
|
|
158
|
+
[`@xlsxflow/pro`](#free-and-pro).
|
|
159
|
+
|
|
160
|
+
`OdsWriter` writes `.ods` with the same rows as `SheetWriter`: values, dates, formulas (converted to
|
|
161
|
+
OpenFormula), merged cells, column widths, frozen panes, hidden sheets and document properties. Cell styles,
|
|
162
|
+
hyperlinks, notes and images are not written to `.ods`.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { OdsWriter } from '@xlsxflow/core';
|
|
166
|
+
|
|
167
|
+
const ods = new OdsWriter({ properties: { title: 'Report' } })
|
|
168
|
+
.addSheet('Data', rows, { columnWidths: [20, 10], freezePanes: { row: 1 } })
|
|
169
|
+
.write(); // ReadableStream<Uint8Array>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Editing an Existing File
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { SheetEditor, createBlobReader } from '@xlsxflow/core';
|
|
176
|
+
|
|
177
|
+
const editor = new SheetEditor();
|
|
178
|
+
editor.setCells('Sheet1', {
|
|
179
|
+
B2: 42, C2: { formula: 'B2*2' }, D9: 'new cell', A3: null, // null clears
|
|
180
|
+
A1: { style: { font: { bold: true }, fill: { type: 'solid', fgColor: 'FFFFFF00' } } }, // restyle, keep content
|
|
181
|
+
B3: { value: 7, style: { numFmt: '0.00' } },
|
|
182
|
+
});
|
|
183
|
+
editor.appendSheet('Sheet1', [['new', 'row']]); // appended after the last existing row
|
|
184
|
+
editor.insertRows('Sheet1', 5, 3); // 3 empty rows before row 5
|
|
185
|
+
editor.deleteRows('Sheet1', 20, 2); // rows 20-21
|
|
186
|
+
editor.insertColumns('Sheet1', 'C'); // or deleteColumns('Sheet1', 'C', 2)
|
|
187
|
+
editor.addSheet('Summary', [['Total', { value: null, formula: 'SUM(Sheet1!B:B)' }]]);
|
|
188
|
+
editor.deleteSheet('Old');
|
|
189
|
+
const edited = editor.edit(createBlobReader(existingBlob)); // ReadableStream<Uint8Array>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Edited cells keep their style. A style change is merged into the cell's current format: font properties, border sides and alignment settings you name change and the rest stay, while a fill or number format replaces the old one. The sheet streams through one row at a time, and every other part of the file is copied without being unpacked. Excel recalculates formulas when it opens the file. A shared formula whose first cell is overwritten is written out in full in the cells that used it.
|
|
193
|
+
|
|
194
|
+
Macro-enabled workbooks (`.xlsm`) keep their VBA project and content type through every edit.
|
|
195
|
+
|
|
196
|
+
`addSheet` takes an array of rows with values, formulas and styles; for hyperlinks, notes and sheet options, write the workbook with `SheetWriter`. `deleteSheet` removes names scoped to the sheet and turns other defined names that point at it into `#REF!`; formulas in other sheets that point at it are not rewritten. `insertRows`, `deleteRows`, `insertColumns` and `deleteColumns` move everything that points at the cells, as Excel does:
|
|
197
|
+
- formulas on every sheet and the workbook's defined names (print areas, named ranges);
|
|
198
|
+
- merged cells, conditional formats, validations, hyperlinks, the filter and its column filters, page breaks and column widths;
|
|
199
|
+
- tables, pictures, notes, sparklines, What-If data tables, chart series and pivot-table sources.
|
|
200
|
+
|
|
201
|
+
Ranges that span inserted rows or columns grow, and ranges over deleted ones shrink. References to deleted cells become `#REF!`. Columns inserted inside a table become table columns named Column1, Column2 and so on. Deleting a table's header row, all its data rows or all its columns is refused. Operations run in the order given, and `setCells` addresses count after the cells have moved. Inserted rows and columns are empty: they don't copy the formatting of their neighbours. Every sheet streams through the editor, because any of its formulas might point at the moved cells.
|
|
202
|
+
|
|
203
|
+
## Styles, Formulas & Conditional Formats
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { SheetWriter } from '@xlsxflow/core';
|
|
207
|
+
|
|
208
|
+
const writer = new SheetWriter();
|
|
209
|
+
writer.addSheet('Sales', [
|
|
210
|
+
[{ value: 'Total Revenue', style: { font: { bold: true }, fill: { type: 'solid', fgColor: 'FF1E3A5F' } } }],
|
|
211
|
+
[100], [250],
|
|
212
|
+
[{ value: null, formula: '=SUM(A2:A3)' }], // cached result computed on write
|
|
213
|
+
[new Date(), { value: new Date(), style: { numFmt: 'dd/mm/yyyy' } }], // UTC; default format yyyy-mm-dd[ hh:mm:ss]
|
|
214
|
+
[{ value: 'Docs', hyperlink: 'https://example.com' }, { value: 'Back to top', hyperlink: '#Sales!A1' }],
|
|
215
|
+
[{ value: null, richText: [{ text: 'Net ', font: { bold: true } }, { text: 'revenue', font: { color: 'FFC00000' } }] }],
|
|
216
|
+
], {
|
|
217
|
+
freezePanes: { row: 1 },
|
|
218
|
+
autoFilter: 'A1:B1',
|
|
219
|
+
conditionalFormats: [{ range: 'A2:A3', rule: { type: 'dataBar', color: 'FF06B6D4' } }],
|
|
220
|
+
images: [
|
|
221
|
+
{ data: logoPng, at: 'D1', height: 40 }, // PNG/JPEG/GIF bytes; width follows the aspect ratio
|
|
222
|
+
{ data: chartJpeg, range: 'D4:H14', altText: 'Trend' }, // stretched over the cells
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Other sheet options:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
writer.addSheet('Report', rows, {
|
|
231
|
+
conditionalFormats: [
|
|
232
|
+
{ range: 'B2:B100', rule: { type: 'cellIs', operator: 'greaterThan', formulae: [1000], style: { fill: { type: 'solid', fgColor: 'FFFFC7CE' } } } },
|
|
233
|
+
{ range: 'A2:A100', rule: { type: 'containsText', text: 'urgent', style: { font: { bold: true, color: 'FF9C0006' } } } },
|
|
234
|
+
{ range: 'C2:C100', rule: { type: 'iconSet', iconSet: '3TrafficLights1' } }, // also expression, top10, aboveAverage, duplicateValues...
|
|
235
|
+
],
|
|
236
|
+
dataValidations: [{ sqref: 'B2:B100', type: 'whole', operator: 'between', formula1: '0', formula2: '100', error: 'Use 0-100' }],
|
|
237
|
+
tables: [{ name: 'Sales', ref: 'A1:C100' }], // header names come from row 1
|
|
238
|
+
rows: { 1: { height: 24 }, 5: { outlineLevel: 1, hidden: true } },
|
|
239
|
+
columns: [{ width: 30 }, { width: 12, outlineLevel: 1 }],
|
|
240
|
+
protection: { password: 'secret', sort: true }, // Excel's legacy hash: deters edits, is not encryption
|
|
241
|
+
pageSetup: { orientation: 'landscape', paperSize: 9, fitToWidth: 1, fitToHeight: 0, printArea: 'A1:C100', printTitleRows: '1', footer: '&CPage &P of &N' },
|
|
242
|
+
tabColor: 'FF00B050',
|
|
243
|
+
mergeCells: ['A1:C1'],
|
|
244
|
+
columnWidths: [30, 12], // in characters; `columns[i].width` wins where both are set
|
|
245
|
+
autoFitColumns: true, // widths from the longest value (array rows only)
|
|
246
|
+
});
|
|
247
|
+
// Notes: [{ value: 'Q3', comment: { text: 'Restated', author: 'Ana' } }]
|
|
248
|
+
// Formatted notes: comment: { text: [{ text: 'Ana:', font: { bold: true } }, { text: ' restated' }] }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Formulas are stored for Excel to calculate when it opens the file. For array rows, the writer also stores a cached result for simple formulas (`SUM`, `AVERAGE`, `COUNT`, `MIN`, `MAX`, `IF`, `CONCATENATE` and arithmetic), so other readers see a value.
|
|
252
|
+
|
|
253
|
+
Strings are written inline, which keeps memory flat. `new SheetWriter({ sharedStrings: true })` stores each distinct string once instead. Files are smaller when values repeat, but the distinct strings stay in memory until the file is finished.
|
|
254
|
+
|
|
255
|
+
## Compared with SheetJS and ExcelJS
|
|
256
|
+
|
|
257
|
+
Checked against each project's own documentation on 8 October 2026. "Pro" means a paid add-on.
|
|
258
|
+
|
|
259
|
+
| | XlsxFlow | SheetJS Community Edition | ExcelJS 4.4 |
|
|
260
|
+
|---|---|---|---|
|
|
261
|
+
| Streaming `.xlsx` read and write | Yes | No (streams CSV, HTML and JSON out) | Yes |
|
|
262
|
+
| Cell styles, read and write | Yes | No (SheetJS Pro) | Yes |
|
|
263
|
+
| Images | Yes | No | Yes |
|
|
264
|
+
| `.xls` | Read | Read and write | No |
|
|
265
|
+
| `.ods` | Read and write | Read and write | No |
|
|
266
|
+
| `.xlsb`, `.numbers` and other formats | No | Yes | No |
|
|
267
|
+
| Charts | Add (Pro) | No (SheetJS Pro) | No |
|
|
268
|
+
| Pivot tables | Add (Pro) | No (SheetJS Pro) | Partial, undocumented |
|
|
269
|
+
| Password-protected files | Open and save (Pro) | Old `.xls` obfuscation only (SheetJS Pro opens AES files) | No |
|
|
270
|
+
| Licence | MIT, Pro is paid | Apache 2.0 | MIT |
|
|
271
|
+
|
|
272
|
+
SheetJS reads and writes far more formats, and ExcelJS has a longer track record (its last release was in
|
|
273
|
+
October 2023). XlsxFlow focuses on `.xlsx`: streaming in flat memory, keeping everything in a file it edits,
|
|
274
|
+
and running on Web APIs alone.
|
|
275
|
+
|
|
276
|
+
## Benchmarks
|
|
277
|
+
|
|
278
|
+
Write benchmark: 10 numeric columns, at 100,000 rows (1M cells) and 1,000,000 rows (10M cells). Each library ran in its own process on Node v25.8.2 with a 4 GB heap limit, and "Heap" is the growth in heap usage. Times are from one run on a laptop with other apps open; runs on that machine varied by up to 2×, so treat differences under about 20% as a tie. Reproduce with `npx tsx scripts/benchmark-competitors.ts` (inside `packages/core`, after `pnpm build`). `BENCH_ROWS=1000000` runs 10M cells, `BENCH_LIBS=xlsxflow,exceljs` runs a subset, and a library still writing after `BENCH_TIMEOUT_MIN` minutes (default 10) is stopped. The 10M runs for SheetJS and excel4node used a 30-minute limit.
|
|
279
|
+
|
|
280
|
+
1M cells:
|
|
281
|
+
|
|
282
|
+
| Library | Write Time | File Size | Heap |
|
|
283
|
+
|---|---|---|---|
|
|
284
|
+
| **XlsxFlow** | **2,899 ms** | **2.9 MB** | **+2 MB** |
|
|
285
|
+
| ExcelJS 4.4 (streaming writer) | 3,397 ms | 3.0 MB | +9 MB |
|
|
286
|
+
| SheetJS 0.20.3, `compression: true` | 4,370 ms | 8.4 MB | +140 MB |
|
|
287
|
+
| SheetJS 0.20.3, default options | 5,379 ms | 31.4 MB | +140 MB |
|
|
288
|
+
| write-excel-file | 7,681 ms | 2.8 MB | +2 MB |
|
|
289
|
+
| xlsx-populate | 9,852 ms | 2.9 MB | +114 MB |
|
|
290
|
+
| excel4node | 14,973 ms | 3.1 MB | +205 MB |
|
|
291
|
+
| msexcel-builder | fails to run (`Invalid character in name: fileVersion`) | | |
|
|
292
|
+
|
|
293
|
+
10M cells:
|
|
294
|
+
|
|
295
|
+
| Library | Write Time | File Size | Heap |
|
|
296
|
+
|---|---|---|---|
|
|
297
|
+
| **XlsxFlow** | **19.5 s** | **29.9 MB** | **+1 MB** |
|
|
298
|
+
| ExcelJS 4.4 (streaming writer) | 22.0 s | 31.2 MB | +7 MB |
|
|
299
|
+
| write-excel-file | 68.1 s | 29.3 MB | +1 MB |
|
|
300
|
+
| xlsx-populate | 75.3 s | 30.0 MB | +1,118 MB |
|
|
301
|
+
| SheetJS 0.20.3 (with and without compression) | not finished after 30 min | | |
|
|
302
|
+
| excel4node | not finished after 30 min | | |
|
|
303
|
+
|
|
304
|
+
Rows are pulled from an async generator. The writer only generates rows as fast as the output stream is consumed, so memory stays flat as row count grows.
|
|
305
|
+
|
|
306
|
+
Read benchmark: a 100,000 × 10 file written by ExcelJS (shared strings, numbers, dates, booleans; 6.4 MB). Every library reads every cell. "Peak RSS" is the peak memory of the reading process. Median of three runs. Reproduce with `npx tsx scripts/benchmark-read-competitors.ts`.
|
|
307
|
+
|
|
308
|
+
| Library | Read Time | Peak RSS |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| **XlsxFlow** | **2,279 ms** | **96 MB** |
|
|
311
|
+
| ExcelJS 4.4 (streaming reader) | 2,669 ms | 263 MB |
|
|
312
|
+
| ExcelJS 4.4 | 3,680 ms | 667 MB |
|
|
313
|
+
| SheetJS 0.20.3 | 6,091 ms | 549 MB |
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Free and Pro
|
|
318
|
+
|
|
319
|
+
`@xlsxflow/core` is free and MIT licensed, including every feature on this page. [`@xlsxflow/pro`](https://www.npmjs.com/package/@xlsxflow/pro) is a paid add-on with a licence key:
|
|
320
|
+
|
|
321
|
+
| | Core (free) | Pro |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| Read, write and edit `.xlsx` / `.xlsm`, styles, formulas, images, tables, notes | Yes | Yes |
|
|
324
|
+
| Read `.xls`, read and write `.ods` | Yes | Yes |
|
|
325
|
+
| Fill Excel templates with data, repeating rows for lists | | Yes |
|
|
326
|
+
| Add column, bar, line, area and pie charts | | Yes |
|
|
327
|
+
| Add pivot tables | | Yes |
|
|
328
|
+
| Open and save password-protected `.xlsx` files | | Yes |
|
|
329
|
+
|
|
330
|
+
Pro is $5 per developer, paid once, with a perpetual licence and a year of updates. See the [Pro README](https://www.npmjs.com/package/@xlsxflow/pro) for details.
|
|
331
|
+
|
|
332
|
+
## Changelog
|
|
333
|
+
|
|
334
|
+
See [CHANGELOG.md](https://github.com/xlsxflow/xlsxflow/blob/main/packages/core/CHANGELOG.md).
|
|
335
|
+
|
|
336
|
+
## Contributing and security
|
|
337
|
+
|
|
338
|
+
Bug reports and pull requests are welcome: see [CONTRIBUTING.md](https://github.com/xlsxflow/xlsxflow/blob/main/CONTRIBUTING.md). Report security issues privately as described in [SECURITY.md](https://github.com/xlsxflow/xlsxflow/blob/main/SECURITY.md).
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
[MIT](https://github.com/xlsxflow/xlsxflow/blob/main/packages/core/LICENSE). Pro is a separate package under its own licence and does not change the terms of the core.
|