Skip to content

Comments

fix: replace xlsx with exceljs to resolve Dependabot CVEs#94

Merged
benbernard merged 2 commits intomasterfrom
feature/replace-xlsx-with-exceljs
Feb 24, 2026
Merged

fix: replace xlsx with exceljs to resolve Dependabot CVEs#94
benbernard merged 2 commits intomasterfrom
feature/replace-xlsx-with-exceljs

Conversation

@benbernard
Copy link
Owner

Summary

  • Replace the xlsx npm package with exceljs to resolve 2 high-severity Dependabot CVEs on the default branch
  • Rewrite fromxls operation to use exceljs's streaming API
  • Add comprehensive tests for fromxls (single sheet, multi-sheet, field selection)
  • Regenerate test fixture xlsx files with exceljs

Test plan

  • bun test tests/operations/input/fromxls.test.ts — all fromxls tests pass
  • bun test — full suite passes (1541 pass, 0 fail)
  • bun run typecheck — no type errors

🤖 Generated with Claude Code

benbernard and others added 2 commits February 24, 2026 13:19
Replace the abandoned SheetJS xlsx npm package (stuck at 0.18.5 with no
patched version) with exceljs to fix CVE-2023-30533 (Prototype Pollution)
and CVE-2024-22363 (ReDoS), both high severity.

Rewrite fromxls.ts to use the exceljs async API, override finish() as
async, and add await to op.finish() calls in the dispatcher and executor
(backward compatible since await on void is a no-op). Add comprehensive
tests for fromxls covering headers, sheet selection, and number parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

Performance Benchmark Results

⚠️ 19 regressions detected out of 103 benchmarks (threshold: 25%)

