@nazarkalytiuk/rusc-xlsx 1.0.10 → 2.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.
Files changed (6) hide show
  1. package/README.md +115 -11
  2. package/index.d.ts +98 -14
  3. package/index.js +367 -104
  4. package/package.json +47 -20
  5. package/wrapper.d.ts +171 -510
  6. package/wrapper.js +1228 -1381
package/README.md CHANGED
@@ -67,8 +67,8 @@ npm install @nazarkalytiuk/rusc-xlsx
67
67
 
68
68
  ### Requirements
69
69
 
70
- - Node.js >= 16
71
- - Rust toolchain (for building from source)
70
+ - Node.js >= 22.13
71
+ - Rust >= 1.95 (only when building from source)
72
72
 
73
73
  ## Quick Start
74
74
 
@@ -169,6 +169,11 @@ workbook.save('output.xlsx');
169
169
  - `addWorksheetWithConstantMemory([name])` - Constant memory mode
170
170
  - `addWorksheetWithLowMemory([name])` - Low memory mode
171
171
  - `setTempdir(path)` - Set temp directory for memory modes
172
+ - `setDefaultFormat(format, rowHeight, columnWidth)` - Set workbook-wide default formatting before adding worksheets
173
+ - `useExcel2023Theme()` / `useCustomTheme(path)` - Select the workbook theme before adding worksheets
174
+ - `setCalculationMode(mode)` - Formula recalculation: `'automatic'`, `'manual'` or `'partial'`
175
+ - `setZipCompressionLevel(level)` - Trade file size for speed (1-9, default 6)
176
+ - `setTabRatio(ratio)` - Width of the sheet tab bar vs. the horizontal scroll bar (0-100)
172
177
  - `save(filename)` - Save to file
173
178
  - `saveToBuffer()` - Save to Buffer
174
179
  - `saveToStream(writable)` - Save through a temp file and await stream completion
@@ -181,17 +186,17 @@ workbook.save('output.xlsx');
181
186
  - **Writing**: `write()`, `writeString()`, `writeNumber()`, `writeBoolean()`, `writeBlank()`
182
187
  - **DateTime**: `writeDatetime()`, `writeDatetimeFromTimestamp()`, `writeDatetimeFromYmd()`, etc.
183
188
  - **Formulas**: `writeFormula()`, `writeArrayFormula()`, `writeDynamicFormula()`
184
- - **Layout**: `setColumnWidth()`, `setRowHeight()`, `setDefaultRowHeight()`, `mergeRange()`, `setName()`
185
- - **Features**: `insertNote()`, `addDataValidation()`, `insertChart()`, `insertImage()`, `addTable()`, `insertButton()`, `insertShape()`
189
+ - **Layout**: `setColumnWidth()`, `setRowHeight()`, `setDefaultRowHeight()`, `mergeRange()`, `setName()`, `autofit()`, `setAutofitMaxRow()`, `setAutofitMaxWidth()`
190
+ - **Features**: `insertNote()`, `addDataValidation()`, `insertChart()`, `insertImage()`, `insertImageFitToCellCentered()`, `addTable()`, `insertButton()`, `insertShape()`
186
191
  - **Conditional Formatting**: 14 methods for all rule types
187
192
  - **Page Setup**: 26 methods for printing, margins, headers, etc.
188
- - **View**: `autofilter()`, `setFreezePanes()`, `setZoom()`, `setTabColor()`, `hideSheet()`
193
+ - **View**: `autofilter()`, `setFreezePanes()`, `setZoom()`, `setTabColor()`, `setHiddenZeroes()`, `setHidden()`
189
194
  - **Hyperlinks**: `writeUrl()`
190
195
  - **Protection**: `protect()`, `protectWithPassword()`
191
196
  - **Rich Text**: `writeRichString()`
192
197
 
193
- ### Format (39 methods)
194
- - **Font**: `setBold()`, `setItalic()`, `setFontSize()`, `setFontColor()`, `setFontName()`, `setUnderline()`, `setStrikethrough()`, `setFontScript()`
198
+ ### Format (40 methods)
199
+ - **Font**: `setBold()`, `setItalic()`, `setFontSize()`, `setFontColor()`, `setFontName()`, `setFontScheme()`, `setUnderline()`, `setStrikethrough()`, `setFontScript()`
195
200
  - **Alignment**: `setAlign()`, `setVerticalAlign()`, `setRotation()`, `setIndent()`, `setTextWrap()`, `setShrink()`
196
201
  - **Borders**: `setBorder()`, `setBorderTop/Bottom/Left/Right()`, `setBorderColor()`, `setBorderDiagonal()`
