@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/esm/index.js CHANGED
@@ -402,8 +402,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
402
402
  this.xfIdToNumFmtId = [];
403
403
  this.customDateFormats = null;
404
404
  }
405
- async open(path2, readSharedStrings = true) {
406
- this.zip = new AdmZip(path2);
405
+ async open(path7, readSharedStrings = true) {
406
+ this.zip = new AdmZip(path7);
407
407
  const wbEntry = this.zip.getEntry("xl/workbook.bin");
408
408
  if (wbEntry) {
409
409
  const buf = wbEntry.getData();
@@ -448,8 +448,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
448
448
  this.resultsCount = this.sheets.length;
449
449
  this._currentSheetIndex = -1;
450
450
  }
451
- _readSharedStrings(path2) {
452
- const entry = this.zip.getEntry(path2);
451
+ _readSharedStrings(path7) {
452
+ const entry = this.zip.getEntry(path7);
453
453
  if (!entry) return;
454
454
  const reader = new BiffReaderWriter(entry.getData());
455
455
  while (reader.readSharedStrings()) {
@@ -458,8 +458,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
458
458
  }
459
459
  }
460
460
  }
461
- _readStyles(path2) {
462
- const entry = this.zip.getEntry(path2);
461
+ _readStyles(path7) {
462
+ const entry = this.zip.getEntry(path7);
463
463
  if (!entry) return;
464
464
  const reader = new BiffReaderWriter(entry.getData());
465
465
  while (reader.readStyles()) {
@@ -582,7 +582,7 @@ function escapeXmlText(str) {
582
582
  }
583
583
  function unescapeXml(str) {
584
584
  if (!str || str.indexOf("&") === -1) return str;
585
- return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
585
+ 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, "'");
586
586
  }
587
587
  function columnIndexToLetter(colIndex) {
588
588
  let n = colIndex + 1;
@@ -611,26 +611,17 @@ function trimTrailingEmptyRows(rows) {
611
611
  }
612
612
  function parseSharedStringsXml(xml) {
613
613
  const result = [];
614
- let pos = 0;
615
- while (true) {
616
- const siStart = xml.indexOf("<si>", pos);
617
- if (siStart === -1) break;
618
- const siEnd = xml.indexOf("</si>", siStart);
619
- if (siEnd === -1) break;
620
- const content = xml.substring(siStart, siEnd);
621
- let val = "";
622
- let tPos = 0;
623
- while (true) {
624
- const tStart = content.indexOf("<t", tPos);
625
- if (tStart === -1) break;
626
- const tagEnd = content.indexOf(">", tStart);
627
- const tEnd = content.indexOf("</t>", tagEnd);
628
- if (tEnd === -1) break;
629
- val += content.substring(tagEnd + 1, tEnd);
630
- tPos = tEnd + 4;
631
- }
632
- result.push(unescapeXml(val));
633
- pos = siEnd + 5;
614
+ const itemRegex = /<(?:(?:[A-Za-z_][\w.-]*):)?si\b[^>]*>([\s\S]*?)<\/(?:(?:[A-Za-z_][\w.-]*):)?si\s*>/gi;
615
+ let itemMatch;
616
+ while ((itemMatch = itemRegex.exec(xml)) !== null) {
617
+ const content = itemMatch[1];
618
+ const textRegex = /<(?:(?:[A-Za-z_][\w.-]*):)?t\b[^>]*>([\s\S]*?)<\/(?:(?:[A-Za-z_][\w.-]*):)?t\s*>/gi;
619
+ let value = "";
620
+ let textMatch;
621
+ while ((textMatch = textRegex.exec(content)) !== null) {
622
+ value += textMatch[1];
623
+ }
624
+ result.push(unescapeXml(value));
634
625
  }
635
626
  return result;
636
627
  }
@@ -651,9 +642,9 @@ var XlsxReader = class extends ExcelReaderAbstract {
651
642
  this.xfIdToNumFmtId = [];
652
643
  this.customDateFormats = /* @__PURE__ */ new Set();
653
644
  }
654
- async open(path2, readSharedStrings = true) {
655
- return new Promise((resolve, reject) => {
656
- yauzl.open(path2, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
645
+ async open(path7, readSharedStrings = true) {
646
+ return new Promise((resolve4, reject) => {
647
+ yauzl.open(path7, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
657
648
  if (err) return reject(err);
658
649
  this.zipfile = zipfile;
659
650
  this.entries = /* @__PURE__ */ new Map();
@@ -701,7 +692,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
701
692
  }
702
693
  this.resultsCount = this.sheets.length;
703
694
  this._currentSheetIndex = -1;
704
- resolve();
695
+ resolve4();
705
696
  } catch (e) {
706
697
  reject(e);
707
698
  }
@@ -715,16 +706,16 @@ var XlsxReader = class extends ExcelReaderAbstract {
715
706
  this.zipfile.close();
716
707
  }
717
708
  }
718
- async _readZipEntryContent(path2) {
719
- const entry = this.entries.get(path2);
709
+ async _readZipEntryContent(path7) {
710
+ const entry = this.entries.get(path7);
720
711
  if (!entry) return null;
721
- return new Promise((resolve, reject) => {
712
+ return new Promise((resolve4, reject) => {
722
713
  this.zipfile.openReadStream(entry, (err, readStream) => {
723
714
  if (err) return reject(err);
724
715
  const chunks = [];
725
716
  readStream.on("data", (chunk) => chunks.push(chunk));
726
717
  readStream.on("end", () => {
727
- resolve(Buffer.concat(chunks).toString("utf8"));
718
+ resolve4(Buffer.concat(chunks).toString("utf8"));
728
719
  });
729
720
  readStream.on("error", reject);
730
721
  });
@@ -3330,7 +3321,7 @@ var XlsbWriter = class _XlsbWriter {
3330
3321
  * Must be awaited before the file is fully written.
3331
3322
  */
3332
3323
  finalize() {
3333
- return new Promise((resolve, reject) => {
3324
+ return new Promise((resolve4, reject) => {
3334
3325
  try {
3335
3326
  this.saveSst();
3336
3327
  this.archive.append(this._buildStylesBin(), { name: "xl/styles.bin" });
@@ -3428,7 +3419,7 @@ var XlsbWriter = class _XlsbWriter {
3428
3419
  </Relationships>`;
3429
3420
  this.archive.append(wsRels, { name: `xl/worksheets/_rels/${sheet.nameInArchive}.rels` });
3430
3421
  }
3431
- this.output.on("close", () => resolve());
3422
+ this.output.on("close", () => resolve4());
3432
3423
  this.archive.on("error", (err) => reject(err));
3433
3424
  this.archive.finalize();
3434
3425
  } catch (err) {
@@ -3776,7 +3767,7 @@ var XlsxWriter = class {
3776
3767
  }
3777
3768
  /** Finalize the ZIP package and close the output stream. */
3778
3769
  finalize() {
3779
- return new Promise((resolve, reject) => {
3770
+ return new Promise((resolve4, reject) => {
3780
3771
  try {
3781
3772
  this._writeSharedStrings();
3782
3773
  this._writeStyles();
@@ -3784,7 +3775,7 @@ var XlsxWriter = class {
3784
3775
  this._writeContentTypes();
3785
3776
  this._writeRels();
3786
3777
  this.output.on("close", () => {
3787
- resolve();
3778
+ resolve4();
3788
3779
  });
3789
3780
  this.archive.on("error", (err) => {
3790
3781
  reject(err);
@@ -3921,13 +3912,237 @@ ${xfEntries.join("\n")}
3921
3912
  };
3922
3913
 
3923
3914
  // src/XlsxUpdater.ts
3924
- import * as fs3 from "fs";
3915
+ import * as fs5 from "fs";
3916
+ import * as os from "os";
3917
+ import * as path4 from "path";
3925
3918
  import AdmZip2 from "adm-zip";
3926
- var XlsxUpdater = class {
3919
+
3920
+ // src/atomicFile.ts
3921
+ import * as fs3 from "fs";
3922
+ import * as path2 from "path";
3923
+ import { randomUUID } from "crypto";
3924
+ var realFileOperations = {
3925
+ exists: (filePath) => fs3.existsSync(filePath),
3926
+ rename: (sourcePath, destinationPath) => fs3.renameSync(sourcePath, destinationPath),
3927
+ remove: (filePath) => fs3.rmSync(filePath)
3928
+ };
3929
+ function errorMessage(error) {
3930
+ return error instanceof Error ? error.message : String(error);
3931
+ }
3932
+ function destinationExistsError(destinationPath) {
3933
+ return new Error(
3934
+ `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${destinationPath}`
3935
+ );
3936
+ }
3937
+ function readDestinationMode(target) {
3938
+ try {
3939
+ if (!fs3.existsSync(target)) return void 0;
3940
+ return fs3.statSync(target).mode & 511;
3941
+ } catch {
3942
+ return void 0;
3943
+ }
3944
+ }
3945
+ function installTemporaryOutput(temporaryPath, destinationPath, overwrite, operations = realFileOperations) {
3946
+ const backupPath = `${destinationPath}.${randomUUID()}.replacement-backup`;
3947
+ let backupCreated = false;
3948
+ let installed = false;
3949
+ if (operations.exists(destinationPath)) {
3950
+ if (!overwrite) {
3951
+ throw destinationExistsError(destinationPath);
3952
+ }
3953
+ operations.rename(destinationPath, backupPath);
3954
+ backupCreated = true;
3955
+ }
3956
+ try {
3957
+ try {
3958
+ operations.rename(temporaryPath, destinationPath);
3959
+ installed = true;
3960
+ } catch (error) {
3961
+ if (overwrite && !backupCreated && operations.exists(destinationPath)) {
3962
+ operations.rename(destinationPath, backupPath);
3963
+ backupCreated = true;
3964
+ operations.rename(temporaryPath, destinationPath);
3965
+ installed = true;
3966
+ } else {
3967
+ throw error;
3968
+ }
3969
+ }
3970
+ if (backupCreated) {
3971
+ operations.remove(backupPath);
3972
+ }
3973
+ } catch (error) {
3974
+ if (backupCreated && !installed) {
3975
+ if (!operations.exists(destinationPath)) {
3976
+ try {
3977
+ operations.rename(backupPath, destinationPath);
3978
+ backupCreated = false;
3979
+ } catch (rollbackError) {
3980
+ throw new Error(
3981
+ `Excel conversion: final destination install failed (${errorMessage(error)}), and restoring the original destination also failed (${errorMessage(rollbackError)}). Original file remains at ${backupPath}.`
3982
+ );
3983
+ }
3984
+ } else {
3985
+ throw new Error(
3986
+ `Excel conversion: final destination install failed (${errorMessage(error)}). A concurrent destination was preserved; the original file remains at ${backupPath}.`
3987
+ );
3988
+ }
3989
+ }
3990
+ throw error;
3991
+ }
3992
+ }
3993
+ function writeBufferAtomically(buffer, destinationPath) {
3994
+ const target = path2.resolve(destinationPath);
3995
+ fs3.mkdirSync(path2.dirname(target), { recursive: true });
3996
+ const temporaryPath = path2.join(
3997
+ path2.dirname(target),
3998
+ `.${path2.basename(target)}.${randomUUID()}.tmp`
3999
+ );
4000
+ const destinationMode = readDestinationMode(target);
4001
+ try {
4002
+ const fd = fs3.openSync(temporaryPath, "w", destinationMode ?? 438);
4003
+ try {
4004
+ if (destinationMode !== void 0) {
4005
+ try {
4006
+ fs3.fchmodSync(fd, destinationMode);
4007
+ } catch {
4008
+ }
4009
+ }
4010
+ fs3.writeFileSync(fd, buffer);
4011
+ fs3.fsyncSync(fd);
4012
+ } finally {
4013
+ fs3.closeSync(fd);
4014
+ }
4015
+ if (destinationMode !== void 0) {
4016
+ try {
4017
+ fs3.chmodSync(temporaryPath, destinationMode);
4018
+ } catch {
4019
+ }
4020
+ }
4021
+ fs3.renameSync(temporaryPath, target);
4022
+ } finally {
4023
+ if (fs3.existsSync(temporaryPath)) {
4024
+ try {
4025
+ fs3.rmSync(temporaryPath);
4026
+ } catch {
4027
+ }
4028
+ }
4029
+ }
4030
+ }
4031
+
4032
+ // src/streamingZip.ts
4033
+ import * as fs4 from "fs";
4034
+ import * as path3 from "path";
4035
+ import { randomUUID as randomUUID2 } from "crypto";
4036
+ import { finished } from "stream/promises";
4037
+ import { Readable as Readable3 } from "stream";
4038
+ import archiver3 from "archiver";
4039
+ function readDestinationMode2(target) {
4040
+ try {
4041
+ if (!fs4.existsSync(target)) return void 0;
4042
+ return fs4.statSync(target).mode & 511;
4043
+ } catch {
4044
+ return void 0;
4045
+ }
4046
+ }
4047
+ function appendAndWait(archive, source, name) {
4048
+ return new Promise((resolve4, reject) => {
4049
+ const cleanup = () => {
4050
+ archive.off("entry", onEntry);
4051
+ archive.off("error", onError);
4052
+ const maybeEmitter2 = source;
4053
+ if (typeof maybeEmitter2.off === "function") {
4054
+ maybeEmitter2.off("error", onError);
4055
+ }
4056
+ };
4057
+ const onEntry = () => {
4058
+ cleanup();
4059
+ resolve4();
4060
+ };
4061
+ const onError = (error) => {
4062
+ cleanup();
4063
+ reject(error);
4064
+ };
4065
+ archive.once("entry", onEntry);
4066
+ archive.once("error", onError);
4067
+ const maybeEmitter = source;
4068
+ if (typeof maybeEmitter.once === "function") {
4069
+ maybeEmitter.once("error", onError);
4070
+ }
4071
+ try {
4072
+ archive.append(source, { name });
4073
+ } catch (error) {
4074
+ cleanup();
4075
+ reject(error);
4076
+ }
4077
+ });
4078
+ }
4079
+ async function writeAdmZipStreaming(zip, stagedParts, destinationPath) {
4080
+ const target = path3.resolve(destinationPath);
4081
+ fs4.mkdirSync(path3.dirname(target), { recursive: true });
4082
+ const temporaryPath = path3.join(
4083
+ path3.dirname(target),
4084
+ `.${path3.basename(target)}.${randomUUID2()}.tmp`
4085
+ );
4086
+ const destinationMode = readDestinationMode2(target);
4087
+ let output = null;
4088
+ let archive = null;
4089
+ try {
4090
+ output = fs4.createWriteStream(
4091
+ temporaryPath,
4092
+ destinationMode !== void 0 ? { mode: destinationMode } : {}
4093
+ );
4094
+ archive = archiver3("zip");
4095
+ archive.pipe(output);
4096
+ for (const entry of zip.getEntries()) {
4097
+ const stagedPath = stagedParts.get(entry.entryName);
4098
+ if (stagedPath) {
4099
+ await appendAndWait(archive, fs4.createReadStream(stagedPath), entry.entryName);
4100
+ } else {
4101
+ await appendAndWait(archive, Readable3.from([entry.getData()]), entry.entryName);
4102
+ }
4103
+ }
4104
+ await archive.finalize();
4105
+ await finished(output);
4106
+ if (destinationMode !== void 0) {
4107
+ try {
4108
+ fs4.chmodSync(temporaryPath, destinationMode);
4109
+ } catch {
4110
+ }
4111
+ }
4112
+ const fd = fs4.openSync(temporaryPath, "r+");
4113
+ try {
4114
+ fs4.fsyncSync(fd);
4115
+ } finally {
4116
+ fs4.closeSync(fd);
4117
+ }
4118
+ fs4.renameSync(temporaryPath, target);
4119
+ } catch (error) {
4120
+ try {
4121
+ archive == null ? void 0 : archive.abort();
4122
+ } catch {
4123
+ }
4124
+ throw error;
4125
+ } finally {
4126
+ if (output && !output.closed) {
4127
+ output.destroy();
4128
+ }
4129
+ if (fs4.existsSync(temporaryPath)) {
4130
+ try {
4131
+ fs4.rmSync(temporaryPath);
4132
+ } catch {
4133
+ }
4134
+ }
4135
+ }
4136
+ }
4137
+
4138
+ // src/XlsxUpdater.ts
4139
+ var XlsxUpdater = class _XlsxUpdater {
3927
4140
  /**
3928
4141
  * @param path Path to an existing `.xlsx` file.
3929
4142
  */
3930
- constructor(path2) {
4143
+ constructor(filePath) {
4144
+ this.temporaryDirectory = null;
4145
+ this.stagedParts = /* @__PURE__ */ new Map();
3931
4146
  this.sheetNameToPath = /* @__PURE__ */ new Map();
3932
4147
  this.pivotCacheDefs = [];
3933
4148
  this.pivotTableDefPaths = [];
@@ -3935,13 +4150,15 @@ var XlsxUpdater = class {
3935
4150
  this.sharedStringsValues = [];
3936
4151
  this.sharedStringsOriginalLength = 0;
3937
4152
  this.sharedStringsCount = 0;
4153
+ this.sharedStringsPending = [];
4154
+ this.sharedStringsDirty = false;
3938
4155
  this.stringIndexMap = /* @__PURE__ */ new Map();
3939
- this.sourcePath = path2;
4156
+ this.sourcePath = filePath;
3940
4157
  this._oaEpoch = Date.UTC(1899, 11, 30);
3941
- if (!fs3.existsSync(path2)) {
3942
- throw new Error(`XlsxUpdater: file not found: ${path2}`);
4158
+ if (!fs5.existsSync(filePath)) {
4159
+ throw new Error(`XlsxUpdater: file not found: ${filePath}`);
3943
4160
  }
3944
- this.zip = new AdmZip2(path2);
4161
+ this.zip = new AdmZip2(filePath);
3945
4162
  if (this.zip.getEntry("xl/workbook.bin")) {
3946
4163
  throw new Error(
3947
4164
  "XlsxUpdater: XLSB files are not supported yet. Convert the workbook to XLSX first."
@@ -3975,8 +4192,15 @@ var XlsxUpdater = class {
3975
4192
  if (!entry) {
3976
4193
  throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
3977
4194
  }
3978
- const sheetXml = entry.getData().toString("utf8");
4195
+ const sheetXml = this._entryData(sheetPath).toString("utf8");
3979
4196
  this._ensureSharedStringsLoaded();
4197
+ if (this.sharedStringsXml !== null) {
4198
+ this.sharedStringsCount = Math.max(
4199
+ 0,
4200
+ this.sharedStringsCount - this._sharedStringRefCount(sheetXml)
4201
+ );
4202
+ this.sharedStringsDirty = true;
4203
+ }
3980
4204
  let colStyles = /* @__PURE__ */ new Map();
3981
4205
  let headerStyles = /* @__PURE__ */ new Map();
3982
4206
  let dateStyle = null;
@@ -3991,6 +4215,7 @@ var XlsxUpdater = class {
3991
4215
  const newSheetData = this._buildSheetData(rows, headers, colStyles, headerStyles, dateStyle);
3992
4216
  const newXml = this._patchSheetXml(sheetXml, newSheetData, totalRows, lastCol);
3993
4217
  this.zip.updateFile(sheetPath, Buffer.from(newXml, "utf8"));
4218
+ this._discardStagedPart(sheetPath);
3994
4219
  if (this.sharedStringsXml !== null) {
3995
4220
  this._commitSharedStrings();
3996
4221
  }
@@ -3998,18 +4223,255 @@ var XlsxUpdater = class {
3998
4223
  this._updatePivotCaches(sheetName, dimRef, rows.length);
3999
4224
  this._addRefreshOnLoadToPivotTables();
4000
4225
  }
4226
+ /**
4227
+ * Replace worksheet rows from a one-pass source without materialising the
4228
+ * complete result set. Use {@link saveStreaming} to keep the final ZIP
4229
+ * write streaming as well.
4230
+ */
4231
+ async replaceSheetDataStream(sheetName, rows, options = {}) {
4232
+ const { headers, styleFallback = "inherit" } = options;
4233
+ if (styleFallback !== "inherit" && styleFallback !== "general") {
4234
+ throw new Error("XlsxUpdater: styleFallback must be 'inherit' or 'general'.");
4235
+ }
4236
+ const sheetPath = this.sheetNameToPath.get(sheetName);
4237
+ if (!sheetPath) {
4238
+ throw new Error(`XlsxUpdater: sheet "${sheetName}" not found in the workbook.`);
4239
+ }
4240
+ const entry = this.zip.getEntry(sheetPath);
4241
+ if (!entry) {
4242
+ throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
4243
+ }
4244
+ const oldStage = this.stagedParts.get(sheetPath);
4245
+ const beforeValuesLength = this.sharedStringsValues.length;
4246
+ const beforePendingLength = this.sharedStringsPending.length;
4247
+ const beforeSharedStringsCount = this.sharedStringsCount;
4248
+ const beforeSharedStringsDirty = this.sharedStringsDirty;
4249
+ const beforeSharedStringsXml = this.sharedStringsXml;
4250
+ const beforeSharedStringsOriginalLength = this.sharedStringsOriginalLength;
4251
+ const beforeStringIndexMap = new Map(this.stringIndexMap);
4252
+ const sharedStringsEntry = this.zip.getEntry("xl/sharedStrings.xml");
4253
+ const beforeSharedStringsBytes = sharedStringsEntry || this.stagedParts.has("xl/sharedStrings.xml") ? Buffer.from(this._entryData("xl/sharedStrings.xml")) : null;
4254
+ const createdParts = [];
4255
+ const oldSheetXml = this._entryData(sheetPath).toString("utf8");
4256
+ const rowsPath = this._temporaryPath(".rows");
4257
+ const outputPath = this._temporaryPath(".xml");
4258
+ createdParts.push(rowsPath, outputPath);
4259
+ let output = null;
4260
+ try {
4261
+ this._ensureSharedStringsLoaded();
4262
+ if (this.sharedStringsXml !== null) {
4263
+ this.sharedStringsCount = Math.max(
4264
+ 0,
4265
+ this.sharedStringsCount - this._sharedStringRefCount(oldSheetXml)
4266
+ );
4267
+ this.sharedStringsDirty = true;
4268
+ }
4269
+ let colStyles = /* @__PURE__ */ new Map();
4270
+ let headerStyles = /* @__PURE__ */ new Map();
4271
+ let dateStyle = null;
4272
+ if (styleFallback !== "general") {
4273
+ const collected = this._collectExistingStyles(oldSheetXml);
4274
+ colStyles = collected.dataStyles;
4275
+ headerStyles = collected.headerStyles;
4276
+ dateStyle = this._findDateStyleIndex();
4277
+ }
4278
+ const prefixMatch = /<([A-Za-z_][\w.-]*:)?sheetData\b/.exec(oldSheetXml);
4279
+ const prefix = (prefixMatch == null ? void 0 : prefixMatch[1]) ?? "";
4280
+ const rowOutput = fs5.createWriteStream(rowsPath);
4281
+ output = rowOutput;
4282
+ let outputError = null;
4283
+ rowOutput.on("error", (error) => {
4284
+ outputError = error;
4285
+ });
4286
+ let nextRow = 1;
4287
+ let width = (headers == null ? void 0 : headers.length) ?? 0;
4288
+ let dataRowsSeen = 0;
4289
+ let keptDataRows = 0;
4290
+ let lastKeptEnd = 0;
4291
+ let pendingEmptyWidth = 0;
4292
+ let writtenBytes = 0;
4293
+ const write = async (data) => {
4294
+ if (outputError) throw outputError;
4295
+ writtenBytes += data.length;
4296
+ if (rowOutput.write(data)) return;
4297
+ await new Promise((resolve4, reject) => {
4298
+ const onDrain = () => {
4299
+ cleanup();
4300
+ resolve4();
4301
+ };
4302
+ const onError = (error) => {
4303
+ cleanup();
4304
+ reject(error);
4305
+ };
4306
+ const cleanup = () => {
4307
+ rowOutput.off("drain", onDrain);
4308
+ rowOutput.off("error", onError);
4309
+ };
4310
+ rowOutput.once("drain", onDrain);
4311
+ rowOutput.once("error", onError);
4312
+ });
4313
+ };
4314
+ if (headers !== void 0) {
4315
+ const cells = headers.map((value, column) => this._valueCell(value, column, nextRow, headerStyles, dateStyle));
4316
+ const header = `<${prefix}row r="${nextRow}">${cells.join("")}</${prefix}row>`;
4317
+ await write(Buffer.from(header, "utf8"));
4318
+ lastKeptEnd = writtenBytes;
4319
+ nextRow++;
4320
+ }
4321
+ for await (const sourceRow of rows) {
4322
+ if (typeof sourceRow === "string" || sourceRow instanceof Uint8Array) {
4323
+ throw new TypeError("Each row must be an iterable of cells, not text");
4324
+ }
4325
+ const row = Array.from(sourceRow);
4326
+ const cells = row.map((value, column) => value === null || value === void 0 ? "" : this._valueCell(value, column, nextRow, colStyles, dateStyle));
4327
+ const rowXml = `<${prefix}row r="${nextRow}">${cells.join("")}</${prefix}row>`;
4328
+ await write(Buffer.from(rowXml, "utf8"));
4329
+ dataRowsSeen++;
4330
+ nextRow++;
4331
+ if (row.every((value) => value === null || value === void 0)) {
4332
+ pendingEmptyWidth = Math.max(pendingEmptyWidth, row.length);
4333
+ } else {
4334
+ width = Math.max(width, pendingEmptyWidth, row.length);
4335
+ pendingEmptyWidth = 0;
4336
+ keptDataRows = dataRowsSeen;
4337
+ lastKeptEnd = writtenBytes;
4338
+ }
4339
+ }
4340
+ await new Promise((resolve4, reject) => {
4341
+ rowOutput.once("close", resolve4);
4342
+ rowOutput.once("error", reject);
4343
+ rowOutput.end();
4344
+ });
4345
+ if (outputError) throw outputError;
4346
+ const rowsFd = fs5.openSync(rowsPath, "r+");
4347
+ try {
4348
+ fs5.ftruncateSync(rowsFd, lastKeptEnd);
4349
+ } finally {
4350
+ fs5.closeSync(rowsFd);
4351
+ }
4352
+ const totalRows = keptDataRows + (headers !== void 0 ? 1 : 0);
4353
+ const lastCol = width - 1;
4354
+ const dimRef = totalRows > 0 && lastCol >= 0 ? `A1:${columnIndexToLetter(lastCol)}${totalRows}` : "A1";
4355
+ this._patchSheetFile(oldSheetXml, rowsPath, outputPath, dimRef);
4356
+ this._stagePart(sheetPath, outputPath);
4357
+ this._commitSharedStrings();
4358
+ this._updatePivotCaches(sheetName, dimRef, keptDataRows);
4359
+ this._addRefreshOnLoadToPivotTables();
4360
+ if (oldStage && oldStage !== outputPath) {
4361
+ this._removeTemporaryFile(oldStage);
4362
+ }
4363
+ createdParts.splice(createdParts.indexOf(outputPath), 1);
4364
+ } catch (error) {
4365
+ this.sharedStringsValues.length = beforeValuesLength;
4366
+ this.sharedStringsPending.length = beforePendingLength;
4367
+ this.sharedStringsCount = beforeSharedStringsCount;
4368
+ this.sharedStringsDirty = beforeSharedStringsDirty;
4369
+ this.sharedStringsXml = beforeSharedStringsXml;
4370
+ this.sharedStringsOriginalLength = beforeSharedStringsOriginalLength;
4371
+ this.stringIndexMap.clear();
4372
+ for (const [value, index] of beforeStringIndexMap) this.stringIndexMap.set(value, index);
4373
+ if (beforeSharedStringsBytes) {
4374
+ this.zip.updateFile("xl/sharedStrings.xml", beforeSharedStringsBytes);
4375
+ }
4376
+ if (oldStage) this.stagedParts.set(sheetPath, oldStage);
4377
+ else this.stagedParts.delete(sheetPath);
4378
+ throw error;
4379
+ } finally {
4380
+ if (output && !output.closed) output.destroy();
4381
+ for (const temporaryPath of createdParts) {
4382
+ this._removeTemporaryFile(temporaryPath);
4383
+ }
4384
+ }
4385
+ }
4001
4386
  /**
4002
4387
  * Write the updated workbook to disk.
4003
4388
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
4004
4389
  */
4005
4390
  save(outputPath) {
4006
4391
  const target = outputPath ?? this.sourcePath;
4007
- fs3.writeFileSync(target, this.zip.toBuffer());
4392
+ this._materializeStagedParts();
4393
+ writeBufferAtomically(this.zip.toBuffer(), target);
4394
+ }
4395
+ /** Save staged streaming replacements without materialising the ZIP. */
4396
+ async saveStreaming(outputPath) {
4397
+ const target = outputPath ?? this.sourcePath;
4398
+ await writeAdmZipStreaming(this.zip, this.stagedParts, target);
4399
+ this._materializeStagedParts();
4008
4400
  }
4009
4401
  /** The updated workbook as an in-memory ZIP buffer. */
4010
4402
  toBuffer() {
4403
+ this._materializeStagedParts();
4011
4404
  return this.zip.toBuffer();
4012
4405
  }
4406
+ /**
4407
+ * Release the staging directory and any unsaved staged parts.
4408
+ * The updater remains usable; a new staging directory is created lazily
4409
+ * on the next streaming replacement.
4410
+ */
4411
+ dispose() {
4412
+ for (const stagedPath of this.stagedParts.values()) {
4413
+ try {
4414
+ fs5.rmSync(stagedPath, { force: true });
4415
+ } catch {
4416
+ }
4417
+ }
4418
+ this.stagedParts.clear();
4419
+ if (this.temporaryDirectory !== null) {
4420
+ try {
4421
+ fs5.rmSync(this.temporaryDirectory, { recursive: true, force: true });
4422
+ } catch {
4423
+ }
4424
+ this.temporaryDirectory = null;
4425
+ }
4426
+ }
4427
+ /** Support `using` declarations for automatic staging cleanup. */
4428
+ [Symbol.dispose]() {
4429
+ this.dispose();
4430
+ }
4431
+ _ensureTemporaryDirectory() {
4432
+ if (this.temporaryDirectory === null) {
4433
+ this.temporaryDirectory = fs5.mkdtempSync(path4.join(os.tmpdir(), "spreadsheet-updater-"));
4434
+ } else {
4435
+ try {
4436
+ fs5.mkdirSync(this.temporaryDirectory, { recursive: true });
4437
+ } catch {
4438
+ }
4439
+ }
4440
+ return this.temporaryDirectory;
4441
+ }
4442
+ _temporaryPath(suffix) {
4443
+ return path4.join(this._ensureTemporaryDirectory(), `part-${Date.now()}-${Math.random().toString(16).slice(2)}${suffix}`);
4444
+ }
4445
+ _removeTemporaryFile(filePath) {
4446
+ try {
4447
+ fs5.rmSync(filePath);
4448
+ } catch (error) {
4449
+ if (error.code !== "ENOENT") throw error;
4450
+ }
4451
+ }
4452
+ _entryData(name) {
4453
+ const stagedPath = this.stagedParts.get(name);
4454
+ if (stagedPath) return fs5.readFileSync(stagedPath);
4455
+ const entry = this.zip.getEntry(name);
4456
+ if (!entry) throw new Error(`XlsxUpdater: ZIP member missing: ${name}`);
4457
+ return entry.getData();
4458
+ }
4459
+ _stagePart(name, filePath) {
4460
+ this.stagedParts.set(name, filePath);
4461
+ }
4462
+ _discardStagedPart(name) {
4463
+ const stagedPath = this.stagedParts.get(name);
4464
+ if (!stagedPath) return;
4465
+ this._removeTemporaryFile(stagedPath);
4466
+ this.stagedParts.delete(name);
4467
+ }
4468
+ _materializeStagedParts() {
4469
+ for (const [name, stagedPath] of this.stagedParts) {
4470
+ this.zip.updateFile(name, fs5.readFileSync(stagedPath));
4471
+ this._removeTemporaryFile(stagedPath);
4472
+ }
4473
+ this.stagedParts.clear();
4474
+ }
4013
4475
  // ------------------------------------------------------------------
4014
4476
  // Workbook structure
4015
4477
  // ------------------------------------------------------------------
@@ -4062,47 +4524,59 @@ var XlsxUpdater = class {
4062
4524
  return;
4063
4525
  }
4064
4526
  const entry = this.zip.getEntry("xl/sharedStrings.xml");
4065
- if (!entry) {
4527
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.xml")) {
4066
4528
  this.sharedStringsXml = null;
4067
4529
  return;
4068
4530
  }
4069
- this.sharedStringsXml = entry.getData().toString("utf8");
4531
+ this.sharedStringsXml = this._entryData("xl/sharedStrings.xml").toString("utf8");
4070
4532
  this.sharedStringsValues = parseSharedStringsXml(this.sharedStringsXml);
4071
- const countMatch = /<sst\b[^>]*>/.exec(this.sharedStringsXml);
4072
- const countAttr = countMatch ? /count="(\d+)"/.exec(countMatch[0]) : null;
4073
- this.sharedStringsCount = countAttr ? parseInt(countAttr[1], 10) : 0;
4533
+ const countMatch = /<(?:[A-Za-z_][\w.-]*:)?sst\b[^>]*>/i.exec(this.sharedStringsXml);
4534
+ const countAttr = countMatch ? /(?:^|\s)count\s*=\s*["'](\d+)["']/.exec(countMatch[0]) : null;
4535
+ this.sharedStringsCount = countAttr ? parseInt(countAttr[1], 10) : this.sharedStringsValues.length;
4074
4536
  this.sharedStringsValues.forEach((value, index) => {
4075
4537
  if (!this.stringIndexMap.has(value)) {
4076
4538
  this.stringIndexMap.set(value, index);
4077
4539
  }
4078
4540
  });
4079
4541
  this.sharedStringsOriginalLength = this.sharedStringsValues.length;
4542
+ this.sharedStringsPending = [];
4543
+ this.sharedStringsDirty = false;
4080
4544
  }
4081
4545
  _commitSharedStrings() {
4082
- if (this.sharedStringsXml === null || this.sharedStringsValues.length === this.sharedStringsOriginalLength) {
4546
+ var _a;
4547
+ if (this.sharedStringsXml === null || !this.sharedStringsDirty) {
4083
4548
  return;
4084
4549
  }
4085
4550
  let xml = this.sharedStringsXml;
4086
- const sstStart = xml.indexOf("<sst");
4087
- const sstTagEnd = xml.indexOf(">", sstStart);
4088
- if (sstStart !== -1 && sstTagEnd !== -1) {
4089
- const tag = xml.substring(sstStart, sstTagEnd + 1);
4090
- const newTag = tag.replace(/count="\d+"/, `count="${this.sharedStringsCount}"`).replace(/uniqueCount="\d+"/, `uniqueCount="${this.sharedStringsValues.length}"`);
4091
- xml = xml.substring(0, sstStart) + newTag + xml.substring(sstTagEnd + 1);
4092
- }
4093
- const sstEnd = xml.lastIndexOf("</sst>");
4094
- if (sstEnd === -1) {
4095
- return;
4096
- }
4097
- let appended = "";
4098
- for (let i = this.sharedStringsOriginalLength; i < this.sharedStringsValues.length; i++) {
4099
- const txt = this.sharedStringsValues[i];
4551
+ const sstMatch = /<(?<prefix>[A-Za-z_][\w.-]*:)?sst\b[^>]*>/i.exec(xml);
4552
+ if (!sstMatch || sstMatch.index === void 0) {
4553
+ throw new Error("XlsxUpdater: sharedStrings.xml has no sst element.");
4554
+ }
4555
+ const tag = sstMatch[0];
4556
+ const prefix = ((_a = sstMatch.groups) == null ? void 0 : _a.prefix) ?? "";
4557
+ const newTag = _XlsxUpdater._replaceOrAddXmlAttribute(
4558
+ _XlsxUpdater._replaceOrAddXmlAttribute(tag, "count", String(this.sharedStringsCount)),
4559
+ "uniqueCount",
4560
+ String(this.sharedStringsValues.length)
4561
+ );
4562
+ xml = xml.substring(0, sstMatch.index) + newTag + xml.substring(sstMatch.index + tag.length);
4563
+ const closingRe = new RegExp(`</${prefix}sst\\s*>\\s*$`, "i");
4564
+ const closingMatch = closingRe.exec(xml);
4565
+ if (!closingMatch || closingMatch.index === void 0) {
4566
+ throw new Error("XlsxUpdater: sharedStrings.xml has no closing sst element.");
4567
+ }
4568
+ const appended = this.sharedStringsPending.map((txt) => {
4100
4569
  const clean = escapeXmlText(txt);
4101
4570
  const preserve = clean.length > 0 && (clean[0] === " " || clean[clean.length - 1] === " " || /[\t\n\r]/.test(clean));
4102
- appended += preserve ? `<si><t xml:space="preserve">${clean}</t></si>` : `<si><t>${clean}</t></si>`;
4103
- }
4104
- xml = xml.substring(0, sstEnd) + appended + xml.substring(sstEnd);
4571
+ const tAttr = preserve ? ' xml:space="preserve"' : "";
4572
+ return `<${prefix}si><${prefix}t${tAttr}>${clean}</${prefix}t></${prefix}si>`;
4573
+ }).join("");
4574
+ xml = xml.substring(0, closingMatch.index) + appended + xml.substring(closingMatch.index);
4105
4575
  this.zip.updateFile("xl/sharedStrings.xml", Buffer.from(xml, "utf8"));
4576
+ this.sharedStringsXml = xml;
4577
+ this.sharedStringsOriginalLength = this.sharedStringsValues.length;
4578
+ this.sharedStringsPending = [];
4579
+ this.sharedStringsDirty = false;
4106
4580
  }
4107
4581
  // ------------------------------------------------------------------
4108
4582
  // Styles
@@ -4253,10 +4727,25 @@ var XlsxUpdater = class {
4253
4727
  index = this.sharedStringsValues.length;
4254
4728
  this.sharedStringsValues.push(text);
4255
4729
  this.stringIndexMap.set(text, index);
4730
+ this.sharedStringsPending.push(text);
4256
4731
  }
4257
4732
  this.sharedStringsCount++;
4733
+ this.sharedStringsDirty = true;
4258
4734
  return `<c r="${colRef}${rowNum}" t="s"${styleAttr}><v>${index}</v></c>`;
4259
4735
  }
4736
+ static _replaceOrAddXmlAttribute(tag, name, value) {
4737
+ const escaped = escapeXmlText(value);
4738
+ const attribute = new RegExp(`(\\s${name}\\s*=\\s*["'])[^"']*(["'])`, "i");
4739
+ if (attribute.test(tag)) {
4740
+ return tag.replace(attribute, `$1${escaped}$2`);
4741
+ }
4742
+ const insertion = ` ${name}="${escaped}"`;
4743
+ return tag.endsWith("/>") ? `${tag.slice(0, -2)}${insertion}/>` : `${tag.slice(0, -1)}${insertion}>`;
4744
+ }
4745
+ _sharedStringRefCount(sheetXml) {
4746
+ const cellTags = sheetXml.match(/<(?:(?:[A-Za-z_][\w.-]*):)?c\b[^>]*>/gi) ?? [];
4747
+ return cellTags.filter((tag) => /(?:^|\s)t\s*=\s*["']s["']/i.test(tag)).length;
4748
+ }
4260
4749
  // ------------------------------------------------------------------
4261
4750
  // Worksheet XML patching
4262
4751
  // ------------------------------------------------------------------
@@ -4298,6 +4787,109 @@ var XlsxUpdater = class {
4298
4787
  }
4299
4788
  return xml;
4300
4789
  }
4790
+ _patchSheetFile(sheetXml, rowsPath, outputPath, dimensionRef) {
4791
+ var _a, _b, _c, _d;
4792
+ const source = Buffer.from(sheetXml, "utf8");
4793
+ const text = sheetXml;
4794
+ const replacements = [];
4795
+ const emptySheetData = /<(?<prefix>[A-Za-z_][\w.-]*:)?sheetData\b(?<attrs>[^>]*)\/>/i.exec(text);
4796
+ if (emptySheetData && emptySheetData.index !== void 0) {
4797
+ const prefix = ((_a = emptySheetData.groups) == null ? void 0 : _a.prefix) ?? "";
4798
+ replacements.push({
4799
+ start: emptySheetData.index,
4800
+ end: emptySheetData.index + emptySheetData[0].length,
4801
+ data: Buffer.from(`<${prefix}sheetData${((_b = emptySheetData.groups) == null ? void 0 : _b.attrs) ?? ""}>`, "utf8"),
4802
+ rows: true,
4803
+ suffix: Buffer.from(`</${prefix}sheetData>`, "utf8")
4804
+ });
4805
+ } else {
4806
+ const opening = /<(?<prefix>[A-Za-z_][\w.-]*:)?sheetData\b[^>]*>/i.exec(text);
4807
+ if (!opening || opening.index === void 0) {
4808
+ throw new Error("XlsxUpdater: <sheetData> element not found in worksheet XML.");
4809
+ }
4810
+ const prefix = ((_c = opening.groups) == null ? void 0 : _c.prefix) ?? "";
4811
+ const closeRe = new RegExp(`</${prefix}sheetData\\s*>`, "i");
4812
+ const closing = closeRe.exec(text.slice(opening.index + opening[0].length));
4813
+ if (!closing || closing.index === void 0) {
4814
+ throw new Error("XlsxUpdater: <sheetData> element not closed in worksheet XML.");
4815
+ }
4816
+ const closingStart = opening.index + opening[0].length + closing.index;
4817
+ replacements.push({
4818
+ start: opening.index + opening[0].length,
4819
+ end: closingStart,
4820
+ rows: true
4821
+ });
4822
+ }
4823
+ const dimension = /<(?<prefix>[A-Za-z_][\w.-]*:)?dimension\b[^>]*>/i.exec(text);
4824
+ if (dimension && dimension.index !== void 0) {
4825
+ replacements.push({
4826
+ start: dimension.index,
4827
+ end: dimension.index + dimension[0].length,
4828
+ data: Buffer.from(
4829
+ _XlsxUpdater._replaceOrAddXmlAttribute(dimension[0], "ref", dimensionRef)
4830
+ )
4831
+ });
4832
+ } else {
4833
+ const root = /<(?<prefix>[A-Za-z_][\w.-]*:)?worksheet\b[^>]*>/i.exec(text);
4834
+ if (root && root.index !== void 0) {
4835
+ const prefix = ((_d = root.groups) == null ? void 0 : _d.prefix) ?? "";
4836
+ const insertAt = root.index + root[0].length;
4837
+ replacements.push({
4838
+ start: insertAt,
4839
+ end: insertAt,
4840
+ data: Buffer.from(`<${prefix}dimension ref="${dimensionRef}"/>`, "utf8")
4841
+ });
4842
+ }
4843
+ }
4844
+ const autoFilter = /<(?<prefix>[A-Za-z_][\w.-]*:)?autoFilter\b[^>]*>/i.exec(text);
4845
+ if (autoFilter && autoFilter.index !== void 0) {
4846
+ replacements.push({
4847
+ start: autoFilter.index,
4848
+ end: autoFilter.index + autoFilter[0].length,
4849
+ data: Buffer.from(
4850
+ _XlsxUpdater._replaceOrAddXmlAttribute(autoFilter[0], "ref", dimensionRef),
4851
+ "utf8"
4852
+ )
4853
+ });
4854
+ }
4855
+ replacements.sort((a, b) => a.start - b.start || a.end - b.end);
4856
+ const fd = fs5.openSync(outputPath, "w");
4857
+ let cursor = 0;
4858
+ const writeBuffer = (data) => {
4859
+ fs5.writeSync(fd, data);
4860
+ };
4861
+ const copyRows = () => {
4862
+ const rowsFd = fs5.openSync(rowsPath, "r");
4863
+ try {
4864
+ const chunk = Buffer.alloc(1024 * 1024);
4865
+ const length = fs5.statSync(rowsPath).size;
4866
+ let position = 0;
4867
+ while (position < length) {
4868
+ const count = fs5.readSync(rowsFd, chunk, 0, Math.min(chunk.length, length - position), position);
4869
+ if (count <= 0) throw new Error("XlsxUpdater: failed to read staged rows.");
4870
+ fs5.writeSync(fd, chunk, 0, count);
4871
+ position += count;
4872
+ }
4873
+ } finally {
4874
+ fs5.closeSync(rowsFd);
4875
+ }
4876
+ };
4877
+ try {
4878
+ for (const replacement of replacements) {
4879
+ const startByte = Buffer.byteLength(text.slice(0, replacement.start), "utf8");
4880
+ const endByte = Buffer.byteLength(text.slice(0, replacement.end), "utf8");
4881
+ if (startByte < cursor) throw new Error("XlsxUpdater: overlapping worksheet replacements.");
4882
+ writeBuffer(source.subarray(cursor, startByte));
4883
+ if (replacement.data) writeBuffer(replacement.data);
4884
+ if (replacement.rows) copyRows();
4885
+ if (replacement.suffix) writeBuffer(replacement.suffix);
4886
+ cursor = endByte;
4887
+ }
4888
+ writeBuffer(source.subarray(cursor));
4889
+ } finally {
4890
+ fs5.closeSync(fd);
4891
+ }
4892
+ }
4301
4893
  // ------------------------------------------------------------------
4302
4894
  // Pivot table integration
4303
4895
  // ------------------------------------------------------------------
@@ -4326,8 +4918,8 @@ var XlsxUpdater = class {
4326
4918
  }
4327
4919
  }
4328
4920
  _addRefreshOnLoadToPivotTables() {
4329
- for (const path2 of this.pivotTableDefPaths) {
4330
- const entry = this.zip.getEntry(path2);
4921
+ for (const path7 of this.pivotTableDefPaths) {
4922
+ const entry = this.zip.getEntry(path7);
4331
4923
  if (!entry) continue;
4332
4924
  let xml = entry.getData().toString("utf8");
4333
4925
  const rootRe = /<pivotTableDefinition\b[^>]*>/;
@@ -4335,13 +4927,15 @@ var XlsxUpdater = class {
4335
4927
  if (!rootMatch || rootMatch[0].includes("refreshOnLoad")) continue;
4336
4928
  const newTag = rootMatch[0].replace(/(\/?>)$/, ' refreshOnLoad="1"$1');
4337
4929
  xml = xml.substring(0, rootMatch.index) + newTag + xml.substring(rootMatch.index + rootMatch[0].length);
4338
- this.zip.updateFile(path2, Buffer.from(xml, "utf8"));
4930
+ this.zip.updateFile(path7, Buffer.from(xml, "utf8"));
4339
4931
  }
4340
4932
  }
4341
4933
  };
4342
4934
 
4343
4935
  // src/XlsbUpdater.ts
4344
- import * as fs4 from "fs";
4936
+ import * as fs6 from "fs";
4937
+ import * as os2 from "os";
4938
+ import * as path5 from "path";
4345
4939
  import AdmZip3 from "adm-zip";
4346
4940
 
4347
4941
  // src/biff12Utils.ts
@@ -4420,7 +5014,9 @@ var XlsbUpdater = class {
4420
5014
  /**
4421
5015
  * @param path Path to an existing `.xlsb` file.
4422
5016
  */
4423
- constructor(path2) {
5017
+ constructor(filePath) {
5018
+ this.temporaryDirectory = null;
5019
+ this.stagedParts = /* @__PURE__ */ new Map();
4424
5020
  this.sheetNameToPath = /* @__PURE__ */ new Map();
4425
5021
  this.pivotCacheDefPaths = [];
4426
5022
  this.sharedStringsBuf = null;
@@ -4429,16 +5025,17 @@ var XlsbUpdater = class {
4429
5025
  this.sharedStringsUnique = 0;
4430
5026
  this.sharedStringsEndSst = 0;
4431
5027
  this.stringIndexMap = /* @__PURE__ */ new Map();
5028
+ this.sharedStringsDirty = false;
4432
5029
  // ------------------------------------------------------------------
4433
5030
  // Shared strings
4434
5031
  // ------------------------------------------------------------------
4435
5032
  this._appendCount = 0;
4436
- this.sourcePath = path2;
5033
+ this.sourcePath = filePath;
4437
5034
  this._oaEpoch = Date.UTC(1899, 11, 30);
4438
- if (!fs4.existsSync(path2)) {
4439
- throw new Error(`XlsbUpdater: file not found: ${path2}`);
5035
+ if (!fs6.existsSync(filePath)) {
5036
+ throw new Error(`XlsbUpdater: file not found: ${filePath}`);
4440
5037
  }
4441
- this.zip = new AdmZip3(path2);
5038
+ this.zip = new AdmZip3(filePath);
4442
5039
  if (!this.zip.getEntry("xl/workbook.bin")) {
4443
5040
  throw new Error(
4444
5041
  "XlsbUpdater: not an XLSB workbook (xl/workbook.bin missing). Convert the file to XLSB first."
@@ -4469,8 +5066,15 @@ var XlsbUpdater = class {
4469
5066
  if (!entry) {
4470
5067
  throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
4471
5068
  }
4472
- const sheetBuf = entry.getData();
5069
+ const sheetBuf = this._entryData(sheetPath);
4473
5070
  this._ensureSharedStringsLoaded();
5071
+ if (this.sharedStringsBuf !== null) {
5072
+ this.sharedStringsTotal = Math.max(
5073
+ 0,
5074
+ this.sharedStringsTotal - this._sharedStringRefCount(sheetBuf)
5075
+ );
5076
+ this.sharedStringsDirty = true;
5077
+ }
4474
5078
  let dataStyles = /* @__PURE__ */ new Map();
4475
5079
  let headerStyles = /* @__PURE__ */ new Map();
4476
5080
  let dateXf = null;
@@ -4499,25 +5103,310 @@ var XlsbUpdater = class {
4499
5103
  }
4500
5104
  this._patchDimension(newSheet, lastRowIdx, lastCol);
4501
5105
  this.zip.updateFile(sheetPath, newSheet);
4502
- if (this.sharedStringsBuf !== null && this._appendCount > 0) {
5106
+ this._discardStagedPart(sheetPath);
5107
+ if (this.sharedStringsBuf !== null) {
4503
5108
  this._commitSharedStrings();
4504
5109
  }
4505
5110
  const rwLast = Math.max(0, rows.length + (headers ? 1 : 0) - 1);
4506
5111
  const colLast = lastCol;
4507
5112
  this._patchPivotCaches(sheetName, rwLast, colLast);
4508
5113
  }
5114
+ /** Replace worksheet rows from a one-pass source. */
5115
+ async replaceSheetDataStream(sheetName, rows, options = {}) {
5116
+ const { headers, styleFallback = "inherit" } = options;
5117
+ if (styleFallback !== "inherit" && styleFallback !== "general") {
5118
+ throw new Error("XlsbUpdater: styleFallback must be 'inherit' or 'general'.");
5119
+ }
5120
+ const sheetPath = this.sheetNameToPath.get(sheetName);
5121
+ if (!sheetPath) {
5122
+ throw new Error(`XlsbUpdater: sheet "${sheetName}" not found in the workbook.`);
5123
+ }
5124
+ const entry = this.zip.getEntry(sheetPath);
5125
+ if (!entry) {
5126
+ throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
5127
+ }
5128
+ const oldStage = this.stagedParts.get(sheetPath);
5129
+ const beforeValuesLength = this.sharedStringsValues.length;
5130
+ const beforeAppendCount = this._appendCount;
5131
+ const beforeSharedStringsTotal = this.sharedStringsTotal;
5132
+ const beforeSharedStringsUnique = this.sharedStringsUnique;
5133
+ const beforeSharedStringsEndSst = this.sharedStringsEndSst;
5134
+ const beforeSharedStringsDirty = this.sharedStringsDirty;
5135
+ const beforeStringIndexMap = new Map(this.stringIndexMap);
5136
+ const sharedStringsEntry = this.zip.getEntry("xl/sharedStrings.bin");
5137
+ const beforeSharedStringsBytes = sharedStringsEntry || this.stagedParts.has("xl/sharedStrings.bin") ? Buffer.from(this._entryData("xl/sharedStrings.bin")) : null;
5138
+ const beforeSharedStringsBuf = this.sharedStringsBuf ? Buffer.from(this.sharedStringsBuf) : null;
5139
+ const oldSheet = this._entryData(sheetPath);
5140
+ const rowsPath = this._temporaryPath(".rows");
5141
+ const outputPath = this._temporaryPath(".bin");
5142
+ const createdParts = [rowsPath, outputPath];
5143
+ let output = null;
5144
+ try {
5145
+ this._ensureSharedStringsLoaded();
5146
+ if (this.sharedStringsBuf !== null) {
5147
+ this.sharedStringsTotal = Math.max(
5148
+ 0,
5149
+ this.sharedStringsTotal - this._sharedStringRefCount(oldSheet)
5150
+ );
5151
+ this.sharedStringsDirty = true;
5152
+ }
5153
+ let dataStyles = /* @__PURE__ */ new Map();
5154
+ let headerStyles = /* @__PURE__ */ new Map();
5155
+ let dateXf = null;
5156
+ if (styleFallback !== "general") {
5157
+ const collected = this._collectStyles(oldSheet);
5158
+ dataStyles = collected.dataStyles;
5159
+ headerStyles = collected.headerStyles;
5160
+ dateXf = this._findDateXf();
5161
+ }
5162
+ const rowOutput = fs6.createWriteStream(rowsPath);
5163
+ output = rowOutput;
5164
+ let outputError = null;
5165
+ rowOutput.on("error", (error) => {
5166
+ outputError = error;
5167
+ });
5168
+ let writtenBytes = 0;
5169
+ let lastKeptEnd = 0;
5170
+ let width = (headers == null ? void 0 : headers.length) ?? 0;
5171
+ let dataRowsSeen = 0;
5172
+ let keptDataRows = 0;
5173
+ let pendingEmptyWidth = 0;
5174
+ let nextRow = 0;
5175
+ const rowHeaderOffsets = [];
5176
+ const write = async (data) => {
5177
+ if (outputError) throw outputError;
5178
+ writtenBytes += data.length;
5179
+ if (rowOutput.write(data)) return;
5180
+ await new Promise((resolve4, reject) => {
5181
+ const onDrain = () => {
5182
+ cleanup();
5183
+ resolve4();
5184
+ };
5185
+ const onError = (error) => {
5186
+ cleanup();
5187
+ reject(error);
5188
+ };
5189
+ const cleanup = () => {
5190
+ rowOutput.off("drain", onDrain);
5191
+ rowOutput.off("error", onError);
5192
+ };
5193
+ rowOutput.once("drain", onDrain);
5194
+ rowOutput.once("error", onError);
5195
+ });
5196
+ };
5197
+ const writeRow = async (rowNumber, cells) => {
5198
+ rowHeaderOffsets.push(writtenBytes);
5199
+ const header = Buffer.alloc(27);
5200
+ header[0] = 0;
5201
+ header[1] = 25;
5202
+ header.writeInt32LE(rowNumber, 2);
5203
+ header[10] = 44;
5204
+ header[11] = 1;
5205
+ header[15] = 1;
5206
+ header.writeInt32LE(0, 19);
5207
+ header.writeInt32LE(0, 23);
5208
+ await write(header);
5209
+ for (const cell of cells) await write(cell);
5210
+ };
5211
+ if (headers !== void 0) {
5212
+ await writeRow(0, headers.map((value, column) => this._stringCellBytes(column, styleFallback === "general" ? 0 : headerStyles.get(column) ?? 0, value)));
5213
+ lastKeptEnd = writtenBytes;
5214
+ nextRow = 1;
5215
+ }
5216
+ for await (const sourceRow of rows) {
5217
+ if (typeof sourceRow === "string" || sourceRow instanceof Uint8Array) {
5218
+ throw new TypeError("Each row must be an iterable of cells, not text");
5219
+ }
5220
+ const row = Array.from(sourceRow);
5221
+ const cells = row.map((value, column) => value === null || value === void 0 ? null : this._valueCellBytes(value, column, dataStyles, dateXf)).filter((cell) => cell !== null);
5222
+ await writeRow(nextRow, cells);
5223
+ dataRowsSeen++;
5224
+ nextRow++;
5225
+ if (row.every((value) => value === null || value === void 0)) {
5226
+ pendingEmptyWidth = Math.max(pendingEmptyWidth, row.length);
5227
+ } else {
5228
+ width = Math.max(width, pendingEmptyWidth, row.length);
5229
+ pendingEmptyWidth = 0;
5230
+ keptDataRows = dataRowsSeen;
5231
+ lastKeptEnd = writtenBytes;
5232
+ }
5233
+ }
5234
+ await new Promise((resolve4, reject) => {
5235
+ rowOutput.once("close", resolve4);
5236
+ rowOutput.once("error", reject);
5237
+ rowOutput.end();
5238
+ });
5239
+ if (outputError) throw outputError;
5240
+ const rowsFd = fs6.openSync(rowsPath, "r+");
5241
+ try {
5242
+ fs6.ftruncateSync(rowsFd, lastKeptEnd);
5243
+ } finally {
5244
+ fs6.closeSync(rowsFd);
5245
+ }
5246
+ const lastCol = Math.max(0, width - 1);
5247
+ const rowPatchFd = fs6.openSync(rowsPath, "r+");
5248
+ try {
5249
+ const lastColBuffer = Buffer.alloc(4);
5250
+ lastColBuffer.writeInt32LE(lastCol, 0);
5251
+ for (const offset of rowHeaderOffsets) {
5252
+ if (offset >= lastKeptEnd) continue;
5253
+ fs6.writeSync(rowPatchFd, lastColBuffer, 0, lastColBuffer.length, offset + 23);
5254
+ }
5255
+ } finally {
5256
+ fs6.closeSync(rowPatchFd);
5257
+ }
5258
+ const { rowsStart, rowsEnd, autoFilterRanges } = this._findRowsRegion(oldSheet);
5259
+ const oldLength = oldSheet.length;
5260
+ const newRowsLength = fs6.statSync(rowsPath).size;
5261
+ const delta = newRowsLength - (rowsEnd - rowsStart);
5262
+ const outputFd = fs6.openSync(outputPath, "w");
5263
+ try {
5264
+ fs6.writeSync(outputFd, oldSheet, 0, rowsStart, null);
5265
+ const rowsFdRead = fs6.openSync(rowsPath, "r");
5266
+ try {
5267
+ const chunk = Buffer.alloc(1024 * 1024);
5268
+ let position = 0;
5269
+ while (position < newRowsLength) {
5270
+ const count = fs6.readSync(rowsFdRead, chunk, 0, Math.min(chunk.length, newRowsLength - position), position);
5271
+ if (count <= 0) throw new Error("XlsbUpdater: failed to read staged rows.");
5272
+ fs6.writeSync(outputFd, chunk, 0, count);
5273
+ position += count;
5274
+ }
5275
+ } finally {
5276
+ fs6.closeSync(rowsFdRead);
5277
+ }
5278
+ fs6.writeSync(outputFd, oldSheet, rowsEnd, oldLength - rowsEnd, rowsStart + newRowsLength);
5279
+ } finally {
5280
+ fs6.closeSync(outputFd);
5281
+ }
5282
+ this._patchSheetMetadataFile(
5283
+ outputPath,
5284
+ oldSheet,
5285
+ rowsEnd,
5286
+ delta,
5287
+ autoFilterRanges,
5288
+ Math.max(0, keptDataRows + (headers !== void 0 ? 1 : 0) - 1),
5289
+ lastCol
5290
+ );
5291
+ this._stagePart(sheetPath, outputPath);
5292
+ this._commitSharedStrings();
5293
+ this._patchPivotCaches(
5294
+ sheetName,
5295
+ Math.max(0, keptDataRows + (headers !== void 0 ? 1 : 0) - 1),
5296
+ lastCol
5297
+ );
5298
+ if (oldStage && oldStage !== outputPath) this._removeTemporaryFile(oldStage);
5299
+ createdParts.splice(createdParts.indexOf(outputPath), 1);
5300
+ } catch (error) {
5301
+ this.sharedStringsValues.length = beforeValuesLength;
5302
+ this._appendCount = beforeAppendCount;
5303
+ this.sharedStringsTotal = beforeSharedStringsTotal;
5304
+ this.sharedStringsUnique = beforeSharedStringsUnique;
5305
+ this.sharedStringsEndSst = beforeSharedStringsEndSst;
5306
+ this.sharedStringsDirty = beforeSharedStringsDirty;
5307
+ this.sharedStringsBuf = beforeSharedStringsBuf;
5308
+ this.stringIndexMap.clear();
5309
+ for (const [value, index] of beforeStringIndexMap) this.stringIndexMap.set(value, index);
5310
+ if (beforeSharedStringsBytes) {
5311
+ this.zip.updateFile("xl/sharedStrings.bin", beforeSharedStringsBytes);
5312
+ }
5313
+ if (oldStage) this.stagedParts.set(sheetPath, oldStage);
5314
+ else this.stagedParts.delete(sheetPath);
5315
+ throw error;
5316
+ } finally {
5317
+ if (output && !output.closed) output.destroy();
5318
+ for (const temporaryPath of createdParts) this._removeTemporaryFile(temporaryPath);
5319
+ }
5320
+ }
4509
5321
  /**
4510
5322
  * Write the updated workbook to disk.
4511
5323
  * @param outputPath Destination path; defaults to the source file (overwrites in place).
4512
5324
  */
4513
5325
  save(outputPath) {
4514
5326
  const target = outputPath ?? this.sourcePath;
4515
- fs4.writeFileSync(target, this.zip.toBuffer());
5327
+ this._materializeStagedParts();
5328
+ writeBufferAtomically(this.zip.toBuffer(), target);
5329
+ }
5330
+ /** Save staged streaming replacements without materialising the ZIP. */
5331
+ async saveStreaming(outputPath) {
5332
+ const target = outputPath ?? this.sourcePath;
5333
+ await writeAdmZipStreaming(this.zip, this.stagedParts, target);
5334
+ this._materializeStagedParts();
4516
5335
  }
4517
5336
  /** The updated workbook as an in-memory ZIP buffer. */
4518
5337
  toBuffer() {
5338
+ this._materializeStagedParts();
4519
5339
  return this.zip.toBuffer();
4520
5340
  }
5341
+ /**
5342
+ * Release the staging directory and any unsaved staged parts.
5343
+ * The updater remains usable; a new staging directory is created lazily
5344
+ * on the next streaming replacement.
5345
+ */
5346
+ dispose() {
5347
+ for (const stagedPath of this.stagedParts.values()) {
5348
+ try {
5349
+ fs6.rmSync(stagedPath, { force: true });
5350
+ } catch {
5351
+ }
5352
+ }
5353
+ this.stagedParts.clear();
5354
+ if (this.temporaryDirectory !== null) {
5355
+ try {
5356
+ fs6.rmSync(this.temporaryDirectory, { recursive: true, force: true });
5357
+ } catch {
5358
+ }
5359
+ this.temporaryDirectory = null;
5360
+ }
5361
+ }
5362
+ /** Support `using` declarations for automatic staging cleanup. */
5363
+ [Symbol.dispose]() {
5364
+ this.dispose();
5365
+ }
5366
+ _ensureTemporaryDirectory() {
5367
+ if (this.temporaryDirectory === null) {
5368
+ this.temporaryDirectory = fs6.mkdtempSync(path5.join(os2.tmpdir(), "spreadsheet-updater-"));
5369
+ } else {
5370
+ try {
5371
+ fs6.mkdirSync(this.temporaryDirectory, { recursive: true });
5372
+ } catch {
5373
+ }
5374
+ }
5375
+ return this.temporaryDirectory;
5376
+ }
5377
+ _temporaryPath(suffix) {
5378
+ return path5.join(this._ensureTemporaryDirectory(), `part-${Date.now()}-${Math.random().toString(16).slice(2)}${suffix}`);
5379
+ }
5380
+ _removeTemporaryFile(filePath) {
5381
+ try {
5382
+ fs6.rmSync(filePath);
5383
+ } catch (error) {
5384
+ if (error.code !== "ENOENT") throw error;
5385
+ }
5386
+ }
5387
+ _entryData(name) {
5388
+ const stagedPath = this.stagedParts.get(name);
5389
+ if (stagedPath) return fs6.readFileSync(stagedPath);
5390
+ const entry = this.zip.getEntry(name);
5391
+ if (!entry) throw new Error(`XlsbUpdater: ZIP member missing: ${name}`);
5392
+ return entry.getData();
5393
+ }
5394
+ _stagePart(name, filePath) {
5395
+ this.stagedParts.set(name, filePath);
5396
+ }
5397
+ _discardStagedPart(name) {
5398
+ const stagedPath = this.stagedParts.get(name);
5399
+ if (!stagedPath) return;
5400
+ this._removeTemporaryFile(stagedPath);
5401
+ this.stagedParts.delete(name);
5402
+ }
5403
+ _materializeStagedParts() {
5404
+ for (const [name, stagedPath] of this.stagedParts) {
5405
+ this.zip.updateFile(name, fs6.readFileSync(stagedPath));
5406
+ this._removeTemporaryFile(stagedPath);
5407
+ }
5408
+ this.stagedParts.clear();
5409
+ }
4521
5410
  // ------------------------------------------------------------------
4522
5411
  // Workbook structure
4523
5412
  // ------------------------------------------------------------------
@@ -4567,11 +5456,11 @@ var XlsbUpdater = class {
4567
5456
  return;
4568
5457
  }
4569
5458
  const entry = this.zip.getEntry("xl/sharedStrings.bin");
4570
- if (!entry) {
5459
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.bin")) {
4571
5460
  this.sharedStringsBuf = null;
4572
5461
  return;
4573
5462
  }
4574
- this.sharedStringsBuf = entry.getData();
5463
+ this.sharedStringsBuf = this._entryData("xl/sharedStrings.bin");
4575
5464
  const parsed = parseSharedStringsBin(this.sharedStringsBuf);
4576
5465
  this.sharedStringsValues = parsed.values;
4577
5466
  this.sharedStringsTotal = parsed.total;
@@ -4583,9 +5472,10 @@ var XlsbUpdater = class {
4583
5472
  }
4584
5473
  });
4585
5474
  this._appendCount = 0;
5475
+ this.sharedStringsDirty = false;
4586
5476
  }
4587
5477
  _commitSharedStrings() {
4588
- if (this.sharedStringsBuf === null || this._appendCount === 0) return;
5478
+ if (this.sharedStringsBuf === null || !this.sharedStringsDirty) return;
4589
5479
  const originalValuesLength = this.sharedStringsValues.length - this._appendCount;
4590
5480
  let newItems = Buffer.alloc(0);
4591
5481
  for (let i = originalValuesLength; i < this.sharedStringsValues.length; i++) {
@@ -4611,6 +5501,9 @@ var XlsbUpdater = class {
4611
5501
  pos = rec.dataEnd;
4612
5502
  }
4613
5503
  this.zip.updateFile("xl/sharedStrings.bin", out);
5504
+ this.sharedStringsBuf = out;
5505
+ this._appendCount = 0;
5506
+ this.sharedStringsDirty = false;
4614
5507
  }
4615
5508
  // ------------------------------------------------------------------
4616
5509
  // Styles
@@ -4793,12 +5686,51 @@ var XlsbUpdater = class {
4793
5686
  this._appendCount++;
4794
5687
  }
4795
5688
  this.sharedStringsTotal++;
5689
+ this.sharedStringsDirty = true;
4796
5690
  const payload = Buffer.alloc(12);
4797
5691
  payload.writeUInt32LE(col, 0);
4798
5692
  payload.writeUInt32LE(style, 4);
4799
5693
  payload.writeUInt32LE(index, 8);
4800
5694
  return buildRecord(7, payload);
4801
5695
  }
5696
+ _sharedStringRefCount(sheetBuf) {
5697
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
5698
+ let pos = 0;
5699
+ let count = 0;
5700
+ while (readRecord(sheetBuf, pos, rec)) {
5701
+ if (rec.id === 7) count++;
5702
+ pos = rec.dataEnd;
5703
+ }
5704
+ return count;
5705
+ }
5706
+ _patchSheetMetadataFile(filePath, originalSheet, rowsEnd, delta, autoFilterRanges, lastRowIdx, lastCol) {
5707
+ const fd = fs6.openSync(filePath, "r+");
5708
+ try {
5709
+ const patchInt32 = (offset, value) => {
5710
+ const bytes = Buffer.alloc(4);
5711
+ bytes.writeInt32LE(value, 0);
5712
+ fs6.writeSync(fd, bytes, 0, bytes.length, offset);
5713
+ };
5714
+ for (const filterOffset of autoFilterRanges) {
5715
+ const newOffset = filterOffset >= rowsEnd ? filterOffset + delta : filterOffset;
5716
+ patchInt32(newOffset + 4, lastRowIdx);
5717
+ patchInt32(newOffset + 12, lastCol);
5718
+ }
5719
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
5720
+ let pos = 0;
5721
+ while (readRecord(originalSheet, pos, rec)) {
5722
+ if (rec.id === 152 && rec.len >= 36) {
5723
+ const shift = rec.dataStart >= rowsEnd ? delta : 0;
5724
+ patchInt32(rec.dataStart + shift + 24, lastRowIdx);
5725
+ patchInt32(rec.dataStart + shift + 32, lastCol);
5726
+ break;
5727
+ }
5728
+ pos = rec.dataEnd;
5729
+ }
5730
+ } finally {
5731
+ fs6.closeSync(fd);
5732
+ }
5733
+ }
4802
5734
  // ------------------------------------------------------------------
4803
5735
  // Rows region detection
4804
5736
  // ------------------------------------------------------------------
@@ -4850,8 +5782,8 @@ var XlsbUpdater = class {
4850
5782
  // Pivot cache integration
4851
5783
  // ------------------------------------------------------------------
4852
5784
  _patchPivotCaches(sheetName, rwLast, colLast) {
4853
- for (const path2 of this.pivotCacheDefPaths) {
4854
- const entry = this.zip.getEntry(path2);
5785
+ for (const path7 of this.pivotCacheDefPaths) {
5786
+ const entry = this.zip.getEntry(path7);
4855
5787
  if (!entry) continue;
4856
5788
  const buf = Buffer.from(entry.getData());
4857
5789
  const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
@@ -4883,11 +5815,188 @@ var XlsbUpdater = class {
4883
5815
  }
4884
5816
  }
4885
5817
  if (patched) {
4886
- this.zip.updateFile(path2, buf);
5818
+ this.zip.updateFile(path7, buf);
4887
5819
  }
4888
5820
  }
4889
5821
  }
4890
5822
  };
5823
+
5824
+ // src/ExcelConverter.ts
5825
+ import * as fs7 from "fs";
5826
+ import * as path6 from "path";
5827
+ import { randomUUID as randomUUID3 } from "crypto";
5828
+ import { spawn } from "child_process";
5829
+ var POWERSHELL_CONVERSION_SCRIPT = `
5830
+ $ErrorActionPreference = 'Stop'
5831
+ $inputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_INPUT')
5832
+ $outputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_OUTPUT')
5833
+ $fileFormat = [int][Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_FORMAT')
5834
+ $excel = $null
5835
+ $workbook = $null
5836
+ $exitCode = 0
5837
+
5838
+ try {
5839
+ $excel = New-Object -ComObject Excel.Application
5840
+ $excel.Visible = $false
5841
+ $excel.DisplayAlerts = $false
5842
+ $excel.ScreenUpdating = $false
5843
+
5844
+ # Open read-only so the source workbook is never changed by the conversion.
5845
+ $workbook = $excel.Workbooks.Open($inputPath, 0, $true)
5846
+ if ($null -eq $workbook) {
5847
+ throw 'Excel did not return a workbook when opening the source file.'
5848
+ }
5849
+
5850
+ $workbook.SaveAs($outputPath, $fileFormat)
5851
+ } catch {
5852
+ $exitCode = 1
5853
+ [Console]::Error.WriteLine($_.Exception.ToString())
5854
+ } finally {
5855
+ if ($null -ne $workbook) {
5856
+ try { $workbook.Close($false) } catch { }
5857
+ try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook) | Out-Null } catch { }
5858
+ }
5859
+ if ($null -ne $excel) {
5860
+ try { $excel.Quit() } catch { }
5861
+ try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null } catch { }
5862
+ }
5863
+ [GC]::Collect()
5864
+ [GC]::WaitForPendingFinalizers()
5865
+ }
5866
+
5867
+ exit $exitCode
5868
+ `;
5869
+ function errorMessage2(error) {
5870
+ return error instanceof Error ? error.message : String(error);
5871
+ }
5872
+ function validateConversionPaths(inputPath, outputPath, inputExtension, outputExtension, overwrite) {
5873
+ const input = path6.resolve(inputPath);
5874
+ const output = path6.resolve(outputPath);
5875
+ if (path6.extname(input).toLowerCase() !== inputExtension) {
5876
+ throw new Error(
5877
+ `Excel conversion: source must have the ${inputExtension} extension: ${inputPath}`
5878
+ );
5879
+ }
5880
+ if (path6.extname(output).toLowerCase() !== outputExtension) {
5881
+ throw new Error(
5882
+ `Excel conversion: destination must have the ${outputExtension} extension: ${outputPath}`
5883
+ );
5884
+ }
5885
+ if (!fs7.existsSync(input)) {
5886
+ throw new Error(`Excel conversion: source file not found: ${inputPath}`);
5887
+ }
5888
+ if (!fs7.statSync(input).isFile()) {
5889
+ throw new Error(`Excel conversion: source path is not a file: ${inputPath}`);
5890
+ }
5891
+ if (!fs7.existsSync(path6.dirname(output))) {
5892
+ throw new Error(`Excel conversion: destination directory not found: ${path6.dirname(output)}`);
5893
+ }
5894
+ if (fs7.existsSync(output)) {
5895
+ if (fs7.statSync(output).isDirectory()) {
5896
+ throw new Error(`Excel conversion: destination path is a directory: ${outputPath}`);
5897
+ }
5898
+ if (!overwrite) {
5899
+ throw new Error(
5900
+ `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${outputPath}`
5901
+ );
5902
+ }
5903
+ }
5904
+ if (process.platform === "win32" && input.toLowerCase() === output.toLowerCase()) {
5905
+ throw new Error("Excel conversion: source and destination must be different files.");
5906
+ }
5907
+ return { input, output };
5908
+ }
5909
+ function runPowerShellConversion(inputPath, outputPath, fileFormat) {
5910
+ const encodedCommand = Buffer.from(POWERSHELL_CONVERSION_SCRIPT, "utf16le").toString("base64");
5911
+ const environment = {
5912
+ ...process.env,
5913
+ JUSTYBASE_EXCEL_INPUT: inputPath,
5914
+ JUSTYBASE_EXCEL_OUTPUT: outputPath,
5915
+ JUSTYBASE_EXCEL_FORMAT: String(fileFormat)
5916
+ };
5917
+ return new Promise((resolve4, reject) => {
5918
+ var _a, _b, _c, _d;
5919
+ let stdout = "";
5920
+ let stderr = "";
5921
+ let settled = false;
5922
+ const child = spawn(
5923
+ "powershell.exe",
5924
+ [
5925
+ "-NoLogo",
5926
+ "-NoProfile",
5927
+ "-NonInteractive",
5928
+ "-ExecutionPolicy",
5929
+ "Bypass",
5930
+ "-EncodedCommand",
5931
+ encodedCommand
5932
+ ],
5933
+ { env: environment, windowsHide: true }
5934
+ );
5935
+ (_a = child.stdout) == null ? void 0 : _a.setEncoding("utf8");
5936
+ (_b = child.stderr) == null ? void 0 : _b.setEncoding("utf8");
5937
+ (_c = child.stdout) == null ? void 0 : _c.on("data", (chunk) => {
5938
+ stdout += chunk;
5939
+ });
5940
+ (_d = child.stderr) == null ? void 0 : _d.on("data", (chunk) => {
5941
+ stderr += chunk;
5942
+ });
5943
+ child.once("error", (error) => {
5944
+ if (settled) return;
5945
+ settled = true;
5946
+ reject(new Error(`Excel conversion: unable to start PowerShell/COM: ${error.message}`));
5947
+ });
5948
+ child.once("close", (code) => {
5949
+ if (settled) return;
5950
+ settled = true;
5951
+ if (code === 0) {
5952
+ resolve4();
5953
+ return;
5954
+ }
5955
+ const details = (stderr || stdout).replace(/\s+/g, " ").trim();
5956
+ reject(new Error(
5957
+ `Excel conversion failed (PowerShell exit code ${code ?? "unknown"})${details ? `: ${details}` : "."}`
5958
+ ));
5959
+ });
5960
+ });
5961
+ }
5962
+ async function convertExcelFile(inputPath, outputPath, inputExtension, outputExtension, fileFormat, options = {}) {
5963
+ const { input, output } = validateConversionPaths(
5964
+ inputPath,
5965
+ outputPath,
5966
+ inputExtension,
5967
+ outputExtension,
5968
+ options.overwrite === true
5969
+ );
5970
+ if (process.platform !== "win32") {
5971
+ throw new Error("Excel conversion requires Windows with desktop Microsoft Excel and COM automation.");
5972
+ }
5973
+ const temporaryOutput = path6.join(
5974
+ path6.dirname(output),
5975
+ `.${path6.basename(output, outputExtension)}.${randomUUID3()}.conversion${outputExtension}`
5976
+ );
5977
+ try {
5978
+ await runPowerShellConversion(input, temporaryOutput, fileFormat);
5979
+ if (!fs7.existsSync(temporaryOutput)) {
5980
+ throw new Error("Excel conversion completed without creating the destination file.");
5981
+ }
5982
+ installTemporaryOutput(temporaryOutput, output, options.overwrite === true);
5983
+ } catch (error) {
5984
+ throw new Error(`Excel conversion ${inputExtension} -> ${outputExtension}: ${errorMessage2(error)}`);
5985
+ } finally {
5986
+ if (fs7.existsSync(temporaryOutput)) {
5987
+ try {
5988
+ fs7.rmSync(temporaryOutput);
5989
+ } catch {
5990
+ }
5991
+ }
5992
+ }
5993
+ }
5994
+ function convertXlsbToXlsx(inputPath, outputPath, options) {
5995
+ return convertExcelFile(inputPath, outputPath, ".xlsb", ".xlsx", 51, options);
5996
+ }
5997
+ function convertXlsxToXlsb(inputPath, outputPath, options) {
5998
+ return convertExcelFile(inputPath, outputPath, ".xlsx", ".xlsb", 50, options);
5999
+ }
4891
6000
  export {
4892
6001
  BiffReaderWriter,
4893
6002
  BigBuffer,
@@ -4899,10 +6008,13 @@ export {
4899
6008
  XlsbReader,
4900
6009
  XlsbUpdater,
4901
6010
  XlsbWriter,
6011
+ XlsxUpdater as XlsmUpdater,
4902
6012
  XlsxReader,
4903
6013
  XlsxUpdater,
4904
6014
  XlsxWriter,
4905
6015
  applyHeaderWidths,
6016
+ convertXlsbToXlsx,
6017
+ convertXlsxToXlsb,
4906
6018
  defaultColWidth,
4907
6019
  getFormat,
4908
6020
  initColWidths,