Benchmark Median Baseline Delta
InputStream.fromString — 100 records 356.1µs 206.3µs +72.7% 🔴
Compiled KeySpec.resolveValue — nested (address/zip) 317.6µs 174.2µs +82.4% 🔴
xform — 10K records (push each record) 3.63ms 2.15ms +69.2% 🔴
sort — 100 records (by score, numeric) 187.1µs 146.9µs +27.3% 🔴
chain — 5 ops (grep eval grep eval
pipe — 2 ops (grep eval), 100 records 503.83ms 388.66ms
pipe — 2 ops (grep eval), 1K records 506.08ms 380.07ms
pipe — 2 ops (grep eval), 10K records 513.71ms 375.39ms
pipe — 3 ops (grep eval grep), 100 records 756.78ms
implicit — 3 ops (grep eval grep), 100 records 167.7µs
chain — 3 ops (grep eval grep), 1K records 305.2µs
pipe — 3 ops (grep eval grep), 1K records 759.23ms
chain — 3 ops (grep eval grep), 10K records 1.39ms
pipe — 3 ops (grep eval grep), 10K records 761.10ms
pipe — 5 ops (grep eval grep eval
implicit — 5 ops (grep eval grep eval
pipe — 5 ops (grep eval grep eval
pipe — 5 ops (grep eval grep eval
manual buffer (isolated) — 100 lines 233.1µs 178.3µs +30.8% 🔴

103 benchmarks: 18 faster, 24 slower, 61 within noise (10%)

ℹ️ Note: Benchmarks are advisory-only. GitHub Actions shared runners have variable performance, so results may fluctuate ±25% between runs. For reliable benchmarking, run locally with bun run bench.

Full benchmark results

JSON Parsing

Benchmark Median Baseline Delta Throughput
Record.fromJSON — 100 lines 153.6µs 153.3µs +0.3% 650.84K rec/s
Record.fromJSON — 10K lines 13.77ms 13.12ms +5.0% 726.27K rec/s, 215.1 MB/s
InputStream.fromString — 100 records 356.1µs 206.3µs +72.7% 🔴 280.80K rec/s
InputStream.fromString — 10K records 18.52ms 18.53ms -0.1% 540.03K rec/s, 159.9 MB/s
JSON.parse baseline — 10K lines (no Record) 13.18ms 12.76ms +3.3% 758.71K rec/s, 224.7 MB/s
JSON.parse single array — 10K records 12.58ms 12.55ms +0.3% 794.81K rec/s, 235.4 MB/s

JSON Serialization

Benchmark Median Baseline Delta Throughput
Record.toString — 100 records 87.7µs 71.3µs +22.9% 🔴 1.14M rec/s
Record.toString — 10K records 8.42ms 8.98ms -6.2% 1.19M rec/s, 351.8 MB/s
Record.toJSON — 10K records 295.4µs 298.7µs -1.1% 33.85M rec/s
JSON.stringify baseline — 10K objects (no Record) 7.98ms 7.94ms +0.5% 1.25M rec/s, 371.1 MB/s
Batch join — 10K records (map+join) 8.61ms 8.41ms +2.4% 1.16M rec/s, 343.9 MB/s

KeySpec Access

Benchmark Median Baseline Delta Throughput
KeySpec — simple key (name) 215.7µs 214.4µs +0.6% 46.36M rec/s
KeySpec — nested key (address/zip) 893.9µs 822.2µs +8.7% 11.19M rec/s
KeySpec — deep nested (address/coords/lat) 1.18ms 1.09ms +7.8% 8.49M rec/s
KeySpec — array index (tags/#0) 456.8µs 884.1µs -48.3% 🟢 21.89M rec/s
Direct property access baseline (rec['name']) 73.7µs 88.2µs -16.5% 🟢 135.64M rec/s
Direct nested access baseline (rec.address.coords.lat) 81.5µs 110.5µs -26.2% 🟢 122.66M rec/s
KeySpec construction — cached (same spec 10K times) 288.7µs 481.3µs -40.0% 🟢 34.64M rec/s
KeySpec construction — unique specs (10K different) 3.05ms 2.81ms +8.4% 3.28M rec/s
Compiled KeySpec.resolveValue — nested (address/zip) 317.6µs 174.2µs +82.4% 🔴 31.48M rec/s
Compiled KeySpec.resolveValue — deep (address/coords/lat) 127.3µs 122.5µs +3.8% 78.59M rec/s
Compiled KeySpec.resolveValue — array (tags/#0) 149.2µs 153.6µs -2.8% 67.03M rec/s
Compiled KeySpec.setValue — nested (address/zip) 144.2µs 201.3µs -28.3% 🟢 69.33M rec/s

Core Operations

Benchmark Median Baseline Delta Throughput
grep — 10K records (r.age > 50) 433.9µs 442.0µs -1.9% 23.05M rec/s
grep — 10K records (string match) 443.1µs 435.7µs +1.7% 22.57M rec/s
eval — 10K records (add computed field) 2.57ms 2.46ms +4.6% 3.89M rec/s
xform — 10K records (push each record) 3.63ms 2.15ms +69.2% 🔴 2.75M rec/s
sort — 100 records (by score, numeric) 187.1µs 146.9µs +27.3% 🔴 534.53K rec/s
sort — 10K records (by score, numeric) 18.98ms 19.51ms -2.7% 526.88K rec/s
sort — 10K records (by name, lexical) 12.25ms 12.66ms -3.3% 816.65K rec/s
collate — 100 records (count by city) 442.1µs 399.6µs +10.6% 🔴 226.21K rec/s
collate — 10K records (count by city) 12.25ms 12.42ms -1.3% 816.07K rec/s
fromcsv — 10K rows (parse CSV to records) 15.96ms 14.52ms +9.9% 626.71K rec/s, 41.2 MB/s

Pipeline Overhead

Benchmark Median Baseline Delta Throughput
chain — single op (grep), 10K records 7.12ms 7.05ms +1.0% 1.40M rec/s
chain — 3 ops (grep eval grep), 10K records 7.79ms 7.34ms
chain — 5 ops (grep eval grep eval grep), 10K records
passthrough baseline — 10K records (direct collector) 5.93ms 6.33ms -6.3% 1.69M rec/s

Record Creation & Serialization

Benchmark Median Baseline Delta Throughput
new Record() — 10K objects 94.8µs 100.0µs -5.2% 105.49M rec/s
new Record() empty — 10K 133.1µs 134.6µs -1.1% 75.14M rec/s
Record.get — 10K records × 3 fields 53.2µs 99.4µs -46.5% 🟢 564.39M rec/s
Record.set — 10K records × 1 field 61.6µs 124.1µs -50.4% 🟢 162.40M rec/s
Record.toJSON — 10K records 279.7µs 298.7µs -6.4% 35.75M rec/s
Record.toString — 10K records 8.52ms 8.98ms -5.1% 1.17M rec/s
Record.clone — 10K records 6.17ms 6.60ms -6.6% 1.62M rec/s
Record.fromJSON — 10K lines 12.78ms 13.12ms -2.6% 782.60K rec/s, 231.8 MB/s
Record.dataRef — 10K records (zero-copy) 38.1µs 86.2µs -55.8% 🟢 262.60M rec/s
Record.sort — 10K records (numeric field) 11.39ms 11.64ms -2.2% 878.01K rec/s
Record.sort — 10K records (lexical field) 6.05ms 6.11ms -0.9% 1.65M rec/s
Record.cmp — 1M comparisons (single field) 106.94ms 110.37ms -3.1% 9.35M rec/s
Record.sort — 10K records (nested field numeric) 16.02ms 16.06ms -0.3% 624.24K rec/s
Record.cmp — 1M comparisons (multi-field cached) 85.99ms 87.19ms -1.4% 11.63M rec/s
Record.sort — 10K records (cached comparator reuse) 12.02ms 12.12ms -0.8% 831.97K rec/s

Chain vs Pipe

Benchmark Median Baseline Delta Throughput
chain — 2 ops (grep eval), 100 records 139.4µs 137.7µs +1.2%
pipe — 2 ops (grep eval), 100 records 503.83ms 388.66ms +29.6% 🔴
implicit — 2 ops (grep eval), 100 records 75.3µs 73.7µs +2.2%
chain — 2 ops (grep eval), 1K records 197.9µs 197.4µs +0.2%
pipe — 2 ops (grep eval), 1K records 506.08ms 380.07ms +33.2% 🔴
implicit — 2 ops (grep eval), 1K records 202.7µs 208.2µs -2.6%
chain — 2 ops (grep eval), 10K records 1.04ms 1.38ms -24.3% 🟢
pipe — 2 ops (grep eval), 10K records 513.71ms 375.39ms +36.8% 🔴
implicit — 2 ops (grep eval), 10K records 1.07ms 1.10ms -2.6%
chain — 3 ops (grep eval grep), 100 records 91.0µs 171.1µs
pipe — 3 ops (grep eval grep), 100 records 756.78ms 572.39ms
implicit — 3 ops (grep eval grep), 100 records 167.7µs 112.2µs
chain — 3 ops (grep eval grep), 1K records 305.2µs 212.4µs
pipe — 3 ops (grep eval grep), 1K records 759.23ms 566.66ms
implicit — 3 ops (grep eval grep), 1K records 215.3µs 210.3µs
chain — 3 ops (grep eval grep), 10K records 1.39ms 1.08ms
pipe — 3 ops (grep eval grep), 10K records 761.10ms 576.72ms
implicit — 3 ops (grep eval grep), 10K records 1.09ms 1.37ms
chain — 5 ops (grep eval grep eval grep), 100 records
pipe — 5 ops (grep eval grep eval grep), 100 records
implicit — 5 ops (grep eval grep eval grep), 100 records
chain — 5 ops (grep eval grep eval grep), 1K records
pipe — 5 ops (grep eval grep eval grep), 1K records
implicit — 5 ops (grep eval grep eval grep), 1K records
chain — 5 ops (grep eval grep eval grep), 10K records
pipe — 5 ops (grep eval grep eval grep), 10K records
implicit — 5 ops (grep eval grep eval grep), 10K records

Line Reading

Benchmark Median Baseline Delta Throughput
InputStream.fromFile — 100 lines 507.7µs 713.4µs -28.8% 🟢 196.95K rec/s, 58.2 MB/s
InputStream.fromString — 100 lines 201.7µs 335.0µs -39.8% 🟢 495.68K rec/s, 146.4 MB/s
manual buffer (isolated) — 100 lines 233.1µs 178.3µs +30.8% 🔴 428.93K rec/s, 126.7 MB/s
bulk text + split — 100 lines 73.7µs 70.3µs +4.8% 1.36M rec/s, 400.9 MB/s
node readline — 100 lines 503.4µs 430.0µs +17.1% 🔴 198.64K rec/s, 58.7 MB/s
TextDecoderStream — 100 lines 260.8µs 312.7µs -16.6% 🟢 383.50K rec/s, 113.3 MB/s
binary newline scan — 100 lines 263.3µs 289.8µs -9.2% 379.85K rec/s, 112.2 MB/s
bun native stdin — 100 lines 24.91ms 25.85ms -3.6% 4.01K rec/s, 1.2 MB/s
InputStream.fromFile — 10K lines 23.36ms 24.10ms -3.1% 428.10K rec/s, 126.8 MB/s
InputStream.fromString — 10K lines 17.94ms 17.97ms -0.1% 557.37K rec/s, 165.1 MB/s
manual buffer (isolated) — 10K lines 6.51ms 5.85ms +11.4% 🔴 1.54M rec/s, 454.8 MB/s
bulk text + split — 10K lines 2.49ms 2.45ms +1.9% 4.01M rec/s, 1188.3 MB/s
node readline — 10K lines 9.12ms 9.33ms -2.3% 1.10M rec/s, 324.9 MB/s
TextDecoderStream — 10K lines 4.44ms 4.72ms -5.9% 2.25M rec/s, 666.9 MB/s
binary newline scan — 10K lines 8.37ms 9.90ms -15.4% 🟢 1.20M rec/s, 353.9 MB/s
bun native stdin — 10K lines 43.63ms 42.60ms +2.4% 229.18K rec/s, 67.9 MB/s
InputStream.fromFile — 100K lines 265.20ms 263.84ms +0.5% 377.07K rec/s, 112.0 MB/s
InputStream.fromString — 100K lines 212.42ms 215.43ms -1.4% 470.76K rec/s, 139.9 MB/s
manual buffer (isolated) — 100K lines 28.34ms 30.50ms -7.1% 3.53M rec/s, 1048.5 MB/s
bulk text + split — 100K lines 28.57ms 30.82ms -7.3% 3.50M rec/s, 1040.3 MB/s
node readline — 100K lines 82.13ms 89.11ms -7.8% 1.22M rec/s, 361.8 MB/s
TextDecoderStream — 100K lines 40.28ms 46.11ms -12.7% 🟢 2.48M rec/s, 737.7 MB/s
binary newline scan — 100K lines 74.58ms 86.48ms -13.8% 🟢 1.34M rec/s, 398.4 MB/s
bun native stdin — 100K lines 125.62ms 118.83ms +5.7% 796.06K rec/s, 236.6 MB/s

@benbernard benbernard merged commit 5aa2eee into master Feb 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant