exceljs compatibility (@xlvane/compat)
@xlvane/compat offers the commonly used part of the exceljs 4.4.0 API on top of xlvane, so that code written for exceljs keeps working after one change in package.json:
"dependencies": {
"exceljs": "npm:@xlvane/compat@^1"
}
npm installs an alias <alias>@npm:<name> under the alias name in node_modules (npm/cli docs/lib/content/using-npm/package-spec.md, "Aliases"), so require('exceljs') and import ExcelJS from 'exceljs' load @xlvane/compat.
- ESM:
import ExcelJS from 'exceljs'works as with exceljs.@xlvane/compatis an ES module, so named imports (import { Workbook } from 'exceljs') work too; exceljs 4.4.0 rejects them because it is a CommonJS package. - CommonJS:
const ExcelJS = require('exceljs')needs Node.js 22.12 or later. The package is ESM-only and exports theExcelJSobject under the name"module.exports", whichrequire()of an ES module returns directly from Node.js 22.12 on (Node.jsdoc/api/modules.md, "Loading ECMAScript modules usingrequire()").engines.nodeis>=22.12. - Unsupported API throws an
XlvErrorwith codeXLV_E_NOT_SUPPORTED, naming the method (docs/errors.md). Nothing is dropped silently.
Status: supported means the same behaviour as exceljs for the cases covered by the ported tests (tests/exceljs/). Partly means supported with the difference given in the notes. Not supported means it throws XLV_E_NOT_SUPPORTED. The source column points to the exceljs 4.4.0 implementation (paths relative to the package root, as installed in bench/node_modules/exceljs).
Workbook
| API | Status | Notes | Source in exceljs |
|---|---|---|---|
new Workbook() | supported | lib/doc/workbook.js | |
creator, lastModifiedBy, created, modified, lastPrinted, title, subject, keywords, category, description, language, revision, contentStatus | supported | Written to docProps/core.xml with exceljs's defaults (creator and lastModifiedBy "Unknown", created and modified the time the workbook was made), and read back; revision reads back as a number. Dates are stored in whole seconds (UTC). | lib/doc/workbook.js L16–27, L153–180; lib/xlsx/xform/core/core-xform.js |
company, manager | supported | Written to docProps/app.xml as exceljs writes them: Company always ('' when not set), Manager when not undefined; read back from the file (undefined when missing). exceljs also writes Application "Microsoft Excel" and AppVersion "16.0300"; xlvane does not claim an application (D-A10), which exceljs does not read back. | lib/xlsx/xform/core/app-xform.js L24–37; lib/xlsx/xlsx.js L345–349 |
addWorksheet(name, { state, views, properties }), worksheet.properties | partly | tabColor (and the deprecated worksheet.tabColor), defaultRowHeight, outlineLevelRow, outlineLevelCol and outlineProperties (summaryBelow, summaryRight) are written and read back (X4-28). outlineLevelRow/outlineLevelCol are written as at least the highest level of the rows/columns (exceljs writes the value as given), so they can read back higher than they were set, and collapsed of rows and columns follows. Not written: dyDescent (a new worksheet has exceljs's default properties { defaultRowHeight: 15, dyDescent: 55, outlineLevelCol: 0, outlineLevelRow: 0 }, D-E06; dyDescent is an attribute of the x14ac extension namespace, which xlvane does not write; no source for its meaning was checked), defaultColWidth. | lib/doc/workbook.js L53; lib/xlsx/xform/sheet/sheet-properties-xform.js; lib/xlsx/xform/sheet/sheet-format-properties-xform.js |
getWorksheet(name | id) | supported | lib/doc/workbook.js L106 | |
worksheets, eachSheet(cb), worksheet.orderNo | supported | Sorted by orderNo as in exceljs: a new sheet gets one more than the largest orderNo, a loaded one its 0-based position in the file (D-R09). | lib/doc/workbook.js L81–85, L119–124, L127, L203 |
removeWorksheet(id), removeWorksheetEx(worksheet) | supported | lib/doc/workbook.js L95, L99 | |
views (workbook windows) | supported | Every view is written as a workbookView with exceljs's defaults (x 0, y 0, width 12000, height 24000) and firstSheet, activeTab, visibility when given; read back with them (X4-28). | lib/doc/workbook.js L28, L164; lib/xlsx/xform/book/workbook-view-xform.js |
calcProperties (fullCalcOnLoad) | partly | fullCalcOnLoad: true is written as calcPr fullCalcOnLoad="1" and read back. exceljs always writes calcPr calcId="171027"; xlvane writes calcPr only when fullCalcOnLoad is set and never a calcId (sml.xsd CT_CalcPr makes it optional; xlvane does not calculate, so it claims no calculation engine version; the meaning of calcId is not checked against Part 1 prose, PROVISIONAL). calcProperties reads back as { fullCalcOnLoad: true } or {}; exceljs reads {} in both cases. | lib/doc/workbook.js L24, L177; lib/xlsx/xform/book/workbook-calc-properties-xform.js |
model (get / set), nextId, clearThemes() | supported | model is a plain snapshot of the workbook that can be set on another workbook (it holds xlvane's worksheet models, not exceljs's internal xform models). clearThemes() does nothing: xlvane never keeps or writes a theme part. | lib/doc/workbook.js L43, L137, L153–215 |
xlsx.load(buffer, options) | supported | Reading goes through xlvane's limits for untrusted files. Options as in exceljs (X4-25): base64 reads base64 text; ignoreNodes skips worksheet elements by name (sheetPr, cols, sheetData, sheetViews, autoFilter, mergeCells, hyperlinks, dataValidations, conditionalFormatting, sheetProtection, pageSetup, headerFooter, rowBreaks, drawing, picture, tableParts); maxRows and maxCols refuse bigger worksheets with XLV_E_LIMIT_EXCEEDED (exceljs throws a plain Error). The same options apply to readFile and read. | lib/xlsx/xlsx.js L257; lib/xlsx/xform/sheet/worksheet-xform.js L95, L372 |
xlsx.readFile(path) | supported | lib/xlsx/xlsx.js L49 | |
xlsx.read(stream) | supported | lib/xlsx/xlsx.js L244 | |
xlsx.writeBuffer(options) | supported | useStyles: false writes no cell or column styles, dates keeping format 14, as exceljs. useSharedStrings: false writes inline strings and no shared string table; zip.compression: 'STORE' stores every part, zip.compressionOptions.level (0–9) sets the deflate level; other zip options are ignored (X4-25). The output stays deterministic. The same options apply to writeFile and write. | lib/xlsx/xlsx.js L602–603, L685 |
xlsx.writeFile(path) | supported | lib/xlsx/xlsx.js L666 | |
xlsx.write(stream) | supported | lib/xlsx/xlsx.js L643 | |
xlsx.createInputStream() | supported | Throws the deprecation error, as in exceljs 4. | lib/xlsx/xlsx.js L238 |
csv.read(stream, options), csv.readFile(path, options) | supported | Adds a worksheet, with exceljs's options (sheetName, dateFormats, map, parserOptions: delimiter, quote, escape, trim/ltrim/rtrim, ignoreEmpty, comment, skipLines, skipRows, maxRows, headers, renameHeaders, encoding) and its default conversion of fields (X4-23), on xlvane's own parser (no fast-csv). Accepts a Node.js readable stream, a Web ReadableStream or any (async) iterable of strings or bytes. Differences: text of white space only stays text (exceljs reads ' ' as 0), -Infinity stays text (it cannot be stored), a leading byte order mark is dropped (exceljs keeps it in the first field), headers: true skips the header line (exceljs throws data.map is not a function), a missing file or a malformed line throws XLV_E_FILE_READ / XLV_E_CSV_MALFORMED. readFile needs node:fs (Node.js, Bun, Deno). Dates are parsed as dayjs 1.11 customParseFormat parses them in strict mode (D-E06): letters that are no token, such as an unbracketed T or dddd, are skipped, and each token is searched in the rest of the text. An unclosed quote is reported on the line where it opens; a quote or escape longer than one character throws XLV_E_INVALID_OPTION. | lib/csv/csv.js L32–98 |
csv.write(stream, options), csv.writeFile(path, options), csv.writeBuffer(options) | supported | Writes one worksheet with exceljs's options (sheetName, sheetId, dateFormat, dateUTC, map, includeEmptyRows, encoding, formatterOptions: delimiter, rowDelimiter, quote, escape, quoteColumns, headers, writeHeaders, includeEndRowDelimiter, writeBOM) and fast-csv's quoting (X4-24); the cross-check sample gives the same text as exceljs 4.4.0. Differences: text that starts with =, +, -, @, tab, CR, LF or another control character, or with a full-width or small form of these signs, is written with a leading ' (CSV injection, docs/facts/csv.md CSV-10, CSV-12; formulaInjection: "off" writes it unchanged, as exceljs); a formula result of 0 or false is written (exceljs writes an empty field), a date result is formatted as a date and an error result as its text; one blank line per missing row also before the first row (exceljs writes one too few there); a sheetName or sheetId that does not exist throws XLV_E_SHEET_NOT_FOUND (exceljs writes an empty file); header names in formatterOptions.headers are escaped like values. With headers, an array row gives one field per header (longer rows cut, shorter ones end in empty fields), as fast-csv writes it; dateFormat: '' uses the default format, as dayjs does (D-E06). writeFile writes utf8, utf16le or latin1 and needs node:fs. | lib/csv/csv.js L109–186 |
csv.createInputStream() | supported | Throws the deprecation error, as in exceljs 4. | lib/csv/csv.js L101 |
definedNames (add, addEx, remove, removeEx, removeAllNames, forEach, getNames, getNamesEx, getRanges, getMatrix, model) | supported | As in exceljs, a name is a set of cells, written as rectangles with absolute references and no sheet scope; on reading, names that are not cell ranges (constants, formulas, #REF!) and the sheet scope (localSheetId) are dropped. Built-in _xlnm. names are not listed: print areas and titles are read into pageSetup, as in exceljs, and xlvane writes the autofilter name itself. Names exceljs accepts but Excel does not (for example A1 or names starting with _xl) throw XLV_E_INVALID_DEFINED_NAME when the workbook is written. | lib/doc/defined-names.js; lib/xlsx/xform/book/defined-name-xform.js; lib/xlsx/xform/book/workbook-xform.js L187 |
addImage(image), getImage(id) | supported | PNG, JPEG and GIF given as buffer, base64 (a data URL is accepted, as in exceljs) or filename; the format is recognised from the bytes, extension is kept but not used (exceljs writes image/jpg for extension: 'jpg'). As in exceljs, a filename is read when the workbook is written (xlsx.writeBuffer/writeFile/write and the streaming WorkbookWriter), so a missing file fails there; outside Node.js, Bun and Deno it gives XLV_E_NOT_SUPPORTED. Other formats throw XLV_E_INVALID_IMAGE. Read back, each image part is one media entry { type, name, extension, buffer }, buffer a Uint8Array. Identical images are stored once, with one relationship per drawing (exceljs writes one per use). | lib/doc/workbook.js L142–151; lib/xlsx/xlsx.js L421–442 (L426–429: filename read at write) |
Worksheet
| API | Status | Notes | Source in exceljs |
|---|---|---|---|
name, id, state | partly | Ids are given as in exceljs (the first free id), written as sheetId and kept on loading and by the model setter, as exceljs does, so getWorksheet(id) finds the same sheet after removing a sheet, writing and loading (D-F03). Differences: an id outside 1 to 65534, the range Office uses (W2-20), is written as the lowest unused sheetId; a file's missing, repeated or larger than 65534 sheetId gives the worksheet the lowest free id (exceljs reads NaN, lets the later sheet replace the earlier one, or uses the large number as an array index); the streaming WorkbookReader numbers worksheets 1, 2, 3… in file order, where exceljs uses the number N of the part xl/worksheets/sheetN.xml (lib/stream/xlsx/workbook-reader.js L107-109, L293-297). | lib/doc/worksheet.js L33; lib/doc/workbook.js L43–51; lib/xlsx/xform/book/sheet-xform.js L7, L18 |
columns (get / set { header, key, width, style }) | supported | As in exceljs, a column exists for every column a cell was created in, also after addRow and on a loaded file without <cols>, so columns.forEach(c => { c.eachCell(…); c.width = … }) works; columns without width, style, hidden flag or outline level are not written (D-R09). An empty sheet gives [] where exceljs gives null. | lib/doc/worksheet.js L203, L209, L243–261; lib/doc/row.js L48 |
getColumn(key | letter | number) | supported | lib/doc/worksheet.js L243 | |
addRow(values | object, style?), addRows | supported | Style 'i' / 'i+' copies the style of the row above, as in exceljs. | lib/doc/worksheet.js L406, L414 |
getRow(n), getRows(start, length) | supported | lib/doc/worksheet.js L388, L397 | |
getCell(ref | row, col) | supported | lib/doc/worksheet.js L592 | |
eachRow({ includeEmpty }, cb) | supported | lib/doc/worksheet.js L551 | |
getSheetValues() | supported | Every row object, also an empty one ([]), as in exceljs. | lib/doc/worksheet.js L571–579 |
rowCount, actualRowCount, columnCount | supported | lib/doc/worksheet.js L374, L378, L313 | |
mergeCells(range | t, l, b, r), unMergeCells | supported | lib/doc/worksheet.js L602, L656 | |
mergeCellsWithoutStyle(...), findRows(start, length), lastColumn, fillFormula(range, formula, results, shareType), model (get / set), destroy() | supported | fillFormula writes the formula into the top-left cell and { sharedFormula } (or the same array formula) into the others, with results from a function, a flat array or a 2-D array, as in exceljs (X4-29). findRows returns _rows.slice(start - 1, start - 1 + length) as exceljs does (D-E06). model is xlvane's own plain snapshot (see workbook.model). | lib/doc/worksheet.js L181, L309, L370, L607, L678–721, L842–903 |
views | partly | The first view is written: frozen panes, split panes (xSplit/ySplit in 1/20 point), zoomScale (10–400), zoomScaleNormal, showGridLines, showRowColHeaders, showRuler, rightToLeft, style (pageBreakPreview, pageLayout), activeCell, and topLeftCell of a normal view (X4-28). Differences: further views are not written, because xlvane's core writes one sheetView per worksheet; workbookViewId is always 0; frozen panes always get the cell right of and below the panes as topLeftCell (exceljs writes a frozen view's own topLeftCell when given, sheet-view-xform.js L50-53); a split view's topLeftCell is written as pane@topLeftCell and read back, as in exceljs (L72-85, L118-124; D-F02), and its activeCell is written and read back as the selection of the active pane; activePane is the pane the split implies; tabSelected is written for the first visible worksheet, whatever the view says. Every worksheet reads back with one view shaped as exceljs reads it. | lib/doc/worksheet.js L114; lib/xlsx/xform/sheet/sheet-view-xform.js; index.d.ts L33–149 |
autoFilter (range string) | supported | lib/doc/worksheet.js L116 | |
insertRow(pos, values, style?), insertRows(pos, values, style?) | partly | Style options n, i, i+, o, o+ as in exceljs. Built on spliceRows, so the differences below apply. | lib/doc/worksheet.js L422, L428, L444 |
spliceRows(start, count, ...inserts) | partly | xlvane moves rows as a spreadsheet application does; exceljs 4.4.0 moves only values, styles and heights (X4-15). Differences: row objects keep their identity and their number follows them (exceljs copies values between row objects); the hidden flag, notes and links move with the row (exceljs leaves hidden/outline at the position and drops notes); merged ranges move, grow when rows are inserted inside them (inserted cells get a copy of the top-left cell's style, the other cells keep their own), shrink when rows are removed, and are dropped when their top-left cell is removed or one cell or nothing is left, as LibreOffice does (R-14; exceljs keeps stale ranges and dissolves merges on removal); the autofilter range moves and shrinks, and stops at the last row or column (exceljs leaves it); cells inserted inside a defined name join it (exceljs leaves a gap), and names follow whatever the case of their sheet name (exceljs compares it exactly); styles are copied, never shared; removing more rows than follow the removed block works (exceljs leaves rows behind). Formulas in every worksheet follow the splice as in a spreadsheet application (xlvane adjustFormula, D-B01; exceljs leaves them unchanged); formulas given in the inserted rows keep the text the caller wrote. If a formula cannot be adjusted safely, the call throws XLV_E_NOT_SUPPORTED before anything changes. | lib/doc/worksheet.js L483–548 |
duplicateRow(rowNum, count, insert = false) | partly | The default of insert is false, as in exceljs's code (its README says true). Copies get the values, styles (copied, not shared) and height; merged ranges inside the row are copied too; a merged range's other cells stay merged instead of getting the master value (exceljs); a formula is copied as a spreadsheet application copies a cell: its relative references move by the distance of the copy (xlvane shiftFormula; exceljs copies the text). With insert = true the rows below move first and every formula follows (spliceRows), so a copy of A5 in row 1 reads A6 in row 1 and A7 in row 2. With insert = false the rows below are overwritten in place: merged ranges that touch them are removed and no other formula changes (exceljs implements this as a removal plus an insertion). | lib/doc/worksheet.js L463 |
spliceColumns(start, count, ...inserts) | partly | Each insert is an array of values by row (index 0 is row 1). Columns move with their cells, widths, styles, keys and notes; merged ranges, the autofilter, defined names and formulas follow, as for spliceRows. Unlike exceljs, falsy values such as 0 and false are kept, values beyond the existing rows are written, headers are not written again into row 1, and the last column does not get width 9. | lib/doc/worksheet.js L264–307 |
| Invalid arguments of the row and column methods | partly | Arguments exceljs accepts and then loses or misplaces data with (insertRow(0), insertRow('2'), a negative count, duplicateRow of a missing row or without a count, insertRows without an array, rows or columns pushed, or inserted, beyond row 1048576 or column XFD) throw XLV_E_INVALID_ARGUMENT before anything changes. | lib/doc/worksheet.js L422–548 |
addImage(id, range), getImages(), addBackgroundImage(id), getBackgroundImageId() | partly | Ranges 'B2:D6', { tl, br, editAs } and { tl, ext } as in exceljs (a range string and { tl, br } without editAs are written with editAs="oneCell", as exceljs does). Differences: a picture with ext is a one-cell anchor, which has no editAs in the schema (exceljs writes one), so it reads back as oneCell; picture tooltips (hyperlinks.tooltip) are not written; { tl } without br or ext throws XLV_E_INVALID_OPTION (exceljs fails with a TypeError); fractional corners are written with exceljs's offsets in EMU; no constant a16:creationId is written. Absolute anchors in read files are not listed by getImages() (exceljs drops them too). Pictures move with spliced rows and columns; a splice that removes a picture's corner throws XLV_E_NOT_SUPPORTED (exceljs leaves pictures in place). On a streamed worksheet addBackgroundImage works as in exceljs, and addImage too (exceljs has no addImage there); images must be added to the writer before the first committed row. | lib/doc/worksheet.js L724–749; lib/doc/image.js; lib/doc/anchor.js |
addTable, getTable, getTables, removeTable; Table (addRow, removeRows, getColumn, addColumn, removeColumns, commit, accessors) | partly | As in exceljs: the header, rows and totals are written into the cells at addTable and commit(); removeTable keeps the cells; column style is applied to the cells. Differences, following Excel's rules (D-D02, D-D04): names, column names and ranges are checked (XLV_E_INVALID_TABLE; exceljs accepts My Table and silently replaces a table of the same name), built-in names such as Print_Area are refused, and a table that overlaps the worksheet's autoFilter is refused when the workbook is written ([MS-OI29500] §2.1.680 h; exceljs writes both); a table needs a data row, so a table without rows gets one empty row in the file, and one with a totals row but no rows is refused; stdDev and var totals are SUBTOTAL(107…) and SUBTOTAL(110…) ([MS-OI29500] §2.1.1094; exceljs writes 106, PRODUCT, and 107); a first column with a totals function gets the function, not the label "Total"; custom totals are written with their totalsRowFormula element; no dxfId on table columns (not in the schema) and no autofilter without a header row; commit() clears its cache, so later changes are committed too; displayName and theme work (exceljs reads displyName and style.name); getTables() returns tables, not tuples. A table read from a file keeps its columns as read, as in exceljs (table.model, without validation): a column without a totals function has no totalsRowFunction and the first column gets no default "Total" label (lib/doc/worksheet.js L917–920; table-column-xform.js L29). Read back, a table has ref, rows and its worksheet, so it can be changed and written again (exceljs loses them). Tables do not move with spliceRows/spliceColumns (as in exceljs): a splice at or before a table's last row or column throws XLV_E_NOT_SUPPORTED. The streaming worksheet has no addTable (as in exceljs). | lib/doc/worksheet.js L790–807; lib/doc/table.js; lib/xlsx/xform/table/* |
dataValidations (add, find, remove, model), cell.dataValidation | supported | Types, operators, formulae, messages and allowBlank are written and read back, also by the streaming WorkbookWriter; formulae read back typed as exceljs reads them (whole and textLength numbers, decimal numbers, date Dates, others strings); a formula of those types that is not a number, such as $C$1, stays text (exceljs gives NaN; D-R01). Differences: remove() deletes the entry (exceljs leaves an empty one and writes a validation without a type); equal validations of single cells are written as ranges that never overlap (exceljs's merging can write overlapping ranges); find() and cell.dataValidation also answer for a cell inside a range rule before the file is saved; a range of more than 100,000 cells (such as a whole column) keeps its range as the key after reading instead of one entry per cell; whole-column and whole-row ranges are written in A1 form (C1:C1048576). Validations Excel does not accept throw XLV_E_INVALID_OPTION when the workbook is written: an errorStyle other than stop, warning, information or error ("error", which exceljs's sample data uses and writes as given although it is no value of ST_DataValidationErrorStyle, is written as the default, stop; D-R01), a type other than any without formulae[0], between/notBetween without formulae[1]. A second formula is written only with between/notBetween, and list and custom validations get no operator ([MS-XLSX] §2.6.5). | lib/doc/data-validations.js; lib/xlsx/xform/sheet/data-validations-xform.js |
addConditionalFormatting({ ref, rules }), removeConditionalFormatting(index | filter | other), conditionalFormattings | partly | expression, cellIs, top10, aboveAverage, colorScale, iconSet (main icon sets), containsText (operators containsText, containsBlanks, notContainsBlanks, containsErrors, notContainsErrors) and timePeriod rules with their styles are written and read back; the formulas exceljs generates for text and timePeriod rules are generated the same way. Differences: the text of a containsText rule is escaped in the formula (quotes doubled) and written as the text attribute (exceljs writes it unescaped and drops it); a containsText rule without an operator is written as containsText (exceljs writes a rule without a type); rules without a priority get unique priorities (exceljs can repeat one); a rule's number format reads back as its code string (exceljs gives { id, formatCode }, and loses it when the file is saved again); styles are deduplicated. x14 data bars and icon sets (X4-20, D-C07): a data bar without gradient: true, the icon sets 3Triangles, 3Stars, 5Boxes and NoIcons, and custom icons are written in the [MS-XLSX] extension and read back in exceljs's shape. Differences: x14 ids are derived from the sheet and rule position (exceljs: random); a bar without gradient keeps exceljs's meaning (gradient fill, lengths 0 and 100) but gets no axis unless axisColor is given, because [MS-XLSX] §2.6.30 requires an axis colour for every axis and exceljs writes none (exceljs writes an invalid axis); borderColor, negativeFillColor and negativeBorderColor are written only when border or the matching …SameAsPositive: false asks for them, as §2.6.30 requires (exceljs writes them whatever the flags say); border: true without borderColor throws XLV_E_INVALID_OPTION; extension properties of a bar with gradient: true are written instead of dropped; custom icons keep their iconId (exceljs replaces it with the position) and read back with custom: true (exceljs drops the flag, so a read rule loses its icons when written again); reverse is not written with custom icons (§2.6.28); NoIcons is written in the extension (exceljs writes it in the main element, where ST_IconSetType has no such value); autoMin/autoMax appear only in the extension (the main element gets min/max). | lib/doc/worksheet.js L811–822; lib/xlsx/xform/sheet/cf/cf-rule-xform.js |
protect(password, options), unprotect(), sheetProtection | partly | The options are written as exceljs writes them (an option of true allows the action) and read back. A password is stored as exceljs stores it: a SHA-512 verifier with a random 16-byte salt and spinCount iterations (default 100000; xlvane hashPassword, SR-50 … SR-53), and it is kept when a loaded file is saved again; verifyPassword from xlvane checks a password against sheet.protection. Differences: the options object is not modified (exceljs rounds its spinCount in place), and a password longer than 255 characters throws XLV_E_INVALID_OPTION ([MS-OFFCRYPTO] §2.4.2.4 limit; exceljs accepts it). spinCount is ignored without a password, as in exceljs. | lib/doc/worksheet.js L753–787; lib/xlsx/xform/sheet/sheet-protection-xform.js |
pageSetup (margins, orientation, paper size, scale, fit to page, page order, black and white, draft, comments, errors, first page number, copies, dpi, showGridLines, showRowColHeaders, centring, printArea, printTitlesRow, printTitlesColumn), headerFooter, row.addPageBreak(), rowBreaks | supported | exceljs's defaults (lib/doc/worksheet.js L64–106), and page margins and page setup written for every sheet, as exceljs does. Differences: cellComments: 'asDisplayed' is written (exceljs writes only its own spelling 'asDisplyed', which xlvane also accepts); several print areas are written as one name (exceljs writes one name per area); an apostrophe in the sheet name is doubled in the print names (exceljs leaves it, and reading such a file fails in exceljs); print titles are recognised as rows or columns by their form, as exceljs reads them; row breaks are read back (exceljs drops them), with max 16383 (exceljs writes 16838); a paper size of 1 (the default) reads back as undefined; usePrinterDefaults is not written. | lib/doc/worksheet.js L64–106; lib/xlsx/xform/sheet/page-setup-xform.js; lib/xlsx/xform/book/workbook-xform.js L37–84, L187–231 |
Row
| API | Status | Notes | Source in exceljs |
|---|---|---|---|
number | supported | lib/doc/row.js L19 | |
values (get / set; sparse, 1-based) | supported | lib/doc/row.js L166, L177 | |
getCell(col) | supported | lib/doc/row.js L56 | |
eachCell({ includeEmpty }, cb) | supported | As in exceljs, cells of a merged range give the master cell's value. | lib/doc/row.js L130 |
height, hidden | supported | lib/doc/row.js L310 | |
outlineLevel, collapsed, protection, getCellEx(address) | supported | outlineLevel (0–7, [MS-OI29500] §2.1.644) is written and read back; a level outside 0–7 throws XLV_E_INVALID_ARGUMENT when it is set (exceljs writes it; D-E06); collapsed is derived from the level and properties.outlineLevelRow, as in exceljs (X4-28). | lib/doc/row.js L45, L286–330 |
font, fill, border, alignment, numFmt setters | supported | Apply to the row's cells. | lib/doc/row.js L274, L306 |
commit() | supported | Meaningful for WorkbookWriter; does nothing in a normal workbook, as in exceljs. | lib/doc/row.js L29 |
cellCount, actualCellCount | supported | lib/doc/row.js L216, L220 | |
splice(start, count, ...inserts) | partly | Cells move with their values, styles and notes, as in exceljs. A row that crosses a merged range at or right of start throws XLV_E_NOT_SUPPORTED (the range cannot follow the cells of one row; exceljs gives wrong values). Removed cells at the end are removed, not set to null. Formulas are not rewritten, as in exceljs. | lib/doc/row.js L77–127 |
Column
| API | Status | Notes | Source in exceljs |
|---|---|---|---|
key, header, width, hidden | supported | lib/doc/column.js L77, L92, L108 | |
header as an array, headers, headerCount | supported | Multi-row headers as in exceljs. | lib/doc/column.js L73–81, L164 |
outlineLevel, collapsed, protection, isDefault, toString(), equivalentTo(other) | supported | outlineLevel (0–7) is written and read back, and a level outside 0–7 throws XLV_E_INVALID_ARGUMENT when it is set (D-E06); collapsed is derived from the level and properties.outlineLevelCol, as in exceljs (X4-28). | lib/doc/column.js L116–147, L238 |
letter, number | supported | lib/doc/column.js L23, L31 | |
values | supported | lib/doc/column.js L179 | |
eachCell({ includeEmpty }, cb) | supported | lib/doc/column.js L168 | |
font, fill, border, alignment, numFmt setters, style | supported | Applied to the column's existing cells, inherited by cells added later (the row style wins), and written as the column's default style (col@style). As in exceljs, a styled column without a width is written with width 9, and a column with width 9 and nothing else is not written. The streaming writer writes the column style only with useStyles: true. | lib/doc/column.js L147, L206, L265 |
Cell values
| Value | Status | Notes | Source in exceljs |
|---|---|---|---|
null, number, string, boolean | supported | lib/doc/cell.js L206, L211 | |
Date | supported | Without numFmt, a date (also a formula result that is a date) is written with built-in format 14, as exceljs does, also by the streaming writer without useStyles; applications show it in the reader's date order, and reading gives numFmt "mm-dd-yy". | lib/doc/cell.js L538; lib/xlsx/xform/style/styles-xform.js L253–266, L498–515 |
{ formula, result } | supported | xlvane does not calculate; result is stored as given. | lib/doc/cell.js L734 |
{ sharedFormula, result }, { formula, shareType, ref, result }, formulaType | supported | Shared formulas are written as t="shared" groups: the master's ref is its own ref or grows to its last dependent, as in exceljs; array formulas (shareType: 'array') are written with their range (X4-29). Read back as exceljs reads them: the master { formula, shareType: 'shared', ref, result }, a dependent { sharedFormula, result } whose cell.formula is the translated formula. formulaType is 0 (none), 1 (master) or 2 (shared). The streaming WorkbookWriter writes a dependent of a master with ref as a shared formula, and any other { sharedFormula } cell as its translated formula, which needs the master's row to be uncommitted. Differences: xlvane refuses (XLV_E_INVALID_VALUE) what exceljs writes against the schema rules of Part 1 §18.3.1.40: overlapping shared ranges, a dependent outside its master's range, and a formula in another cell of an array formula's range. A splice (spliceRows, spliceColumns, insertRow(s), duplicateRow with insertion) turns every shared group of the workbook into plain formulas, each adjusted for the splice, because moving the master's text to each dependent would give wrong references; array formulas keep their group and their range moves (D-E06). A $ in a shared master's ref (exceljs fillFormula over an absolute range) is read as the same range without $. | lib/doc/cell.js L744, L811; lib/xlsx/xform/sheet/cell-xform.js L94–160, L460–470 |
| A style on a cell without a value | supported | Written and read back: xlvane's reader keeps cells that hold only a style (D-E05). | lib/xlsx/xform/sheet/cell-xform.js |
{ error: '#N/A' } | supported | lib/doc/cell.js L970 | |
| Other objects and arrays (JSON values) | supported | Kept on the cell with type String and written as their JSON text, as exceljs; read back as that text. | lib/doc/cell.js L1014–1054 |
{ richText: [...] } | supported | Written and read back with all run font properties of exceljs (name, size, family, scheme, charset, bold, italic, underline, strike, outline, vertAlign, color, shadow, condense, extend); cell.text joins the runs and type is RichText. A 6-digit argb colour reads back with alpha FF (8 digits). | lib/doc/cell.js L494; lib/xlsx/xform/strings/rich-text-xform.js; index.d.ts L336–343 |
{ text, hyperlink, tooltip } | partly | Written and read back; type is Hyperlink, cell.hyperlink the target and cell.text the text (also rich text). Differences, because xlvane follows the specification (D-A08): a hyperlink starting with # is written as a place in the workbook (location, no relationship), where exceljs writes every link as an external relationship; the tooltip is read back, which exceljs drops; links stored only as a location (as Excel writes links inside a workbook) are read as # + location, which exceljs drops. Limits of [MS-OI29500] apply (XLV_E_INVALID_HYPERLINK, e.g. a tooltip over 255 characters). The streaming WorkbookWriter writes links with the row they are in; the streaming WorkbookReader ignores them, as exceljs does by default. | lib/doc/cell.js L582–666, L1078; lib/xlsx/xform/sheet/hyperlink-xform.js; index.d.ts L345 |
note (string or { texts, margins, protection, editAs }) | supported | Written and read back, also by the streaming WorkbookWriter; every note is written with the author "Author" and with exceljs's default box merged with the note's margins (centimetres, written ×10 as mm), protection and editAs (D-E07, X4-31), and read back from the VML. A note of one run without a font reads back as a string, as in exceljs. Differences: the raw anchor text exceljs adds on reading is not given, and the box position (x:Anchor) follows the ECMA-376 example rather than exceljs's; a note on a cell without a value is kept, where exceljs loses it on reading. The streaming WorkbookReader ignores notes, as exceljs does. protection.locked/lockText read back as 'True'/'False' (the VML values Excel writes); exceljs reads back the text it wrote, so a note written by exceljs with locked: true reads back as 'true' there (vml-client-data-xform.js L31–33, L90–91; vml-protection-xform.js L14, L28). | lib/doc/cell.js L224–230; lib/doc/note.js L51–64; lib/xlsx/xform/comment/comments-xform.js L29–31; vml-textbox-xform.js, vml-client-data-xform.js |
type (ValueType numbers) | supported | Same numbers as exceljs: Null 0, Merge 1, Number 2, String 3, Date 4, Hyperlink 5, Formula 6, SharedString 7, RichText 8, Boolean 9, Error 10. | lib/doc/enums.js L4–16; lib/doc/cell.js L142 |
text, address, row, col | supported | lib/doc/cell.js L123, L232 | |
isMerged, master | supported | lib/doc/cell.js L165, L190 | |
name, names, addName, removeName, removeAllNames, fullAddress | supported | Through workbook.definedNames. | lib/doc/cell.js L270–310 |
toCsvString(), model (get / set), workbook, isMergedTo(master), merge(master), unmerge() | supported | toCsvString() as in exceljs: text and rich text in double quotes with " doubled, dates in ISO form, booleans 1/0, a link's target, a formula's result (empty for none), an error's text (X4-30). model is xlvane's own plain snapshot of the cell. | lib/doc/cell.js L36, L150, L169–187, L329–338, L395–1049 |
Styles
| API | Status | Notes | Source in exceljs |
|---|---|---|---|
numFmt | supported | Codes that exceljs maps to built-in formats ('0.00', 'mm-dd-yy', '@', …) are written as the built-in id, as exceljs does. | lib/doc/cell.js L51; lib/xlsx/defaultnumformats.js; lib/xlsx/xform/style/styles-xform.js L374 |
font | supported | All exceljs font properties are written and read back (vertAlign, family, scheme, charset, outline, shadow, condense, extend too; X4-27). A font without name or size is written with the workbook's default font for the missing ones (Calibri 11), so it reads back with them; exceljs writes only what is given. A single underline (underline: true or 'single') is written as <u/> and reads back as true, as in exceljs (underline-xform.js L14–30, L40–45); a file with <u val="single"/>, which exceljs reads as 'single', also reads back as true here. | lib/doc/cell.js L59; lib/xlsx/xform/style/font-xform.js; index.d.ts L217 |
fill (pattern and gradient) | supported | Gradient fills: gradient: 'angle' with degree is written as a linear gradient, gradient: 'path' with center (right/bottom default to left/top, as exceljs) and the stops; read back in exceljs's shape (X4-27). | lib/doc/cell.js L83; lib/xlsx/xform/style/fill-xform.js; index.d.ts L161–215 |
border (top, left, bottom, right, diagonal) | supported | diagonal with up, down, style and color, as in exceljs (X4-27). | lib/doc/cell.js L75; lib/xlsx/xform/style/border-xform.js; index.d.ts L258 |
alignment | supported | horizontal, vertical, wrapText, shrinkToFit, indent, readingOrder ('ltr'/'rtl') and textRotation ('vertical' is 255, −90…−1 are 91…180, as exceljs writes them; X4-27). textRotation and indent follow exceljs's validInt (D-E06): numbers are truncated and numeric strings accepted; a rotation outside −90…90 (other than 'vertical') is dropped and an indent below 0 becomes 0, as in exceljs. An indent above 255 in a loaded file (more than Office accepts) is left out, so the workbook can be written again. | lib/doc/cell.js L67; lib/xlsx/xform/style/alignment-xform.js; index.d.ts L280 |
protection (locked, hidden) of cells, rows, columns and styles | supported | Written when it differs from the defaults (locked: true, hidden: false), as in exceljs (X4-27). | lib/xlsx/xform/style/protection-xform.js |
Colours { argb }, { theme, tint }, { indexed } | supported | index.d.ts |
Streaming
| API | Status | Notes | Source in exceljs |
|---|---|---|---|
stream.xlsx.WorkbookWriter({ filename | stream, useStyles, useSharedStrings, zip }) | supported | Built on writeWorkbookStream; zip.zlib.level (0–9) sets the deflate level. Time grows linearly with the rows; Deflate in JavaScript (fflate) makes it about 2× exceljs's time, which uses Node.js zlib (300 000 rows of addRow(…).commit(): 6.1 s against 2.8 s, D-R09). | lib/stream/xlsx/workbook-writer.js L22, L33, L37 |
writer.addWorksheet, worksheet.addRow(...).commit(), worksheet.commit(), await writer.commit() | supported | lib/stream/xlsx/workbook-writer.js L97, L136; lib/stream/xlsx/worksheet-writer.js L215, L407 | |
insertRow(s), spliceRows, duplicateRow, spliceColumns on a streamed worksheet | not supported | exceljs's streaming worksheet has none of these methods (a TypeError); xlvane throws XLV_E_NOT_SUPPORTED. row.splice works on rows not yet committed. | lib/stream/xlsx/worksheet-writer.js |
Sheet layout (columns, views, properties.tabColor, autoFilter, merges, data validations, conditional formatting, protection, pageSetup, headerFooter, row breaks) of a streamed worksheet | partly | Must be set before the worksheet's first row is committed. A change made later throws XLV_E_NOT_SUPPORTED (at mergeCells() or at worksheet.commit()), because xlvane writes the layout before the rows. exceljs writes autoFilter, merges, validations, conditional formatting and print settings when the worksheet is committed. | lib/stream/xlsx/worksheet-writer.js |
WorkbookWriter options created, modified, creator, lastModifiedBy, lastPrinted; definedNames | partly | Written with exceljs's defaults (creator and lastModifiedBy "ExcelJS"). Names and these properties are written when the first row is committed (writing starts then); a change made later throws XLV_E_NOT_SUPPORTED at writer.commit(). exceljs writes them at the end. | lib/stream/xlsx/workbook-writer.js L22–40, L64, L264 |
stream.xlsx.WorkbookReader with for await over worksheets and rows | supported | Built on openWorkbookStream. Rich text cells come back as { richText }. | lib/stream/xlsx/workbook-reader.js L19, L71; lib/stream/xlsx/worksheet-reader.js L105 |
WorkbookReader options and events (read(), parse(), on('worksheet' | 'entry' | 'end' | 'finished' | 'error'); worksheet read(), on('row' | 'finished')) | partly | Options are checked against exceljs's WorkbookReader.Options (X4-26). styles: 'cache' gives cells their styles; entries: 'emit' sends entry events for the workbook and each worksheet (not for shared strings, styles or hyperlinks); hyperlinks: 'cache' is accepted and, as in exceljs for files in schema order, attaches nothing. sharedStrings: 'emit' | 'ignore' and hyperlinks: 'emit' throw XLV_E_NOT_SUPPORTED. Without styles: 'cache' a date is still read as a Date (exceljs gives the serial number, because it does not know the format). The worksheet reader has name, id, state, workbook, dimensions, columns, getColumn, the column-key methods and destroy (throws). The events come from a small emitter in the package, not from node:events. | lib/stream/xlsx/workbook-reader.js L20–66, L151, L326; lib/stream/xlsx/worksheet-reader.js L13–115 |
Other
| API | Status | Notes |
|---|---|---|
Module shape: default export, require() | supported | See above; require() needs Node.js 22.12+. |
| Named ESM imports | extra | Not available with exceljs 4.4.0. |
exceljs/dist/es5, exceljs/dist/es5/index.js, exceljs/dist/exceljs.js, exceljs/dist/exceljs.min.js, exceljs/dist/exceljs.bare.js, exceljs/dist/exceljs.bare.min.js | supported | Mapped to the main entry in the package's exports (an ES module; require() needs Node.js 22.12+). exceljs's files there are an ES5 build and UMD browser bundles; the API is the same (X4-26). |
Other deep imports (exceljs/lib/...) | not supported | Internal files of exceljs; not offered. |
ModelContainer | not supported | exceljs exports it as a holder of a workbook model with an xlsx property (lib/exceljs.nodejs.js L3, lib/doc/modelcontainer.js). The class is on the module object so that code referring to it loads; constructing it throws XLV_E_NOT_SUPPORTED. xlvane migrate lists its uses. |
RelationshipType, DocumentType, ReadingOrder | supported | The enums of lib/doc/enums.js, on the module object and as named exports. |
PaperSize | supported | exceljs declares it as a const enum in index.d.ts (inlined by TypeScript, absent at run time); @xlvane/compat exports it as an object and on the default export. |
TypeScript types (ExcelJS.Worksheet, ExcelJS.Fill, ExcelJS.stream.xlsx.WorkbookWriterOptions, …) | supported | The type names of exceljs's index.d.ts are exported and usable through import * as ExcelJS, import ExcelJS from, and import ExcelJS = require('exceljs'), under moduleResolution bundler, nodenext (ESM and CommonJS) and module: commonjs (tests/exceljs/types-compat.test.ts, which type-checks code from the exceljs README). In a CommonJS project TypeScript refuses import ExcelJS = require(...) of an ES module under module: node16 (TS1471, checked with TypeScript 5.9.3); module: nodenext and module: commonjs accept it. Buffer is Uint8Array. Enums (ValueType, FormulaType, ErrorValue, PaperSize) are also types. WorkbookStreamReaderOptions is an alias of WorkbookReaderOptions. As in exceljs's declarations, getTable(name) returns Table, workbook.getImage(id) returns Image, getImages()[i].range is required and fillFormula's results is (row, col) => value, a flat or a 2-D array (index.d.ts L1327, L1342–1346, L1365, L1780); at run time an unknown table name or image id gives undefined, as in exceljs. The module object is frozen: adding or replacing its properties (monkey-patching) has no effect. |
| Bundling for browsers and Cloudflare Workers | supported | The file helpers of xlvane/node are loaded only by readFile/writeFile and the filename forms of the streaming classes, through the package imports #xlvane-node and #node-fs (package.json imports), which resolve to xlvane/node and a re-export of node:fs/promises under the node condition (Node.js, Bun, Deno) and to a module that fails to load elsewhere, so bundlers such as esbuild, vite and wrangler leave out every node: import (D-R07); in browsers and Workers these methods throw XLV_E_NOT_SUPPORTED. npm run test:runtimes bundles the built packages for the browser and runs them in Chromium and workerd. |
Writing workbooks that were read from files
A workbook loaded with xlsx.load() is written again by xlsx.writeBuffer() (tests/exceljs/corpus-roundtrip.test.ts loads and saves every file of the POI corpus the layer reads). Where a file holds something xlvane does not write as it is, the layer writes the closest valid form (D-R01):
- Page setup
paperSize,copies,horizontalDpiorverticalDpiof 0 (allowed by the schema, refused by Office, [MS-OI29500] §2.1.638) are left out, so the defaults apply. exceljs writes the 0. - Shared-formula groups the writer cannot write as shared (master not the top-left cell of its range, groups that overlap each other or an array formula) are written as one ordinary formula per cell, translated from the master as
cell.formulagives it; a cell whose formula cannot be translated keeps its cached result as a value. - EMF and WMF pictures are written unchanged (
image/x-emf,image/x-wmf, as Excel declares them);workbook.addImageaccepts them as well. - A hyperlink with neither a target nor a location is not read (exceljs, which reads only links with a relationship, drops it too); a target that is itself a place in the workbook (
#Sheet2!A1) is not joined with the location. - Row heights above 409.5 points are read as 409.5; 409.5 itself, which Excel writes, is accepted.
- A table whose style is a custom style of the file is written without a style name, because xlvane does not write the file's custom table styles; a style name set in code must still be one of the built-in names.
- Totals-row formulas keep their cached results.
activeTabandfirstSheetcount chart sheets in the file; chart sheets are not loaded, so the indexes are mapped to the worksheet they fall on, or the worksheet before a chart sheet.- Row height, visibility and outline level are also read for rows without cells, up to the sheet's
dimensionor its last cell; rows after both are not looked at.
Other differences found in the pre-release review
worksheet.dimensionsis aRangeas in exceljs (top,left,bottom,right,tl,br,range,$range,shortRange,count,model,toString(); an empty sheet gives the model{ top: 0, left: 0, bottom: 0, right: 0 }and "A1:A1").intersects,containsandforEachAddressare not implemented.- A 7-digit tab colour such as
'FFC0000'(exceljs README) is completed with a final 0 (FFC00000); exceljs writes the 7 digits. - A solid conditional-format fill reads back with
bgColor(the core writes the same foreground and background colour), also when it was set withfgColor. row.destroy()andcell.destroy()do nothing; the row or cell stays usable (exceljs deletes its internals).- Errors found while writing name the worksheet and, for cell values and styles, the cell (
error.location). worksheet.tablesanddataValidations.modelhave no prototype: keys such asconstructoror__proto__are plain entries.- CSV writing: a field whose first character after any NUL characters is a formula character is escaped (NULs are removed when writing); with
formatterOptions.quote: false, a field with CR, LF or the delimiter throwsXLV_E_INVALID_OPTION(fast-csv writes it and the CSV then has other fields or rows). CSV reading: fields such asconstructoror__proto__stay text.
Known differences found by the cross-check
tests/exceljs/cross-check.test.ts runs tests/exceljs/sample-project against exceljs 4.4.0 and stores its output in expected-exceljs-4.4.0.json. tests/exceljs/alias.test.ts requires @xlvane/compat to produce the same output, except for the entries of KNOWN_DIFFERENCES in tests/exceljs/project-helper.ts, which are listed here:
- None so far for what the sample reads back. The sample covers document properties (including
companyandmanager), rich text, a column style, defined names (includingcell.name), the built-in date format, a tab colour, a zoomed frozen view external,mailto:and#links (without tooltips, which exceljs does not read back), gradient fills, a diagonal border, alignment (textRotation,shrinkToFit,readingOrder) and protection, a shared formula fromfillFormula, outline levels of rows and columns, workbook views, andinsertRowwith style'i',spliceRows,duplicateRow,spliceColumns,insertRowsandrow.spliceon plain values (no formulas, merges, notes or names, which exceljs does not move); CSV written with the default and with custom options and read back (no formula-like text, no formula result 0, no gap before the first row);cross-check.test.tsandalias.test.tsalso run the documented deep imports (deep-imports.cjs);alias.test.tsalso checks that LibreOffice shows the dates as exceljs's files do (1/31/2024).
Source: docs/exceljs-compat.md (version 1.2.2)