@xlsxflow/core 1.1.4 → 1.1.6
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 +24 -0
- package/README.md +33 -12
- package/dist/index.cjs +52 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +52 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.6
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `SheetWriter`, `SheetEditor` and `OdsWriter` keep up to four chunks queued for compression instead of one, so building the next chunk overlaps compressing the last. Writing 10M cells takes about 10% less time; the files are the same, and the writer still waits while nobody reads its output.
|
|
8
|
+
|
|
9
|
+
### Testing
|
|
10
|
+
|
|
11
|
+
- Public CI on every push and pull request: the tests on Node 20, 22 and 24 with coverage; the README's claims in Node, Bun, Deno, Chromium, Firefox, WebKit and Cloudflare Workers; and written and edited files validated with Microsoft's Open XML SDK and opened in LibreOffice. OpenSSF Scorecard and CodeQL check the repository.
|
|
12
|
+
|
|
13
|
+
## 1.1.5
|
|
14
|
+
|
|
15
|
+
Fixes found while testing every claim in the README on Node 20.12 and 25, Bun, Deno, Chrome and Cloudflare Workers.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `SheetWriter`, `SheetEditor` and `OdsWriter` wait while nobody reads their output on Node and Bun too. Their `CompressionStream` takes thousands of chunks without pushing back, so a slow consumer let the writer pull the whole row source into memory.
|
|
20
|
+
- On Bun, `.ods` files and other ZIP entries stored without compression read correctly. Bun's `Blob.slice().stream()` runs past the end of the slice, which 1.1.4's size check rejected.
|
|
21
|
+
- `SheetWriter` refuses a merge that overlaps a table. Excel tables cannot hold merged cells, and Excel repaired such files by removing the table.
|
|
22
|
+
|
|
23
|
+
### Docs
|
|
24
|
+
|
|
25
|
+
- The README says to close a `createFileReader` reader (Node 25 stops the process when an open file is garbage-collected), lists Deno as tested, fixes the sheet-options example that merged cells inside a table, and corrects the comparison table: ExcelJS 4.4 has no pivot tables.
|
|
26
|
+
|
|
3
27
|
## 1.1.4
|
|
4
28
|
|
|
5
29
|
Fixes from the third round of end-user tests (700 cases).
|
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
[](https://www.npmjs.com/package/@xlsxflow/core)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://github.com/xlsxflow/xlsxflow/actions/workflows/ci.yml)
|
|
11
|
+
[](https://scorecard.dev/viewer/?uri=github.com/xlsxflow/xlsxflow)
|
|
10
12
|
|
|
11
13
|
<p>
|
|
12
14
|
<a href="#features">Features</a> •
|
|
@@ -28,8 +30,8 @@ Rows are read and written one at a time instead of loading the whole workbook, s
|
|
|
28
30
|
## Features
|
|
29
31
|
|
|
30
32
|
- **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
|
|
32
|
-
- **Runs anywhere with Web APIs**: tested on Node 20.12
|
|
33
|
+
- **Streaming**: rows are read and written one at a time, so memory stays flat as files grow (10M cells written with about 2 MB of extra heap; see [Benchmarks](#benchmarks)).
|
|
34
|
+
- **Runs anywhere with Web APIs**: tested on Node 20.12 and later, Bun, Deno, Chrome, Firefox, Safari (WebKit) and Cloudflare Workers (without `nodejs_compat`).
|
|
33
35
|
- **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
36
|
- **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
37
|
- **Older and open formats**: the same reader opens Excel 97-2003 `.xls` files and OpenDocument `.ods` files, and `OdsWriter` writes `.ods`.
|
|
@@ -89,11 +91,20 @@ Parts held in memory (workbook, shared strings, styles) are capped at 1 GiB unco
|
|
|
89
91
|
In Node.js, read straight from disk with `XlsxFlow.readFile`, or pass `await createFileReader(path)` to any function that takes a reader:
|
|
90
92
|
|
|
91
93
|
```typescript
|
|
92
|
-
import { XlsxFlow } from '@xlsxflow/core';
|
|
94
|
+
import { XlsxFlow, SheetReader, createFileReader } from '@xlsxflow/core';
|
|
93
95
|
|
|
94
96
|
for await (const row of await XlsxFlow.readFile('./data.xlsx')) {
|
|
95
97
|
console.log(row.cells);
|
|
96
98
|
}
|
|
99
|
+
|
|
100
|
+
// createFileReader keeps the file open until you close it, after you are done with
|
|
101
|
+
// everything read through it (Node 25 stops the process when an open file is garbage-collected)
|
|
102
|
+
const file = await createFileReader('./data.xlsx');
|
|
103
|
+
try {
|
|
104
|
+
console.log((await new SheetReader().readWorkbook(file)).sheets);
|
|
105
|
+
} finally {
|
|
106
|
+
await file.close();
|
|
107
|
+
}
|
|
97
108
|
```
|
|
98
109
|
|
|
99
110
|
### Writing an Excel File
|
|
@@ -243,7 +254,7 @@ writer.addSheet('Report', rows, {
|
|
|
243
254
|
protection: { password: 'secret', sort: true }, // Excel's legacy hash: deters edits, is not encryption
|
|
244
255
|
pageSetup: { orientation: 'landscape', paperSize: 9, fitToWidth: 1, fitToHeight: 0, printArea: 'A1:C100', printTitleRows: '1', footer: '&CPage &P of &N' },
|
|
245
256
|
tabColor: 'FF00B050',
|
|
246
|
-
mergeCells: ['
|
|
257
|
+
mergeCells: ['E1:G1'], // not inside a table: Excel tables cannot hold merged cells
|
|
247
258
|
columnWidths: [30, 12], // in characters; `columns[i].width` wins where both are set
|
|
248
259
|
autoFitColumns: true, // widths from the longest value (array rows only)
|
|
249
260
|
});
|
|
@@ -253,11 +264,11 @@ writer.addSheet('Report', rows, {
|
|
|
253
264
|
|
|
254
265
|
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`, `&`, comparisons and arithmetic, including over other formula cells), so other readers see a value. Errors are stored as error values (`#DIV/0!`). Rows from an AsyncIterable get no cached results, since the writer cannot look back at them.
|
|
255
266
|
|
|
256
|
-
Strings are written inline, which keeps memory flat. `new SheetWriter({ sharedStrings: true })` stores each distinct string once instead. Files are smaller when
|
|
267
|
+
Strings are written inline, which keeps memory flat. `new SheetWriter({ sharedStrings: true })` stores each distinct string once instead. Files are smaller when many different strings repeat (with only a handful of distinct values, inline strings compress as well or better), but the distinct strings stay in memory until the file is finished.
|
|
257
268
|
|
|
258
269
|
## Compared with SheetJS and ExcelJS
|
|
259
270
|
|
|
260
|
-
Checked against each project's own documentation on
|
|
271
|
+
Checked against each project's own documentation, and ExcelJS 4.4.0's published code, on 9 October 2026. "Pro" means a paid add-on.
|
|
261
272
|
|
|
262
273
|
| | XlsxFlow | SheetJS Community Edition | ExcelJS 4.4 |
|
|
263
274
|
|---|---|---|---|
|
|
@@ -268,7 +279,7 @@ Checked against each project's own documentation on 8 October 2026. "Pro" means
|
|
|
268
279
|
| `.ods` | Read and write | Read and write | No |
|
|
269
280
|
| `.xlsb`, `.numbers` and other formats | No | Yes | No |
|
|
270
281
|
| Charts | Add (Pro) | No (SheetJS Pro) | No |
|
|
271
|
-
| Pivot tables | Add (Pro) | No (SheetJS Pro) |
|
|
282
|
+
| Pivot tables | Add (Pro) | No (SheetJS Pro) | No |
|
|
272
283
|
| Password-protected files | Open and save (Pro) | Old `.xls` obfuscation only (SheetJS Pro opens AES files) | No |
|
|
273
284
|
| Licence | MIT, Pro is paid | Apache 2.0 | MIT |
|
|
274
285
|
|
|
@@ -276,16 +287,26 @@ SheetJS reads and writes far more formats, and ExcelJS has a longer track record
|
|
|
276
287
|
October 2023). XlsxFlow focuses on `.xlsx`: streaming in flat memory, keeping everything in a file it edits,
|
|
277
288
|
and running on Web APIs alone.
|
|
278
289
|
|
|
290
|
+
## Testing
|
|
291
|
+
|
|
292
|
+
Every push and pull request runs, [in public CI](https://github.com/xlsxflow/xlsxflow/actions/workflows/ci.yml):
|
|
293
|
+
|
|
294
|
+
- the test suite (nearly 400 tests, including fuzzing with malformed ZIP and XML input and files saved by Excel, LibreOffice, SheetJS, ExcelJS, openpyxl and xlsx-populate) on Node 20, 22 and 24, with 96% line coverage;
|
|
295
|
+
- the README's claims, checked in Node, Bun, Deno, Chromium, Firefox, WebKit and Cloudflare Workers;
|
|
296
|
+
- written and edited workbooks validated against the Office Open XML schema with Microsoft's [Open XML SDK](https://github.com/dotnet/Open-XML-SDK), then opened and recalculated in LibreOffice.
|
|
297
|
+
|
|
298
|
+
[OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/xlsxflow/xlsxflow) and CodeQL check the repository, and npm releases are published from CI with [provenance](https://docs.npmjs.com/generating-provenance-statements).
|
|
299
|
+
|
|
279
300
|
## Benchmarks
|
|
280
301
|
|
|
281
|
-
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.
|
|
302
|
+
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. XlsxFlow and ExcelJS times are medians of five alternating runs (XlsxFlow 1.1.6 was faster in all five at both sizes); the other libraries' times are from one run. The laptop had other apps open and single runs 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.
|
|
282
303
|
|
|
283
304
|
1M cells:
|
|
284
305
|
|
|
285
306
|
| Library | Write Time | File Size | Heap |
|
|
286
307
|
|---|---|---|---|
|
|
287
|
-
| **XlsxFlow** | **
|
|
288
|
-
| ExcelJS 4.4 (streaming writer) |
|
|
308
|
+
| **XlsxFlow** | **1,723 ms** | **2.9 MB** | **+2 MB** |
|
|
309
|
+
| ExcelJS 4.4 (streaming writer) | 2,290 ms | 3.0 MB | +9 MB |
|
|
289
310
|
| SheetJS 0.20.3, `compression: true` | 4,370 ms | 8.4 MB | +140 MB |
|
|
290
311
|
| SheetJS 0.20.3, default options | 5,379 ms | 31.4 MB | +140 MB |
|
|
291
312
|
| write-excel-file | 7,681 ms | 2.8 MB | +2 MB |
|
|
@@ -297,8 +318,8 @@ Write benchmark: 10 numeric columns, at 100,000 rows (1M cells) and 1,000,000 ro
|
|
|
297
318
|
|
|
298
319
|
| Library | Write Time | File Size | Heap |
|
|
299
320
|
|---|---|---|---|
|
|
300
|
-
| **XlsxFlow** | **19.
|
|
301
|
-
| ExcelJS 4.4 (streaming writer) |
|
|
321
|
+
| **XlsxFlow** | **19.1 s** | **29.9 MB** | **+2 MB** |
|
|
322
|
+
| ExcelJS 4.4 (streaming writer) | 20.5 s | 31.2 MB | +7 MB |
|
|
302
323
|
| write-excel-file | 68.1 s | 29.3 MB | +1 MB |
|
|
303
324
|
| xlsx-populate | 75.3 s | 30.0 MB | +1,118 MB |
|
|
304
325
|
| SheetJS 0.20.3 (with and without compression) | not finished after 30 min | | |
|
package/dist/index.cjs
CHANGED
|
@@ -58,21 +58,41 @@ class ZipStreamWriter {
|
|
|
58
58
|
// Stream data, tracking sizes and CRC32
|
|
59
59
|
let uncompressedSize = 0;
|
|
60
60
|
let crc = 0xffffffff;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
const compressor = new CompressionStream('deflate-raw');
|
|
62
|
+
const writer = compressor.writable.getWriter();
|
|
63
|
+
const reader = compressor.readable.getReader();
|
|
64
|
+
const input = inputStream.getReader();
|
|
65
|
+
// Node's and Bun's CompressionStream accept thousands of writes without backpressure, so only a few
|
|
66
|
+
// chunks are fed ahead: a write resolves once the chunk is compressed, which waits while nobody reads
|
|
67
|
+
// the output. Rows are then only pulled as fast as the ZIP is consumed.
|
|
68
|
+
// A few writes in flight keep the compressor busy while the next chunk is built
|
|
69
|
+
const inFlight = [];
|
|
70
|
+
const feed = (async () => {
|
|
71
|
+
try {
|
|
72
|
+
while (true) {
|
|
73
|
+
const { done, value } = await input.read();
|
|
74
|
+
if (done)
|
|
75
|
+
break;
|
|
76
|
+
await this.roomInQueue();
|
|
77
|
+
uncompressedSize += value.length;
|
|
78
|
+
crc = crc32Update(crc, value);
|
|
79
|
+
const written = writer.write(value);
|
|
80
|
+
written.catch(() => { }); // after a failure, writes nobody awaits any more must not go unhandled
|
|
81
|
+
inFlight.push(written);
|
|
82
|
+
if (inFlight.length >= 4)
|
|
83
|
+
await inFlight.shift();
|
|
84
|
+
}
|
|
85
|
+
await Promise.all(inFlight);
|
|
86
|
+
await writer.close();
|
|
69
87
|
}
|
|
70
|
-
|
|
88
|
+
catch (err) {
|
|
89
|
+
// Stop the source too (a row generator's finally runs, a database cursor closes)
|
|
90
|
+
await input.cancel(err).catch(() => { });
|
|
91
|
+
await writer.abort(err).catch(() => { });
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
71
95
|
let compressedSize = 0;
|
|
72
|
-
const reader = inputStream
|
|
73
|
-
.pipeThrough(crcStream)
|
|
74
|
-
.pipeThrough(new CompressionStream('deflate-raw'))
|
|
75
|
-
.getReader();
|
|
76
96
|
try {
|
|
77
97
|
while (true) {
|
|
78
98
|
const { done, value } = await reader.read();
|
|
@@ -81,10 +101,12 @@ class ZipStreamWriter {
|
|
|
81
101
|
compressedSize += value.length;
|
|
82
102
|
await this.pushChunk(value);
|
|
83
103
|
}
|
|
104
|
+
await feed;
|
|
84
105
|
}
|
|
85
106
|
catch (err) {
|
|
86
|
-
// Stop the source too (a row generator's finally runs, a database cursor closes)
|
|
87
107
|
await reader.cancel(err).catch(() => { });
|
|
108
|
+
await input.cancel(err).catch(() => { });
|
|
109
|
+
await feed.catch(() => { });
|
|
88
110
|
throw err;
|
|
89
111
|
}
|
|
90
112
|
crc = (crc ^ 0xffffffff) >>> 0;
|
|
@@ -3858,6 +3880,10 @@ class SheetWriter {
|
|
|
3858
3880
|
const box = parseRange$1(table.ref, 'table range');
|
|
3859
3881
|
if (boxes.some(b => overlaps(b, box)))
|
|
3860
3882
|
throw new Error(`Table "${table.name}" overlaps another table on sheet "${sheet.name}".`);
|
|
3883
|
+
// Excel tables cannot hold merged cells: it repairs the file by removing the table
|
|
3884
|
+
const merge = sheet.options.mergeCells?.find(ref => overlaps(parseRange$1(ref, 'merge range'), box));
|
|
3885
|
+
if (merge)
|
|
3886
|
+
throw new Error(`Merge "${merge}" overlaps table "${table.name}" on sheet "${sheet.name}"; Excel tables cannot contain merged cells.`);
|
|
3861
3887
|
boxes.push(box);
|
|
3862
3888
|
const first = colIndex(m[1]);
|
|
3863
3889
|
const width = colIndex(m[3]) - first + 1;
|
|
@@ -4359,8 +4385,18 @@ function createBlobReader(blob) {
|
|
|
4359
4385
|
return new Uint8Array(await slice.arrayBuffer());
|
|
4360
4386
|
},
|
|
4361
4387
|
stream(offset, length) {
|
|
4362
|
-
|
|
4363
|
-
|
|
4388
|
+
// Bun's Blob.slice(start, end).stream() runs on to the end of the original blob, so stop at length
|
|
4389
|
+
let left = length;
|
|
4390
|
+
return blob.slice(offset, offset + length).stream().pipeThrough(new TransformStream({
|
|
4391
|
+
start(controller) { if (left <= 0)
|
|
4392
|
+
controller.terminate(); },
|
|
4393
|
+
transform(chunk, controller) {
|
|
4394
|
+
controller.enqueue(chunk.length > left ? chunk.subarray(0, left) : chunk);
|
|
4395
|
+
left -= chunk.length;
|
|
4396
|
+
if (left <= 0)
|
|
4397
|
+
controller.terminate();
|
|
4398
|
+
}
|
|
4399
|
+
}));
|
|
4364
4400
|
},
|
|
4365
4401
|
async close() { }
|
|
4366
4402
|
};
|