@js-ak/excel-toolbox 1.5.0 → 1.6.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.
Files changed (25) hide show
  1. package/README.md +41 -62
  2. package/build/cjs/lib/template/template-fs.js +137 -57
  3. package/build/cjs/lib/template/template-memory.js +281 -59
  4. package/build/cjs/lib/template/utils/index.js +25 -0
  5. package/build/cjs/lib/template/utils/prepare-row-to-cells.js +5 -1
  6. package/build/cjs/lib/template/utils/regexp.js +32 -0
  7. package/build/cjs/lib/template/utils/update-dimension.js +15 -0
  8. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
  9. package/build/cjs/lib/template/utils/write-rows-to-stream.js +57 -17
  10. package/build/esm/lib/template/template-fs.js +134 -57
  11. package/build/esm/lib/template/template-memory.js +281 -59
  12. package/build/esm/lib/template/utils/index.js +2 -0
  13. package/build/esm/lib/template/utils/prepare-row-to-cells.js +5 -1
  14. package/build/esm/lib/template/utils/regexp.js +28 -0
  15. package/build/esm/lib/template/utils/update-dimension.js +15 -0
  16. package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
  17. package/build/esm/lib/template/utils/write-rows-to-stream.js +57 -17
  18. package/build/types/lib/template/template-fs.d.ts +2 -0
  19. package/build/types/lib/template/template-memory.d.ts +61 -0
  20. package/build/types/lib/template/utils/index.d.ts +2 -0
  21. package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -1
  22. package/build/types/lib/template/utils/regexp.d.ts +24 -0
  23. package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
  24. package/build/types/lib/template/utils/write-rows-to-stream.d.ts +22 -9
  25. package/package.json +1 -1
package/README.md CHANGED
@@ -2,100 +2,79 @@
2
2
 
3
3
  ![ci-cd](https://github.com/JS-AK/excel-toolbox/actions/workflows/ci-cd-master.yml/badge.svg)
4
4
 
5
- A lightweight toolkit for merging sheets from multiple `.xlsx` Excel files without dependencies.
5
+ 📘 **Docs:** [js-ak.github.io/excel-toolbox](https://js-ak.github.io/excel-toolbox/)
6
6
 
7
- ## Installation
7
+ A lightweight toolkit for working with `.xlsx` Excel files — modify templates, merge sheets, and handle massive datasets without dependencies.
8
8
 
9
- You can install the package via npm:
9
+ ## Installation
10
10
 
11
11
  ```bash
12
12
  npm install @js-ak/excel-toolbox
13
13
  ```
14
14
 
15
- ## Getting Started
16
-
17
- To merge rows from multiple Excel files into one:
15
+ ## Features
18
16
 
19
- ```ts
20
- import fs from "node:fs";
21
- import { mergeSheetsToBaseFileSync } from "@js-ak/excel-toolbox";
17
+ - ✨ Work with templates using `TemplateFs` (filesystem) or `TemplateMemory` (in-memory)
18
+ - 📥 Insert and stream rows into Excel files
19
+ - 🧩 Merge sheets from multiple `.xlsx` files
20
+ - 🧼 Remove sheets by name or index
21
+ - 💎 Preserve styles, merges, and shared strings
22
22
 
23
- const baseFile = fs.readFileSync("base.xlsx");
24
- const otherFile = fs.readFileSync("data.xlsx");
23
+ ## Template API
25
24
 
26
- const resultBuffer = mergeSheetsToBaseFileSync({
27
- baseFile,
28
- additions: [
29
- { file: otherFile, sheetIndexes: [1] }
30
- ],
31
- gap: 2,
32
- });
25
+ ### `TemplateFs` and `TemplateMemory`
33
26
 
34
- fs.writeFileSync("output.xlsx", resultBuffer);
35
- ```
27
+ Both classes provide the same API for modifying Excel templates.
36
28
 
37
- ## Features
29
+ #### Common Features
38
30
 
39
- - 🧩 **Merge sheets** from multiple Excel files
40
- - 🧼 **Clean sheet removal** by name or index
41
- - 📎 **Keeps styles and merged cells**
42
- - 🍃 **Lightweight ZIP and XML handling**
31
+ - `substitute()` replace placeholders like `${name}` or `${table:name}`
32
+ - `insertRows()` / `insertRowsStream()`insert rows statically or via stream
33
+ - `copySheet()` duplicate existing sheets
34
+ - `validate()` and `save()` / `saveStream()` — output the result
43
35
 
44
- ## API
36
+ ```ts
37
+ import { TemplateFs } from "@js-ak/excel-toolbox";
45
38
 
46
- ### `mergeSheetsToBaseFileSync(options): Buffer`
39
+ const template = await TemplateFs.from({
40
+ destination: "/tmp/template",
41
+ source: fs.readFileSync("template.xlsx"),
42
+ });
47
43
 
48
- #### Parameters
44
+ await template.substitute("Sheet1", { name: "Alice" });
45
+ await template.insertRows({ sheetName: "Sheet1", rows: [["Data"]] });
46
+ const buffer = await template.save();
47
+ fs.writeFileSync("output.xlsx", buffer);
48
+ ```
49
49
 
50
- | Name | Type | Description |
51
- |-----------------------|--------------------------------------------------------------------|------------------------------------------------|
52
- | `baseFile` | `Buffer` | The base Excel file. |
53
- | `additions` | `{ file: Buffer; sheetIndexes: number[]; isBaseFile?: boolean }[]` | Files and sheet indices to merge. |
54
- | `baseSheetIndex` | `number` (default: `1`) | The sheet index in the base file to append to. |
55
- | `gap` | `number` (default: `0`) | Empty rows inserted between merged blocks. |
56
- | `sheetNamesToRemove` | `string[]` (default: `[]`) | Sheets to remove by name. |
57
- | `sheetsToRemove` | `number[]` (default: `[]`) | Sheets to remove by index (1-based). |
50
+ ## Sheet Merging API
58
51
 
59
- #### Returns
52
+ ### `mergeSheetsToBaseFileSync(options): Buffer`
60
53
 
61
- `Buffer` the merged Excel file.
54
+ Synchronously merges sheets into a base file.
62
55
 
63
56
  ### `mergeSheetsToBaseFile(options): Promise<Buffer>`
64
57
 
65
- Asynchronous version of `mergeSheetsToBaseFileSync`.
66
-
67
- #### Parameters
68
-
69
- Same as [`mergeSheetsToBaseFileSync`](#mergesheetstobasefilesyncoptions).
70
-
71
- #### Returns
72
-
73
- `Promise<Buffer>` — resolves with the merged Excel file.
58
+ Async version of the above.
74
59
 
75
60
  #### Example
76
61
 
77
62
  ```ts
78
- import fs from "node:fs/promises";
79
- import { mergeSheetsToBaseFile } from "@js-ak/excel-toolbox";
63
+ import fs from "node:fs";
64
+ import { mergeSheetsToBaseFileSync } from "@js-ak/excel-toolbox";
80
65
 
81
- const baseFile = await fs.readFile("base.xlsx");
82
- const otherFile = await fs.readFile("data.xlsx");
66
+ const baseFile = fs.readFileSync("base.xlsx");
67
+ const dataFile = fs.readFileSync("data.xlsx");
83
68
 
84
- const output = await mergeSheetsToBaseFile({
69
+ const result = mergeSheetsToBaseFileSync({
85
70
  baseFile,
86
- additions: [
87
- { file: otherFile, sheetIndexes: [1] }
88
- ],
89
- gap: 1,
71
+ additions: [{ file: dataFile, sheetIndexes: [1] }],
72
+ gap: 2,
90
73
  });
91
74
 
92
- await fs.writeFile("output.xlsx", output);
75
+ fs.writeFileSync("output.xlsx", result);
93
76
  ```
94
77
 
95
- ## Contributing
96
-
97
- Contributions are welcome! Feel free to open an issue or submit a pull request if you have ideas or encounter bugs.
98
-
99
78
  ## License
100
79
 
101
- MIT — see [LICENSE](./LICENSE) for details.
80
+ MIT — see [LICENSE](./LICENSE)
@@ -32,12 +32,16 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.TemplateFs = void 0;
37
40
  const fs = __importStar(require("node:fs/promises"));
38
41
  const fsSync = __importStar(require("node:fs"));
39
42
  const path = __importStar(require("node:path"));
40
43
  const readline = __importStar(require("node:readline"));
44
+ const node_crypto_1 = __importDefault(require("node:crypto"));
41
45
  const Xml = __importStar(require("../xml/index.js"));
42
46
  const Zip = __importStar(require("../zip/index.js"));
43
47
  const Utils = __importStar(require("./utils/index.js"));
@@ -67,6 +71,16 @@ class TemplateFs {
67
71
  * @type {boolean}
68
72
  */
69
73
  #isProcessing = false;
74
+ /**
75
+ * The keys for the Excel files in the template.
76
+ */
77
+ #excelKeys = {
78
+ contentTypes: "[Content_Types].xml",
79
+ sharedStrings: "xl/sharedStrings.xml",
80
+ styles: "xl/styles.xml",
81
+ workbook: "xl/workbook.xml",
82
+ workbookRels: "xl/_rels/workbook.xml.rels",
83
+ };
70
84
  /**
71
85
  * Creates a Template instance.
72
86
  *
@@ -149,17 +163,19 @@ class TemplateFs {
149
163
  * @returns The path of the sheet inside the workbook.
150
164
  * @throws {Error} If the sheet is not found.
151
165
  */
152
- async #getSheetPath(sheetName) {
166
+ async #getSheetPathByName(sheetName) {
153
167
  // Read XML workbook to find sheet name and path
154
- const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
155
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
156
- if (!sheetMatch)
168
+ const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbook));
169
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
170
+ if (!sheetMatch || !sheetMatch[1]) {
157
171
  throw new Error(`Sheet "${sheetName}" not found`);
172
+ }
158
173
  const rId = sheetMatch[1];
159
- const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
160
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
161
- if (!relMatch)
174
+ const relsXml = Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbookRels));
175
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
176
+ if (!relMatch || !relMatch[1]) {
162
177
  throw new Error(`Relationship "${rId}" not found`);
178
+ }
163
179
  return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
164
180
  }
165
181
  /**
@@ -209,8 +225,8 @@ class TemplateFs {
209
225
  await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
210
226
  }
211
227
  async #substitute(sheetName, replacements) {
212
- const sharedStringsPath = "xl/sharedStrings.xml";
213
- const sheetPath = await this.#getSheetPath(sheetName);
228
+ const sharedStringsPath = this.#excelKeys.sharedStrings;
229
+ const sheetPath = await this.#getSheetPathByName(sheetName);
214
230
  let sharedStringsContent = "";
215
231
  let sheetContent = "";
216
232
  if (this.fileKeys.has(sharedStringsPath)) {
@@ -269,30 +285,31 @@ class TemplateFs {
269
285
  this.#ensureNotDestroyed();
270
286
  this.#isProcessing = true;
271
287
  try {
288
+ if (sourceName === newName) {
289
+ throw new Error("Source and new sheet names cannot be the same");
290
+ }
272
291
  // Read workbook.xml and find the source sheet
273
- const workbookXmlPath = "xl/workbook.xml";
292
+ const workbookXmlPath = this.#excelKeys.workbook;
274
293
  const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
275
294
  // Find the source sheet
276
- const sheetRegex = new RegExp(`<sheet[^>]+name="${sourceName}"[^>]+r:id="([^"]+)"[^>]*/>`);
277
- const sheetMatch = workbookXml.match(sheetRegex);
278
- if (!sheetMatch)
295
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
296
+ if (!sheetMatch || !sheetMatch[1]) {
279
297
  throw new Error(`Sheet "${sourceName}" not found`);
280
- const sourceRId = sheetMatch[1];
298
+ }
281
299
  // Check if a sheet with the new name already exists
282
300
  if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
283
301
  throw new Error(`Sheet "${newName}" already exists`);
284
302
  }
285
303
  // Read workbook.rels
286
304
  // Find the source sheet path by rId
287
- const relsXmlPath = "xl/_rels/workbook.xml.rels";
305
+ const rId = sheetMatch[1];
306
+ const relsXmlPath = this.#excelKeys.workbookRels;
288
307
  const relsXml = Xml.extractXmlFromSheet(await this.#readFile(relsXmlPath));
289
- const relRegex = new RegExp(`<Relationship[^>]+Id="${sourceRId}"[^>]+Target="([^"]+)"[^>]*/>`);
290
- const relMatch = relsXml.match(relRegex);
291
- if (!relMatch)
292
- throw new Error(`Relationship "${sourceRId}" not found`);
308
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
309
+ if (!relMatch || !relMatch[1]) {
310
+ throw new Error(`Relationship "${rId}" not found`);
311
+ }
293
312
  const sourceTarget = relMatch[1]; // sheetN.xml
