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 12 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) |
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 6407141. Configure here.
| } | ||
|
|
||
| const buffer = await workbook.xlsx.writeBuffer() | ||
| return Uint8Array.from(buffer as Buffer) |
There was a problem hiding this comment.
Wrong writeBuffer to bytes conversion
High Severity
toFile() turns ExcelJS writeBuffer() output into bytes with Uint8Array.from(). When the writer returns an ArrayBuffer (typical in browser builds), TypedArray.from() does not copy those bytes and can yield an empty or invalid Uint8Array, so exported .xlsx files may be corrupt or unusable outside Node where a Buffer is iterable.
Reviewed by Cursor Bugbot for commit 6407141. 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% 97.15% -0.01%
===========================================
Files 176 179 +3
Lines 15322 15411 +89
Branches 3356 3408 +52
===========================================
+ Hits 14887 14973 +86
- Misses 427 438 +11
+ Partials 8 0 -8
🚀 New features to boost your workflow:
|
Performance comparison of head (6407141) vs base (4e302ff) |


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 factory/instance APIs and a large runtime dependency affect bundle size and UMD consumers who must supply ExcelJS; formula/import edge cases are intentionally best-effort in v1.
Overview
Adds first-class
.xlsximport and export onHyperFormula: asyncbuildFromFile(ArrayBuffer | Uint8Array)builds an engine from workbook bytes, andtoFile()returns serialized sheets asUint8Arrayxlsx output. Invalid or empty input surfacesUnsupportedFileError(empty/unparseable).Implementation lives in new
src/io/xlsx/(loadWorkbook,workbookToSheets,sheetsToXlsx) using ExcelJS, moved from dev-only to a runtime dependency with lazyimport('exceljs'). UMD builds externalizeexceljs(globalExcelJS) so CDN bundles stay smaller and license checks stay valid;tsconfigmoduleis es2020 for dynamic import.v1 round-trips values and formulas only (formulas get
=prefix on import;_xlfn./_xlws.stripped); styling, merges, and formats are not preserved. Docs infile-import.mdnow describe the built-in API plus Node/browser examples.Reviewed by Cursor Bugbot for commit 6407141. Bugbot is set up for automated code reviews on this repo. Configure here.