@justybase/spreadsheet-tasks 2.1.0 → 2.1.1

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,18 @@ 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
+
15
+ ### Fixed
16
+
17
+ - Destination replacement during Excel conversion now preserves files created
18
+ concurrently and rolls back safely when the final rename fails.
19
+
8
20
  ## [2.1.0] - 2026-08-07
9
21
 
10
22
  ### Added
package/README.md CHANGED
@@ -31,6 +31,7 @@ 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
+ - **Excel COM Conversion** — Convert XLSB and XLSX through desktop Microsoft Excel on Windows
34
35
  - **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
35
36
 
36
37
  ## Benchmark Results
@@ -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
 
@@ -242,6 +244,30 @@ xlsb.save('report_new.xlsb');
242
244
  > headers), so the pivot's columns would be replaced by the first record's
243
245
  > values.
244
246
 
247
+ ### Converting XLSB and XLSX with Excel
248
+
249
+ On Windows, existing workbooks can be converted by asking the installed desktop
250
+ Microsoft Excel to perform `SaveAs` through COM. The source file is opened
251
+ read-only and the destination is written separately:
252
+
253
+ ```typescript
254
+ import {
255
+ convertXlsbToXlsx,
256
+ convertXlsxToXlsb,
257
+ } from '@justybase/spreadsheet-tasks';
258
+
259
+ await convertXlsbToXlsx('report.xlsb', 'report.xlsx');
260
+ await convertXlsxToXlsb('report.xlsx', 'report.xlsb');
261
+
262
+ // Existing destinations are protected by default.
263
+ await convertXlsbToXlsx('report.xlsb', 'report.xlsx', { overwrite: true });
264
+ ```
265
+
266
+ Conversion is a Node.js-only API and requires Windows plus desktop Microsoft
267
+ Excel. Unsupported files, unavailable COM automation, and Excel conversion
268
+ errors are reported as rejected promises. VBA/macros are not guaranteed when
269
+ the target format does not support them.
270
+
245
271
  ## API Documentation
246
272
 
247
273
  See [API Documentation](./docs/API.md) for detailed API reference.
@@ -511,6 +511,16 @@ declare class XlsbUpdater {
511
511
  private _patchPivotCaches;
512
512
  }
513
513
 
514
+ /** Options for an Excel format conversion. */
515
+ interface ExcelConversionOptions {
516
+ /** Allow replacing an existing destination file. Defaults to false. */
517
+ overwrite?: boolean;
518
+ }
519
+ /** Convert an Excel Binary Workbook (.xlsb) to an Open XML Workbook (.xlsx). */
520
+ declare function convertXlsbToXlsx(inputPath: string, outputPath: string, options?: ExcelConversionOptions): Promise<void>;
521
+ /** Convert an Open XML Workbook (.xlsx) to an Excel Binary Workbook (.xlsb). */
522
+ declare function convertXlsxToXlsb(inputPath: string, outputPath: string, options?: ExcelConversionOptions): Promise<void>;
523
+
514
524
  /** Excel sheet name characters that must be replaced. */
515
525
  declare const INVALID_SHEET_NAME_CHARS: RegExp;
