@justybase/spreadsheet-tasks 2.1.0 → 2.2.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 CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `convertXlsbToXlsx` and `convertXlsxToXlsb` — Windows/Excel COM conversions
13
+ with protected destinations and optional explicit overwriting.
14
+ - `replaceSheetDataStream()` and `saveStreaming()` for XLSX/XLSB updaters,
15
+ accepting synchronous or asynchronous row sources without collecting the
16
+ complete result set in memory.
17
+ - `XlsmUpdater` as an explicit alias for `XlsxUpdater`; VBA package members are
18
+ preserved as opaque ZIP parts.
19
+
20
+ ### Fixed
21
+
22
+ - Destination replacement during Excel conversion now preserves files created
23
+ concurrently and rolls back safely when the final rename fails.
24
+ - Shared-string `count` values now reflect the replaced worksheet rather than
25
+ accumulating stale references across repeated updates. Pending strings are
26
+ committed only once.
27
+ - Updater saves now install the destination atomically through a same-directory
28
+ temporary file.
29
+
8
30
  ## [2.1.0] - 2026-08-07
9
31
 
10
32
  ### Added
package/README.md CHANGED
@@ -31,7 +31,8 @@ High-performance TypeScript library for reading and writing Excel files in XLSB
31
31
  - **Streaming Support** — Efficient memory usage for large files
32
32
  - **Multiple Sheets** — Support for multiple worksheets per workbook
33
33
  - **Auto-filter** — Automatic filter headers support
34
- - **In-place Updates** — Replace the data of a worksheet inside an existing XLSX or XLSB (e.g. `report.xlsx` / `report.xlsb`) while keeping pivot tables, other sheets and formatting untouched
34
+ - **Excel COM Conversion** — Convert XLSB and XLSX through desktop Microsoft Excel on Windows
35
+ - **In-place Updates** — Replace the data of a worksheet inside an existing XLSX, XLSM or XLSB while keeping pivot tables, other sheets and formatting untouched
35
36
 
36
37
  ## Benchmark Results
37
38
 
@@ -46,6 +47,7 @@ Measured with `npm run benchmark` (50K rows). Your results may vary by hardware
46
47
  ## Requirements
47
48
 
48
49
  - **Node.js** 16 or newer ([`engines`](./package.json) in `package.json`)
50
+ - XLSB/XLSX conversion additionally requires Windows, PowerShell and desktop Microsoft Excel with COM automation
49
51
 
50
52
  ## Installation
51
53
 
@@ -211,7 +213,7 @@ Excel file (like a report with pivot tables wired to a data sheet) and only
211
213
  refresh its data:
212
214
 
213
215
  ```typescript
214
- import { XlsxUpdater, XlsbUpdater } from '@justybase/spreadsheet-tasks';
216
+ import { XlsxUpdater, XlsbUpdater, XlsmUpdater } from '@justybase/spreadsheet-tasks';
215
217
 
216
218
  // XLSX
217
219
  const xlsx = new XlsxUpdater('report.xlsx');
@@ -222,8 +224,26 @@ xlsx.save(); // overwrite in place, or xlsx.save('report_new.xls
222
224
  const xlsb = new XlsbUpdater('report.xlsb');
223
225
  xlsb.replaceSheetData('data1', rows, { headers: ['ID', 'NAME', 'AMOUNT'] });
224
226
  xlsb.save('report_new.xlsb');
227
+
228
+ // XLSM uses the XLSX updater and keeps opaque VBA parts intact.
229
+ const xlsm = new XlsmUpdater('report.xlsm');
230
+ xlsm.replaceSheetData('data1', rows, { headers: ['ID', 'NAME', 'AMOUNT'] });
231
+ xlsm.save('report_new.xlsm');
225
232
  ```
226
233
 
