@js-ak/excel-toolbox 1.4.1 → 1.5.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/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-memory.js +531 -0
- package/build/cjs/lib/template/utils/prepare-row-to-cells.js +13 -0
- package/build/cjs/lib/template/utils/write-rows-to-stream.js +15 -11
- 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-memory.js +494 -0
- package/build/esm/lib/template/utils/prepare-row-to-cells.js +10 -0
- package/build/esm/lib/template/utils/write-rows-to-stream.js +15 -11
- 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-memory.d.ts +85 -0
- package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +1 -0
- package/build/types/lib/template/utils/write-rows-to-stream.d.ts +6 -2
- package/package.json +5 -3
@@ -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;
|
@@ -0,0 +1,531 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
36
|
+
exports.TemplateMemory = void 0;
|
37
|
+
const fs = __importStar(require("node:fs/promises"));
|
38
|
+
const Xml = __importStar(require("../xml/index.js"));
|
39
|
+
const Zip = __importStar(require("../zip/index.js"));
|
40
|
+
const Utils = __importStar(require("./utils/index.js"));
|
41
|
+
const memory_write_stream_js_1 = require("./memory-write-stream.js");
|
42
|
+
/**
|
43
|
+
* A class for manipulating Excel templates by extracting, modifying, and repacking Excel files.
|
44
|
+
*
|
45
|
+
* @experimental This API is experimental and might change in future versions.
|
46
|
+
*/
|
47
|
+
class TemplateMemory {
|
48
|
+
files;
|
49
|
+
/**
|
50
|
+
* Flag indicating whether this template instance has been destroyed.
|
51
|
+
* @type {boolean}
|
52
|
+
*/
|
53
|
+
destroyed = false;
|
54
|
+
/**
|
55
|
+
* Flag indicating whether this template instance is currently being processed.
|
56
|
+
* @type {boolean}
|
57
|
+
*/
|
58
|
+
#isProcessing = false;
|
59
|
+
constructor(files) {
|
60
|
+
this.files = files;
|
61
|
+
}
|
62
|
+
/**
|
63
|
+
* Ensures that this Template instance has not been destroyed.
|
64
|
+
* @private
|
65
|
+
* @throws {Error} If the template instance has already been destroyed.
|
66
|
+
* @experimental This API is experimental and might change in future versions.
|
67
|
+
*/
|
68
|
+
#ensureNotDestroyed() {
|
69
|
+
if (this.destroyed) {
|
70
|
+
throw new Error("This Template instance has already been saved and destroyed.");
|
71
|
+
}
|
72
|
+
}
|
73
|
+
/**
|
74
|
+
* Ensures that this Template instance is not currently being processed.
|
75
|
+
* @throws {Error} If the template instance is currently being processed.
|
76
|
+
* @experimental This API is experimental and might change in future versions.
|
77
|
+
*/
|
78
|
+
#ensureNotProcessing() {
|
79
|
+
if (this.#isProcessing) {
|
80
|
+
throw new Error("This Template instance is currently being processed.");
|
81
|
+
}
|
82
|
+
}
|
83
|
+
/**
|
84
|
+
* Expand table rows in the given sheet and shared strings XML.
|
85
|
+
*
|
86
|
+
* @param {string} sheetXml - The XML content of the sheet.
|
87
|
+
* @param {string} sharedStringsXml - The XML content of the shared strings.
|
88
|
+
* @param {Record<string, unknown>} replacements - An object containing replacement values.
|
89
|
+
*
|
90
|
+
* @returns {Object} An object with two properties:
|
91
|
+
* - sheet: The expanded sheet XML.
|
92
|
+
* - shared: The expanded shared strings XML.
|
93
|
+
* @experimental This API is experimental and might change in future versions.
|
94
|
+
*/
|
95
|
+
#expandTableRows(sheetXml, sharedStringsXml, replacements) {
|
96
|
+
const { initialMergeCells, mergeCellMatches, modifiedXml, } = Utils.processMergeCells(sheetXml);
|
97
|
+
const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = Utils.processSharedStrings(sharedStringsXml);
|
98
|
+
const { lastIndex, resultRows, rowShift } = Utils.processRows({
|
99
|
+
mergeCellMatches,
|
100
|
+
replacements,
|
101
|
+
sharedIndexMap,
|
102
|
+
sharedStrings,
|
103
|
+
sheetMergeCells,
|
104
|
+
sheetXml: modifiedXml,
|
105
|
+
});
|
106
|
+
return Utils.processMergeFinalize({
|
107
|
+
initialMergeCells,
|
108
|
+
lastIndex,
|
109
|
+
mergeCellMatches,
|
110
|
+
resultRows,
|
111
|
+
rowShift,
|
112
|
+
sharedStrings,
|
113
|
+
sharedStringsHeader,
|
114
|
+
sheetMergeCells,
|
115
|
+
sheetXml: modifiedXml,
|
116
|
+
});
|
117
|
+
}
|
118
|
+
#getXml(fileKey) {
|
119
|
+
if (!this.files[fileKey]) {
|
120
|
+
throw new Error(`${fileKey} not found`);
|
121
|
+
}
|
122
|
+
return Xml.extractXmlFromSheet(this.files[fileKey]);
|
123
|
+
}
|
124
|
+
/**
|
125
|
+
* Get the path of the sheet with the given name inside the workbook.
|
126
|
+
* @param sheetName The name of the sheet to find.
|
127
|
+
* @returns The path of the sheet inside the workbook.
|
128
|
+
* @throws {Error} If the sheet is not found.
|
129
|
+
*/
|
130
|
+
async #getSheetPath(sheetName) {
|
131
|
+
// Read XML workbook to find sheet name and path
|
132
|
+
const workbookXml = this.#getXml("xl/workbook.xml");
|
133
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
134
|
+
if (!sheetMatch)
|
135
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
136
|
+
const rId = sheetMatch[1];
|
137
|
+
const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
|
138
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
139
|
+
if (!relMatch)
|
140
|
+
throw new Error(`Relationship "${rId}" not found`);
|
141
|
+
return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
142
|
+
}
|
143
|
+
/**
|
144
|
+
* Replaces the contents of a file in the template.
|
145
|
+
*
|
146
|
+
* @param {string} key - The Excel path of the file to replace.
|
147
|
+
* @param {Buffer|string} content - The new content.
|
148
|
+
* @returns {Promise<void>}
|
149
|
+
* @throws {Error} If the template instance has been destroyed.
|
150
|
+
* @throws {Error} If the file does not exist in the template.
|
151
|
+
* @experimental This API is experimental and might change in future versions.
|
152
|
+
*/
|
153
|
+
async #set(key, content) {
|
154
|
+
this.files[key] = content;
|
155
|
+
}
|
156
|
+
async #substitute(sheetName, replacements) {
|
157
|
+
const sharedStringsPath = "xl/sharedStrings.xml";
|
158
|
+
const sheetPath = await this.#getSheetPath(sheetName);
|
159
|
+
let sharedStringsContent = "";
|
160
|
+
let sheetContent = "";
|
161
|
+
if (this.files[sharedStringsPath]) {
|
162
|
+
sharedStringsContent = this.#getXml(sharedStringsPath);
|
163
|
+
}
|
164
|
+
if (this.files[sheetPath]) {
|
165
|
+
sheetContent = this.#getXml(sheetPath);
|
166
|
+
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
167
|
+
const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
|
168
|
+
if (hasTablePlaceholders) {
|
169
|
+
const result = this.#expandTableRows(sheetContent, sharedStringsContent, replacements);
|
170
|
+
sheetContent = result.sheet;
|
171
|
+
sharedStringsContent = result.shared;
|
172
|
+
}
|
173
|
+
}
|
174
|
+
if (this.files[sharedStringsPath]) {
|
175
|
+
sharedStringsContent = Utils.applyReplacements(sharedStringsContent, replacements);
|
176
|
+
await this.#set(sharedStringsPath, Buffer.from(sharedStringsContent));
|
177
|
+
}
|
178
|
+
if (this.files[sheetPath]) {
|
179
|
+
sheetContent = Utils.applyReplacements(sheetContent, replacements);
|
180
|
+
await this.#set(sheetPath, Buffer.from(sheetContent));
|
181
|
+
}
|
182
|
+
}
|
183
|
+
/**
|
184
|
+
* Copies a sheet from the template to a new name.
|
185
|
+
*
|
186
|
+
* @param {string} sourceName - The name of the sheet to copy.
|
187
|
+
* @param {string} newName - The new name for the sheet.
|
188
|
+
* @returns {Promise<void>}
|
189
|
+
* @throws {Error} If the sheet with the source name does not exist.
|
190
|
+
* @throws {Error} If a sheet with the new name already exists.
|
191
|
+
* @experimental This API is experimental and might change in future versions.
|
192
|
+
*/
|
193
|
+
async copySheet(sourceName, newName) {
|
194
|
+
this.#ensureNotProcessing();
|
195
|
+
this.#ensureNotDestroyed();
|
196
|
+
this.#isProcessing = true;
|
197
|
+
try {
|
198
|
+
// Read workbook.xml and find the source sheet
|
199
|
+
const workbookXmlPath = "xl/workbook.xml";
|
200
|
+
const workbookXml = this.#getXml(workbookXmlPath);
|
201
|
+
// Find the source sheet
|
202
|
+
const sheetRegex = new RegExp(`<sheet[^>]+name="${sourceName}"[^>]+r:id="([^"]+)"[^>]*/>`);
|
203
|
+
const sheetMatch = workbookXml.match(sheetRegex);
|
204
|
+
if (!sheetMatch)
|
205
|
+
throw new Error(`Sheet "${sourceName}" not found`);
|
206
|
+
const sourceRId = sheetMatch[1];
|
207
|
+
// Check if a sheet with the new name already exists
|
208
|
+
if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
|
209
|
+
throw new Error(`Sheet "${newName}" already exists`);
|
210
|
+
}
|
211
|
+
// Read workbook.rels
|
212
|
+
// Find the source sheet path by rId
|
213
|
+
const relsXmlPath = "xl/_rels/workbook.xml.rels";
|
214
|
+
const relsXml = this.#getXml(relsXmlPath);
|
215
|
+
const relRegex = new RegExp(`<Relationship[^>]+Id="${sourceRId}"[^>]+Target="([^"]+)"[^>]*/>`);
|
216
|
+
const relMatch = relsXml.match(relRegex);
|
217
|
+
if (!relMatch)
|
218
|
+
throw new Error(`Relationship "${sourceRId}" not found`);
|
219
|
+
const sourceTarget = relMatch[1]; // sheetN.xml
|
220
|
+
if (!sourceTarget)
|
221
|
+
throw new Error(`Relationship "${sourceRId}" not found`);
|
222
|
+
const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
|
223
|
+
// Get the index of the new sheet
|
224
|
+
const sheetNumbers = Array.from(Object.keys(this.files))
|
225
|
+
.map((key) => key.match(/^xl\/worksheets\/sheet(\d+)\.xml$/))
|
226
|
+
.filter(Boolean)
|
227
|
+
.map((match) => parseInt(match[1], 10));
|
228
|
+
const nextSheetIndex = sheetNumbers.length > 0 ? Math.max(...sheetNumbers) + 1 : 1;
|
229
|
+
const newSheetFilename = `sheet${nextSheetIndex}.xml`;
|
230
|
+
const newSheetPath = `xl/worksheets/${newSheetFilename}`;
|
231
|
+
const newTarget = `worksheets/${newSheetFilename}`;
|
232
|
+
// Generate a unique rId
|
233
|
+
const usedRIds = [...relsXml.matchAll(/Id="(rId\d+)"/g)].map(m => m[1]);
|
234
|
+
let nextRIdNum = 1;
|
235
|
+
while (usedRIds.includes(`rId${nextRIdNum}`))
|
236
|
+
nextRIdNum++;
|
237
|
+
const newRId = `rId${nextRIdNum}`;
|
238
|
+
// Copy the source sheet file
|
239
|
+
const sheetContent = this.files[sourceSheetPath];
|
240
|
+
if (!sheetContent) {
|
241
|
+
throw new Error(`Sheet "${sourceSheetPath}" not found`);
|
242
|
+
}
|
243
|
+
function copyBuffer(source) {
|
244
|
+
const target = Buffer.alloc(source.length);
|
245
|
+
source.copy(target);
|
246
|
+
return target;
|
247
|
+
}
|
248
|
+
await this.#set(newSheetPath, copyBuffer(sheetContent));
|
249
|
+
// Update workbook.xml
|
250
|
+
const updatedWorkbookXml = workbookXml.replace("</sheets>", `<sheet name="${newName}" sheetId="${nextSheetIndex}" r:id="${newRId}"/></sheets>`);
|
251
|
+
await this.#set(workbookXmlPath, Buffer.from(updatedWorkbookXml));
|
252
|
+
// Update workbook.xml.rels
|
253
|
+
const updatedRelsXml = relsXml.replace("</Relationships>", `<Relationship Id="${newRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="${newTarget}"/></Relationships>`);
|
254
|
+
await this.#set(relsXmlPath, Buffer.from(updatedRelsXml));
|
255
|
+
// Read [Content_Types].xml
|
256
|
+
// Update [Content_Types].xml
|
257
|
+
const contentTypesPath = "[Content_Types].xml";
|
258
|
+
const contentTypesXml = this.#getXml(contentTypesPath);
|
259
|
+
const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
260
|
+
const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
|
261
|
+
await this.#set(contentTypesPath, Buffer.from(updatedContentTypesXml));
|
262
|
+
}
|
263
|
+
finally {
|
264
|
+
this.#isProcessing = false;
|
265
|
+
}
|
266
|
+
}
|
267
|
+
/**
|
268
|
+
* Replaces placeholders in the given sheet with values from the replacements map.
|
269
|
+
*
|
270
|
+
* The function searches for placeholders in the format `${key}` within the sheet
|
271
|
+
* content, where `key` corresponds to a path in the replacements object.
|
272
|
+
* If a value is found for the key, it replaces the placeholder with the value.
|
273
|
+
* If no value is found, the original placeholder remains unchanged.
|
274
|
+
*
|
275
|
+
* @param sheetName - The name of the sheet to be replaced.
|
276
|
+
* @param replacements - An object where keys represent placeholder paths and values are the replacements.
|
277
|
+
* @returns A promise that resolves when the substitution is complete.
|
278
|
+
*/
|
279
|
+
substitute(sheetName, replacements) {
|
280
|
+
this.#ensureNotProcessing();
|
281
|
+
this.#ensureNotDestroyed();
|
282
|
+
this.#isProcessing = true;
|
283
|
+
try {
|
284
|
+
return this.#substitute(sheetName, replacements);
|
285
|
+
}
|
286
|
+
finally {
|
287
|
+
this.#isProcessing = false;
|
288
|
+
}
|
289
|
+
}
|
290
|
+
/**
|
291
|
+
* Inserts rows into a specific sheet in the template.
|
292
|
+
*
|
293
|
+
* @param {Object} data - The data for row insertion.
|
294
|
+
* @param {string} data.sheetName - The name of the sheet to insert rows into.
|
295
|
+
* @param {number} [data.startRowNumber] - The row number to start inserting from.
|
296
|
+
* @param {unknown[][]} data.rows - The rows to insert.
|
297
|
+
* @returns {Promise<void>}
|
298
|
+
* @throws {Error} If the template instance has been destroyed.
|
299
|
+
* @throws {Error} If the sheet does not exist.
|
300
|
+
* @throws {Error} If the row number is out of range.
|
301
|
+
* @throws {Error} If a column is out of range.
|
302
|
+
* @experimental This API is experimental and might change in future versions.
|
303
|
+
*/
|
304
|
+
async insertRows(data) {
|
305
|
+
this.#ensureNotProcessing();
|
306
|
+
this.#ensureNotDestroyed();
|
307
|
+
this.#isProcessing = true;
|
308
|
+
try {
|
309
|
+
const { rows, sheetName, startRowNumber } = data;
|
310
|
+
const preparedRows = rows.map(row => Utils.toExcelColumnObject(row));
|
311
|
+
// Validation
|
312
|
+
Utils.checkStartRow(startRowNumber);
|
313
|
+
Utils.checkRows(preparedRows);
|
314
|
+
// Find the sheet
|
315
|
+
const workbookXml = this.#getXml("xl/workbook.xml");
|
316
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
317
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
318
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
319
|
+
}
|
320
|
+
const rId = sheetMatch[1];
|
321
|
+
const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
|
322
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
323
|
+
if (!relMatch || !relMatch[1]) {
|
324
|
+
throw new Error(`Relationship "${rId}" not found`);
|
325
|
+
}
|
326
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
327
|
+
const sheetXml = this.#getXml(sheetPath);
|
328
|
+
let nextRow = 0;
|
329
|
+
if (!startRowNumber) {
|
330
|
+
// Find the last row
|
331
|
+
let lastRowNumber = 0;
|
332
|
+
const rowMatches = [...sheetXml.matchAll(/<row[^>]+r="(\d+)"[^>]*>/g)];
|
333
|
+
if (rowMatches.length > 0) {
|
334
|
+
lastRowNumber = Math.max(...rowMatches.map((m) => parseInt(m[1], 10)));
|
335
|
+
}
|
336
|
+
nextRow = lastRowNumber + 1;
|
337
|
+
}
|
338
|
+
else {
|
339
|
+
nextRow = startRowNumber;
|
340
|
+
}
|
341
|
+
// Generate XML for all rows
|
342
|
+
const rowsXml = preparedRows.map((cells, i) => {
|
343
|
+
const rowNumber = nextRow + i;
|
344
|
+
const cellTags = Object.entries(cells).map(([col, value]) => {
|
345
|
+
const colUpper = col.toUpperCase();
|
346
|
+
const ref = `${colUpper}${rowNumber}`;
|
347
|
+
return `<c r="${ref}" t="inlineStr"><is><t>${Utils.escapeXml(value)}</t></is></c>`;
|
348
|
+
}).join("");
|
349
|
+
return `<row r="${rowNumber}">${cellTags}</row>`;
|
350
|
+
}).join("");
|
351
|
+
let updatedXml;
|
352
|
+
if (/<sheetData\s*\/>/.test(sheetXml)) {
|
353
|
+
updatedXml = sheetXml.replace(/<sheetData\s*\/>/, `<sheetData>${rowsXml}</sheetData>`);
|
354
|
+
}
|
355
|
+
else if (/<sheetData>([\s\S]*?)<\/sheetData>/.test(sheetXml)) {
|
356
|
+
updatedXml = sheetXml.replace(/<\/sheetData>/, `${rowsXml}</sheetData>`);
|
357
|
+
}
|
358
|
+
else {
|
359
|
+
updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
|
360
|
+
}
|
361
|
+
await this.#set(sheetPath, Buffer.from(updatedXml));
|
362
|
+
}
|
363
|
+
finally {
|
364
|
+
this.#isProcessing = false;
|
365
|
+
}
|
366
|
+
}
|
367
|
+
async insertRowsStream(data) {
|
368
|
+
this.#ensureNotProcessing();
|
369
|
+
this.#ensureNotDestroyed();
|
370
|
+
this.#isProcessing = true;
|
371
|
+
try {
|
372
|
+
const { rows, sheetName, startRowNumber } = data;
|
373
|
+
if (!sheetName)
|
374
|
+
throw new Error("Sheet name is required");
|
375
|
+
const workbookXml = this.#getXml("xl/workbook.xml");
|
376
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
377
|
+
if (!sheetMatch)
|
378
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
379
|
+
const rId = sheetMatch[1];
|
380
|
+
const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
|
381
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
382
|
+
if (!relMatch || !relMatch[1])
|
383
|
+
throw new Error(`Relationship "${rId}" not found`);
|
384
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
385
|
+
const sheetXml = this.#getXml(sheetPath);
|
386
|
+
const output = new memory_write_stream_js_1.MemoryWriteStream();
|
387
|
+
let inserted = false;
|
388
|
+
// --- Case 1: <sheetData>...</sheetData> on one line ---
|
389
|
+
const singleLineMatch = sheetXml.match(/(<sheetData[^>]*>)(.*)(<\/sheetData>)/);
|
390
|
+
if (!inserted && singleLineMatch) {
|
391
|
+
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(sheetXml);
|
392
|
+
const openTag = singleLineMatch[1];
|
393
|
+
const innerRows = singleLineMatch[2].trim();
|
394
|
+
const closeTag = singleLineMatch[3];
|
395
|
+
const innerRowsMap = Utils.parseRows(innerRows);
|
396
|
+
output.write(sheetXml.slice(0, singleLineMatch.index));
|
397
|
+
output.write(openTag);
|
398
|
+
if (innerRows) {
|
399
|
+
if (startRowNumber) {
|
400
|
+
const filtered = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
401
|
+
if (filtered)
|
402
|
+
output.write(filtered);
|
403
|
+
}
|
404
|
+
else {
|
405
|
+
output.write(innerRows);
|
406
|
+
}
|
407
|
+
}
|
408
|
+
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
409
|
+
if (innerRows) {
|
410
|
+
const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
411
|
+
if (filtered)
|
412
|
+
output.write(filtered);
|
413
|
+
}
|
414
|
+
output.write(closeTag);
|
415
|
+
output.write(sheetXml.slice(singleLineMatch.index + singleLineMatch[0].length));
|
416
|
+
inserted = true;
|
417
|
+
}
|
418
|
+
// --- Case 2: <sheetData/> ---
|
419
|
+
if (!inserted && /<sheetData\s*\/>/.test(sheetXml)) {
|
420
|
+
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(sheetXml);
|
421
|
+
const match = sheetXml.match(/<sheetData\s*\/>/);
|
422
|
+
const matchIndex = match.index;
|
423
|
+
output.write(sheetXml.slice(0, matchIndex));
|
424
|
+
output.write("<sheetData>");
|
425
|
+
await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
426
|
+
output.write("</sheetData>");
|
427
|
+
output.write(sheetXml.slice(matchIndex + match[0].length));
|
428
|
+
inserted = true;
|
429
|
+
}
|
430
|
+
// --- Case 3: Multiline <sheetData> ---
|
431
|
+
if (!inserted && sheetXml.includes("<sheetData")) {
|
432
|
+
const openTagMatch = sheetXml.match(/<sheetData[^>]*>/);
|
433
|
+
const closeTag = "</sheetData>";
|
434
|
+
if (!openTagMatch)
|
435
|
+
throw new Error("Invalid sheetData structure");
|
436
|
+
const openTag = openTagMatch[0];
|
437
|
+
const openIdx = sheetXml.indexOf(openTag);
|
438
|
+
const closeIdx = sheetXml.lastIndexOf(closeTag);
|
439
|
+
if (closeIdx === -1)
|
440
|
+
throw new Error("Missing </sheetData>");
|
441
|
+
const beforeRows = sheetXml.slice(0, openIdx + openTag.length);
|
442
|
+
const innerRows = sheetXml.slice(openIdx + openTag.length, closeIdx).trim();
|
443
|
+
const afterRows = sheetXml.slice(closeIdx + closeTag.length);
|
444
|
+
const innerRowsMap = Utils.parseRows(innerRows);
|
445
|
+
output.write(beforeRows);
|
446
|
+
if (innerRows) {
|
447
|
+
if (startRowNumber) {
|
448
|
+
const filtered = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
449
|
+
if (filtered)
|
450
|
+
output.write(filtered);
|
451
|
+
}
|
452
|
+
else {
|
453
|
+
output.write(innerRows);
|
454
|
+
}
|
455
|
+
}
|
456
|
+
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, Utils.getMaxRowNumber(innerRows));
|
457
|
+
if (innerRows) {
|
458
|
+
const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
459
|
+
if (filtered)
|
460
|
+
output.write(filtered);
|
461
|
+
}
|
462
|
+
output.write(closeTag);
|
463
|
+
output.write(afterRows);
|
464
|
+
inserted = true;
|
465
|
+
}
|
466
|
+
if (!inserted)
|
467
|
+
throw new Error("Failed to locate <sheetData> for insertion");
|
468
|
+
// ← теперь мы не собираем строку, а собираем Buffer
|
469
|
+
this.files[sheetPath] = output.toBuffer();
|
470
|
+
}
|
471
|
+
finally {
|
472
|
+
this.#isProcessing = false;
|
473
|
+
}
|
474
|
+
}
|
475
|
+
/**
|
476
|
+
* Saves the modified Excel template to a buffer.
|
477
|
+
*
|
478
|
+
* @returns {Promise<Buffer>} The modified Excel template as a buffer.
|
479
|
+
* @throws {Error} If the template instance has been destroyed.
|
480
|
+
* @experimental This API is experimental and might change in future versions.
|
481
|
+
*/
|
482
|
+
async save() {
|
483
|
+
this.#ensureNotProcessing();
|
484
|
+
this.#ensureNotDestroyed();
|
485
|
+
this.#isProcessing = true;
|
486
|
+
try {
|
487
|
+
const zipBuffer = await Zip.create(this.files);
|
488
|
+
this.destroyed = true;
|
489
|
+
// Очистка всех буферов
|
490
|
+
for (const key in this.files) {
|
491
|
+
if (this.files.hasOwnProperty(key)) {
|
492
|
+
this.files[key] = Buffer.alloc(0); // Заменяем на пустой буфер
|
493
|
+
}
|
494
|
+
}
|
495
|
+
return zipBuffer;
|
496
|
+
}
|
497
|
+
finally {
|
498
|
+
this.#isProcessing = false;
|
499
|
+
}
|
500
|
+
}
|
501
|
+
/**
|
502
|
+
* Replaces the contents of a file in the template.
|
503
|
+
*
|
504
|
+
* @param {string} key - The Excel path of the file to replace.
|
505
|
+
* @param {Buffer|string} content - The new content.
|
506
|
+
* @returns {Promise<void>}
|
507
|
+
* @throws {Error} If the template instance has been destroyed.
|
508
|
+
* @throws {Error} If the file does not exist in the template.
|
509
|
+
* @experimental This API is experimental and might change in future versions.
|
510
|
+
*/
|
511
|
+
async set(key, content) {
|
512
|
+
this.#ensureNotProcessing();
|
513
|
+
this.#ensureNotDestroyed();
|
514
|
+
this.#isProcessing = true;
|
515
|
+
try {
|
516
|
+
await this.#set(key, content);
|
517
|
+
}
|
518
|
+
finally {
|
519
|
+
this.#isProcessing = false;
|
520
|
+
}
|
521
|
+
}
|
522
|
+
static async from(data) {
|
523
|
+
const { source } = data;
|
524
|
+
const buffer = typeof source === "string"
|
525
|
+
? await fs.readFile(source)
|
526
|
+
: source;
|
527
|
+
const files = await Zip.read(buffer);
|
528
|
+
return new TemplateMemory(files);
|
529
|
+
}
|
530
|
+
}
|
531
|
+
exports.TemplateMemory = TemplateMemory;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.prepareRowToCells = prepareRowToCells;
|
4
|
+
const column_index_to_letter_js_1 = require("./column-index-to-letter.js");
|
5
|
+
const escape_xml_js_1 = require("./escape-xml.js");
|
6
|
+
function prepareRowToCells(row, rowNumber) {
|
7
|
+
return row.map((value, colIndex) => {
|
8
|
+
const colLetter = (0, column_index_to_letter_js_1.columnIndexToLetter)(colIndex);
|
9
|
+
const cellRef = `${colLetter}${rowNumber}`;
|
10
|
+
const cellValue = (0, escape_xml_js_1.escapeXml)(String(value ?? ""));
|
11
|
+
return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`;
|
12
|
+
});
|
13
|
+
}
|
@@ -1,8 +1,7 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.writeRowsToStream = writeRowsToStream;
|
4
|
-
const
|
5
|
-
const escape_xml_js_1 = require("./escape-xml.js");
|
4
|
+
const prepare_row_to_cells_js_1 = require("./prepare-row-to-cells.js");
|
6
5
|
/**
|
7
6
|
* Writes an async iterable of rows to an Excel XML file.
|
8
7
|
*
|
@@ -28,15 +27,20 @@ async function writeRowsToStream(output, rows, startRowNumber) {
|
|
28
27
|
let rowNumber = startRowNumber;
|
29
28
|
for await (const row of rows) {
|
30
29
|
// Transform the row into XML
|
31
|
-
|
32
|
-
const
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
30
|
+
if (Array.isArray(row[0])) {
|
31
|
+
for (const subRow of row) {
|
32
|
+
const cells = (0, prepare_row_to_cells_js_1.prepareRowToCells)(subRow, rowNumber);
|
33
|
+
// Write the row to the file
|
34
|
+
output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
|
35
|
+
rowNumber++;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
else {
|
39
|
+
const cells = (0, prepare_row_to_cells_js_1.prepareRowToCells)(row, rowNumber);
|
40
|
+
// Write the row to the file
|
41
|
+
output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
|
42
|
+
rowNumber++;
|
43
|
+
}
|
40
44
|
}
|
41
45
|
return { rowNumber };
|
42
46
|
}
|
@@ -0,0 +1,494 @@
|
|
1
|
+
import * as fs from "node:fs/promises";
|
2
|
+
import * as Xml from "../xml/index.js";
|
3
|
+
import * as Zip from "../zip/index.js";
|
4
|
+
import * as Utils from "./utils/index.js";
|
5
|
+
import { MemoryWriteStream } from "./memory-write-stream.js";
|
6
|
+
/**
|
7
|
+
* A class for manipulating Excel templates by extracting, modifying, and repacking Excel files.
|
8
|
+
*
|
9
|
+
* @experimental This API is experimental and might change in future versions.
|
10
|
+
*/
|
11
|
+
export class TemplateMemory {
|
12
|
+
files;
|
13
|
+
/**
|
14
|
+
* Flag indicating whether this template instance has been destroyed.
|
15
|
+
* @type {boolean}
|
16
|
+
*/
|
17
|
+
destroyed = false;
|
18
|
+
/**
|
19
|
+
* Flag indicating whether this template instance is currently being processed.
|
20
|
+
* @type {boolean}
|
21
|
+
*/
|
22
|
+
#isProcessing = false;
|
23
|
+
constructor(files) {
|
24
|
+
this.files = files;
|
25
|
+
}
|
26
|
+
/**
|
27
|
+
* Ensures that this Template instance has not been destroyed.
|
28
|
+
* @private
|
29
|
+
* @throws {Error} If the template instance has already been destroyed.
|
30
|
+
* @experimental This API is experimental and might change in future versions.
|
31
|
+
*/
|
32
|
+
#ensureNotDestroyed() {
|
33
|
+
if (this.destroyed) {
|
34
|
+
throw new Error("This Template instance has already been saved and destroyed.");
|
35
|
+
}
|
36
|
+
}
|
37
|
+
/**
|
38
|
+
* Ensures that this Template instance is not currently being processed.
|
39
|
+
* @throws {Error} If the template instance is currently being processed.
|
40
|
+
* @experimental This API is experimental and might change in future versions.
|
41
|
+
*/
|
42
|
+
#ensureNotProcessing() {
|
43
|
+
if (this.#isProcessing) {
|
44
|
+
throw new Error("This Template instance is currently being processed.");
|
45
|
+
}
|
46
|
+
}
|
47
|
+
/**
|
48
|
+
* Expand table rows in the given sheet and shared strings XML.
|
49
|
+
*
|
50
|
+
* @param {string} sheetXml - The XML content of the sheet.
|
51
|
+
* @param {string} sharedStringsXml - The XML content of the shared strings.
|
52
|
+
* @param {Record<string, unknown>} replacements - An object containing replacement values.
|
53
|
+
*
|
54
|
+
* @returns {Object} An object with two properties:
|
55
|
+
* - sheet: The expanded sheet XML.
|
56
|
+
* - shared: The expanded shared strings XML.
|
57
|
+
* @experimental This API is experimental and might change in future versions.
|
58
|
+
*/
|
59
|
+
#expandTableRows(sheetXml, sharedStringsXml, replacements) {
|
60
|
+
const { initialMergeCells, mergeCellMatches, modifiedXml, } = Utils.processMergeCells(sheetXml);
|
61
|
+
const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = Utils.processSharedStrings(sharedStringsXml);
|
62
|
+
const { lastIndex, resultRows, rowShift } = Utils.processRows({
|
63
|
+
mergeCellMatches,
|
64
|
+
replacements,
|
65
|
+
sharedIndexMap,
|
66
|
+
sharedStrings,
|
67
|
+
sheetMergeCells,
|
68
|
+
sheetXml: modifiedXml,
|
69
|
+
});
|
70
|
+
return Utils.processMergeFinalize({
|
71
|
+
initialMergeCells,
|
72
|
+
lastIndex,
|
73
|
+
mergeCellMatches,
|
74
|
+
resultRows,
|
75
|
+
rowShift,
|
76
|
+
sharedStrings,
|
77
|
+
sharedStringsHeader,
|
78
|
+
sheetMergeCells,
|
79
|
+
sheetXml: modifiedXml,
|
80
|
+
});
|
81
|
+
}
|
82
|
+
#getXml(fileKey) {
|
83
|
+
if (!this.files[fileKey]) {
|
84
|
+
throw new Error(`${fileKey} not found`);
|
85
|
+
}
|
86
|
+
return Xml.extractXmlFromSheet(this.files[fileKey]);
|
87
|
+
}
|
88
|
+
/**
|
89
|
+
* Get the path of the sheet with the given name inside the workbook.
|
90
|
+
* @param sheetName The name of the sheet to find.
|
91
|
+
* @returns The path of the sheet inside the workbook.
|
92
|
+
* @throws {Error} If the sheet is not found.
|
93
|
+
*/
|
94
|
+
async #getSheetPath(sheetName) {
|
95
|
+
// Read XML workbook to find sheet name and path
|
96
|
+
const workbookXml = this.#getXml("xl/workbook.xml");
|
97
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
98
|
+
if (!sheetMatch)
|
99
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
100
|
+
const rId = sheetMatch[1];
|
101
|
+
const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
|
102
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
103
|
+
if (!relMatch)
|
104
|
+
throw new Error(`Relationship "${rId}" not found`);
|
105
|
+
return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
106
|
+
}
|
107
|
+
/**
|
108
|
+
* Replaces the contents of a file in the template.
|
109
|
+
*
|
110
|
+
* @param {string} key - The Excel path of the file to replace.
|
111
|
+
* @param {Buffer|string} content - The new content.
|
112
|
+
* @returns {Promise<void>}
|
113
|
+
* @throws {Error} If the template instance has been destroyed.
|
114
|
+
* @throws {Error} If the file does not exist in the template.
|
115
|
+
* @experimental This API is experimental and might change in future versions.
|
116
|
+
*/
|
117
|
+
async #set(key, content) {
|
118
|
+
this.files[key] = content;
|
119
|
+
}
|
120
|
+
async #substitute(sheetName, replacements) {
|
121
|
+
const sharedStringsPath = "xl/sharedStrings.xml";
|
122
|
+
const sheetPath = await this.#getSheetPath(sheetName);
|
123
|
+
let sharedStringsContent = "";
|
124
|
+
let sheetContent = "";
|
125
|
+
if (this.files[sharedStringsPath]) {
|
126
|
+
sharedStringsContent = this.#getXml(sharedStringsPath);
|
127
|
+
}
|
128
|
+
if (this.files[sheetPath]) {
|
129
|
+
sheetContent = this.#getXml(sheetPath);
|
130
|
+
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
131
|
+
const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
|
132
|
+
if (hasTablePlaceholders) {
|
133
|
+
const result = this.#expandTableRows(sheetContent, sharedStringsContent, replacements);
|
134
|
+
sheetContent = result.sheet;
|
135
|
+
sharedStringsContent = result.shared;
|
136
|
+
}
|
137
|
+
}
|
138
|
+
if (this.files[sharedStringsPath]) {
|
139
|
+
sharedStringsContent = Utils.applyReplacements(sharedStringsContent, replacements);
|
140
|
+
await this.#set(sharedStringsPath, Buffer.from(sharedStringsContent));
|
141
|
+
}
|
142
|
+
if (this.files[sheetPath]) {
|
143
|
+
sheetContent = Utils.applyReplacements(sheetContent, replacements);
|
144
|
+
await this.#set(sheetPath, Buffer.from(sheetContent));
|
145
|
+
}
|
146
|
+
}
|
147
|
+
/**
|
148
|
+
* Copies a sheet from the template to a new name.
|
149
|
+
*
|
150
|
+
* @param {string} sourceName - The name of the sheet to copy.
|
151
|
+
* @param {string} newName - The new name for the sheet.
|
152
|
+
* @returns {Promise<void>}
|
153
|
+
* @throws {Error} If the sheet with the source name does not exist.
|
154
|
+
* @throws {Error} If a sheet with the new name already exists.
|
155
|
+
* @experimental This API is experimental and might change in future versions.
|
156
|
+
*/
|
157
|
+
async copySheet(sourceName, newName) {
|
158
|
+
this.#ensureNotProcessing();
|
159
|
+
this.#ensureNotDestroyed();
|
160
|
+
this.#isProcessing = true;
|
161
|
+
try {
|
162
|
+
// Read workbook.xml and find the source sheet
|
163
|
+
const workbookXmlPath = "xl/workbook.xml";
|
164
|
+
const workbookXml = this.#getXml(workbookXmlPath);
|
165
|
+
// Find the source sheet
|
166
|
+
const sheetRegex = new RegExp(`<sheet[^>]+name="${sourceName}"[^>]+r:id="([^"]+)"[^>]*/>`);
|
167
|
+
const sheetMatch = workbookXml.match(sheetRegex);
|
168
|
+
if (!sheetMatch)
|
169
|
+
throw new Error(`Sheet "${sourceName}" not found`);
|
170
|
+
const sourceRId = sheetMatch[1];
|
171
|
+
// Check if a sheet with the new name already exists
|
172
|
+
if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
|
173
|
+
throw new Error(`Sheet "${newName}" already exists`);
|
174
|
+
}
|
175
|
+
// Read workbook.rels
|
176
|
+
// Find the source sheet path by rId
|
177
|
+
const relsXmlPath = "xl/_rels/workbook.xml.rels";
|
178
|
+
const relsXml = this.#getXml(relsXmlPath);
|
179
|
+
const relRegex = new RegExp(`<Relationship[^>]+Id="${sourceRId}"[^>]+Target="([^"]+)"[^>]*/>`);
|
180
|
+
const relMatch = relsXml.match(relRegex);
|
181
|
+
if (!relMatch)
|
182
|
+
throw new Error(`Relationship "${sourceRId}" not found`);
|
183
|
+
const sourceTarget = relMatch[1]; // sheetN.xml
|
184
|
+
if (!sourceTarget)
|
185
|
+
throw new Error(`Relationship "${sourceRId}" not found`);
|
186
|
+
const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
|
187
|
+
// Get the index of the new sheet
|
188
|
+
const sheetNumbers = Array.from(Object.keys(this.files))
|
189
|
+
.map((key) => key.match(/^xl\/worksheets\/sheet(\d+)\.xml$/))
|
190
|
+
.filter(Boolean)
|
191
|
+
.map((match) => parseInt(match[1], 10));
|
192
|
+
const nextSheetIndex = sheetNumbers.length > 0 ? Math.max(...sheetNumbers) + 1 : 1;
|
193
|
+
const newSheetFilename = `sheet${nextSheetIndex}.xml`;
|
194
|
+
const newSheetPath = `xl/worksheets/${newSheetFilename}`;
|
195
|
+
const newTarget = `worksheets/${newSheetFilename}`;
|
196
|
+
// Generate a unique rId
|
197
|
+
const usedRIds = [...relsXml.matchAll(/Id="(rId\d+)"/g)].map(m => m[1]);
|
198
|
+
let nextRIdNum = 1;
|
199
|
+
while (usedRIds.includes(`rId${nextRIdNum}`))
|
200
|
+
nextRIdNum++;
|
201
|
+
const newRId = `rId${nextRIdNum}`;
|
202
|
+
// Copy the source sheet file
|
203
|
+
const sheetContent = this.files[sourceSheetPath];
|
204
|
+
if (!sheetContent) {
|
205
|
+
throw new Error(`Sheet "${sourceSheetPath}" not found`);
|
206
|
+
}
|
207
|
+
function copyBuffer(source) {
|
208
|
+
const target = Buffer.alloc(source.length);
|
209
|
+
source.copy(target);
|
210
|
+
return target;
|
211
|
+
}
|
212
|
+
await this.#set(newSheetPath, copyBuffer(sheetContent));
|
213
|
+
// Update workbook.xml
|
214
|
+
const updatedWorkbookXml = workbookXml.replace("</sheets>", `<sheet name="${newName}" sheetId="${nextSheetIndex}" r:id="${newRId}"/></sheets>`);
|
215
|
+
await this.#set(workbookXmlPath, Buffer.from(updatedWorkbookXml));
|
216
|
+
// Update workbook.xml.rels
|
217
|
+
const updatedRelsXml = relsXml.replace("</Relationships>", `<Relationship Id="${newRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="${newTarget}"/></Relationships>`);
|
218
|
+
await this.#set(relsXmlPath, Buffer.from(updatedRelsXml));
|
219
|
+
// Read [Content_Types].xml
|
220
|
+
// Update [Content_Types].xml
|
221
|
+
const contentTypesPath = "[Content_Types].xml";
|
222
|
+
const contentTypesXml = this.#getXml(contentTypesPath);
|
223
|
+
const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
224
|
+
const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
|
225
|
+
await this.#set(contentTypesPath, Buffer.from(updatedContentTypesXml));
|
226
|
+
}
|
227
|
+
finally {
|
228
|
+
this.#isProcessing = false;
|
229
|
+
}
|
230
|
+
}
|
231
|
+
/**
|
232
|
+
* Replaces placeholders in the given sheet with values from the replacements map.
|
233
|
+
*
|
234
|
+
* The function searches for placeholders in the format `${key}` within the sheet
|
235
|
+
* content, where `key` corresponds to a path in the replacements object.
|
236
|
+
* If a value is found for the key, it replaces the placeholder with the value.
|
237
|
+
* If no value is found, the original placeholder remains unchanged.
|
238
|
+
*
|
239
|
+
* @param sheetName - The name of the sheet to be replaced.
|
240
|
+
* @param replacements - An object where keys represent placeholder paths and values are the replacements.
|
241
|
+
* @returns A promise that resolves when the substitution is complete.
|
242
|
+
*/
|
243
|
+
substitute(sheetName, replacements) {
|
244
|
+
this.#ensureNotProcessing();
|
245
|
+
this.#ensureNotDestroyed();
|
246
|
+
this.#isProcessing = true;
|
247
|
+
try {
|
248
|
+
return this.#substitute(sheetName, replacements);
|
249
|
+
}
|
250
|
+
finally {
|
251
|
+
this.#isProcessing = false;
|
252
|
+
}
|
253
|
+
}
|
254
|
+
/**
|
255
|
+
* Inserts rows into a specific sheet in the template.
|
256
|
+
*
|
257
|
+
* @param {Object} data - The data for row insertion.
|
258
|
+
* @param {string} data.sheetName - The name of the sheet to insert rows into.
|
259
|
+
* @param {number} [data.startRowNumber] - The row number to start inserting from.
|
260
|
+
* @param {unknown[][]} data.rows - The rows to insert.
|
261
|
+
* @returns {Promise<void>}
|
262
|
+
* @throws {Error} If the template instance has been destroyed.
|
263
|
+
* @throws {Error} If the sheet does not exist.
|
264
|
+
* @throws {Error} If the row number is out of range.
|
265
|
+
* @throws {Error} If a column is out of range.
|
266
|
+
* @experimental This API is experimental and might change in future versions.
|
267
|
+
*/
|
268
|
+
async insertRows(data) {
|
269
|
+
this.#ensureNotProcessing();
|
270
|
+
this.#ensureNotDestroyed();
|
271
|
+
this.#isProcessing = true;
|
272
|
+
try {
|
273
|
+
const { rows, sheetName, startRowNumber } = data;
|
274
|
+
const preparedRows = rows.map(row => Utils.toExcelColumnObject(row));
|
275
|
+
// Validation
|
276
|
+
Utils.checkStartRow(startRowNumber);
|
277
|
+
Utils.checkRows(preparedRows);
|
278
|
+
// Find the sheet
|
279
|
+
const workbookXml = this.#getXml("xl/workbook.xml");
|
280
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
281
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
282
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
283
|
+
}
|
284
|
+
const rId = sheetMatch[1];
|
285
|
+
const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
|
286
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
287
|
+
if (!relMatch || !relMatch[1]) {
|
288
|
+
throw new Error(`Relationship "${rId}" not found`);
|
289
|
+
}
|
290
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
291
|
+
const sheetXml = this.#getXml(sheetPath);
|
292
|
+
let nextRow = 0;
|
293
|
+
if (!startRowNumber) {
|
294
|
+
// Find the last row
|
295
|
+
let lastRowNumber = 0;
|
296
|
+
const rowMatches = [...sheetXml.matchAll(/<row[^>]+r="(\d+)"[^>]*>/g)];
|
297
|
+
if (rowMatches.length > 0) {
|
298
|
+
lastRowNumber = Math.max(...rowMatches.map((m) => parseInt(m[1], 10)));
|
299
|
+
}
|
300
|
+
nextRow = lastRowNumber + 1;
|
301
|
+
}
|
302
|
+
else {
|
303
|
+
nextRow = startRowNumber;
|
304
|
+
}
|
305
|
+
// Generate XML for all rows
|
306
|
+
const rowsXml = preparedRows.map((cells, i) => {
|
307
|
+
const rowNumber = nextRow + i;
|
308
|
+
const cellTags = Object.entries(cells).map(([col, value]) => {
|
309
|
+
const colUpper = col.toUpperCase();
|
310
|
+
const ref = `${colUpper}${rowNumber}`;
|
311
|
+
return `<c r="${ref}" t="inlineStr"><is><t>${Utils.escapeXml(value)}</t></is></c>`;
|
312
|
+
}).join("");
|
313
|
+
return `<row r="${rowNumber}">${cellTags}</row>`;
|
314
|
+
}).join("");
|
315
|
+
let updatedXml;
|
316
|
+
if (/<sheetData\s*\/>/.test(sheetXml)) {
|
317
|
+
updatedXml = sheetXml.replace(/<sheetData\s*\/>/, `<sheetData>${rowsXml}</sheetData>`);
|
318
|
+
}
|
319
|
+
else if (/<sheetData>([\s\S]*?)<\/sheetData>/.test(sheetXml)) {
|
320
|
+
updatedXml = sheetXml.replace(/<\/sheetData>/, `${rowsXml}</sheetData>`);
|
321
|
+
}
|
322
|
+
else {
|
323
|
+
updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
|
324
|
+
}
|
325
|
+
await this.#set(sheetPath, Buffer.from(updatedXml));
|
326
|
+
}
|
327
|
+
finally {
|
328
|
+
this.#isProcessing = false;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
async insertRowsStream(data) {
|
332
|
+
this.#ensureNotProcessing();
|
333
|
+
this.#ensureNotDestroyed();
|
334
|
+
this.#isProcessing = true;
|
335
|
+
try {
|
336
|
+
const { rows, sheetName, startRowNumber } = data;
|
337
|
+
if (!sheetName)
|
338
|
+
throw new Error("Sheet name is required");
|
339
|
+
const workbookXml = this.#getXml("xl/workbook.xml");
|
340
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
341
|
+
if (!sheetMatch)
|
342
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
343
|
+
const rId = sheetMatch[1];
|
344
|
+
const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
|
345
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
346
|
+
if (!relMatch || !relMatch[1])
|
347
|
+
throw new Error(`Relationship "${rId}" not found`);
|
348
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
349
|
+
const sheetXml = this.#getXml(sheetPath);
|
350
|
+
const output = new MemoryWriteStream();
|
351
|
+
let inserted = false;
|
352
|
+
// --- Case 1: <sheetData>...</sheetData> on one line ---
|
353
|
+
const singleLineMatch = sheetXml.match(/(<sheetData[^>]*>)(.*)(<\/sheetData>)/);
|
354
|
+
if (!inserted && singleLineMatch) {
|
355
|
+
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(sheetXml);
|
356
|
+
const openTag = singleLineMatch[1];
|
357
|
+
const innerRows = singleLineMatch[2].trim();
|
358
|
+
const closeTag = singleLineMatch[3];
|
359
|
+
const innerRowsMap = Utils.parseRows(innerRows);
|
360
|
+
output.write(sheetXml.slice(0, singleLineMatch.index));
|
361
|
+
output.write(openTag);
|
362
|
+
if (innerRows) {
|
363
|
+
if (startRowNumber) {
|
364
|
+
const filtered = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
365
|
+
if (filtered)
|
366
|
+
output.write(filtered);
|
367
|
+
}
|
368
|
+
else {
|
369
|
+
output.write(innerRows);
|
370
|
+
}
|
371
|
+
}
|
372
|
+
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
373
|
+
if (innerRows) {
|
374
|
+
const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
375
|
+
if (filtered)
|
376
|
+
output.write(filtered);
|
377
|
+
}
|
378
|
+
output.write(closeTag);
|
379
|
+
output.write(sheetXml.slice(singleLineMatch.index + singleLineMatch[0].length));
|
380
|
+
inserted = true;
|
381
|
+
}
|
382
|
+
// --- Case 2: <sheetData/> ---
|
383
|
+
if (!inserted && /<sheetData\s*\/>/.test(sheetXml)) {
|
384
|
+
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(sheetXml);
|
385
|
+
const match = sheetXml.match(/<sheetData\s*\/>/);
|
386
|
+
const matchIndex = match.index;
|
387
|
+
output.write(sheetXml.slice(0, matchIndex));
|
388
|
+
output.write("<sheetData>");
|
389
|
+
await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
390
|
+
output.write("</sheetData>");
|
391
|
+
output.write(sheetXml.slice(matchIndex + match[0].length));
|
392
|
+
inserted = true;
|
393
|
+
}
|
394
|
+
// --- Case 3: Multiline <sheetData> ---
|
395
|
+
if (!inserted && sheetXml.includes("<sheetData")) {
|
396
|
+
const openTagMatch = sheetXml.match(/<sheetData[^>]*>/);
|
397
|
+
const closeTag = "</sheetData>";
|
398
|
+
if (!openTagMatch)
|
399
|
+
throw new Error("Invalid sheetData structure");
|
400
|
+
const openTag = openTagMatch[0];
|
401
|
+
const openIdx = sheetXml.indexOf(openTag);
|
402
|
+
const closeIdx = sheetXml.lastIndexOf(closeTag);
|
403
|
+
if (closeIdx === -1)
|
404
|
+
throw new Error("Missing </sheetData>");
|
405
|
+
const beforeRows = sheetXml.slice(0, openIdx + openTag.length);
|
406
|
+
const innerRows = sheetXml.slice(openIdx + openTag.length, closeIdx).trim();
|
407
|
+
const afterRows = sheetXml.slice(closeIdx + closeTag.length);
|
408
|
+
const innerRowsMap = Utils.parseRows(innerRows);
|
409
|
+
output.write(beforeRows);
|
410
|
+
if (innerRows) {
|
411
|
+
if (startRowNumber) {
|
412
|
+
const filtered = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
413
|
+
if (filtered)
|
414
|
+
output.write(filtered);
|
415
|
+
}
|
416
|
+
else {
|
417
|
+
output.write(innerRows);
|
418
|
+
}
|
419
|
+
}
|
420
|
+
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, Utils.getMaxRowNumber(innerRows));
|
421
|
+
if (innerRows) {
|
422
|
+
const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
423
|
+
if (filtered)
|
424
|
+
output.write(filtered);
|
425
|
+
}
|
426
|
+
output.write(closeTag);
|
427
|
+
output.write(afterRows);
|
428
|
+
inserted = true;
|
429
|
+
}
|
430
|
+
if (!inserted)
|
431
|
+
throw new Error("Failed to locate <sheetData> for insertion");
|
432
|
+
// ← теперь мы не собираем строку, а собираем Buffer
|
433
|
+
this.files[sheetPath] = output.toBuffer();
|
434
|
+
}
|
435
|
+
finally {
|
436
|
+
this.#isProcessing = false;
|
437
|
+
}
|
438
|
+
}
|
439
|
+
/**
|
440
|
+
* Saves the modified Excel template to a buffer.
|
441
|
+
*
|
442
|
+
* @returns {Promise<Buffer>} The modified Excel template as a buffer.
|
443
|
+
* @throws {Error} If the template instance has been destroyed.
|
444
|
+
* @experimental This API is experimental and might change in future versions.
|
445
|
+
*/
|
446
|
+
async save() {
|
447
|
+
this.#ensureNotProcessing();
|
448
|
+
this.#ensureNotDestroyed();
|
449
|
+
this.#isProcessing = true;
|
450
|
+
try {
|
451
|
+
const zipBuffer = await Zip.create(this.files);
|
452
|
+
this.destroyed = true;
|
453
|
+
// Очистка всех буферов
|
454
|
+
for (const key in this.files) {
|
455
|
+
if (this.files.hasOwnProperty(key)) {
|
456
|
+
this.files[key] = Buffer.alloc(0); // Заменяем на пустой буфер
|
457
|
+
}
|
458
|
+
}
|
459
|
+
return zipBuffer;
|
460
|
+
}
|
461
|
+
finally {
|
462
|
+
this.#isProcessing = false;
|
463
|
+
}
|
464
|
+
}
|
465
|
+
/**
|
466
|
+
* Replaces the contents of a file in the template.
|
467
|
+
*
|
468
|
+
* @param {string} key - The Excel path of the file to replace.
|
469
|
+
* @param {Buffer|string} content - The new content.
|
470
|
+
* @returns {Promise<void>}
|
471
|
+
* @throws {Error} If the template instance has been destroyed.
|
472
|
+
* @throws {Error} If the file does not exist in the template.
|
473
|
+
* @experimental This API is experimental and might change in future versions.
|
474
|
+
*/
|
475
|
+
async set(key, content) {
|
476
|
+
this.#ensureNotProcessing();
|
477
|
+
this.#ensureNotDestroyed();
|
478
|
+
this.#isProcessing = true;
|
479
|
+
try {
|
480
|
+
await this.#set(key, content);
|
481
|
+
}
|
482
|
+
finally {
|
483
|
+
this.#isProcessing = false;
|
484
|
+
}
|
485
|
+
}
|
486
|
+
static async from(data) {
|
487
|
+
const { source } = data;
|
488
|
+
const buffer = typeof source === "string"
|
489
|
+
? await fs.readFile(source)
|
490
|
+
: source;
|
491
|
+
const files = await Zip.read(buffer);
|
492
|
+
return new TemplateMemory(files);
|
493
|
+
}
|
494
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { columnIndexToLetter } from "./column-index-to-letter.js";
|
2
|
+
import { escapeXml } from "./escape-xml.js";
|
3
|
+
export function prepareRowToCells(row, rowNumber) {
|
4
|
+
return row.map((value, colIndex) => {
|
5
|
+
const colLetter = columnIndexToLetter(colIndex);
|
6
|
+
const cellRef = `${colLetter}${rowNumber}`;
|
7
|
+
const cellValue = escapeXml(String(value ?? ""));
|
8
|
+
return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`;
|
9
|
+
});
|
10
|
+
}
|
@@ -1,5 +1,4 @@
|
|
1
|
-
import {
|
2
|
-
import { escapeXml } from "./escape-xml.js";
|
1
|
+
import { prepareRowToCells } from "./prepare-row-to-cells.js";
|
3
2
|
/**
|
4
3
|
* Writes an async iterable of rows to an Excel XML file.
|
5
4
|
*
|
@@ -25,15 +24,20 @@ export async function writeRowsToStream(output, rows, startRowNumber) {
|
|
25
24
|
let rowNumber = startRowNumber;
|
26
25
|
for await (const row of rows) {
|
27
26
|
// Transform the row into XML
|
28
|
-
|
29
|
-
const
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
if (Array.isArray(row[0])) {
|
28
|
+
for (const subRow of row) {
|
29
|
+
const cells = prepareRowToCells(subRow, rowNumber);
|
30
|
+
// Write the row to the file
|
31
|
+
output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
|
32
|
+
rowNumber++;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
else {
|
36
|
+
const cells = prepareRowToCells(row, rowNumber);
|
37
|
+
// Write the row to the file
|
38
|
+
output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
|
39
|
+
rowNumber++;
|
40
|
+
}
|
37
41
|
}
|
38
42
|
return { rowNumber };
|
39
43
|
}
|
@@ -0,0 +1,85 @@
|
|
1
|
+
/**
|
2
|
+
* A class for manipulating Excel templates by extracting, modifying, and repacking Excel files.
|
3
|
+
*
|
4
|
+
* @experimental This API is experimental and might change in future versions.
|
5
|
+
*/
|
6
|
+
export declare class TemplateMemory {
|
7
|
+
#private;
|
8
|
+
files: Record<string, Buffer>;
|
9
|
+
/**
|
10
|
+
* Flag indicating whether this template instance has been destroyed.
|
11
|
+
* @type {boolean}
|
12
|
+
*/
|
13
|
+
destroyed: boolean;
|
14
|
+
constructor(files: Record<string, Buffer>);
|
15
|
+
/**
|
16
|
+
* Copies a sheet from the template to a new name.
|
17
|
+
*
|
18
|
+
* @param {string} sourceName - The name of the sheet to copy.
|
19
|
+
* @param {string} newName - The new name for the sheet.
|
20
|
+
* @returns {Promise<void>}
|
21
|
+
* @throws {Error} If the sheet with the source name does not exist.
|
22
|
+
* @throws {Error} If a sheet with the new name already exists.
|
23
|
+
* @experimental This API is experimental and might change in future versions.
|
24
|
+
*/
|
25
|
+
copySheet(sourceName: string, newName: string): Promise<void>;
|
26
|
+
/**
|
27
|
+
* Replaces placeholders in the given sheet with values from the replacements map.
|
28
|
+
*
|
29
|
+
* The function searches for placeholders in the format `${key}` within the sheet
|
30
|
+
* content, where `key` corresponds to a path in the replacements object.
|
31
|
+
* If a value is found for the key, it replaces the placeholder with the value.
|
32
|
+
* If no value is found, the original placeholder remains unchanged.
|
33
|
+
*
|
34
|
+
* @param sheetName - The name of the sheet to be replaced.
|
35
|
+
* @param replacements - An object where keys represent placeholder paths and values are the replacements.
|
36
|
+
* @returns A promise that resolves when the substitution is complete.
|
37
|
+
*/
|
38
|
+
substitute(sheetName: string, replacements: Record<string, unknown>): Promise<void>;
|
39
|
+
/**
|
40
|
+
* Inserts rows into a specific sheet in the template.
|
41
|
+
*
|
42
|
+
* @param {Object} data - The data for row insertion.
|
43
|
+
* @param {string} data.sheetName - The name of the sheet to insert rows into.
|
44
|
+
* @param {number} [data.startRowNumber] - The row number to start inserting from.
|
45
|
+
* @param {unknown[][]} data.rows - The rows to insert.
|
46
|
+
* @returns {Promise<void>}
|
47
|
+
* @throws {Error} If the template instance has been destroyed.
|
48
|
+
* @throws {Error} If the sheet does not exist.
|
49
|
+
* @throws {Error} If the row number is out of range.
|
50
|
+
* @throws {Error} If a column is out of range.
|
51
|
+
* @experimental This API is experimental and might change in future versions.
|
52
|
+
*/
|
53
|
+
insertRows(data: {
|
54
|
+
sheetName: string;
|
55
|
+
startRowNumber?: number;
|
56
|
+
rows: unknown[][];
|
57
|
+
}): Promise<void>;
|
58
|
+
insertRowsStream(data: {
|
59
|
+
sheetName: string;
|
60
|
+
startRowNumber?: number;
|
61
|
+
rows: AsyncIterable<unknown[]>;
|
62
|
+
}): Promise<void>;
|
63
|
+
/**
|
64
|
+
* Saves the modified Excel template to a buffer.
|
65
|
+
*
|
66
|
+
* @returns {Promise<Buffer>} The modified Excel template as a buffer.
|
67
|
+
* @throws {Error} If the template instance has been destroyed.
|
68
|
+
* @experimental This API is experimental and might change in future versions.
|
69
|
+
*/
|
70
|
+
save(): Promise<Buffer>;
|
71
|
+
/**
|
72
|
+
* Replaces the contents of a file in the template.
|
73
|
+
*
|
74
|
+
* @param {string} key - The Excel path of the file to replace.
|
75
|
+
* @param {Buffer|string} content - The new content.
|
76
|
+
* @returns {Promise<void>}
|
77
|
+
* @throws {Error} If the template instance has been destroyed.
|
78
|
+
* @throws {Error} If the file does not exist in the template.
|
79
|
+
* @experimental This API is experimental and might change in future versions.
|
80
|
+
*/
|
81
|
+
set(key: string, content: Buffer): Promise<void>;
|
82
|
+
static from(data: {
|
83
|
+
source: string | Buffer;
|
84
|
+
}): Promise<TemplateMemory>;
|
85
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare function prepareRowToCells(row: unknown[], rowNumber: number): string[];
|
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
interface WritableLike {
|
2
|
+
write(chunk: string | Buffer): boolean;
|
3
|
+
end?: () => void;
|
4
|
+
}
|
2
5
|
/**
|
3
6
|
* Writes an async iterable of rows to an Excel XML file.
|
4
7
|
*
|
@@ -20,6 +23,7 @@ import * as fs from "node:fs";
|
|
20
23
|
* last row number written to the file (i.e., the `startRowNumber`
|
21
24
|
* plus the number of rows written).
|
22
25
|
*/
|
23
|
-
export declare function writeRowsToStream(output:
|
26
|
+
export declare function writeRowsToStream(output: WritableLike, rows: AsyncIterable<unknown[] | unknown[][]>, startRowNumber: number): Promise<{
|
24
27
|
rowNumber: number;
|
25
28
|
}>;
|
29
|
+
export {};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@js-ak/excel-toolbox",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.5.0",
|
4
4
|
"description": "excel-toolbox",
|
5
5
|
"publishConfig": {
|
6
6
|
"access": "public",
|
@@ -45,7 +45,8 @@
|
|
45
45
|
"postbuild:esm": "node scripts/write-esm-package.js",
|
46
46
|
"postbuild:cjs": "node scripts/write-cjs-package.js",
|
47
47
|
"test": "vitest run",
|
48
|
-
"test:watch": "vitest"
|
48
|
+
"test:watch": "vitest",
|
49
|
+
"test:coverage": "vitest run --coverage"
|
49
50
|
},
|
50
51
|
"repository": {
|
51
52
|
"type": "git",
|
@@ -70,6 +71,7 @@
|
|
70
71
|
"@stylistic/eslint-plugin-ts": "4.2.0",
|
71
72
|
"@types/node": "22.14.0",
|
72
73
|
"@types/pako": "2.0.3",
|
74
|
+
"@vitest/coverage-v8": "3.1.2",
|
73
75
|
"eslint": "9.24.0",
|
74
76
|
"eslint-plugin-sort-destructure-keys": "2.0.0",
|
75
77
|
"eslint-plugin-sort-exports": "0.9.1",
|
@@ -77,7 +79,7 @@
|
|
77
79
|
"semantic-release": "24.0.0",
|
78
80
|
"typescript": "5.8.3",
|
79
81
|
"typescript-eslint": "8.29.0",
|
80
|
-
"vitest": "3.1.
|
82
|
+
"vitest": "3.1.2"
|
81
83
|
},
|
82
84
|
"dependencies": {
|
83
85
|
"pako": "2.1.0"
|