294
- if (!sourceTarget)
295
- throw new Error(`Relationship "${sourceRId}" not found`);
296
313
  const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
297
314
  // Get the index of the new sheet
298
315
  const sheetNumbers = [...this.fileKeys]
@@ -321,7 +338,7 @@ class TemplateFs {
321
338
  await this.#set(relsXmlPath, updatedRelsXml);
322
339
  // Read [Content_Types].xml
323
340
  // Update [Content_Types].xml
324
- const contentTypesPath = "[Content_Types].xml";
341
+ const contentTypesPath = this.#excelKeys.contentTypes;
325
342
  const contentTypesXml = Xml.extractXmlFromSheet(await this.#readFile(contentTypesPath));
326
343
  const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
327
344
  const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
@@ -379,18 +396,7 @@ class TemplateFs {
379
396
  Utils.checkStartRow(startRowNumber);
380
397
  Utils.checkRows(preparedRows);
381
398
  // Find the sheet
382
- const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
383
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
384
- if (!sheetMatch || !sheetMatch[1]) {
385
- throw new Error(`Sheet "${sheetName}" not found`);
386
- }
387
- const rId = sheetMatch[1];
388
- const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
389
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
390
- if (!relMatch || !relMatch[1]) {
391
- throw new Error(`Relationship "${rId}" not found`);
392
- }
393
- const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
399
+ const sheetPath = await this.#getSheetPathByName(sheetName);
394
400
  const sheetXmlRaw = await this.#readFile(sheetPath);
395
401
  const sheetXml = Xml.extractXmlFromSheet(sheetXmlRaw);
396
402
  let nextRow = 0;
@@ -426,7 +432,7 @@ class TemplateFs {
426
432
  else {
427
433
  updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
428
434
  }
429
- await this.#set(sheetPath, updatedXml);
435
+ await this.#set(sheetPath, Utils.updateDimension(updatedXml));
430
436
  }
431
437
  finally {
432
438
  this.#isProcessing = false;
@@ -454,18 +460,8 @@ class TemplateFs {
454
460
  const { rows, sheetName, startRowNumber } = data;
455
461
  if (!sheetName)
456
462
  throw new Error("Sheet name is required");
457
- // Read XML workbook to find sheet name and path
458
- const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
459
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
460
- if (!sheetMatch)
461
- throw new Error(`Sheet "${sheetName}" not found`);
462
- const rId = sheetMatch[1];
463
- const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
464
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
465
- if (!relMatch)
466
- throw new Error(`Relationship "${rId}" not found`);
467
- // Path to the desired sheet (sheet1.xml)
468
- const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
463
+ // Get the path to the sheet
464
+ const sheetPath = await this.#getSheetPathByName(sheetName);
469
465
  // The temporary file for writing
470
466
  const fullPath = path.join(this.destination, ...sheetPath.split("/"));
471
467
  const tempPath = fullPath + ".tmp";
@@ -474,6 +470,13 @@ class TemplateFs {
474
470
  const output = fsSync.createWriteStream(tempPath, { encoding: "utf-8" });
475
471
  // Inserted rows flag
476
472
  let inserted = false;
473
+ let initialDimension = "";
474
+ const dimension = {
475
+ maxColumn: "A",
476
+ maxRow: 1,
477
+ minColumn: "A",
478
+ minRow: 1,
479
+ };
477
480
  const rl = readline.createInterface({
478
481
  // Process all line breaks
479
482
  crlfDelay: Infinity,
@@ -482,6 +485,21 @@ class TemplateFs {
482
485
  let isCollecting = false;
483
486
  let collection = "";
484
487
  for await (const line of rl) {
488
+ // Process <dimension>
489
+ if (!initialDimension && /<dimension\s+ref="[^"]*"/.test(line)) {
490
+ const dimensionMatch = line.match(/<dimension\s+ref="([^"]*)"/);
491
+ if (dimensionMatch) {
492
+ const dimensionRef = dimensionMatch[1];
493
+ if (dimensionRef) {
494
+ const [min, max] = dimensionRef.split(":");
495
+ dimension.minColumn = min.slice(0, 1);
496
+ dimension.minRow = parseInt(min.slice(1));
497
+ dimension.maxColumn = max.slice(0, 1);
498
+ dimension.maxRow = parseInt(max.slice(1));
499
+ }
500
+ initialDimension = line.match(/<dimension\s+ref="[^"]*"/)?.[0] || "";
501
+ }
502
+ }
485
503
  // Collect lines between <sheetData> and </sheetData>
486
504
  if (!inserted && isCollecting) {
487
505
  collection += line;
@@ -508,7 +526,13 @@ class TemplateFs {
508
526
  output.write(innerRows);
509
527
  }
510
528
  }
511
- const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
529
+ const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
530
+ if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
531
+ dimension.maxColumn = newDimension.maxColumn;
532
+ }
533
+ if (newDimension.maxRow > dimension.maxRow) {
534
+ dimension.maxRow = newDimension.maxRow;
535
+ }
512
536
  if (innerRows) {
513
537
  const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
514
538
  if (filteredRows)
@@ -549,7 +573,13 @@ class TemplateFs {
549
573
  }
550
574
  }
551
575
  // new <row>
552
- const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
576
+ const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
577
+ if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
578
+ dimension.maxColumn = newDimension.maxColumn;
579
+ }
580
+ if (newDimension.maxRow > dimension.maxRow) {
581
+ dimension.maxRow = newDimension.maxRow;
582
+ }
553
583
  if (innerRows) {
554
584
  const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
555
585
  if (filteredRows) {
@@ -576,7 +606,13 @@ class TemplateFs {
576
606
  // Insert opening tag
577
607
  output.write("<sheetData>");
578
608
  // Prepare the rows
579
- await Utils.writeRowsToStream(output, rows, maxRowNumber);
609
+ const { dimension: newDimension } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
610
+ if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
611
+ dimension.maxColumn = newDimension.maxColumn;
612
+ }
613
+ if (newDimension.maxRow > dimension.maxRow) {
614
+ dimension.maxRow = newDimension.maxRow;
615
+ }
580
616
  // Insert closing tag
581
617
  output.write("</sheetData>");
582
618
  if (after) {
@@ -597,8 +633,42 @@ class TemplateFs {
597
633
  // Close the streams
598
634
  rl.close();
599
635
  output.end();
600
- // Move the temporary file to the original location
601
- await fs.rename(tempPath, fullPath);
636
+ // update dimension
637
+ {
638
+ const target = initialDimension;
639
+ const refRange = `${dimension.minColumn}${dimension.minRow}:${dimension.maxColumn}${dimension.maxRow}`;
640
+ const replacement = `<dimension ref="${refRange}"`;
641
+ let buffer = "";
642
+ let replaced = false;
643
+ const input = fsSync.createReadStream(tempPath, { encoding: "utf8" });
644
+ const output = fsSync.createWriteStream(fullPath);
645
+ await new Promise((resolve, reject) => {
646
+ input.on("data", chunk => {
647
+ buffer += chunk;
648
+ if (!replaced) {
649
+ const index = buffer.indexOf(target);
650
+ if (index !== -1) {
651
+ // Заменяем только первое вхождение
652
+ buffer = buffer.replace(target, replacement);
653
+ replaced = true;
654
+ }
655
+ }
656
+ output.write(buffer);
657
+ buffer = ""; // очищаем, т.к. мы уже записали
658
+ });
659
+ input.on("error", reject);
660
+ output.on("error", reject);
661
+ input.on("end", () => {
662
+ // на всякий случай дописываем остаток
663
+ if (buffer) {
664
+ output.write(buffer);
665
+ }
666
+ output.end();
667
+ resolve(true);
668
+ });
669
+ });
670
+ }
671
+ await fs.unlink(tempPath);
602
672
  }
603
673
  finally {
604
674
  this.#isProcessing = false;
@@ -699,29 +769,39 @@ class TemplateFs {
699
769
  * @param {Object} data - The data to create the template from.
700
770
  * @param {string} data.source - The path or buffer of the Excel file.
701
771
  * @param {string} data.destination - The path to save the template to.
772
+ * @param {boolean} data.isUniqueDestination - Whether to add a random UUID to the destination path.
702
773
  * @returns {Promise<Template>} A new Template instance.
703
774
  * @throws {Error} If reading or writing files fails.
704
775
  * @experimental This API is experimental and might change in future versions.
705
776
  */
706
777
  static async from(data) {
707
- const { destination, source } = data;
778
+ const { destination, isUniqueDestination = true, source } = data;
708
779
  if (!destination) {
709
780
  throw new Error("Destination is required");
710
781
  }
782
+ // add random uuid to destination
783
+ const destinationWithUuid = isUniqueDestination
784
+ ? path.join(destination, node_crypto_1.default.randomUUID())
785
+ : destination;
711
786
  const buffer = typeof source === "string"
712
787
  ? await fs.readFile(source)
713
788
  : source;
714
789
  const files = await Zip.read(buffer);
715
790
  // if destination exists, remove it
716
- await fs.rm(destination, { force: true, recursive: true });
791
+ await fs.rm(destinationWithUuid, { force: true, recursive: true });
717
792
  // Write all files to the file system, preserving exact paths
718
- await fs.mkdir(destination, { recursive: true });
793
+ await fs.mkdir(destinationWithUuid, { recursive: true });
719
794
  await Promise.all(Object.entries(files).map(async ([filePath, content]) => {
720
- const fullPath = path.join(destination, ...filePath.split("/"));
795
+ const fullPath = path.join(destinationWithUuid, ...filePath.split("/"));
721
796
  await fs.mkdir(path.dirname(fullPath), { recursive: true });
722
797
  await fs.writeFile(fullPath, content);
723
798
  }));
724
- return new TemplateFs(new Set(Object.keys(files)), destination);
799
+ return new TemplateFs(new Set(Object.keys(files)), destinationWithUuid);
725
800
  }
726
801
  }
727
802
  exports.TemplateFs = TemplateFs;
803
+ const compareColumns = (a, b) => {
804
+ if (a === b)
805
+ return 0;
806
+ return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
807
+ };