@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/dist/cjs/index.js CHANGED
@@ -40,10 +40,13 @@ __export(index_exports, {
40
40
  XlsbReader: () => XlsbReader,
41
41
  XlsbUpdater: () => XlsbUpdater,
42
42
  XlsbWriter: () => XlsbWriter,
43
+ XlsmUpdater: () => XlsxUpdater,
43
44
  XlsxReader: () => XlsxReader,
44
45
  XlsxUpdater: () => XlsxUpdater,
45
46
  XlsxWriter: () => XlsxWriter,
46
47
  applyHeaderWidths: () => applyHeaderWidths,
48
+ convertXlsbToXlsx: () => convertXlsbToXlsx,
49
+ convertXlsxToXlsb: () => convertXlsxToXlsb,
47
50
  defaultColWidth: () => defaultColWidth,
48
51
  getFormat: () => getFormat,
49
52
  initColWidths: () => initColWidths,
@@ -458,8 +461,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
458
461
  this.xfIdToNumFmtId = [];
459
462
  this.customDateFormats = null;
460
463
  }
461
- async open(path2, readSharedStrings = true) {
462
- this.zip = new import_adm_zip.default(path2);
464
+ async open(path7, readSharedStrings = true) {
465
+ this.zip = new import_adm_zip.default(path7);
463
466
  const wbEntry = this.zip.getEntry("xl/workbook.bin");
464
467
  if (wbEntry) {
465
468
  const buf = wbEntry.getData();
@@ -504,8 +507,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
504
507
  this.resultsCount = this.sheets.length;
505
508
  this._currentSheetIndex = -1;
506
509
  }
507
- _readSharedStrings(path2) {
508
- const entry = this.zip.getEntry(path2);
510
+ _readSharedStrings(path7) {
511
+ const entry = this.zip.getEntry(path7);
509
512
  if (!entry) return;
510
513
  const reader = new BiffReaderWriter(entry.getData());
511
514
  while (reader.readSharedStrings()) {
@@ -514,8 +517,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
514
517
  }
515
518
  }
516
519
  }
517
- _readStyles(path2) {
518
- const entry = this.zip.getEntry(path2);
520
+ _readStyles(path7) {
521
+ const entry = this.zip.getEntry(path7);
519
522
  if (!entry) return;
520
523
  const reader = new BiffReaderWriter(entry.getData());
521
524
  while (reader.readStyles()) {
@@ -638,7 +641,7 @@ function escapeXmlText(str) {
638
641
  }
639
642
  function unescapeXml(str) {
640
643
  if (!str || str.indexOf("&") === -1) return str;
641
- return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
644
+ return str.replace(/&#x([0-9a-f]+);/gi, (_match, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_match, decimal) => String.fromCodePoint(parseInt(decimal, 10))).replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
642
645
  }
643
646
  function columnIndexToLetter(colIndex) {
644
647
  let n = colIndex + 1;
@@ -667,26 +670,17 @@ function trimTrailingEmptyRows(rows) {
667
670
  }
668
671
  function parseSharedStringsXml(xml) {
669
672
  const result = [];
670
- let pos = 0;
671
- while (true) {
672
- const siStart = xml.indexOf("<si>", pos);
673
- if (siStart === -1) break;
674
- const siEnd = xml.indexOf("</si>", siStart);
675
- if (siEnd === -1) break;
676
- const content = xml.substring(siStart, siEnd);
677
- let val = "";
678
- let tPos = 0;
679
- while (true) {
680
- const tStart = content.indexOf("<t", tPos);
681
- if (tStart === -1) break;
682
- const tagEnd = content.indexOf(">", tStart);
683
- const tEnd = content.indexOf("</t>", tagEnd);
684
- if (tEnd === -1) break;
685
- val += content.substring(tagEnd + 1, tEnd);
686
- tPos = tEnd + 4;
687
- }
688
- result.push(unescapeXml(val));
689
- pos = siEnd + 5;
673
+ const itemRegex = /<(?:(?:[A-Za-z_][\w.-]*):)?si\b[^>]*>([\s\S]*?)<\/(?:(?:[A-Za-z_][\w.-]*):)?si\s*>/gi;
674
+ let itemMatch;
675
+ while ((itemMatch = itemRegex.exec(xml)) !== null) {
676
+ const content = itemMatch[1];
677
+ const textRegex = /<(?:(?:[A-Za-z_][\w.-]*):)?t\b[^>]*>([\s\S]*?)<\/(?:(?:[A-Za-z_][\w.-]*):)?t\s*>/gi;
678
+ let value = "";
679
+ let textMatch;
680
+ while ((textMatch = textRegex.exec(content)) !== null) {
681
+ value += textMatch[1];
682
+ }
683
+ result.push(unescapeXml(value));
690
684
  }
691
685
  return result;
692
686
  }
@@ -707,9 +701,9 @@ var XlsxReader = class extends ExcelReaderAbstract {
707
701
  this.xfIdToNumFmtId = [];
708
702
  this.customDateFormats = /* @__PURE__ */ new Set();
709
703
  }
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) => {
704
+ async open(path7, readSharedStrings = true) {
705
+ return new Promise((resolve4, reject) => {
706
+ import_yauzl.default.open(path7, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
713
707
  if (err) return reject(err);
714
708
  this.zipfile = zipfile;
715
709
  this.entries = /* @__PURE__ */ new Map();
@@ -757,7 +751,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
757
751
  }
758
752
  this.resultsCount = this.sheets.length;
759
753
  this._currentSheetIndex = -1;
760
- resolve();
754
+ resolve4();
761
755
  } catch (e) {
762
756
  reject(e);
763
757
  }
@@ -771,16 +765,16 @@ var XlsxReader = class extends ExcelReaderAbstract {
771
765
  this.zipfile.close();
772
766
  }
773
767
  }
774
- async _readZipEntryContent(path2) {
775
- const entry = this.entries.get(path2);
768
+ async _readZipEntryContent(path7) {
769
+ const entry = this.entries.get(path7);
776
770
  if (!entry) return null;
777
- return new Promise((resolve, reject) => {
771
+ return new Promise((resolve4, reject) => {
778
772
  this.zipfile.openReadStream(entry, (err, readStream) => {
779
773
  if (err) return reject(err);
780
774
  const chunks = [];
781
775
  readStream.on("data", (chunk) => chunks.push(chunk));
782
776
  readStream.on("end", () => {
783
- resolve(Buffer.concat(chunks).toString("utf8"));
777
+ resolve4(Buffer.concat(chunks).toString("utf8"));
784
778
  });
785
779
  readStream.on("error", reject);
786
780
  });
@@ -3386,7 +3380,7 @@ var XlsbWriter = class _XlsbWriter {
3386
3380
  * Must be awaited before the file is fully written.
3387
3381
  */
3388
3382
  finalize() {
3389
- return new Promise((resolve, reject) => {
3383
+ return new Promise((resolve4, reject) => {
3390
3384
  try {
3391
3385
  this.saveSst();
3392
3386
  this.archive.append(this._buildStylesBin(), { name: "xl/styles.bin" });
@@ -3484,7 +3478,7 @@ var XlsbWriter = class _XlsbWriter {
3484
3478
  </Relationships>`;
3485
3479
  this.archive.append(wsRels, { name: `xl/worksheets/_rels/${sheet.nameInArchive}.rels` });
3486
3480
  }
3487
- this.output.on("close", () => resolve());
3481
+ this.output.on("close", () => resolve4());
3488
3482
  this.archive.on("error", (err) => reject(err));
3489
3483
  this.archive.finalize();
3490
3484
  } catch (err) {
@@ -3832,7 +3826,7 @@ var XlsxWriter = class {
3832
3826
  }
3833
3827
  /** Finalize the ZIP package and close the output stream. */
3834
3828
  finalize() {
3835
- return new Promise((resolve, reject) => {
3829
+ return new Promise((resolve4, reject) => {
3836
3830
  try {
3837
3831
  this._writeSharedStrings();
3838
3832
  this._writeStyles();
@@ -3840,7 +3834,7 @@ var XlsxWriter = class {
3840
3834
  this._writeContentTypes();
3841
3835
  this._writeRels();
3842
3836
  this.output.on("close", () => {
3843
- resolve();
3837
+ resolve4();
3844
3838
  });
3845
3839
  this.archive.on("error", (err) => {
3846
3840
  reject(err);
@@ -3977,13 +3971,237 @@ ${xfEntries.join("\n")}
3977
3971
  };
3978
3972
 
3979
3973
  // src/XlsxUpdater.ts
3980
- var fs3 = __toESM(require("fs"));
3974
+ var fs5 = __toESM(require("fs"));
3975
+ var os = __toESM(require("os"));
3976
+ var path4 = __toESM(require("path"));
3981
3977
  var import_adm_zip2 = __toESM(require("adm-zip"));
3982
- var XlsxUpdater = class {
3978
+
3979
+ // src/atomicFile.ts
3980
+ var fs3 = __toESM(require("fs"));
3981
+ var path2 = __toESM(require("path"));
3982
+ var import_crypto = require("crypto");
3983
+ var realFileOperations = {
3984
+ exists: (filePath) => fs3.existsSync(filePath),
3985
+ rename: (sourcePath, destinationPath) => fs3.renameSync(sourcePath, destinationPath),
3986
+ remove: (filePath) => fs3.rmSync(filePath)
3987
+ };
3988
+ function errorMessage(error) {
3989
+ return error instanceof Error ? error.message : String(error);
3990
+ }
3991
+ function destinationExistsError(destinationPath) {
3992
+ return new Error(
3993
+ `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${destinationPath}`
3994
+ );
3995
+ }
3996
+ function readDestinationMode(target) {
3997
+ try {
3998
+ if (!fs3.existsSync(target)) return void 0;
3999
+ return fs3.statSync(target).mode & 511;
4000
+ } catch {
4001
+ return void 0;
4002
+ }
4003
+ }
4004
+ function installTemporaryOutput(temporaryPath, destinationPath, overwrite, operations = realFileOperations) {
4005
+ const backupPath = `${destinationPath}.${(0, import_crypto.randomUUID)()}.replacement-backup`;
4006
+ let backupCreated = false;
4007
+ let installed = false;
4008
+ if (operations.exists(destinationPath)) {
4009
+ if (!overwrite) {
4010
+ throw destinationExistsError(destinationPath);
4011
+ }
4012
+ operations.rename(destinationPath, backupPath);
4013
+ backupCreated = true;
4014
+ }
4015
+ try {
4016
+ try {
4017
+ operations.rename(temporaryPath, destinationPath);
4018
+ installed = true;
4019
+ } catch (error) {
4020
+ if (overwrite && !backupCreated && operations.exists(destinationPath)) {
4021
+ operations.rename(destinationPath, backupPath);
4022
+ backupCreated = true;
4023
+ operations.rename(temporaryPath, destinationPath);
4024
+ installed = true;
4025
+ } else {
4026
+ throw error;
4027
+ }
4028
+ }
4029
+ if (backupCreated) {
4030
+ operations.remove(backupPath);
4031
+ }
4032
+ } catch (error) {
4033
+ if (backupCreated && !installed) {
4034
+ if (!operations.exists(destinationPath)) {
4035
+ try {
4036
+ operations.rename(backupPath, destinationPath);
4037
+ backupCreated = false;
4038
+ } catch (rollbackError) {
4039
+ throw new Error(
4040
+ `Excel conversion: final destination install failed (${errorMessage(error)}), and restoring the original destination also failed (${errorMessage(rollbackError)}). Original file remains at ${backupPath}.`
4041
+ );
4042
+ }
4043
+ } else {
4044
+ throw new Error(
4045
+ `Excel conversion: final destination install failed (${errorMessage(error)}). A concurrent destination was preserved; the original file remains at ${backupPath}.`
4046
+ );
4047
+ }
4048
+ }
4049
+ throw error;
4050
+ }
4051
+ }
4052
+ function writeBufferAtomically(buffer, destinationPath) {
4053
+ const target = path2.resolve(destinationPath);
4054
+ fs3.mkdirSync(path2.dirname(target), { recursive: true });
4055
+ const temporaryPath = path2.join(
4056
+ path2.dirname(target),
4057
+ `.${path2.basename(target)}.${(0, import_crypto.randomUUID)()}.tmp`
4058
+ );
4059
+ const destinationMode = readDestinationMode(target);
4060
+ try {
4061
+ const fd = fs3.openSync(temporaryPath, "w", destinationMode ?? 438);
4062
+ try {
4063
+ if (destinationMode !== void 0) {
4064
+ try {
4065
+ fs3.fchmodSync(fd, destinationMode);
4066
+ } catch {
4067
+ }
4068
+ }
4069
+ fs3.writeFileSync(fd, buffer);
4070
+ fs3.fsyncSync(fd);
4071
+ } finally {
4072
+ fs3.closeSync(fd);
4073
+ }
4074
+ if (destinationMode !== void 0) {
4075
+ try {
4076
+ fs3.chmodSync(temporaryPath, destinationMode);
4077
+ } catch {
4078
+ }
4079
+ }
4080
+ fs3.renameSync(temporaryPath, target);
4081
+ } finally {
4082
+ if (fs3.existsSync(temporaryPath)) {
4083
+ try {
4084
+ fs3.rmSync(temporaryPath);
4085
+ } catch {
4086
+ }
4087
+ }
4088
+ }
4089
+ }
4090
+
4091
+ // src/streamingZip.ts
4092
+ var fs4 = __toESM(require("fs"));
4093
+ var path3 = __toESM(require("path"));
4094
+ var import_crypto2 = require("crypto");
4095
+ var import_promises = require("stream/promises");
4096
+ var import_stream3 = require("stream");
4097
+ var import_archiver3 = __toESM(require("archiver"));
4098
+ function readDestinationMode2(target) {
4099
+ try {
4100
+ if (!fs4.existsSync(target)) return void 0;
4101
+ return fs4.statSync(target).mode & 511;
4102
+ } catch {
4103
+ return void 0;
4104
+ }
4105
+ }
4106
+ function appendAndWait(archive, source, name) {
4107
+ return new Promise((resolve4, reject) => {
4108
+ const cleanup = () => {
4109
+ archive.off("entry", onEntry);
4110
+ archive.off("error", onError);
4111
+ const maybeEmitter2 = source;
4112
+ if (typeof maybeEmitter2.off === "function") {
4113
+ maybeEmitter2.off("error", onError);
4114
+ }
4115
+ };
4116
+ const onEntry = () => {
4117
+ cleanup();
4118
+ resolve4();
4119
+ };
4120
+ const onError = (error) => {
4121
+ cleanup();
4122
+ reject(error);
4123
+ };
4124
+ archive.once("entry", onEntry);
4125
+ archive.once("error", onError);
4126
+ const maybeEmitter = source;
4127
+ if (typeof maybeEmitter.once === "function") {
4128
+ maybeEmitter.once("error", onError);
4129
+ }
4130
+ try {
4131
+ archive.append(source, { name });
4132
+ } catch (error) {
4133
+ cleanup();
4134
+ reject(error);
4135
+ }
4136
+ });
4137
+ }
4138
+ async function writeAdmZipStreaming(zip, stagedParts, destinationPath) {
4139
+ const target = path3.resolve(destinationPath);
4140
+ fs4.mkdirSync(path3.dirname(target), { recursive: true });
4141
+ const temporaryPath = path3.join(
4142
+ path3.dirname(target),
4143
+ `.${path3.basename(target)}.${(0, import_crypto2.randomUUID)()}.tmp`
4144
+ );
4145
+ const destinationMode = readDestinationMode2(target);
4146
+ let output = null;
4147
+ let archive = null;
4148
+ try {
4149
+ output = fs4.createWriteStream(
4150
+ temporaryPath,
4151
+ destinationMode !== void 0 ? { mode: destinationMode } : {}
4152
+ );
4153
+ archive = (0, import_archiver3.default)("zip");
4154
+ archive.pipe(output);
4155
+ for (const entry of zip.getEntries()) {
4156
+ const stagedPath = stagedParts.get(entry.entryName);
4157
+ if (stagedPath) {
4158
+ await appendAndWait(archive, fs4.createReadStream(stagedPath), entry.entryName);
4159
+ } else {
4160
+ await appendAndWait(archive, import_stream3.Readable.from([entry.getData()]), entry.entryName);
4161
+ }
4162
+ }
4163
+ await archive.finalize();
4164
+ await (0, import_promises.finished)(output);
4165
+ if (destinationMode !== void 0) {
4166
+ try {
4167
+ fs4.chmodSync(temporaryPath, destinationMode);
4168
+ } catch {
4169
+ }
4170
+ }
4171
+ const fd = fs4.openSync(temporaryPath, "r+");
4172
+ try {
4173
+ fs4.fsyncSync(fd);
4174
+ } finally {
4175
+ fs4.closeSync(fd);
4176
+ }
4177
+ fs4.renameSync(temporaryPath, target);
4178
+ } catch (error) {
4179
+ try {
4180
+ archive == null ? void 0 : archive.abort();
4181
+ } catch {
4182
+ }
4183
+ throw error;
4184
+ } finally {
4185
+ if (output && !output.closed) {
4186
+ output.destroy();
4187
+ }
4188
+ if (fs4.existsSync(temporaryPath)) {
4189
+ try {
4190
+ fs4.rmSync(temporaryPath);
4191
+ } catch {
4192
+ }
4193
+ }
4194
+ }
4195
+ }
4196
+
4197
+ // src/XlsxUpdater.ts
4198
+ var XlsxUpdater = class _XlsxUpdater {
3983
4199
  /**
3984
4200
  * @param path Path to an existing `.xlsx` file.
3985
4201
  */
3986
- constructor(path2) {
4202
+ constructor(filePath) {
4203
+ this.temporaryDirectory = null;
4204
+ this.stagedParts = /* @__PURE__ */ new Map();
3987
4205
  this.sheetNameToPath = /* @__PURE__ */ new Map();
3988
4206
  this.pivotCacheDefs = [];
3989
4207
  this.pivotTableDefPaths = [];
@@ -3991,13 +4209,15 @@ var XlsxUpdater = class {
3991
4209
  this.sharedStringsValues = [];
3992
4210
  this.sharedStringsOriginalLength = 0;
3993
4211
  this.sharedStringsCount = 0;
4212
+ this.sharedStringsPending = [];
4213
+ this.sharedStringsDirty = false;
3994
4214
  this.stringIndexMap = /* @__PURE__ */ new Map();
3995
- this.sourcePath = path2;
4215
+ this.sourcePath = filePath;
3996
4216
  this._oaEpoch = Date.UTC(1899, 11, 30);
3997
- if (!fs3.existsSync(path2)) {
3998
- throw new Error(`XlsxUpdater: file not found: ${path2}`);
4217
+ if (!fs5.existsSync(filePath)) {
4218
+ throw new Error(`XlsxUpdater: file not found: ${filePath}`);
3999
4219
  }
4000
- this.zip = new import_adm_zip2.default(path2);
4220
+ this.zip = new import_adm_zip2.default(filePath);
4001
4221
  if (this.zip.getEntry("xl/workbook.bin")) {
4002
4222
  throw new Error(
4003
4223
  "XlsxUpdater: XLSB files are not supported yet. Convert the workbook to XLSX first."
@@ -4031,8 +4251,15 @@ var XlsxUpdater = class {
4031
4251
  if (!entry) {
4032
4252
  throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
4033
4253
  }
4034
- const sheetXml = entry.getData().toString("utf8");
4254
+ const sheetXml = this._entryData(sheetPath).toString("utf8");
4035
4255
  this._ensureSharedStringsLoaded();
4256
+ if (this.sharedStringsXml !== null) {
4257
+ this.sharedStringsCount = Math.max(
4258
+ 0,
4259
+ this.sharedStringsCount - this._sharedStringRefCount(sheetXml)
4260
+ );
4261
+ this.sharedStringsDirty = true;
4262
+ }
4036
4263
  let colStyles = /* @__PURE__ */ new Map();
4037
4264
  let headerStyles = /* @__PURE__ */ new Map();
4038
4265
  let dateStyle = null;
@@ -4047,6 +4274,7 @@ var XlsxUpdater = class {
4047
4274
  const newSheetData = this._buildSheetData(rows, headers, colStyles, headerStyles, dateStyle);
4048
4275
  const newXml = this._patchSheetXml(sheetXml, newSheetData, totalRows, lastCol);
4049
4276
  this.zip.updateFile(sheetPath, Buffer.from(newXml, "utf8"));
4277
+ this._discardStagedPart(sheetPath);
4050
4278
  if (this.sharedStringsXml !== null) {
4051
4279
  this._commitSharedStrings();
4052
4280
  }
@@ -4054,18 +4282,255 @@ var XlsxUpdater = class {
4054
4282
  this._updatePivotCaches(sheetName, dimRef, rows.length);
4055
4283
  this._addRefreshOnLoadToPivotTables();
4056
4284
  }
4285
+ /**
4286
+ * Replace worksheet rows from a one-pass source without materialising the
4287
+ * complete result set. Use {@link saveStreaming} to keep the final ZIP
4288
+ * write streaming as well.
4289
+ */
4290
+ async replaceSheetDataStream(sheetName, rows, options = {}) {
4291
+ const { headers, styleFallback = "inherit" } = options;
4292
+ if (styleFallback !== "inherit" && styleFallback !== "general") {
4293
+ throw new Error("XlsxUpdater: styleFallback must be 'inherit' or 'general'.");
4294
+ }
4295
+ const sheetPath = this.sheetNameToPath.get(sheetName);
4296
+ if (!sheetPath) {
4297
+ throw new Error(`XlsxUpdater: sheet "${sheetName}" not found in the workbook.`);
4298
+ }
4299
+ const entry = this.zip.getEntry(sheetPath);
4300
+ if (!entry) {
4301
+ throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
4302
+ }
4303
+ const oldStage = this.stagedParts.get(sheetPath);
4304
+ const beforeValuesLength = this.sharedStringsValues.length;
4305
+ const beforePendingLength = this.sharedStringsPending.length;
4306
+ const beforeSharedStringsCount = this.sharedStringsCount;
4307
+ const beforeSharedStringsDirty = this.sharedStringsDirty;
4308
+ const beforeSharedStringsXml = this.sharedStringsXml;
4309
+ const beforeSharedStringsOriginalLength = this.sharedStringsOriginalLength;
4310
+ const beforeStringIndexMap = new Map(this.stringIndexMap);
4311
+ const sharedStringsEntry = this.zip.getEntry("xl/sharedStrings.xml");
4312
+ const beforeSharedStringsBytes = sharedStringsEntry || this.stagedParts.has("xl/sharedStrings.xml") ? Buffer.from(this._entryData("xl/sharedStrings.xml")) : null;
4313
+ const createdParts = [];
4314
+ const oldSheetXml = this._entryData(sheetPath).toString("utf8");
4315
+ const rowsPath = this._temporaryPath(".rows");
4316
+ const outputPath = this._temporaryPath(".xml");
4317
+ createdParts.push(rowsPath, outputPath);
4318
+ let output = null;
4319
+ try {
4320
+ this._ensureSharedStringsLoaded();
4321
+ if (this.sharedStringsXml !== null) {
4322
+ this.sharedStringsCount = Math.max(
4323
+ 0,
4324
+ this.sharedStringsCount - this._sharedStringRefCount(oldSheetXml)
4325
+ );
4326
+ this.sharedStringsDirty = true;
4327
+ }
4328
+ let colStyles = /* @__PURE__ */ new Map();
4329
+ let headerStyles = /* @__PURE__ */ new Map();
4330
+ let dateStyle = null;
4331
+ if (styleFallback !== "general") {
4332
+ const collected = this._collectExistingStyles(oldSheetXml);
4333
+ colStyles = collected.dataStyles;
4334
+ headerStyles = collected.headerStyles;
4335
+ dateStyle = this._findDateStyleIndex();
4336
+ }
4337
+ const prefixMatch = /<([A-Za-z_][\w.-]*:)?sheetData\b/.exec(oldSheetXml);
4338
+ const prefix = (prefixMatch == null ? void 0 : prefixMatch[1]) ?? "";
4339
+ const rowOutput = fs5.createWriteStream(rowsPath);
4340
+ output = rowOutput;
4341
+ let outputError = null;
4342
+ rowOutput.on("error", (error) => {
4343
+ outputError = error;
4344
+ });
4345
+ let nextRow = 1;
4346
+ let width = (headers == null ? void 0 : headers.length) ?? 0;
4347
+ let dataRowsSeen = 0;
4348
+ let keptDataRows = 0;
4349
+ let lastKeptEnd = 0;
4350
+ let pendingEmptyWidth = 0;
4351
+ let writtenBytes = 0;
4352
+ const write = async (data) => {
4353
+ if (outputError) throw outputError;
4354
+ writtenBytes += data.length;
4355
+ if (rowOutput.write(data)) return;
4356
+ await new Promise((resolve4, reject) => {
4357
+ const onDrain = () => {
4358
+ cleanup();
4359
+ resolve4();
4360
+ };
4361
+ const onError = (error) => {
4362
+ cleanup();
4363
+ reject(error);
4364
+ };
4365
+ const cleanup = () => {
4366
+ rowOutput.off("drain", onDrain);
4367
+ rowOutput.off("error", onError);
4368
+ };
4369
+ rowOutput.once("drain", onDrain);
4370
+ rowOutput.once("error", onError);
4371
+ });
4372
+ };
4373
+ if (headers !== void 0) {
4374
+ const cells = headers.map((value, column) => this._valueCell(value, column, nextRow, headerStyles, dateStyle));
4375
+ const header = `<${prefix}row r="${nextRow}">${cells.join("")}</${prefix}row>`;
4376
+ await write(Buffer.from(header, "utf8"));
4377
+ lastKeptEnd = writtenBytes;
4378
+ nextRow++;
4379
+ }
4380
+ for await (const sourceRow of rows) {
4381
+ if (typeof sourceRow === "string" || sourceRow instanceof Uint8Array) {
4382
+ throw new TypeError("Each row must be an iterable of cells, not text");
4383
+ }
4384
+ const row = Array.from(sourceRow);
4385
+ const cells = row.map((value, column) => value === null || value === void 0 ? "" : this._valueCell(value, column, nextRow, colStyles, dateStyle));
4386
+ const rowXml = `<${prefix}row r="${nextRow}">${cells.join("")}</${prefix}row>`;
4387
+ await write(Buffer.from(rowXml, "utf8"));
4388
+ dataRowsSeen++;
4389
+ nextRow++;
4390
+ if (row.every((value) => value === null || value === void 0)) {
4391
+ pendingEmptyWidth = Math.max(pendingEmptyWidth, row.length);
4392
+ } else {
4393
+ width = Math.max(width, pendingEmptyWidth, row.length);
4394
+ pendingEmptyWidth = 0;
4395
+ keptDataRows = dataRowsSeen;
4396
+ lastKeptEnd = writtenBytes;
4397
+ }
4398
+ }
4399
+ await new Promise((resolve4, reject) => {
4400
+ rowOutput.once("close", resolve4);
4401
+ rowOutput.once("error", reject);
4402
+ rowOutput.end();
4403
+ });
4404
+ if (outputError) throw outputError;
4405
+ const rowsFd = fs5.openSync(rowsPath, "r+");
4406
+ try {
4407
+ fs5.ftruncateSync(rowsFd, lastKeptEnd);
4408
+ } finally {
4409
+ fs5.closeSync(rowsFd);
4410
+ }
4411
+ const totalRows = keptDataRows + (headers !== void 0 ? 1 : 0);
4412
+ const lastCol = width - 1;
4413
+ const dimRef = totalRows > 0 && lastCol >= 0 ? `A1:${columnIndexToLetter(lastCol)}${totalRows}` : "A1";
4414
+ this._patchSheetFile(oldSheetXml, rowsPath, outputPath, dimRef);
4415
+ this._stagePart(sheetPath, outputPath);
4416
+ this._commitSharedStrings();
4417
+ this._updatePivotCaches(sheetName, dimRef, keptDataRows);
4418
+ this._addRefreshOnLoadToPivotTables();
4419
+ if (oldStage && oldStage !== outputPath) {
4420
+ this._removeTemporaryFile(oldStage);
4421
+ }
4422
+ createdParts.splice(createdParts.indexOf(outputPath), 1);
4423
+ } catch (error) {
4424
+ this.sharedStringsValues.length = beforeValuesLength;
4425
+ this.sharedStringsPending.length = beforePendingLength;
4426
+ this.sharedStringsCount = beforeSharedStringsCount;
4427
+ this.sharedStringsDirty = beforeSharedStringsDirty;
4428
+ this.sharedStringsXml = beforeSharedStringsXml;
4429
+ this.sharedStringsOriginalLength = beforeSharedStringsOriginalLength;
4430
+ this.stringIndexMap.clear();
4431
+ for (const [value, index] of beforeStringIndexMap) this.stringIndexMap.set(value, index);
4432
+ if (beforeSharedStringsBytes) {
4433
+ this.zip.updateFile("xl/sharedStrings.xml", beforeSharedStringsBytes);
4434
+ }
4435
+ if (oldStage) this.stagedParts.set(sheetPath, oldStage);
4436
+ else this.stagedParts.delete(sheetPath);
4437
+ throw error;
4438
+ } finally {
4439
+ if (output && !output.closed) output.destroy();
4440
+ for (const temporaryPath of createdParts) {
4441
+ this._removeTemporaryFile(temporaryPath);
4442
+ }
4443
+ }
4444
+ }
4057
4445
  /**
4058
4446
  * Write the updated workbook to disk.
4059
4447
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
4060
4448
  */
4061
4449
  save(outputPath) {
4062
4450
  const target = outputPath ?? this.sourcePath;
4063
- fs3.writeFileSync(target, this.zip.toBuffer());
4451
+ this._materializeStagedParts();
4452
+ writeBufferAtomically(this.zip.toBuffer(), target);
4453
+ }
4454
+ /** Save staged streaming replacements without materialising the ZIP. */
4455
+ async saveStreaming(outputPath) {
4456
+ const target = outputPath ?? this.sourcePath;
4457
+ await writeAdmZipStreaming(this.zip, this.stagedParts, target);
4458
+ this._materializeStagedParts();
4064
4459
  }
4065
4460
  /** The updated workbook as an in-memory ZIP buffer. */
4066
4461
  toBuffer() {
4462
+ this._materializeStagedParts();
4067
4463
  return this.zip.toBuffer();
4068
4464
  }
4465
+ /**
4466
+ * Release the staging directory and any unsaved staged parts.
4467
+ * The updater remains usable; a new staging directory is created lazily
4468
+ * on the next streaming replacement.
4469
+ */
4470
+ dispose() {
4471
+ for (const stagedPath of this.stagedParts.values()) {
4472
+ try {
4473
+ fs5.rmSync(stagedPath, { force: true });
4474
+ } catch {
4475
+ }
4476
+ }
4477
+ this.stagedParts.clear();
4478
+ if (this.temporaryDirectory !== null) {
4479
+ try {
4480
+ fs5.rmSync(this.temporaryDirectory, { recursive: true, force: true });
4481
+ } catch {
4482
+ }
4483
+ this.temporaryDirectory = null;
4484
+ }
4485
+ }
4486
+ /** Support `using` declarations for automatic staging cleanup. */
4487
+ [Symbol.dispose]() {
4488
+ this.dispose();
4489
+ }
4490
+ _ensureTemporaryDirectory() {
4491
+ if (this.temporaryDirectory === null) {
4492
+ this.temporaryDirectory = fs5.mkdtempSync(path4.join(os.tmpdir(), "spreadsheet-updater-"));
4493
+ } else {
4494
+ try {
4495
+ fs5.mkdirSync(this.temporaryDirectory, { recursive: true });
4496
+ } catch {
4497
+ }
4498
+ }
4499
+ return this.temporaryDirectory;
4500
+ }
4501
+ _temporaryPath(suffix) {
4502
+ return path4.join(this._ensureTemporaryDirectory(), `part-${Date.now()}-${Math.random().toString(16).slice(2)}${suffix}`);
4503
+ }
4504
+ _removeTemporaryFile(filePath) {
4505
+ try {
4506
+ fs5.rmSync(filePath);
4507
+ } catch (error) {
4508
+ if (error.code !== "ENOENT") throw error;
4509
+ }
4510
+ }
4511
+ _entryData(name) {
4512
+ const stagedPath = this.stagedParts.get(name);
4513
+ if (stagedPath) return fs5.readFileSync(stagedPath);
4514
+ const entry = this.zip.getEntry(name);
4515
+ if (!entry) throw new Error(`XlsxUpdater: ZIP member missing: ${name}`);
4516
+ return entry.getData();
4517
+ }
4518
+ _stagePart(name, filePath) {
4519
+ this.stagedParts.set(name, filePath);
4520
+ }
4521
+ _discardStagedPart(name) {
4522
+ const stagedPath = this.stagedParts.get(name);
4523
+ if (!stagedPath) return;
4524
+ this._removeTemporaryFile(stagedPath);
4525
+ this.stagedParts.delete(name);
4526
+ }
4527
+ _materializeStagedParts() {
4528
+ for (const [name, stagedPath] of this.stagedParts) {
4529
+ this.zip.updateFile(name, fs5.readFileSync(stagedPath));
4530
+ this._removeTemporaryFile(stagedPath);
4531
+ }
4532
+ this.stagedParts.clear();
4533
+ }
4069
4534
  // ------------------------------------------------------------------
4070
4535
  // Workbook structure
4071
4536
  // ------------------------------------------------------------------
@@ -4118,47 +4583,59 @@ var XlsxUpdater = class {
4118
4583
  return;
4119
4584
  }
4120
4585
  const entry = this.zip.getEntry("xl/sharedStrings.xml");
4121
- if (!entry) {
4586
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.xml")) {
4122
4587
  this.sharedStringsXml = null;
4123
4588
  return;
4124
4589
  }
4125
- this.sharedStringsXml = entry.getData().toString("utf8");
4590
+ this.sharedStringsXml = this._entryData("xl/sharedStrings.xml").toString("utf8");
4126
4591
  this.sharedStringsValues = parseSharedStringsXml(this.sharedStringsXml);
4127
- const countMatch = /<sst\b[^>]*>/.exec(this.sharedStringsXml);
4128
- const countAttr = countMatch ? /count="(\d+)"/.exec(countMatch[0]) : null;
4129
- this.sharedStringsCount = countAttr ? parseInt(countAttr[1], 10) : 0;
4592
+ const countMatch = /<(?:[A-Za-z_][\w.-]*:)?sst\b[^>]*>/i.exec(this.sharedStringsXml);
4593
+ const countAttr = countMatch ? /(?:^|\s)count\s*=\s*["'](\d+)["']/.exec(countMatch[0]) : null;
4594
+ this.sharedStringsCount = countAttr ? parseInt(countAttr[1], 10) : this.sharedStringsValues.length;
4130
4595
  this.sharedStringsValues.forEach((value, index) => {
4131
4596
  if (!this.stringIndexMap.has(value)) {
4132
4597
  this.stringIndexMap.set(value, index);
4133
4598
  }
4134
4599
  });
4135
4600
  this.sharedStringsOriginalLength = this.sharedStringsValues.length;
4601
+ this.sharedStringsPending = [];
4602
+ this.sharedStringsDirty = false;
4136
4603
  }
4137
4604
  _commitSharedStrings() {
4138
- if (this.sharedStringsXml === null || this.sharedStringsValues.length === this.sharedStringsOriginalLength) {
4605
+ var _a;
4606
+ if (this.sharedStringsXml === null || !this.sharedStringsDirty) {
4139
4607
  return;
4140
4608
  }
4141
4609
  let xml = this.sharedStringsXml;
4142
- const sstStart = xml.indexOf("<sst");
4143
- const sstTagEnd = xml.indexOf(">", sstStart);
4144
- if (sstStart !== -1 && sstTagEnd !== -1) {
4145
- const tag = xml.substring(sstStart, sstTagEnd + 1);
4146
- const newTag = tag.replace(/count="\d+"/, `count="${this.sharedStringsCount}"`).replace(/uniqueCount="\d+"/, `uniqueCount="${this.sharedStringsValues.length}"`);
4147
- xml = xml.substring(0, sstStart) + newTag + xml.substring(sstTagEnd + 1);
4148
- }
4149
- const sstEnd = xml.lastIndexOf("</sst>");
4150
- if (sstEnd === -1) {
4151
- return;
4152
- }
4153
- let appended = "";
4154
- for (let i = this.sharedStringsOriginalLength; i < this.sharedStringsValues.length; i++) {
4155
- const txt = this.sharedStringsValues[i];
4610
+ const sstMatch = /<(?<prefix>[A-Za-z_][\w.-]*:)?sst\b[^>]*>/i.exec(xml);
4611
+ if (!sstMatch || sstMatch.index === void 0) {
4612
+ throw new Error("XlsxUpdater: sharedStrings.xml has no sst element.");
4613
+ }
4614
+ const tag = sstMatch[0];
4615
+ const prefix = ((_a = sstMatch.groups) == null ? void 0 : _a.prefix) ?? "";
4616
+ const newTag = _XlsxUpdater._replaceOrAddXmlAttribute(
4617
+ _XlsxUpdater._replaceOrAddXmlAttribute(tag, "count", String(this.sharedStringsCount)),
4618
+ "uniqueCount",
4619
+ String(this.sharedStringsValues.length)
4620
+ );
4621
+ xml = xml.substring(0, sstMatch.index) + newTag + xml.substring(sstMatch.index + tag.length);
4622
+ const closingRe = new RegExp(`</${prefix}sst\\s*>\\s*$`, "i");
4623
+ const closingMatch = closingRe.exec(xml);
4624
+ if (!closingMatch || closingMatch.index === void 0) {
4625
+ throw new Error("XlsxUpdater: sharedStrings.xml has no closing sst element.");
4626
+ }
4627
+ const appended = this.sharedStringsPending.map((txt) => {
4156
4628
  const clean = escapeXmlText(txt);
4157
4629
  const preserve = clean.length > 0 && (clean[0] === " " || clean[clean.length - 1] === " " || /[\t\n\r]/.test(clean));
4158
- appended += preserve ? `<si><t xml:space="preserve">${clean}</t></si>` : `<si><t>${clean}</t></si>`;
4159
- }
4160
- xml = xml.substring(0, sstEnd) + appended + xml.substring(sstEnd);
4630
+ const tAttr = preserve ? ' xml:space="preserve"' : "";
4631
+ return `<${prefix}si><${prefix}t${tAttr}>${clean}</${prefix}t></${prefix}si>`;
4632
+ }).join("");
4633
+ xml = xml.substring(0, closingMatch.index) + appended + xml.substring(closingMatch.index);
4161
4634
  this.zip.updateFile("xl/sharedStrings.xml", Buffer.from(xml, "utf8"));
4635
+ this.sharedStringsXml = xml;
4636
+ this.sharedStringsOriginalLength = this.sharedStringsValues.length;
4637
+ this.sharedStringsPending = [];
4638
+ this.sharedStringsDirty = false;
4162
4639
  }
4163
4640
  // ------------------------------------------------------------------
4164
4641
  // Styles
@@ -4309,10 +4786,25 @@ var XlsxUpdater = class {
4309
4786
  index = this.sharedStringsValues.length;
4310
4787
  this.sharedStringsValues.push(text);
4311
4788
  this.stringIndexMap.set(text, index);
4789
+ this.sharedStringsPending.push(text);
4312
4790
  }
4313
4791
  this.sharedStringsCount++;
4792
+ this.sharedStringsDirty = true;
4314
4793
  return `<c r="${colRef}${rowNum}" t="s"${styleAttr}><v>${index}</v></c>`;
4315
4794
  }
4795
+ static _replaceOrAddXmlAttribute(tag, name, value) {
4796
+ const escaped = escapeXmlText(value);
4797
+ const attribute = new RegExp(`(\\s${name}\\s*=\\s*["'])[^"']*(["'])`, "i");
4798
+ if (attribute.test(tag)) {
4799
+ return tag.replace(attribute, `$1${escaped}$2`);
4800
+ }
4801
+ const insertion = ` ${name}="${escaped}"`;
4802
+ return tag.endsWith("/>") ? `${tag.slice(0, -2)}${insertion}/>` : `${tag.slice(0, -1)}${insertion}>`;
4803
+ }
4804
+ _sharedStringRefCount(sheetXml) {
4805
+ const cellTags = sheetXml.match(/<(?:(?:[A-Za-z_][\w.-]*):)?c\b[^>]*>/gi) ?? [];
4806
+ return cellTags.filter((tag) => /(?:^|\s)t\s*=\s*["']s["']/i.test(tag)).length;
4807
+ }
4316
4808
  // ------------------------------------------------------------------
4317
4809
  // Worksheet XML patching
4318
4810
  // ------------------------------------------------------------------
@@ -4354,6 +4846,109 @@ var XlsxUpdater = class {
4354
4846
  }
4355
4847
  return xml;
4356
4848
  }
4849
+ _patchSheetFile(sheetXml, rowsPath, outputPath, dimensionRef) {
4850
+ var _a, _b, _c, _d;
4851
+ const source = Buffer.from(sheetXml, "utf8");
4852
+ const text = sheetXml;
4853
+ const replacements = [];
4854
+ const emptySheetData = /<(?<prefix>[A-Za-z_][\w.-]*:)?sheetData\b(?<attrs>[^>]*)\/>/i.exec(text);
4855
+ if (emptySheetData && emptySheetData.index !== void 0) {
4856
+ const prefix = ((_a = emptySheetData.groups) == null ? void 0 : _a.prefix) ?? "";
4857
+ replacements.push({
4858
+ start: emptySheetData.index,
4859
+ end: emptySheetData.index + emptySheetData[0].length,
4860
+ data: Buffer.from(`<${prefix}sheetData${((_b = emptySheetData.groups) == null ? void 0 : _b.attrs) ?? ""}>`, "utf8"),
4861
+ rows: true,
4862
+ suffix: Buffer.from(`</${prefix}sheetData>`, "utf8")
4863
+ });
4864
+ } else {
4865
+ const opening = /<(?<prefix>[A-Za-z_][\w.-]*:)?sheetData\b[^>]*>/i.exec(text);
4866
+ if (!opening || opening.index === void 0) {
4867
+ throw new Error("XlsxUpdater: <sheetData> element not found in worksheet XML.");
4868
+ }
4869
+ const prefix = ((_c = opening.groups) == null ? void 0 : _c.prefix) ?? "";
4870
+ const closeRe = new RegExp(`</${prefix}sheetData\\s*>`, "i");
4871
+ const closing = closeRe.exec(text.slice(opening.index + opening[0].length));
4872
+ if (!closing || closing.index === void 0) {
4873
+ throw new Error("XlsxUpdater: <sheetData> element not closed in worksheet XML.");
4874
+ }
4875
+ const closingStart = opening.index + opening[0].length + closing.index;
4876
+ replacements.push({
4877
+ start: opening.index + opening[0].length,
4878
+ end: closingStart,
4879
+ rows: true
4880
+ });
4881
+ }
4882
+ const dimension = /<(?<prefix>[A-Za-z_][\w.-]*:)?dimension\b[^>]*>/i.exec(text);
4883
+ if (dimension && dimension.index !== void 0) {
4884
+ replacements.push({
4885
+ start: dimension.index,
4886
+ end: dimension.index + dimension[0].length,
4887
+ data: Buffer.from(
4888
+ _XlsxUpdater._replaceOrAddXmlAttribute(dimension[0], "ref", dimensionRef)
4889
+ )
4890
+ });
4891
+ } else {
4892
+ const root = /<(?<prefix>[A-Za-z_][\w.-]*:)?worksheet\b[^>]*>/i.exec(text);
4893
+ if (root && root.index !== void 0) {
4894
+ const prefix = ((_d = root.groups) == null ? void 0 : _d.prefix) ?? "";
4895
+ const insertAt = root.index + root[0].length;
4896
+ replacements.push({
4897
+ start: insertAt,
4898
+ end: insertAt,
4899
+ data: Buffer.from(`<${prefix}dimension ref="${dimensionRef}"/>`, "utf8")
4900
+ });
4901
+ }
4902
+ }
4903
+ const autoFilter = /<(?<prefix>[A-Za-z_][\w.-]*:)?autoFilter\b[^>]*>/i.exec(text);
4904
+ if (autoFilter && autoFilter.index !== void 0) {
4905
+ replacements.push({
4906
+ start: autoFilter.index,
4907
+ end: autoFilter.index + autoFilter[0].length,
4908
+ data: Buffer.from(
4909
+ _XlsxUpdater._replaceOrAddXmlAttribute(autoFilter[0], "ref", dimensionRef),
4910
+ "utf8"
4911
+ )
4912
+ });
4913
+ }
4914
+ replacements.sort((a, b) => a.start - b.start || a.end - b.end);
4915
+ const fd = fs5.openSync(outputPath, "w");
4916
+ let cursor = 0;
4917
+ const writeBuffer = (data) => {
4918
+ fs5.writeSync(fd, data);
4919
+ };
4920
+ const copyRows = () => {
4921
+ const rowsFd = fs5.openSync(rowsPath, "r");
4922
+ try {
4923
+ const chunk = Buffer.alloc(1024 * 1024);
4924
+ const length = fs5.statSync(rowsPath).size;
4925
+ let position = 0;
4926
+ while (position < length) {
4927
+ const count = fs5.readSync(rowsFd, chunk, 0, Math.min(chunk.length, length - position), position);
4928
+ if (count <= 0) throw new Error("XlsxUpdater: failed to read staged rows.");
4929
+ fs5.writeSync(fd, chunk, 0, count);
4930
+ position += count;
4931
+ }
4932
+ } finally {
4933
+ fs5.closeSync(rowsFd);
4934
+ }
4935
+ };
4936
+ try {
4937
+ for (const replacement of replacements) {
4938
+ const startByte = Buffer.byteLength(text.slice(0, replacement.start), "utf8");
4939
+ const endByte = Buffer.byteLength(text.slice(0, replacement.end), "utf8");
4940
+ if (startByte < cursor) throw new Error("XlsxUpdater: overlapping worksheet replacements.");
4941
+ writeBuffer(source.subarray(cursor, startByte));
4942
+ if (replacement.data) writeBuffer(replacement.data);
4943
+ if (replacement.rows) copyRows();
4944
+ if (replacement.suffix) writeBuffer(replacement.suffix);
4945
+ cursor = endByte;
4946
+ }
4947
+ writeBuffer(source.subarray(cursor));
4948
+ } finally {
4949
+ fs5.closeSync(fd);
4950
+ }
4951
+ }
4357
4952
  // ------------------------------------------------------------------
4358
4953
  // Pivot table integration
4359
4954
  // ------------------------------------------------------------------
@@ -4382,8 +4977,8 @@ var XlsxUpdater = class {
4382
4977
  }
4383
4978
  }
4384
4979
  _addRefreshOnLoadToPivotTables() {
4385
- for (const path2 of this.pivotTableDefPaths) {
4386
- const entry = this.zip.getEntry(path2);
4980
+ for (const path7 of this.pivotTableDefPaths) {
4981
+ const entry = this.zip.getEntry(path7);
4387
4982
  if (!entry) continue;
4388
4983
  let xml = entry.getData().toString("utf8");
4389
4984
  const rootRe = /<pivotTableDefinition\b[^>]*>/;
@@ -4391,13 +4986,15 @@ var XlsxUpdater = class {
4391
4986
  if (!rootMatch || rootMatch[0].includes("refreshOnLoad")) continue;
4392
4987
  const newTag = rootMatch[0].replace(/(\/?>)$/, ' refreshOnLoad="1"$1');
4393
4988
  xml = xml.substring(0, rootMatch.index) + newTag + xml.substring(rootMatch.index + rootMatch[0].length);
4394
- this.zip.updateFile(path2, Buffer.from(xml, "utf8"));
4989
+ this.zip.updateFile(path7, Buffer.from(xml, "utf8"));
4395
4990
  }
4396
4991
  }
4397
4992
  };
4398
4993
 
4399
4994
  // src/XlsbUpdater.ts
4400
- var fs4 = __toESM(require("fs"));
4995
+ var fs6 = __toESM(require("fs"));
4996
+ var os2 = __toESM(require("os"));
4997
+ var path5 = __toESM(require("path"));
4401
4998
  var import_adm_zip3 = __toESM(require("adm-zip"));
4402
4999
 
4403
5000
  // src/biff12Utils.ts
@@ -4476,7 +5073,9 @@ var XlsbUpdater = class {
4476
5073
  /**
4477
5074
  * @param path Path to an existing `.xlsb` file.
4478
5075
  */
4479
- constructor(path2) {
5076
+ constructor(filePath) {
5077
+ this.temporaryDirectory = null;
5078
+ this.stagedParts = /* @__PURE__ */ new Map();
4480
5079
  this.sheetNameToPath = /* @__PURE__ */ new Map();
4481
5080
  this.pivotCacheDefPaths = [];
4482
5081
  this.sharedStringsBuf = null;
@@ -4485,16 +5084,17 @@ var XlsbUpdater = class {
4485
5084
  this.sharedStringsUnique = 0;
4486
5085
  this.sharedStringsEndSst = 0;
4487
5086
  this.stringIndexMap = /* @__PURE__ */ new Map();
5087
+ this.sharedStringsDirty = false;
4488
5088
  // ------------------------------------------------------------------
4489
5089
  // Shared strings
4490
5090
  // ------------------------------------------------------------------
4491
5091
  this._appendCount = 0;
4492
- this.sourcePath = path2;
5092
+ this.sourcePath = filePath;
4493
5093
  this._oaEpoch = Date.UTC(1899, 11, 30);
4494
- if (!fs4.existsSync(path2)) {
4495
- throw new Error(`XlsbUpdater: file not found: ${path2}`);
5094
+ if (!fs6.existsSync(filePath)) {
5095
+ throw new Error(`XlsbUpdater: file not found: ${filePath}`);
4496
5096
  }
4497
- this.zip = new import_adm_zip3.default(path2);
5097
+ this.zip = new import_adm_zip3.default(filePath);
4498
5098
  if (!this.zip.getEntry("xl/workbook.bin")) {
4499
5099
  throw new Error(
4500
5100
  "XlsbUpdater: not an XLSB workbook (xl/workbook.bin missing). Convert the file to XLSB first."
@@ -4525,8 +5125,15 @@ var XlsbUpdater = class {
4525
5125
  if (!entry) {
4526
5126
  throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
4527
5127
  }
4528
- const sheetBuf = entry.getData();
5128
+ const sheetBuf = this._entryData(sheetPath);
4529
5129
  this._ensureSharedStringsLoaded();
5130
+ if (this.sharedStringsBuf !== null) {
5131
+ this.sharedStringsTotal = Math.max(
5132
+ 0,
5133
+ this.sharedStringsTotal - this._sharedStringRefCount(sheetBuf)
5134
+ );
5135
+ this.sharedStringsDirty = true;
5136
+ }
4530
5137
  let dataStyles = /* @__PURE__ */ new Map();
4531
5138
  let headerStyles = /* @__PURE__ */ new Map();
4532
5139
  let dateXf = null;
@@ -4555,25 +5162,310 @@ var XlsbUpdater = class {
4555
5162
  }
4556
5163
  this._patchDimension(newSheet, lastRowIdx, lastCol);
4557
5164
  this.zip.updateFile(sheetPath, newSheet);
4558
- if (this.sharedStringsBuf !== null && this._appendCount > 0) {
5165
+ this._discardStagedPart(sheetPath);
5166
+ if (this.sharedStringsBuf !== null) {
4559
5167
  this._commitSharedStrings();
4560
5168
  }
4561
5169
  const rwLast = Math.max(0, rows.length + (headers ? 1 : 0) - 1);
4562
5170
  const colLast = lastCol;
4563
5171
  this._patchPivotCaches(sheetName, rwLast, colLast);
4564
5172
  }
5173
+ /** Replace worksheet rows from a one-pass source. */
5174
+ async replaceSheetDataStream(sheetName, rows, options = {}) {
5175
+ const { headers, styleFallback = "inherit" } = options;
5176
+ if (styleFallback !== "inherit" && styleFallback !== "general") {
5177
+ throw new Error("XlsbUpdater: styleFallback must be 'inherit' or 'general'.");
5178
+ }
5179
+ const sheetPath = this.sheetNameToPath.get(sheetName);
5180
+ if (!sheetPath) {
5181
+ throw new Error(`XlsbUpdater: sheet "${sheetName}" not found in the workbook.`);
5182
+ }
5183
+ const entry = this.zip.getEntry(sheetPath);
5184
+ if (!entry) {
5185
+ throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
5186
+ }
5187
+ const oldStage = this.stagedParts.get(sheetPath);
5188
+ const beforeValuesLength = this.sharedStringsValues.length;
5189
+ const beforeAppendCount = this._appendCount;
5190
+ const beforeSharedStringsTotal = this.sharedStringsTotal;
5191
+ const beforeSharedStringsUnique = this.sharedStringsUnique;
5192
+ const beforeSharedStringsEndSst = this.sharedStringsEndSst;
5193
+ const beforeSharedStringsDirty = this.sharedStringsDirty;
5194
+ const beforeStringIndexMap = new Map(this.stringIndexMap);
5195
+ const sharedStringsEntry = this.zip.getEntry("xl/sharedStrings.bin");
5196
+ const beforeSharedStringsBytes = sharedStringsEntry || this.stagedParts.has("xl/sharedStrings.bin") ? Buffer.from(this._entryData("xl/sharedStrings.bin")) : null;
5197
+ const beforeSharedStringsBuf = this.sharedStringsBuf ? Buffer.from(this.sharedStringsBuf) : null;
5198
+ const oldSheet = this._entryData(sheetPath);
5199
+ const rowsPath = this._temporaryPath(".rows");
5200
+ const outputPath = this._temporaryPath(".bin");
5201
+ const createdParts = [rowsPath, outputPath];
5202
+ let output = null;
5203
+ try {
5204
+ this._ensureSharedStringsLoaded();
5205
+ if (this.sharedStringsBuf !== null) {
5206
+ this.sharedStringsTotal = Math.max(
5207
+ 0,
5208
+ this.sharedStringsTotal - this._sharedStringRefCount(oldSheet)
5209
+ );
5210
+ this.sharedStringsDirty = true;
5211
+ }
5212
+ let dataStyles = /* @__PURE__ */ new Map();
5213
+ let headerStyles = /* @__PURE__ */ new Map();
5214
+ let dateXf = null;
5215
+ if (styleFallback !== "general") {
5216
+ const collected = this._collectStyles(oldSheet);
5217
+ dataStyles = collected.dataStyles;
5218
+ headerStyles = collected.headerStyles;
5219
+ dateXf = this._findDateXf();
5220
+ }
5221
+ const rowOutput = fs6.createWriteStream(rowsPath);
5222
+ output = rowOutput;
5223
+ let outputError = null;
5224
+ rowOutput.on("error", (error) => {
5225
+ outputError = error;
5226
+ });
5227
+ let writtenBytes = 0;
5228
+ let lastKeptEnd = 0;
5229
+ let width = (headers == null ? void 0 : headers.length) ?? 0;
5230
+ let dataRowsSeen = 0;
5231
+ let keptDataRows = 0;
5232
+ let pendingEmptyWidth = 0;
5233
+ let nextRow = 0;
5234
+ const rowHeaderOffsets = [];
5235
+ const write = async (data) => {
5236
+ if (outputError) throw outputError;
5237
+ writtenBytes += data.length;
5238
+ if (rowOutput.write(data)) return;
5239
+ await new Promise((resolve4, reject) => {
5240
+ const onDrain = () => {
5241
+ cleanup();
5242
+ resolve4();
5243
+ };
5244
+ const onError = (error) => {
5245
+ cleanup();
5246
+ reject(error);
5247
+ };
5248
+ const cleanup = () => {
5249
+ rowOutput.off("drain", onDrain);
5250
+ rowOutput.off("error", onError);
5251
+ };
5252
+ rowOutput.once("drain", onDrain);
5253
+ rowOutput.once("error", onError);
5254
+ });
5255
+ };
5256
+ const writeRow = async (rowNumber, cells) => {
5257
+ rowHeaderOffsets.push(writtenBytes);
5258
+ const header = Buffer.alloc(27);
5259
+ header[0] = 0;
5260
+ header[1] = 25;
5261
+ header.writeInt32LE(rowNumber, 2);
5262
+ header[10] = 44;
5263
+ header[11] = 1;
5264
+ header[15] = 1;
5265
+ header.writeInt32LE(0, 19);
5266
+ header.writeInt32LE(0, 23);
5267
+ await write(header);
5268
+ for (const cell of cells) await write(cell);
5269
+ };
5270
+ if (headers !== void 0) {
5271
+ await writeRow(0, headers.map((value, column) => this._stringCellBytes(column, styleFallback === "general" ? 0 : headerStyles.get(column) ?? 0, value)));
5272
+ lastKeptEnd = writtenBytes;
5273
+ nextRow = 1;
5274
+ }
5275
+ for await (const sourceRow of rows) {
5276
+ if (typeof sourceRow === "string" || sourceRow instanceof Uint8Array) {
5277
+ throw new TypeError("Each row must be an iterable of cells, not text");
5278
+ }
5279
+ const row = Array.from(sourceRow);
5280
+ const cells = row.map((value, column) => value === null || value === void 0 ? null : this._valueCellBytes(value, column, dataStyles, dateXf)).filter((cell) => cell !== null);
5281
+ await writeRow(nextRow, cells);
5282
+ dataRowsSeen++;
5283
+ nextRow++;
5284
+ if (row.every((value) => value === null || value === void 0)) {
5285
+ pendingEmptyWidth = Math.max(pendingEmptyWidth, row.length);
5286
+ } else {
5287
+ width = Math.max(width, pendingEmptyWidth, row.length);
5288
+ pendingEmptyWidth = 0;
5289
+ keptDataRows = dataRowsSeen;
5290
+ lastKeptEnd = writtenBytes;
5291
+ }
5292
+ }
5293
+ await new Promise((resolve4, reject) => {
5294
+ rowOutput.once("close", resolve4);
5295
+ rowOutput.once("error", reject);
5296
+ rowOutput.end();
5297
+ });
5298
+ if (outputError) throw outputError;
5299
+ const rowsFd = fs6.openSync(rowsPath, "r+");
5300
+ try {
5301
+ fs6.ftruncateSync(rowsFd, lastKeptEnd);
5302
+ } finally {
5303
+ fs6.closeSync(rowsFd);
5304
+ }
5305
+ const lastCol = Math.max(0, width - 1);
5306
+ const rowPatchFd = fs6.openSync(rowsPath, "r+");
5307
+ try {
5308
+ const lastColBuffer = Buffer.alloc(4);
5309
+ lastColBuffer.writeInt32LE(lastCol, 0);
5310
+ for (const offset of rowHeaderOffsets) {
5311
+ if (offset >= lastKeptEnd) continue;
5312
+ fs6.writeSync(rowPatchFd, lastColBuffer, 0, lastColBuffer.length, offset + 23);
5313
+ }
5314
+ } finally {
5315
+ fs6.closeSync(rowPatchFd);
5316
+ }
5317
+ const { rowsStart, rowsEnd, autoFilterRanges } = this._findRowsRegion(oldSheet);
5318
+ const oldLength = oldSheet.length;
5319
+ const newRowsLength = fs6.statSync(rowsPath).size;
5320
+ const delta = newRowsLength - (rowsEnd - rowsStart);
5321
+ const outputFd = fs6.openSync(outputPath, "w");
5322
+ try {
5323
+ fs6.writeSync(outputFd, oldSheet, 0, rowsStart, null);
5324
+ const rowsFdRead = fs6.openSync(rowsPath, "r");
5325
+ try {
5326
+ const chunk = Buffer.alloc(1024 * 1024);
5327
+ let position = 0;
5328
+ while (position < newRowsLength) {
5329
+ const count = fs6.readSync(rowsFdRead, chunk, 0, Math.min(chunk.length, newRowsLength - position), position);
5330
+ if (count <= 0) throw new Error("XlsbUpdater: failed to read staged rows.");
5331
+ fs6.writeSync(outputFd, chunk, 0, count);
5332
+ position += count;
5333
+ }
5334
+ } finally {
5335
+ fs6.closeSync(rowsFdRead);
5336
+ }
5337
+ fs6.writeSync(outputFd, oldSheet, rowsEnd, oldLength - rowsEnd, rowsStart + newRowsLength);
5338
+ } finally {
5339
+ fs6.closeSync(outputFd);
5340
+ }
5341
+ this._patchSheetMetadataFile(
5342
+ outputPath,
5343
+ oldSheet,
5344
+ rowsEnd,
5345
+ delta,
5346
+ autoFilterRanges,
5347
+ Math.max(0, keptDataRows + (headers !== void 0 ? 1 : 0) - 1),
5348
+ lastCol
5349
+ );
5350
+ this._stagePart(sheetPath, outputPath);
5351
+ this._commitSharedStrings();
5352
+ this._patchPivotCaches(
5353
+ sheetName,
5354
+ Math.max(0, keptDataRows + (headers !== void 0 ? 1 : 0) - 1),
5355
+ lastCol
5356
+ );
5357
+ if (oldStage && oldStage !== outputPath) this._removeTemporaryFile(oldStage);
5358
+ createdParts.splice(createdParts.indexOf(outputPath), 1);
5359
+ } catch (error) {
5360
+ this.sharedStringsValues.length = beforeValuesLength;
5361
+ this._appendCount = beforeAppendCount;
5362
+ this.sharedStringsTotal = beforeSharedStringsTotal;
5363
+ this.sharedStringsUnique = beforeSharedStringsUnique;
5364
+ this.sharedStringsEndSst = beforeSharedStringsEndSst;
5365
+ this.sharedStringsDirty = beforeSharedStringsDirty;
5366
+ this.sharedStringsBuf = beforeSharedStringsBuf;
5367
+ this.stringIndexMap.clear();
5368
+ for (const [value, index] of beforeStringIndexMap) this.stringIndexMap.set(value, index);
5369
+ if (beforeSharedStringsBytes) {
5370
+ this.zip.updateFile("xl/sharedStrings.bin", beforeSharedStringsBytes);
5371
+ }
5372
+ if (oldStage) this.stagedParts.set(sheetPath, oldStage);
5373
+ else this.stagedParts.delete(sheetPath);
5374
+ throw error;
5375
+ } finally {
5376
+ if (output && !output.closed) output.destroy();
5377
+ for (const temporaryPath of createdParts) this._removeTemporaryFile(temporaryPath);
5378
+ }
5379
+ }
4565
5380
  /**
4566
5381
  * Write the updated workbook to disk.
4567
5382
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
4568
5383
  */
4569
5384
  save(outputPath) {
4570
5385
  const target = outputPath ?? this.sourcePath;
4571
- fs4.writeFileSync(target, this.zip.toBuffer());
5386
+ this._materializeStagedParts();
5387
+ writeBufferAtomically(this.zip.toBuffer(), target);
5388
+ }
5389
+ /** Save staged streaming replacements without materialising the ZIP. */
5390
+ async saveStreaming(outputPath) {
5391
+ const target = outputPath ?? this.sourcePath;
5392
+ await writeAdmZipStreaming(this.zip, this.stagedParts, target);
5393
+ this._materializeStagedParts();
4572
5394
  }
4573
5395
  /** The updated workbook as an in-memory ZIP buffer. */
4574
5396
  toBuffer() {
5397
+ this._materializeStagedParts();
4575
5398
  return this.zip.toBuffer();
4576
5399
  }
5400
+ /**
5401
+ * Release the staging directory and any unsaved staged parts.
5402
+ * The updater remains usable; a new staging directory is created lazily
5403
+ * on the next streaming replacement.
5404
+ */
5405
+ dispose() {
5406
+ for (const stagedPath of this.stagedParts.values()) {
5407
+ try {
5408
+ fs6.rmSync(stagedPath, { force: true });
5409
+ } catch {
5410
+ }
5411
+ }
5412
+ this.stagedParts.clear();
5413
+ if (this.temporaryDirectory !== null) {
5414
+ try {
5415
+ fs6.rmSync(this.temporaryDirectory, { recursive: true, force: true });
5416
+ } catch {
5417
+ }
5418
+ this.temporaryDirectory = null;
5419
+ }
5420
+ }
5421
+ /** Support `using` declarations for automatic staging cleanup. */
5422
+ [Symbol.dispose]() {
5423
+ this.dispose();
5424
+ }
5425
+ _ensureTemporaryDirectory() {
5426
+ if (this.temporaryDirectory === null) {
5427
+ this.temporaryDirectory = fs6.mkdtempSync(path5.join(os2.tmpdir(), "spreadsheet-updater-"));
5428
+ } else {
5429
+ try {
5430
+ fs6.mkdirSync(this.temporaryDirectory, { recursive: true });
5431
+ } catch {
5432
+ }
5433
+ }
5434
+ return this.temporaryDirectory;
5435
+ }
5436
+ _temporaryPath(suffix) {
5437
+ return path5.join(this._ensureTemporaryDirectory(), `part-${Date.now()}-${Math.random().toString(16).slice(2)}${suffix}`);
5438
+ }
5439
+ _removeTemporaryFile(filePath) {
5440
+ try {
5441
+ fs6.rmSync(filePath);
5442
+ } catch (error) {
5443
+ if (error.code !== "ENOENT") throw error;
5444
+ }
5445
+ }
5446
+ _entryData(name) {
5447
+ const stagedPath = this.stagedParts.get(name);
5448
+ if (stagedPath) return fs6.readFileSync(stagedPath);
5449
+ const entry = this.zip.getEntry(name);
5450
+ if (!entry) throw new Error(`XlsbUpdater: ZIP member missing: ${name}`);
5451
+ return entry.getData();
5452
+ }
5453
+ _stagePart(name, filePath) {
5454
+ this.stagedParts.set(name, filePath);
5455
+ }
5456
+ _discardStagedPart(name) {
5457
+ const stagedPath = this.stagedParts.get(name);
5458
+ if (!stagedPath) return;
5459
+ this._removeTemporaryFile(stagedPath);
5460
+ this.stagedParts.delete(name);
5461
+ }
5462
+ _materializeStagedParts() {
5463
+ for (const [name, stagedPath] of this.stagedParts) {
5464
+ this.zip.updateFile(name, fs6.readFileSync(stagedPath));
5465
+ this._removeTemporaryFile(stagedPath);
5466
+ }
5467
+ this.stagedParts.clear();
5468
+ }
4577
5469
  // ------------------------------------------------------------------
4578
5470
  // Workbook structure
4579
5471
  // ------------------------------------------------------------------
@@ -4623,11 +5515,11 @@ var XlsbUpdater = class {
4623
5515
  return;
4624
5516
  }
4625
5517
  const entry = this.zip.getEntry("xl/sharedStrings.bin");
4626
- if (!entry) {
5518
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.bin")) {
4627
5519
  this.sharedStringsBuf = null;
4628
5520
  return;
4629
5521
  }
4630
- this.sharedStringsBuf = entry.getData();
5522
+ this.sharedStringsBuf = this._entryData("xl/sharedStrings.bin");
4631
5523
  const parsed = parseSharedStringsBin(this.sharedStringsBuf);
4632
5524
  this.sharedStringsValues = parsed.values;
4633
5525
  this.sharedStringsTotal = parsed.total;
@@ -4639,9 +5531,10 @@ var XlsbUpdater = class {
4639
5531
  }
4640
5532
  });
4641
5533
  this._appendCount = 0;
5534
+ this.sharedStringsDirty = false;
4642
5535
  }
4643
5536
  _commitSharedStrings() {
4644
- if (this.sharedStringsBuf === null || this._appendCount === 0) return;
5537
+ if (this.sharedStringsBuf === null || !this.sharedStringsDirty) return;
4645
5538
  const originalValuesLength = this.sharedStringsValues.length - this._appendCount;
4646
5539
  let newItems = Buffer.alloc(0);
4647
5540
  for (let i = originalValuesLength; i < this.sharedStringsValues.length; i++) {
@@ -4667,6 +5560,9 @@ var XlsbUpdater = class {
4667
5560
  pos = rec.dataEnd;
4668
5561
  }
4669
5562
  this.zip.updateFile("xl/sharedStrings.bin", out);
5563
+ this.sharedStringsBuf = out;
5564
+ this._appendCount = 0;
5565
+ this.sharedStringsDirty = false;
4670
5566
  }
4671
5567
  // ------------------------------------------------------------------
4672
5568
  // Styles
@@ -4849,12 +5745,51 @@ var XlsbUpdater = class {
4849
5745
  this._appendCount++;
4850
5746
  }
4851
5747
  this.sharedStringsTotal++;
5748
+ this.sharedStringsDirty = true;
4852
5749
  const payload = Buffer.alloc(12);
4853
5750
  payload.writeUInt32LE(col, 0);
4854
5751
  payload.writeUInt32LE(style, 4);
4855
5752
  payload.writeUInt32LE(index, 8);
4856
5753
  return buildRecord(7, payload);
4857
5754
  }
5755
+ _sharedStringRefCount(sheetBuf) {
5756
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
5757
+ let pos = 0;
5758
+ let count = 0;
5759
+ while (readRecord(sheetBuf, pos, rec)) {
5760
+ if (rec.id === 7) count++;
5761
+ pos = rec.dataEnd;
5762
+ }
5763
+ return count;
5764
+ }
5765
+ _patchSheetMetadataFile(filePath, originalSheet, rowsEnd, delta, autoFilterRanges, lastRowIdx, lastCol) {
5766
+ const fd = fs6.openSync(filePath, "r+");
5767
+ try {
5768
+ const patchInt32 = (offset, value) => {
5769
+ const bytes = Buffer.alloc(4);
5770
+ bytes.writeInt32LE(value, 0);
5771
+ fs6.writeSync(fd, bytes, 0, bytes.length, offset);
5772
+ };
5773
+ for (const filterOffset of autoFilterRanges) {
5774
+ const newOffset = filterOffset >= rowsEnd ? filterOffset + delta : filterOffset;
5775
+ patchInt32(newOffset + 4, lastRowIdx);
5776
+ patchInt32(newOffset + 12, lastCol);
5777
+ }
5778
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
5779
+ let pos = 0;
5780
+ while (readRecord(originalSheet, pos, rec)) {
5781
+ if (rec.id === 152 && rec.len >= 36) {
5782
+ const shift = rec.dataStart >= rowsEnd ? delta : 0;
5783
+ patchInt32(rec.dataStart + shift + 24, lastRowIdx);
5784
+ patchInt32(rec.dataStart + shift + 32, lastCol);
5785
+ break;
5786
+ }
5787
+ pos = rec.dataEnd;
5788
+ }
5789
+ } finally {
5790
+ fs6.closeSync(fd);
5791
+ }
5792
+ }
4858
5793
  // ------------------------------------------------------------------
4859
5794
  // Rows region detection
4860
5795
  // ------------------------------------------------------------------
@@ -4906,8 +5841,8 @@ var XlsbUpdater = class {
4906
5841
  // Pivot cache integration
4907
5842
  // ------------------------------------------------------------------
4908
5843
  _patchPivotCaches(sheetName, rwLast, colLast) {
4909
- for (const path2 of this.pivotCacheDefPaths) {
4910
- const entry = this.zip.getEntry(path2);
5844
+ for (const path7 of this.pivotCacheDefPaths) {
5845
+ const entry = this.zip.getEntry(path7);
4911
5846
  if (!entry) continue;
4912
5847
  const buf = Buffer.from(entry.getData());
4913
5848
  const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
@@ -4939,11 +5874,188 @@ var XlsbUpdater = class {
4939
5874
  }
4940
5875
  }
4941
5876
  if (patched) {
4942
- this.zip.updateFile(path2, buf);
5877
+ this.zip.updateFile(path7, buf);
4943
5878
  }
4944
5879
  }
4945
5880
  }
4946
5881
  };
5882
+
5883
+ // src/ExcelConverter.ts
5884
+ var fs7 = __toESM(require("fs"));
5885
+ var path6 = __toESM(require("path"));
5886
+ var import_crypto3 = require("crypto");
5887
+ var import_child_process = require("child_process");
5888
+ var POWERSHELL_CONVERSION_SCRIPT = `
5889
+ $ErrorActionPreference = 'Stop'
5890
+ $inputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_INPUT')
5891
+ $outputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_OUTPUT')
5892
+ $fileFormat = [int][Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_FORMAT')
5893
+ $excel = $null
5894
+ $workbook = $null
5895
+ $exitCode = 0
5896
+
5897
+ try {
5898
+ $excel = New-Object -ComObject Excel.Application
5899
+ $excel.Visible = $false
5900
+ $excel.DisplayAlerts = $false
5901
+ $excel.ScreenUpdating = $false
5902
+
5903
+ # Open read-only so the source workbook is never changed by the conversion.
5904
+ $workbook = $excel.Workbooks.Open($inputPath, 0, $true)
5905
+ if ($null -eq $workbook) {
5906
+ throw 'Excel did not return a workbook when opening the source file.'
5907
+ }
5908
+
5909
+ $workbook.SaveAs($outputPath, $fileFormat)
5910
+ } catch {
5911
+ $exitCode = 1
5912
+ [Console]::Error.WriteLine($_.Exception.ToString())
5913
+ } finally {
5914
+ if ($null -ne $workbook) {
5915
+ try { $workbook.Close($false) } catch { }
5916
+ try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook) | Out-Null } catch { }
5917
+ }
5918
+ if ($null -ne $excel) {
5919
+ try { $excel.Quit() } catch { }
5920
+ try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null } catch { }
5921
+ }
5922
+ [GC]::Collect()
5923
+ [GC]::WaitForPendingFinalizers()
5924
+ }
5925
+
5926
+ exit $exitCode
5927
+ `;
5928
+ function errorMessage2(error) {
5929
+ return error instanceof Error ? error.message : String(error);
5930
+ }
5931
+ function validateConversionPaths(inputPath, outputPath, inputExtension, outputExtension, overwrite) {
5932
+ const input = path6.resolve(inputPath);
5933
+ const output = path6.resolve(outputPath);
5934
+ if (path6.extname(input).toLowerCase() !== inputExtension) {
5935
+ throw new Error(
5936
+ `Excel conversion: source must have the ${inputExtension} extension: ${inputPath}`
5937
+ );
5938
+ }
5939
+ if (path6.extname(output).toLowerCase() !== outputExtension) {
5940
+ throw new Error(
5941
+ `Excel conversion: destination must have the ${outputExtension} extension: ${outputPath}`
5942
+ );
5943
+ }
5944
+ if (!fs7.existsSync(input)) {
5945
+ throw new Error(`Excel conversion: source file not found: ${inputPath}`);
5946
+ }
5947
+ if (!fs7.statSync(input).isFile()) {
5948
+ throw new Error(`Excel conversion: source path is not a file: ${inputPath}`);
5949
+ }
5950
+ if (!fs7.existsSync(path6.dirname(output))) {
5951
+ throw new Error(`Excel conversion: destination directory not found: ${path6.dirname(output)}`);
5952
+ }
5953
+ if (fs7.existsSync(output)) {
5954
+ if (fs7.statSync(output).isDirectory()) {
5955
+ throw new Error(`Excel conversion: destination path is a directory: ${outputPath}`);
5956
+ }
5957
+ if (!overwrite) {
5958
+ throw new Error(
5959
+ `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${outputPath}`
5960
+ );
5961
+ }
5962
+ }
5963
+ if (process.platform === "win32" && input.toLowerCase() === output.toLowerCase()) {
5964
+ throw new Error("Excel conversion: source and destination must be different files.");
5965
+ }
5966
+ return { input, output };
5967
+ }
5968
+ function runPowerShellConversion(inputPath, outputPath, fileFormat) {
5969
+ const encodedCommand = Buffer.from(POWERSHELL_CONVERSION_SCRIPT, "utf16le").toString("base64");
5970
+ const environment = {
5971
+ ...process.env,
5972
+ JUSTYBASE_EXCEL_INPUT: inputPath,
5973
+ JUSTYBASE_EXCEL_OUTPUT: outputPath,
5974
+ JUSTYBASE_EXCEL_FORMAT: String(fileFormat)
5975
+ };
5976
+ return new Promise((resolve4, reject) => {
5977
+ var _a, _b, _c, _d;
5978
+ let stdout = "";
5979
+ let stderr = "";
5980
+ let settled = false;
5981
+ const child = (0, import_child_process.spawn)(
5982
+ "powershell.exe",
5983
+ [
5984
+ "-NoLogo",
5985
+ "-NoProfile",
5986
+ "-NonInteractive",
5987
+ "-ExecutionPolicy",
5988
+ "Bypass",
5989
+ "-EncodedCommand",
5990
+ encodedCommand
5991
+ ],
5992
+ { env: environment, windowsHide: true }
5993
+ );
5994
+ (_a = child.stdout) == null ? void 0 : _a.setEncoding("utf8");
5995
+ (_b = child.stderr) == null ? void 0 : _b.setEncoding("utf8");
5996
+ (_c = child.stdout) == null ? void 0 : _c.on("data", (chunk) => {
5997
+ stdout += chunk;
5998
+ });
5999
+ (_d = child.stderr) == null ? void 0 : _d.on("data", (chunk) => {
6000
+ stderr += chunk;
6001
+ });
6002
+ child.once("error", (error) => {
6003
+ if (settled) return;
6004
+ settled = true;
6005
+ reject(new Error(`Excel conversion: unable to start PowerShell/COM: ${error.message}`));
6006
+ });
6007
+ child.once("close", (code) => {
6008
+ if (settled) return;
6009
+ settled = true;
6010
+ if (code === 0) {
6011
+ resolve4();
6012
+ return;
6013
+ }
6014
+ const details = (stderr || stdout).replace(/\s+/g, " ").trim();
6015
+ reject(new Error(
6016
+ `Excel conversion failed (PowerShell exit code ${code ?? "unknown"})${details ? `: ${details}` : "."}`
6017
+ ));
6018
+ });
6019
+ });
6020
+ }
6021
+ async function convertExcelFile(inputPath, outputPath, inputExtension, outputExtension, fileFormat, options = {}) {
6022
+ const { input, output } = validateConversionPaths(
6023
+ inputPath,
6024
+ outputPath,
6025
+ inputExtension,
6026
+ outputExtension,
6027
+ options.overwrite === true
6028
+ );
6029
+ if (process.platform !== "win32") {
6030
+ throw new Error("Excel conversion requires Windows with desktop Microsoft Excel and COM automation.");
6031
+ }
6032
+ const temporaryOutput = path6.join(
6033
+ path6.dirname(output),
6034
+ `.${path6.basename(output, outputExtension)}.${(0, import_crypto3.randomUUID)()}.conversion${outputExtension}`
6035
+ );
6036
+ try {
6037
+ await runPowerShellConversion(input, temporaryOutput, fileFormat);
6038
+ if (!fs7.existsSync(temporaryOutput)) {
6039
+ throw new Error("Excel conversion completed without creating the destination file.");
6040
+ }
6041
+ installTemporaryOutput(temporaryOutput, output, options.overwrite === true);
6042
+ } catch (error) {
6043
+ throw new Error(`Excel conversion ${inputExtension} -> ${outputExtension}: ${errorMessage2(error)}`);
6044
+ } finally {
6045
+ if (fs7.existsSync(temporaryOutput)) {
6046
+ try {
6047
+ fs7.rmSync(temporaryOutput);
6048
+ } catch {
6049
+ }
6050
+ }
6051
+ }
6052
+ }
6053
+ function convertXlsbToXlsx(inputPath, outputPath, options) {
6054
+ return convertExcelFile(inputPath, outputPath, ".xlsb", ".xlsx", 51, options);
6055
+ }
6056
+ function convertXlsxToXlsb(inputPath, outputPath, options) {
6057
+ return convertExcelFile(inputPath, outputPath, ".xlsx", ".xlsb", 50, options);
6058
+ }
4947
6059
  // Annotate the CommonJS export names for ESM import in node:
4948
6060
  0 && (module.exports = {
4949
6061
  BiffReaderWriter,
@@ -4956,10 +6068,13 @@ var XlsbUpdater = class {
4956
6068
  XlsbReader,
4957
6069
  XlsbUpdater,
4958
6070
  XlsbWriter,
6071
+ XlsmUpdater,
4959
6072
  XlsxReader,
4960
6073
  XlsxUpdater,
4961
6074
  XlsxWriter,
4962
6075
  applyHeaderWidths,
6076
+ convertXlsbToXlsx,
6077
+ convertXlsxToXlsb,
4963
6078
  defaultColWidth,
4964
6079
  getFormat,
4965
6080
  initColWidths,