234
+ For a result set that should not be collected into a 2D array, use the additive
235
+ streaming API. It accepts either a synchronous iterable or an async iterable and
236
+ writes the final ZIP through a temporary file before installing it atomically:
237
+
238
+ ```typescript
239
+ const updater = new XlsxUpdater('large-report.xlsx');
240
+ await updater.replaceSheetDataStream('data1', databaseRows(), { headers });
241
+ await updater.saveStreaming('large-report-updated.xlsx');
242
+ ```
243
+
244
+ `save()` remains available for the existing synchronous API. Both save methods
245
+ replace their destination atomically after a successful write.
246
+
227
247
  **What happens under the hood:**
228
248
 
229
249
  - The target sheet's cell data is cleared and replaced with the new rows.
@@ -242,6 +262,30 @@ xlsb.save('report_new.xlsb');
242
262
  > headers), so the pivot's columns would be replaced by the first record's
243
263
  > values.
244
264
 
265
+ ### Converting XLSB and XLSX with Excel
266
+
267
+ On Windows, existing workbooks can be converted by asking the installed desktop
268
+ Microsoft Excel to perform `SaveAs` through COM. The source file is opened
269
+ read-only and the destination is written separately:
270
+
271
+ ```typescript
272
+ import {
273
+ convertXlsbToXlsx,
274
+ convertXlsxToXlsb,
275
+ } from '@justybase/spreadsheet-tasks';
276
+
277
+ await convertXlsbToXlsx('report.xlsb', 'report.xlsx');
278
+ await convertXlsxToXlsb('report.xlsx', 'report.xlsb');
279
+
280
+ // Existing destinations are protected by default.
281
+ await convertXlsbToXlsx('report.xlsb', 'report.xlsx', { overwrite: true });
282
+ ```
283
+
284
+ Conversion is a Node.js-only API and requires Windows plus desktop Microsoft
285
+ Excel. Unsupported files, unavailable COM automation, and Excel conversion
286
+ errors are reported as rejected promises. VBA/macros are not guaranteed when
287
+ the target format does not support them.
288
+
245
289
  ## API Documentation
246
290
 
247
291
  See [API Documentation](./docs/API.md) for detailed API reference.
@@ -367,6 +367,8 @@ interface ReplaceSheetDataOptions$1 {
367
367
  */
368
368
  styleFallback?: 'inherit' | 'general';
369
369
  }
370
+ /** A one-pass synchronous or asynchronous source of worksheet rows. */
371
+ type RowSource$1 = Iterable<ReadonlyArray<CellValue>> | AsyncIterable<ReadonlyArray<CellValue>>;
370
372
  /**
371
373
  * Update the data of a worksheet inside an existing XLSX file without rebuilding
372
374
  * the workbook. Everything outside the target sheet's cell data — pivot tables
@@ -383,6 +385,8 @@ interface ReplaceSheetDataOptions$1 {
383
385
  declare class XlsxUpdater {
384
386
  private readonly zip;
385
387
  private readonly sourcePath;
388
+ private temporaryDirectory;
389
+ private readonly stagedParts;
386
390
  private sheetNameToPath;
387
391
  private pivotCacheDefs;
388
392
  private pivotTableDefPaths;
@@ -390,12 +394,14 @@ declare class XlsxUpdater {
390
394
  private sharedStringsValues;
391
395
  private sharedStringsOriginalLength;
392
396
  private sharedStringsCount;
397
+ private sharedStringsPending;
398
+ private sharedStringsDirty;
393
399
  private stringIndexMap;
394
400
  private readonly _oaEpoch;
395
401
  /**
396
402
  * @param path Path to an existing `.xlsx` file.
397
403
  */
398
- constructor(path: string);
404
+ constructor(filePath: string);
399
405
  /** Worksheet names in workbook order. */
400
406
  getSheetNames(): string[];
401
407
  /**
@@ -407,13 +413,36 @@ declare class XlsxUpdater {
407
413
  * @param options See {@link ReplaceSheetDataOptions}.
408
414
  */
409
415
  replaceSheetData(sheetName: string, rows: CellValue[][], options?: ReplaceSheetDataOptions$1): void;
