@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.
@@ -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
- validateSpec(spec);
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
- validateSpec(spec);
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 sink = resolveSink(target);
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
- let totalRows = sheets.reduce((n, s) => n + (s.data?.length || 0), 0);
189
- let rowsProcessed = 0;
190
- for (let sIndex = 0; sIndex < sheets.length; sIndex++) {
191
- if (signal?.aborted) throw new ExcelIoError("Aborted", { code: "ABORTED" });
192
- const sheetSpec = sheets[sIndex];
193
- const sheetName = sanitiseSheetName(sheetSpec.sheet || `Sheet ${sIndex + 1}`, usedNames, logger);
194
- const wsOptions = {};
195
- if (sheetSpec.styling?.freezeHeader) {
196
- wsOptions.views = [{ state: "frozen", ySplit: 1 }];
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 ws = wb.addWorksheet(sheetName, wsOptions);
199
- const cols = resolveColumns(sheetSpec.columns, sheetSpec.data);
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
- const data = sheetSpec.data || [];
210
- for (let i = 0; i < data.length; i++) {
211
- if (signal?.aborted) throw new ExcelIoError("Aborted", { code: "ABORTED" });
212
- let row = data[i];
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
- row = beforeWrite(row, {
326
+ out = beforeWrite(row, {
215
327
  workbookIndex: wbIndex,
216
328
  workbookName: workbookSpec.workbook,
217
329
  sheetIndex: sIndex,
218
330
  sheetName,
219
- rowNumber: i + 2
331
+ sheetPart: part,
332
+ rowNumber: rowsInPart + 2
220
333
  // header is row 1
221
334
  }) ?? row;
222
335
  }
223
- const rowValues = cols.length ? cols.reduce((acc, c) => {
224
- acc[c.key] = valueForWrite(row[c.key], c.type);
336
+ const values = cols.length ? cols.reduce((acc, c) => {
337
+ acc[c.key] = valueForWrite(out[c.key], c.type);
225
338
  return acc;
226
- }, {}) : row;
227
- ws.addRow(rowValues).commit();
228
- rowsProcessed++;
229
- if (onProgress && rowsProcessed % batchSize === 0) {
230
- onProgress({ phase: "write", workbookIndex: wbIndex, sheetIndex: sIndex, rowsProcessed, totalRows });
231
- }
339
+ }, {}) : out;
340
+ ws.addRow(values).commit();
341
+ rowsInPart++;
342
+ onRow();
343
+ },
344
+ finish() {
345
+ ws?.commit();
232
346
  }
233
- ws.commit();
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
- await wb.commit();
236
- if (sink.cleanup) await sink.cleanup();
237
- if (onProgress) {
238
- onProgress({ phase: "write", workbookIndex: wbIndex, sheetIndex: sheets.length - 1, rowsProcessed, totalRows });
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
- function resolveSink(target) {
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 closed = new Promise((resolve, reject) => {
245
- fileStream.once("finish", resolve);
246
- fileStream.once("error", reject);
247
- });
248
- return { writable: fileStream, cleanup: () => closed };
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 { writable: target, cleanup: null };
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
- for (const wb of spec) {
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?.length || 0;
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 via the stream's 'error' event.
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
  /**