@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 CHANGED
@@ -1,6 +1,37 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 (Unreleased)
3
+ ## 1.1.0 (2026-11-06)
4
+
5
+ ### Added
6
+
7
+ - Sheet `data` accepts any iterable or async iterable (generators, database cursors,
8
+ object-mode streams) as well as arrays; iterable sheets are written inline with
9
+ output backlog bounded at about 8 MiB, so memory stays flat for large exports.
10
+ - `maxRowsPerSheet` write option: sheets continue on `"<name> (2)"`, `"<name> (3)"`, ….
11
+ - `sharedStrings: 'auto' | true | false` write option (`'auto'`: off when a workbook
12
+ has an iterable sheet).
13
+ - `totalRows` sheet hint for progress, and `sheetPart` in `beforeWrite` context and
14
+ write progress events (which now also carry `sheetName`).
15
+ - `SOURCE_ERROR` error code for failing row sources.
16
+ - `npm run test:memory` release gate.
17
+
18
+ ### Fixed
19
+
20
+ - Non-array `data` (for example a generator) silently wrote an empty sheet.
21
+ - Sheets over 1,048,575 rows produced files Excel cannot open; they now roll over.
22
+ - Failed writes to a file path left a partial file and an open handle; the file is
23
+ now removed. Failed writes to a caller's stream no longer end that stream.
24
+ - Early file errors (such as a missing directory) could hang the write.
25
+ - `batchSize: 0` silently disabled progress events.
26
+
27
+ ### Changed
28
+
29
+ - Invalid `batchSize` (not a positive integer) now throws `INVALID_SPEC`.
30
+ - Specs and write options are validated before worker routing.
31
+ - `SheetSpec.data` is optional in the type declarations. TypeScript code with an
32
+ exhaustive switch over `ExcelIoErrorCode` must handle `SOURCE_ERROR`.
33
+
34
+ ## 1.0.0 (2026-10-08)
4
35
 
5
36
  - Package as `@dipanshuhandoo/excelio`, supporting Node.js 22 and 24 LTS.
6
37
  - Add readable/minified ESM and CommonJS builds, worker bundles, and declarations.
package/README.md CHANGED
@@ -1,9 +1,80 @@
1
1
  # ExcelIO
2
2
 
3
3
  `@dipanshuhandoo/excelio` converts JavaScript row objects to XLSX workbooks and
