@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/esm/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/writer.js
|
|
2
|
-
import {
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
3
|
import { createWriteStream } from "node:fs";
|
|
4
|
+
import { rm } from "node:fs/promises";
|
|
5
|
+
import { setImmediate as tick, setTimeout as sleep } from "node:timers/promises";
|
|
4
6
|
import ExcelJS from "exceljs";
|
|
5
7
|
|
|
6
8
|
// src/errors.js
|
|
@@ -101,11 +103,44 @@ function formatDate(d, fmt) {
|
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
// src/rowSource.js
|
|
107
|
+
function rowSourceKind(data) {
|
|
108
|
+
if (data === void 0 || data === null) return "empty";
|
|
109
|
+
if (Array.isArray(data)) return "array";
|
|
110
|
+
if (typeof data !== "object" && typeof data !== "function") return "invalid";
|
|
111
|
+
if (typeof data[Symbol.asyncIterator] === "function") return "async";
|
|
112
|
+
if (typeof data[Symbol.iterator] === "function") return "sync";
|
|
113
|
+
return "invalid";
|
|
114
|
+
}
|
|
115
|
+
var isIterableSource = (data) => {
|
|
116
|
+
const kind = rowSourceKind(data);
|
|
117
|
+
return kind === "sync" || kind === "async";
|
|
118
|
+
};
|
|
119
|
+
var specHasIterable = (spec) => Array.isArray(spec) && spec.some((wb) => (Array.isArray(wb?.sheets) ? wb.sheets : []).some((s) => isIterableSource(s?.data)));
|
|
120
|
+
async function closeIterator(iterator) {
|
|
121
|
+
if (typeof iterator?.return !== "function") return;
|
|
122
|
+
try {
|
|
123
|
+
await iterator.return();
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function sourceFailure(err, details, signal) {
|
|
128
|
+
if (signal?.aborted) return new ExcelIoError("Aborted", { code: "ABORTED", cause: err, details });
|
|
129
|
+
return new ExcelIoError(`Row source failed for sheet "${details.sheetName}": ${err?.message ?? err}`, {
|
|
130
|
+
code: "SOURCE_ERROR",
|
|
131
|
+
cause: err,
|
|
132
|
+
details
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
// src/writer.js
|
|
105
137
|
var DEFAULT_BATCH = 5e3;
|
|
106
138
|
var SHEET_NAME_MAX = 31;
|
|
139
|
+
var EXCEL_MAX_DATA_ROWS = 1048575;
|
|
140
|
+
var OUTPUT_HIGH_WATER_MARK = 8 * 1024 * 1024;
|
|
141
|
+
var BACKLOG_POLL_MS = 5;
|
|
107
142
|
async function writeSpec(spec, options = {}) {
|
|
108
|
-
|
|
143
|
+
preflightWrite(spec, options);
|
|
109
144
|
const { to } = options;
|
|
110
145
|
if (to !== void 0 && spec.length > 1) {
|
|
111
146
|
throw new ExcelIoError(
|
|
@@ -125,41 +160,106 @@ async function writeSpec(spec, options = {}) {
|
|
|
125
160
|
return buffers.length === 1 ? buffers[0] : buffers;
|
|
126
161
|
}
|
|
127
162
|
function writeSpecStream(spec, options = {}) {
|
|
128
|
-
|
|
163
|
+
preflightWrite(spec, options);
|
|
129
164
|
const passthrough = new PassThrough();
|
|
130
165
|
writeOneWorkbook(spec[0], 0, passthrough, options).catch((err) => passthrough.destroy(err));
|
|
131
166
|
return passthrough;
|
|
132
167
|
}
|
|
168
|
+
function preflightWrite(spec, options = {}) {
|
|
169
|
+
validateSpec(spec);
|
|
170
|
+
validateWriteOptions(options);
|
|
171
|
+
}
|
|
133
172
|
async function writeOneWorkbookToBuffer(workbookSpec, wbIndex, options) {
|
|
134
173
|
const chunks = [];
|
|
135
174
|
const collector = new PassThrough();
|
|
136
175
|
collector.on("data", (chunk) => chunks.push(chunk));
|
|
137
|
-
await writeOneWorkbook(workbookSpec, wbIndex, collector, options);
|
|
176
|
+
await writeOneWorkbook(workbookSpec, wbIndex, collector, options, { owned: true });
|
|
138
177
|
return Buffer.concat(chunks);
|
|
139
178
|
}
|
|
140
|
-
async function writeOneWorkbook(workbookSpec, wbIndex, target, options) {
|
|
179
|
+
async function writeOneWorkbook(workbookSpec, wbIndex, target, options, sinkOptions = {}) {
|
|
141
180
|
const { signal, onProgress, beforeWrite, batchSize = DEFAULT_BATCH, logger } = options;
|
|
142
|
-
const
|
|
143
|
-
const wb = new ExcelJS.stream.xlsx.WorkbookWriter({
|
|
144
|
-
stream: sink.writable,
|
|
145
|
-
useStyles: true,
|
|
146
|
-
useSharedStrings: true
|
|
147
|
-
});
|
|
148
|
-
applyWorkbookProperties(wb, workbookSpec.properties);
|
|
149
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
181
|
+
const maxRowsPerSheet = options.maxRowsPerSheet ?? EXCEL_MAX_DATA_ROWS;
|
|
150
182
|
const sheets = workbookSpec.sheets || [];
|
|
151
|
-
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
const sink = resolveSink(target, sinkOptions);
|
|
184
|
+
let wb = null;
|
|
185
|
+
try {
|
|
186
|
+
wb = new ExcelJS.stream.xlsx.WorkbookWriter({
|
|
187
|
+
stream: sink.writable,
|
|
188
|
+
useStyles: true,
|
|
189
|
+
useSharedStrings: resolveSharedStrings(options.sharedStrings, sheets)
|
|
190
|
+
});
|
|
191
|
+
applyWorkbookProperties(wb, workbookSpec.properties);
|
|
192
|
+
const used = /* @__PURE__ */ new Set();
|
|
193
|
+
const firstNames = sheets.map((s, i) => sanitiseSheetName(s.sheet || `Sheet ${i + 1}`, used, logger));
|
|
194
|
+
const totalRows = computeTotalRows(sheets);
|
|
195
|
+
const state = { rowsProcessed: 0, last: { sheetIndex: 0, sheetName: firstNames[0], sheetPart: 1 } };
|
|
196
|
+
const emit = () => onProgress?.({
|
|
197
|
+
phase: "write",
|
|
198
|
+
workbookIndex: wbIndex,
|
|
199
|
+
...state.last,
|
|
200
|
+
rowsProcessed: state.rowsProcessed,
|
|
201
|
+
totalRows
|
|
202
|
+
});
|
|
203
|
+
for (let sIndex = 0; sIndex < sheets.length; sIndex++) {
|
|
204
|
+
throwIfAborted(signal);
|
|
205
|
+
const sheetSpec = sheets[sIndex];
|
|
206
|
+
const writer = createSheetWriter({
|
|
207
|
+
wb,
|
|
208
|
+
workbookSpec,
|
|
209
|
+
wbIndex,
|
|
210
|
+
sIndex,
|
|
211
|
+
sheetSpec,
|
|
212
|
+
firstName: firstNames[sIndex],
|
|
213
|
+
used,
|
|
214
|
+
beforeWrite,
|
|
215
|
+
maxRowsPerSheet,
|
|
216
|
+
onOpen: (sheetName, sheetPart) => {
|
|
217
|
+
state.last = { sheetIndex: sIndex, sheetName, sheetPart };
|
|
218
|
+
},
|
|
219
|
+
onRow: () => {
|
|
220
|
+
state.rowsProcessed++;
|
|
221
|
+
if (onProgress && state.rowsProcessed % batchSize === 0) emit();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
const kind = rowSourceKind(sheetSpec.data);
|
|
225
|
+
if (kind === "sync" || kind === "async") {
|
|
226
|
+
await writeIterableSheet({ writer, wb, sink, sheetSpec, kind, signal, batchSize, wbIndex, sIndex });
|
|
227
|
+
} else {
|
|
228
|
+
const data = sheetSpec.data || [];
|
|
229
|
+
writer.start(resolveColumns(sheetSpec.columns, data));
|
|
230
|
+
for (let i = 0; i < data.length; i++) {
|
|
231
|
+
throwIfAborted(signal);
|
|
232
|
+
writer.write(data[i]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
writer.finish();
|
|
160
236
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
237
|
+
const committing = wb.commit();
|
|
238
|
+
committing.catch(() => {
|
|
239
|
+
});
|
|
240
|
+
await Promise.race([committing, sink.failed]);
|
|
241
|
+
if (sink.cleanup) await Promise.race([sink.cleanup(), sink.failed]);
|
|
242
|
+
sink.release();
|
|
243
|
+
emit();
|
|
244
|
+
} catch (err) {
|
|
245
|
+
await abortWorkbook(wb, sink, err);
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function createSheetWriter({ wb, workbookSpec, wbIndex, sIndex, sheetSpec, firstName, used, beforeWrite, maxRowsPerSheet, onOpen, onRow }) {
|
|
250
|
+
const baseName = sheetSpec.sheet || `Sheet ${sIndex + 1}`;
|
|
251
|
+
let cols = [];
|
|
252
|
+
let ws = null;
|
|
253
|
+
let sheetName = null;
|
|
254
|
+
let part = 0;
|
|
255
|
+
let rowsInPart = 0;
|
|
256
|
+
const open = () => {
|
|
257
|
+
part += 1;
|
|
258
|
+
rowsInPart = 0;
|
|
259
|
+
sheetName = part === 1 ? firstName : sanitiseSheetName(baseName, used, null);
|
|
260
|
+
const wsOptions = {};
|
|
261
|
+
if (sheetSpec.styling?.freezeHeader) wsOptions.views = [{ state: "frozen", ySplit: 1 }];
|
|
262
|
+
ws = wb.addWorksheet(sheetName, wsOptions);
|
|
163
263
|
if (cols.length) {
|
|
164
264
|
ws.columns = cols.map((c) => ({
|
|
165
265
|
key: c.key,
|
|
@@ -169,52 +269,188 @@ async function writeOneWorkbook(workbookSpec, wbIndex, target, options) {
|
|
|
169
269
|
}));
|
|
170
270
|
}
|
|
171
271
|
applyStylingPostColumns(ws, sheetSpec.styling, cols.length);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
272
|
+
onOpen(sheetName, part);
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
get sheetName() {
|
|
276
|
+
return sheetName ?? firstName;
|
|
277
|
+
},
|
|
278
|
+
start(resolvedColumns) {
|
|
279
|
+
cols = resolvedColumns;
|
|
280
|
+
open();
|
|
281
|
+
},
|
|
282
|
+
write(row) {
|
|
283
|
+
if (rowsInPart >= maxRowsPerSheet) {
|
|
284
|
+
ws.commit();
|
|
285
|
+
open();
|
|
286
|
+
}
|
|
287
|
+
let out = row;
|
|
176
288
|
if (beforeWrite) {
|
|
177
|
-
|
|
289
|
+
out = beforeWrite(row, {
|
|
178
290
|
workbookIndex: wbIndex,
|
|
179
291
|
workbookName: workbookSpec.workbook,
|
|
180
292
|
sheetIndex: sIndex,
|
|
181
293
|
sheetName,
|
|
182
|
-
|
|
294
|
+
sheetPart: part,
|
|
295
|
+
rowNumber: rowsInPart + 2
|
|
183
296
|
// header is row 1
|
|
184
297
|
}) ?? row;
|
|
185
298
|
}
|
|
186
|
-
const
|
|
187
|
-
acc[c.key] = valueForWrite(
|
|
299
|
+
const values = cols.length ? cols.reduce((acc, c) => {
|
|
300
|
+
acc[c.key] = valueForWrite(out[c.key], c.type);
|
|
188
301
|
return acc;
|
|
189
|
-
}, {}) :
|
|
190
|
-
ws.addRow(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
302
|
+
}, {}) : out;
|
|
303
|
+
ws.addRow(values).commit();
|
|
304
|
+
rowsInPart++;
|
|
305
|
+
onRow();
|
|
306
|
+
},
|
|
307
|
+
finish() {
|
|
308
|
+
ws?.commit();
|
|
195
309
|
}
|
|
196
|
-
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function writeIterableSheet({ writer, wb, sink, sheetSpec, kind, signal, batchSize, wbIndex, sIndex }) {
|
|
313
|
+
let rowsRead = 0;
|
|
314
|
+
const details = () => ({ workbookIndex: wbIndex, sheetIndex: sIndex, sheetName: writer.sheetName, rowsRead });
|
|
315
|
+
let iterator;
|
|
316
|
+
try {
|
|
317
|
+
iterator = kind === "async" ? sheetSpec.data[Symbol.asyncIterator]() : sheetSpec.data[Symbol.iterator]();
|
|
318
|
+
} catch (err) {
|
|
319
|
+
throw sourceFailure(err, details(), signal);
|
|
197
320
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
321
|
+
const next = async () => {
|
|
322
|
+
let step;
|
|
323
|
+
try {
|
|
324
|
+
step = await iterator.next();
|
|
325
|
+
} catch (err) {
|
|
326
|
+
throw sourceFailure(err, details(), signal);
|
|
327
|
+
}
|
|
328
|
+
if (step === null || typeof step !== "object") {
|
|
329
|
+
throw sourceFailure(new TypeError("Iterator result is not an object"), details(), signal);
|
|
330
|
+
}
|
|
331
|
+
return step;
|
|
332
|
+
};
|
|
333
|
+
let finished = false;
|
|
334
|
+
try {
|
|
335
|
+
let step = await next();
|
|
336
|
+
writer.start(resolveColumns(sheetSpec.columns, step.done ? [] : [step.value]));
|
|
337
|
+
while (!step.done) {
|
|
338
|
+
throwIfAborted(signal);
|
|
339
|
+
writer.write(step.value);
|
|
340
|
+
rowsRead++;
|
|
341
|
+
if (rowsRead % batchSize === 0) await waitForCapacity({ wb, sink, signal });
|
|
342
|
+
step = await next();
|
|
343
|
+
}
|
|
344
|
+
finished = true;
|
|
345
|
+
} finally {
|
|
346
|
+
if (!finished) await closeIterator(iterator);
|
|
202
347
|
}
|
|
203
348
|
}
|
|
204
|
-
|
|
349
|
+
var len = (n) => Number.isFinite(n) ? n : 0;
|
|
350
|
+
var streamLen = (s) => len(s?.writableLength ?? s?._writableState?.length) + len(s?.readableLength ?? s?._readableState?.length);
|
|
351
|
+
function backlogBytes(wb, sink) {
|
|
352
|
+
let total = streamLen(wb?.zip) + streamLen(wb?.zip?._module) + streamLen(sink?.writable);
|
|
353
|
+
for (const ws of wb?._worksheets ?? []) {
|
|
354
|
+
const sb = ws?._stream;
|
|
355
|
+
if (!sb) continue;
|
|
356
|
+
for (const buf of sb.buffers ?? []) total += len(buf?.length);
|
|
357
|
+
for (const p of sb.pipes ?? []) total += streamLen(p);
|
|
358
|
+
}
|
|
359
|
+
return total;
|
|
360
|
+
}
|
|
361
|
+
var backlogStats = { peak: 0 };
|
|
362
|
+
async function waitForCapacity({ wb, sink, signal }) {
|
|
363
|
+
const check = () => {
|
|
364
|
+
throwIfAborted(signal);
|
|
365
|
+
if (sink.failure) throw sink.failure;
|
|
366
|
+
};
|
|
367
|
+
for (; ; ) {
|
|
368
|
+
check();
|
|
369
|
+
const backlog = backlogBytes(wb, sink);
|
|
370
|
+
if (backlog > backlogStats.peak) backlogStats.peak = backlog;
|
|
371
|
+
if (backlog <= OUTPUT_HIGH_WATER_MARK) break;
|
|
372
|
+
await sleep(BACKLOG_POLL_MS);
|
|
373
|
+
}
|
|
374
|
+
await tick();
|
|
375
|
+
check();
|
|
376
|
+
}
|
|
377
|
+
function resolveSink(target, { owned = false } = {}) {
|
|
378
|
+
const track = (writable, isOwned) => {
|
|
379
|
+
const sink = { writable, owned: isOwned, failure: null };
|
|
380
|
+
let onError;
|
|
381
|
+
sink.failed = new Promise((_, reject) => {
|
|
382
|
+
onError = (err) => {
|
|
383
|
+
if (sink.failure) return;
|
|
384
|
+
sink.failure = err;
|
|
385
|
+
reject(err);
|
|
386
|
+
};
|
|
387
|
+
writable.on("error", onError);
|
|
388
|
+
});
|
|
389
|
+
sink.failed.catch(() => {
|
|
390
|
+
});
|
|
391
|
+
sink.release = () => {
|
|
392
|
+
if (!isOwned) writable.removeListener("error", onError);
|
|
393
|
+
};
|
|
394
|
+
return sink;
|
|
395
|
+
};
|
|
205
396
|
if (typeof target === "string") {
|
|
206
397
|
const fileStream = createWriteStream(target);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
398
|
+
const sink = track(fileStream, true);
|
|
399
|
+
const finished = new Promise((resolve) => fileStream.once("finish", resolve));
|
|
400
|
+
const closed = new Promise((resolve) => fileStream.once("close", resolve));
|
|
401
|
+
sink.cleanup = () => finished;
|
|
402
|
+
sink.remove = async () => {
|
|
403
|
+
fileStream.destroy();
|
|
404
|
+
await closed;
|
|
405
|
+
await rm(target, { force: true });
|
|
406
|
+
};
|
|
407
|
+
return sink;
|
|
212
408
|
}
|
|
213
409
|
if (target && typeof target.write === "function") {
|
|
214
|
-
return
|
|
410
|
+
return track(target, owned);
|
|
215
411
|
}
|
|
216
412
|
throw new ExcelIoError(`Invalid write target \u2014 expected file path or writable stream`, { code: "INVALID_SPEC" });
|
|
217
413
|
}
|
|
414
|
+
async function abortWorkbook(wb, sink, err) {
|
|
415
|
+
const quiet = (s) => s?.on?.("error", () => {
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
if (wb) {
|
|
419
|
+
wb.zip?.unpipe?.(sink.writable);
|
|
420
|
+
quiet(wb.zip);
|
|
421
|
+
quiet(wb.zip?._module);
|
|
422
|
+
for (const ws of wb._worksheets ?? []) (ws?._stream?.pipes ?? []).forEach(quiet);
|
|
423
|
+
wb.zip?.abort?.();
|
|
424
|
+
}
|
|
425
|
+
if (sink.remove) await sink.remove();
|
|
426
|
+
else if (sink.owned) sink.writable.destroy?.();
|
|
427
|
+
sink.release();
|
|
428
|
+
} catch (cleanupError) {
|
|
429
|
+
try {
|
|
430
|
+
if (typeof err === "object" && err !== null || typeof err === "function") err.cleanupError = cleanupError;
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function resolveSharedStrings(option = "auto", sheets) {
|
|
436
|
+
if (typeof option === "boolean") return option;
|
|
437
|
+
return !sheets.some((s) => isIterableSource(s?.data));
|
|
438
|
+
}
|
|
439
|
+
function computeTotalRows(sheets) {
|
|
440
|
+
let total = 0;
|
|
441
|
+
for (const s of sheets) {
|
|
442
|
+
const kind = rowSourceKind(s.data);
|
|
443
|
+
if (kind === "array") total += s.data.length;
|
|
444
|
+
else if (kind === "sync" || kind === "async") {
|
|
445
|
+
if (s.totalRows === void 0) return void 0;
|
|
446
|
+
total += s.totalRows;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return total;
|
|
450
|
+
}
|
|
451
|
+
function throwIfAborted(signal) {
|
|
452
|
+
if (signal?.aborted) throw new ExcelIoError("Aborted", { code: "ABORTED" });
|
|
453
|
+
}
|
|
218
454
|
function applyWorkbookProperties(wb, props) {
|
|
219
455
|
if (!props) return;
|
|
220
456
|
if (props.author) wb.creator = props.author;
|
|
@@ -258,13 +494,47 @@ function validateSpec(spec) {
|
|
|
258
494
|
if (!Array.isArray(spec) || spec.length === 0) {
|
|
259
495
|
throw new ExcelIoError("Spec must be a non-empty array of workbooks", { code: "INVALID_SPEC" });
|
|
260
496
|
}
|
|
261
|
-
|
|
497
|
+
const owners = /* @__PURE__ */ new Map();
|
|
498
|
+
spec.forEach((wb, wbIndex) => {
|
|
262
499
|
if (!wb || typeof wb !== "object") {
|
|
263
500
|
throw new ExcelIoError("Workbook entries must be objects", { code: "INVALID_SPEC" });
|
|
264
501
|
}
|
|
265
502
|
if (!Array.isArray(wb.sheets) || wb.sheets.length === 0) {
|
|
266
503
|
throw new ExcelIoError("Workbook must have at least one sheet", { code: "INVALID_SPEC" });
|
|
267
504
|
}
|
|
505
|
+
wb.sheets.forEach((s, i) => {
|
|
506
|
+
if (!s || typeof s !== "object") {
|
|
507
|
+
throw new ExcelIoError(`Workbook ${wbIndex}: sheet entries must be objects`, { code: "INVALID_SPEC" });
|
|
508
|
+
}
|
|
509
|
+
const name = s.sheet || `Sheet ${i + 1}`;
|
|
510
|
+
const kind = rowSourceKind(s.data);
|
|
511
|
+
if (kind === "invalid") {
|
|
512
|
+
throw new ExcelIoError(`Sheet "${name}": data must be an array or an iterable of row objects`, { code: "INVALID_SPEC" });
|
|
513
|
+
}
|
|
514
|
+
if (kind === "sync" || kind === "async") {
|
|
515
|
+
if (owners.has(s.data)) {
|
|
516
|
+
throw new ExcelIoError(
|
|
517
|
+
`Sheet "${name}": its data source is already used by sheet "${owners.get(s.data)}". Pass a separate iterable (or an array) per sheet.`,
|
|
518
|
+
{ code: "INVALID_SPEC" }
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
owners.set(s.data, name);
|
|
522
|
+
}
|
|
523
|
+
if (s.totalRows !== void 0 && !(Number.isInteger(s.totalRows) && s.totalRows >= 0)) {
|
|
524
|
+
throw new ExcelIoError(`Sheet "${name}": totalRows must be a non-negative integer`, { code: "INVALID_SPEC" });
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function validateWriteOptions({ maxRowsPerSheet, sharedStrings, batchSize } = {}) {
|
|
530
|
+
if (maxRowsPerSheet !== void 0 && !(Number.isInteger(maxRowsPerSheet) && maxRowsPerSheet >= 1 && maxRowsPerSheet <= EXCEL_MAX_DATA_ROWS)) {
|
|
531
|
+
throw new ExcelIoError(`maxRowsPerSheet must be an integer between 1 and ${EXCEL_MAX_DATA_ROWS}`, { code: "INVALID_SPEC" });
|
|
532
|
+
}
|
|
533
|
+
if (sharedStrings !== void 0 && sharedStrings !== "auto" && typeof sharedStrings !== "boolean") {
|
|
534
|
+
throw new ExcelIoError(`sharedStrings must be 'auto', true or false`, { code: "INVALID_SPEC" });
|
|
535
|
+
}
|
|
536
|
+
if (batchSize !== void 0 && !(Number.isInteger(batchSize) && batchSize >= 1)) {
|
|
537
|
+
throw new ExcelIoError("batchSize must be a positive integer", { code: "INVALID_SPEC" });
|
|
268
538
|
}
|
|
269
539
|
}
|
|
270
540
|
|
|
@@ -418,6 +688,10 @@ function decideMode(op, payload, options) {
|
|
|
418
688
|
if (op === "read" && payload && typeof payload.pipe === "function") {
|
|
419
689
|
return { mode: "inline", reason: "stream input" };
|
|
420
690
|
}
|
|
691
|
+
if (op === "write" && specHasIterable(payload)) {
|
|
692
|
+
if (useWorker === true) options.logger?.warn?.("excelio: iterable sheet data is written inline; useWorker ignored");
|
|
693
|
+
return { mode: "inline", reason: "iterable sheet data" };
|
|
694
|
+
}
|
|
421
695
|
if (hasHook(options)) return { mode: "inline", reason: "hooks present" };
|
|
422
696
|
if (useWorker === false) return { mode: "inline", reason: "disabled" };
|
|
423
697
|
if (useWorker === true) return { mode: "worker", reason: "forced" };
|
|
@@ -445,7 +719,7 @@ function estimateRows(spec) {
|
|
|
445
719
|
let n = 0;
|
|
446
720
|
for (const wb of spec) {
|
|
447
721
|
for (const sheet of wb?.sheets || []) {
|
|
448
|
-
n += sheet.data
|
|
722
|
+
n += Array.isArray(sheet?.data) ? sheet.data.length : 0;
|
|
449
723
|
}
|
|
450
724
|
}
|
|
451
725
|
return n;
|
|
@@ -521,13 +795,16 @@ var excelio = {
|
|
|
521
795
|
* is provided and returns the target.
|
|
522
796
|
*/
|
|
523
797
|
async write(spec, options = {}) {
|
|
798
|
+
preflightWrite(spec, options);
|
|
524
799
|
return maybeUseWorker("write", spec, options, () => writeSpec(spec, options));
|
|
525
800
|
},
|
|
526
801
|
/**
|
|
527
802
|
* Returns a Readable stream for the first workbook. Synchronous return;
|
|
528
|
-
* errors during the underlying write surface
|
|
803
|
+
* spec errors throw immediately, errors during the underlying write surface
|
|
804
|
+
* via the stream's 'error' event.
|
|
529
805
|
*/
|
|
530
806
|
writeStream(spec, options = {}) {
|
|
807
|
+
preflightWrite(spec, options);
|
|
531
808
|
return writeSpecStream(spec, options);
|
|
532
809
|
},
|
|
533
810
|
/**
|