@xlsxflow/core 1.1.4 → 1.1.5

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.5
4
+
5
+ Fixes found while testing every claim in the README on Node 20.12 and 25, Bun, Deno, Chrome and Cloudflare Workers.
6
+
7
+ ### Fixed
8
+
9
+ - `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.
10
+ - 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.
11
+ - `SheetWriter` refuses a merge that overlaps a table. Excel tables cannot hold merged cells, and Excel repaired such files by removing the table.
12
+
13
+ ### Docs
14
+
15
+ - 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.
16
+
3
17
  ## 1.1.4
4
18
 
5
19
  Fixes from the third round of end-user tests (700 cases).
package/README.md CHANGED
@@ -29,7 +29,7 @@ Rows are read and written one at a time instead of loading the whole workbook, s
29
29
 
30
30
  - **No dependencies**: TypeScript on Web APIs (`ReadableStream`, `CompressionStream`, `Blob`).
31
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.
32
+ - **Runs anywhere with Web APIs**: tested on Node 20.12 and later, Bun, Deno, Chrome and Cloudflare Workers (without `nodejs_compat`).
33
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
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
35
  - **Older and open formats**: the same reader opens Excel 97-2003 `.xls` files and OpenDocument `.ods` files, and `OdsWriter` writes `.ods`.
@@ -89,11 +89,20 @@ Parts held in memory (workbook, shared strings, styles) are capped at 1 GiB unco
89
89
  In Node.js, read straight from disk with `XlsxFlow.readFile`, or pass `await createFileReader(path)` to any function that takes a reader:
90
90
 
91
91
  ```typescript
92
- import { XlsxFlow } from '@xlsxflow/core';
92
+ import { XlsxFlow, SheetReader, createFileReader } from '@xlsxflow/core';
93
93
 
94
94
  for await (const row of await XlsxFlow.readFile('./data.xlsx')) {
95
95
  console.log(row.cells);
96
96
  }
