Releases: office-kit/xlsx
Release list
xlsx-kit@0.8.0
Minor Changes
-
#78
2a46931Thanks @baseballyama! - Breaking:Chartsheet.properties.tabColorreplacestabColorRgb.Worksheets already expose
SheetProperties.tabColoras a fullColor(rgb / indexed / theme / auto / tint). Chartsheets carried a stringly-typedtabColorRgbinstead, which forced callers to special-case the two sheet kinds and silently dropped every non-RGB colour attribute Excel produces.The new field is a
Colorobject so both sheet kinds share one tab-colour model. Migration:// Before cs.properties = { tabColorRgb: "FF8800" }; // After cs.properties = { tabColor: { rgb: "FF8800" } };
Reads recover the additional
indexed/theme/auto/tintattributes that the old shape discarded. -
#78
2a46931Thanks @baseballyama! - fix: release streaming sinks when serialization fails, and bound the streaming write-only buffer against CJK payloads.When
saveWorkbookorcreateWriteOnlyWorkbook().finalize()threw partway through serialization, the underlying sink stayed open —toFilewould leave a half-written.xlsxon disk that callers could mistake for a successful save.BufferedSinkWriternow exposes an optionalabort(cause?)hook; the ZIP writer + workbook writers call it from a surroundingcatchso streaming destinations (toFile/toWritable) are released and the partial file is best-effort removed. The write-only workbook also exposes its ownabort(cause?)for callers driving it from a custom pipeline.The streaming write-only worksheet's pending-byte counter and the XML stream writer's flush threshold now use an accurate UTF-8 length (
utf8ByteLength) instead ofstring.length. The previous accounting undercounted CJK text by ~3×, letting the in-flight buffer grow well past the configured flush threshold; Japanese / Chinese workloads now flush at the documented ~64 KB ceiling.API:
BufferedSinkWriter.abort(cause?)is optional. Custom sinks don't need to implement it, but doing so letssaveWorkbookclean up streaming destinations on failure.WriteOnlyWorkbook.abort(cause?)is the matching escape hatch on the write-only API.XlsxSink.toBytesis now required at the type level. It was already required in practice — the writer threw at runtime when it was missing — and built-in sinks (toBuffer/toBlob/toArrayBuffer/toFile/toWritable) already implement it.
-
#78
2a46931Thanks @baseballyama! - fix: tighten three worksheet / cell mutation paths that previously produced silently-bad workbooks.-
copyRange/moveRangeno longer carryhyperlinkId/commentIdacross worksheets. Those fields are indexes into the source sheet'shyperlinks/legacyCommentsarrays and would point at unrelated records (or out of bounds) on the destination. Same-sheet copy / move still preserves them. -
setSheetStaterefuses to hide the last visible sheet. Excel rejects workbooks with every sheet hidden ("Excel cannot use the object linking and embedding features…"); raising at mutation time keeps the workbook recoverable instead of producing a save Excel will reject. -
makeTextRunthrowsOpenXmlSchemaErrorinstead ofTypeErrorso every public error path uses the documentedOpenXmlErrorsubclass hierarchy.
-
Patch Changes
-
#78
2a46931Thanks @baseballyama! - chore(ci): tighten the CI gate and surface CLAUDE.md hard rules in the linter.-
The test matrix now runs on macOS and Windows alongside Ubuntu. Path-separator and TextDecoder differences that ubuntu-only CI would silently miss are now exercised on every PR. Each OS installs
libxml2-utils/libxml2so the ECMA-376 conformance gate never silently degrades to "no schema validation ran". -
A dedicated
perfjob runspnpm test:perfwithPERF_GATE=1, promoting the throughput / heap thresholds intests/perf/from informational to fatal. CI runners are noisier than the M1 baseline the gates target — if a specific Node minor turns flaky, retune the thresholds invitest.perf.config.tsrather than reverting this gate. -
typescript/no-explicit-anyiserrorinsrc/andoffintests/(whereas anyis a legitimate way to exercise error paths). The remaining intentionalanyinsrc/schema/core.tsis annotated with// oxlint-disable-next-line typescript/no-explicit-anyand an explanation comment.
-
-
#78
2a46931Thanks @baseballyama! - refactor: route every XML writer through one canonicalescapeXmlAttr/escapeXmlTextpair insrc/utils/escape.ts.Before, twelve files each carried their own near-identical escape regex —
src/io/save.tsdeliberately skipped>while the rest escaped it, and three of them also escaped\r/\n/\tvia numeric character references. The discrepancies were quiet correctness bugs (attribute values containing]]>rendered differently across writers) and a maintenance hazard.The unified helpers escape
&,<,>, and"; whitespace bytes stay literal because our parser (fast-xml-parser) does not decode numeric character references and would otherwise break the round-trip.No user-visible behaviour change beyond consistent attribute escaping across every writer.
-
#78
2a46931Thanks @baseballyama! - perf: drop quadratic / linear-scan patterns on four read/write hot paths.-
The
loadWorkbookresolver indexes each sheet's rels file once via the newindexRelsByIdhelper instead of running a freshArray.find(r => r.id === relId)per table / comments / drawing / chart / picture cross-reference. Worksheets with many drawings or pivot tables load in O(refs) instead of O(refs × rels). -
containsCommentMarkerin the VML drawing classifier replaces a byte-by-byte JS loop with a single latin1String.indexOf— multi-megabyte legacy VML drawings load noticeably faster. -
The streaming
iterParsequeue uses a head pointer instead ofArray#shift(). A single SAX batch with hundreds of events (typical for a wide<row>) is now O(N) instead of O(N²); a stale dead-code branch in the prologue gate is gone too. -
serializeHyperlinksallocates rIds via aSetindex instead of nestedArray.some()calls. Worksheets with hundreds of hyperlinks (dashboards / link-heavy index sheets) finish save in O(N) rather than O(N²).
-
-
#78
2a46931Thanks @baseballyama! - refactor: collapsedescribeWorkbookinto a single pass, hardengetRowValues, and documentnormalizePath's..handling.describeWorkbookused to walk every cell three times — once forgetWorkbookStats, once forgetWorkbookCellsByKind, and once again for the per-sheet counts. The new implementation fuses all three into one pass and shares the value-kind classifier withcountCellsByKindvia the newly exportedclassifyCellValue(so the two never drift on edge cases likeDatevsduration).getRowValuesno longer derivesmaxColviaMath.max(...rowMap.keys()). The spread is limited by V8's argument-count cap (~125 k), whichgetRowValueswould silently blow past on a dense row near MAX_COL — the call now uses a linear scan to derive the max.normalizePathpicks up an explanatory comment for the..-when-out-is-empty case, so the next reader doesn't have to second-guess whether path-traversal is possible (it isn't — the archive lookup catches escape attempts naturally). -
#78
2a46931Thanks @baseballyama! - fix: harden the ZIP64 read path against decompression bombs.Previously, archives whose End-of-Central-Directory carried ZIP64 sentinel values fell back to
fflate.unzipSync, which inflates every entry up front. A crafted ZIP64-shaped xlsx could exhaust memory before the per-entry / archive-total caps ran. The random-access reader now parses ZIP64 EOCD + the Zip64 Extended Information extra field directly, so the existing decompression-bomb guards apply to ZIP64 archives the same way they apply to ZIP32.loadWorkbook(decompressionLimits)defaults are unchanged; only the underlying enforcement path is stricter.
xlsx-kit@0.7.1
Patch Changes
- #76
e646e1cThanks @baseballyama! -addAutoFilterColumn,makeHyperlink, andsetPrintTitlesnow throw
OpenXmlSchemaErrorinstead of the genericErrorwhen their preconditions
are violated. Existing catch blocks that checkerr instanceof OpenXmlError
now match these errors uniformly with the rest of the library.
xlsx-kit@0.7.0
Minor Changes
-
#74
fb57baeThanks @baseballyama! - Harden the reader against decompression-bomb attacks and tighten release
hygiene ahead of1.0:loadWorkbook/loadWorkbookStreamnow apply adecompressionLimits
guard by default (per-entry size cap, total archive cap, compression-ratio
cap). The newOpenXmlDecompressionBombError(a subclass of
OpenXmlIoError) is thrown when an archive trips the limit. Pass
decompressionLimits: falseto disable, or supply a partial override to
tighten or loosen specific bounds.saveWorkbook/workbookToBytesnow validate sheet titles against
Excel's rules (1–31 chars, forbidden: \ / ? * [ ], no leading/trailing
apostrophe, reservedHistory, case-insensitive uniqueness) at save time,
catching invalid names that were introduced by direct mutation of
ws.titleafteraddWorksheet.size-limitnow tracks the minified parse size (no brotli, no gzip) of
xlsx-kit/streamingandxlsx-kit/ioin addition to the existing
min+brotli budgets, so transitive bundle growth (e.g. the stylesheet
writer chunk) is caught at PR time.- New
SECURITY.mddocuments the supported versions, the private security
advisory reporting process, anddecompressionLimitsrecommendations for
consumers. - New
CONTRIBUTING.md, GitHub Issue / PR templates, a
template-complianceworkflow, and a project-specificCLAUDE.mdfor
contributors and AI agents working in the repository. docs/migrate-from-openpyxl.mdrealigned to the 0.6.x API surface
(iterRows,setCellByCoord,addWorksheetreturning an empty workbook,
ZIP64 entry-count support, the current passthrough part list).
xlsx-kit@0.6.0
Minor Changes
-
#71
551901cThanks @baseballyama! - Re-export DML colour, fill, and text-body primitives fromxlsx-kit/drawing. Chart styling reaches<a:srgbClr>/<a:solidFill>/<a:bodyPr>…<a:p>…<a:r>throughShapeProperties.fill,Series.spPr,Axis.txPr, etc., but the building blocks (DmlColor,DmlColorWithMods,Fill,TextBody,TextParagraph,RunProperties, …) and their constructors (makeColor,makeSrgbColor,makeSchemeColor,makeSolidFill,makeTextBody,makeParagraph,makeRun,makeRunProperties, …) previously had no public home. They now ship as part ofxlsx-kit/drawingalongsidemakeShapeProperties. Closes #55, closes #56. -
#72
80a06bfThanks @baseballyama! -AxisShared.majorGridlinesandAxisShared.minorGridlinesnow acceptboolean | Gridlinesinstead of justboolean. TheGridlinesshape carries aShapeProperties, so<c:majorGridlines><c:spPr><a:ln>…</a:ln></c:spPr></c:majorGridlines>can be emitted to colour / dash / weight the gridline (e.g. corporate-style light greyD9D9D9). The plaintrueform keeps emitting<c:majorGridlines/>so all existing call sites stay unchanged. Round-trip throughparseChartXmlis preserved for both forms. Closes #57.
Patch Changes
-
#69
2317545Thanks @baseballyama! - Rename the chart-internalNumberFormatinterface toChartNumberFormatand re-export it fromxlsx-kit/chart. The interface was already part of the public surface throughAxisShared.numFmtandDataLabelList.numFmt, but the type itself was not exported — callers building axis / data-label options had to write the literal inline. The new name also disambiguates from the cell-stylesheetNumberFormatexported fromxlsx-kit/styles, which is a different shape ({ numFmtId, formatCode }). Closes #58. -
#68
060f436Thanks @baseballyama! - Harden DrawingMLFillserializer against two natural mis-uses. (1) Passing a colour withoutmods(e.g.{ base: { kind: 'srgb', value: 'FF0000' } }instead of{ base, mods: [] }) no longer crashes the chart serializer withCannot read properties of undefined (reading 'map'); the missing modifier list is now treated as empty. (2) Passing aFillwith an unknownkind(e.g.'solid'instead of'solidFill') used to silently emit an empty<c:spPr></c:spPr>and lose the caller's styling intent; the serializer now throwsOpenXmlSchemaErrorso the mistake surfaces immediately. -
#73
f376354Thanks @baseballyama! - Document the small set of openpyxl → xlsx-kit defaults that differ, in particular thatcreateWorkbook()returns an empty workbook with no sheets (unlikeopenpyxl.Workbook()which creates a default'Sheet'). Direct ports of openpyxl code that include awb.remove(wb.active)call afterWorkbook()were translating that into a no-opremoveSheet(wb, 'Sheet')— the new README "Migrating from openpyxl" subsection calls this out alongside thesetCellandmakeBorder/makeSideequivalents. Closes #62.
xlsx-kit@0.5.0
Minor Changes
- #66
afecdc3Thanks @baseballyama! - Remove the silently-ignoredreadOnly/keepLinks/keepVba/dataOnly/richTextplaceholders fromLoadOptions. They were declared on the public surface but the loader (src/io/load.ts) accepted them via_optsand dropped them on the floor, so production callers expectingdataOnly: trueto suppress formulas — orreadOnly: trueto enable a special path — got the default behaviour instead.LoadOptionsis now an empty type until the underlying behaviour ships; future toggles will land here once they actually do something. TheloadWorkbook(source, opts)signature is unchanged.
Patch Changes
-
#64
b613607Thanks @baseballyama! - Treat sheet names as case-insensitive for uniqueness, matching Excel. PreviouslyaddWorksheet(wb, 'Data')followed byaddWorksheet(wb, 'data')succeeded locally but produced a workbook Excel and LibreOffice refuse to open.addWorksheet,addChartsheet,duplicateSheet,renameSheet, andpickUniqueSheetTitlenow compare titles case-insensitively. A case-only rename of the same sheet (renameSheet(wb, 'Data', 'data')) is allowed. -
#51
1cf8d0cThanks @baseballyama! - Tighten the streaming I/O surface so the README's "fixed-memory" claims hold up
in practice.toFile().toBytes().finish()no longer re-reads the just-written file. The
previous code calledfs.readFile(path)fromfinish()and returned the
full archive bytes, defeating the chunk-streamed write — a 10M-row workbook
ended its save by reloading the entire output into memory.finish()now
resolves with an emptyUint8Arrayonce the underlying write stream has
flushed; callers that need the bytes shouldfs.readFile()the path
themselves.toFileandtoWritablehonour write-stream backpressure: whenwrite()
returnsfalse, subsequent chunks chain off adrainevent before
proceeding, so peak memory tracks the writable'shighWaterMarkrather
than the producer's pace.workbookToBytesno longer depends onBuffer. Browser bundles that omit
the NodeBufferpolyfill previously broke attoBuffer().result()
because the in-memory sink ended its result withBuffer.from(...). The
helper now uses aUint8Array-only sink; a regression test
(tests/phase-1/io/browser.test.ts) saves a workbook withglobalThis.Buffer
shadowed toundefined.- The streaming read path inflates worksheet entries chunk-by-chunk. A new
ZipArchive.readStream(path)returns aReadableStream<Uint8Array>that
drives fflate'sInflateincrementally, andloadWorkbookStream's
whole-sheetiterRows()feeds the SAX parser directly off that stream so
the inflated worksheet body is never fully resident. Band queries
(minRow > 1) still materialise the inflated sheet to build the row-offset
index — that trade-off is unchanged. - Documentation: the
XlsxSink/BufferedSinkWriterJSDoc no longer
describestoBytes()as the "buffered mode" — that name was historical;
the underlying object can either accumulate (buffered sinks) or forward
chunks as they arrive (streaming sinks). The README also clarifies that
the streaming reader still loads the compressed archive up front (ZIP
needs random access to the central directory) — the win is that the
inflated worksheet payload is never fully resident.
-
#63
fa73fc5Thanks @baseballyama! - Tighten sheet-title validation on the streaming write path. ThecreateWriteOnlyWorkbookaddWorksheetcall now applies the same rules as the bufferedaddWorksheet(no: \ / ? * [ ], no leading / trailing apostrophe, not the reserved nameHistory) and rejects duplicate titles case-insensitively so the streaming path can't produce a workbook Excel refuses to open.
xlsx-kit@0.4.0
Minor Changes
-
f9f273dThanks @baseballyama! - Expose the full ECMA-376 axis attribute surface onCategoryAxisand
ValueAxis. Previously the serializer emitted fixed defaults for several
elements; these are now driven by typed fields, unblocking horizontal-bar
reversal (scaling.orientation: 'maxMin'), 100 %-stacked axis caps
(scaling.max), value-axis crossing rules, custom tick formatting, axis
titles, and more.Newly exposed shared fields:
scaling(orientation/min/max/logBase),
crosses,crossesAt,numFmt,majorTickMark,minorTickMark,
tickLblPos,title,minorGridlines.ValueAxisgainscrossBetween,
majorUnit,minorUnit.CategoryAxisgainsauto,lblAlgn,
lblOffset,noMultiLvlLbl. All previously-emitted defaults remain the
output when fields are unset, so existing files are unchanged.New type exports:
AxisCrossBetween,AxisCrosses,AxisOrientation,
AxisScaling,CategoryLabelAlignment,TickLabelPosition,TickMark.Closes #46.
-
2e5e460Thanks @baseballyama! - Exposeoverlap?: numberonBarChart(andmakeBarChart). The serializer
now emits<c:overlap val="N"/>(range -100..100) inside<c:barChart>when
set, unblocking flush stacking (overlap: 100) and negative-space clustered
bars. When unset, the serializer continues to emit the prior default of
<c:overlap val="100"/>forstacked/percentStackedgrouping so existing
output is unchanged.Closes #45.
-
19e8368Thanks @baseballyama! - Exposestyle?: numberonChartSpace(andmakeChartSpace). The serializer
emits<c:style val="N"/>(range 1..48) between<c:roundedCorners>and
<c:chart>, selecting one of Excel's built-in "Chart Styles" gallery presets
— the same single attribute openpyxl writes viachart.style = N.Closes #48.
-
1541291Thanks @baseballyama! - AddDateAxisandSeriesAxistypes anddateAx?/serAx?slots on
PlotArea.DateAxiscarriesauto,lblOffset,baseTimeUnit,
majorUnit,majorTimeUnit,minorUnit,minorTimeUniton top of the
shared axis surface — unblocking time-series charts (<c:dateAx>).
SeriesAxisaddstickLblSkipandtickMarkSkip, used by surface charts
(<c:serAx>). The serializer emits both inside<c:plotArea>between the
inferred cat/val axes and<c:spPr>; the parser round-trips them.New type exports:
DateAxis,SeriesAxis,TimeUnit. -
0708aa8Thanks @baseballyama! - AddLayout/ManualLayouttypes and exposelayout?: Layouton
ChartTitle,PlotArea, andLegend. The serializer emits
<c:layout><c:manualLayout>withlayoutTarget,xMode/yMode/
wMode/hMode, andx/y/w/hwhen set, falling back to the
existing empty<c:layout/>placeholder when unset — so output is unchanged
for charts that don't configure manual layout. Parser round-trips both
forms.New type exports:
Layout,LayoutMode,LayoutTarget,ManualLayout. -
0989eecThanks @baseballyama! - Expose per-pointdPt?: DataPoint[]onBarSeries(used by bar / line /
area / pie / doughnut / radar / stock / surface),ScatterSeries, and
BubbleSeries, with the newDataPointtype carryingidx,
invertIfNegative?,marker?,bubble3D?,explosion?, andspPr?.
The serializer emits<c:dPt>children between the series'
<c:marker>/<c:spPr>and<c:dLbls>per ECMA-376 sequence — unblocking
per-slice colours on pie / doughnut charts, per-bar colours on single-series
bar charts, and per-point styling on line / scatter / bubble.Closes #44.
-
7f9e143Thanks @baseballyama! - AddinvertIfNegative?: booleanandexplosion?: numbertoBarSeries
(used by bar / line / area / pie / doughnut / radar / stock / surface) and
invertIfNegative?: booleantoBubbleSeries. The serializer emits
<c:invertIfNegative>and<c:explosion>between<c:spPr>and<c:dPt>
per ECMA-376 sequence — unblocking per-series colour inversion on negative
values and pie/doughnut slice explosion at the series level (in addition to
the per-pointDataPoint.explosion). -
ffa777cThanks @baseballyama! - Exposemarker?: MarkeronLineSeriesandScatterSeries(with the new
Marker/MarkerSymboltypes). The serializer emits<c:marker>between
the series'<c:spPr>and<c:dLbls>per ECMA-376 sequence, carrying
<c:symbol>,<c:size>, and an optional nested<c:spPr>for marker
fill / line colour — matching openpyxl'sseries.marker = Marker(...).Closes #47.
-
70a2f17Thanks @baseballyama! - ExtendStockChart.hiLowLinesandStockChart.upDownBarsto accept a
detailed object form in addition to the existing boolean flag. The
detailed form lets callers style the lines (HiLowLines.spPr) and the
up/down bars (UpDownBars.gapWidth+upBars.spPr+downBars.spPr)
with per-element shape properties.The boolean form (
hiLowLines: true) keeps its existing meaning and
output, so existing callers are unaffected. Parser round-trips both
forms, picking the boolean form when no detail is found.New type exports:
BarFrame,HiLowLines,UpDownBars. -
7cd181cThanks @baseballyama! - Addview3D?: View3Dandfloor?/sideWall?/backWall?(typed
SurfaceFrame) toChartSpace(andmakeChartSpace). The serializer
emits<c:view3D>(withrotX,rotY,depthPercent,hPercent,
rAngAx,perspective) and<c:floor>/<c:sideWall>/<c:backWall>
(withthicknessandspPr) between<c:autoTitleDeleted>and
<c:plotArea>per ECMA-376 sequence — unblocking real 3-D chart viewpoints
and wall styling forbar3DChart/line3DChart/pie3DChart/
area3DChart/surface3DChart.
xlsx-kit@0.3.1
Patch Changes
- #41
a04f645Thanks @baseballyama! - Relaxengines.nodefrom>=24.15.0back to>=22.0.0so the published
package installs on every active Node LTS line (22.x, 24.x) plus current
(26.x), matching the CI matrix. 0.3.0 inadvertently shipped a Node 24+
floor that excluded the still-supported 22.x LTS; this restores broader
LTS coverage. The library does not rely on any Node 24-only API.
xlsx-kit@0.3.0
Minor Changes
-
#32
87a0051Thanks @baseballyama! - Breaking:iterRows/iterValues(inxlsx-kit/worksheet) now
iterate rectangularly over the populated bounding box rather than
skipping empty rows and gaps.iterRowsyields
(Cell | undefined)[](one entry per[minCol, maxCol]position);
iterValuesyieldsCellValue[]withnullfilling the gaps.Default extent switches from
MAX_ROW/MAX_COL(the 1M × 16K sheet
limit) togetMaxRow(ws)/getMaxCol(ws)(the populated bounding
box). TheIterRowsOptions.valuesOnlyflag is removed — it was already
unread.Migration:
- Aggregation callers that want populated rows only:
[...iterRows(ws)].filter((row) => row.some((c) => c !== undefined)). - Cell-by-cell streaming over populated cells only: keep using
iterCells
(unchanged).
Closes #24.
- Aggregation callers that want populated rows only:
-
#31
9297c46Thanks @baseballyama! - ExtendcellValueAsString(inxlsx-kit/cell) with optional
dateFormat/emptyTextoverrides and add a sibling
cellValueAsPrimitivethat maps aCellValueto the most natural JS
primitive (string | number | boolean | Date | null) without forcing a
single target type. Closes #25. -
#30
78d04fdThanks @baseballyama! - AddworkbookToBuffertoxlsx-kit/node. One-shot Node-flavored helper
that returns aBufferdirectly, paralleling the existingfromBuffer
source. Closes #28.
xlsx-kit@0.2.0
Minor Changes
-
b36ca45Thanks @baseballyama! - Hardening and docs release.- Add a 3-tier ECMA-376 conformance validator and broaden conformance coverage to the writer surface, real-world fixtures, and fast-check property tests.
- Add
knipto CI to keep the public export surface tight; prune unused exports flagged by it. - Refresh the docs site: redesigned landing and docs UI with a new typography system, new logo and favicons, and new "Why xlsx-kit" / comparison / motivation sections in the README.
- Tighten release / dependency automation: pin dependencies, drop EOL Node 18/20 from the test matrix and add Node 26, bump the project Node engine to 22.22.2.