416
+ /**
417
+ * Replace worksheet rows from a one-pass source without materialising the
418
+ * complete result set. Use {@link saveStreaming} to keep the final ZIP
419
+ * write streaming as well.
420
+ */
421
+ replaceSheetDataStream(sheetName: string, rows: RowSource$1, options?: ReplaceSheetDataOptions$1): Promise<void>;
410
422
  /**
411
423
  * Write the updated workbook to disk.
412
424
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
413
425
  */
414
426
  save(outputPath?: string): void;
427
+ /** Save staged streaming replacements without materialising the ZIP. */
428
+ saveStreaming(outputPath?: string): Promise<void>;
415
429
  /** The updated workbook as an in-memory ZIP buffer. */
416
430
  toBuffer(): Buffer;
431
+ /**
432
+ * Release the staging directory and any unsaved staged parts.
433
+ * The updater remains usable; a new staging directory is created lazily
434
+ * on the next streaming replacement.
435
+ */
436
+ dispose(): void;
437
+ /** Support `using` declarations for automatic staging cleanup. */
438
+ [Symbol.dispose](): void;
439
+ private _ensureTemporaryDirectory;
440
+ private _temporaryPath;
441
+ private _removeTemporaryFile;
442
+ private _entryData;
443
+ private _stagePart;
444
+ private _discardStagedPart;
445
+ private _materializeStagedParts;
417
446
  private _loadWorkbookStructure;
418
447
  private _ensureSharedStringsLoaded;
419
448
  private _commitSharedStrings;
@@ -422,7 +451,10 @@ declare class XlsxUpdater {
422
451
  private _buildSheetData;
423
452
  private _valueCell;
424
453
  private _stringCell;
454
+ private static _replaceOrAddXmlAttribute;
455
+ private _sharedStringRefCount;
425
456
  private _patchSheetXml;
457
+ private _patchSheetFile;
426
458
  private _updatePivotCaches;
427
459
  private _addRefreshOnLoadToPivotTables;
428
460
  }
@@ -445,6 +477,8 @@ interface ReplaceSheetDataOptions {
445
477
  */
446
478
  styleFallback?: 'inherit' | 'general';
447
479
  }
480
+ /** A one-pass synchronous or asynchronous source of worksheet rows. */
481
+ type RowSource = Iterable<ReadonlyArray<CellValue>> | AsyncIterable<ReadonlyArray<CellValue>>;
448
482
  /**
449
483
  * Update the data of a worksheet inside an existing XLSB file without rebuilding
450
484
  * the workbook. Everything outside the target sheet's rows — pivot tables and
@@ -460,6 +494,8 @@ interface ReplaceSheetDataOptions {
460
494
  declare class XlsbUpdater {
461
495
  private readonly zip;
462
496
  private readonly sourcePath;
497
+ private temporaryDirectory;
498
+ private readonly stagedParts;
463
499
  private sheetNameToPath;
464
500
  private pivotCacheDefPaths;
465
501
  private sharedStringsBuf;
@@ -468,11 +504,12 @@ declare class XlsbUpdater {
468
504
  private sharedStringsUnique;
469
505
  private sharedStringsEndSst;
470
506
  private stringIndexMap;
507
+ private sharedStringsDirty;
471
508
  private readonly _oaEpoch;
472
509
  /**
473
510
  * @param path Path to an existing `.xlsb` file.
474
511
  */
475
- constructor(path: string);
512
+ constructor(filePath: string);
476
513
  /** Worksheet names in workbook order. */
477
514
  getSheetNames(): string[];
478
515
  /**
@@ -484,13 +521,32 @@ declare class XlsbUpdater {
484
521
  * @param options See {@link ReplaceSheetDataOptions}.
485
522
  */
486
523
  replaceSheetData(sheetName: string, rows: CellValue[][], options?: ReplaceSheetDataOptions): void;
524
+ /** Replace worksheet rows from a one-pass source. */
525
+ replaceSheetDataStream(sheetName: string, rows: RowSource, options?: ReplaceSheetDataOptions): Promise<void>;
487
526
  /**
488
527
  * Write the updated workbook to disk.
489
528
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
490
529
  */
491
530
  save(outputPath?: string): void;
531
+ /** Save staged streaming replacements without materialising the ZIP. */
532
+ saveStreaming(outputPath?: string): Promise<void>;
492
533
  /** The updated workbook as an in-memory ZIP buffer. */
493
534
  toBuffer(): Buffer;
535
+ /**
536
+ * Release the staging directory and any unsaved staged parts.
537
+ * The updater remains usable; a new staging directory is created lazily
538
+ * on the next streaming replacement.
539
+ */
540
+ dispose(): void;
541
+ /** Support `using` declarations for automatic staging cleanup. */
542
+ [Symbol.dispose](): void;
543
+ private _ensureTemporaryDirectory;
544
+ private _temporaryPath;
545
+ private _removeTemporaryFile;
546
+ private _entryData;
547
+ private _stagePart;
548
+ private _discardStagedPart;
549
+ private _materializeStagedParts;
494
550
  private _loadWorkbookStructure;
495
551
  private _appendCount;
496
552
  private _ensureSharedStringsLoaded;
@@ -500,6 +556,8 @@ declare class XlsbUpdater {
500
556
  private _buildRows;
501
557
  private _valueCellBytes;
502
558
  private _stringCellBytes;
559
+ private _sharedStringRefCount;
560
+ private _patchSheetMetadataFile;
503
561
  private _findRowsRegion;
504
562
  /**
505
563
  * Keep the sheet dimension record (0x98) in sync with the new data.
@@ -511,6 +569,16 @@ declare class XlsbUpdater {
511
569
  private _patchPivotCaches;
512
570
  }
513
571
 
572
+ /** Options for an Excel format conversion. */
573
+ interface ExcelConversionOptions {
574
+ /** Allow replacing an existing destination file. Defaults to false. */
575
+ overwrite?: boolean;
576
+ }
577
+ /** Convert an Excel Binary Workbook (.xlsb) to an Open XML Workbook (.xlsx). */
578
+ declare function convertXlsbToXlsx(inputPath: string, outputPath: string, options?: ExcelConversionOptions): Promise<void>;
579
+ /** Convert an Open XML Workbook (.xlsx) to an Excel Binary Workbook (.xlsb). */
580
+ declare function convertXlsxToXlsb(inputPath: string, outputPath: string, options?: ExcelConversionOptions): Promise<void>;
581
+
514
582
  /** Excel sheet name characters that must be replaced. */
515
583
  declare const INVALID_SHEET_NAME_CHARS: RegExp;
516
584
  /**
@@ -556,4 +624,4 @@ declare class StreamingSheetState {
556
624
  end(): void;
557
625
  }
558
626
 
559
- export { BiffReaderWriter, BigBuffer, type CellValue, ExcelReaderAbstract, F, type FormattedCell, INVALID_SHEET_NAME_CHARS, type PrimitiveCellValue, ReaderFactory, type ReplaceSheetDataOptions$1 as ReplaceSheetDataOptions, StreamingSheetState, XlsbReader, type ReplaceSheetDataOptions as XlsbReplaceSheetDataOptions, XlsbUpdater, XlsbWriter, XlsxReader, XlsxUpdater, XlsxWriter, applyHeaderWidths, defaultColWidth, getFormat, initColWidths, isFormattedCell, sanitizeSheetName, unwrapCell, updateColWidthsFromRows };
627
+ export { BiffReaderWriter, BigBuffer, type CellValue, type ExcelConversionOptions, ExcelReaderAbstract, F, type FormattedCell, INVALID_SHEET_NAME_CHARS, type PrimitiveCellValue, ReaderFactory, type ReplaceSheetDataOptions$1 as ReplaceSheetDataOptions, type RowSource$1 as RowSource, StreamingSheetState, XlsbReader, type ReplaceSheetDataOptions as XlsbReplaceSheetDataOptions, XlsbUpdater, XlsbWriter, XlsxUpdater as XlsmUpdater, XlsxReader, XlsxUpdater, XlsxWriter, applyHeaderWidths, convertXlsbToXlsx, convertXlsxToXlsb, defaultColWidth, getFormat, initColWidths, isFormattedCell, sanitizeSheetName, unwrapCell, updateColWidthsFromRows };