516
526
  /**
@@ -556,4 +566,4 @@ declare class StreamingSheetState {
556
566
  end(): void;
557
567
  }
558
568
 
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 };
569
+ export { BiffReaderWriter, BigBuffer, type CellValue, type ExcelConversionOptions, 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, convertXlsbToXlsx, convertXlsxToXlsb, defaultColWidth, getFormat, initColWidths, isFormattedCell, sanitizeSheetName, unwrapCell, updateColWidthsFromRows };
package/dist/cjs/index.js CHANGED
@@ -44,6 +44,8 @@ __export(index_exports, {
44
44
  XlsxUpdater: () => XlsxUpdater,
45
45
  XlsxWriter: () => XlsxWriter,
46
46
  applyHeaderWidths: () => applyHeaderWidths,
47
+ convertXlsbToXlsx: () => convertXlsbToXlsx,
48
+ convertXlsxToXlsb: () => convertXlsxToXlsb,
47
49
  defaultColWidth: () => defaultColWidth,
48
50
  getFormat: () => getFormat,
49
51
  initColWidths: () => initColWidths,
@@ -458,8 +460,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
458
460
  this.xfIdToNumFmtId = [];
459
461
  this.customDateFormats = null;
460
462
  }
461
- async open(path2, readSharedStrings = true) {
462
- this.zip = new import_adm_zip.default(path2);
463
+ async open(path3, readSharedStrings = true) {
464
+ this.zip = new import_adm_zip.default(path3);
463
465
  const wbEntry = this.zip.getEntry("xl/workbook.bin");
464
466
  if (wbEntry) {
465
467
  const buf = wbEntry.getData();
@@ -504,8 +506,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
504
506
  this.resultsCount = this.sheets.length;
505
507
  this._currentSheetIndex = -1;
506
508
  }
507
- _readSharedStrings(path2) {
508
- const entry = this.zip.getEntry(path2);
509
+ _readSharedStrings(path3) {
510
+ const entry = this.zip.getEntry(path3);
509
511
  if (!entry) return;
510
512
  const reader = new BiffReaderWriter(entry.getData());
511
513
  while (reader.readSharedStrings()) {
@@ -514,8 +516,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
514
516
  }
515
517
  }
516
518
  }
517
- _readStyles(path2) {
518
- const entry = this.zip.getEntry(path2);
519
+ _readStyles(path3) {
520
+ const entry = this.zip.getEntry(path3);
519
521
  if (!entry) return;
520
522
  const reader = new BiffReaderWriter(entry.getData());
521
523
  while (reader.readStyles()) {
@@ -707,9 +709,9 @@ var XlsxReader = class extends ExcelReaderAbstract {
707
709
  this.xfIdToNumFmtId = [];
708
710
  this.customDateFormats = /* @__PURE__ */ new Set();
709
711
  }
