@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 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
  [![npm version](https://img.shields.io/npm/v/@xlsxflow/core.svg?style=flat-square)](https://www.npmjs.com/package/@xlsxflow/core)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
10
+ [![CI](https://img.shields.io/github/actions/workflow/status/xlsxflow/xlsxflow/ci.yml?branch=main&label=CI&style=flat-square)](https://github.com/xlsxflow/xlsxflow/actions/workflows/ci.yml)
11
+ [![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/xlsxflow/xlsxflow?label=OpenSSF%20Scorecard&style=flat-square)](https://scorecard.dev/viewer/?uri=github.com/xlsxflow/xlsxflow)
12
+ [![OpenSSF Best Practices](https://img.shields.io/cii/level/15329?label=OpenSSF%20Best%20Practices&style=flat-square)](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 &amp; 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 1 MB of extra heap; see [Benchmarks](#benchmarks)).
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. 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.
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** | **2,899 ms** | **2.9 MB** | **+2 MB** |
297
- | ExcelJS 4.4 (streaming writer) | 3,397 ms | 3.0 MB | +9 MB |
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.5 s** | **29.9 MB** | **+1 MB** |
310
- | ExcelJS 4.4 (streaming writer) | 22.0 s | 31.2 MB | +7 MB |
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 = new CompressionStream('deflate-raw');
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 one chunk
66
- // is fed at a time: a write resolves once the chunk is compressed, which waits while nobody reads
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
- await writer.write(value);
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(new DecompressionStream('deflate-raw'));
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;