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