710
- async open(path2, readSharedStrings = true) {
711
- return new Promise((resolve, reject) => {
712
- import_yauzl.default.open(path2, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
712
+ async open(path3, readSharedStrings = true) {
713
+ return new Promise((resolve2, reject) => {
714
+ import_yauzl.default.open(path3, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
713
715
  if (err) return reject(err);
714
716
  this.zipfile = zipfile;
715
717
  this.entries = /* @__PURE__ */ new Map();
@@ -757,7 +759,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
757
759
  }
758
760
  this.resultsCount = this.sheets.length;
759
761
  this._currentSheetIndex = -1;
760
- resolve();
762
+ resolve2();
761
763
  } catch (e) {
762
764
  reject(e);
763
765
  }
@@ -771,16 +773,16 @@ var XlsxReader = class extends ExcelReaderAbstract {
771
773
  this.zipfile.close();
772
774
  }
773
775
  }
774
- async _readZipEntryContent(path2) {
775
- const entry = this.entries.get(path2);
776
+ async _readZipEntryContent(path3) {
777
+ const entry = this.entries.get(path3);
776
778
  if (!entry) return null;
777
- return new Promise((resolve, reject) => {
779
+ return new Promise((resolve2, reject) => {
778
780
  this.zipfile.openReadStream(entry, (err, readStream) => {
779
781
  if (err) return reject(err);
780
782
  const chunks = [];
781
783
  readStream.on("data", (chunk) => chunks.push(chunk));
782
784
  readStream.on("end", () => {
783
- resolve(Buffer.concat(chunks).toString("utf8"));
785
+ resolve2(Buffer.concat(chunks).toString("utf8"));
784
786
  });
785
787
  readStream.on("error", reject);
786
788
  });
@@ -3386,7 +3388,7 @@ var XlsbWriter = class _XlsbWriter {
3386
3388
  * Must be awaited before the file is fully written.
3387
3389
  */
3388
3390
  finalize() {
3389
- return new Promise((resolve, reject) => {
3391
+ return new Promise((resolve2, reject) => {
3390
3392
  try {
3391
3393
  this.saveSst();
3392
3394
  this.archive.append(this._buildStylesBin(), { name: "xl/styles.bin" });
@@ -3484,7 +3486,7 @@ var XlsbWriter = class _XlsbWriter {
3484
3486
  </Relationships>`;
3485
3487
  this.archive.append(wsRels, { name: `xl/worksheets/_rels/${sheet.nameInArchive}.rels` });
3486
3488
  }
3487
- this.output.on("close", () => resolve());
3489
+ this.output.on("close", () => resolve2());
3488
3490
  this.archive.on("error", (err) => reject(err));
3489
3491
  this.archive.finalize();
3490
3492
  } catch (err) {
@@ -3832,7 +3834,7 @@ var XlsxWriter = class {
3832
3834
  }
3833
3835
  /** Finalize the ZIP package and close the output stream. */
3834
3836
  finalize() {
3835
- return new Promise((resolve, reject) => {
3837
+ return new Promise((resolve2, reject) => {
3836
3838
  try {
3837
3839
  this._writeSharedStrings();
3838
3840
  this._writeStyles();
@@ -3840,7 +3842,7 @@ var XlsxWriter = class {
3840
3842
  this._writeContentTypes();
3841
3843
  this._writeRels();
3842
3844
  this.output.on("close", () => {
3843
- resolve();
3845
+ resolve2();
3844
3846
  });
3845
3847
  this.archive.on("error", (err) => {
3846
3848
  reject(err);
@@ -3983,7 +3985,7 @@ var XlsxUpdater = class {
3983
3985
  /**
3984
3986
  * @param path Path to an existing `.xlsx` file.
3985
3987
  */
3986
- constructor(path2) {
3988
+ constructor(path3) {
3987
3989
  this.sheetNameToPath = /* @__PURE__ */ new Map();
3988
3990
  this.pivotCacheDefs = [];
3989
3991
  this.pivotTableDefPaths = [];
@@ -3992,12 +3994,12 @@ var XlsxUpdater = class {
3992
3994
  this.sharedStringsOriginalLength = 0;
3993
3995
  this.sharedStringsCount = 0;
3994
3996
  this.stringIndexMap = /* @__PURE__ */ new Map();
3995
- this.sourcePath = path2;
3997
+ this.sourcePath = path3;
3996
3998
  this._oaEpoch = Date.UTC(1899, 11, 30);
3997
- if (!fs3.existsSync(path2)) {
3998
- throw new Error(`XlsxUpdater: file not found: ${path2}`);
3999
+ if (!fs3.existsSync(path3)) {
4000
+ throw new Error(`XlsxUpdater: file not found: ${path3}`);
3999
4001
  }
4000
- this.zip = new import_adm_zip2.default(path2);
4002
+ this.zip = new import_adm_zip2.default(path3);
4001
4003
  if (this.zip.getEntry("xl/workbook.bin")) {
4002
4004
  throw new Error(
4003
4005
  "XlsxUpdater: XLSB files are not supported yet. Convert the workbook to XLSX first."
@@ -4382,8 +4384,8 @@ var XlsxUpdater = class {
4382
4384
  }
4383
4385
  }
4384
4386
  _addRefreshOnLoadToPivotTables() {
4385
- for (const path2 of this.pivotTableDefPaths) {
4386
- const entry = this.zip.getEntry(path2);
4387
+ for (const path3 of this.pivotTableDefPaths) {
4388
+ const entry = this.zip.getEntry(path3);
4387
4389
  if (!entry) continue;
4388
4390
  let xml = entry.getData().toString("utf8");
4389
4391
  const rootRe = /<pivotTableDefinition\b[^>]*>/;
@@ -4391,7 +4393,7 @@ var XlsxUpdater = class {
4391
4393
  if (!rootMatch || rootMatch[0].includes("refreshOnLoad")) continue;
4392
4394
  const newTag = rootMatch[0].replace(/(\/?>)$/, ' refreshOnLoad="1"$1');
4393
4395
  xml = xml.substring(0, rootMatch.index) + newTag + xml.substring(rootMatch.index + rootMatch[0].length);
4394
- this.zip.updateFile(path2, Buffer.from(xml, "utf8"));
4396
+ this.zip.updateFile(path3, Buffer.from(xml, "utf8"));
4395
4397
  }
4396
4398
  }
4397
4399
  };
@@ -4476,7 +4478,7 @@ var XlsbUpdater = class {
4476
4478
  /**
4477
4479
  * @param path Path to an existing `.xlsb` file.
4478
4480
  */
4479
- constructor(path2) {
4481
+ constructor(path3) {
4480
4482
  this.sheetNameToPath = /* @__PURE__ */ new Map();
4481
4483
  this.pivotCacheDefPaths = [];
4482
4484
  this.sharedStringsBuf = null;
@@ -4489,12 +4491,12 @@ var XlsbUpdater = class {
4489
4491
  // Shared strings
4490
4492
  // ------------------------------------------------------------------
4491
4493
  this._appendCount = 0;
4492
- this.sourcePath = path2;
4494
+ this.sourcePath = path3;
4493
4495
  this._oaEpoch = Date.UTC(1899, 11, 30);
4494
- if (!fs4.existsSync(path2)) {
4495
- throw new Error(`XlsbUpdater: file not found: ${path2}`);
4496
+ if (!fs4.existsSync(path3)) {
4497
+ throw new Error(`XlsbUpdater: file not found: ${path3}`);
4496
4498
  }
4497
- this.zip = new import_adm_zip3.default(path2);
4499
+ this.zip = new import_adm_zip3.default(path3);
4498
4500
  if (!this.zip.getEntry("xl/workbook.bin")) {
4499
4501
  throw new Error(
4500
4502
  "XlsbUpdater: not an XLSB workbook (xl/workbook.bin missing). Convert the file to XLSB first."
@@ -4906,8 +4908,8 @@ var XlsbUpdater = class {
4906
4908
  // Pivot cache integration
4907
4909
  // ------------------------------------------------------------------
4908
4910
  _patchPivotCaches(sheetName, rwLast, colLast) {
4909
- for (const path2 of this.pivotCacheDefPaths) {
4910
- const entry = this.zip.getEntry(path2);
4911
+ for (const path3 of this.pivotCacheDefPaths) {
4912
+ const entry = this.zip.getEntry(path3);
4911
4913
  if (!entry) continue;
4912
4914
  const buf = Buffer.from(entry.getData());
4913
4915
  const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
@@ -4939,11 +4941,255 @@ var XlsbUpdater = class {
4939
4941
  }
4940
4942
  }
4941
4943
  if (patched) {
4942
- this.zip.updateFile(path2, buf);
4944
+ this.zip.updateFile(path3, buf);
4943
4945
  }
4944
4946
  }
4945
4947
  }
4946
4948
  };
4949
+
4950
+ // src/ExcelConverter.ts
4951
+ var fs6 = __toESM(require("fs"));
4952
+ var path2 = __toESM(require("path"));
4953
+ var import_crypto2 = require("crypto");
4954
+ var import_child_process = require("child_process");
4955
+
4956
+ // src/atomicFile.ts
4957
+ var fs5 = __toESM(require("fs"));
4958
+ var import_crypto = require("crypto");
4959
+ var realFileOperations = {
4960
+ exists: (filePath) => fs5.existsSync(filePath),
4961
+ rename: (sourcePath, destinationPath) => fs5.renameSync(sourcePath, destinationPath),
4962
+ remove: (filePath) => fs5.rmSync(filePath)
4963
+ };
4964
+ function errorMessage(error) {
4965
+ return error instanceof Error ? error.message : String(error);
4966
+ }
4967
+ function destinationExistsError(destinationPath) {
4968
+ return new Error(
4969
+ `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${destinationPath}`
4970
+ );
4971
+ }
4972
+ function installTemporaryOutput(temporaryPath, destinationPath, overwrite, operations = realFileOperations) {
4973
+ const backupPath = `${destinationPath}.${(0, import_crypto.randomUUID)()}.replacement-backup`;
4974
+ let backupCreated = false;
4975
+ let installed = false;
4976
+ if (operations.exists(destinationPath)) {
4977
+ if (!overwrite) {
4978
+ throw destinationExistsError(destinationPath);
4979
+ }
4980
+ operations.rename(destinationPath, backupPath);
4981
+ backupCreated = true;
4982
+ }
4983
+ try {
4984
+ try {
4985
+ operations.rename(temporaryPath, destinationPath);
4986
+ installed = true;
4987
+ } catch (error) {
4988
+ if (overwrite && !backupCreated && operations.exists(destinationPath)) {
4989
+ operations.rename(destinationPath, backupPath);
4990
+ backupCreated = true;
4991
+ operations.rename(temporaryPath, destinationPath);
4992
+ installed = true;
4993
+ } else {
4994
+ throw error;
4995
+ }
4996
+ }
4997
+ if (backupCreated) {
4998
+ operations.remove(backupPath);
4999
+ }
5000
+ } catch (error) {
5001
+ if (backupCreated && !installed) {
5002
+ if (!operations.exists(destinationPath)) {
5003
+ try {
5004
+ operations.rename(backupPath, destinationPath);
5005
+ backupCreated = false;
5006
+ } catch (rollbackError) {
5007
+ throw new Error(
5008
+ `Excel conversion: final destination install failed (${errorMessage(error)}), and restoring the original destination also failed (${errorMessage(rollbackError)}). Original file remains at ${backupPath}.`
5009
+ );
5010
+ }
5011
+ } else {
5012
+ throw new Error(
5013
+ `Excel conversion: final destination install failed (${errorMessage(error)}). A concurrent destination was preserved; the original file remains at ${backupPath}.`
5014
+ );
5015
+ }
5016
+ }
5017
+ throw error;
5018
+ }
5019
+ }
5020
+
5021
+ // src/ExcelConverter.ts
5022
+ var POWERSHELL_CONVERSION_SCRIPT = `
5023
+ $ErrorActionPreference = 'Stop'
5024
+ $inputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_INPUT')
5025
+ $outputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_OUTPUT')
5026
+ $fileFormat = [int][Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_FORMAT')
5027
+ $excel = $null
5028
+ $workbook = $null
5029
+ $exitCode = 0
5030
+
5031
+ try {
5032
+ $excel = New-Object -ComObject Excel.Application
5033
+ $excel.Visible = $false
5034
+ $excel.DisplayAlerts = $false
5035
+ $excel.ScreenUpdating = $false
5036
+
5037
+ # Open read-only so the source workbook is never changed by the conversion.
5038
+ $workbook = $excel.Workbooks.Open($inputPath, 0, $true)
5039
+ if ($null -eq $workbook) {
5040
+ throw 'Excel did not return a workbook when opening the source file.'
5041
+ }
5042
+
5043
+ $workbook.SaveAs($outputPath, $fileFormat)
5044
+ } catch {
5045
+ $exitCode = 1
5046
+ [Console]::Error.WriteLine($_.Exception.ToString())
5047
+ } finally {
5048
+ if ($null -ne $workbook) {
5049
+ try { $workbook.Close($false) } catch { }
5050
+ try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook) | Out-Null } catch { }
5051
+ }
5052
+ if ($null -ne $excel) {
5053
+ try { $excel.Quit() } catch { }
5054
+ try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null } catch { }
5055
+ }
5056
+ [GC]::Collect()
5057
+ [GC]::WaitForPendingFinalizers()
5058
+ }
5059
+
5060
+ exit $exitCode
5061
+ `;
5062
+ function errorMessage2(error) {
5063
+ return error instanceof Error ? error.message : String(error);
5064
+ }
5065
+ function validateConversionPaths(inputPath, outputPath, inputExtension, outputExtension, overwrite) {
5066
+ const input = path2.resolve(inputPath);
5067
+ const output = path2.resolve(outputPath);
5068
+ if (path2.extname(input).toLowerCase() !== inputExtension) {
5069
+ throw new Error(
5070
+ `Excel conversion: source must have the ${inputExtension} extension: ${inputPath}`
5071
+ );
5072
+ }
5073
+ if (path2.extname(output).toLowerCase() !== outputExtension) {
5074
+ throw new Error(
5075
+ `Excel conversion: destination must have the ${outputExtension} extension: ${outputPath}`
5076
+ );
5077
+ }
5078
+ if (!fs6.existsSync(input)) {
5079
+ throw new Error(`Excel conversion: source file not found: ${inputPath}`);
5080
+ }
5081
+ if (!fs6.statSync(input).isFile()) {
5082
+ throw new Error(`Excel conversion: source path is not a file: ${inputPath}`);
5083
+ }
5084
+ if (!fs6.existsSync(path2.dirname(output))) {
5085
+ throw new Error(`Excel conversion: destination directory not found: ${path2.dirname(output)}`);
5086
+ }
5087
+ if (fs6.existsSync(output)) {
5088
+ if (fs6.statSync(output).isDirectory()) {
5089
+ throw new Error(`Excel conversion: destination path is a directory: ${outputPath}`);
5090
+ }
5091
+ if (!overwrite) {
5092
+ throw new Error(
5093
+ `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${outputPath}`
5094
+ );
5095
+ }
5096
+ }
5097
+ if (process.platform === "win32" && input.toLowerCase() === output.toLowerCase()) {
5098
+ throw new Error("Excel conversion: source and destination must be different files.");
5099
+ }
5100
+ return { input, output };
5101
+ }
5102
+ function runPowerShellConversion(inputPath, outputPath, fileFormat) {
5103
+ const encodedCommand = Buffer.from(POWERSHELL_CONVERSION_SCRIPT, "utf16le").toString("base64");
5104
+ const environment = {
5105
+ ...process.env,
5106
+ JUSTYBASE_EXCEL_INPUT: inputPath,
5107
+ JUSTYBASE_EXCEL_OUTPUT: outputPath,
5108
+ JUSTYBASE_EXCEL_FORMAT: String(fileFormat)
5109
+ };
5110
+ return new Promise((resolve2, reject) => {
5111
+ var _a, _b, _c, _d;
5112
+ let stdout = "";
5113
+ let stderr = "";
5114
+ let settled = false;
5115
+ const child = (0, import_child_process.spawn)(
5116
+ "powershell.exe",
5117
+ [
5118
+ "-NoLogo",
5119
+ "-NoProfile",
5120
+ "-NonInteractive",
5121
+ "-ExecutionPolicy",
5122
+ "Bypass",
5123
+ "-EncodedCommand",
5124
+ encodedCommand
5125
+ ],
5126
+ { env: environment, windowsHide: true }
5127
+ );
5128
+ (_a = child.stdout) == null ? void 0 : _a.setEncoding("utf8");
5129
+ (_b = child.stderr) == null ? void 0 : _b.setEncoding("utf8");
5130
+ (_c = child.stdout) == null ? void 0 : _c.on("data", (chunk) => {
5131
+ stdout += chunk;
5132
+ });
5133
+ (_d = child.stderr) == null ? void 0 : _d.on("data", (chunk) => {
5134
+ stderr += chunk;
5135
+ });
5136
+ child.once("error", (error) => {
5137
+ if (settled) return;
5138
+ settled = true;
5139
+ reject(new Error(`Excel conversion: unable to start PowerShell/COM: ${error.message}`));
5140
+ });
5141
+ child.once("close", (code) => {
5142
+ if (settled) return;
5143
+ settled = true;
5144
+ if (code === 0) {
5145
+ resolve2();
5146
+ return;
5147
+ }
5148
+ const details = (stderr || stdout).replace(/\s+/g, " ").trim();
5149
+ reject(new Error(
5150
+ `Excel conversion failed (PowerShell exit code ${code ?? "unknown"})${details ? `: ${details}` : "."}`
5151
+ ));
5152
+ });
5153
+ });
5154
+ }
5155
+ async function convertExcelFile(inputPath, outputPath, inputExtension, outputExtension, fileFormat, options = {}) {
5156
+ const { input, output } = validateConversionPaths(
5157
+ inputPath,
5158
+ outputPath,
5159
+ inputExtension,
5160
+ outputExtension,
5161
+ options.overwrite === true
5162
+ );
5163
+ if (process.platform !== "win32") {
5164
+ throw new Error("Excel conversion requires Windows with desktop Microsoft Excel and COM automation.");
5165
+ }
5166
+ const temporaryOutput = path2.join(
5167
+ path2.dirname(output),
5168
+ `.${path2.basename(output, outputExtension)}.${(0, import_crypto2.randomUUID)()}.conversion${outputExtension}`
5169
+ );
5170
+ try {
5171
+ await runPowerShellConversion(input, temporaryOutput, fileFormat);
5172
+ if (!fs6.existsSync(temporaryOutput)) {
5173
+ throw new Error("Excel conversion completed without creating the destination file.");
5174
+ }
5175
+ installTemporaryOutput(temporaryOutput, output, options.overwrite === true);
5176
+ } catch (error) {
5177
+ throw new Error(`Excel conversion ${inputExtension} -> ${outputExtension}: ${errorMessage2(error)}`);
5178
+ } finally {
5179
+ if (fs6.existsSync(temporaryOutput)) {
5180
+ try {
5181
+ fs6.rmSync(temporaryOutput);
5182
+ } catch {
5183
+ }
5184
+ }
5185
+ }
5186
+ }
5187
+ function convertXlsbToXlsx(inputPath, outputPath, options) {
5188
+ return convertExcelFile(inputPath, outputPath, ".xlsb", ".xlsx", 51, options);
5189
+ }
5190
+ function convertXlsxToXlsb(inputPath, outputPath, options) {
5191
+ return convertExcelFile(inputPath, outputPath, ".xlsx", ".xlsb", 50, options);
5192
+ }
4947
5193
  // Annotate the CommonJS export names for ESM import in node:
4948
5194
  0 && (module.exports = {
4949
5195
  BiffReaderWriter,
@@ -4960,6 +5206,8 @@ var XlsbUpdater = class {
4960
5206
  XlsxUpdater,
4961
5207
  XlsxWriter,
4962
5208
  applyHeaderWidths,
5209
+ convertXlsbToXlsx,
5210
+ convertXlsxToXlsb,
4963
5211
  defaultColWidth,
4964
5212
  getFormat,
4965
5213
  initColWidths,