197
202
  - **Fill**: `setBackgroundColor()`, `setForegroundColor()`, `setPattern()`
@@ -202,7 +207,7 @@ workbook.save('output.xlsx');
202
207
  ### Supporting Classes
203
208
  - **Chart**: 25 chart types with series, titles, legends, 48 styles
204
209
  - **Image**: Insert with scaling, positioning, alt text, URLs
205
- - **Table**: Custom columns, total rows, 60 styles
210
+ - **Table**: Custom columns, total rows, alternative text, 60 styles
206
211
  - **Sparkline**: 3 types, markers, colors, 36 styles
207
212
  - **DataValidation**: All validation types with custom messages
208
213
  - **Note**: Comments with author, size, color, visibility
@@ -245,14 +250,31 @@ See the `examples/` directory for complete examples:
245
250
  git clone https://github.com/NazarKalytiuk/napi_rust_xlsxwrighter.git
246
251
  cd napi_rust_xlsxwrighter
247
252
 
248
- # Install dependencies
253
+ # Install dependencies (also builds wrapper.js + wrapper.d.ts from wrapper.ts via tsdown)
249
254
  npm install
250
255
 
251
256
  # Build native module
252
257
  npm run build
253
258
 
254
- # Run tests (577 tests)
259
+ # Rebuild the JavaScript wrapper after editing wrapper.ts
260
+ npm run build:wrapper
261
+
262
+ # Run tests (Vitest)
255
263
  npm test
264
+ npm run test:watch # re-run on change
265
+ npm run test:coverage # V8 coverage report for wrapper.ts
266
+ npm run test:coverage:rust # line coverage of src/*.rs via an instrumented addon;
267
+ # needs cargo-llvm-cov and `rustup component add llvm-tools`
268
+
269
+ # Format, lint (oxfmt, oxlint, rustfmt, clippy) and typecheck — same as CI
270
+ npm run check
271
+ npm run fmt # apply oxfmt + rustfmt
272
+
273
+ # rust_xlsxwriter coverage checklist (rust-xlsxwriter-coverage-checklist.md)
274
+ npm run coverage:check # verify test call sites, counts and the pinned version
275
+ npm run coverage:sync # refresh them after editing tests or row statuses; after a
276
+ # rust_xlsxwriter upgrade it documents the new release with
277
+ # rustdoc JSON (`rustup toolchain install nightly-2026-11-13`)
256
278
 
257
279
  # Run examples
258
280
  node examples/basic.js
@@ -265,7 +287,7 @@ node examples/basic.js
265
287
  Built with Rust and NAPI-RS for native-level performance:
266
288
  - **Fast execution**: Native Rust performance via NAPI bindings
267
289
  - **Low memory usage**: Optimized memory footprint across all modes
268
- - **Test performance**: All 577 tests complete in < 1 second
290
+ - **Test performance**: All 601 tests complete in < 1 second
269
291
 
270
292
  ### Memory Modes
271
293
 
