diff --git a/.config/webpack/development.js b/.config/webpack/development.js index 029b8246ce..5602efd5a7 100644 --- a/.config/webpack/development.js +++ b/.config/webpack/development.js @@ -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` })); }); diff --git a/docs/guide/file-import.md b/docs/guide/file-import.md index 1c9d04d278..ed677d2e50 100644 --- a/docs/guide/file-import.md +++ b/docs/guide/file-import.md @@ -1,69 +1,121 @@ -# 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. - -## 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. +HyperFormula can import and export `.xlsx` files out of the box, using the +[ExcelJS](https://www.npmjs.com/package/exceljs) library under the hood. Import +with the [`buildFromFile`](../api/classes/hyperformula.md#buildfromfile) factory +method and export with the [`toFile`](../api/classes/hyperformula.md#tofile) +method. + +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 — HyperFormula stores only +values and formulas. Formula import assumes the English (`en`) function dialect. +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. +::: ## Import XLSX files -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. - -### Example: Import XLSX files in Node - -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.1.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 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.1.x/read-excel-file). diff --git a/package-lock.json b/package-lock.json index 32585c79d1..b44cdac87c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-3.0-only", "dependencies": { "chevrotain": "^6.5.0", + "exceljs": "^4.4.0", "tiny-emitter": "^2.1.0" }, "devDependencies": { @@ -27,7 +28,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", @@ -49,7 +49,6 @@ "eslint-plugin-license-header": "^0.6.1", "eslint-plugin-prettier": "^5.2.1", "esm": "^3.2.25", - "exceljs": "^4.4.0", "full-icu": "^1.5.0", "jasmine": "^5.4.0", "jest": "^26.6.3", @@ -176,6 +175,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2344,6 +2344,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2367,6 +2368,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2479,7 +2481,6 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "^14.0.1", @@ -2494,14 +2495,12 @@ "version": "14.18.63", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", - "dev": true, "license": "MIT" }, "node_modules/@fast-csv/parse": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "^14.0.1", @@ -2517,7 +2516,6 @@ "version": "14.18.63", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", - "dev": true, "license": "MIT" }, "node_modules/@gar/promisify": { @@ -3883,16 +3881,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/exceljs": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@types/exceljs/-/exceljs-0.5.3.tgz", - "integrity": "sha512-a0PLZEJGbA4kHHoSS8cQ20ynIv17vCBV1reqsrX5ksQQb077YYvhEKC82lEq6/+mMufhk68KJ5jwugL3DKG8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -4078,7 +4066,8 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", @@ -4320,6 +4309,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -6309,6 +6299,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6413,6 +6404,7 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6615,7 +6607,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, "license": "MIT", "dependencies": { "archiver-utils": "^2.1.0", @@ -6634,7 +6625,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, "license": "MIT", "dependencies": { "glob": "^7.1.4", @@ -6656,14 +6646,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/archiver-utils/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -6679,14 +6667,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -6953,7 +6939,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, "license": "MIT" }, "node_modules/async-each": { @@ -7503,7 +7488,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base": { @@ -7556,7 +7540,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -7617,7 +7600,6 @@ "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, "license": "Unlicense", "engines": { "node": ">=0.6" @@ -7637,7 +7619,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dev": true, "license": "MIT", "dependencies": { "buffers": "~0.1.1", @@ -7675,7 +7656,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -7687,7 +7667,6 @@ "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", - "dev": true, "license": "MIT" }, "node_modules/bn.js": { @@ -7871,7 +7850,6 @@ "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8058,6 +8036,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -8099,7 +8078,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -8124,7 +8102,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -8148,7 +8125,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10" @@ -8172,7 +8148,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "dev": true, "engines": { "node": ">=0.2.0" } @@ -8683,7 +8658,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dev": true, "license": "MIT/X11", "dependencies": { "traverse": ">=0.3.0 <0.4" @@ -8698,6 +8672,7 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -9054,7 +9029,8 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/comment-parser": { "version": "1.4.1", @@ -9087,7 +9063,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "^0.2.13", @@ -9162,7 +9137,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -9722,7 +9696,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -9811,7 +9784,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, "license": "Apache-2.0", "bin": { "crc32": "bin/crc32.njs" @@ -9824,7 +9796,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, "license": "MIT", "dependencies": { "crc-32": "^1.2.0", @@ -10546,7 +10517,6 @@ "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "dev": true, "license": "MIT" }, "node_modules/de-indent": { @@ -11343,7 +11313,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "readable-stream": "^2.0.2" @@ -11353,14 +11322,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/duplexer2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -11376,14 +11343,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/duplexer2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -11548,7 +11513,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -11885,6 +11849,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -12222,6 +12187,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -12278,6 +12244,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -12607,6 +12574,7 @@ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -12768,7 +12736,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", - "dev": true, "license": "MIT", "dependencies": { "archiver": "^5.0.0", @@ -13122,7 +13089,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", - "dev": true, "license": "MIT", "dependencies": { "@fast-csv/format": "4.3.5", @@ -13728,7 +13694,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -13844,7 +13809,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -13867,7 +13831,6 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "dependencies": { "graceful-fs": "^4.1.2", @@ -13884,7 +13847,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -14087,7 +14049,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -14268,7 +14229,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -15117,7 +15077,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -15155,7 +15114,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, "license": "MIT" }, "node_modules/import-cwd": { @@ -15349,7 +15307,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -15360,7 +15317,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -16451,6 +16407,7 @@ "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^26.6.3", "import-local": "^3.0.2", @@ -18643,7 +18600,6 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -18656,14 +18612,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -18679,14 +18633,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/jszip/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -19023,7 +18975,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, "license": "MIT", "dependencies": { "readable-stream": "^2.0.5" @@ -19036,14 +18987,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -19059,14 +19008,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -19153,7 +19100,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, "license": "MIT", "dependencies": { "immediate": "~3.0.5" @@ -19180,7 +19126,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", - "dev": true, "license": "ISC" }, "node_modules/load-json-file": { @@ -19306,42 +19251,36 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true, "license": "MIT" }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true, "license": "MIT" }, "node_modules/lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", - "dev": true, "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.isequal": { @@ -19349,35 +19288,30 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "dev": true, "license": "MIT" }, "node_modules/lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", - "dev": true, "license": "MIT" }, "node_modules/lodash.isnil": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", - "dev": true, "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.isundefined": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.kebabcase": { @@ -19441,14 +19375,12 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, "license": "MIT" }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true, "license": "MIT" }, "node_modules/log4js": { @@ -19596,6 +19528,7 @@ "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^1.0.7", "entities": "~1.1.1", @@ -20081,7 +20014,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -20094,7 +20026,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -20275,7 +20206,6 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -20694,7 +20624,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21092,7 +21021,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -21367,7 +21295,6 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/parallel-transform": { @@ -21554,7 +21481,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -23589,7 +23515,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/progress": { @@ -24134,7 +24059,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -24149,7 +24073,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "minimatch": "^5.1.0" @@ -24159,7 +24082,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -24169,7 +24091,6 @@ "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -25083,7 +25004,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -25498,7 +25418,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -25834,7 +25753,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, "license": "MIT" }, "node_modules/setprototypeof": { @@ -26903,7 +26821,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -27491,7 +27408,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -27857,7 +27773,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -28083,7 +27998,6 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "dev": true, "license": "MIT/X11", "engines": { "node": "*" @@ -28270,6 +28184,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -28656,6 +28571,7 @@ "integrity": "sha512-oz1765PN+imfz1MlZzSZPtC/tqcwsCyIYA8L47EkRnRW97ztRk83SzMiWLrnChC0vqoYxSU1fcFUDA5gV/ZiPg==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -28928,7 +28844,6 @@ "version": "0.10.14", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dev": true, "license": "MIT", "dependencies": { "big-integer": "^1.6.17", @@ -28947,14 +28862,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/unzipper/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -28970,14 +28883,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/unzipper/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -29284,7 +29195,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/util-extend": { @@ -29339,7 +29249,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -29466,6 +29375,7 @@ "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" @@ -30323,6 +30233,7 @@ "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -30398,6 +30309,7 @@ "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", @@ -32195,7 +32107,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -32267,7 +32178,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, "license": "MIT" }, "node_modules/xtend": { @@ -32442,7 +32352,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, "license": "MIT", "dependencies": { "archiver-utils": "^3.0.4", @@ -32457,7 +32366,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, "license": "MIT", "dependencies": { "glob": "^7.2.3", diff --git a/package.json b/package.json index e2c8d29d17..ae01b753a2 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ }, "dependencies": { "chevrotain": "^6.5.0", + "exceljs": "^4.4.0", "tiny-emitter": "^2.1.0" }, "devDependencies": { @@ -119,7 +120,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", @@ -141,7 +141,6 @@ "eslint-plugin-license-header": "^0.6.1", "eslint-plugin-prettier": "^5.2.1", "esm": "^3.2.25", - "exceljs": "^4.4.0", "full-icu": "^1.5.0", "jasmine": "^5.4.0", "jest": "^26.6.3", diff --git a/src/HyperFormula.ts b/src/HyperFormula.ts index 566957270a..811e484ab1 100644 --- a/src/HyperFormula.ts +++ b/src/HyperFormula.ts @@ -49,6 +49,9 @@ import {FunctionRegistry, FunctionTranslationsPackage} from './interpreter/Funct import {FormatInfo} from './interpreter/InterpreterValue' import {LazilyTransformingAstService} from './LazilyTransformingAstService' import {ColumnSearchStrategy} from './Lookup/SearchStrategy' +import {sheetsToXlsx} from './io/xlsx/exporter' +import {loadXlsxWorkbook} from './io/xlsx/loadWorkbook' +import {workbookToSheets} from './io/xlsx/importer' import {NamedExpression, NamedExpressionOptions, NamedExpressions} from './NamedExpressions' import {normalizeAddedIndexes, normalizeRemovedIndexes} from './Operations' import { @@ -323,6 +326,47 @@ export class HyperFormula implements TypedEmitter { return this.buildFromEngineState(BuildEngineFactory.buildFromSheets(sheets, configInput, namedExpressions)) } + /** + * Builds the engine from the raw bytes of an .xlsx file. The file is parsed and its + * worksheets are converted into sheets, keyed by worksheet name, exactly as if they had + * been passed to [[buildFromSheets]]. + * Can be configured with the optional second parameter that represents a [[ConfigParams]]. + * If not specified the engine will be built with the default configuration. + * + * v1 scope: no import options, no named expressions, no cell-format detection (see HF-107). + * + * @param {ArrayBuffer | Uint8Array} data - the raw bytes of an .xlsx file + * @param {Partial} configInput - engine configuration + * + * @returns {Promise} a promise that resolves to the built engine + * + * @throws [[UnsupportedFileError]] when `data` cannot be read as a valid .xlsx workbook + * (e.g. it is empty or not a well-formed .xlsx file) + * @throws [[SheetSizeLimitExceededError]] when a worksheet size exceeds the limits + * + * @example + * ```js + * // Node.js: read the file from disk first + * const fs = require('fs/promises'); + * const buffer = await fs.readFile('example.xlsx'); + * const hfInstance = await HyperFormula.buildFromFile(buffer, { licenseKey: 'gpl-v3' }); + * ``` + * + * @example + * ```js + * // Browser: read bytes from a File (e.g. an element) + * const arrayBuffer = await file.arrayBuffer(); + * const hfInstance = await HyperFormula.buildFromFile(arrayBuffer, { licenseKey: 'gpl-v3' }); + * ``` + * + * @category Factories + */ + public static async buildFromFile(data: ArrayBuffer | Uint8Array, configInput: Partial = {}): Promise { + const workbook = await loadXlsxWorkbook(data) + const sheets = workbookToSheets(workbook) + return HyperFormula.buildFromSheets(sheets, configInput) + } + /** * Builds an empty engine instance. * Can be configured with the optional parameter that represents a [[ConfigParams]]. @@ -1014,6 +1058,32 @@ export class HyperFormula implements TypedEmitter { return this._serialization.getAllSheetsSerialized() } + /** + * Exports the current engine state to `.xlsx` file bytes. + * + * v1 scope: values and formulas only, no styles/number-formats/merged cells + * (see HF-107). Formulas are serialized exactly as {@link getAllSheetsSerialized} + * would return them. + * + * @throws [[EvaluationSuspendedError]] when the evaluation is suspended + * + * @returns {Promise} a promise that resolves to the raw bytes of an .xlsx file + * + * @example + * ```js + * const hfInstance = HyperFormula.buildFromArray([ + * ['1', '2', '=A1+10'], + * ]); + * + * const bytes = await hfInstance.toFile(); + * ``` + * + * @category Sheets + */ + public async toFile(): Promise { + return sheetsToXlsx(this.getAllSheetsSerialized()) + } + /** * Updates the config with given new metadata. It is an expensive operation, as it might trigger rebuilding the engine and recalculation of all formulas. * diff --git a/src/errors.ts b/src/errors.ts index 66a73a2add..316987cc3c 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -392,3 +392,15 @@ export class AliasAlreadyExisting extends Error { super(`Alias id ${name} in plugin ${pluginName} already defined as a function or alias.`) } } + +/** + * Error thrown when a file cannot be read as a supported spreadsheet format, + * e.g. when its byte buffer is empty or cannot be parsed as an .xlsx workbook. + */ +export class UnsupportedFileError extends Error { + constructor(public readonly reason: 'empty' | 'unparseable', detail?: string) { + super(detail === undefined + ? `Unsupported or unreadable file (${reason})` + : `Unsupported or unreadable file (${reason}): ${detail}`) + } +} diff --git a/src/index.ts b/src/index.ts index 4039d4a209..4698a2b598 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,8 @@ import { SheetSizeLimitExceededError, SourceLocationHasArrayError, TargetLocationHasArrayError, - UnableToParseError + UnableToParseError, + UnsupportedFileError } from './errors' import {ExportedCellChange, ExportedChange, ExportedNamedExpressionChange} from './Exporter' import {HyperFormula} from './HyperFormula' @@ -102,6 +103,7 @@ class HyperFormulaNS extends HyperFormula { public static SourceLocationHasArrayError = SourceLocationHasArrayError public static TargetLocationHasArrayError = TargetLocationHasArrayError public static UnableToParseError = UnableToParseError + public static UnsupportedFileError = UnsupportedFileError } const defaultLanguage = Config.defaultConfig.language @@ -183,5 +185,6 @@ export { SourceLocationHasArrayError, TargetLocationHasArrayError, UnableToParseError, + UnsupportedFileError, SerializedNamedExpression, } diff --git a/src/io/xlsx/exporter.ts b/src/io/xlsx/exporter.ts new file mode 100644 index 0000000000..8089738894 --- /dev/null +++ b/src/io/xlsx/exporter.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright (c) 2025 Handsoncode. All rights reserved. + */ + +import {RawCellContent} from '../../CellContentParser' + +/** + * Converts HF's serialized sheets (as returned by + * {@link HyperFormula#getAllSheetsSerialized}) into `.xlsx` file bytes. + * + * Pure and async (ExcelJS's writer is async); does not touch a live engine. + * + * v1 mapping (deliberately minimal, see HF-107 task brief): + * - a `null`/`undefined` cell is left unset, so it reloads blank (not `0`/`''`). + * - a formula string (`cell[0] === '='`) becomes an ExcelJS formula cell. + * - `number` | `string` | `boolean` | `Date` pass through as-is. + * + * Values and formulas only — no styles, number formats, or merged cells. + */ +export async function sheetsToXlsx(sheets: Record): Promise { + const ExcelJS = await import(/* webpackMode: "eager" */ 'exceljs') + const workbook = new ExcelJS.Workbook() + + for (const sheetName of Object.keys(sheets)) { + const worksheet = workbook.addWorksheet(sheetName) + const rows = sheets[sheetName] + + for (let r = 0; r < rows.length; r++) { + const row = rows[r] + + for (let c = 0; c < row.length; c++) { + const cell = row[c] + + if (cell == null) { + continue + } + + const xc = worksheet.getCell(r + 1, c + 1) + + if (typeof cell === 'string' && cell[0] === '=') { + xc.value = {formula: cell.slice(1)} + } else { + xc.value = cell + } + } + } + } + + const buffer = await workbook.xlsx.writeBuffer() + return Uint8Array.from(buffer as Buffer) +} diff --git a/src/io/xlsx/importer.ts b/src/io/xlsx/importer.ts new file mode 100644 index 0000000000..d7ae54ff1f --- /dev/null +++ b/src/io/xlsx/importer.ts @@ -0,0 +1,108 @@ +/** + * @license + * Copyright (c) 2025 Handsoncode. All rights reserved. + */ + +import type {Cell, Workbook, Worksheet} from 'exceljs' +import {RawCellContent} from '../../CellContentParser' +import {Sheet, Sheets} from '../../Sheet' + +/** + * Converts an already-loaded ExcelJS {@link Workbook} into HyperFormula's + * {@link Sheets} shape. + * + * Pure and synchronous: byte-loading happens upstream in `loadXlsxWorkbook`. + * + * v1 mapping (deliberately minimal, see HF-107 task brief): + * - a formula cell (`cell.formula != null`) becomes the string `'=' + formula`. + * - `null`/`undefined` values become `null`. + * - a `Date` value passes through as-is (best-effort). + * - `number` | `string` | `boolean` pass through as-is. + * - any other (object-valued: error / rich text / hyperlink) cell falls back + * to `cell.text` so we never emit the raw object. + * + * Excel's `_xlfn.` / `_xlws.` function-name prefixes are stripped from + * formula strings (see {@link stripFunctionPrefixes}); shared formulas and + * cross-sheet references already come back correctly rebased/verbatim from + * ExcelJS, so no further formula rewriting is needed. + * + * A full Excel/HF error-token table is out of scope here (deferred). + */ +export function workbookToSheets(workbook: Workbook): Sheets { + const sheets: Sheets = {} + + workbook.eachSheet((worksheet: Worksheet) => { + sheets[worksheet.name] = worksheetToSheet(worksheet) + }) + + return sheets +} + +/** + * Builds a rectangular {@link Sheet} from a worksheet's used range + * (1..rowCount x 1..columnCount), mapping ExcelJS's 1-based coordinates to + * the 0-based array. + */ +function worksheetToSheet(worksheet: Worksheet): Sheet { + const rowCount = worksheet.rowCount ?? 0 + const columnCount = worksheet.columnCount ?? 0 + + if (rowCount === 0) { + return [] + } + + const sheet: Sheet = [] + + for (let r = 1; r <= rowCount; r++) { + const row = worksheet.getRow(r) + const rowContent: RawCellContent[] = [] + + for (let c = 1; c <= columnCount; c++) { + rowContent.push(cellToRawContent(row.getCell(c))) + } + + sheet.push(rowContent) + } + + return sheet +} + +/** + * Strips Excel's internal `_xlfn.` / `_xlws.` function-name prefixes. + * + * Excel stores newer/relocated functions (e.g. `XLOOKUP`, dynamic-array + * functions) with a `_xlfn.` prefix, and some with a combined + * `_xlfn._xlws.` prefix, in the raw formula string. Left in place, HF would + * fail to recognize the function name (`_xlfn.XLOOKUP` → `#NAME?`) even when + * it natively supports the function. + * + * NOTE: this operates on the whole formula string, so a matching substring + * inside a string literal (e.g. `="_xlfn."`) would also be rewritten. That + * edge case is not handled here — accepted best-effort limitation for v1. + */ +function stripFunctionPrefixes(formula: string): string { + return formula.replace(/_xlfn\.|_xlws\./g, '') +} + +/** Maps a single ExcelJS cell to a HyperFormula {@link RawCellContent} per the v1 mapping rule. */ +function cellToRawContent(cell: Cell): RawCellContent { + if (cell.formula != null) { + return '=' + stripFunctionPrefixes(cell.formula) + } + + const value = cell.value + + if (value == null) { + return null + } + + if (value instanceof Date) { + return value + } + + if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') { + return value + } + + return cell.text ?? null +} diff --git a/src/io/xlsx/loadWorkbook.ts b/src/io/xlsx/loadWorkbook.ts new file mode 100644 index 0000000000..c36dce2669 --- /dev/null +++ b/src/io/xlsx/loadWorkbook.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright (c) 2025 Handsoncode. All rights reserved. + */ + +import type {Workbook} from 'exceljs' +import {UnsupportedFileError} from '../../errors' + +/** + * Loads raw xlsx bytes into an ExcelJS {@link Workbook}. + * + * v1 scope: attempt the ExcelJS load and wrap any failure. Byte-signature / + * ZIP-magic / CFB format detection is deliberately out of scope (YAGNI). + */ +export async function loadXlsxWorkbook(data: ArrayBuffer | Uint8Array): Promise { + if (data.byteLength === 0) { + throw new UnsupportedFileError('empty') + } + + // Copy to a fresh, zero-offset Uint8Array (works in Node and the browser + // without pulling in the Node `Buffer` polyfill). `.slice()` on a subarray + // view copies exactly the view's logical bytes, so byteOffset is honored. + const bytes = data instanceof Uint8Array ? data.slice() : new Uint8Array(data) + + try { + const ExcelJS = await import(/* webpackMode: "eager" */ 'exceljs') + const workbook = new ExcelJS.Workbook() + await workbook.xlsx.load(bytes.buffer) + return workbook + } catch (e) { + const detail = e instanceof Error ? e.message : String(e) + throw new UnsupportedFileError('unparseable', detail) + } +} diff --git a/tsconfig.json b/tsconfig.json index 812d955676..d8ef6d31bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,8 @@ /* Basic Options */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ - "module": "es2015", - /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "module": "es2020", + /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. es2020 (was es2015) is required for the lazy `import('exceljs')` dynamic import; emit is unchanged for every module that does not use dynamic import / import.meta. */ "lib": [ "es6", "dom",