feat: built-in XLSX import/export via buildFromFile/toFile (HF-107)#1702
feat: built-in XLSX import/export via buildFromFile/toFile (HF-107)#1702marcin-kordas-hoc wants to merge 13 commits into
Conversation
…HF-107) exceljs was a devDependency; HF-107 makes xlsx import/export a built-in feature, so it becomes a runtime dependency. @types/exceljs stays a devDependency (build-time only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a public UnsupportedFileError class to src/errors.ts for later HF-107 tasks (workbook loader/importer/buildFromFile) to throw when a byte buffer can't be read as an .xlsx file. v1 covers only 'empty' and 'unparseable' reasons per spec; richer reasons are a deferred follow-up. Re-exported from src/index.ts alongside the other public error types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1 scope: attempt the ExcelJS load and wrap any failure in
UnsupportedFileError('empty' | 'unparseable'). Byte-signature/ZIP-magic/CFB
format detection is deliberately deferred (YAGNI). Handles both ArrayBuffer
and Uint8Array inputs, including subarray views with a non-zero byteOffset,
via Buffer.from(data) (never data.buffer).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert an already-loaded ExcelJS Workbook into HyperFormula's Sheets
shape. Pure and synchronous: byte-loading stays in loadXlsxWorkbook
(Task 3). Maps simple formulas ('=' + cell.formula), primitive values,
best-effort Date pass-through, and falls back to cell.text for
object-valued cells (error/richText/hyperlink) so raw objects are
never emitted. Shared/array formulas, _xlfn. normalization, and the
full error-token table are deliberately deferred (Task 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Excel stores newer/relocated functions (e.g. XLOOKUP) with an internal _xlfn. (sometimes _xlfn._xlws.) prefix in the raw formula string. Left in place, HF would fail to resolve the function name even when it natively supports it. Add stripFunctionPrefixes() and apply it in the importer's formula branch. Confirming tests added for shared-formula per-cell rebasing and cross-sheet/quoted-name references, which a probe of ExcelJS's real round-trip behavior showed were already handled correctly by the existing '=' + cell.formula mapping - no rebasing logic needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…107) Chains loadXlsxWorkbook -> workbookToSheets -> buildFromSheets into a single async static factory so consumers can go straight from raw .xlsx bytes to a live engine. No import options, named expressions, or format detection in v1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sheetsToXlsx() maps HF's getAllSheetsSerialized() output (values + '='-prefixed formula strings) onto an ExcelJS workbook and returns Uint8Array bytes. toFile() wires this to a live engine instance. v1 scope: values and formulas only, no styles/number-formats/merged cells. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers identity round-trip for HF-authored values+formulas (dates excluded, by design), a second-round-trip stability check seeded from an ExcelJS-authored workbook (breaks buildFromArray symmetry), and a lossy-formatting proof (fill/font/numFmt do not survive). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrites the file-import guide to lead with the built-in buildFromFile (import) and toFile (export) API, with Node and browser snippets, a values+formulas-only limitations note, and the third-party approach kept as an advanced fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep exceljs a core dependency but load it lazily so it is no longer
inlined into the eager module graph of the always-imported HyperFormula
class, nor into the default CDN (base) bundle.
- importer.ts: value import -> `import type` (Cell/Workbook/Worksheet are
types only; it never constructs a Workbook).
- loadWorkbook.ts / exporter.ts: `import type {Workbook}` for the signature
type plus a runtime `await import(/* webpackMode: "eager" */ 'exceljs')`
inside the async functions. The eager magic comment keeps HyperFormula's
own single-file `full` UMD from splitting into a separate chunk while
downstream ESM/CJS bundlers still see a real dynamic import and code-split.
- .config/webpack/development.js: add exceljs to the base bundle externals
(alongside chevrotain/tiny-emitter); configFull still inlines everything.
- tsconfig.json: `module` es2015 -> es2020, required for dynamic `import()`
to typecheck/compile (TS1323). Emit is unchanged for every module that does
not use dynamic import / import.meta (only these two files change).
- importer.ts: note stripFunctionPrefixes may rewrite a matching substring
inside a string literal (accepted best-effort v1 limitation).
- docs/guide/file-import.md: note exported dynamic-array formulas lack Excel's
internal `_xlfn.` prefix and may not resolve in real Excel.
- package.json: drop unused @types/exceljs (exceljs 4.4.0 ships its own types).
Verified: jest test/io + smoke (40 tests green), tsc --noEmit clean, eslint
0 errors. Base bundle no longer inlines exceljs (jszip markers 7 -> 0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- loadXlsxWorkbook: convert input via a zero-offset Uint8Array copy and pass its ArrayBuffer to ExcelJS, instead of Buffer.from (which pulled webpack's ~feross/buffer polyfill into the browser bundle). - webpack: externalize exceljs in the 'full' bundle too (not just base), so neither UMD bundle inlines exceljs; this keeps the single-license-preamble invariant (verify:umd / verify:umd:full assert exactly 2 @license comments). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the two-repo workflow, HF-107's unit/round-trip specs live in the private hyperformula-tests repo (matching branch), not the public repo. Source, docs, and build config remain here; the specs move out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
❌ Deploy Preview for hyperformula-dev-docs failed. Why did it fail? →
|
|
Task linked: HF-107 Import/export files (XLSX, CSV) |
Performance comparison of head (f376892) vs base (4e302ff) |
…(HF-107) ExcelJS `writeBuffer()` returns a Node `Buffer` in Node but an `ArrayBuffer` in the browser. `Uint8Array.from()` treats an `ArrayBuffer` as an empty array-like and silently drops all bytes, so `toFile()` produced corrupt/empty `.xlsx` files in browser builds. Use `new Uint8Array(...)`, which views an ArrayBuffer and copies a Buffer — correct on both targets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f376892. Configure here.
| try { | ||
| const ExcelJS = await import(/* webpackMode: "eager" */ 'exceljs') | ||
| const workbook = new ExcelJS.Workbook() | ||
| await workbook.xlsx.load(bytes.buffer) |
There was a problem hiding this comment.
Broken XLSX load from Node Buffers
High Severity
loadXlsxWorkbook copies input with .slice() then passes bytes.buffer to ExcelJS. On Node, Buffer (a Uint8Array) overrides .slice() to share memory, and .buffer ignores byteOffset/byteLength. Pooled or sliced Buffers—common from fs.readFile for small files—can load the wrong bytes and fail as UnsupportedFileError, despite the docs advertising that path.
Reviewed by Cursor Bugbot for commit f376892. Configure here.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1702 +/- ##
===========================================
- Coverage 97.16% 96.42% -0.75%
===========================================
Files 176 179 +3
Lines 15322 15561 +239
Branches 3356 3448 +92
===========================================
+ Hits 14887 15004 +117
- Misses 427 557 +130
+ Partials 8 0 -8
🚀 New features to boost your workflow:
|


What
Implements HF-107: built-in XLSX import/export.
HyperFormula.buildFromFile(data, config?)— static async factory; acceptsArrayBuffer | Uint8Array, returnsPromise<HyperFormula>.hf.toFile()— instance method; returnsPromise<Uint8Array>(xlsx bytes).UnsupportedFileError— thrown when the bytes can't be read as xlsx (reason: 'empty' | 'unparseable').src/io/xlsx/module (loadWorkbook,importer,exporter) backed by ExcelJS.exceljsbecomes a runtime dependency, lazy-loaded (await import()) and externalized in the UMD bundles, so the default/CDN bundles stay lean and keep the single-license-preamble invariant (verify:umd/verify:umd:full).docs/guide/file-import.md.Scope (v1)
Preserves multiple sheets, values, formulas (
=-prefixed,_xlfn./_xlws.stripped), best-effort dates/errors. Values + formulas only — styling, number formats, merged cells, etc. are not preserved (HyperFormula has no formatting model).Deferred follow-ups: robust byte-signature / CFB format detection,
date1904+ full error-token mapping + object-cell fidelity, named expressions, CSV, code-level Premium gating.Tests
Specs live in the tests repo — see the paired handsontable/hyperformula-tests PR on branch
feature/hf-107-xlsx-import-export(7 spec files / 36 tests: unit + round-trip, incl. an ExcelJS-authored round-trip and a formatting-loss proof).Notes
ExcelJSfor xlsx (external dependency).tsconfigmodulebumped es2015 → es2020 (required for the lazy dynamicimport()).🤖 Generated with Claude Code
Note
Medium Risk
New public async APIs and a large runtime dependency affect bundle/CDN consumers (must supply ExcelJS for UMD xlsx); round-trip fidelity is intentionally limited to values/formulas.
Overview
Adds first-class
.xlsximport and export onHyperFormula: asyncbuildFromFile(ArrayBuffer | Uint8Array)builds an engine from workbook bytes, andtoFile()returns serialized sheets as.xlsxbytes. Parsing and writing go through a newsrc/io/xlsx/layer (load → import / export) backed by ExcelJS, which moves from dev-only to a runtime dependency and is lazy-loaded via dynamicimport()so bundles stay smaller until xlsx is used.Invalid or empty input raises
UnsupportedFileError(empty/unparseable). UMD builds externalizeexceljs(globalExcelJSon full/base CDN bundles) to preserve license-preamble checks;tsconfigmoduleis bumped toes2020for dynamic import emit. The file guide is rewritten around the new APIs; v1 is values and formulas only (no styles, merges, etc.).Reviewed by Cursor Bugbot for commit f376892. Bugbot is set up for automated code reviews on this repo. Configure here.