@dipanshuhandoo/excelio 1.0.0 → 1.1.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/CHANGELOG.md +32 -1
- package/README.md +218 -22
- package/dist/cjs/index.cjs +329 -52
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/index.d.cts +23 -4
- package/dist/cjs/workers/reader.worker.cjs.map +2 -2
- package/dist/cjs/workers/writer.worker.cjs +318 -49
- package/dist/cjs/workers/writer.worker.cjs.map +4 -4
- package/dist/cjs-min/index.cjs +1 -1
- package/dist/cjs-min/index.cjs.map +4 -4
- package/dist/cjs-min/index.d.cts +23 -4
- package/dist/cjs-min/workers/reader.worker.cjs.map +2 -2
- package/dist/cjs-min/workers/writer.worker.cjs +1 -1
- package/dist/cjs-min/workers/writer.worker.cjs.map +4 -4
- package/dist/esm/index.d.ts +23 -4
- package/dist/esm/index.js +330 -53
- package/dist/esm/index.js.map +3 -3
- package/dist/esm/workers/reader.worker.js.map +2 -2
- package/dist/esm/workers/writer.worker.js +319 -50
- package/dist/esm/workers/writer.worker.js.map +3 -3
- package/dist/esm-min/index.d.ts +23 -4
- package/dist/esm-min/index.js +1 -1
- package/dist/esm-min/index.js.map +4 -4
- package/dist/esm-min/workers/reader.worker.js.map +2 -2
- package/dist/esm-min/workers/writer.worker.js +1 -1
- package/dist/esm-min/workers/writer.worker.js.map +4 -4
- package/package.json +3 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -38,6 +38,8 @@ module.exports = __toCommonJS(src_exports);
|
|
|
38
38
|
// src/writer.js
|
|
39
39
|
var import_node_stream = require("node:stream");
|
|
40
40
|
var import_node_fs = require("node:fs");
|
|
41
|
+
var import_promises = require("node:fs/promises");
|
|
42
|
+
var import_promises2 = require("node:timers/promises");
|
|
41
43
|
var import_exceljs = __toESM(require("exceljs"), 1);
|
|
42
44
|
|
|
43
45
|
// src/errors.js
|
|
@@ -138,11 +140,44 @@ function formatDate(d, fmt) {
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
// src/rowSource.js
|
|
144
|
+
function rowSourceKind(data) {
|
|
145
|
+
if (data === void 0 || data === null) return "empty";
|
|
146
|
+
if (Array.isArray(data)) return "array";
|
|
147
|
+
if (typeof data !== "object" && typeof data !== "function") return "invalid";
|
|
148
|
+
if (typeof data[Symbol.asyncIterator] === "function") return "async";
|
|
149
|
+
if (typeof data[Symbol.iterator] === "function") return "sync";
|
|
150
|
+
return "invalid";
|
|
151
|
+
}
|
|
152
|
+
var isIterableSource = (data) => {
|
|
153
|
+
const kind = rowSourceKind(data);
|
|
154
|
+
return kind === "sync" || kind === "async";
|
|
155
|
+
};
|
|
156
|
+
var specHasIterable = (spec) => Array.isArray(spec) && spec.some((wb) => (Array.isArray(wb?.sheets) ? wb.sheets : []).some((s) => isIterableSource(s?.data)));
|
|
157
|
+
async function closeIterator(iterator) {
|
|
158
|
+
if (typeof iterator?.return !== "function") return;
|
|
159
|
+
try {
|
|
160
|
+
await iterator.return();
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function sourceFailure(err, details, signal) {
|
|
165
|
+
if (signal?.aborted) return new ExcelIoError("Aborted", { code: "ABORTED", cause: err, details });
|
|
166
|
+
return new ExcelIoError(`Row source failed for sheet "${details.sheetName}": ${err?.message ?? err}`, {
|
|
167
|
+
code: "SOURCE_ERROR",
|
|
168
|
+
cause: err,
|
|
169
|
+
details
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
141
173
|
// src/writer.js
|
|
142
174
|
var DEFAULT_BATCH = 5e3;
|
|
143
175
|
var SHEET_NAME_MAX = 31;
|
|
176
|
+
var EXCEL_MAX_DATA_ROWS = 1048575;
|
|
177
|
+
var OUTPUT_HIGH_WATER_MARK = 8 * 1024 * 1024;
|
|
178
|
+
var BACKLOG_POLL_MS = 5;
|
|
144
179
|
async function writeSpec(spec, options = {}) {
|
|
145
|
-
|
|
180
|
+
preflightWrite(spec, options);
|
|
146
181
|
const { to } = options;
|
|
147
182
|
if (to !== void 0 && spec.length > 1) {
|
|
148
183
|
throw new ExcelIoError(
|
|
@@ -162,41 +197,106 @@ async function writeSpec(spec, options = {}) {
|
|
|
162
197
|
return buffers.length === 1 ? buffers[0] : buffers;
|
|
163
198
|
}
|
|
164
199
|
function writeSpecStream(spec, options = {}) {
|
|
165
|
-
|
|
200
|
+
preflightWrite(spec, options);
|
|
166
201
|
const passthrough = new import_node_stream.PassThrough();
|
|
167
202
|
writeOneWorkbook(spec[0], 0, passthrough, options).catch((err) => passthrough.destroy(err));
|
|
168
203
|
return passthrough;
|
|
169
204
|
}
|
|
205
|
+
function preflightWrite(spec, options = {}) {
|
|
206
|
+
validateSpec(spec);
|
|
207
|
+
validateWriteOptions(options);
|
|
208
|
+
}
|
|
170
209
|
async function writeOneWorkbookToBuffer(workbookSpec, wbIndex, options) {
|
|
171
210
|
const chunks = [];
|
|
172
211
|
const collector = new import_node_stream.PassThrough();
|
|
173
212
|
collector.on("data", (chunk) => chunks.push(chunk));
|
|
174
|
-
await writeOneWorkbook(workbookSpec, wbIndex, collector, options);
|
|
213
|
+
await writeOneWorkbook(workbookSpec, wbIndex, collector, options, { owned: true });
|
|
175
214
|
return Buffer.concat(chunks);
|
|
176
215
|
}
|
|
177
|
-
async function writeOneWorkbook(workbookSpec, wbIndex, target, options) {
|
|
216
|
+
async function writeOneWorkbook(workbookSpec, wbIndex, target, options, sinkOptions = {}) {
|
|
178
217
|
const { signal, onProgress, beforeWrite, batchSize = DEFAULT_BATCH, logger } = options;
|
|
179
|
-
const
|
|
180
|
-
const wb = new import_exceljs.default.stream.xlsx.WorkbookWriter({
|
|
181
|
-
stream: sink.writable,
|
|
182
|
-
useStyles: true,
|
|
183
|
-
useSharedStrings: true
|
|
184
|
-
});
|
|
185
|
-
applyWorkbookProperties(wb, workbookSpec.properties);
|
|
186
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
218
|
+
const maxRowsPerSheet = options.maxRowsPerSheet ?? EXCEL_MAX_DATA_ROWS;
|
|
187
219
|
const sheets = workbookSpec.sheets || [];
|
|
188
|
-
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
220
|
+
const sink = resolveSink(target, sinkOptions);
|
|
221
|
+
let wb = null;
|
|
222
|
+
try {
|
|
223
|
+
wb = new import_exceljs.default.stream.xlsx.WorkbookWriter({
|
|
224
|
+
stream: sink.writable,
|
|
225
|
+
useStyles: true,
|
|
226
|
+
useSharedStrings: resolveSharedStrings(options.sharedStrings, sheets)
|
|
227
|
+
});
|
|
228
|
+
applyWorkbookProperties(wb, workbookSpec.properties);
|
|
229
|
+
const used = /* @__PURE__ */ new Set();
|
|
230
|
+
const firstNames = sheets.map((s, i) => sanitiseSheetName(s.sheet || `Sheet ${i + 1}`, used, logger));
|
|
231
|
+
const totalRows = computeTotalRows(sheets);
|
|
232
|
+
const state = { rowsProcessed: 0, last: { sheetIndex: 0, sheetName: firstNames[0], sheetPart: 1 } };
|
|
233
|
+
const emit = () => onProgress?.({
|
|
234
|
+
phase: "write",
|
|
235
|
+
workbookIndex: wbIndex,
|
|
236
|
+
...state.last,
|
|
237
|
+
rowsProcessed: state.rowsProcessed,
|
|
238
|
+
totalRows
|
|
239
|
+
});
|
|
240
|
+
for (let sIndex = 0; sIndex < sheets.length; sIndex++) {
|
|
241
|
+
throwIfAborted(signal);
|
|
242
|
+
const sheetSpec = sheets[sIndex];
|
|
243
|
+
const writer = createSheetWriter({
|
|
244
|
+
wb,
|
|
245
|
+
workbookSpec,
|
|
246
|
+
wbIndex,
|
|
247
|
+
sIndex,
|
|
248
|
+
sheetSpec,
|
|
249
|
+
firstName: firstNames[sIndex],
|
|
250
|
+
used,
|
|
251
|
+
beforeWrite,
|
|
252
|
+
maxRowsPerSheet,
|
|
253
|
+
onOpen: (sheetName, sheetPart) => {
|
|
254
|
+
state.last = { sheetIndex: sIndex, sheetName, sheetPart };
|
|
255
|
+
},
|
|
256
|
+
onRow: () => {
|
|
257
|
+
state.rowsProcessed++;
|
|
258
|
+
if (onProgress && state.rowsProcessed % batchSize === 0) emit();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
const kind = rowSourceKind(sheetSpec.data);
|
|
262
|
+
if (kind === "sync" || kind === "async") {
|
|
263
|
+
await writeIterableSheet({ writer, wb, sink, sheetSpec, kind, signal, batchSize, wbIndex, sIndex });
|
|
264
|
+
} else {
|
|
265
|
+
const data = sheetSpec.data || [];
|
|
266
|
+
writer.start(resolveColumns(sheetSpec.columns, data));
|
|
267
|
+
for (let i = 0; i < data.length; i++) {
|
|
268
|
+
throwIfAborted(signal);
|
|
269
|
+
writer.write(data[i]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
writer.finish();
|
|
197
273
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
274
|
+
const committing = wb.commit();
|
|
275
|
+
committing.catch(() => {
|
|
276
|
+
});
|
|
277
|
+
await Promise.race([committing, sink.failed]);
|
|
278
|
+
if (sink.cleanup) await Promise.race([sink.cleanup(), sink.failed]);
|
|
279
|
+
sink.release();
|
|
280
|
+
emit();
|
|
281
|
+
} catch (err) {
|
|
282
|
+
await abortWorkbook(wb, sink, err);
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function createSheetWriter({ wb, workbookSpec, wbIndex, sIndex, sheetSpec, firstName, used, beforeWrite, maxRowsPerSheet, onOpen, onRow }) {
|
|
287
|
+
const baseName = sheetSpec.sheet || `Sheet ${sIndex + 1}`;
|
|
288
|
+
let cols = [];
|
|
289
|
+
let ws = null;
|
|
290
|
+
let sheetName = null;
|
|
291
|
+
let part = 0;
|
|
292
|
+
let rowsInPart = 0;
|
|
293
|
+
const open = () => {
|
|
294
|
+
part += 1;
|
|
295
|
+
rowsInPart = 0;
|
|
296
|
+
sheetName = part === 1 ? firstName : sanitiseSheetName(baseName, used, null);
|
|
297
|
+
const wsOptions = {};
|
|
298
|
+
if (sheetSpec.styling?.freezeHeader) wsOptions.views = [{ state: "frozen", ySplit: 1 }];
|
|
299
|
+
ws = wb.addWorksheet(sheetName, wsOptions);
|
|
200
300
|
if (cols.length) {
|
|
201
301
|
ws.columns = cols.map((c) => ({
|
|
202
302
|
key: c.key,
|
|
@@ -206,52 +306,188 @@ async function writeOneWorkbook(workbookSpec, wbIndex, target, options) {
|
|
|
206
306
|
}));
|
|
207
307
|
}
|
|
208
308
|
applyStylingPostColumns(ws, sheetSpec.styling, cols.length);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
309
|
+
onOpen(sheetName, part);
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
get sheetName() {
|
|
313
|
+
return sheetName ?? firstName;
|
|
314
|
+
},
|
|
315
|
+
start(resolvedColumns) {
|
|
316
|
+
cols = resolvedColumns;
|
|
317
|
+
open();
|
|
318
|
+
},
|
|
319
|
+
write(row) {
|
|
320
|
+
if (rowsInPart >= maxRowsPerSheet) {
|
|
321
|
+
ws.commit();
|
|
322
|
+
open();
|
|
323
|
+
}
|
|
324
|
+
let out = row;
|
|
213
325
|
if (beforeWrite) {
|
|
214
|
-
|
|
326
|
+
out = beforeWrite(row, {
|
|
215
327
|
workbookIndex: wbIndex,
|
|
216
328
|
workbookName: workbookSpec.workbook,
|
|
217
329
|
sheetIndex: sIndex,
|
|
218
330
|
sheetName,
|
|
219
|
-
|
|
331
|
+
sheetPart: part,
|
|
332
|
+
rowNumber: rowsInPart + 2
|
|
220
333
|
// header is row 1
|
|
221
334
|
}) ?? row;
|
|
222
335
|
}
|
|
223
|
-
const
|
|
224
|
-
acc[c.key] = valueForWrite(
|
|
336
|
+
const values = cols.length ? cols.reduce((acc, c) => {
|
|
337
|
+
acc[c.key] = valueForWrite(out[c.key], c.type);
|
|
225
338
|
return acc;
|
|
226
|
-
}, {}) :
|
|
227
|
-
ws.addRow(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
339
|
+
}, {}) : out;
|
|
340
|
+
ws.addRow(values).commit();
|
|
341
|
+
rowsInPart++;
|
|
342
|
+
onRow();
|
|
343
|
+
},
|
|
344
|
+
finish() {
|
|
345
|
+
ws?.commit();
|
|
232
346
|
}
|
|
233
|
-
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function writeIterableSheet({ writer, wb, sink, sheetSpec, kind, signal, batchSize, wbIndex, sIndex }) {
|
|
350
|
+
let rowsRead = 0;
|
|
351
|
+
const details = () => ({ workbookIndex: wbIndex, sheetIndex: sIndex, sheetName: writer.sheetName, rowsRead });
|
|
352
|
+
let iterator;
|
|
353
|
+
try {
|
|
354
|
+
iterator = kind === "async" ? sheetSpec.data[Symbol.asyncIterator]() : sheetSpec.data[Symbol.iterator]();
|
|
355
|
+
} catch (err) {
|
|
356
|
+
throw sourceFailure(err, details(), signal);
|
|
234
357
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
358
|
+
const next = async () => {
|
|
359
|
+
let step;
|
|
360
|
+
try {
|
|
361
|
+
step = await iterator.next();
|
|
362
|
+
} catch (err) {
|
|
363
|
+
throw sourceFailure(err, details(), signal);
|
|
364
|
+
}
|
|
365
|
+
if (step === null || typeof step !== "object") {
|
|
366
|
+
throw sourceFailure(new TypeError("Iterator result is not an object"), details(), signal);
|
|
367
|
+
}
|
|
368
|
+
return step;
|
|
369
|
+
};
|
|
370
|
+
let finished = false;
|
|
371
|
+
try {
|
|
372
|
+
let step = await next();
|
|
373
|
+
writer.start(resolveColumns(sheetSpec.columns, step.done ? [] : [step.value]));
|
|
374
|
+
while (!step.done) {
|
|
375
|
+
throwIfAborted(signal);
|
|
376
|
+
writer.write(step.value);
|
|
377
|
+
rowsRead++;
|
|
378
|
+
if (rowsRead % batchSize === 0) await waitForCapacity({ wb, sink, signal });
|
|
379
|
+
step = await next();
|
|
380
|
+
}
|
|
381
|
+
finished = true;
|
|
382
|
+
} finally {
|
|
383
|
+
if (!finished) await closeIterator(iterator);
|
|
239
384
|
}
|
|
240
385
|
}
|
|
241
|
-
|
|
386
|
+
var len = (n) => Number.isFinite(n) ? n : 0;
|
|
387
|
+
var streamLen = (s) => len(s?.writableLength ?? s?._writableState?.length) + len(s?.readableLength ?? s?._readableState?.length);
|
|
388
|
+
function backlogBytes(wb, sink) {
|
|
389
|
+
let total = streamLen(wb?.zip) + streamLen(wb?.zip?._module) + streamLen(sink?.writable);
|
|
390
|
+
for (const ws of wb?._worksheets ?? []) {
|
|
391
|
+
const sb = ws?._stream;
|
|
392
|
+
if (!sb) continue;
|
|
393
|
+
for (const buf of sb.buffers ?? []) total += len(buf?.length);
|
|
394
|
+
for (const p of sb.pipes ?? []) total += streamLen(p);
|
|
395
|
+
}
|
|
396
|
+
return total;
|
|
397
|
+
}
|
|
398
|
+
var backlogStats = { peak: 0 };
|
|
399
|
+
async function waitForCapacity({ wb, sink, signal }) {
|
|
400
|
+
const check = () => {
|
|
401
|
+
throwIfAborted(signal);
|
|
402
|
+
if (sink.failure) throw sink.failure;
|
|
403
|
+
};
|
|
404
|
+
for (; ; ) {
|
|
405
|
+
check();
|
|
406
|
+
const backlog = backlogBytes(wb, sink);
|
|
407
|
+
if (backlog > backlogStats.peak) backlogStats.peak = backlog;
|
|
408
|
+
if (backlog <= OUTPUT_HIGH_WATER_MARK) break;
|
|
409
|
+
await (0, import_promises2.setTimeout)(BACKLOG_POLL_MS);
|
|
410
|
+
}
|
|
411
|
+
await (0, import_promises2.setImmediate)();
|
|
412
|
+
check();
|
|
413
|
+
}
|
|
414
|
+
function resolveSink(target, { owned = false } = {}) {
|
|
415
|
+
const track = (writable, isOwned) => {
|
|
416
|
+
const sink = { writable, owned: isOwned, failure: null };
|
|
417
|
+
let onError;
|
|
418
|
+
sink.failed = new Promise((_, reject) => {
|
|
419
|
+
onError = (err) => {
|
|
420
|
+
if (sink.failure) return;
|
|
421
|
+
sink.failure = err;
|
|
422
|
+
reject(err);
|
|
423
|
+
};
|
|
424
|
+
writable.on("error", onError);
|
|
425
|
+
});
|
|
426
|
+
sink.failed.catch(() => {
|
|
427
|
+
});
|
|
428
|
+
sink.release = () => {
|
|
429
|
+
if (!isOwned) writable.removeListener("error", onError);
|
|
430
|
+
};
|
|
431
|
+
return sink;
|
|
432
|
+
};
|
|
242
433
|
if (typeof target === "string") {
|
|
243
434
|
const fileStream = (0, import_node_fs.createWriteStream)(target);
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
435
|
+
const sink = track(fileStream, true);
|
|
436
|
+
const finished = new Promise((resolve) => fileStream.once("finish", resolve));
|
|
437
|
+
const closed = new Promise((resolve) => fileStream.once("close", resolve));
|
|
438
|
+
sink.cleanup = () => finished;
|
|
439
|
+
sink.remove = async () => {
|
|
440
|
+
fileStream.destroy();
|
|
441
|
+
await closed;
|
|
442
|
+
await (0, import_promises.rm)(target, { force: true });
|
|
443
|
+
};
|
|
444
|
+
return sink;
|
|
249
445
|
}
|
|
250
446
|
if (target && typeof target.write === "function") {
|
|
251
|
-
return
|
|
447
|
+
return track(target, owned);
|
|
252
448
|
}
|
|
253
449
|
throw new ExcelIoError(`Invalid write target \u2014 expected file path or writable stream`, { code: "INVALID_SPEC" });
|
|
254
450
|
}
|
|
451
|
+
async function abortWorkbook(wb, sink, err) {
|
|
452
|
+
const quiet = (s) => s?.on?.("error", () => {
|
|
453
|
+
});
|
|
454
|
+
try {
|
|
455
|
+
if (wb) {
|
|
456
|
+
wb.zip?.unpipe?.(sink.writable);
|
|
457
|
+
quiet(wb.zip);
|
|
458
|
+
quiet(wb.zip?._module);
|
|
459
|
+
for (const ws of wb._worksheets ?? []) (ws?._stream?.pipes ?? []).forEach(quiet);
|
|
460
|
+
wb.zip?.abort?.();
|
|
461
|
+
}
|
|
462
|
+
if (sink.remove) await sink.remove();
|
|
463
|
+
else if (sink.owned) sink.writable.destroy?.();
|
|
464
|
+
sink.release();
|
|
465
|
+
} catch (cleanupError) {
|
|
466
|
+
try {
|
|
467
|
+
if (typeof err === "object" && err !== null || typeof err === "function") err.cleanupError = cleanupError;
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function resolveSharedStrings(option = "auto", sheets) {
|
|
473
|
+
if (typeof option === "boolean") return option;
|
|
474
|
+
return !sheets.some((s) => isIterableSource(s?.data));
|
|
475
|
+
}
|
|
476
|
+
function computeTotalRows(sheets) {
|
|
477
|
+
let total = 0;
|
|
478
|
+
for (const s of sheets) {
|
|
479
|
+
const kind = rowSourceKind(s.data);
|
|
480
|
+
if (kind === "array") total += s.data.length;
|
|
481
|
+
else if (kind === "sync" || kind === "async") {
|
|
482
|
+
if (s.totalRows === void 0) return void 0;
|
|
483
|
+
total += s.totalRows;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return total;
|
|
487
|
+
}
|
|
488
|
+
function throwIfAborted(signal) {
|
|
489
|
+
if (signal?.aborted) throw new ExcelIoError("Aborted", { code: "ABORTED" });
|
|
490
|
+
}
|
|
255
491
|
function applyWorkbookProperties(wb, props) {
|
|
256
492
|
if (!props) return;
|
|
257
493
|
if (props.author) wb.creator = props.author;
|
|
@@ -295,13 +531,47 @@ function validateSpec(spec) {
|
|
|
295
531
|
if (!Array.isArray(spec) || spec.length === 0) {
|
|
296
532
|
throw new ExcelIoError("Spec must be a non-empty array of workbooks", { code: "INVALID_SPEC" });
|
|
297
533
|
}
|
|
298
|
-
|
|
534
|
+
const owners = /* @__PURE__ */ new Map();
|
|
535
|
+
spec.forEach((wb, wbIndex) => {
|
|
299
536
|
if (!wb || typeof wb !== "object") {
|
|
300
537
|
throw new ExcelIoError("Workbook entries must be objects", { code: "INVALID_SPEC" });
|
|
301
538
|
}
|
|
302
539
|
if (!Array.isArray(wb.sheets) || wb.sheets.length === 0) {
|
|
303
540
|
throw new ExcelIoError("Workbook must have at least one sheet", { code: "INVALID_SPEC" });
|
|
304
541
|
}
|
|
542
|
+
wb.sheets.forEach((s, i) => {
|
|
543
|
+
if (!s || typeof s !== "object") {
|
|
544
|
+
throw new ExcelIoError(`Workbook ${wbIndex}: sheet entries must be objects`, { code: "INVALID_SPEC" });
|
|
545
|
+
}
|
|
546
|
+
const name = s.sheet || `Sheet ${i + 1}`;
|
|
547
|
+
const kind = rowSourceKind(s.data);
|
|
548
|
+
if (kind === "invalid") {
|
|
549
|
+
throw new ExcelIoError(`Sheet "${name}": data must be an array or an iterable of row objects`, { code: "INVALID_SPEC" });
|
|
550
|
+
}
|
|
551
|
+
if (kind === "sync" || kind === "async") {
|
|
552
|
+
if (owners.has(s.data)) {
|
|
553
|
+
throw new ExcelIoError(
|
|
554
|
+
`Sheet "${name}": its data source is already used by sheet "${owners.get(s.data)}". Pass a separate iterable (or an array) per sheet.`,
|
|
555
|
+
{ code: "INVALID_SPEC" }
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
owners.set(s.data, name);
|
|
559
|
+
}
|
|
560
|
+
if (s.totalRows !== void 0 && !(Number.isInteger(s.totalRows) && s.totalRows >= 0)) {
|
|
561
|
+
throw new ExcelIoError(`Sheet "${name}": totalRows must be a non-negative integer`, { code: "INVALID_SPEC" });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function validateWriteOptions({ maxRowsPerSheet, sharedStrings, batchSize } = {}) {
|
|
567
|
+
if (maxRowsPerSheet !== void 0 && !(Number.isInteger(maxRowsPerSheet) && maxRowsPerSheet >= 1 && maxRowsPerSheet <= EXCEL_MAX_DATA_ROWS)) {
|
|
568
|
+
throw new ExcelIoError(`maxRowsPerSheet must be an integer between 1 and ${EXCEL_MAX_DATA_ROWS}`, { code: "INVALID_SPEC" });
|
|
569
|
+
}
|
|
570
|
+
if (sharedStrings !== void 0 && sharedStrings !== "auto" && typeof sharedStrings !== "boolean") {
|
|
571
|
+
throw new ExcelIoError(`sharedStrings must be 'auto', true or false`, { code: "INVALID_SPEC" });
|
|
572
|
+
}
|
|
573
|
+
if (batchSize !== void 0 && !(Number.isInteger(batchSize) && batchSize >= 1)) {
|
|
574
|
+
throw new ExcelIoError("batchSize must be a positive integer", { code: "INVALID_SPEC" });
|
|
305
575
|
}
|
|
306
576
|
}
|
|
307
577
|
|
|
@@ -455,6 +725,10 @@ function decideMode(op, payload, options) {
|
|
|
455
725
|
if (op === "read" && payload && typeof payload.pipe === "function") {
|
|
456
726
|
return { mode: "inline", reason: "stream input" };
|
|
457
727
|
}
|
|
728
|
+
if (op === "write" && specHasIterable(payload)) {
|
|
729
|
+
if (useWorker === true) options.logger?.warn?.("excelio: iterable sheet data is written inline; useWorker ignored");
|
|
730
|
+
return { mode: "inline", reason: "iterable sheet data" };
|
|
731
|
+
}
|
|
458
732
|
if (hasHook(options)) return { mode: "inline", reason: "hooks present" };
|
|
459
733
|
if (useWorker === false) return { mode: "inline", reason: "disabled" };
|
|
460
734
|
if (useWorker === true) return { mode: "worker", reason: "forced" };
|
|
@@ -482,7 +756,7 @@ function estimateRows(spec) {
|
|
|
482
756
|
let n = 0;
|
|
483
757
|
for (const wb of spec) {
|
|
484
758
|
for (const sheet of wb?.sheets || []) {
|
|
485
|
-
n += sheet.data
|
|
759
|
+
n += Array.isArray(sheet?.data) ? sheet.data.length : 0;
|
|
486
760
|
}
|
|
487
761
|
}
|
|
488
762
|
return n;
|
|
@@ -558,13 +832,16 @@ var excelio = {
|
|
|
558
832
|
* is provided and returns the target.
|
|
559
833
|
*/
|
|
560
834
|
async write(spec, options = {}) {
|
|
835
|
+
preflightWrite(spec, options);
|
|
561
836
|
return maybeUseWorker("write", spec, options, () => writeSpec(spec, options));
|
|
562
837
|
},
|
|
563
838
|
/**
|
|
564
839
|
* Returns a Readable stream for the first workbook. Synchronous return;
|
|
565
|
-
* errors during the underlying write surface
|
|
840
|
+
* spec errors throw immediately, errors during the underlying write surface
|
|
841
|
+
* via the stream's 'error' event.
|
|
566
842
|
*/
|
|
567
843
|
writeStream(spec, options = {}) {
|
|
844
|
+
preflightWrite(spec, options);
|
|
568
845
|
return writeSpecStream(spec, options);
|
|
569
846
|
},
|
|
570
847
|
/**
|