4
- reads XLSX files into structured data. It supports streaming writes, multiple
5
- sheets/workbooks, row transforms and validation, and configurable worker-thread
6
- offloading. It is a Node.js library, not a browser library.
4
+ reads XLSX files into structured data. Rows can come from arrays or be streamed
5
+ from generators, database cursors, and paged APIs with bounded memory. It is a
6
+ Node.js library, not a browser library. Its only runtime dependency is ExcelJS.
7
+
8
+ ## Features
9
+
10
+ **Writing**
11
+
12
+ - One call writes one or many workbooks, each with multiple sheets, to a Buffer,
13
+ a file path, a writable stream, or a readable stream (`writeStream`).
14
+ - Sheet `data` can be an array, a sync iterable, or an async iterable (async
15
+ generator, MongoDB/Mongoose cursor, object-mode stream, paged API).
16
+ - Large exports: streamed rows with a file or stream target keep memory flat
17
+ (about 50 MB growth for 1,000,000 rows), with output backpressure built in.
18
+ - Automatic sheet rollover at Excel's row limit, or earlier with
19
+ `maxRowsPerSheet`: `Links`, `Links (2)`, `Links (3)` … keep columns, header,
20
+ and styling.
21
+ - Columns: explicit keys, headers, widths, types (`string`, `number`, `boolean`,
22
+ `date`), and Excel number formats, or keys inferred from the first row.
23
+ - Write-side coercion of numeric/boolean strings and dates; multiline and
24
+ multilingual text.
25
+ - Styling: bold header, frozen header row, auto-filter; workbook properties
26
+ (author, company, title, created, modified).
27
+ - Safe sheet names: forbidden characters replaced, 31-character limit,
28
+ duplicate suffixes.
29
+ - Shared-strings control (`sharedStrings`) to trade file size for memory.
30
+
31
+ **Reading**
32
+
33
+ - Reads a Buffer, file path, or readable stream into
34
+ `{ ok, data, errors, stats }`, covering all sheets with headers as keys.
35
+ - Options for header row, blank handling, trimming, and date output (ISO string,
36
+ Date, or epoch).
37
+ - Formula cells return cached values; rich text and hyperlinks return displayed
38
+ text.
39
+
40
+ **Transforms and validation**
41
+
42
+ - `beforeWrite` and `afterRead` row hooks with row context (workbook, sheet,
43
+ part, Excel row number).
44
+ - `validateRow` collects per-row errors (partial data accepted) or fails fast
45
+ with `onRowError: 'fail'`.
46
+
47
+ **Operations**
48
+
49
+ - Worker-thread offload for large plain jobs (`useWorker: 'auto' | true | false`,
50
+ configurable thresholds); hooks, streams, and iterables run inline
51
+ automatically.
52
+ - Progress callbacks (`onProgress`, `batchSize`) with sheet, part, and total row
53
+ information.
54
+ - Cancellation through `AbortSignal`; streamed sheets respond during the write.
55
+ - Failure cleanup: a failed file write removes the partial file, and a caller's
56
+ stream is not ended or destroyed; row-source errors are reported as
57
+ `SOURCE_ERROR` with row details.
58
+ - Typed `ExcelIoError` codes: `INVALID_SPEC`, `IO_ERROR`, `ABORTED`,
59
+ `WORKER_ERROR`, `ROW_VALIDATION`, `SOURCE_ERROR`.
60
+
61
+ **Packaging**
62
+
63
+ - ESM and CommonJS, readable and minified (`/min`) builds, worker bundles,
64
+ source maps, and TypeScript declarations.
65
+
66
+ | Use case | Example |
67
+ | --- | --- |
68
+ | In-memory round-trip | [01-buffer-roundtrip](https://github.com/DipanshuHandoo/excelio/blob/main/examples/01-buffer-roundtrip.mjs) |
69
+ | Styled multi-sheet report to a file | [02-sales-report](https://github.com/DipanshuHandoo/excelio/blob/main/examples/02-sales-report.mjs) |
70
+ | Validated import with row errors | [03-import-validation](https://github.com/DipanshuHandoo/excelio/blob/main/examples/03-import-validation.mjs) |
71
+ | Stream a workbook to a destination | [04-streaming](https://github.com/DipanshuHandoo/excelio/blob/main/examples/04-streaming.mjs) |
72
+ | Workers and concurrent exports | [05-workers-and-concurrency](https://github.com/DipanshuHandoo/excelio/blob/main/examples/05-workers-and-concurrency.mjs) |
73
+ | Progress and cancellation | [06-progress-and-cancellation](https://github.com/DipanshuHandoo/excelio/blob/main/examples/06-progress-and-cancellation.mjs) |
74
+ | CommonJS and minified builds | [07-commonjs-and-minified](https://github.com/DipanshuHandoo/excelio/blob/main/examples/07-commonjs-and-minified.cjs) |
75
+ | XLSX to JSON | [08-excel-to-json](https://github.com/DipanshuHandoo/excelio/blob/main/examples/08-excel-to-json.mjs) |
76
+ | Service export migration | [09-service-exports](https://github.com/DipanshuHandoo/excelio/blob/main/examples/09-service-exports.mjs) |
77
+ | Stream rows from a paged source with rollover | [10-streaming-rows](https://github.com/DipanshuHandoo/excelio/blob/main/examples/10-streaming-rows.mjs) |
7
78
 
8
79
  ## Installation
9
80
 
@@ -41,6 +112,21 @@ const result = await excelio.read(buffer);
41
112
  console.log(result.data[0].sheets[0].data);
42
113
  ```
43
114
 
115
+ Stream a large export straight from a source, without building an array:
116
+
117
+ ```js
118
+ async function* orders() {
119
+ for await (const doc of Order.find({}).lean().cursor({ batchSize: 1000 })) {
120
+ yield { customer: doc.customer, amount: doc.amount, date: doc.createdAt };
121
+ }
122
+ }
123
+
124
+ await excelio.write([{
125
+ workbook: 'orders',
126
+ sheets: [{ sheet: 'Orders', columns: spec[0].sheets[0].columns, data: orders() }]
127
+ }], { to: 'orders.xlsx', signal, onProgress: console.log });
128
+ ```
129
+
44
130
  CommonJS exposes the same named exports:
45
131
 
46
132
  ```js
@@ -61,8 +147,9 @@ workbook-processing time. Both builds include source maps and declarations.
61
147
 
62
148
  See the [example guide](https://github.com/DipanshuHandoo/excelio/blob/main/examples/README.md)
63
149
  for buffer round-trips, styled multi-sheet reports, validated imports, streaming,
64
- workers/concurrent exports, progress/cancellation, CommonJS/minified usage, and
65
- XLSX-to-JSON conversion.
150
+ workers/concurrent exports, progress/cancellation, CommonJS/minified usage,
151
+ XLSX-to-JSON conversion, service export migration, and streamed rows with sheet
152
+ rollover.
66
153
  Examples live in the repository and use public package exports.
67
154
 
68
155
  From a checkout, run all cases or select one:
@@ -79,10 +166,49 @@ Run the file examples directly after building to keep their XLSX/JSON output:
79
166
  node examples/02-sales-report.mjs
80
167
  node examples/04-streaming.mjs
81
168
  node examples/08-excel-to-json.mjs
169
+ node examples/10-streaming-rows.mjs
82
170
  ```
83
171
 
84
172
  These save to the ignored example-output/ directory by default.
85
173
 
174
+ ## Options Reference
175
+
176
+ Common to `write`, `writeStream`, and `read`:
177
+
178
+ | Option | Default | Purpose |
179
+ | --- | --- | --- |
180
+ | `onProgress` | none | `(event) => void`, called every `batchSize` rows; writes also emit a final event |
181
+ | `batchSize` | 5000 | Progress interval; on write it must be a positive integer and also sets the backpressure/yield interval for streamed sheets |
182
+ | `signal` | none | `AbortSignal`; rejects with `ABORTED` |
183
+ | `logger` | none | `{ info?, warn?, error? }` |
184
+ | `useWorker` | `'auto'` | `'auto'`, `true`, or `false` |
185
+ | `workerThreshold` | `{ rows: 50_000, bytes: 10 MiB }` | Auto-mode offload thresholds |
186
+
187
+ Write only:
188
+
189
+ | Option | Default | Purpose |
190
+ | --- | --- | --- |
191
+ | `to` | none (returns Buffer/Buffer[]) | File path or writable stream |
192
+ | `beforeWrite` | none | `(row, ctx) => row`, synchronous transform |
193
+ | `maxRowsPerSheet` | 1,048,575 | Rows per worksheet before continuing on `"<name> (2)"` |
194
+ | `sharedStrings` | `'auto'` | `'auto'` (off when a workbook has a streamed sheet), `true`, or `false` |
195
+
196
+ Read only:
197
+
198
+ | Option | Default | Purpose |
199
+ | --- | --- | --- |
200
+ | `headerRow` | 1 | Row containing headers |
201
+ | `nullForBlank` | `true` | Empty cells become `null` |
202
+ | `trimStrings` | `true` | Trim string cells |
203
+ | `dateFormat` | `'iso'` | `'iso'`, `'date'`, or `'epoch'` |
204
+ | `afterRead` | none | `(row, ctx) => row`, synchronous transform |
205
+ | `validateRow` | none | `(row, ctx) => null \| { message, code?, column? }` |
206
+ | `onRowError` | `'collect'` | `'collect'` or `'fail'` |
207
+
208
+ Sheet spec: `sheet`, `columns`, `styling` (`headerBold`, `freezeHeader`,
209
+ `autoFilter`), `data` (array, iterable, or async iterable; optional), and
210
+ `totalRows` (progress hint for streamed sheets).
211
+
86
212
  ## API
87
213
 
88
214
  ### Writing
@@ -98,15 +224,64 @@ These save to the ignored example-output/ directory by default.
98
224
  separate buffers, or write each workbook individually. No ZIP wrapper is added.
99
225
 
100
226
  `spec` is a non-empty array of workbooks, each containing a non-empty `sheets`
101
- array. Sheets contain `sheet`, optional `columns` and `styling`, and a `data`
102
- array. Workbook properties support author, company, title, created, and modified.
227
+ array. Sheets contain `sheet`, optional `columns` and `styling`, and optional
228
+ `data`. Workbook properties support author, company, title, created, and modified.
103
229
  The `workbook` field is logical metadata, not an automatic filename.
104
230
 
231
+ `data` accepts an array, any iterable (for example a `function*` generator), or
232
+ any async iterable (an `async function*`, an object-mode stream, or any async
233
+ iterable such as a database cursor; excelio has no database dependency). Omitted
234
+ or null `data` writes an empty sheet. Iterables are consumed once, so pass a
235
+ separate iterable per sheet; reusing one iterable object across sheets throws
236
+ `INVALID_SPEC` (the same array may be reused).
237
+
238
+ ```js
239
+ async function* rows() {
240
+ for await (const doc of collection.find({}).batchSize(1000)) {
241
+ yield { url: doc.url, status: doc.status };
242
+ }
243
+ }
244
+
245
+ await excelio.write([{
246
+ workbook: 'audit',
247
+ sheets: [{ sheet: 'Pages', columns: [{ key: 'url' }, { key: 'status' }], data: rows() }]
248
+ }], { to: 'audit.xlsx' });
249
+ ```
250
+
251
+ When excelio stops early (error or abort) it calls the iterator's `return()`,
252
+ which ends a `for await` loop in the generator and closes the underlying cursor.
253
+
105
254
  Columns support key, header, width, type (`string`, `number`, `boolean`, `date`),
106
255
  and Excel number-format string. Without explicit columns, keys are inferred from
107
- the first row only. Width defaults to 15. Sheet names replace forbidden Excel
108
- characters, truncate to 31 characters, and receive suffixes when duplicated.
109
- `logger.warn` reports truncation.
256
+ the first row only (for iterables, the first row is peeked and still written).
257
+ Explicit columns are recommended for iterables: stable order and types. Width
258
+ defaults to 15. Sheet names replace forbidden Excel characters, truncate to 31
259
+ characters, and receive suffixes when duplicated. `logger.warn` reports truncation.
260
+
261
+ ### Large Exports
262
+
263
+ - Combine iterable `data` with `to` (a file path or writable stream). Buffer
264
+ output still collects the whole file in memory.
265
+ - Iterable sheets wait whenever about 8 MiB of produced XML is queued for
266
+ compression or the target, checking every `batchSize` rows. Compression is
267
+ slower than row production, so this matters for local files too.
268
+ - `sharedStrings: 'auto' | true | false` (default `'auto'`): the Excel
269
+ shared-strings table is on unless a workbook has an iterable sheet. On gives a
270
+ smaller file, but memory grows with the number of distinct strings (1,000,000
271
+ rows of distinct URLs added 1.4–2.1 GB). Off keeps memory flat and writes
272
+ plain strings as `t="str"` cells.
273
+ - `maxRowsPerSheet` (default and maximum 1,048,575, Excel's limit minus the
274
+ header) continues a sheet on `"<name> (2)"`, `"<name> (3)"`, … with the same
275
+ columns, header, and styling. It applies to arrays and iterables. Names are
276
+ allocated after every logical sheet's own name, so a continuation never takes
277
+ a name another sheet uses.
278
+ - `totalRows` on an iterable sheet is an optional hint used for progress
279
+ `totalRows`; without it, progress `totalRows` is undefined.
280
+ - Measured with exceljs 4.4.0 on Node 22 (Windows): 1,000,000 rows with six
281
+ distinct-string columns to a file grew RSS by about 50 MB and took 24–58 s
282
+ across runs; a slow 64 KB sink and two 200,000-row sheets stayed under 25 MB. The
283
+ bound relies on exceljs/archiver internals, so it is re-measured on every
284
+ exceljs upgrade (`npm run test:memory`).
110
285
 
111
286
  ### Reading
112
287
 
@@ -155,7 +330,10 @@ await excelio.write(spec, {
155
330
  ```
156
331
 
157
332
  Hooks are synchronous. They receive `(row, ctx)` where ctx contains workbookIndex,
158
- workbookName, sheetIndex, sheetName, and the 1-based Excel rowNumber. Read applies
333
+ workbookName, sheetIndex, sheetName, and the 1-based Excel rowNumber. On write,
334
+ ctx also has `sheetPart` (1, 2, … after `maxRowsPerSheet` rollover); sheetIndex
335
+ stays the spec index, sheetName is the actual worksheet name, and rowNumber
336
+ restarts at 2 on each continuation sheet. Read applies
159
337
  afterRead before validateRow. Invalid rows are excluded from data and collected
160
338
  in errors by default; `onRowError: 'fail'` throws on the first issue. A hook
161
339
  returning null/undefined keeps the original row. Write transforms do not add
@@ -176,19 +354,25 @@ reusable worker pool, concurrency cap, or parallel splitting of a workbook.
176
354
  Submit independent operations concurrently only when memory and CPU permit.
177
355
 
178
356
  Functions (`beforeWrite`, `afterRead`, `validateRow`, `onProgress`), `logger`,
179
- `signal`, stream inputs, and writable stream destinations force inline execution,
180
- even when useWorker is true. Worker payloads must be structured-cloneable;
357
+ `signal`, stream inputs, writable stream destinations, and iterable sheet data
358
+ force inline execution, even when useWorker is true (with iterable data,
359
+ `logger.warn` reports the ignored useWorker). Worker payloads must be structured-cloneable;
181
360
  functions or unsupported values embedded in row data cannot be sent to workers.
182
361
  Bundlers/serverless deployments must preserve the emitted entrypoint and its
183
362
  sibling workers directory. Import the package normally rather than copying a
184
363
  single bundled file.
185
364
 
186
365
  `onProgress` receives phase and rowsProcessed plus available workbook/sheet
187
- metadata. `batchSize` defaults to 5,000 and controls notifications, not memory
188
- or yielding. Writes emit a final notification; reads notify at batch boundaries.
189
- `signal` is checked between rows and sheets, but cannot interrupt initial XLSX
190
- loading, commit, or synchronous row loops. Do not rely on timely cancellation of
191
- an in-progress operation from an event-loop callback.
366
+ metadata; write events include sheetIndex, sheetName, and sheetPart. `batchSize`
367
+ defaults to 5,000 and must be a positive integer. Writes emit a final
368
+ notification; reads notify at batch boundaries.
369
+
370
+ `signal` is checked between rows and sheets. Iterable sheets check it on every
371
+ row and yield to the event loop every `batchSize` rows, so an abort fired from a
372
+ timer or request handler is observed during long iterable writes. A source stuck
373
+ inside `next()` cannot be interrupted by excelio; pass the same signal to the
374
+ source. Array sheets still run as a synchronous loop, and initial XLSX loading
375
+ and commit cannot be interrupted.
192
376
 
193
377
  ### Values and Errors
194
378
 
@@ -198,16 +382,28 @@ the strings 'true'/'false', and 1/0. Unsupported coercions are left unchanged;
198
382
  these hints are not schema validation. Multiline and multilingual text is supported.
199
383
 
200
384
  `ExcelIoError` exposes code, optional cause, and optional details. Codes include
201
- INVALID_SPEC, IO_ERROR, ABORTED, WORKER_ERROR, and ROW_VALIDATION. Collected read
385
+ INVALID_SPEC, IO_ERROR, ABORTED, WORKER_ERROR, ROW_VALIDATION, and SOURCE_ERROR.
386
+ SOURCE_ERROR means an iterable sheet's source threw (or returned a non-object
387
+ iterator result); `cause` is the original error and `details` holds
388
+ workbookIndex, sheetIndex, sheetName, and rowsRead. If the signal is already
389
+ aborted when the source fails, the error is ABORTED with the source error as
390
+ `cause`. Collected read
202
391
  errors contain workbook index, sheet, row, column, code, and message. `ok` is
203
392
  false when collected errors exist; callers decide whether to accept partial data.
204
393
  Some lower-level ExcelJS/write errors can propagate as ordinary Error instances.
205
394
 
395
+ On a failed write, a file-path target is closed and removed; a caller's writable
396
+ stream is unpiped but neither ended nor destroyed; the original error is thrown,
397
+ with `err.cleanupError` set if cleanup itself failed. On success a caller's
398
+ stream is ended by the write, as before.
399
+
206
400
  ## Memory and Scope
207
401
 
208
- Writes use ExcelJS's streaming XLSX writer and commit rows incrementally, but
209
- input arrays and shared strings remain in memory; buffer output also collects
210
- the complete file. Direct file/stream output avoids that final buffer.
402
+ Writes use ExcelJS's streaming XLSX writer and commit rows incrementally. Array
403
+ `data` holds all rows in memory, and so does the shared-strings table when it is
404
+ on. Iterable `data` with the default `sharedStrings` and a file or stream target
405
+ keeps write memory bounded (see Large Exports). Buffer output collects the
406
+ complete file either way; direct file/stream output avoids that final buffer.
211
407
  Reading currently loads the entire workbook and collects accepted rows.
212
408
  Workers keep processing off the main thread but do not guarantee lower total
213
409
  memory or faster completion; cloning large inputs adds overhead.