Skip to content

feat: built-in XLSX import/export via buildFromFile/toFile (HF-107)#1702

Open
marcin-kordas-hoc wants to merge 12 commits into
developfrom
feature/hf-107-xlsx-import-export
Open

feat: built-in XLSX import/export via buildFromFile/toFile (HF-107)#1702
marcin-kordas-hoc wants to merge 12 commits into
developfrom
feature/hf-107-xlsx-import-export

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Implements HF-107: built-in XLSX import/export.

  • HyperFormula.buildFromFile(data, config?) — static async factory; accepts ArrayBuffer | Uint8Array, returns Promise<HyperFormula>.
  • hf.toFile() — instance method; returns Promise<Uint8Array> (xlsx bytes).
  • UnsupportedFileError — thrown when the bytes can't be read as xlsx (reason: 'empty' | 'unparseable').
  • New src/io/xlsx/ module (loadWorkbook, importer, exporter) backed by ExcelJS.
  • exceljs becomes 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).
  • Guide updated: 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

  • The UMD full bundle now expects a global ExcelJS for xlsx (external dependency).
  • tsconfig module bumped es2015 → es2020 (required for the lazy dynamic import()).

🤖 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 .xlsx import and export on HyperFormula: async buildFromFile(ArrayBuffer | Uint8Array) builds an engine from workbook bytes, and toFile() returns serialized sheets as Uint8Array xlsx output. Invalid or empty input surfaces UnsupportedFileError (empty / unparseable).

Implementation lives in new src/io/xlsx/ (loadWorkbook, workbookToSheets, sheetsToXlsx) using ExcelJS, moved from dev-only to a runtime dependency with lazy import('exceljs'). UMD builds externalize exceljs (global ExcelJS) so CDN bundles stay smaller and license checks stay valid; tsconfig module is 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 in file-import.md now 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.

marcin-kordas-hoc and others added 12 commits July 7, 2026 12:34
…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>
@netlify

netlify Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploy Preview for hyperformula-dev-docs failed. Why did it fail? →

Name Link
🔨 Latest commit 6407141
🔍 Latest deploy log https://app.netlify.com/projects/hyperformula-dev-docs/deploys/6a4e05eddad4d10008bcadeb

@qunabu

qunabu commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Task linked: HF-107 Import/export files (XLSX, CSV)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/io/xlsx/exporter.ts
}

const buffer = await workbook.xlsx.writeBuffer()
return Uint8Array.from(buffer as Buffer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6407141. Configure here.

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.15%. Comparing base (00e5c73) to head (6407141).
⚠️ Report is 8 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             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     
Files with missing lines Coverage Δ
src/HyperFormula.ts 99.73% <100.00%> (+<0.01%) ⬆️
src/errors.ts 100.00% <100.00%> (ø)
src/index.ts 100.00% <100.00%> (ø)
src/io/xlsx/exporter.ts 100.00% <100.00%> (ø)
src/io/xlsx/importer.ts 100.00% <100.00%> (ø)
src/io/xlsx/loadWorkbook.ts 100.00% <100.00%> (ø)

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Performance comparison of head (6407141) vs base (4e302ff)

                                     testName |   base |   head | change
------------------------------------------------------------------------
                                      Sheet A | 494.32 | 494.93 | +0.12%
                                      Sheet B |  158.4 | 160.21 | +1.14%
                                      Sheet T | 141.33 | 142.97 | +1.16%
                                Column ranges | 471.94 | 475.01 | +0.65%
Sheet A:  change value, add/remove row/column |  15.27 |   15.1 | -1.11%
 Sheet B: change value, add/remove row/column |  137.5 | 129.08 | -6.12%
                   Column ranges - add column | 147.41 | 141.68 | -3.89%
                Column ranges - without batch |  456.2 | 447.73 | -1.86%
                        Column ranges - batch | 112.38 | 115.48 | +2.76%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants