Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7be379f
build(io): move exceljs to runtime dependency for built-in xlsx I/O (…
marcin-kordas-hoc Jul 7, 2026
ac57c94
feat(errors): add UnsupportedFileError for xlsx import (HF-107)
marcin-kordas-hoc Jul 7, 2026
a014139
feat(io): add loadXlsxWorkbook to load and validate xlsx bytes (HF-107)
marcin-kordas-hoc Jul 7, 2026
fc557e5
feat(io): add workbookToSheets xlsx importer core (HF-107)
marcin-kordas-hoc Jul 7, 2026
282a963
fix(io): strip _xlfn./_xlws. prefixes in xlsx importer formulas (HF-107)
marcin-kordas-hoc Jul 7, 2026
61b5eaa
feat(xlsx): add HyperFormula.buildFromFile public import factory (HF-…
marcin-kordas-hoc Jul 7, 2026
8839b8e
feat(io): add xlsx exporter and HyperFormula.toFile (HF-107)
marcin-kordas-hoc Jul 7, 2026
6abbcbd
test(io): add xlsx round-trip specs for import<->export (HF-107)
marcin-kordas-hoc Jul 7, 2026
2080532
docs(guide): document built-in buildFromFile/toFile xlsx I/O (HF-107)
marcin-kordas-hoc Jul 7, 2026
2c433bf
refactor(io): lazy-load exceljs + externalize in base bundle (HF-107)
marcin-kordas-hoc Jul 7, 2026
ce85904
fix(io): keep exceljs and buffer polyfill out of UMD bundles (HF-107)
marcin-kordas-hoc Jul 7, 2026
76ee6ca
test(io): relocate xlsx specs to hyperformula-tests repo (HF-107)
marcin-kordas-hoc Jul 7, 2026
f28710e
fix(io): correct xlsx export byte conversion for browser ArrayBuffer …
marcin-kordas-hoc Jul 16, 2026
5d9f252
implement-iterate: green after 1 iteration(s)
marcin-kordas-hoc Aug 28, 2026
7ad1ff2
Merge develop into the XLSX import/export branch
marcin-kordas-hoc Sep 11, 2026
7c4f9e0
Declare exceljs an optional peer dependency instead of a runtime one
marcin-kordas-hoc Sep 14, 2026
f7803d8
Keep the lockfile change to the dependency reclassification alone
marcin-kordas-hoc Sep 14, 2026
148f8e6
Keep exceljs external in the documentation's server bundle
marcin-kordas-hoc Sep 14, 2026
7f67a1f
Bundle exceljs into the Karma test bundle instead of externalising it
marcin-kordas-hoc Sep 14, 2026
3c20da7
HF-107: Merge develop, which moved the engine into hyperformula/, int…
marcin-kordas-hoc Oct 23, 2026
10f30e4
HF-107: Load ExcelJS from its default export when a native import() e…
marcin-kordas-hoc Oct 23, 2026
0f6fb42
HF-107: Keep an imported text cell that starts with = or an apostroph…
marcin-kordas-hoc Oct 23, 2026
e976e44
HF-107: Export cell values instead of their input, and formulas in th…
marcin-kordas-hoc Oct 23, 2026
4ca6c46
HF-107: Correct the file import guide and the changelog entry for xls…
marcin-kordas-hoc Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Added a message after `npm i hyperformula` that points AI assistants to the SKILLS file. The message needs `--foreground-scripts` to be visible in npm, and pnpm 10 needs the `postinstall` script to be approved first. [#1829](https://github.com/handsontable/hyperformula/pull/1829)
- Added `.xlsx` import and export of cell values and formulas: the `buildFromFile()` and `toFile()` methods. Both need ExcelJS, an optional peer dependency: install it with `npm install exceljs`. [#1702](https://github.com/handsontable/hyperformula/pull/1702)

### Fixed

Expand Down
11 changes: 11 additions & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ const DOCS_DEST = process.env.DOCS_DEST || buildConfigOverrides.dest || '.vuepre
const DOCS_HOSTNAME = process.env.DOCS_HOSTNAME || buildConfigOverrides.hostname || 'https://handsontable.com';

module.exports = {
// The docs bundle HyperFormula's 'full' build, which keeps exceljs external and therefore
// emits a bare require('exceljs'). In the server-side render that resolves to ExcelJS's Node
// entry, whose stream reader reaches unzipper -> fstream -> rimraf; rimraf ships ESM using
// optional chaining, which this webpack cannot parse, so the server bundle failed to compile.
// Nothing in the documentation calls buildFromFile or toFile, so exceljs is left external here
// too: Node would resolve it at call time, and there is no call.
configureWebpack: (config, isServer) => {
if (isServer) {
config.externals = [].concat(config.externals || [], 'exceljs');
}
},
// Default page globs, minus the built-in-functions template: it is the INPUT of docs:generate-function-docs,
// not a page, and without this exclusion VuePress would publish it as a duplicate, table-less
// /guide/built-in-functions.tmpl.html (also polluting the sitemap, search index, and llms.txt corpus).
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ docs # All documentation files
│   ├── subtheme # Subtheme files
│   ├── templates # HTML templates
│   ├── theme # Local theme extending the default theme (see below)
│   ├── config.js # VuePress configuration
│   ├── config.js # VuePress configuration; keeps exceljs external in the server-side bundle
│   ├── enhanceApp.js # VuePress app-level enhancements
│   └── highlight.js # Code highlight configuration
├── api # The API reference files, generated automatically from JsDoc. Do not edit!
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ have dependencies, they need to be added manually
* `./dist/hyperformula.full.min.js` - a minified version with
dependencies

None of them includes ExcelJS, which the `buildFromFile` and `toFile` methods
need: load it separately, as the global `ExcelJS`.

## Build the project

To build the project you can use the following commands:
Expand Down
147 changes: 108 additions & 39 deletions docs/guide/file-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,72 +7,141 @@ tags:
- read Excel file
---

# File import
# File import and export

Import XLSX and CSV files into HyperFormula.
Import and export XLSX files, and import CSV files, with HyperFormula.

## Overview

HyperFormula has no built-in file import functionality. But its [factory methods](../api/classes/hyperformula.md#factories) use standard JavaScript data types, for easy integration with any way of importing data.
HyperFormula imports and exports `.xlsx` files through the
[ExcelJS](https://www.npmjs.com/package/exceljs) library. Import with the
[`buildFromFile`](../api/classes/hyperformula.md#buildfromfile) factory method
and export with the [`toFile`](../api/classes/hyperformula.md#tofile) method.

## Import CSV files

To import CSV files, use a third-party [CSV parser](https://www.npmjs.com/search?q=csv) (e.g., [PapaParse](https://www.npmjs.com/package/papaparse) or [csv-parse](https://www.npmjs.com/package/csv-parse)). Then pass the result to HyperFormula as a JavaScript array.
ExcelJS is an **optional peer dependency**, so it is not installed with
HyperFormula. Add it only if you use these two methods:

## Import XLSX files
```bash
npm install exceljs
```

To import XLSX files, use a third-party [XLSX parser](https://www.npmjs.com/search?q=xlsx) (e.g., [ExcelJS](https://www.npmjs.com/package/exceljs) or [xlsx](https://www.npmjs.com/package/xlsx)). Then pass the result to HyperFormula as a JavaScript array.
In Node.js, everything else in HyperFormula works without it: the library is
loaded on demand, so calling `buildFromFile` or `toFile` without ExcelJS
installed fails at the point of the call. A bundler such as webpack or esbuild,
however, resolves the library when it builds your application, so the build
fails if ExcelJS is not installed, even if your code never calls these two
methods. In the UMD `full` build, ExcelJS is kept out of the bundle and has to
be provided as the global `ExcelJS`.

For any other format (for example CSV), HyperFormula's
[factory methods](../api/classes/hyperformula.md#factories) accept standard
JavaScript arrays, so you can plug in any third-party parser.

::: tip
Only **cell values and formulas** are imported and exported. Cell styling,
number formats, merged cells, charts, images, data validation, and other
Excel-specific features are **not** preserved &mdash; HyperFormula stores only
values and formulas. A cell that holds a value is exported as its value, so a
number entered as a string, such as `'1'`, is exported as a number. Formulas are
converted between the syntax of the file (English function names, `,` between
arguments, `.` as the decimal separator) and the language and separators your
configuration uses. On export, formulas that use newer or dynamic-array Excel functions are written
without Excel's internal `_xlfn.` prefix, so they may not resolve when the
exported file is opened in real Excel.
:::

### Example: Import XLSX files in Node
## Import XLSX files

This example uses [ExcelJS](https://www.npmjs.com/package/exceljs) to import XLSX files into HyperFormula.
Pass the file's bytes (an `ArrayBuffer` or `Uint8Array`) to the asynchronous
[`buildFromFile`](../api/classes/hyperformula.md#buildfromfile) factory method.
Reading the file itself is left to your environment, so the same API works in
both Node.js and the browser.

See full example on [GitHub](https://github.com/handsontable/hyperformula-demos/tree/3.5.x/read-excel-file).
### In Node.js

```js
const ExcelJS = require('exceljs');
const fs = require('fs');
const { HyperFormula } = require('hyperformula');

async function run(filename) {
const xlsxWorkbook = await readXlsxWorkbookFromFile(filename);
const sheetsAsJavascriptArrays = convertXlsxWorkbookToJavascriptArrays(xlsxWorkbook)
const hf = HyperFormula.buildFromSheets(sheetsAsJavascriptArrays, { licenseKey: 'gpl-v3' });
async function importXlsx(filePath) {
const data = fs.readFileSync(filePath); // a Buffer (a Uint8Array)
const hf = await HyperFormula.buildFromFile(data, { licenseKey: 'gpl-v3' });

console.log('Formulas:', hf.getSheetSerialized(0));
console.log('Values: ', hf.getSheetValues(0));
}

async function readXlsxWorkbookFromFile(filename) {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(filename);
return workbook;
return hf;
}
```

### In the browser

```js
import { HyperFormula } from 'hyperformula';

function convertXlsxWorkbookToJavascriptArrays(workbook) {
const workbookData = {};
// `file` is a File obtained from an <input type="file"> element.
async function importXlsx(file) {
const data = await file.arrayBuffer(); // an ArrayBuffer
const hf = await HyperFormula.buildFromFile(data, { licenseKey: 'gpl-v3' });

workbook.eachSheet((worksheet) => {
const sheetDimensions = worksheet.dimensions
const sheetData = [];
return hf;
}
```

for (let rowNum = sheetDimensions.top; rowNum <= sheetDimensions.bottom; rowNum++) {
const rowData = [];
If the bytes cannot be read as an `.xlsx` file, `buildFromFile` rejects with an
[`UnsupportedFileError`](../api/classes/unsupportedfileerror.md).

for (let colNum = sheetDimensions.left; colNum <= sheetDimensions.right; colNum++) {
const cell = worksheet.getCell(rowNum, colNum)
## Export XLSX files

const cellData = cell.formula ? `=${cell.formula}` : cell.value;
rowData.push(cellData);
}
The asynchronous [`toFile`](../api/classes/hyperformula.md#tofile) method returns
the workbook's bytes as a `Uint8Array`. Writing those bytes is left to your
environment.

sheetData.push(rowData);
}
### In Node.js

workbookData[worksheet.name] = sheetData;
})
```js
const fs = require('fs');

return workbookData;
async function exportXlsx(hf, filePath) {
const bytes = await hf.toFile();
fs.writeFileSync(filePath, bytes);
}
```

### In the browser

run('sample_file.xlsx');
```js
async function downloadXlsx(hf, fileName) {
const bytes = await hf.toFile();
const blob = new Blob([bytes], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});

const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
}
```

## Import CSV files

HyperFormula has no built-in CSV parser. Use a third-party
[CSV parser](https://www.npmjs.com/search?q=csv) (e.g.,
[PapaParse](https://www.npmjs.com/package/papaparse) or
[csv-parse](https://www.npmjs.com/package/csv-parse)), then pass the resulting
array to [`buildFromArray`](../api/classes/hyperformula.md#buildfromarray).

## Advanced: custom XLSX handling

If you need control over how the workbook is read or written (for example, to
read cached values instead of formulas, or to handle Excel features outside
HyperFormula's value-and-formula model), parse the file yourself with a
third-party [XLSX parser](https://www.npmjs.com/search?q=xlsx) such as
[ExcelJS](https://www.npmjs.com/package/exceljs) or
[xlsx](https://www.npmjs.com/package/xlsx), convert the result to JavaScript
arrays, and pass them to
[`buildFromSheets`](../api/classes/hyperformula.md#buildfromsheets). See a full
example on [GitHub](https://github.com/handsontable/hyperformula-demos/tree/3.5.x/read-excel-file).
8 changes: 8 additions & 0 deletions hyperformula/.config/karma/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ module.exports.create = function(config) {
// Take the second config from an array - full HF build.
const config = webpackConfigFactory('development')[1];

// That build keeps exceljs external so consumers can supply the global themselves. A test
// bundle has no consumer to supply it, so the reference resolved to nothing in the browser
// and every spec touching xlsx import/export failed. Bundle it in instead: the size and
// license reasons for externalising it apply to what is published, not to what Karma runs.
if (config.externals && config.externals.exceljs) {
delete config.externals.exceljs;
}

// Loaders are executed from bottom to top. Push ts-loader as a first loader.
config.module.rules[0].use.push({
loader: 'ts-loader',
Expand Down
18 changes: 18 additions & 0 deletions hyperformula/.config/webpack/development.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,30 @@ module.exports.create = function create() {
commonjs: 'tiny-emitter',
amd: 'tiny-emitter',
},
exceljs: {
root: 'ExcelJS',
commonjs2: 'exceljs',
commonjs: 'exceljs',
amd: 'exceljs',
},
};
c.plugins.push(new WebpackBar({ name: ` ${PACKAGE_FILENAME}.js` }));
});

configFull.forEach(function(c) {
c.output.filename = `${PACKAGE_FILENAME}.full.js`;
// The 'full' bundle inlines HyperFormula's own dependencies, but exceljs is
// kept external: it is large and its transitive licenses would break the
// single-preamble bundle invariant. xlsx import/export in the UMD 'full'
// build therefore requires providing exceljs (global `ExcelJS`) separately.
c.externals = {
exceljs: {
root: 'ExcelJS',
commonjs2: 'exceljs',
commonjs: 'exceljs',
amd: 'exceljs',
},
};
c.plugins.push(new WebpackBar({ name: ` ${PACKAGE_FILENAME}.full.js` }));
});

Expand Down
9 changes: 8 additions & 1 deletion hyperformula/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@
"@babel/preset-typescript": "^7.26.0",
"@babel/register": "^7.25.9",
"@babel/runtime": "^7.26.0",
"@types/exceljs": "^0.5.3",
"@types/jasmine": "^5.1.4",
"@types/jest": "^26.0.24",
"@types/jsdom": "^21.1.7",
Expand Down Expand Up @@ -614,5 +613,13 @@
"import": "./es/i18n/languages/trTR.mjs",
"require": "./commonjs/i18n/languages/trTR.js"
}
},
"peerDependencies": {
"exceljs": "^4.4.0"
},
"peerDependenciesMeta": {
"exceljs": {
"optional": true
}
}
}
Loading
Loading