97
+
98
+ // createFileReader keeps the file open until you close it, after you are done with
99
+ // everything read through it (Node 25 stops the process when an open file is garbage-collected)
100
+ const file = await createFileReader('./data.xlsx');
101
+ try {
102
+ console.log((await new SheetReader().readWorkbook(file)).sheets);
103
+ } finally {
104
+ await file.close();
105
+ }
97
106
  ```
98
107
 
99
108
  ### Writing an Excel File
@@ -243,7 +252,7 @@ writer.addSheet('Report', rows, {
243
252
  protection: { password: 'secret', sort: true }, // Excel's legacy hash: deters edits, is not encryption
244
253
  pageSetup: { orientation: 'landscape', paperSize: 9, fitToWidth: 1, fitToHeight: 0, printArea: 'A1:C100', printTitleRows: '1', footer: '&CPage &P of &N' },
245
254
  tabColor: 'FF00B050',
246
- mergeCells: ['A1:C1'],
255
+ mergeCells: ['E1:G1'], // not inside a table: Excel tables cannot hold merged cells
247
256
  columnWidths: [30, 12], // in characters; `columns[i].width` wins where both are set
248
257
  autoFitColumns: true, // widths from the longest value (array rows only)
249
258
  });
@@ -253,11 +262,11 @@ writer.addSheet('Report', rows, {
253
262
 
254
263
  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
264
 
256
- 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.
265
+ 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
266
 
258
267
  ## Compared with SheetJS and ExcelJS
259
268
 
260
- Checked against each project's own documentation on 8 October 2026. "Pro" means a paid add-on.
269
+ 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
270
 
262
271
  | | XlsxFlow | SheetJS Community Edition | ExcelJS 4.4 |
263
272
  |---|---|---|---|
@@ -268,7 +277,7 @@ Checked against each project's own documentation on 8 October 2026. "Pro" means
268
277
  | `.ods` | Read and write | Read and write | No |
269
278
  | `.xlsb`, `.numbers` and other formats | No | Yes | No |
270
279
  | Charts | Add (Pro) | No (SheetJS Pro) | No |
271
- | Pivot tables | Add (Pro) | No (SheetJS Pro) | Partial, undocumented |
280
+ | Pivot tables | Add (Pro) | No (SheetJS Pro) | No |
272
281
  | Password-protected files | Open and save (Pro) | Old `.xls` obfuscation only (SheetJS Pro opens AES files) | No |
273
282
  | Licence | MIT, Pro is paid | Apache 2.0 | MIT |
274
283
 
package/dist/index.cjs CHANGED
@@ -58,21 +58,34 @@ class ZipStreamWriter {
58
58
  // Stream data, tracking sizes and CRC32
59
59
  let uncompressedSize = 0;
60
60
  let crc = 0xffffffff;
61
- const crcStream = new TransformStream({
62
- // CompressionStream takes every write at once, so the input is held back here instead,
63
- // while nobody reads the output
64
- transform: async (chunk, controller) => {
65
- await this.roomInQueue();
66
- uncompressedSize += chunk.length;
67
- crc = crc32Update(crc, chunk);
68
- controller.enqueue(chunk);
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 one chunk
66
+ // is fed at a time: 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
+ const feed = (async () => {
69
+ try {
70
+ while (true) {
71
+ const { done, value } = await input.read();
72
+ if (done)
73
+ break;
74
+ await this.roomInQueue();
75
+ uncompressedSize += value.length;
76
+ crc = crc32Update(crc, value);
77
+ await writer.write(value);
78
+ }
79
+ await writer.close();
69
80
  }
70
- });
81
+ catch (err) {
82
+ // Stop the source too (a row generator's finally runs, a database cursor closes)
83
+ await input.cancel(err).catch(() => { });
84
+ await writer.abort(err).catch(() => { });
85
+ throw err;
86
+ }
87
+ })();
71
88
  let compressedSize = 0;
72
- const reader = inputStream
73
- .pipeThrough(crcStream)
74
- .pipeThrough(new CompressionStream('deflate-raw'))
75
- .getReader();
76
89
  try {
77
90
  while (true) {
78
91
  const { done, value } = await reader.read();
@@ -81,10 +94,12 @@ class ZipStreamWriter {
81
94
  compressedSize += value.length;
82
95
  await this.pushChunk(value);
83
96
  }
97
+ await feed;
84
98
  }
85
99
  catch (err) {
86
- // Stop the source too (a row generator's finally runs, a database cursor closes)
87
100
  await reader.cancel(err).catch(() => { });
101
+ await input.cancel(err).catch(() => { });
102
+ await feed.catch(() => { });
88
103
  throw err;
89
104
  }
90
105
  crc = (crc ^ 0xffffffff) >>> 0;
@@ -3858,6 +3873,10 @@ class SheetWriter {
3858
3873
  const box = parseRange$1(table.ref, 'table range');
3859
3874
  if (boxes.some(b => overlaps(b, box)))
3860
3875
  throw new Error(`Table "${table.name}" overlaps another table on sheet "${sheet.name}".`);
3876
+ // Excel tables cannot hold merged cells: it repairs the file by removing the table
3877
+ const merge = sheet.options.mergeCells?.find(ref => overlaps(parseRange$1(ref, 'merge range'), box));
3878
+ if (merge)
3879
+ throw new Error(`Merge "${merge}" overlaps table "${table.name}" on sheet "${sheet.name}"; Excel tables cannot contain merged cells.`);
3861
3880
  boxes.push(box);
3862
3881
  const first = colIndex(m[1]);
3863
3882
  const width = colIndex(m[3]) - first + 1;
@@ -4359,8 +4378,18 @@ function createBlobReader(blob) {
4359
4378
  return new Uint8Array(await slice.arrayBuffer());
4360
4379
  },
4361
4380
  stream(offset, length) {
4362
- const slice = blob.slice(offset, offset + length);
4363
- return slice.stream();
4381
+ // Bun's Blob.slice(start, end).stream() runs on to the end of the original blob, so stop at length
4382
+ let left = length;
4383
+ return blob.slice(offset, offset + length).stream().pipeThrough(new TransformStream({
4384
+ start(controller) { if (left <= 0)
4385
+ controller.terminate(); },
4386
+ transform(chunk, controller) {
4387
+ controller.enqueue(chunk.length > left ? chunk.subarray(0, left) : chunk);
4388
+ left -= chunk.length;
4389
+ if (left <= 0)
4390
+ controller.terminate();
4391
+ }
4392
+ }));
4364
4393
  },
4365
4394
  async close() { }
4366
4395
  };