@justybase/spreadsheet-tasks 2.1.1 → 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,6 +40,7 @@ __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,
@@ -460,8 +461,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
460
461
  this.xfIdToNumFmtId = [];
461
462
  this.customDateFormats = null;
462
463
  }
463
- async open(path3, readSharedStrings = true) {
464
- this.zip = new import_adm_zip.default(path3);
464
+ async open(path7, readSharedStrings = true) {
465
+ this.zip = new import_adm_zip.default(path7);
465
466
  const wbEntry = this.zip.getEntry("xl/workbook.bin");
466
467
  if (wbEntry) {
467
468
  const buf = wbEntry.getData();
@@ -506,8 +507,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
506
507
  this.resultsCount = this.sheets.length;
507
508
  this._currentSheetIndex = -1;
508
509
  }
509
- _readSharedStrings(path3) {
510
- const entry = this.zip.getEntry(path3);
510
+ _readSharedStrings(path7) {
511
+ const entry = this.zip.getEntry(path7);
511
512
  if (!entry) return;
512
513
  const reader = new BiffReaderWriter(entry.getData());
513
514
  while (reader.readSharedStrings()) {
@@ -516,8 +517,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
516
517
  }
517
518
  }
518
519
  }
519
- _readStyles(path3) {
520
- const entry = this.zip.getEntry(path3);
520
+ _readStyles(path7) {
521
+ const entry = this.zip.getEntry(path7);
521
522
  if (!entry) return;
522
523
  const reader = new BiffReaderWriter(entry.getData());
523
524
  while (reader.readStyles()) {
@@ -640,7 +641,7 @@ function escapeXmlText(str) {
640
641
  }
641
642
  function unescapeXml(str) {
642
643
  if (!str || str.indexOf("&") === -1) return str;
643
- 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, "'");
644
645
  }
645
646
  function columnIndexToLetter(colIndex) {
646
647
  let n = colIndex + 1;
@@ -669,26 +670,17 @@ function trimTrailingEmptyRows(rows) {
669
670
  }
670
671
  function parseSharedStringsXml(xml) {
671
672
  const result = [];
672
- let pos = 0;
673
- while (true) {
674
- const siStart = xml.indexOf("<si>", pos);
675
- if (siStart === -1) break;
676
- const siEnd = xml.indexOf("</si>", siStart);
677
- if (siEnd === -1) break;
678
- const content = xml.substring(siStart, siEnd);
679
- let val = "";
680
- let tPos = 0;
681
- while (true) {
682
- const tStart = content.indexOf("<t", tPos);
683
- if (tStart === -1) break;
684
- const tagEnd = content.indexOf(">", tStart);
685
- const tEnd = content.indexOf("</t>", tagEnd);
686
- if (tEnd === -1) break;
687
- val += content.substring(tagEnd + 1, tEnd);
688
- tPos = tEnd + 4;
689
- }
690
- result.push(unescapeXml(val));
691
- 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));
692
684
  }
693
685
  return result;
694
686
  }
@@ -709,9 +701,9 @@ var XlsxReader = class extends ExcelReaderAbstract {
709
701
  this.xfIdToNumFmtId = [];
710
702
  this.customDateFormats = /* @__PURE__ */ new Set();
711
703
  }
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) => {
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) => {
715
707
  if (err) return reject(err);
716
708
  this.zipfile = zipfile;
717
709
  this.entries = /* @__PURE__ */ new Map();
@@ -759,7 +751,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
759
751
  }
760
752
  this.resultsCount = this.sheets.length;
761
753
  this._currentSheetIndex = -1;
762
- resolve2();
754
+ resolve4();
763
755
  } catch (e) {
764
756
  reject(e);
765
757
  }
@@ -773,16 +765,16 @@ var XlsxReader = class extends ExcelReaderAbstract {
773
765
  this.zipfile.close();
774
766
  }
775
767
  }
