@xlsxflow/core 1.1.5 → 1.1.7
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 +20 -0
- package/README.md +27 -7
- package/dist/index.cjs +21 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +21 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.7
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Migration guides from [ExcelJS](https://github.com/xlsxflow/xlsxflow/blob/main/docs/migrating-from-exceljs.md) and [SheetJS](https://github.com/xlsxflow/xlsxflow/blob/main/docs/migrating-from-sheetjs.md), and [recipes](https://github.com/xlsxflow/xlsxflow/blob/main/examples/README.md) for streaming downloads, uploads, database exports and browser downloads. CI runs the recipes on every push.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- On a runtime without `CompressionStream` or `DecompressionStream` for `deflate-raw` (Chrome before 103, Firefox before 113, Safari before 16.4), reading and writing throw an error naming the minimum versions instead of a bare `ReferenceError` or `TypeError`.
|
|
12
|
+
|
|
13
|
+
## 1.1.6
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `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.
|
|
18
|
+
|
|
19
|
+
### Testing
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
3
23
|
## 1.1.5
|
|
4
24
|
|
|
5
25
|
Fixes found while testing every claim in the README on Node 20.12 and 25, Bun, Deno, Chrome and Cloudflare Workers.
|
package/README.md
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
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)
|
|
12
|
+
[](https://www.bestpractices.dev/projects/15329)
|
|
10
13
|
|
|
11
14
|
<p>
|
|
12
15
|
<a href="#features">Features</a> •
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
<a href="#quick-start">Quick Start</a> •
|
|
15
18
|
<a href="#styles-formulas--conditional-formats">Styles & Formulas</a> •
|
|
16
19
|
<a href="#compared-with-sheetjs-and-exceljs">Comparison</a> •
|
|
20
|
+
<a href="#recipes-and-migration-guides">Recipes</a> •
|
|
17
21
|
<a href="#benchmarks">Benchmarks</a> •
|
|
18
22
|
<a href="#free-and-pro">Free and Pro</a>
|
|
19
23
|
</p>
|
|
@@ -28,8 +32,8 @@ Rows are read and written one at a time instead of loading the whole workbook, s
|
|
|
28
32
|
## Features
|
|
29
33
|
|
|
30
34
|
- **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 and later, Bun, Deno, Chrome and Cloudflare Workers (without `nodejs_compat`).
|
|
35
|
+
- **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)).
|
|
36
|
+
- **Runs anywhere with Web APIs**: tested on Node 20.12 and later, Bun, Deno, Chrome, Firefox, Safari (WebKit) and Cloudflare Workers (without `nodejs_compat`). Browsers need `CompressionStream` with `deflate-raw`: Chrome 103+, Firefox 113+, Safari 16.4+; older ones get an error saying so.
|
|
33
37
|
- **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
38
|
- **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
39
|
- **Older and open formats**: the same reader opens Excel 97-2003 `.xls` files and OpenDocument `.ods` files, and `OdsWriter` writes `.ods`.
|
|
@@ -285,16 +289,32 @@ SheetJS reads and writes far more formats, and ExcelJS has a longer track record
|
|
|
285
289
|
October 2023). XlsxFlow focuses on `.xlsx`: streaming in flat memory, keeping everything in a file it edits,
|
|
286
290
|
and running on Web APIs alone.
|
|
287
291
|
|
|
292
|
+
## Recipes and migration guides
|
|
293
|
+
|
|
294
|
+
- [Recipes](https://github.com/xlsxflow/xlsxflow/blob/main/examples/README.md): streaming downloads from Next.js, Cloudflare Workers, Bun, Deno and Express; reading uploads; exporting a database table; an "Export to Excel" button in the browser. Each runs in CI.
|
|
295
|
+
- [Migrating from ExcelJS](https://github.com/xlsxflow/xlsxflow/blob/main/docs/migrating-from-exceljs.md) and [Migrating from SheetJS](https://github.com/xlsxflow/xlsxflow/blob/main/docs/migrating-from-sheetjs.md): each common task in both libraries, side by side, and what works differently.
|
|
296
|
+
|
|
297
|
+
## Testing
|
|
298
|
+
|
|
299
|
+
Every push and pull request runs, [in public CI](https://github.com/xlsxflow/xlsxflow/actions/workflows/ci.yml):
|
|
300
|
+
|
|
301
|
+
- the test suite (nearly 400 tests, including fuzz and property-based tests with [fast-check](https://fast-check.dev) on 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;
|
|
302
|
+
- the README's claims, checked in Node, Bun, Deno, Chromium, Firefox, WebKit and Cloudflare Workers, and the [recipes](https://github.com/xlsxflow/xlsxflow/blob/main/examples/README.md);
|
|
303
|
+
- 10M cells written to disk and read back with Node's heap capped at 32 MB, smaller than the 34 MB file;
|
|
304
|
+
- 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.
|
|
305
|
+
|
|
306
|
+
The project holds the [OpenSSF Best Practices](https://www.bestpractices.dev/projects/15329) passing badge; [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).
|
|
307
|
+
|
|
288
308
|
## Benchmarks
|
|
289
309
|
|
|
290
|
-
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.
|
|
310
|
+
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.
|
|
291
311
|
|
|
292
312
|
1M cells:
|
|
293
313
|
|
|
294
314
|
| Library | Write Time | File Size | Heap |
|
|
295
315
|
|---|---|---|---|
|
|
296
|
-
| **XlsxFlow** | **
|
|
297
|
-
| ExcelJS 4.4 (streaming writer) |
|
|
316
|
+
| **XlsxFlow** | **1,723 ms** | **2.9 MB** | **+2 MB** |
|
|
317
|
+
| ExcelJS 4.4 (streaming writer) | 2,290 ms | 3.0 MB | +9 MB |
|
|
298
318
|
| SheetJS 0.20.3, `compression: true` | 4,370 ms | 8.4 MB | +140 MB |
|
|
299
319
|
| SheetJS 0.20.3, default options | 5,379 ms | 31.4 MB | +140 MB |
|
|
300
320
|
| write-excel-file | 7,681 ms | 2.8 MB | +2 MB |
|
|
@@ -306,8 +326,8 @@ Write benchmark: 10 numeric columns, at 100,000 rows (1M cells) and 1,000,000 ro
|
|
|
306
326
|
|
|
307
327
|
| Library | Write Time | File Size | Heap |
|
|
308
328
|
|---|---|---|---|
|
|
309
|
-
| **XlsxFlow** | **19.
|
|
310
|
-
| ExcelJS 4.4 (streaming writer) |
|
|
329
|
+
| **XlsxFlow** | **19.1 s** | **29.9 MB** | **+2 MB** |
|
|
330
|
+
| ExcelJS 4.4 (streaming writer) | 20.5 s | 31.2 MB | +7 MB |
|
|
311
331
|
| write-excel-file | 68.1 s | 29.3 MB | +1 MB |
|
|
312
332
|
| xlsx-populate | 75.3 s | 30.0 MB | +1,118 MB |
|
|
313
333
|
| SheetJS 0.20.3 (with and without compression) | not finished after 30 min | | |
|
package/dist/index.cjs
CHANGED
|
@@ -13,6 +13,15 @@ const CRC_TABLE = (() => {
|
|
|
13
13
|
function crc32(bytes) {
|
|
14
14
|
return (crc32Update(0xffffffff, bytes) ^ 0xffffffff) >>> 0;
|
|
15
15
|
}
|
|
16
|
+
// Older runtimes lack CompressionStream or its 'deflate-raw' format; say what is needed instead of a bare ReferenceError
|
|
17
|
+
function deflateRaw(kind) {
|
|
18
|
+
try {
|
|
19
|
+
return (kind === 'compress' ? new CompressionStream('deflate-raw') : new DecompressionStream('deflate-raw'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`XlsxFlow needs ${kind === 'compress' ? 'CompressionStream' : 'DecompressionStream'} with 'deflate-raw': Chrome 103+, Firefox 113+, Safari 16.4+, Node 20.12+, Deno, Bun or Cloudflare Workers`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
16
25
|
// Running CRC-32 over chunks: start at 0xffffffff, finish with (crc ^ 0xffffffff) >>> 0
|
|
17
26
|
function crc32Update(crc, bytes) {
|
|
18
27
|
for (let i = 0; i < bytes.length; i++)
|
|
@@ -58,13 +67,15 @@ class ZipStreamWriter {
|
|
|
58
67
|
// Stream data, tracking sizes and CRC32
|
|
59
68
|
let uncompressedSize = 0;
|
|
60
69
|
let crc = 0xffffffff;
|
|
61
|
-
const compressor =
|
|
70
|
+
const compressor = deflateRaw('compress');
|
|
62
71
|
const writer = compressor.writable.getWriter();
|
|
63
72
|
const reader = compressor.readable.getReader();
|
|
64
73
|
const input = inputStream.getReader();
|
|
65
|
-
// Node's and Bun's CompressionStream accept thousands of writes without backpressure, so
|
|
66
|
-
//
|
|
74
|
+
// Node's and Bun's CompressionStream accept thousands of writes without backpressure, so only a few
|
|
75
|
+
// chunks are fed ahead: a write resolves once the chunk is compressed, which waits while nobody reads
|
|
67
76
|
// the output. Rows are then only pulled as fast as the ZIP is consumed.
|
|
77
|
+
// A few writes in flight keep the compressor busy while the next chunk is built
|
|
78
|
+
const inFlight = [];
|
|
68
79
|
const feed = (async () => {
|
|
69
80
|
try {
|
|
70
81
|
while (true) {
|
|
@@ -74,8 +85,13 @@ class ZipStreamWriter {
|
|
|
74
85
|
await this.roomInQueue();
|
|
75
86
|
uncompressedSize += value.length;
|
|
76
87
|
crc = crc32Update(crc, value);
|
|
77
|
-
|
|
88
|
+
const written = writer.write(value);
|
|
89
|
+
written.catch(() => { }); // after a failure, writes nobody awaits any more must not go unhandled
|
|
90
|
+
inFlight.push(written);
|
|
91
|
+
if (inFlight.length >= 4)
|
|
92
|
+
await inFlight.shift();
|
|
78
93
|
}
|
|
94
|
+
await Promise.all(inFlight);
|
|
79
95
|
await writer.close();
|
|
80
96
|
}
|
|
81
97
|
catch (err) {
|
|
@@ -498,7 +514,7 @@ class ZipRandomAccessParser {
|
|
|
498
514
|
}
|
|
499
515
|
const stream = await this.extractRawStream(filename);
|
|
500
516
|
const data = record.compressionMethod === 0 ? stream
|
|
501
|
-
: stream.pipeThrough(
|
|
517
|
+
: stream.pipeThrough(deflateRaw('decompress'));
|
|
502
518
|
// Node reports bad deflate data as a bare TypeError; name the entry instead
|
|
503
519
|
const inflated = data.getReader();
|
|
504
520
|
let total = 0;
|