@js-ak/excel-toolbox 1.4.1 → 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.
- package/README.md +41 -62
- package/build/cjs/lib/template/index.js +1 -0
- package/build/cjs/lib/template/memory-write-stream.js +17 -0
- package/build/cjs/lib/template/template-fs.js +137 -57
- package/build/cjs/lib/template/template-memory.js +753 -0
- package/build/cjs/lib/template/utils/index.js +25 -0
- package/build/cjs/lib/template/utils/prepare-row-to-cells.js +17 -0
- package/build/cjs/lib/template/utils/regexp.js +32 -0
- package/build/cjs/lib/template/utils/update-dimension.js +15 -0
- package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
- package/build/cjs/lib/template/utils/write-rows-to-stream.js +66 -22
- package/build/esm/lib/template/index.js +1 -0
- package/build/esm/lib/template/memory-write-stream.js +13 -0
- package/build/esm/lib/template/template-fs.js +134 -57
- package/build/esm/lib/template/template-memory.js +716 -0
- package/build/esm/lib/template/utils/index.js +2 -0
- package/build/esm/lib/template/utils/prepare-row-to-cells.js +14 -0
- package/build/esm/lib/template/utils/regexp.js +28 -0
- package/build/esm/lib/template/utils/update-dimension.js +15 -0
- package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
- package/build/esm/lib/template/utils/write-rows-to-stream.js +66 -22
- package/build/types/lib/template/index.d.ts +1 -0
- package/build/types/lib/template/memory-write-stream.d.ts +6 -0
- package/build/types/lib/template/template-fs.d.ts +2 -0
- package/build/types/lib/template/template-memory.d.ts +146 -0
- package/build/types/lib/template/utils/index.d.ts +2 -0
- package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -0
- package/build/types/lib/template/utils/regexp.d.ts +24 -0
- package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
- package/build/types/lib/template/utils/write-rows-to-stream.d.ts +28 -11
- package/package.json +5 -3
package/README.md
CHANGED
@@ -2,100 +2,79 @@
|
|
2
2
|
|
3
3
|

|
4
4
|
|
5
|
-
|
5
|
+
📘 **Docs:** [js-ak.github.io/excel-toolbox](https://js-ak.github.io/excel-toolbox/)
|
6
6
|
|
7
|
-
|
7
|
+
A lightweight toolkit for working with `.xlsx` Excel files — modify templates, merge sheets, and handle massive datasets without dependencies.
|
8
8
|
|
9
|
-
|
9
|
+
## Installation
|
10
10
|
|
11
11
|
```bash
|
12
12
|
npm install @js-ak/excel-toolbox
|
13
13
|
```
|
14
14
|
|
15
|
-
##
|
16
|
-
|
17
|
-
To merge rows from multiple Excel files into one:
|
15
|
+
## Features
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
const otherFile = fs.readFileSync("data.xlsx");
|
23
|
+
## Template API
|
25
24
|
|
26
|
-
|
27
|
-
baseFile,
|
28
|
-
additions: [
|
29
|
-
{ file: otherFile, sheetIndexes: [1] }
|
30
|
-
],
|
31
|
-
gap: 2,
|
32
|
-
});
|
25
|
+
### `TemplateFs` and `TemplateMemory`
|
33
26
|
|
34
|
-
|
35
|
-
```
|
27
|
+
Both classes provide the same API for modifying Excel templates.
|
36
28
|
|
37
|
-
|
29
|
+
#### Common Features
|
38
30
|
|
39
|
-
-
|
40
|
-
-
|
41
|
-
-
|
42
|
-
-
|
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
|
-
|
36
|
+
```ts
|
37
|
+
import { TemplateFs } from "@js-ak/excel-toolbox";
|
45
38
|
|
46
|
-
|
39
|
+
const template = await TemplateFs.from({
|
40
|
+
destination: "/tmp/template",
|
41
|
+
source: fs.readFileSync("template.xlsx"),
|
42
|
+
});
|
47
43
|
|
48
|
-
|
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
|
-
|
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
|
-
|
52
|
+
### `mergeSheetsToBaseFileSync(options): Buffer`
|
60
53
|
|
61
|
-
|
54
|
+
Synchronously merges sheets into a base file.
|
62
55
|
|
63
56
|
### `mergeSheetsToBaseFile(options): Promise<Buffer>`
|
64
57
|
|
65
|
-
|
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
|
79
|
-
import {
|
63
|
+
import fs from "node:fs";
|
64
|
+
import { mergeSheetsToBaseFileSync } from "@js-ak/excel-toolbox";
|
80
65
|
|
81
|
-
const baseFile =
|
82
|
-
const
|
66
|
+
const baseFile = fs.readFileSync("base.xlsx");
|
67
|
+
const dataFile = fs.readFileSync("data.xlsx");
|
83
68
|
|
84
|
-
const
|
69
|
+
const result = mergeSheetsToBaseFileSync({
|
85
70
|
baseFile,
|
86
|
-
additions: [
|
87
|
-
|
88
|
-
],
|
89
|
-
gap: 1,
|
71
|
+
additions: [{ file: dataFile, sheetIndexes: [1] }],
|
72
|
+
gap: 2,
|
90
73
|
});
|
91
74
|
|
92
|
-
|
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)
|
80
|
+
MIT — see [LICENSE](./LICENSE)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.MemoryWriteStream = void 0;
|
4
|
+
class MemoryWriteStream {
|
5
|
+
chunks = [];
|
6
|
+
write(chunk) {
|
7
|
+
this.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8"));
|
8
|
+
return true;
|
9
|
+
}
|
10
|
+
end() {
|
11
|
+
// no-op
|
12
|
+
}
|
13
|
+
toBuffer() {
|
14
|
+
return Buffer.concat(this.chunks);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
exports.MemoryWriteStream = MemoryWriteStream;
|
@@ -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 #
|
166
|
+
async #getSheetPathByName(sheetName) {
|
153
167
|
// Read XML workbook to find sheet name and path
|
154
|
-
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(
|
155
|
-
const sheetMatch = workbookXml.match(
|
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(
|
160
|
-
const relMatch = relsXml.match(
|
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 =
|
213
|
-
const sheetPath = await this.#
|
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 =
|
292
|
+
const workbookXmlPath = this.#excelKeys.workbook;
|
274
293
|
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
|
275
294
|
// Find the source sheet
|
276
|
-
const
|
277
|
-
|
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
|
-
|
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
|
305
|
+
const rId = sheetMatch[1];
|
306
|
+
const relsXmlPath = this.#excelKeys.workbookRels;
|
288
307
|
const relsXml = Xml.extractXmlFromSheet(await this.#readFile(relsXmlPath));
|
289
|
-
const
|
290
|
-
|
291
|
-
|
292
|
-
|
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 =
|
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
|
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
|
-
//
|
458
|
-
const
|
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
|
-
//
|
601
|
-
|
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(
|
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(
|
793
|
+
await fs.mkdir(destinationWithUuid, { recursive: true });
|
719
794
|
await Promise.all(Object.entries(files).map(async ([filePath, content]) => {
|
720
|
-
const fullPath = path.join(
|
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)),
|
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
|
+
};
|