@nazarkalytiuk/rusc-xlsx 2.0.0 → 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.
- package/README.md +68 -5
- package/index.d.ts +72 -14
- package/index.js +167 -32
- package/package.json +44 -17
- package/wrapper.d.ts +127 -521
- package/wrapper.js +1228 -1424
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ npm install @nazarkalytiuk/rusc-xlsx
|
|
|
68
68
|
### Requirements
|
|
69
69
|
|
|
70
70
|
- Node.js >= 22.13
|
|
71
|
-
- Rust >= 1.
|
|
71
|
+
- Rust >= 1.95 (only when building from source)
|
|
72
72
|
|
|
73
73
|
## Quick Start
|
|
74
74
|
|
|
@@ -171,6 +171,9 @@ workbook.save('output.xlsx');
|
|
|
171
171
|
- `setTempdir(path)` - Set temp directory for memory modes
|
|
172
172
|
- `setDefaultFormat(format, rowHeight, columnWidth)` - Set workbook-wide default formatting before adding worksheets
|
|
173
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)
|
|
174
177
|
- `save(filename)` - Save to file
|
|
175
178
|
- `saveToBuffer()` - Save to Buffer
|
|
176
179
|
- `saveToStream(writable)` - Save through a temp file and await stream completion
|
|
@@ -187,7 +190,7 @@ workbook.save('output.xlsx');
|
|
|
187
190
|
- **Features**: `insertNote()`, `addDataValidation()`, `insertChart()`, `insertImage()`, `insertImageFitToCellCentered()`, `addTable()`, `insertButton()`, `insertShape()`
|
|
188
191
|
- **Conditional Formatting**: 14 methods for all rule types
|
|
189
192
|
- **Page Setup**: 26 methods for printing, margins, headers, etc.
|
|
190
|
-
- **View**: `autofilter()`, `setFreezePanes()`, `setZoom()`, `setTabColor()`, `
|
|
193
|
+
- **View**: `autofilter()`, `setFreezePanes()`, `setZoom()`, `setTabColor()`, `setHiddenZeroes()`, `setHidden()`
|
|
191
194
|
- **Hyperlinks**: `writeUrl()`
|
|
192
195
|
- **Protection**: `protect()`, `protectWithPassword()`
|
|
193
196
|
- **Rich Text**: `writeRichString()`
|
|
@@ -247,14 +250,31 @@ See the `examples/` directory for complete examples:
|
|
|
247
250
|
git clone https://github.com/NazarKalytiuk/napi_rust_xlsxwrighter.git
|
|
248
251
|
cd napi_rust_xlsxwrighter
|
|
249
252
|
|
|
250
|
-
# Install dependencies
|
|
253
|
+
# Install dependencies (also builds wrapper.js + wrapper.d.ts from wrapper.ts via tsdown)
|
|
251
254
|
npm install
|
|
252
255
|
|
|
253
256
|
# Build native module
|
|
254
257
|
npm run build
|
|
255
258
|
|
|
256
|
-
#
|
|
259
|
+
# Rebuild the JavaScript wrapper after editing wrapper.ts
|
|
260
|
+
npm run build:wrapper
|
|
261
|
+
|
|
262
|
+
# Run tests (Vitest)
|
|
257
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`)
|
|
258
278
|
|
|
259
279
|
# Run examples
|
|
260
280
|
node examples/basic.js
|
|
@@ -267,7 +287,7 @@ node examples/basic.js
|
|
|
267
287
|
Built with Rust and NAPI-RS for native-level performance:
|
|
268
288
|
- **Fast execution**: Native Rust performance via NAPI bindings
|
|
269
289
|
- **Low memory usage**: Optimized memory footprint across all modes
|
|
270
|
-
- **Test performance**: All
|
|
290
|
+
- **Test performance**: All 601 tests complete in < 1 second
|
|
271
291
|
|
|
272
292
|
### Memory Modes
|
|
273
293
|
|
|
@@ -337,6 +357,49 @@ for await (const item of cursor) {
|
|
|
337
357
|
This keeps memory bounded to the current cursor item while retaining
|
|
338
358
|
constant-memory worksheet generation.
|
|
339
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
|
+
|
|
340
403
|
### Node.js vs Go, C#, and Python Benchmark
|
|
341
404
|
|
|
342
405
|
The cross-language benchmark compares concrete streaming XLSX libraries, not
|
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
|
|
@@ -277,10 +286,10 @@ export declare class RuscRichText {
|
|
|
277
286
|
/** Create a new empty rich text string */
|
|
278
287
|
constructor()
|
|
279
288
|
/**
|
|
280
|
-
* Add a text segment
|
|
281
|
-
* 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.
|
|
282
291
|
*/
|
|
283
|
-
addSegment(text: string, format
|
|
292
|
+
addSegment(text: string, format?: RuscFormat | undefined | null): this
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
export declare class RuscShape {
|
|
@@ -301,7 +310,10 @@ export declare class RuscShape {
|
|
|
301
310
|
export declare class RuscSparkline {
|
|
302
311
|
/** Create a new Sparkline */
|
|
303
312
|
static new(): RuscSparkline
|
|
304
|
-
/**
|
|
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
|
+
*/
|
|
305
317
|
setRange(range: string): this
|
|
306
318
|
/** Set the type of sparkline: 'line', 'column', or 'winLose' */
|
|
307
319
|
setType(sparklineType: string): this
|
|
@@ -374,7 +386,10 @@ export declare class RuscTable {
|
|
|
374
386
|
setAltText(altText: string): this
|
|
375
387
|
/** Set the alternative text title for screen readers. */
|
|
376
388
|
setAltTextTitle(title: string): this
|
|
377
|
-
/**
|
|
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
|
+
*/
|
|
378
393
|
setStyle(styleId: number): this
|
|
379
394
|
/** Add columns to the table */
|
|
380
395
|
setColumns(columns: Array<RuscTableColumn>): this
|
|
@@ -410,6 +425,21 @@ export declare class RuscWorkbook {
|
|
|
410
425
|
* Must be called before adding worksheets.
|
|
411
426
|
*/
|
|
412
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
|
|
413
443
|
/**
|
|
414
444
|
* Use the Excel 2023 Office theme with the Aptos default font.
|
|
415
445
|
* Must be called before adding worksheets.
|
|
@@ -452,20 +482,25 @@ export declare class RuscWorkbook {
|
|
|
452
482
|
|
|
453
483
|
/** Worksheet wrapper for Excel sheet manipulation */
|
|
454
484
|
export declare class RuscWorksheet {
|
|
455
|
-
/**
|
|
456
|
-
|
|
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
|
|
457
490
|
/**
|
|
458
491
|
* Write an array of values as a row starting at the given column.
|
|
459
492
|
* Uses lazy Array iteration — reads one cell at a time from the JS array
|
|
460
493
|
* instead of deserializing the entire row into Rust memory.
|
|
461
|
-
*
|
|
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.
|
|
462
496
|
*/
|
|
463
497
|
writeRow(row: number, startCol: number, values: unknown[], format?: RuscFormat | undefined | null): void
|
|
464
498
|
/**
|
|
465
499
|
* Write a 2D array of values starting at the given position.
|
|
466
500
|
* Uses lazy Array iteration — reads one row/cell at a time from JS,
|
|
467
501
|
* never materializing the entire grid in Rust memory.
|
|
468
|
-
*
|
|
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.
|
|
469
504
|
*/
|
|
470
505
|
writeRows(startRow: number, startCol: number, rows: unknown[], format?: RuscFormat | undefined | null): void
|
|
471
506
|
/** Write a string to a cell */
|
|
@@ -488,8 +523,11 @@ export declare class RuscWorksheet {
|
|
|
488
523
|
writeDatetimeFromYmdHms(row: number, col: number, year: number, month: number, day: number, hour: number, min: number, sec: number, format?: RuscFormat | undefined | null): void
|
|
489
524
|
/** Write a datetime from an ISO 8601 string (e.g., "2023-01-25T12:30:00") */
|
|
490
525
|
writeDatetimeFromString(row: number, col: number, datetimeStr: string, format?: RuscFormat | undefined | null): void
|
|
491
|
-
/**
|
|
492
|
-
|
|
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
|
|
493
531
|
/** Set the width of a column */
|
|
494
532
|
setColumnWidth(col: number, width: number): void
|
|
495
533
|
/** Set the height of a row */
|
|
@@ -600,6 +638,11 @@ export declare class RuscWorksheet {
|
|
|
600
638
|
setHidden(enable: boolean): void
|
|
601
639
|
/** Set the worksheet zoom level (10-400) */
|
|
602
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
|
|
603
646
|
/** Set the default cell selection when opening the worksheet */
|
|
604
647
|
setSelection(firstRow: number, firstCol: number, lastRow: number, lastCol: number): void
|
|
605
648
|
/** Set which cell appears in the top-left corner when opening the worksheet */
|
|
@@ -640,8 +683,23 @@ export declare class RuscWorksheet {
|
|
|
640
683
|
insertShapeWithOffset(row: number, col: number, shape: RuscShape, xOffset: number, yOffset: number): void
|
|
641
684
|
}
|
|
642
685
|
|
|
643
|
-
/**
|
|
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
|
+
*/
|
|
644
695
|
export declare function rustMemoryResetPeak(): void
|
|
645
696
|
|
|
646
|
-
/**
|
|
647
|
-
|
|
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]
|
package/index.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
// prettier-ignore
|
|
2
1
|
/* eslint-disable */
|
|
3
2
|
// @ts-nocheck
|
|
4
3
|
/* auto-generated by NAPI-RS */
|
|
5
4
|
|
|
6
5
|
const { readFileSync } = require('fs')
|
|
7
6
|
let nativeBinding = null
|
|
7
|
+
// Which artifact actually loaded. The WASI fallback chain overwrites it with
|
|
8
|
+
// the flavor it resolved; the late native retry below leaves it alone because
|
|
9
|
+
// it only runs while no WASI candidate has been loaded. The take-over check
|
|
10
|
+
// after the load-failure block can also set it.
|
|
11
|
+
let __napiLoadedBindingTarget = 'native'
|
|
12
|
+
// What this loader's own `requireNative()` calls returned. The check after
|
|
13
|
+
// the load-failure block compares it with `nativeBinding` to tell a binding
|
|
14
|
+
// this loader loaded from one that code outside it put in its place.
|
|
15
|
+
let __napiRequiredNativeBinding = null
|
|
8
16
|
const loadErrors = []
|
|
9
17
|
|
|
10
18
|
const isMusl = () => {
|
|
@@ -63,7 +71,16 @@ const isMuslFromChildProcess = () => {
|
|
|
63
71
|
function requireNative() {
|
|
64
72
|
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
|
65
73
|
try {
|
|
66
|
-
|
|
74
|
+
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
|
|
75
|
+
// The override may be a generated WASI loader, which already reports its
|
|
76
|
+
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
|
|
77
|
+
// 'native' would both misreport the artifact and overwrite the loader's
|
|
78
|
+
// marker through the alias.
|
|
79
|
+
__napiLoadedBindingTarget =
|
|
80
|
+
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
|
|
81
|
+
? overrideBinding.__napiBindingTarget
|
|
82
|
+
: 'native'
|
|
83
|
+
return overrideBinding
|
|
67
84
|
} catch (err) {
|
|
68
85
|
loadErrors.push(err)
|
|
69
86
|
}
|
|
@@ -107,36 +124,36 @@ function requireNative() {
|
|
|
107
124
|
if (process.arch === 'x64') {
|
|
108
125
|
if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) {
|
|
109
126
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
return require('./rusc_xlsx.win32-x64-gnu.node')
|
|
128
|
+
} catch (e) {
|
|
129
|
+
loadErrors.push(e)
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const binding = require('@nazarkalytiuk/rusc-xlsx-win32-x64-gnu')
|
|
133
|
+
const bindingPackageVersion = require('@nazarkalytiuk/rusc-xlsx-win32-x64-gnu/package.json').version
|
|
134
|
+
if (bindingPackageVersion !== '2.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
135
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
136
|
+
}
|
|
137
|
+
return binding
|
|
138
|
+
} catch (e) {
|
|
139
|
+
loadErrors.push(e)
|
|
119
140
|
}
|
|
120
|
-
return binding
|
|
121
|
-
} catch (e) {
|
|
122
|
-
loadErrors.push(e)
|
|
123
|
-
}
|
|
124
141
|
} else {
|
|
125
142
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
return require('./rusc_xlsx.win32-x64-msvc.node')
|
|
144
|
+
} catch (e) {
|
|
145
|
+
loadErrors.push(e)
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const binding = require('@nazarkalytiuk/rusc-xlsx-win32-x64-msvc')
|
|
149
|
+
const bindingPackageVersion = require('@nazarkalytiuk/rusc-xlsx-win32-x64-msvc/package.json').version
|
|
150
|
+
if (bindingPackageVersion !== '2.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
151
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
152
|
+
}
|
|
153
|
+
return binding
|
|
154
|
+
} catch (e) {
|
|
155
|
+
loadErrors.push(e)
|
|
135
156
|
}
|
|
136
|
-
return binding
|
|
137
|
-
} catch (e) {
|
|
138
|
-
loadErrors.push(e)
|
|
139
|
-
}
|
|
140
157
|
}
|
|
141
158
|
} else if (process.arch === 'ia32') {
|
|
142
159
|
try {
|
|
@@ -257,6 +274,22 @@ function requireNative() {
|
|
|
257
274
|
} catch (e) {
|
|
258
275
|
loadErrors.push(e)
|
|
259
276
|
}
|
|
277
|
+
} else if (process.arch === 'ppc64') {
|
|
278
|
+
try {
|
|
279
|
+
return require('./rusc_xlsx.freebsd-ppc64.node')
|
|
280
|
+
} catch (e) {
|
|
281
|
+
loadErrors.push(e)
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const binding = require('@nazarkalytiuk/rusc-xlsx-freebsd-ppc64')
|
|
285
|
+
const bindingPackageVersion = require('@nazarkalytiuk/rusc-xlsx-freebsd-ppc64/package.json').version
|
|
286
|
+
if (bindingPackageVersion !== '2.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
287
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
288
|
+
}
|
|
289
|
+
return binding
|
|
290
|
+
} catch (e) {
|
|
291
|
+
loadErrors.push(e)
|
|
292
|
+
}
|
|
260
293
|
} else {
|
|
261
294
|
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
|
|
262
295
|
}
|
|
@@ -550,7 +583,7 @@ function createLoadErrorChain(errors) {
|
|
|
550
583
|
//
|
|
551
584
|
// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict
|
|
552
585
|
// WASI loading. It never crosses into another flavor or falls back to native.
|
|
553
|
-
const __napiWasiFlavors = [
|
|
586
|
+
const __napiWasiFlavors = ['wasm32-wasi']
|
|
554
587
|
const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR
|
|
555
588
|
const __napiWasiFlavorRequested =
|
|
556
589
|
typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0
|
|
@@ -573,6 +606,7 @@ const forceWasi =
|
|
|
573
606
|
|
|
574
607
|
if (!forceWasi) {
|
|
575
608
|
nativeBinding = requireNative()
|
|
609
|
+
__napiRequiredNativeBinding = nativeBinding
|
|
576
610
|
}
|
|
577
611
|
|
|
578
612
|
if (!nativeBinding || forceWasi) {
|
|
@@ -619,15 +653,16 @@ if (!nativeBinding || forceWasi) {
|
|
|
619
653
|
}
|
|
620
654
|
return null
|
|
621
655
|
}
|
|
622
|
-
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor ===
|
|
656
|
+
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) {
|
|
623
657
|
let candidateError = null
|
|
624
658
|
let candidateFailed = false
|
|
625
659
|
try {
|
|
626
|
-
candidateError = __napiWasiResolveCandidate('./rusc_xlsx.wasi.cjs', false, [
|
|
660
|
+
candidateError = __napiWasiResolveCandidate('./rusc_xlsx.wasi.cjs', false, ['./rusc_xlsx.wasm32-wasi.debug.wasm', './rusc_xlsx.wasm32-wasi.wasm'])
|
|
627
661
|
candidateFailed = candidateError !== null
|
|
628
662
|
if (!candidateFailed) {
|
|
629
663
|
wasiBinding = require('./rusc_xlsx.wasi.cjs')
|
|
630
664
|
nativeBinding = wasiBinding
|
|
665
|
+
__napiLoadedBindingTarget = 'wasm32-wasi'
|
|
631
666
|
wasiBindingLoaded = true
|
|
632
667
|
}
|
|
633
668
|
} catch (err) {
|
|
@@ -639,7 +674,7 @@ if (!nativeBinding || forceWasi) {
|
|
|
639
674
|
loadErrors.push(candidateError)
|
|
640
675
|
}
|
|
641
676
|
}
|
|
642
|
-
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor ===
|
|
677
|
+
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) {
|
|
643
678
|
let candidateError = null
|
|
644
679
|
let candidateFailed = false
|
|
645
680
|
try {
|
|
@@ -654,6 +689,7 @@ if (!nativeBinding || forceWasi) {
|
|
|
654
689
|
}
|
|
655
690
|
wasiBinding = require('@nazarkalytiuk/rusc-xlsx-wasm32-wasi')
|
|
656
691
|
nativeBinding = wasiBinding
|
|
692
|
+
__napiLoadedBindingTarget = 'wasm32-wasi'
|
|
657
693
|
wasiBindingLoaded = true
|
|
658
694
|
}
|
|
659
695
|
} catch (err) {
|
|
@@ -672,6 +708,7 @@ if (!nativeBinding || forceWasi) {
|
|
|
672
708
|
!__napiWasiFlavorRequested
|
|
673
709
|
) {
|
|
674
710
|
nativeBinding = requireNative()
|
|
711
|
+
__napiRequiredNativeBinding = nativeBinding
|
|
675
712
|
}
|
|
676
713
|
if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) {
|
|
677
714
|
const error = new Error(
|
|
@@ -699,6 +736,104 @@ if (!nativeBinding) {
|
|
|
699
736
|
throw new Error(`Failed to load native binding`)
|
|
700
737
|
}
|
|
701
738
|
|
|
739
|
+
// A package can patch its own fallback in front of the load-failure check
|
|
740
|
+
// above, e.g. a WebContainer download through `napi-postinstall/fallback`.
|
|
741
|
+
// That fallback swaps `nativeBinding` for a generated WASI loader, which has
|
|
742
|
+
// already stamped its own flavor, while the target above still says 'native'.
|
|
743
|
+
// Take that flavor over, or the CommonJS stamp throws a conflict and the ESM
|
|
744
|
+
// export reports the wrong artifact. Three limits keep the conflict check:
|
|
745
|
+
// - only a binding this loader did not load itself, so a native addon whose
|
|
746
|
+
// `#[napi(module_exports)]` hook claims the name is still rejected;
|
|
747
|
+
// - only an own data property, read through its descriptor, so no addon
|
|
748
|
+
// getter runs here, and it must read back the same, as the stamp reads it;
|
|
749
|
+
// - only a flavor in `__napiWasiFlavors`, the ones this loader's own WASI
|
|
750
|
+
// chain can report ('wasm32-wasi' for a package without a WASI target).
|
|
751
|
+
// This is metadata and never fails a load: a throwing Proxy trap keeps 'native'.
|
|
752
|
+
if (
|
|
753
|
+
__napiLoadedBindingTarget === 'native' &&
|
|
754
|
+
nativeBinding !== __napiRequiredNativeBinding
|
|
755
|
+
) {
|
|
756
|
+
try {
|
|
757
|
+
const __napiBindingTargetDescriptor = Object.getOwnPropertyDescriptor(
|
|
758
|
+
nativeBinding,
|
|
759
|
+
'__napiBindingTarget',
|
|
760
|
+
)
|
|
761
|
+
if (
|
|
762
|
+
__napiBindingTargetDescriptor &&
|
|
763
|
+
__napiWasiFlavors.indexOf(__napiBindingTargetDescriptor.value) !== -1 &&
|
|
764
|
+
nativeBinding.__napiBindingTarget === __napiBindingTargetDescriptor.value
|
|
765
|
+
) {
|
|
766
|
+
__napiLoadedBindingTarget = __napiBindingTargetDescriptor.value
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
// keep 'native'
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function __napiStampBindingTarget(exportsObject, target) {
|
|
774
|
+
if (
|
|
775
|
+
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
|
|
776
|
+
) {
|
|
777
|
+
if (exportsObject.__napiBindingTarget === target) {
|
|
778
|
+
// Already ours: the root entry aliases the object it loaded, so a WASI
|
|
779
|
+
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
|
|
780
|
+
// is a generated loader — arrives already stamped with this same value.
|
|
781
|
+
return target
|
|
782
|
+
}
|
|
783
|
+
const error = new Error(
|
|
784
|
+
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
|
|
785
|
+
)
|
|
786
|
+
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
|
|
787
|
+
throw error
|
|
788
|
+
}
|
|
789
|
+
if (!Object.isExtensible(exportsObject)) {
|
|
790
|
+
// A `#[napi(module_exports)]` hook may seal or freeze this object
|
|
791
|
+
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
|
|
792
|
+
// never a reason to fail an otherwise successful load, so the stamp is
|
|
793
|
+
// skipped. What a consumer still sees then follows the entry point: the
|
|
794
|
+
// browser and deferred loaders declare `__napiBindingTarget` at module
|
|
795
|
+
// level and go on reporting it, while the CommonJS entries hand back this
|
|
796
|
+
// very object as `module.exports`, so there the value is absent.
|
|
797
|
+
return target
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
|
|
801
|
+
// chain, so an inherited accessor could swallow the value or throw and
|
|
802
|
+
// fail an otherwise successful load. The descriptor is what a successful
|
|
803
|
+
// assignment would have produced.
|
|
804
|
+
Object.defineProperty(exportsObject, '__napiBindingTarget', {
|
|
805
|
+
configurable: true,
|
|
806
|
+
enumerable: true,
|
|
807
|
+
value: target,
|
|
808
|
+
writable: true,
|
|
809
|
+
})
|
|
810
|
+
} catch {
|
|
811
|
+
// Same rule as the non-extensible skip above: reporting the artifact is
|
|
812
|
+
// metadata, never a reason to fail an otherwise successful load. An exotic
|
|
813
|
+
// object (a Proxy whose defineProperty trap refuses) is skipped, not
|
|
814
|
+
// thrown over.
|
|
815
|
+
}
|
|
816
|
+
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
|
|
817
|
+
// therefore Node's CJS -> ESM named export detection — can see
|
|
818
|
+
// `__napiBindingTarget` statically.
|
|
819
|
+
return target
|
|
820
|
+
}
|
|
821
|
+
// Stamp before the alias, not after. The guard only reads `nativeBinding`
|
|
822
|
+
// (`hasOwnProperty` plus a comparison), which is safe against any addon
|
|
823
|
+
// accessor; an assignment is not, because a `#[napi(module_exports)]` hook can
|
|
824
|
+
// expose a getter reporting this very value and a setter that throws. So the
|
|
825
|
+
// assignment lands on the loader's own `module.exports`, still the original
|
|
826
|
+
// object here, and the alias below replaces it.
|
|
827
|
+
//
|
|
828
|
+
// The assignment is what keeps the marker a statically visible CommonJS export:
|
|
829
|
+
// `cjs-module-lexer` is Node's CJS -> ESM named export detection, it cannot see
|
|
830
|
+
// a bare call, and the later `module.exports = nativeBinding` does not undo the
|
|
831
|
+
// detection. The assignment itself always succeeds — its target is this
|
|
832
|
+
// loader's own, still extensible `module.exports` — and the alias below then
|
|
833
|
+
// discards the value it wrote. What a consumer reads is whatever the guard put
|
|
834
|
+
// on `nativeBinding`, so on a frozen binding, where the guard skips, the
|
|
835
|
+
// linked import resolves to `undefined`.
|
|
836
|
+
module.exports.__napiBindingTarget = __napiStampBindingTarget(nativeBinding, __napiLoadedBindingTarget)
|
|
702
837
|
module.exports = nativeBinding
|
|
703
838
|
module.exports.RuscButton = nativeBinding.RuscButton
|
|
704
839
|
module.exports.RuscChart = nativeBinding.RuscChart
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nazarkalytiuk/rusc-xlsx",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Fast Excel XLSX library for Node.js powered by rust_xlsxwriter",
|
|
5
|
+
"type": "commonjs",
|
|
5
6
|
"main": "wrapper.js",
|
|
6
7
|
"types": "wrapper.d.ts",
|
|
7
8
|
"repository": {
|
|
@@ -32,14 +33,29 @@
|
|
|
32
33
|
"artifacts": "napi artifacts",
|
|
33
34
|
"build": "napi build --platform --release",
|
|
34
35
|
"build:debug": "napi build --platform",
|
|
36
|
+
"build:wrapper": "tsdown",
|
|
37
|
+
"prepare": "tsdown",
|
|
35
38
|
"benchmark": "node benchmarks/compare-generation.js",
|
|
36
39
|
"benchmark:languages": "node benchmarks/compare-languages.js",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
40
|
+
"benchmark:runtimes": "node benchmarks/compare-runtimes.js",
|
|
41
|
+
"benchmark:runtimes:exceljs": "node benchmarks/compare-runtimes.js --library exceljs",
|
|
42
|
+
"prepublishOnly": "napi prepublish -t npm --no-gh-release",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
46
|
+
"test:coverage:rust": "bash scripts/rust-coverage.sh",
|
|
47
|
+
"lint": "oxlint --deny-warnings",
|
|
48
|
+
"lint:rust": "cargo clippy --locked --all-targets -- -D warnings",
|
|
49
|
+
"fmt": "oxfmt && cargo fmt --all",
|
|
50
|
+
"fmt:check": "oxfmt --check && cargo fmt --all -- --check",
|
|
51
|
+
"lint:package": "publint --strict && attw --pack .",
|
|
39
52
|
"typecheck": "tsc --project tsconfig.json",
|
|
53
|
+
"check": "npm run fmt:check && npm run lint && npm run lint:rust && npm run lint:package && npm run typecheck && npm run coverage:check",
|
|
40
54
|
"universal": "napi universalize",
|
|
41
55
|
"version": "napi version",
|
|
42
|
-
"clean": "cargo clean && rm -rf *.node"
|
|
56
|
+
"clean": "cargo clean && rm -rf *.node",
|
|
57
|
+
"coverage:sync": "node scripts/coverage-checklist/cli.mts",
|
|
58
|
+
"coverage:check": "node scripts/coverage-checklist/cli.mts --check"
|
|
43
59
|
},
|
|
44
60
|
"keywords": [
|
|
45
61
|
"xlsx",
|
|
@@ -61,22 +77,33 @@
|
|
|
61
77
|
"LICENSE"
|
|
62
78
|
],
|
|
63
79
|
"devDependencies": {
|
|
64
|
-
"@
|
|
65
|
-
"@
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
80
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
81
|
+
"@napi-rs/cli": "^3.10.12",
|
|
82
|
+
"@types/node": "^26.6.6",
|
|
83
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
84
|
+
"adm-zip": "^0.6.1",
|
|
85
|
+
"exceljs": "^4.4.0",
|
|
86
|
+
"oxc-parser": "^0.158.0",
|
|
87
|
+
"oxfmt": "^0.78.0",
|
|
88
|
+
"oxlint": "^1.93.0",
|
|
89
|
+
"publint": "^0.3.25",
|
|
90
|
+
"tsdown": "^0.23.7",
|
|
91
|
+
"typescript": "^7.0.2",
|
|
92
|
+
"vitest": "^5.1.0"
|
|
69
93
|
},
|
|
70
94
|
"engines": {
|
|
71
95
|
"node": ">=22.13.0"
|
|
72
96
|
},
|
|
97
|
+
"publishConfig": {
|
|
98
|
+
"access": "public"
|
|
99
|
+
},
|
|
73
100
|
"optionalDependencies": {
|
|
74
|
-
"@nazarkalytiuk/rusc-xlsx-win32-x64-msvc": "2.
|
|
75
|
-
"@nazarkalytiuk/rusc-xlsx-win32-ia32-msvc": "2.
|
|
76
|
-
"@nazarkalytiuk/rusc-xlsx-win32-arm64-msvc": "2.
|
|
77
|
-
"@nazarkalytiuk/rusc-xlsx-darwin-universal": "2.
|
|
78
|
-
"@nazarkalytiuk/rusc-xlsx-linux-x64-gnu": "2.
|
|
79
|
-
"@nazarkalytiuk/rusc-xlsx-linux-arm64-gnu": "2.
|
|
80
|
-
"@nazarkalytiuk/rusc-xlsx-linux-x64-musl": "2.
|
|
101
|
+
"@nazarkalytiuk/rusc-xlsx-win32-x64-msvc": "2.1.0",
|
|
102
|
+
"@nazarkalytiuk/rusc-xlsx-win32-ia32-msvc": "2.1.0",
|
|
103
|
+
"@nazarkalytiuk/rusc-xlsx-win32-arm64-msvc": "2.1.0",
|
|
104
|
+
"@nazarkalytiuk/rusc-xlsx-darwin-universal": "2.1.0",
|
|
105
|
+
"@nazarkalytiuk/rusc-xlsx-linux-x64-gnu": "2.1.0",
|
|
106
|
+
"@nazarkalytiuk/rusc-xlsx-linux-arm64-gnu": "2.1.0",
|
|
107
|
+
"@nazarkalytiuk/rusc-xlsx-linux-x64-musl": "2.1.0"
|
|
81
108
|
}
|
|
82
|
-
}
|
|
109
|
+
}
|