@@ -335,6 +357,88 @@ for await (const item of cursor) {
335
357
  This keeps memory bounded to the current cursor item while retaining
336
358
  constant-memory worksheet generation.
337
359
 
360
+ ### Node.js vs Bun Benchmarks
361
+
362
+ The runtime benchmark executes the same package workload through the same
363
+ release N-API binary on Node.js and Bun:
364
+
365
+ ```bash
366
+ npm run benchmark:runtimes
367
+ ```
368
+
369
+ Bun must be installed and available in `PATH`. The default workload generates
370
+ 200,000 rows × 20 columns through `writeRow()` in constant-memory mode. The
371
+ runner alternates runtime order across three runs, reports both in-process
372
+ generation time and end-to-end process wall time, and verifies identical
373
+ worksheet XML size and CRC for every Node.js/Bun pair.
374
+
375
+ Override the workload or package API, or reuse the existing release build:
376
+
377
+ ```bash
378
+ npm run benchmark:runtimes -- --rows 100000 --columns 20 --runs 5
379
+ npm run benchmark:runtimes -- --write-mode cell
380
+ npm run benchmark:runtimes -- --write-mode batch --batch-rows 1000
381
+ npm run benchmark:runtimes -- --skip-build
382
+ ```
383
+
384
+ #### ExcelJS runtime comparison
385
+
386
+ To isolate the runtime difference for a JavaScript XLSX implementation, run the
387
+ same dataset through the ExcelJS streaming writer:
388
+
389
+ ```bash
390
+ npm run benchmark:runtimes:exceljs
391
+ ```
392
+
393
+ This profile uses ExcelJS `stream.xlsx.WorkbookWriter`, disables styles and
394
+ shared strings, fixes ZIP DEFLATE compression at level 6, and commits every
395
+ row. It uses the same alternating order, timing and memory metrics, and
396
+ worksheet-equivalence checks as the native-package profile. Override the
397
+ default workload when needed:
398
+
399
+ ```bash
400
+ npm run benchmark:runtimes:exceljs -- --rows 100000 --columns 20 --runs 5
401
+ ```
402
+
403
+ ### Node.js vs Go, C#, and Python Benchmark
404
+
405
+ The cross-language benchmark compares concrete streaming XLSX libraries, not
406
+ language runtimes in isolation:
407
+
408
+ | Runtime | XLSX implementation | Bounded-memory API |
409
+ |---------|---------------------|--------------------|
410
+ | Node.js | `@nazarkalytiuk/rusc-xlsx` | Constant-memory `writeRow()` |
411
+ | Go | Excelize 2.11.0 | `StreamWriter.SetRow()` |
412
+ | C# | Open XML SDK 3.5.1 | Forward-only worksheet XML writer |
413
+ | Python | XlsxWriter 3.2.9 | `constant_memory` + `write_row()` |
414
+
415
+ Install Rust, Go 1.25+, the .NET 8 SDK, and Python 3.10+, then run:
416
+
417
+ ```bash
418
+ npm run benchmark:languages
419
+ ```
420
+
421
+ The runner builds release artifacts and creates an isolated Python virtual
422
+ environment under `benchmarks/python-xlsxwriter/.venv`. The default workload is
423
+ the same 200,000 rows × 20 columns, 50/50 string-and-number dataset used by the
424
+ Rust comparison. Before measurement, each implementation generates a small
425
+ workbook that is checked for its worksheet name, complete row/cell shape, and
426
+ representative values. Measured execution order is balanced across runs, and
427
+ the report includes median generation time, end-to-end process wall time,
428
+ throughput, peak RSS, and XLSX size.
429
+
430
+ Override the workload, select available runtimes, or reuse existing artifacts:
431
+
432
+ ```bash
433
+ npm run benchmark:languages -- --rows 100000 --columns 20 --runs 5
434
+ npm run benchmark:languages -- --implementations node,go,python
435
+ npm run benchmark:languages -- --skip-build
436
+ ```
437
+
438
+ Use `--node-write-mode cell|row|batch` to measure a different Node.js API;
439
+ `row` is the cross-language default because all four implementations consume
440
+ the workload sequentially by row.
441
+
338
442
  ## Limitations
339
443
 
340
444
  - ❌ **Read-only**: Cannot read existing Excel files (rust_xlsxwriter is write-only)
package/index.d.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  /* auto-generated by NAPI-RS */
2
2
  /* eslint-disable */
3
+
4
+ /**
5
+ * Which binding artifact the generated loader actually loaded: `'native'` for
6
+ * a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
7
+ * flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
8
+ * can point the loader at a WASI artifact this package does not build itself.
9
+ */
10
+ export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
11
+
3
12
  export declare class RuscButton {
4
13
  /** Create a new Button */
5
14
  static new(): RuscButton
@@ -122,6 +131,8 @@ export declare class RuscFormat {
122
131
  setNumFormat(numFormat: string): void
123
132
  /** Set font name (e.g., 'Arial', 'Calibri', 'Times New Roman') */
124
133
  setFontName(fontName: string): void
134
+ /** Set whether the font uses a body, headings, or custom font scheme. */
135
+ setFontScheme(scheme: string): void
125
136
  /** Set text underline style */
126
137
  setUnderline(underline: string): void
127
138
  /** Set strikethrough text */
@@ -275,10 +286,10 @@ export declare class RuscRichText {
275
286
  /** Create a new empty rich text string */
276
287
  constructor()
277
288
  /**
278
- * Add a text segment with formatting
279
- * The format is cloned internally for storage
289
+ * Add a text segment, optionally formatted; without a format the segment uses
290
+ * the default cell font. The format is cloned internally for storage.
280
291
  */
281
- addSegment(text: string, format: RuscFormat): this
292
+ addSegment(text: string, format?: RuscFormat | undefined | null): this
282
293
  }
283
294
 
284
295
  export declare class RuscShape {
@@ -299,7 +310,10 @@ export declare class RuscShape {
299
310
  export declare class RuscSparkline {
300
311
  /** Create a new Sparkline */
301
312
  static new(): RuscSparkline
302
- /** Set the data range for the sparkline (e.g., "Sheet1!A1:E1") */
313
+ /**
314
+ * Set the data range for the sparkline, e.g. "Sheet1!A1:E1", or "A1:E1"
315
+ * for a range on the worksheet the sparkline is added to
316
+ */
303
317
  setRange(range: string): this
304
318
  /** Set the type of sparkline: 'line', 'column', or 'winLose' */
305
319
  setType(sparklineType: string): this
@@ -368,7 +382,14 @@ export declare class RuscTable {
368
382
  setLastColumn(show: boolean): this
369
383
  /** Set the name of the table */
370
384
  setName(name: string): this
371
- /** Set the style of the table (1-21 for light styles, 22-49 for medium styles, 50-61 for dark styles) */
385
+ /** Set alternative text for screen readers. */
386
+ setAltText(altText: string): this
387
+ /** Set the alternative text title for screen readers. */
388
+ setAltTextTitle(title: string): this
389
+ /**
390
+ * Set the style of the table: 0 for no style, 1-21 for light styles,
391
+ * 22-49 for medium styles and 50-60 for dark styles
392
+ */
372
393
  setStyle(styleId: number): this
373
394
  /** Add columns to the table */
374
395
  setColumns(columns: Array<RuscTableColumn>): this
@@ -399,6 +420,33 @@ export declare class RuscWorkbook {
399
420
  addWorksheetWithLowMemory(name?: string | undefined | null): RuscWorksheet
400
421
  /** Set a custom temporary directory for constant memory mode */
401
422
  setTempdir(dir: string): void
423
+ /**
424
+ * Set the default workbook format and row/column dimensions in pixels.
425
+ * Must be called before adding worksheets.
426
+ */
427
+ setDefaultFormat(format: RuscFormat, rowHeight: number, columnWidth: number): void
428
+ /**
429
+ * Set the zip compression level of the xlsx container (1-9, default 6).
430
+ * Lower levels are faster, higher levels produce smaller files.
431
+ */
432
+ setZipCompressionLevel(level: number): void
433
+ /**
434
+ * Set when Excel recalculates formulas: "automatic" (default), "manual",
435
+ * or "partial" (automatic except for data tables).
436
+ */
437
+ setCalculationMode(mode: string): void
438
+ /**
439
+ * Set the width of the worksheet tab bar as a percentage (0-100, default
440
+ * 60) of the space it shares with the horizontal scroll bar.
441
+ */
442
+ setTabRatio(ratio: number): void
443
+ /**
444
+ * Use the Excel 2023 Office theme with the Aptos default font.
445
+ * Must be called before adding worksheets.
446
+ */
447
+ useExcel2023Theme(): void
448
+ /** Load a custom theme from a .thmx, .xlsx, or theme XML file. */
449
+ useCustomTheme(path: string): void
402
450
  /** Save the workbook to a file */
403
451
  save(filename: string): void
404
452
  /** Save the workbook to a buffer */
@@ -434,20 +482,25 @@ export declare class RuscWorkbook {
434
482
 
435
483
  /** Worksheet wrapper for Excel sheet manipulation */
436
484
  export declare class RuscWorksheet {
437
- /** Write a value to a cell (auto-detects type) */
438
- write(row: number, col: number, value: string | number | boolean, format?: RuscFormat | undefined | null): void
485
+ /**
486
+ * Write a value to a cell (auto-detects type). `null` or `undefined`
487
+ * writes nothing, or a blank cell with the format when one is given.
488
+ */
489
+ write(row: number, col: number, value: string | number | boolean | null | undefined, format?: RuscFormat | undefined | null): void
439
490
  /**
440
491
  * Write an array of values as a row starting at the given column.
441
492
  * Uses lazy Array iteration — reads one cell at a time from the JS array
442
493
  * instead of deserializing the entire row into Rust memory.
443
- * Null/undefined values are skipped (preserving column positions).
494
+ * Values are written as `write` writes them: `null`, `undefined` and
495
+ * holes leave the cell empty, or write a blank cell with the format.
444
496
  */
445
497
  writeRow(row: number, startCol: number, values: unknown[], format?: RuscFormat | undefined | null): void
446
498
  /**
447
499
  * Write a 2D array of values starting at the given position.
448
500
  * Uses lazy Array iteration — reads one row/cell at a time from JS,
449
501
  * never materializing the entire grid in Rust memory.
450
- * Null/undefined values are skipped (preserving column positions).
502
+ * Values are written as `write` writes them: `null`, `undefined` and
503
+ * holes leave the cell empty, or write a blank cell with the format.
451
504
  */
452
505
  writeRows(startRow: number, startCol: number, rows: unknown[], format?: RuscFormat | undefined | null): void
453
506
  /** Write a string to a cell */
@@ -470,14 +523,23 @@ export declare class RuscWorksheet {
470
523
  writeDatetimeFromYmdHms(row: number, col: number, year: number, month: number, day: number, hour: number, min: number, sec: number, format?: RuscFormat | undefined | null): void
471
524
  /** Write a datetime from an ISO 8601 string (e.g., "2023-01-25T12:30:00") */
472
525
  writeDatetimeFromString(row: number, col: number, datetimeStr: string, format?: RuscFormat | undefined | null): void
473
- /** Write a value with formatting to a cell */
474
- writeWithFormat(row: number, col: number, value: string | number | boolean, format: RuscFormat): void
526
+ /**
527
+ * Write a value with formatting to a cell; `null` or `undefined` writes
528
+ * a blank cell with the format
529
+ */
530
+ writeWithFormat(row: number, col: number, value: string | number | boolean | null | undefined, format: RuscFormat): void
475
531
  /** Set the width of a column */
476
532
  setColumnWidth(col: number, width: number): void
477
533
  /** Set the height of a row */
478
534
  setRowHeight(row: number, height: number): void
479
535
  /** Set the default height for all worksheet rows efficiently */
480
536
  setDefaultRowHeight(height: number): void
537
+ /** Set the exclusive upper row bound inspected by autofit. */
538
+ setAutofitMaxRow(maxRow: number): void
539
+ /** Limit the autofitted column width in pixels. */
540
+ setAutofitMaxWidth(maxWidth: number): void
541
+ /** Autofit worksheet columns based on their cell contents. */
542
+ autofit(): void
481
543
  /** Set the name of the worksheet */
482
544
  setName(name: string): void
483
545
  /** Merge a range of cells */
@@ -576,6 +638,11 @@ export declare class RuscWorksheet {
576
638
  setHidden(enable: boolean): void
577
639
  /** Set the worksheet zoom level (10-400) */
578
640
  setZoom(zoom: number): void
641
+ /**
642
+ * Hide zero values in worksheet cells, the equivalent of turning off the
643
+ * Excel option "Show a zero in cells that have zero value"
644
+ */
645
+ setHiddenZeroes(enable: boolean): void
579
646
  /** Set the default cell selection when opening the worksheet */
580
647
  setSelection(firstRow: number, firstCol: number, lastRow: number, lastCol: number): void
581
648
  /** Set which cell appears in the top-left corner when opening the worksheet */
@@ -598,6 +665,8 @@ export declare class RuscWorksheet {
598
665
  insertImageWithOffset(row: number, col: number, image: RuscImage, xOffset: number, yOffset: number): void
599
666
  /** Insert an image that fits to a cell */
600
667
  insertImageFitToCell(row: number, col: number, image: RuscImage, keepAspectRatio: boolean): void
668
+ /** Insert an image scaled proportionally and centered within a cell. */
669
+ insertImageFitToCellCentered(row: number, col: number, image: RuscImage): void
601
670
  /** Add a table to the worksheet */
602
671
  addTable(firstRow: number, firstCol: number, lastRow: number, lastCol: number, table: RuscTable): void
603
672
  /** Add a sparkline to a single cell */
@@ -614,8 +683,23 @@ export declare class RuscWorksheet {
614
683
  insertShapeWithOffset(row: number, col: number, shape: RuscShape, xOffset: number, yOffset: number): void
615
684
  }
616
685
 
617
- /** Resets peak memory tracking to current usage */
686
+ /**
687
+ * Resets peak memory tracking to the current allocation level.
688
+ * Call before a section you want to measure.
689
+ *
690
+ * @example
691
+ * rustMemoryResetPeak();
692
+ * // ... do work ...
693
+ * const [current, peak] = rustMemoryStats();
694
+ */
618
695
  export declare function rustMemoryResetPeak(): void
619
696
 
620
- /** Returns [currentBytes, peakBytes] of Rust allocator usage */
621
- export declare function rustMemoryStats(): Array<number>
697
+ /**
698
+ * Returns [currentBytes, peakBytes] of Rust allocator usage.
699
+ * Useful for profiling memory consumption of the native layer.
700
+ *
701
+ * @example
702
+ * const [current, peak] = rustMemoryStats();
703
+ * console.log(`Rust heap: ${(current / 1024 / 1024).toFixed(2)} MB (peak: ${(peak / 1024 / 1024).toFixed(2)} MB)`);
704
+ */
705
+ export declare function rustMemoryStats(): [number, number]