776
- async _readZipEntryContent(path3) {
777
- const entry = this.entries.get(path3);
768
+ async _readZipEntryContent(path7) {
769
+ const entry = this.entries.get(path7);
778
770
  if (!entry) return null;
779
- return new Promise((resolve2, reject) => {
771
+ return new Promise((resolve4, reject) => {
780
772
  this.zipfile.openReadStream(entry, (err, readStream) => {
781
773
  if (err) return reject(err);
782
774
  const chunks = [];
783
775
  readStream.on("data", (chunk) => chunks.push(chunk));
784
776
  readStream.on("end", () => {
785
- resolve2(Buffer.concat(chunks).toString("utf8"));
777
+ resolve4(Buffer.concat(chunks).toString("utf8"));
786
778
  });
787
779
  readStream.on("error", reject);
788
780
  });
@@ -3388,7 +3380,7 @@ var XlsbWriter = class _XlsbWriter {
3388
3380
  * Must be awaited before the file is fully written.
3389
3381
  */
3390
3382
  finalize() {
3391
- return new Promise((resolve2, reject) => {
3383
+ return new Promise((resolve4, reject) => {
3392
3384
  try {
3393
3385
  this.saveSst();
3394
3386
  this.archive.append(this._buildStylesBin(), { name: "xl/styles.bin" });
@@ -3486,7 +3478,7 @@ var XlsbWriter = class _XlsbWriter {
3486
3478
  </Relationships>`;
3487
3479
  this.archive.append(wsRels, { name: `xl/worksheets/_rels/${sheet.nameInArchive}.rels` });
3488
3480
  }
3489
- this.output.on("close", () => resolve2());
3481
+ this.output.on("close", () => resolve4());
3490
3482
  this.archive.on("error", (err) => reject(err));
3491
3483
  this.archive.finalize();
3492
3484
  } catch (err) {
@@ -3834,7 +3826,7 @@ var XlsxWriter = class {
3834
3826
  }
3835
3827
  /** Finalize the ZIP package and close the output stream. */
3836
3828
  finalize() {
3837
- return new Promise((resolve2, reject) => {
3829
+ return new Promise((resolve4, reject) => {
3838
3830
  try {
3839
3831
  this._writeSharedStrings();
3840
3832
  this._writeStyles();
@@ -3842,7 +3834,7 @@ var XlsxWriter = class {
3842
3834
  this._writeContentTypes();
3843
3835
  this._writeRels();
3844
3836
  this.output.on("close", () => {
3845
- resolve2();
3837
+ resolve4();
3846
3838
  });
3847
3839
  this.archive.on("error", (err) => {
3848
3840
  reject(err);
@@ -3979,13 +3971,237 @@ ${xfEntries.join("\n")}
3979
3971
  };
3980
3972
 
3981
3973
  // src/XlsxUpdater.ts
3982
- var fs3 = __toESM(require("fs"));
3974
+ var fs5 = __toESM(require("fs"));
3975
+ var os = __toESM(require("os"));
3976
+ var path4 = __toESM(require("path"));
3983
3977
  var import_adm_zip2 = __toESM(require("adm-zip"));
3984
- 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 {
3985
4199
  /**
3986
4200
  * @param path Path to an existing `.xlsx` file.
3987
4201
  */
3988
- constructor(path3) {
4202
+ constructor(filePath) {
4203
+ this.temporaryDirectory = null;
4204
+ this.stagedParts = /* @__PURE__ */ new Map();
3989
4205
  this.sheetNameToPath = /* @__PURE__ */ new Map();
3990
4206
  this.pivotCacheDefs = [];
3991
4207
  this.pivotTableDefPaths = [];
@@ -3993,13 +4209,15 @@ var XlsxUpdater = class {
3993
4209
  this.sharedStringsValues = [];
3994
4210
  this.sharedStringsOriginalLength = 0;
3995
4211
  this.sharedStringsCount = 0;
4212
+ this.sharedStringsPending = [];
4213
+ this.sharedStringsDirty = false;
3996
4214
  this.stringIndexMap = /* @__PURE__ */ new Map();
3997
- this.sourcePath = path3;
4215
+ this.sourcePath = filePath;
3998
4216
  this._oaEpoch = Date.UTC(1899, 11, 30);
3999
- if (!fs3.existsSync(path3)) {
4000
- throw new Error(`XlsxUpdater: file not found: ${path3}`);
4217
+ if (!fs5.existsSync(filePath)) {
4218
+ throw new Error(`XlsxUpdater: file not found: ${filePath}`);
4001
4219
  }
4002
- this.zip = new import_adm_zip2.default(path3);
4220
+ this.zip = new import_adm_zip2.default(filePath);
4003
4221
  if (this.zip.getEntry("xl/workbook.bin")) {
4004
4222
  throw new Error(
4005
4223
  "XlsxUpdater: XLSB files are not supported yet. Convert the workbook to XLSX first."
@@ -4033,8 +4251,15 @@ var XlsxUpdater = class {
4033
4251
  if (!entry) {
4034
4252
  throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
4035
4253
  }
4036
- const sheetXml = entry.getData().toString("utf8");
4254
+ const sheetXml = this._entryData(sheetPath).toString("utf8");
4037
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
+ }
4038
4263
  let colStyles = /* @__PURE__ */ new Map();
4039
4264
  let headerStyles = /* @__PURE__ */ new Map();
4040
4265
  let dateStyle = null;
@@ -4049,6 +4274,7 @@ var XlsxUpdater = class {
4049
4274
  const newSheetData = this._buildSheetData(rows, headers, colStyles, headerStyles, dateStyle);
4050
4275
  const newXml = this._patchSheetXml(sheetXml, newSheetData, totalRows, lastCol);
4051
4276
  this.zip.updateFile(sheetPath, Buffer.from(newXml, "utf8"));
4277
+ this._discardStagedPart(sheetPath);
4052
4278
  if (this.sharedStringsXml !== null) {
4053
4279
  this._commitSharedStrings();
4054
4280
  }
@@ -4056,18 +4282,255 @@ var XlsxUpdater = class {
4056
4282
  this._updatePivotCaches(sheetName, dimRef, rows.length);
4057
4283
  this._addRefreshOnLoadToPivotTables();
4058
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
+ }
4059
4445
  /**
4060
4446
  * Write the updated workbook to disk.
4061
4447
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
4062
4448
  */
4063
4449
  save(outputPath) {
4064
4450
  const target = outputPath ?? this.sourcePath;
4065
- 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();
4066
4459
  }
4067
4460
  /** The updated workbook as an in-memory ZIP buffer. */
4068
4461
  toBuffer() {
4462
+ this._materializeStagedParts();
4069
4463
  return this.zip.toBuffer();
4070
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
+ }
4071
4534
  // ------------------------------------------------------------------
4072
4535
  // Workbook structure
4073
4536
  // ------------------------------------------------------------------
@@ -4120,47 +4583,59 @@ var XlsxUpdater = class {
4120
4583
  return;
4121
4584
  }
4122
4585
  const entry = this.zip.getEntry("xl/sharedStrings.xml");
4123
- if (!entry) {
4586
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.xml")) {
4124
4587
  this.sharedStringsXml = null;
4125
4588
  return;
4126
4589
  }
4127
- this.sharedStringsXml = entry.getData().toString("utf8");
4590
+ this.sharedStringsXml = this._entryData("xl/sharedStrings.xml").toString("utf8");
4128
4591
  this.sharedStringsValues = parseSharedStringsXml(this.sharedStringsXml);
4129
- const countMatch = /<sst\b[^>]*>/.exec(this.sharedStringsXml);
4130
- const countAttr = countMatch ? /count="(\d+)"/.exec(countMatch[0]) : null;
4131
- 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;
4132
4595
  this.sharedStringsValues.forEach((value, index) => {
4133
4596
  if (!this.stringIndexMap.has(value)) {
4134
4597
  this.stringIndexMap.set(value, index);
4135
4598
  }
4136
4599
  });
4137
4600
  this.sharedStringsOriginalLength = this.sharedStringsValues.length;
4601
+ this.sharedStringsPending = [];
4602
+ this.sharedStringsDirty = false;
4138
4603
  }
4139
4604
  _commitSharedStrings() {
4140
- if (this.sharedStringsXml === null || this.sharedStringsValues.length === this.sharedStringsOriginalLength) {
4605
+ var _a;
4606
+ if (this.sharedStringsXml === null || !this.sharedStringsDirty) {
4141
4607
  return;
4142
4608
  }
4143
4609
  let xml = this.sharedStringsXml;
4144
- const sstStart = xml.indexOf("<sst");
4145
- const sstTagEnd = xml.indexOf(">", sstStart);
4146
- if (sstStart !== -1 && sstTagEnd !== -1) {
4147
- const tag = xml.substring(sstStart, sstTagEnd + 1);
4148
- const newTag = tag.replace(/count="\d+"/, `count="${this.sharedStringsCount}"`).replace(/uniqueCount="\d+"/, `uniqueCount="${this.sharedStringsValues.length}"`);
4149
- xml = xml.substring(0, sstStart) + newTag + xml.substring(sstTagEnd + 1);
4150
- }
4151
- const sstEnd = xml.lastIndexOf("</sst>");
4152
- if (sstEnd === -1) {
4153
- return;
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.");
4154
4626
  }
4155
- let appended = "";
4156
- for (let i = this.sharedStringsOriginalLength; i < this.sharedStringsValues.length; i++) {
4157
- const txt = this.sharedStringsValues[i];
4627
+ const appended = this.sharedStringsPending.map((txt) => {
4158
4628
  const clean = escapeXmlText(txt);
4159
4629
  const preserve = clean.length > 0 && (clean[0] === " " || clean[clean.length - 1] === " " || /[\t\n\r]/.test(clean));
4160
- appended += preserve ? `<si><t xml:space="preserve">${clean}</t></si>` : `<si><t>${clean}</t></si>`;
4161
- }
4162
- 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);
4163
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;
4164
4639
  }
4165
4640
  // ------------------------------------------------------------------
4166
4641
  // Styles
@@ -4311,10 +4786,25 @@ var XlsxUpdater = class {
4311
4786
  index = this.sharedStringsValues.length;
4312
4787
  this.sharedStringsValues.push(text);
4313
4788
  this.stringIndexMap.set(text, index);
4789
+ this.sharedStringsPending.push(text);
4314
4790
  }
4315
4791
  this.sharedStringsCount++;
4792
+ this.sharedStringsDirty = true;
4316
4793
  return `<c r="${colRef}${rowNum}" t="s"${styleAttr}><v>${index}</v></c>`;
4317
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
+ }
4318
4808
  // ------------------------------------------------------------------
4319
4809
  // Worksheet XML patching
4320
4810
  // ------------------------------------------------------------------
@@ -4356,6 +4846,109 @@ var XlsxUpdater = class {
4356
4846
  }
4357
4847
  return xml;
4358
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
+ }
4359
4952
  // ------------------------------------------------------------------
4360
4953
  // Pivot table integration
4361
4954
  // ------------------------------------------------------------------
@@ -4384,8 +4977,8 @@ var XlsxUpdater = class {
4384
4977
  }
4385
4978
  }
4386
4979
  _addRefreshOnLoadToPivotTables() {
4387
- for (const path3 of this.pivotTableDefPaths) {
4388
- const entry = this.zip.getEntry(path3);
4980
+ for (const path7 of this.pivotTableDefPaths) {
4981
+ const entry = this.zip.getEntry(path7);
4389
4982
  if (!entry) continue;
4390
4983
  let xml = entry.getData().toString("utf8");
4391
4984
  const rootRe = /<pivotTableDefinition\b[^>]*>/;
@@ -4393,13 +4986,15 @@ var XlsxUpdater = class {
4393
4986
  if (!rootMatch || rootMatch[0].includes("refreshOnLoad")) continue;
4394
4987
  const newTag = rootMatch[0].replace(/(\/?>)$/, ' refreshOnLoad="1"$1');
4395
4988
  xml = xml.substring(0, rootMatch.index) + newTag + xml.substring(rootMatch.index + rootMatch[0].length);
4396
- this.zip.updateFile(path3, Buffer.from(xml, "utf8"));
4989
+ this.zip.updateFile(path7, Buffer.from(xml, "utf8"));
4397
4990
  }
4398
4991
  }
4399
4992
  };
4400
4993
 
4401
4994
  // src/XlsbUpdater.ts
4402
- var fs4 = __toESM(require("fs"));
4995
+ var fs6 = __toESM(require("fs"));
4996
+ var os2 = __toESM(require("os"));
4997
+ var path5 = __toESM(require("path"));
4403
4998
  var import_adm_zip3 = __toESM(require("adm-zip"));
4404
4999
 
4405
5000
  // src/biff12Utils.ts
@@ -4478,7 +5073,9 @@ var XlsbUpdater = class {
4478
5073
  /**
4479
5074
  * @param path Path to an existing `.xlsb` file.
4480
5075
  */
4481
- constructor(path3) {
5076
+ constructor(filePath) {
5077
+ this.temporaryDirectory = null;
5078
+ this.stagedParts = /* @__PURE__ */ new Map();
4482
5079
  this.sheetNameToPath = /* @__PURE__ */ new Map();
4483
5080
  this.pivotCacheDefPaths = [];
4484
5081
  this.sharedStringsBuf = null;
@@ -4487,16 +5084,17 @@ var XlsbUpdater = class {
4487
5084
  this.sharedStringsUnique = 0;
4488
5085
  this.sharedStringsEndSst = 0;
4489
5086
  this.stringIndexMap = /* @__PURE__ */ new Map();
5087
+ this.sharedStringsDirty = false;
4490
5088
  // ------------------------------------------------------------------
4491
5089
  // Shared strings
4492
5090
  // ------------------------------------------------------------------
4493
5091
  this._appendCount = 0;
4494
- this.sourcePath = path3;
5092
+ this.sourcePath = filePath;
4495
5093
  this._oaEpoch = Date.UTC(1899, 11, 30);
4496
- if (!fs4.existsSync(path3)) {
4497
- throw new Error(`XlsbUpdater: file not found: ${path3}`);
5094
+ if (!fs6.existsSync(filePath)) {
5095
+ throw new Error(`XlsbUpdater: file not found: ${filePath}`);
4498
5096
  }
4499
- this.zip = new import_adm_zip3.default(path3);
5097
+ this.zip = new import_adm_zip3.default(filePath);
4500
5098
  if (!this.zip.getEntry("xl/workbook.bin")) {
4501
5099
  throw new Error(
4502
5100
  "XlsbUpdater: not an XLSB workbook (xl/workbook.bin missing). Convert the file to XLSB first."
@@ -4527,8 +5125,15 @@ var XlsbUpdater = class {
4527
5125
  if (!entry) {
4528
5126
  throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
4529
5127
  }
4530
- const sheetBuf = entry.getData();
5128
+ const sheetBuf = this._entryData(sheetPath);
4531
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
+ }
4532
5137
  let dataStyles = /* @__PURE__ */ new Map();
4533
5138
  let headerStyles = /* @__PURE__ */ new Map();
4534
5139
  let dateXf = null;
@@ -4557,25 +5162,310 @@ var XlsbUpdater = class {
4557
5162
  }
4558
5163
  this._patchDimension(newSheet, lastRowIdx, lastCol);
4559
5164
  this.zip.updateFile(sheetPath, newSheet);
4560
- if (this.sharedStringsBuf !== null && this._appendCount > 0) {
5165
+ this._discardStagedPart(sheetPath);
5166
+ if (this.sharedStringsBuf !== null) {
4561
5167
  this._commitSharedStrings();
4562
5168
  }
4563
5169
  const rwLast = Math.max(0, rows.length + (headers ? 1 : 0) - 1);
4564
5170
  const colLast = lastCol;
4565
5171
  this._patchPivotCaches(sheetName, rwLast, colLast);
4566
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
+ }
4567
5380
  /**
4568
5381
  * Write the updated workbook to disk.
4569
5382
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
4570
5383
  */
4571
5384
  save(outputPath) {
4572
5385
  const target = outputPath ?? this.sourcePath;
4573
- 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();
4574
5394
  }
4575
5395
  /** The updated workbook as an in-memory ZIP buffer. */
4576
5396
  toBuffer() {
5397
+ this._materializeStagedParts();
4577
5398
  return this.zip.toBuffer();
4578
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
+ }
4579
5469
  // ------------------------------------------------------------------
4580
5470
  // Workbook structure
4581
5471
  // ------------------------------------------------------------------
@@ -4625,11 +5515,11 @@ var XlsbUpdater = class {
4625
5515
  return;
4626
5516
  }
4627
5517
  const entry = this.zip.getEntry("xl/sharedStrings.bin");
4628
- if (!entry) {
5518
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.bin")) {
4629
5519
  this.sharedStringsBuf = null;
4630
5520
  return;
4631
5521
  }
4632
- this.sharedStringsBuf = entry.getData();
5522
+ this.sharedStringsBuf = this._entryData("xl/sharedStrings.bin");
4633
5523
  const parsed = parseSharedStringsBin(this.sharedStringsBuf);
4634
5524
  this.sharedStringsValues = parsed.values;
4635
5525
  this.sharedStringsTotal = parsed.total;
@@ -4641,9 +5531,10 @@ var XlsbUpdater = class {
4641
5531
  }
4642
5532
  });
4643
5533
  this._appendCount = 0;
5534
+ this.sharedStringsDirty = false;
4644
5535
  }
4645
5536
  _commitSharedStrings() {
4646
- if (this.sharedStringsBuf === null || this._appendCount === 0) return;
5537
+ if (this.sharedStringsBuf === null || !this.sharedStringsDirty) return;
4647
5538
  const originalValuesLength = this.sharedStringsValues.length - this._appendCount;
4648
5539
  let newItems = Buffer.alloc(0);
4649
5540
  for (let i = originalValuesLength; i < this.sharedStringsValues.length; i++) {
@@ -4669,6 +5560,9 @@ var XlsbUpdater = class {
4669
5560
  pos = rec.dataEnd;
4670
5561
  }
4671
5562
  this.zip.updateFile("xl/sharedStrings.bin", out);
5563
+ this.sharedStringsBuf = out;
5564
+ this._appendCount = 0;
5565
+ this.sharedStringsDirty = false;
4672
5566
  }
4673
5567
  // ------------------------------------------------------------------
4674
5568
  // Styles
@@ -4851,12 +5745,51 @@ var XlsbUpdater = class {
4851
5745
  this._appendCount++;
4852
5746
  }
4853
5747
  this.sharedStringsTotal++;
5748
+ this.sharedStringsDirty = true;
4854
5749
  const payload = Buffer.alloc(12);
4855
5750
  payload.writeUInt32LE(col, 0);
4856
5751
  payload.writeUInt32LE(style, 4);
4857
5752
  payload.writeUInt32LE(index, 8);
4858
5753
  return buildRecord(7, payload);
4859
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
+ }
4860
5793
  // ------------------------------------------------------------------
4861
5794
  // Rows region detection
4862
5795
  // ------------------------------------------------------------------
@@ -4908,8 +5841,8 @@ var XlsbUpdater = class {
4908
5841
  // Pivot cache integration
4909
5842
  // ------------------------------------------------------------------
4910
5843
  _patchPivotCaches(sheetName, rwLast, colLast) {
4911
- for (const path3 of this.pivotCacheDefPaths) {
4912
- const entry = this.zip.getEntry(path3);
5844
+ for (const path7 of this.pivotCacheDefPaths) {
5845
+ const entry = this.zip.getEntry(path7);
4913
5846
  if (!entry) continue;
4914
5847
  const buf = Buffer.from(entry.getData());
4915
5848
  const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
@@ -4941,84 +5874,17 @@ var XlsbUpdater = class {
4941
5874
  }
4942
5875
  }
4943
5876
  if (patched) {
4944
- this.zip.updateFile(path3, buf);
5877
+ this.zip.updateFile(path7, buf);
4945
5878
  }
4946
5879
  }
4947
5880
  }
4948
5881
  };
4949
5882
 
4950
5883
  // src/ExcelConverter.ts
4951
- var fs6 = __toESM(require("fs"));
4952
- var path2 = __toESM(require("path"));
4953
- var import_crypto2 = require("crypto");
5884
+ var fs7 = __toESM(require("fs"));
5885
+ var path6 = __toESM(require("path"));
5886
+ var import_crypto3 = require("crypto");
4954
5887
  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
5888
  var POWERSHELL_CONVERSION_SCRIPT = `
5023
5889
  $ErrorActionPreference = 'Stop'
5024
5890
  $inputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_INPUT')
@@ -5063,29 +5929,29 @@ function errorMessage2(error) {
5063
5929
  return error instanceof Error ? error.message : String(error);
5064
5930
  }
5065
5931
  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) {
5932
+ const input = path6.resolve(inputPath);
5933
+ const output = path6.resolve(outputPath);
5934
+ if (path6.extname(input).toLowerCase() !== inputExtension) {
5069
5935
  throw new Error(
5070
5936
  `Excel conversion: source must have the ${inputExtension} extension: ${inputPath}`
5071
5937
  );
5072
5938
  }
5073
- if (path2.extname(output).toLowerCase() !== outputExtension) {
5939
+ if (path6.extname(output).toLowerCase() !== outputExtension) {
5074
5940
  throw new Error(
5075
5941
  `Excel conversion: destination must have the ${outputExtension} extension: ${outputPath}`
5076
5942
  );
5077
5943
  }
5078
- if (!fs6.existsSync(input)) {
5944
+ if (!fs7.existsSync(input)) {
5079
5945
  throw new Error(`Excel conversion: source file not found: ${inputPath}`);
5080
5946
  }
5081
- if (!fs6.statSync(input).isFile()) {
5947
+ if (!fs7.statSync(input).isFile()) {
5082
5948
  throw new Error(`Excel conversion: source path is not a file: ${inputPath}`);
5083
5949
  }
5084
- if (!fs6.existsSync(path2.dirname(output))) {
5085
- throw new Error(`Excel conversion: destination directory not found: ${path2.dirname(output)}`);
5950
+ if (!fs7.existsSync(path6.dirname(output))) {
5951
+ throw new Error(`Excel conversion: destination directory not found: ${path6.dirname(output)}`);
5086
5952
  }
5087
- if (fs6.existsSync(output)) {
5088
- if (fs6.statSync(output).isDirectory()) {
5953
+ if (fs7.existsSync(output)) {
5954
+ if (fs7.statSync(output).isDirectory()) {
5089
5955
  throw new Error(`Excel conversion: destination path is a directory: ${outputPath}`);
5090
5956
  }
5091
5957
  if (!overwrite) {
@@ -5107,7 +5973,7 @@ function runPowerShellConversion(inputPath, outputPath, fileFormat) {
5107
5973
  JUSTYBASE_EXCEL_OUTPUT: outputPath,
5108
5974
  JUSTYBASE_EXCEL_FORMAT: String(fileFormat)
5109
5975
  };
5110
- return new Promise((resolve2, reject) => {
5976
+ return new Promise((resolve4, reject) => {
5111
5977
  var _a, _b, _c, _d;
5112
5978
  let stdout = "";
5113
5979
  let stderr = "";
@@ -5142,7 +6008,7 @@ function runPowerShellConversion(inputPath, outputPath, fileFormat) {
5142
6008
  if (settled) return;
5143
6009
  settled = true;
5144
6010
  if (code === 0) {
5145
- resolve2();
6011
+ resolve4();
5146
6012
  return;
5147
6013
  }
5148
6014
  const details = (stderr || stdout).replace(/\s+/g, " ").trim();
@@ -5163,22 +6029,22 @@ async function convertExcelFile(inputPath, outputPath, inputExtension, outputExt
5163
6029
  if (process.platform !== "win32") {
5164
6030
  throw new Error("Excel conversion requires Windows with desktop Microsoft Excel and COM automation.");
5165
6031
  }
5166
- const temporaryOutput = path2.join(
5167
- path2.dirname(output),
5168
- `.${path2.basename(output, outputExtension)}.${(0, import_crypto2.randomUUID)()}.conversion${outputExtension}`
6032
+ const temporaryOutput = path6.join(
6033
+ path6.dirname(output),
6034
+ `.${path6.basename(output, outputExtension)}.${(0, import_crypto3.randomUUID)()}.conversion${outputExtension}`
5169
6035
  );
5170
6036
  try {
5171
6037
  await runPowerShellConversion(input, temporaryOutput, fileFormat);
5172
- if (!fs6.existsSync(temporaryOutput)) {
6038
+ if (!fs7.existsSync(temporaryOutput)) {
5173
6039
  throw new Error("Excel conversion completed without creating the destination file.");
5174
6040
  }
5175
6041
  installTemporaryOutput(temporaryOutput, output, options.overwrite === true);
5176
6042
  } catch (error) {
5177
6043
  throw new Error(`Excel conversion ${inputExtension} -> ${outputExtension}: ${errorMessage2(error)}`);
5178
6044
  } finally {
5179
- if (fs6.existsSync(temporaryOutput)) {
6045
+ if (fs7.existsSync(temporaryOutput)) {
5180
6046
  try {
5181
- fs6.rmSync(temporaryOutput);
6047
+ fs7.rmSync(temporaryOutput);
5182
6048
  } catch {
5183
6049
  }
5184
6050
  }
@@ -5202,6 +6068,7 @@ function convertXlsxToXlsb(inputPath, outputPath, options) {
5202
6068
  XlsbReader,
5203
6069
  XlsbUpdater,
5204
6070
  XlsbWriter,
6071
+ XlsmUpdater,
5205
6072
  XlsxReader,
5206
6073
  XlsxUpdater,
5207
6074
  XlsxWriter,